@neatui/vue 5.2.0 → 5.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.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,18 @@
1
1
  # @neatui/vue 更新记录
2
2
 
3
+ ## 5.2.1
4
+
5
+ ### IFollowView(移动端底部面板)
6
+ - 面板改为从底部滑入滑出,暗层同步淡入淡出,使用 `cubic-bezier(0.32, 0.72, 0, 1)` 减速曲线;支持“减少动态效果”。
7
+ - 面板通过 Popover API 进入浏览器 top layer,不再依赖 z-index;不支持时回退为普通 fixed。
8
+ - 插槽内容撑满面板宽度,覆盖桌面端写死的宽度。
9
+ - 面板贴底,Home 指示条安全区以面板底色留白(需页面 viewport 设置 `viewport-fit=cover`)。
10
+ - 顶部圆角改为 `0.75em`;上方间距统一由标题栏提供;关闭按钮改用 CSS 图标 `ui-icon="close"`。
11
+ - 移动端样式只作用于本层面板盒子,不再影响插槽内其他 `ui-tips` 结构。
12
+
13
+ ### 依赖
14
+ - CSS 依赖更新为 `@neatui/css@^5.2.1`。
15
+
3
16
  ## 5.2.0
4
17
 
5
18
  ### Layer
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@neatui/vue",
3
- "version": "5.2.0",
3
+ "version": "5.3.0",
4
4
  "description": "basic component ui",
5
5
  "main": "./src/index.ts",
6
6
  "license": "MIT",
@@ -36,7 +36,7 @@
36
36
  "@fekit/scrollto": "^3.0.3",
37
37
  "@fekit/toast": "^2.7.2",
38
38
  "@fekit/utils": "^6.0.0",
39
- "@neatui/css": "^5.2.0",
39
+ "@neatui/css": "^5.3.0",
40
40
  "@rollup/plugin-commonjs": "^26.0.1",
41
41
  "@rollup/plugin-node-resolve": "^15.2.3",
42
42
  "@vue/compiler-sfc": "^3.4.30",
@@ -17,14 +17,14 @@
17
17
  @click="clickhide"
18
18
  >
19
19
  <div ref="box" :ui-tips-box="state.pos" :class="tipBoxClass" :style="tipBoxStyle">
20
- <div ui-hide=">mob" class="mob-follow-tips w-full n-ss" ui-flex="row xm">
20
+ <div ui-hide=">mob" class="mob-follow-tips w-full nx-sl nt-sl nb-ss" ui-flex="row xm">
21
21
  <div class="w-xs*s"></div>
22
22
  <div>
23
23
  <h5 class="bold">{{ title }}</h5>
24
24
  </div>
25
25
  <div class="w-xs*s" ui-flex="row rm">
26
26
  <button ui-btn="@a none s :square" @click="toggle">
27
- <i class="icon icon-close"></i>
27
+ <i ui-icon="close"></i>
28
28
  </button>
29
29
  </div>
30
30
  </div>
@@ -67,6 +67,8 @@
67
67
  mobile?: boolean;
68
68
  title?: string;
69
69
  zIndex?: string | number | null;
70
+ /** 桌面端是否放入浏览器 top layer(Popover API),盖住整页任意层级;移动端底部面板始终放入。 */
71
+ topLayer?: boolean;
70
72
  }
71
73
 
72
74
  // 入参
@@ -87,6 +89,7 @@
87
89
  mobile: true,
88
90
  title: '',
89
91
  zIndex: undefined,
92
+ topLayer: true,
90
93
  });
91
94
 
92
95
  const dom: any = ref(null);
@@ -168,6 +171,73 @@
168
171
  },
169
172
  );
170
173
 
174
+ // 弹层放入浏览器 top layer(Popover API),无需比较 z-index 即可盖住整页:
175
+ // 移动端底部面板始终放入;桌面端由 topLayer 控制(默认开启),关闭时按 Follow 的祖先层级定位。
176
+ // 不支持的浏览器退回普通 fixed。
177
+ const SHEET_LEAVE_MS = 500;
178
+ let sheetTimer: ReturnType<typeof setTimeout> | undefined;
179
+ const supportsPopover = () => typeof HTMLElement !== 'undefined' && 'showPopover' in HTMLElement.prototype;
180
+ const isSheet = () => props.mobile && window.matchMedia('(max-width: 799px)').matches;
181
+ const useTopLayer = () => props.topLayer || isSheet();
182
+ const releaseSheet = () => {
183
+ clearTimeout(sheetTimer);
184
+ const el = tip.value;
185
+ if (!el?.hasAttribute('popover')) return;
186
+ try {
187
+ if (el.matches(':popover-open')) el.hidePopover();
188
+ } catch {}
189
+ el.removeAttribute('popover');
190
+ };
191
+ const openSheet = () => {
192
+ const el = tip.value;
193
+ if (!el || !supportsPopover()) return;
194
+ clearTimeout(sheetTimer);
195
+ // 不需要 top layer 时主动清理(如关闭动画期间禁用 topLayer 后快速重开)。
196
+ if (!useTopLayer()) return releaseSheet();
197
+ el.setAttribute('popover', 'manual');
198
+ try {
199
+ // 已打开时先关再开,使其移到 top layer 最上方。
200
+ if (el.matches(':popover-open')) el.hidePopover();
201
+ el.showPopover();
202
+ } catch {
203
+ el.removeAttribute('popover');
204
+ }
205
+ };
206
+ // 打开状态下跨越断点(如桌面端拖窄窗口)时,同步进入/离开 top layer。
207
+ let sheetMql: MediaQueryList | null = null;
208
+ const onSheetMediaChange = () => {
209
+ if (!state.show) return;
210
+ if (!useTopLayer()) return releaseSheet();
211
+ // 已在 top layer 时保持原顺序,避免父面板被重排到子面板之上。
212
+ if (!tip.value?.matches(':popover-open')) openSheet();
213
+ };
214
+ onMounted(() => {
215
+ sheetMql = window.matchMedia('(max-width: 799px)');
216
+ sheetMql.addEventListener('change', onSheetMediaChange);
217
+ });
218
+ onUnmounted(() => {
219
+ sheetMql?.removeEventListener('change', onSheetMediaChange);
220
+ });
221
+ // 展开期间动态切换 topLayer 时同步。
222
+ watch(() => props.topLayer, onSheetMediaChange);
223
+ watch(
224
+ () => state.show,
225
+ (show) => {
226
+ const el = tip.value;
227
+ if (!el || !supportsPopover()) return;
228
+ clearTimeout(sheetTimer);
229
+ if (show) {
230
+ openSheet();
231
+ } else if (el.hasAttribute('popover')) {
232
+ // 等滑出动画结束再离开 top layer。
233
+ sheetTimer = setTimeout(() => {
234
+ if (!state.show) releaseSheet();
235
+ }, SHEET_LEAVE_MS);
236
+ }
237
+ },
238
+ { flush: 'post' },
239
+ );
240
+
171
241
  const hovershow = () => {
172
242
  if (props.event === 'hover') {
173
243
  state.hover = 1;
@@ -337,8 +407,11 @@
337
407
  },
338
408
  { hideOnScrollOut: false, zIndex: props.zIndex },
339
409
  );
340
- // 初始即显示(如 modelValue=1)时 state.show 不会再变化,需要立即启动定位。
341
- if (state.show) ex.value.listen();
410
+ // 初始即显示(如 modelValue=1)时 state.show 不会再变化,需要立即启动定位并同步 top layer。
411
+ if (state.show) {
412
+ ex.value.listen();
413
+ openSheet();
414
+ }
342
415
  } else {
343
416
  ex.value = null;
344
417
  }
@@ -361,6 +434,7 @@
361
434
  document.removeEventListener('touchend', onTouchEnd);
362
435
  document.removeEventListener('touchcancel', onTouchCancel);
363
436
  parentChildren?.delete(containsFollow);
437
+ releaseSheet();
364
438
  outsideTouch = null;
365
439
  state.show = 0;
366
440
 
@@ -391,6 +465,20 @@
391
465
  </script>
392
466
  <style lang="scss">
393
467
  @use '@neatui/css/query' as neatui;
468
+ // 抵消 [popover] 的 UA 默认样式(桌面与移动端共用);放在媒体查询外,跨断点时不会走样。
469
+ [ui-tips-view][popover] {
470
+ inset: auto;
471
+ width: auto;
472
+ height: auto;
473
+ margin: 0;
474
+ padding: 0;
475
+ border: 0;
476
+ overflow: visible;
477
+ color: inherit;
478
+ background: transparent;
479
+ max-width: none;
480
+ max-height: none;
481
+ }
394
482
  @include neatui.mob {
395
483
  .fekit-follow-mobile {
396
484
  position: fixed !important;
@@ -404,26 +492,52 @@
404
492
  align-content: center !important;
405
493
  inset: 0 0 0 0 !important;
406
494
 
495
+ // 底部面板:暗层淡入淡出,面板从底部滑入滑出(iOS 风格减速曲线)。
496
+ --follow-sheet-ease: cubic-bezier(0.32, 0.72, 0, 1);
497
+ background-color: rgba(0, 0, 0, 0);
498
+ transition: background-color 0.4s var(--follow-sheet-ease);
499
+
407
500
  &[ui-tips-view='1'] {
408
501
  background-color: rgba(0, 0, 0, 0.5);
409
502
  pointer-events: auto !important;
410
503
  }
411
504
 
412
- &[ui-tips] [ui-tips-box] {
505
+ &[ui-tips][ui-tips-view='1'] > [ui-tips-box] {
506
+ transform: translate3d(0, 0, 0) !important;
507
+ }
508
+
509
+ &[ui-tips] > [ui-tips-box] {
413
510
  position: relative !important;
414
511
 
415
512
  width: 100% !important;
416
- bottom: env(safe-area-inset-bottom) !important;
513
+ bottom: 0 !important;
514
+ // iPhone Home 指示条安全区:面板背景铺到屏幕底边,内容让出安全区。
515
+ // 用透明下边框而非 padding,避免覆盖各组件 tipBoxClass 的内边距(背景默认绘制到边框下)。
516
+ border-bottom: env(safe-area-inset-bottom, 0px) solid transparent !important;
417
517
  right: unset !important;
418
518
  top: unset !important;
419
519
  left: unset !important;
420
- transform: translate3d(0, 0.5em, 0);
520
+ border-radius: 0.75em 0.75em 0 0 !important;
521
+ // 上方间距统一由标题栏提供,忽略各组件 tipBoxClass 的上内边距(如 ny-sm)。
522
+ padding-top: 0 !important;
523
+ // 全程不透明,只做位移。
524
+ opacity: 1 !important;
525
+ transform: translate3d(0, 100%, 0) !important;
526
+ transition: transform 0.45s var(--follow-sheet-ease) !important;
527
+ will-change: transform;
421
528
 
422
529
  display: flex !important;
423
530
  flex-direction: column !important;
424
531
  justify-content: flex-end !important;
425
- align-items: center !important;
426
- align-content: center !important;
532
+ align-items: stretch !important;
533
+ align-content: stretch !important;
534
+
535
+ // 插槽内容撑满面板宽度,覆盖桌面端写死的宽度(如 w-ml*s)。
536
+ > * {
537
+ width: 100% !important;
538
+ max-width: 100%;
539
+ box-sizing: border-box;
540
+ }
427
541
 
428
542
  [ui-tips-arrow] {
429
543
  display: none;
@@ -431,9 +545,15 @@
431
545
  }
432
546
  }
433
547
 
548
+ @media (prefers-reduced-motion: reduce) {
549
+ .fekit-follow-mobile,
550
+ .fekit-follow-mobile[ui-tips] > [ui-tips-box] {
551
+ transition-duration: 0.01s !important;
552
+ }
553
+ }
554
+
434
555
  .mob-follow-tips {
435
556
  position: relative;
436
- padding-bottom: env(safe-area-inset-bottom, 16px);
437
557
  &::before {
438
558
  position: absolute;
439
559
  content: '';
@@ -442,7 +562,7 @@
442
562
  height: 3px;
443
563
  border-radius: 1em;
444
564
  background-color: var(--co-text-o-ss);
445
- top: 5px;
565
+ top: 6px;
446
566
  left: 50%;
447
567
  transform: translateX(-50%);
448
568
  }
@@ -4,7 +4,7 @@
4
4
  ref="ifollow"
5
5
  :tipBoxStyle="`${state.flatOpts.length && state.flatOpts[0]?.length > 0 ? 'min-width: 10em;' : 'min-width: 100%'}`"
6
6
  pos="bl"
7
- tipBoxClass="ny-sm nx-no"
7
+ tipBoxClass="ny-sm nx-ss"
8
8
  >
9
9
  <Input v-model="state.selectd.label" :clearable="false" :disabled="disabled" readonly v-bind="bindProps">
10
10
  <template #suffix><i ui-form-select=""></i></template>
@@ -1,6 +1,6 @@
1
1
  <template>
2
2
  <div :ui-form="`${theme} type:select ${sz ? 'sz:' + sz : ''}`">
3
- <IFollowView :ui-tips="uiTips" ref="ifollow" tipBoxClass="ny-sm nx-no" tipBoxStyle="min-width: 100%">
3
+ <IFollowView :ui-tips="uiTips" ref="ifollow" tipBoxClass="ny-sm nx-ss" tipBoxStyle="min-width: 100%">
4
4
  <slot>
5
5
  <Input
6
6
  v-bind="{ placeholder, rules, theme, sz, co, block, ready: true, type: 'text', tips, disabled, readonly, clearable: false, verify, prefix, suffix, interacted }"
@@ -1,6 +1,6 @@
1
1
  <template>
2
2
  <div :ui-form="`${theme} type:select sz:${sz}`" :ui-form-open="state.show ? 'show' : 'hide'">
3
- <IFollowView :ui-tips="uiTips" ref="ifollow" v-model="state.show" tipBoxClass="ny-sm nx-no" tipBoxStyle="min-width: 100%" @onshow="onshow" @onhide="onhide">
3
+ <IFollowView :ui-tips="uiTips" ref="ifollow" v-model="state.show" tipBoxClass="ny-sm nx-ss" tipBoxStyle="min-width: 100%" @onshow="onshow" @onhide="onhide">
4
4
  <Input
5
5
  v-bind="{ placeholder, rules, theme, sz, co, block, ready, type: 'text', tips, disabled, readonly: true, clearable: false, verify, prefix, suffix, interacted }"
6
6
  :modelValue="state.label"
@@ -1,6 +1,6 @@
1
1
  <template>
2
2
  <div :ui-form="`${theme} type:select ${sz ? 'sz:' + sz : ''}`" :ui-form-open="state.show ? 'show' : ''" ref="dom">
3
- <IFollowView :ui-tips="uiTips" ref="ifollow" tipBoxClass="ny-sm nx-no" tipBoxStyle="min-width: 100%">
3
+ <IFollowView :ui-tips="uiTips" ref="ifollow" tipBoxClass="ny-sm nx-ss" tipBoxStyle="min-width: 100%">
4
4
  <slot>
5
5
  <Input v-bind="$props" :clearable="false" readonly v-model="state.selectd">
6
6
  <template #suffix><i ui-form-select=""></i></template>
@@ -1,6 +1,6 @@
1
1
  <template>
2
2
  <div :ui-form="`${theme} type:select sz:${sz}`" :ui-form-open="state.show ? 'show' : ''" ref="dom">
3
- <IFollowView :ui-tips="uiTips" ref="ifollow" tipBoxClass="ny-sm nx-no" tipBoxStyle="min-width: 100%">
3
+ <IFollowView :ui-tips="uiTips" ref="ifollow" tipBoxClass="ny-sm nx-ss" tipBoxStyle="min-width: 100%">
4
4
  <Input v-bind="$props" :clearable="false" readonly v-model="state.selectd.label" :interacted="interacted">
5
5
  <template #suffix><i ui-form-select=""></i></template>
6
6
  </Input>
@@ -1,6 +1,6 @@
1
1
  <template>
2
2
  <div :ui-form="`${theme} type:select sz:${sz}`" :ui-form-open="state.show ? 'show' : ''">
3
- <IFollowView :ui-tips="uiTips" ref="ifollow" tipBoxClass="ny-sm nx-no" tipBoxStyle="min-width: 100%" @onshow="onshow" @onhide="onhide">
3
+ <IFollowView :ui-tips="uiTips" ref="ifollow" tipBoxClass="ny-sm nx-ss" tipBoxStyle="min-width: 100%" @onshow="onshow" @onhide="onhide">
4
4
  <Input v-bind="$props" :clearable="false" readonly v-model="state.selectd" @click="toggle" :interacted="interacted">
5
5
  <template #suffix><i ui-form-select=""></i></template>
6
6
  </Input>
@@ -1,6 +1,6 @@
1
1
  <template>
2
2
  <div :ui-form="`${theme} type:select sz:${sz}`" :ui-form-open="state.show ? 'show' : ''">
3
- <IFollowView :ui-tips="uiTips" ref="ifollow" tipBoxClass="ny-sm nx-no" tipBoxStyle="min-width: 100%" @onshow="onshow" @onhide="onhide">
3
+ <IFollowView :ui-tips="uiTips" ref="ifollow" tipBoxClass="ny-sm nx-ss" tipBoxStyle="min-width: 100%" @onshow="onshow" @onhide="onhide">
4
4
  <Input
5
5
  v-bind="{ placeholder, rules, theme, sz, co, block, ready, type, tips, disabled, readonly: true, clearable: false, verify, prefix, suffix, interacted }"
6
6
  v-model="state.selectd.label"
@@ -1,6 +1,6 @@
1
1
  <template>
2
2
  <div :ui-form="`${theme} type:select sz:${sz}`" :ui-form-open="state.show ? 'show' : ''">
3
- <IFollowView :ui-tips="uiTips" ref="ifollow" tipBoxClass="ny-sm nx-no" tipBoxStyle="min-width: 100%" @onshow="onshow" @onhide="onhide">
3
+ <IFollowView :ui-tips="uiTips" ref="ifollow" tipBoxClass="ny-sm nx-ss" tipBoxStyle="min-width: 100%" @onshow="onshow" @onhide="onhide">
4
4
  <Input
5
5
  v-bind="{ placeholder, rules, theme, sz, co, block, ready, type, tips, disabled, readonly: true, clearable: false, verify, prefix, suffix, interacted }"
6
6
  v-model="state.displayLabel"
@@ -1,6 +1,6 @@
1
1
  <template>
2
2
  <div :ui-form="`${theme} type:select sz:${sz}`" :ui-form-open="state.show ? 'show' : 'hide'">
3
- <IFollowView :ui-tips="uiTips" ref="ifollow" v-model="state.show" tipBoxClass="ny-sm nx-no" tipBoxStyle="min-width: 100%" @onshow="onshow" @onhide="onhide">
3
+ <IFollowView :ui-tips="uiTips" ref="ifollow" v-model="state.show" tipBoxClass="ny-sm nx-ss" tipBoxStyle="min-width: 100%" @onshow="onshow" @onhide="onhide">
4
4
  <Input
5
5
  v-bind="{ placeholder, rules, theme, sz, co, block, ready, type, tips, disabled, readonly: true, clearable: false, verify, prefix, suffix, interacted }"
6
6
  v-model="state.label"