reborn-ui 0.1.76 → 0.1.78

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 (50) hide show
  1. package/dist/index.js +182 -240
  2. package/dist/index.js.map +1 -1
  3. package/package.json +53 -53
  4. package/registry/components/reborn-affix.json +8 -3
  5. package/registry/components/reborn-back-top.json +9 -4
  6. package/registry/components/reborn-badge.json +11 -5
  7. package/registry/components/reborn-button.json +5 -5
  8. package/registry/components/reborn-card.json +18 -0
  9. package/registry/components/reborn-cascader.json +18 -0
  10. package/registry/components/reborn-checkbox.json +4 -4
  11. package/registry/components/reborn-chip.json +11 -5
  12. package/registry/components/reborn-collapse.json +11 -5
  13. package/registry/components/reborn-color-picker.json +50 -0
  14. package/registry/components/reborn-draggable.json +32 -0
  15. package/registry/components/reborn-drawer.json +17 -0
  16. package/registry/components/reborn-dropdown-select.json +18 -0
  17. package/registry/components/reborn-footer.json +40 -0
  18. package/registry/components/reborn-form.json +11 -6
  19. package/registry/components/reborn-image.json +10 -5
  20. package/registry/components/reborn-input-number.json +12 -6
  21. package/registry/components/reborn-input-otp.json +40 -0
  22. package/registry/components/reborn-input.json +4 -4
  23. package/registry/components/reborn-loading.json +23 -0
  24. package/registry/components/reborn-loadmore.json +23 -0
  25. package/registry/components/reborn-overlay.json +38 -0
  26. package/registry/components/reborn-page.json +18 -0
  27. package/registry/components/reborn-picker-view.json +26 -0
  28. package/registry/components/reborn-popover.json +58 -0
  29. package/registry/components/reborn-popup.json +23 -0
  30. package/registry/components/reborn-qrcode.json +45 -0
  31. package/registry/components/reborn-radio.json +45 -0
  32. package/registry/components/reborn-rate.json +40 -0
  33. package/registry/components/reborn-root-portal.json +26 -0
  34. package/registry/components/reborn-select-date.json +40 -0
  35. package/registry/components/reborn-select-trigger.json +25 -0
  36. package/registry/components/reborn-select.json +41 -0
  37. package/registry/components/reborn-slider.json +40 -0
  38. package/registry/components/reborn-sticky.json +12 -6
  39. package/registry/components/reborn-switch.json +13 -7
  40. package/registry/components/reborn-tabbar.json +38 -0
  41. package/registry/components/reborn-tabs copy.json +46 -0
  42. package/registry/components/reborn-tabs-test.json +46 -0
  43. package/registry/components/reborn-tabs.json +12 -6
  44. package/registry/components/reborn-text.json +34 -0
  45. package/registry/components/reborn-textarea.json +5 -5
  46. package/registry/components/reborn-toast.json +38 -0
  47. package/registry/components/reborn-transition.json +38 -0
  48. package/registry/components/reborn-waterfall.json +18 -0
  49. package/registry/components/scroll-island.json +2 -2
  50. package/registry/registry.json +1101 -97
@@ -0,0 +1,23 @@
1
+ {
2
+ "name": "reborn-loading",
3
+ "dependencies": [],
4
+ "files": [
5
+ {
6
+ "path": "index.ts",
7
+ "content": "// @ts-ignore\r\nimport RebornLoading from './reborn-loading.vue'\r\n\r\nexport default RebornLoading\r\nexport { RebornLoading }\r\nexport type { LoadingType } from './reborn-loading.vue'\r\n",
8
+ "target": "uniapp"
9
+ },
10
+ {
11
+ "path": "reborn-loading.config.ts",
12
+ "content": "export const LoadingTypes = ['outline', 'ring', 'spinner', 'bars-scale', 'blocks-shuffle', 'blocks-wave', 'gooey-balls'] as const\r\nexport const LoadingColors = ['primary', 'secondary', 'success', 'info', 'warning', 'error', 'neutral'] as const\r\n\r\nconst config = {\r\n slots: {\r\n root: 'inline-block align-middle rb-loading',\r\n container: 'w-full h-full relative',\r\n indicator: 'w-full h-full rb-loading-indicator',\r\n outlineTrack: 'absolute inset-0 rounded-full border-[3px] opacity-20',\r\n spinnerItem: 'absolute top-0 left-[46%] w-[8%] h-[25%] bg-current rounded-sm origin-[50%_200%] rb-loading-spinnerItem',\r\n barItem: 'w-[15%] h-[60%] bg-current rounded-sm rb-loading-barItem',\r\n blockItem: 'absolute w-[40%] h-[40%] bg-current rounded-sm',\r\n waveItem: 'bg-current rounded-[1px] rb-loading-waveItem',\r\n gooeyItem: 'absolute w-[40%] h-[40%] bg-current rounded-full',\r\n },\r\n variants: {\r\n type: {\r\n ring: {\r\n container: 'flex items-center justify-center',\r\n indicator: 'rounded-full border-[3px] border-solid'\r\n },\r\n outline: {\r\n container: 'flex items-center justify-center',\r\n indicator: 'absolute inset-0 rounded-full border-[3px]'\r\n },\r\n spinner: {\r\n container: 'flex items-center justify-center'\r\n },\r\n 'bars-scale': {\r\n container: 'flex justify-between items-center'\r\n },\r\n 'blocks-shuffle': {\r\n container: 'block'\r\n },\r\n 'blocks-wave': {\r\n container: 'grid grid-cols-3 grid-rows-3 gap-[10%]'\r\n },\r\n 'gooey-balls': {\r\n container: 'flex items-center justify-center'\r\n }\r\n },\r\n color: {\r\n primary: { container: 'text-primary' },\r\n secondary: { container: 'text-secondary' },\r\n success: { container: 'text-success' },\r\n info: { container: 'text-info' },\r\n warning: { container: 'text-warning' },\r\n error: { container: 'text-error' },\r\n neutral: { container: 'text-neutral' }\r\n }\r\n },\r\n defaultVariants: {\r\n color: 'primary',\r\n type: 'ring'\r\n }\r\n} as const\r\n\r\nexport type LoadingUI = {\r\n root?: string\r\n container?: string\r\n indicator?: string\r\n outlineTrack?: string\r\n spinnerItem?: string\r\n barItem?: string\r\n blockItem?: string\r\n waveItem?: string\r\n gooeyItem?: string\r\n}\r\n\r\nexport default config",
13
+ "target": "uniapp"
14
+ },
15
+ {
16
+ "path": "RebornLoading.vue",
17
+ "content": "<template>\r\n <view :class=\"ui.root()\" :style=\"rootStyle\">\r\n <view v-if=\"props.type === 'ring' || props.type === 'outline'\" :key=\"props.type\" :class=\"ui.container()\"\r\n :style=\"containerStyle\">\r\n <view v-if=\"props.type === 'outline'\" :class=\"ui.outlineTrack()\" />\r\n <view :class=\"ui.indicator()\" :style=\"ringIndicatorStyle\" />\r\n </view>\r\n\r\n <view v-else-if=\"props.type === 'spinner'\" key=\"loading-spinner\" :class=\"ui.container()\"\r\n :style=\"containerStyle\">\r\n <view v-for=\"i in 12\" :key=\"i\" :class=\"ui.spinnerItem()\" :style=\"{ '--i': i }\" />\r\n </view>\r\n\r\n <view v-else-if=\"props.type === 'bars-scale'\" key=\"loading-bars-scale\" :class=\"ui.container()\"\r\n :style=\"containerStyle\">\r\n <view v-for=\"i in 5\" :key=\"i\" :class=\"ui.barItem()\" :style=\"{ '--i': i }\" />\r\n </view>\r\n\r\n <view v-else-if=\"props.type === 'blocks-shuffle'\" key=\"loading-blocks-shuffle\" :class=\"ui.container()\"\r\n :style=\"containerStyle\">\r\n <view :class=\"ui.blockItem()\" class=\"rb-shuffle-1\" />\r\n <view :class=\"ui.blockItem()\" class=\"rb-shuffle-2\" />\r\n </view>\r\n\r\n <view v-else-if=\"props.type === 'blocks-wave'\" key=\"loading-blocks-wave\" :class=\"ui.container()\"\r\n :style=\"containerStyle\">\r\n <view v-for=\"i in 9\" :key=\"i\" :class=\"ui.waveItem()\" :style=\"{ '--d': getWaveDelay(i) }\" />\r\n </view>\r\n\r\n <view v-else-if=\"props.type === 'gooey-balls'\" key=\"loading-gooey-balls\" :class=\"ui.container()\"\r\n :style=\"containerStyle\">\r\n <view :class=\"ui.gooeyItem()\" class=\"rb-gooey-1\" />\r\n <view :class=\"ui.gooeyItem()\" class=\"rb-gooey-2\" />\r\n </view>\r\n </view>\r\n</template>\r\n\r\n<script lang=\"ts\">\r\nexport default {\r\n name: 'reborn-loading',\r\n options: { virtualHost: true, addGlobalClass: true, styleIsolation: 'shared' }\r\n}\r\n</script>\r\n\r\n<script lang=\"ts\" setup>\r\nimport { computed, watch, ref } from 'vue'\r\nimport { addUnit, isDef, objToStyle } from '@/lib/util'\r\nimport { tv } from '@/lib/tv'\r\nimport { cn } from '@/lib/utils'\r\nimport theme, { type LoadingUI, LoadingColors, LoadingTypes } from './reborn-loading.config'\r\n\r\nexport type RebornLoadingProps = {\r\n ui?: LoadingUI\r\n type?: typeof LoadingTypes[number]\r\n color?: typeof LoadingColors[number] | string\r\n size?: string | number\r\n customClass?: string\r\n}\r\n\r\nconst props = withDefaults(defineProps<RebornLoadingProps>(), {\r\n ui: () => ({}),\r\n type: 'ring',\r\n color: 'primary',\r\n size: '30px'\r\n})\r\n\r\nconst iconSize = ref<string>('30px')\r\nwatch(() => props.size, (val) => { iconSize.value = addUnit(val) }, { immediate: true })\r\n\r\nconst isPresetColor = computed(() => LoadingColors.includes(props.color as typeof LoadingColors[number]))\r\n\r\n// bars-scale / blocks-wave 用百分比布局,尺寸过小时子元素会接近 0 不显示,故设最小宽高\r\nconst MIN_SIZE_FOR_GRID = '32px'\r\nconst rootStyle = computed(() => {\r\n const style: Record<string, string> = {\r\n width: iconSize.value,\r\n height: iconSize.value,\r\n }\r\n if (props.type === 'bars-scale' || props.type === 'blocks-wave') {\r\n style.minWidth = MIN_SIZE_FOR_GRID\r\n style.minHeight = MIN_SIZE_FOR_GRID\r\n }\r\n return objToStyle(style)\r\n})\r\nconst containerStyle = computed(() => {\r\n const style: Record<string, string> = {}\r\n if (props.color && !isPresetColor.value) {\r\n style.color = props.color\r\n }\r\n\r\n return objToStyle(style)\r\n})\r\n\r\n// ring 在自定义颜色时直接给 indicator 设边框色,避免 currentColor 在小程序等环境不继承导致显示成 U 形\r\nconst ringIndicatorStyle = computed(() => {\r\n if (props.type !== 'ring' || isPresetColor.value) return {}\r\n const c = props.color as string\r\n if (!c) return {}\r\n return {\r\n borderColor: c,\r\n borderTopColor: 'transparent',\r\n }\r\n})\r\n\r\nconst b = tv(theme)\r\nconst ui = computed(() => {\r\n const styles = b({\r\n color: isPresetColor.value ? props.color as typeof LoadingColors[number] : undefined,\r\n type: props.type\r\n })\r\n // 映射所有 slots\r\n const slots = ['root', 'container', 'indicator', 'outlineTrack', 'spinnerItem', 'barItem', 'blockItem', 'waveItem', 'gooeyItem'] as const\r\n const res: any = {}\r\n slots.forEach(slot => {\r\n res[slot] = (opts?: { class?: any }) => styles[slot]({ class: cn(opts?.class, slot === 'root' ? props.customClass : undefined, (props.ui as any)?.[slot]) })\r\n })\r\n return res as Record<keyof LoadingUI, (opts?: { class?: any }) => string>\r\n})\r\n\r\n// 仅保留复杂的延迟算法在 JS 中,其余交给 CSS\r\nfunction getWaveDelay(index: number) {\r\n return ((index - 1) % 3 + Math.floor((index - 1) / 3)) * 0.12 + 's'\r\n}\r\n</script>\r\n\r\n<style>\r\n/* 1. 性能基础设置 */\r\n.rb-loading view {\r\n box-sizing: border-box;\r\n backface-visibility: hidden;\r\n transform: translateZ(0);\r\n /* 开启硬件加速 */\r\n}\r\n\r\n/* 2. 动画定义 */\r\n@keyframes rb-rotate {\r\n from {\r\n transform: rotate(0deg);\r\n }\r\n\r\n to {\r\n transform: rotate(360deg);\r\n }\r\n}\r\n\r\n@keyframes rb-spinner {\r\n 0% {\r\n opacity: 1;\r\n }\r\n\r\n 100% {\r\n opacity: 0.15;\r\n }\r\n}\r\n\r\n@keyframes rb-bars-scale {\r\n\r\n 0%,\r\n 100% {\r\n transform: scaleY(0.5);\r\n opacity: 0.5;\r\n }\r\n\r\n 50% {\r\n transform: scaleY(1.2);\r\n opacity: 1;\r\n }\r\n}\r\n\r\n@keyframes rb-blocks-shuffle {\r\n 0% {\r\n transform: translate(0, 0);\r\n }\r\n\r\n 25% {\r\n transform: translate(120%, 0);\r\n }\r\n\r\n 50% {\r\n transform: translate(120%, 120%);\r\n }\r\n\r\n 75% {\r\n transform: translate(0, 120%);\r\n }\r\n\r\n 100% {\r\n transform: translate(0, 0);\r\n }\r\n}\r\n\r\n@keyframes rb-blocks-wave {\r\n\r\n 0%,\r\n 100% {\r\n transform: scale(1);\r\n opacity: 1;\r\n }\r\n\r\n 50% {\r\n transform: scale(0.3);\r\n opacity: 0.3;\r\n }\r\n}\r\n\r\n@keyframes rb-gooey-1 {\r\n\r\n 0%,\r\n 100% {\r\n transform: translateX(-50%) scale(1);\r\n }\r\n\r\n 50% {\r\n transform: translateX(20%) scale(1.5);\r\n }\r\n}\r\n\r\n@keyframes rb-gooey-2 {\r\n\r\n 0%,\r\n 100% {\r\n transform: translateX(50%) scale(1.5);\r\n }\r\n\r\n 50% {\r\n transform: translateX(-20%) scale(1);\r\n }\r\n}\r\n\r\n/* 3. 动画逻辑应用 (核心优化) */\r\n/* Ring & Outline */\r\n.rb-loading .rb-loading-indicator {\r\n border-color: currentColor;\r\n border-top-color: transparent !important;\r\n animation: rb-rotate 0.8s linear infinite;\r\n will-change: transform;\r\n}\r\n\r\n/* Spinner */\r\n.rb-loading .rb-loading-spinnerItem {\r\n transform: rotate(calc((var(--i) - 1) * 30deg));\r\n animation: rb-spinner 1s linear infinite;\r\n animation-delay: calc((var(--i) - 1) * 0.08s);\r\n will-change: opacity;\r\n}\r\n\r\n/* Bars Scale */\r\n.rb-loading .rb-loading-barItem {\r\n animation: rb-bars-scale 1s ease-in-out infinite;\r\n animation-delay: calc((var(--i) - 1) * 0.12s);\r\n will-change: transform, opacity;\r\n}\r\n\r\n/* Blocks Shuffle */\r\n.rb-shuffle-1 {\r\n animation: rb-blocks-shuffle 1s linear infinite;\r\n}\r\n\r\n.rb-shuffle-2 {\r\n animation: rb-blocks-shuffle 1s linear infinite -0.5s;\r\n}\r\n\r\n/* Blocks Wave */\r\n.rb-loading .rb-loading-waveItem {\r\n animation: rb-blocks-wave 1s ease-in-out infinite;\r\n animation-delay: var(--d);\r\n will-change: transform, opacity;\r\n}\r\n\r\n/* Gooey */\r\n.rb-gooey-1 {\r\n animation: rb-gooey-1 0.75s ease-in-out infinite;\r\n}\r\n\r\n.rb-gooey-2 {\r\n animation: rb-gooey-2 0.75s ease-in-out infinite;\r\n}\r\n</style>",
18
+ "target": "uniapp"
19
+ }
20
+ ],
21
+ "fileCount": 3,
22
+ "contentHash": "ebc960ea7244d697fa0da5d92a8a43bbb3970b42"
23
+ }
@@ -0,0 +1,23 @@
1
+ {
2
+ "name": "reborn-loadmore",
3
+ "dependencies": [],
4
+ "files": [
5
+ {
6
+ "path": "index.ts",
7
+ "content": "// @ts-ignore\r\nimport RebornLoadmore from './reborn-loadmore.vue'\r\n\r\nexport default RebornLoadmore\r\nexport { RebornLoadmore }\r\nexport type { LoadMoreState } from './reborn-loadmore.vue'\r\n",
8
+ "target": "uniapp"
9
+ },
10
+ {
11
+ "path": "reborn-loadmore.config.ts",
12
+ "content": "export const loadMoreColors = ['primary', 'secondary', 'success', 'info', 'warning', 'error', 'neutral'] as const\r\nexport const LoadMoreState = ['loading', 'error', 'finished'] as const\r\n\r\nexport type LoadMoreUI = {\r\n root?: string\r\n divider?: string\r\n line?: string\r\n text?: string\r\n errorText?: string\r\n refresh?: string\r\n}\r\n\r\nconst config = {\r\n slots: {\r\n root: 'w-full h-[48px] leading-[48px] text-center text-[#999999] bg-transparent',\r\n divider: 'flex items-center justify-center w-[80%] mx-auto',\r\n line: 'h-[1px] bg-[#e8e8e8] flex-1',\r\n text: 'inline-block text-[14px] align-middle',\r\n errorText: 'inline-block text-[14px] align-middle px-[6px] cursor-pointer',\r\n refresh: 'inline-block align-middle text-[16px] cursor-pointer ml-1',\r\n },\r\n variants: {\r\n color: {\r\n primary: { text: 'text-primary', errorText: 'text-primary', refresh: 'text-primary' },\r\n secondary: { text: 'text-secondary', errorText: 'text-secondary', refresh: 'text-secondary' },\r\n success: { text: 'text-success', errorText: 'text-success', refresh: 'text-success' },\r\n info: { text: 'text-info', errorText: 'text-info', refresh: 'text-info' },\r\n warning: { text: 'text-warning', errorText: 'text-warning', refresh: 'text-warning' },\r\n error: { text: 'text-error', errorText: 'text-error', refresh: 'text-error' },\r\n neutral: { text: 'text-neutral', errorText: 'text-neutral', refresh: 'text-neutral' },\r\n },\r\n state: {\r\n finished: { text: 'px-2', },\r\n error: {},\r\n loading: {},\r\n }\r\n },\r\n defaultVariants: {\r\n color: 'neutral',\r\n },\r\n} as const\r\n\r\nexport default config\r\n",
13
+ "target": "uniapp"
14
+ },
15
+ {
16
+ "path": "RebornLoadmore.vue",
17
+ "content": "<script lang=\"ts\">\r\nexport default {\r\n name: 'reborn-loadmore',\r\n options: {\r\n virtualHost: true,\r\n addGlobalClass: true,\r\n styleIsolation: 'shared'\r\n }\r\n}\r\n</script>\r\n\r\n<script lang=\"ts\" setup>\r\nimport { computed, ref } from 'vue'\r\nimport RebornLoading, { type RebornLoadingProps } from '@/components/reborn-loading/RebornLoading.vue'\r\nimport { isDef, isUndefined, omitBy } from '@/lib/util'\r\nimport { tv } from '@/lib/tv'\r\nimport { cn } from '@/lib/utils'\r\nimport theme, { type LoadMoreUI, loadMoreColors, LoadMoreState } from './reborn-loadmore.config'\r\n\r\n\r\nexport interface LoadMoreProps {\r\n customClass?: string\r\n customStyle?: string\r\n state: typeof LoadMoreState[number]\r\n loadingText?: string\r\n finishedText?: string\r\n errorText?: string\r\n color?: typeof loadMoreColors[number]\r\n ui?: LoadMoreUI\r\n loadingProps?: RebornLoadingProps\r\n}\r\n\r\nconst props = withDefaults(defineProps<LoadMoreProps>(), {\r\n customClass: '',\r\n customStyle: '',\r\n state: 'loading',\r\n color: 'neutral',\r\n ui: () => ({}),\r\n loadingProps: () => ({\r\n size: '40rpx'\r\n })\r\n})\r\n\r\nconst emit = defineEmits(['reload'])\r\n\r\nconst currentState = ref<typeof LoadMoreState[number] | null>(null)\r\n\r\nfunction reload() {\r\n if (props.state !== 'error') return\r\n currentState.value = 'loading'\r\n emit('reload')\r\n}\r\n\r\nconst b = tv(theme)\r\n\r\nconst customLoadingProps = computed(() => {\r\n const loadingProps = isDef(props.loadingProps) ? omitBy(props.loadingProps, isUndefined) : {}\r\n loadingProps.customClass = cn('inline-block align-middle mr-2 w-4 h-4', loadingProps.customClass)\r\n if (!loadingProps.color) {\r\n loadingProps.color = props.color\r\n }\r\n return loadingProps\r\n})\r\n\r\nconst ui = computed(() => {\r\n const styles = b({\r\n color: props.color,\r\n state: props.state\r\n })\r\n return {\r\n root: (opts?: { class?: any }) => styles.root({ class: cn(opts?.class, props.customClass, props.ui?.root) }),\r\n divider: (opts?: { class?: any }) => styles.divider({ class: cn(opts?.class, props.ui?.divider) }),\r\n line: (opts?: { class?: any }) => styles.line({ class: cn(opts?.class, props.ui?.line) }),\r\n text: (opts?: { class?: any }) => styles.text({ class: cn(opts?.class, props.ui?.text) }),\r\n errorText: (opts?: { class?: any }) => styles.errorText({ class: cn(opts?.class, props.ui?.errorText) }),\r\n refresh: (opts?: { class?: any }) => styles.refresh({ class: cn(opts?.class, props.ui?.refresh) }),\r\n }\r\n})\r\n</script>\r\n<template>\r\n <view :class=\"ui.root()\" :style=\"customStyle\" @click=\"reload\">\r\n <view v-if=\"state === 'finished'\" :class=\"ui.divider()\">\r\n <view :class=\"ui.line()\"></view>\r\n <text :class=\"ui.text()\">{{ finishedText || '没有更多了' }}</text>\r\n <view :class=\"ui.line()\"></view>\r\n </view>\r\n <block v-else-if=\"state === 'error'\">\r\n <text :class=\"ui.text()\">{{ errorText || '加载失败' }}</text>\r\n <text :class=\"ui.errorText()\">{{ '点击重试' }}</text>\r\n <svg :class=\"ui.refresh()\" viewBox=\"0 0 1024 1024\" xmlns=\"http://www.w3.org/2000/svg\" width=\"1em\"\r\n height=\"1em\" fill=\"currentColor\">\r\n <path\r\n d=\"M512 1024C229.2 1024 0 794.8 0 512S229.2 0 512 0s512 229.2 512 512-229.2 512-512 512zM512 66.8C266.3 66.8 66.8 266.3 66.8 512S266.3 957.2 512 957.2 957.2 757.7 957.2 512 757.7 66.8 512 66.8z\">\r\n </path>\r\n <path\r\n d=\"M512 795.1c-156.1 0-283.1-127-283.1-283.1S355.9 228.9 512 228.9c35.6 0 70 6.6 102.1 19.5 17.6 7 26.1 27.1 19 44.7s-27.1 26.1-44.7 19c-24.3-9.7-50.2-14.7-76.4-14.7-119.3 0-216.3 97-216.3 216.3s97 216.3 216.3 216.3c103.5 0 193.3-73.4 212.4-175 3.4-18.7 21.4-31 40-27.6s31 21.4 27.6 40c-25.2 133.5-143.1 229.2-279.7 229.2z\">\r\n </path>\r\n <path\r\n d=\"M478.6 478.6c13.1 13.1 34.3 13.1 47.3 0L764 240.4c13.1-13.1 13.1-34.3 0-47.3-13.1-13.1-34.3-13.1-47.3 0L478.6 431.2c-13.1 13.1-13.1 34.4 0 47.4z\">\r\n </path>\r\n <path\r\n d=\"M856 507.2c-15.3 0-28.7-10.4-32.3-25.7-4.3-18-9.6-35.8-15.8-53.1-6.1-17.6-25.4-26.8-43-20.7s-26.8 25.4-20.7 43c4.8 13.3 8.9 27.1 12.2 41 4.2 18 22.3 29.3 40.3 25 1.7-.4 3.4-.9 5-.1 13.9 6.2 30 0 36.2-13.9.7-1.6 1.4-3.3 2.1-5s-.2-4.1-.2-4.1-1.3-4.8-3.8-6.4z\">\r\n </path>\r\n </svg>\r\n </block>\r\n <block v-else-if=\"state === 'loading'\">\r\n <reborn-loading v-bind=\"customLoadingProps\" />\r\n <text :class=\"ui.text()\">{{ loadingText || '加载中...' }}</text>\r\n </block>\r\n </view>\r\n</template>\r\n",
18
+ "target": "uniapp"
19
+ }
20
+ ],
21
+ "fileCount": 3,
22
+ "contentHash": "ef721509b9edbe248bd5ce4db4defc7f1365cdc9"
23
+ }
@@ -0,0 +1,38 @@
1
+ {
2
+ "name": "reborn-overlay",
3
+ "dependencies": [],
4
+ "files": [
5
+ {
6
+ "path": "index.ts",
7
+ "content": "export { default as RebornOverlay } from './RebornOverlay.vue';\r\n",
8
+ "target": "web"
9
+ },
10
+ {
11
+ "path": "reborn-overlay.config.ts",
12
+ "content": "export default { base: 'inset-0 bg-gray-8/70 dark:bg-gray-2/70' } as const;\r\n",
13
+ "target": "web"
14
+ },
15
+ {
16
+ "path": "RebornOverlay.vue",
17
+ "content": "<script setup lang=\"ts\">\r\nimport { computed, watch } from 'vue';\r\nimport RebornTransition from '../reborn-transition/RebornTransition.vue';\r\nimport theme from './reborn-overlay.config';\r\n\r\ninterface Props {\r\n modelValue?: boolean;\r\n duration?: number;\r\n lockScroll?: boolean;\r\n zIndex?: number;\r\n closeOnClickOverlay?: boolean;\r\n absolute?: boolean;\r\n customClass?: string;\r\n customStyle?: string;\r\n}\r\n\r\nconst props = withDefaults(defineProps<Props>(), {\r\n modelValue: false,\r\n duration: 300,\r\n lockScroll: true,\r\n zIndex: 100,\r\n closeOnClickOverlay: true,\r\n absolute: false,\r\n customClass: '',\r\n customStyle: ''\r\n});\r\nconst emit = defineEmits(['update:modelValue', 'close']);\r\n\r\nwatch(() => props.modelValue && props.lockScroll, (locked) => {\r\n if (typeof document !== 'undefined') document.body.style.overflow = locked ? 'hidden' : '';\r\n}, { immediate: true });\r\n\r\nconst overlayClass = computed(() => `${props.absolute ? 'absolute' : 'fixed'} ${theme.base} ${props.customClass}`);\r\nconst overlayStyle = computed(() => `z-index:${props.zIndex};${props.customStyle}`);\r\nconst onClick = () => { if (props.closeOnClickOverlay) { emit('update:modelValue', false); emit('close'); } };\r\n</script>\r\n<template>\r\n <RebornTransition :show=\"props.modelValue\" name=\"fade\" :duration=\"props.duration\" :custom-class=\"overlayClass\"\r\n :custom-style=\"overlayStyle\" :disable-touch-move=\"props.lockScroll\" @click=\"onClick\">\r\n <slot />\r\n </RebornTransition>\r\n</template>\r\n",
18
+ "target": "web"
19
+ },
20
+ {
21
+ "path": "index.ts",
22
+ "content": "// @ts-ignore\r\nimport RebornOverlay from './reborn-overlay.vue'\r\n\r\nexport default RebornOverlay\r\nexport { RebornOverlay }\r\n",
23
+ "target": "uniapp"
24
+ },
25
+ {
26
+ "path": "reborn-overlay.config.ts",
27
+ "content": "const config = {\r\n base: 'inset-0 bg-gray-8/70 dark:bg-gray-2/70',\r\n variants: {\r\n absolute: {\r\n true: 'absolute',\r\n false: 'fixed'\r\n }\r\n },\r\n defaultVariants: {\r\n absolute: false\r\n }\r\n} as const\r\n\r\nexport default config\r\n",
28
+ "target": "uniapp"
29
+ },
30
+ {
31
+ "path": "RebornOverlay.vue",
32
+ "content": "<script lang=\"ts\">\r\nexport default {\r\n name: 'reborn-overlay',\r\n options: {\r\n virtualHost: true,\r\n addGlobalClass: true,\r\n styleIsolation: 'shared'\r\n }\r\n}\r\n</script>\r\n\r\n<script lang=\"ts\" setup>\r\nimport { computed, type PropType } from 'vue'\r\nimport RebornTransition from '../reborn-transition/RebornTransition.vue'\r\nimport { tv } from '@/lib/tv'\r\nimport theme from './reborn-overlay.config'\r\n// #ifdef H5\r\nimport { useLockScroll } from '@/composables/useLockScroll'\r\n// #endif\r\n\r\nconst props = defineProps({\r\n customClass: { type: String, default: '' },\r\n customStyle: { type: String, default: '' },\r\n modelValue: { type: Boolean, default: false },\r\n duration: {\r\n type: [Object, Number, Boolean] as PropType<Record<string, number> | number | boolean>,\r\n default: 300\r\n },\r\n lockScroll: { type: Boolean, default: true },\r\n zIndex: { type: Number, default: 10 },\r\n closeOnClickOverlay: { type: Boolean, default: true },\r\n absolute: { type: Boolean, default: false },\r\n})\r\n\r\nconst emit = defineEmits(['update:modelValue', 'close'])\r\n\r\nconst b = tv(theme)\r\n\r\nconst overlayClass = computed(() => {\r\n return `${b({ absolute: props.absolute })} ${props.customClass}`\r\n})\r\n\r\nfunction handleClick() {\r\n if (props.closeOnClickOverlay) {\r\n emit('update:modelValue', false)\r\n emit('close')\r\n }\r\n}\r\n\r\n// #ifdef H5\r\nuseLockScroll(() => props.modelValue && props.lockScroll)\r\n// #endif\r\n</script>\r\n\r\n<template>\r\n <reborn-transition :show=\"modelValue\" name=\"fade\" :custom-class=\"overlayClass\" :duration=\"duration\"\r\n :custom-style=\"`z-index: ${zIndex}; ${customStyle}`\" :disable-touch-move=\"lockScroll\" @click=\"handleClick\">\r\n <slot></slot>\r\n </reborn-transition>\r\n</template>\r\n",
33
+ "target": "uniapp"
34
+ }
35
+ ],
36
+ "fileCount": 6,
37
+ "contentHash": "aa390631bae238e04896b84b570c8e3ad3f84a20"
38
+ }
@@ -0,0 +1,18 @@
1
+ {
2
+ "name": "reborn-page",
3
+ "dependencies": [],
4
+ "files": [
5
+ {
6
+ "path": "reborn-page.config.ts",
7
+ "content": "const config = {\r\n slots: {\r\n root: 'min-h-screen w-full flex flex-col gap-4 bg-gray-2 transition-colors duration-300',\r\n header: 'flex flex-col gap-2 p-4',\r\n title: 'text-xl font-bold text-gray-800 dark:text-white',\r\n description: 'text-sm text-gray-5 dark:text-gray-3',\r\n body: 'p-4',\r\n },\r\n} as const\r\n\r\nexport type PageUI = {\r\n root?: string\r\n header?: string\r\n title?: string\r\n description?: string\r\n body?: string\r\n}\r\n\r\nexport default config\r\n",
8
+ "target": "uniapp"
9
+ },
10
+ {
11
+ "path": "RebornPage.vue",
12
+ "content": "<script setup lang=\"ts\">\r\nimport { computed, type PropType } from 'vue'\r\nimport { tv } from '@/lib/tv'\r\nimport { cn } from '@/lib/utils'\r\nimport theme, { type PageUI } from './reborn-page.config'\r\nimport RebornToast from '@/components/reborn-toast/RebornToast.vue'\r\n\r\nconst props = defineProps({\r\n title: { type: String, default: '' },\r\n description: { type: String, default: '' },\r\n customClass: { type: String, default: '' },\r\n ui: {\r\n type: Object as PropType<PageUI>,\r\n default: () => ({}),\r\n },\r\n})\r\n\r\nconst b = tv(theme)\r\nconst uiOverrides = computed(() => props.ui || {})\r\n\r\nconst ui = computed(() => {\r\n const styles = b()\r\n return {\r\n root: (opts?: { class?: any }) => styles.root({ class: cn(opts?.class, uiOverrides.value.root) }),\r\n header: (opts?: { class?: any }) => styles.header({ class: cn(opts?.class, uiOverrides.value.header) }),\r\n title: (opts?: { class?: any }) => styles.title({ class: cn(opts?.class, uiOverrides.value.title) }),\r\n description: (opts?: { class?: any }) => styles.description({ class: cn(opts?.class, uiOverrides.value.description) }),\r\n body: (opts?: { class?: any }) => styles.body({ class: cn(opts?.class, uiOverrides.value.body) }),\r\n }\r\n})\r\n</script>\r\n\r\n<template>\r\n <view :class=\"ui.root({ class: props.customClass })\">\r\n <view v-if=\"title || description || $slots.header\" :class=\"ui.header()\">\r\n <slot name=\"header\">\r\n <view v-if=\"title\" :class=\"ui.title()\">\r\n {{ title }}\r\n </view>\r\n <view v-if=\"description\" :class=\"ui.description()\">\r\n {{ description }}\r\n </view>\r\n </slot>\r\n </view>\r\n\r\n <view :class=\"ui.body()\">\r\n <slot />\r\n </view>\r\n\r\n <RebornToast />\r\n </view>\r\n</template>\r\n\r\n<script lang=\"ts\">\r\nexport default {\r\n name: 'reborn-page',\r\n options: {\r\n virtualHost: true,\r\n addGlobalClass: true,\r\n styleIsolation: 'shared',\r\n },\r\n}\r\n</script>\r\n",
13
+ "target": "uniapp"
14
+ }
15
+ ],
16
+ "fileCount": 2,
17
+ "contentHash": "18b77a8894d6184f635b8f62c301f69caeeeee90"
18
+ }
@@ -0,0 +1,26 @@
1
+ {
2
+ "name": "reborn-picker-view",
3
+ "dependencies": [
4
+ "clsx",
5
+ "lodash-es"
6
+ ],
7
+ "files": [
8
+ {
9
+ "path": "index.ts",
10
+ "content": "export { default as RebornPickerView } from './RebornPickerView.vue'\r\nexport type { PickerViewProps, SelectOption } from './RebornPickerView.vue'\r\n",
11
+ "target": "uniapp"
12
+ },
13
+ {
14
+ "path": "reborn-picker-view.config.ts",
15
+ "content": "const color = ['primary', 'success', 'info', 'warning', 'error', 'neutral'] as const\r\nconst config = {\r\n slots: {\r\n wrapper: 'w-full h-full',\r\n header: 'flex flex-row items-center py-4',\r\n headerText: 'flex-1 text-center font-medium text-gray-8 dark:text-gray-2',\r\n pickerContainer: 'px-1',\r\n item: 'flex flex-row items-center justify-center w-full',\r\n itemText: 'transition-colors duration-200',\r\n indicator: 'bg-primary/10 before:content-[\"\"] before:absolute before:top-0 before:border-none after:content-[\"\"] after:absolute after:bottom-0 after:border-none',\r\n },\r\n variants: {\r\n color: {\r\n primary: {\r\n indicator: 'bg-primary/10',\r\n },\r\n success: {\r\n indicator: 'bg-success/10',\r\n },\r\n info: {\r\n indicator: 'bg-info/10',\r\n },\r\n warning: {\r\n indicator: 'bg-warning/10',\r\n },\r\n error: {\r\n indicator: 'bg-error/10',\r\n },\r\n neutral: {\r\n indicator: 'bg-neutral/10',\r\n },\r\n },\r\n active: {\r\n false: {\r\n itemText: 'text-gray-8 dark:text-gray-6',\r\n },\r\n true: {\r\n itemText: 'font-bold',\r\n },\r\n },\r\n },\r\n compoundVariants: [\r\n { color: 'primary' as (typeof color)[number], active: true as const, class: { itemText: 'text-primary dark:text-primary' } },\r\n { color: 'success' as (typeof color)[number], active: true as const, class: { itemText: 'text-success dark:text-success' } },\r\n { color: 'info' as (typeof color)[number], active: true as const, class: { itemText: 'text-info dark:text-info' } },\r\n { color: 'warning' as (typeof color)[number], active: true as const, class: { itemText: 'text-warning dark:text-warning' } },\r\n { color: 'error' as (typeof color)[number], active: true as const, class: { itemText: 'text-error dark:text-error' } },\r\n { color: 'neutral' as (typeof color)[number], active: true as const, class: { itemText: 'text-gray-9 dark:text-white' } },\r\n ],\r\n defaultVariants: {\r\n color: 'primary' as (typeof color)[number],\r\n active: false,\r\n },\r\n}\r\n\r\nexport { color as pickerColors }\r\n\r\nexport default config\r\n",
16
+ "target": "uniapp"
17
+ },
18
+ {
19
+ "path": "RebornPickerView.vue",
20
+ "content": "<script setup lang=\"ts\">\r\nimport type { ClassValue } from 'clsx'\r\nimport type { pickerColors } from './reborn-picker-view.config'\r\nimport { isEqual, isNull } from 'lodash-es'\r\nimport { computed, nextTick, onMounted, ref, shallowRef, watch } from 'vue'\r\nimport { initTheme, isAppAndroid, isAppIOS } from '@/lib/device'\r\nimport { tv } from '@/lib/tv'\r\nimport { cn } from '@/lib/utils'\r\nimport theme from './reborn-picker-view.config'\r\n\r\ndefineOptions({\r\n name: 'RebornPickerView',\r\n})\r\n\r\nconst props = withDefaults(defineProps<PickerViewProps>(), {\r\n color: 'primary',\r\n headers: () => [],\r\n value: () => [],\r\n columns: () => [],\r\n itemHeight: isAppIOS() ? 50 : 42,\r\n height: 300,\r\n})\r\n\r\nconst emit = defineEmits<{\r\n (e: 'change-value', values: any[]): void\r\n (e: 'change-index', indexes: number[]): void\r\n (e: 'change-item', item: any): void\r\n}>()\r\n\r\nexport interface SelectOption {\r\n label: string\r\n value: any\r\n children?: SelectOption[]\r\n [key: string]: any\r\n}\r\n\r\nexport interface PickerViewProps {\r\n /** 颜色 */\r\n color?: typeof pickerColors[number]\r\n /** 表头 */\r\n headers?: string[]\r\n /** 当前选中索引 */\r\n value?: number[]\r\n /** 列数据 */\r\n columns?: SelectOption[][]\r\n /** 每项高度 */\r\n itemHeight?: number\r\n /** 整体高度 */\r\n height?: number\r\n /** 样式覆盖 */\r\n ui?: Partial<{\r\n wrapper: ClassValue\r\n header: ClassValue\r\n headerText: ClassValue\r\n pickerContainer: ClassValue\r\n item: ClassValue\r\n itemText: ClassValue\r\n indicator: ClassValue\r\n }>\r\n}\r\n\r\n// ui 样式系统\r\nconst uiOverrides = computed(() => props.ui || {})\r\nconst b = tv(theme)\r\n\r\nconst ui = computed(() => {\r\n const styles = b({\r\n color: props.color,\r\n })\r\n\r\n return {\r\n wrapper: (opts?: { class?: any }) =>\r\n styles.wrapper({ class: cn(opts?.class, uiOverrides.value.wrapper) }),\r\n header: (opts?: { class?: any }) =>\r\n styles.header({ class: cn(opts?.class, uiOverrides.value.header) }),\r\n headerText: (opts?: { class?: any }) =>\r\n styles.headerText({ class: cn(opts?.class, uiOverrides.value.headerText) }),\r\n pickerContainer: (opts?: { class?: any }) =>\r\n styles.pickerContainer({ class: cn(opts?.class, uiOverrides.value.pickerContainer) }),\r\n item: (opts?: { class?: any }) =>\r\n styles.item({ class: cn(opts?.class, uiOverrides.value.item) }),\r\n itemText: (opts?: { class?: any, active?: boolean, color?: any }) =>\r\n styles.itemText({ active: opts?.active, color: opts?.color, class: cn(opts?.class, uiOverrides.value.itemText) }),\r\n indicator: (opts?: { class?: any }) =>\r\n styles.indicator({ class: cn(opts?.class, uiOverrides.value.indicator) }),\r\n }\r\n})\r\n\r\n// 获取窗口宽度,用于计算选择器列宽\r\nconst { windowWidth } = uni.getWindowInfo()\r\nconst isDark = ref(false)\r\n\r\n// 顶部显示表头\r\nconst computedHeaders = computed(() => {\r\n return props.headers.slice(0, props.columns.length)\r\n})\r\n\r\n// 监听选择器值改变事件\r\nfunction onChange(e: any) {\r\n const indexs = e.detail.value\r\n\r\n // 处理快速滑动导致的索引越界\r\n indexs.forEach((v: number, i: number, arr: number[]) => {\r\n if (i < props.columns.length) {\r\n const n = props.columns[i].length\r\n if (v >= n) {\r\n arr[i] = n - 1\r\n }\r\n }\r\n })\r\n\r\n // 相同值不触发事件\r\n if (isEqual(indexs, props.value)) {\r\n return\r\n }\r\n\r\n // 获取所有列的值\r\n const values = props.columns.map((c, i) => {\r\n return c?.[indexs[i]]?.value ?? 0\r\n })\r\n const select = props.columns.map((c, i) => {\r\n return c?.[indexs[i]] ?? null\r\n })\r\n emit('change-value', values)\r\n emit('change-index', indexs)\r\n emit('change-item', select)\r\n}\r\n\r\n// === Android Canvas 渲染 ===\r\nconst columnItemRef = shallowRef<any[]>([])\r\n\r\nfunction renderColumnItem() {\r\n const fontSize = 14\r\n const color = isDark.value ? 'white' : '#666666'\r\n\r\n for (let i = 0; i < columnItemRef.value.length; i++) {\r\n const column = props.columns[i]\r\n const dom = columnItemRef.value[i]\r\n if (!dom) { continue }\r\n const rect = dom.getBoundingClientRect()\r\n const ctx = dom.getDrawableContext()\r\n if (!ctx) { continue }\r\n\r\n ctx.reset()\r\n ctx.textAlign = 'center'\r\n\r\n const x = rect.width / 2\r\n\r\n for (let j = 0; j < column.length; j++) {\r\n ctx.fillStyle = color\r\n ctx.font = `${fontSize}px`\r\n const y = 12 + (props.itemHeight - fontSize) / 2 + props.itemHeight * j\r\n ctx.fillText(column[j].label, x, y)\r\n }\r\n ctx.update()\r\n }\r\n}\r\n\r\n// 遮罩层样式\r\nconst maskStyle = computed(() => {\r\n if (isDark.value) {\r\n return `background-image: linear-gradient(180deg, rgba(0, 0, 0, 0), rgba(0, 0, 0, 0))`\r\n }\r\n return ''\r\n})\r\n\r\n// 选择器指示器样式\r\nconst indicatorStyle = computed(() => {\r\n let str = ''\r\n const columnsCount = props.columns.length || 1\r\n const width = Math.ceil((windowWidth - 8) / columnsCount - 10)\r\n\r\n const style: Record<string, string> = {\r\n 'height': `${props.itemHeight}px`,\r\n 'width': 'calc(100% - 4px)',\r\n 'left': '2px',\r\n 'border-radius': '10px',\r\n 'box-sizing': 'border-box',\r\n 'border': 'none',\r\n }\r\n\r\n if (isAppIOS()) {\r\n if (isDark.value) {\r\n style['box-shadow'] = 'none'\r\n style.width = `${width - 3}px`\r\n }\r\n else {\r\n style.width = `${width + 2}px`\r\n }\r\n }\r\n\r\n if (isAppAndroid()) {\r\n style.width = `${width + 1}px`\r\n }\r\n\r\n const objKeys = Object.keys(style)\r\n for (let i = 0; i < objKeys.length; i++) {\r\n const key = objKeys[i]\r\n str += `${key}: ${style[key]};`\r\n }\r\n return str\r\n})\r\n\r\nfunction render() {\r\n renderColumnItem()\r\n}\r\n\r\nonMounted(() => {\r\n isDark.value = initTheme() === 'dark'\r\n nextTick(() => {\r\n render()\r\n })\r\n\r\n watch(\r\n computed(() => [props.columns, props.itemHeight]),\r\n () => {\r\n render()\r\n },\r\n )\r\n})\r\n</script>\r\n\r\n<template>\r\n <view class=\"reborn-picker-view\" :class=\"ui.wrapper()\">\r\n <view v-if=\"computedHeaders.length > 0\" :class=\"ui.header()\" @touchstart.stop @touchmove.stop @touchend.stop\r\n @touchcancel.stop>\r\n <text v-for=\"(label, index) in computedHeaders\" :key=\"index\" :class=\"ui.headerText()\">\r\n {{ label }}\r\n </text>\r\n </view>\r\n\r\n <view :class=\"ui.pickerContainer()\" :style=\"{ height: `${height}px` }\" @touchstart.stop @touchmove.stop\r\n @touchend.stop @touchcancel.stop>\r\n <picker-view class=\"h-full\" :value=\"value\" :mask-style=\"maskStyle\" :mask-top-style=\"maskStyle\"\r\n :indicator-class=\"ui.indicator()\" :mask-bottom-style=\"maskStyle\" :immediate-change=\"true\"\r\n :indicator-style=\"indicatorStyle\" @change=\"onChange\">\r\n <picker-view-column v-for=\"(column, columnIndex) in columns\" :key=\"columnIndex\">\r\n <!-- #ifdef APP-ANDROID -->\r\n <view ref=\"columnItemRef\" :style=\"{ height: `${itemHeight * column.length}px` }\" />\r\n <!-- #endif -->\r\n\r\n <!-- #ifndef APP-ANDROID -->\r\n <view v-for=\"(item, index) in column\" :key=\"index\" :class=\"ui.item()\" :style=\"{ height: `${itemHeight}px` }\">\r\n <slot :item=\"item\" :index=\"index\">\r\n <text :class=\"ui.itemText({ active: index == value[columnIndex], color })\">\r\n {{ item.label }}\r\n </text>\r\n </slot>\r\n </view>\r\n <!-- #endif -->\r\n </picker-view-column>\r\n </picker-view>\r\n </view>\r\n </view>\r\n</template>\r\n\r\n<style lang=\"scss\" scoped>\r\n.reborn-picker-view {\r\n .uni-picker-view-indicator {\r\n\r\n // #ifdef H5\r\n &::after,\r\n &::before {\r\n display: none;\r\n }\r\n\r\n // #endif\r\n }\r\n}\r\n</style>\r\n",
21
+ "target": "uniapp"
22
+ }
23
+ ],
24
+ "fileCount": 3,
25
+ "contentHash": "fafead3d1436230dd169a7e77158aacbe82d54ef"
26
+ }
@@ -0,0 +1,58 @@
1
+ {
2
+ "name": "reborn-popover",
3
+ "dependencies": [],
4
+ "files": [
5
+ {
6
+ "path": "index.ts",
7
+ "content": "export { default as RebornPopover } from './RebornPopover.vue';\r\n",
8
+ "target": "web"
9
+ },
10
+ {
11
+ "path": "reborn-popover.config.ts",
12
+ "content": "export const popoverAnimations = {\r\n base: {\r\n enterActiveClass: \"transition ease-out duration-200\",\r\n enterToClass: \"opacity-100 scale-100 translate-x-0 translate-y-0\",\r\n leaveActiveClass: \"transition ease-in duration-150\",\r\n leaveFromClass: \"opacity-100 scale-100 translate-x-0 translate-y-0\",\r\n },\r\n top: {\r\n enterFromClass: \"opacity-0 translate-y-2 scale-95\",\r\n leaveToClass: \"opacity-0 translate-y-2 scale-95\",\r\n },\r\n bottom: {\r\n enterFromClass: \"opacity-0 -translate-y-2 scale-95\",\r\n leaveToClass: \"opacity-0 -translate-y-2 scale-95\",\r\n },\r\n left: {\r\n enterFromClass: \"opacity-0 translate-x-2 scale-95\",\r\n leaveToClass: \"opacity-0 translate-x-2 scale-95\",\r\n },\r\n right: {\r\n enterFromClass: \"opacity-0 -translate-x-2 scale-95\",\r\n leaveToClass: \"opacity-0 -translate-x-2 scale-95\",\r\n }\r\n} as const;\r\n\r\nexport default {\r\n slots: {\r\n wrapper: \"relative inline-block\",\r\n trigger: \"inline-block\",\r\n contentWrapper: \"fixed top-0 left-0 z-[9999]\",\r\n content: \"relative bg-white dark:bg-gray-900 border border-gray-200 dark:border-gray-800 shadow-xl rounded-xl p-3\",\r\n arrow: \"absolute w-3 h-3 border dark:border-gray-800 border-gray-200 bg-white dark:bg-gray-900\",\r\n bridge: \"absolute inset-0 z-[-1]\",\r\n mask: \"fixed inset-0 bg-black/30 z-[9998]\",\r\n },\r\n variants: {\r\n side: {\r\n top: {\r\n content: \"origin-bottom\"\r\n },\r\n bottom: {\r\n content: \"origin-top\"\r\n },\r\n left: {\r\n content: \"origin-right\"\r\n },\r\n right: {\r\n content: \"origin-left\"\r\n },\r\n },\r\n align: {\r\n start: {},\r\n center: {},\r\n end: {},\r\n },\r\n },\r\n defaultVariants: {\r\n side: \"bottom\",\r\n align: \"center\",\r\n },\r\n} as const;\r\n",
13
+ "target": "web"
14
+ },
15
+ {
16
+ "path": "RebornPopover.vue",
17
+ "content": "<script setup lang=\"ts\">\r\nimport { ref, computed, watch, nextTick, onMounted, onUnmounted, toRef } from \"vue\"\r\nimport { tv } from \"~/lib/tv\"\r\nimport theme, { popoverAnimations } from \"./reborn-popover.config\"\r\nimport { cn } from \"~/lib/utils\"\r\n\r\ndefineOptions({\r\n name: \"RebornPopover\"\r\n})\r\n\r\n/**\r\n * Popover 内容配置\r\n */\r\nexport interface PopoverContentProps {\r\n /** Popover 相对于触发器的显示位置 */\r\n side?: \"top\" | \"right\" | \"bottom\" | \"left\"\r\n /** Popover 沿触发器轴线的对齐方式 */\r\n align?: \"start\" | \"center\" | \"end\"\r\n /** Popover 与触发器之间的间距 */\r\n sideOffset?: number\r\n}\r\n\r\n/**\r\n * RebornPopover 组件属性\r\n */\r\nexport interface PopoverProps {\r\n /** 触发模式:'click' (默认) 或 'hover' */\r\n mode?: \"click\" | \"hover\"\r\n /** 内容位置与偏移配置 */\r\n content?: PopoverContentProps\r\n /** 是否显示箭头 */\r\n arrow?: boolean\r\n /** 是否将 Popover 渲染到指定的 DOM 节点 (通常为 'body') */\r\n portal?: boolean | string\r\n /** 点击外部时是否关闭 Popover */\r\n dismissible?: boolean\r\n /** 受控显示状态 */\r\n open?: boolean\r\n /** 非受控默认显示状态 */\r\n defaultOpen?: boolean\r\n /** 是否显示遮罩层并捕获焦点 */\r\n modal?: boolean\r\n /** 延迟打开时间 (ms) - 适用于 hover 模式 */\r\n openDelay?: number\r\n /** 延迟关闭时间 (ms) - 适用于 hover 模式,防止意外关闭 */\r\n closeDelay?: number\r\n /** 额外的类名 */\r\n class?: any\r\n /** UI 覆盖配置 */\r\n ui?: any\r\n}\r\n\r\nconst props = withDefaults(defineProps<PopoverProps>(), {\r\n mode: \"click\",\r\n portal: true,\r\n arrow: false,\r\n dismissible: true,\r\n modal: false,\r\n openDelay: 0,\r\n closeDelay: 120,\r\n content: () => ({\r\n side: \"bottom\",\r\n align: \"center\",\r\n sideOffset: 8\r\n })\r\n})\r\n\r\nconst emit = defineEmits<{\r\n /** 当显示状态发生变化时触发 */\r\n (e: \"update:open\", v: boolean): void\r\n}>()\r\n\r\n/* ---------------- 显示状态 ---------------- */\r\n\r\n/** 内部状态,与 props.defaultOpen 或 props.open 同步 */\r\nconst internalOpen = ref(props.defaultOpen ?? props.open ?? false)\r\n\r\nwatch(() => props.open, v => {\r\n if (v !== undefined) internalOpen.value = v\r\n})\r\n\r\n/** 计算后的显示状态,处理 v-model 同步 */\r\nconst open = computed({\r\n get: () => internalOpen.value,\r\n set: v => {\r\n internalOpen.value = v\r\n emit(\"update:open\", v)\r\n }\r\n})\r\n\r\n/* ---------------- refs ---------------- */\r\n\r\nconst wrapperRef = ref<HTMLElement>()\r\nconst triggerRef = ref<HTMLElement>()\r\nconst contentRef = ref<HTMLElement>()\r\n\r\n/* ---------------- 悬停逻辑 ---------------- */\r\n\r\nlet hoverCount = 0\r\nlet hoverTimer: any = null\r\n\r\n/** 处理 hover 模式下的鼠标移入,支持延迟打开 */\r\nconst onMouseEnter = () => {\r\n if (props.mode !== \"hover\") return\r\n\r\n hoverCount++\r\n clearTimeout(hoverTimer)\r\n\r\n hoverTimer = setTimeout(() => {\r\n open.value = true\r\n }, props.openDelay)\r\n}\r\n\r\n/** 处理 hover 模式下的鼠标移出,支持延迟关闭并检查连续性 */\r\nconst onMouseLeave = () => {\r\n if (props.mode !== \"hover\") return\r\n\r\n hoverCount--\r\n\r\n hoverTimer = setTimeout(() => {\r\n if (hoverCount <= 0) open.value = false\r\n }, props.closeDelay)\r\n}\r\n\r\n/* ---------------- click trigger ---------------- */\r\n\r\nconst onClickTrigger = () => {\r\n if (props.mode === \"click\") {\r\n open.value = !open.value\r\n }\r\n}\r\n\r\n/* ---------------- 点击外部关闭 ---------------- */\r\n\r\nconst onClickOutside = (e: MouseEvent) => {\r\n if (!open.value || !props.dismissible) return\r\n\r\n const target = e.target as Node\r\n\r\n if (\r\n wrapperRef.value?.contains(target) ||\r\n contentRef.value?.contains(target)\r\n ) {\r\n return\r\n }\r\n\r\n open.value = false\r\n}\r\n\r\n/* ---------------- 位置计算 ---------------- */\r\n\r\nconst style = ref<Record<string, string>>({\r\n transform: \"translate3d(0,0,0)\"\r\n})\r\n\r\n/**\r\n * 根据触发器的边界动态计算 Popover 的位置。\r\n * 使用 offsetWidth/Height 以排除绝对定位箭头的溢出干扰。\r\n */\r\nconst calculatePosition = () => {\r\n if (!triggerRef.value || !contentRef.value) return\r\n\r\n const rect = triggerRef.value.getBoundingClientRect()\r\n // 不直接测量 contentRef 的 bounds,因为它包含了绝对定位出的箭头,\r\n // 我们需要测量定义的视觉边界内容盒子。\r\n const contentBox = contentRef.value.firstElementChild as HTMLElement\r\n const cWidth = contentBox.offsetWidth\r\n const cHeight = contentBox.offsetHeight\r\n\r\n const side = props.content?.side || \"bottom\"\r\n const align = props.content?.align || \"center\"\r\n const offset = props.content?.sideOffset ?? 8\r\n\r\n let x = rect.left\r\n let y = rect.bottom + offset\r\n\r\n // 基于 'side' 属性的坐标计算\r\n if (side === \"top\") {\r\n y = rect.top - cHeight - offset\r\n }\r\n\r\n if (side === \"left\") {\r\n x = rect.left - cWidth - offset\r\n y = rect.top + rect.height / 2 - cHeight / 2\r\n }\r\n\r\n if (side === \"right\") {\r\n x = rect.right + offset\r\n y = rect.top + rect.height / 2 - cHeight / 2\r\n }\r\n\r\n // side 轴线上的对齐逻辑\r\n if (side === \"bottom\" || side === \"top\") {\r\n if (align === \"center\") {\r\n x = rect.left + rect.width / 2 - cWidth / 2\r\n }\r\n\r\n if (align === \"end\") {\r\n x = rect.right - cWidth\r\n }\r\n }\r\n\r\n if (side === \"left\" || side === \"right\") {\r\n if (align === \"start\") {\r\n y = rect.top\r\n }\r\n\r\n if (align === \"end\") {\r\n y = rect.bottom - cHeight\r\n }\r\n }\r\n\r\n // 屏幕碰撞排查逻辑(防止 Popover 超出视口)\r\n const vWidth = window.innerWidth\r\n const vHeight = document.documentElement.clientHeight\r\n\r\n if (x < 8) x = 8\r\n if (x + cWidth > vWidth - 8) x = vWidth - cWidth - 8\r\n if (y < 8) y = 8\r\n if (y + cHeight > vHeight - 8) y = vHeight - cHeight - 8\r\n\r\n style.value = {\r\n transform: `translate3d(${x}px, ${y}px, 0)`\r\n }\r\n}\r\n\r\n\r\nlet frame: number | null = null\r\n\r\nconst updatePosition = () => {\r\n if (frame) return\r\n\r\n frame = requestAnimationFrame(() => {\r\n calculatePosition()\r\n frame = null\r\n })\r\n}\r\n\r\n\r\nlet resizeObserver: ResizeObserver | null = null;\r\n\r\n/** 当内容元素被检测到或尺寸变化时自动更新位置 */\r\nwatch(contentRef, (el) => {\r\n if (resizeObserver) {\r\n resizeObserver.disconnect()\r\n resizeObserver = null\r\n }\r\n if (el) {\r\n resizeObserver = new ResizeObserver(() => {\r\n if (open.value) updatePosition()\r\n })\r\n resizeObserver.observe(el)\r\n\r\n if (open.value) {\r\n // 使用双重 requestAnimationFrame 确保在显示状态切换(display:block)后的布局重计算已完成\r\n requestAnimationFrame(() => {\r\n requestAnimationFrame(() => calculatePosition())\r\n })\r\n }\r\n }\r\n}, { immediate: true })\r\n\r\nonMounted(() => {\r\n document.addEventListener(\"mousedown\", onClickOutside)\r\n window.addEventListener(\"resize\", updatePosition)\r\n window.addEventListener(\"scroll\", updatePosition, true)\r\n})\r\n\r\nonUnmounted(() => {\r\n document.removeEventListener(\"mousedown\", onClickOutside)\r\n window.removeEventListener(\"resize\", updatePosition)\r\n window.removeEventListener(\"scroll\", updatePosition, true)\r\n if (resizeObserver) resizeObserver.disconnect()\r\n})\r\n\r\nwatch(open, v => {\r\n if (v) {\r\n requestAnimationFrame(() => {\r\n requestAnimationFrame(() => {\r\n calculatePosition()\r\n })\r\n })\r\n }\r\n})\r\n\r\n\r\n/**\r\n * 计算指示箭头的精确样式。\r\n * 使用 clip-path 渲染一个真正的三角形,并隐藏旋转正方形中不需要的边缘。\r\n */\r\nconst arrowStyle = computed(() => {\r\n const side = props.content?.side || \"bottom\"\r\n const offsetScale = \"-6px\"\r\n\r\n // 为了形成完美的三角形并防止重叠,我们在旋转后的正方形上使用 clip-path 裁剪。\r\n if (side === \"bottom\")\r\n return {\r\n top: offsetScale, left: \"50%\", transform: \"translateX(-50%) rotate(45deg)\",\r\n clipPath: \"polygon(0 0, 100% 0, 0 100%)\",\r\n borderBottomWidth: \"0\", borderRightWidth: \"0\"\r\n }\r\n\r\n if (side === \"top\")\r\n return {\r\n bottom: offsetScale, left: \"50%\", transform: \"translateX(-50%) rotate(45deg)\",\r\n clipPath: \"polygon(100% 100%, 100% 0, 0 100%)\",\r\n borderTopWidth: \"0\", borderLeftWidth: \"0\"\r\n }\r\n\r\n if (side === \"left\")\r\n return {\r\n right: offsetScale, top: \"50%\", transform: \"translateY(-50%) rotate(45deg)\",\r\n clipPath: \"polygon(100% 0, 0 0, 100% 100%)\",\r\n borderBottomWidth: \"0\", borderLeftWidth: \"0\"\r\n }\r\n\r\n if (side === \"right\")\r\n return {\r\n left: offsetScale, top: \"50%\", transform: \"translateY(-50%) rotate(45deg)\",\r\n clipPath: \"polygon(0 100%, 0 0, 100% 100%)\",\r\n borderTopWidth: \"0\", borderRightWidth: \"0\"\r\n }\r\n})\r\n\r\n/* ---------------- styles ---------------- */\r\n\r\nconst b = tv(theme)\r\nconst ui = computed(() => {\r\n return b({\r\n side: props.content?.side,\r\n align: props.content?.align\r\n })\r\n})\r\n\r\n/**\r\n * 计算动画类名,根据 side 动态选择方向性动画偏移\r\n */\r\nconst ani = computed(() => {\r\n const side = props.content?.side || \"bottom\"\r\n const specific = popoverAnimations[side]\r\n return {\r\n ...popoverAnimations.base,\r\n enterFromClass: cn(popoverAnimations.base.enterActiveClass && \"\", specific.enterFromClass),\r\n leaveToClass: cn(popoverAnimations.base.leaveActiveClass && \"\", specific.leaveToClass)\r\n }\r\n})\r\n\r\ndefineExpose({\r\n close: () => (open.value = false)\r\n})\r\n</script>\r\n\r\n<template>\r\n <div ref=\"wrapperRef\" :class=\"ui.wrapper({ class: props.class })\" @mouseenter=\"onMouseEnter\"\r\n @mouseleave=\"onMouseLeave\">\r\n <div ref=\"triggerRef\" :class=\"ui.trigger()\" @click=\"onClickTrigger\">\r\n <slot :open=\"open\" />\r\n </div>\r\n\r\n <Teleport :to=\"typeof portal === 'string' ? portal : 'body'\" :disabled=\"!portal\">\r\n\r\n <!-- mask -->\r\n <div v-if=\"open && modal\" :class=\"ui.mask()\" @click=\"props.dismissible && (open = false)\" />\r\n\r\n <Transition :enter-active-class=\"ani.enterActiveClass\" :enter-from-class=\"ani.enterFromClass\"\r\n :enter-to-class=\"ani.enterToClass\" :leave-active-class=\"ani.leaveActiveClass\"\r\n :leave-from-class=\"ani.leaveFromClass\" :leave-to-class=\"ani.leaveToClass\">\r\n <div v-show=\"open\" ref=\"contentRef\" :class=\"ui.contentWrapper()\" :style=\"style\"\r\n @mouseenter=\"onMouseEnter\" @mouseleave=\"onMouseLeave\">\r\n <div :class=\"ui.content()\">\r\n <slot name=\"content\" />\r\n\r\n <!-- 隐形的悬停桥接层,用于通过间隙时保持鼠标连续性 -->\r\n <div v-if=\"props.mode === 'hover'\" :class=\"ui.bridge()\"\r\n :style=\"{ margin: `-${props.content?.sideOffset ?? 8}px` }\" />\r\n\r\n <div v-if=\"arrow\" :class=\"ui.arrow()\" :style=\"arrowStyle\" />\r\n </div>\r\n </div>\r\n </Transition>\r\n\r\n </Teleport>\r\n </div>\r\n</template>",
18
+ "target": "web"
19
+ },
20
+ {
21
+ "path": "composables/clickoutside.ts",
22
+ "content": "let queue: any[] = []\r\n\r\nexport function pushToQueue(comp: any) {\r\n queue.push(comp)\r\n}\r\n\r\nexport function removeFromQueue(comp: any) {\r\n queue = queue.filter((item) => {\r\n return item.$.uid !== comp.$.uid\r\n })\r\n}\r\n\r\nexport function closeOther(comp: any) {\r\n queue.forEach((item) => {\r\n if (item.$.uid !== comp.$.uid) {\r\n if (item.$.exposed && item.$.exposed.close) {\r\n item.$.exposed.close()\r\n }\r\n }\r\n })\r\n}\r\n\r\nexport function closeOutside() {\r\n queue.forEach((item) => {\r\n if (item.$.exposed && item.$.exposed.close) {\r\n item.$.exposed.close()\r\n }\r\n })\r\n}\r\n",
23
+ "target": "uniapp"
24
+ },
25
+ {
26
+ "path": "composables/usePopover.ts",
27
+ "content": "import { getCurrentInstance, ref } from 'vue'\r\n\r\nexport function getRect(selector: string, all: boolean = false, context?: any) {\r\n return new Promise<any>((resolve) => {\r\n let query = uni.createSelectorQuery()\r\n if (context) {\r\n query = query.in(context)\r\n }\r\n query[all ? 'selectAll' : 'select'](selector)\r\n .boundingClientRect((rect) => {\r\n if (all && Array.isArray(rect) && rect.length) {\r\n resolve(rect)\r\n } else if (!all && rect) {\r\n resolve(rect)\r\n } else {\r\n resolve(null)\r\n }\r\n })\r\n .exec()\r\n })\r\n}\r\n\r\nexport function usePopover() {\r\n const { proxy } = getCurrentInstance() as any\r\n const popStyle = ref<string>('')\r\n const arrowStyle = ref<string>('')\r\n const showStyle = ref<string>('')\r\n\r\n const arrowSide = ref<'top' | 'bottom' | 'left' | 'right' | 'none'>('none')\r\n\r\n const popWidth = ref<number>(0)\r\n const popHeight = ref<number>(0)\r\n const left = ref<number>(0)\r\n const bottom = ref<number>(0)\r\n const width = ref<number>(0)\r\n const height = ref<number>(0)\r\n const top = ref<number>(0)\r\n\r\n function noop() { }\r\n\r\n function init(\r\n side: 'top' | 'right' | 'bottom' | 'left',\r\n align: 'start' | 'center' | 'end',\r\n visibleArrow: boolean,\r\n ) {\r\n if (visibleArrow) {\r\n if (side === 'top') arrowSide.value = 'bottom'\r\n else if (side === 'bottom') arrowSide.value = 'top'\r\n else if (side === 'left') arrowSide.value = 'right'\r\n else if (side === 'right') arrowSide.value = 'left'\r\n } else {\r\n arrowSide.value = 'none'\r\n }\r\n\r\n getRect('#target', false, proxy).then((rect) => {\r\n if (!rect) return\r\n left.value = rect.left as number\r\n bottom.value = rect.bottom as number\r\n width.value = rect.width as number\r\n height.value = rect.height as number\r\n top.value = rect.top as number\r\n })\r\n\r\n getRect('#pos', false, proxy).then((rect) => {\r\n if (!rect) return\r\n popWidth.value = rect.width as number\r\n popHeight.value = rect.height as number\r\n })\r\n }\r\n\r\n function control(\r\n side: 'top' | 'right' | 'bottom' | 'left',\r\n align: 'start' | 'center' | 'end',\r\n offset: number,\r\n visibleArrow: boolean\r\n ) {\r\n const arrowSize = visibleArrow ? 9 : 0\r\n let placement = side as string\r\n if (align === 'start') placement += '-start'\r\n else if (align === 'end') placement += '-end'\r\n\r\n const verticalX = width.value / 2\r\n const verticalY = arrowSize + height.value + offset + 5\r\n const horizontalX = width.value + arrowSize + offset + 5\r\n const horizontalY = height.value / 2\r\n\r\n const offsetX = (verticalX - 17 > 0 ? 0 : verticalX - 25)\r\n const offsetY = (horizontalY - 17 > 0 ? 0 : horizontalY - 25)\r\n\r\n const placements = new Map([\r\n ['top', [`left: ${verticalX}px; bottom: ${verticalY}px; transform: translateX(-50%);`, 'left: 50%; margin-left: -4.5px;']],\r\n ['top-start', [`left: ${offsetX}px; bottom: ${verticalY}px;`, 'left: 16px;']],\r\n ['top-end', [`right: ${offsetX}px; bottom: ${verticalY}px;`, 'right: 16px;']],\r\n\r\n ['bottom', [`left: ${verticalX}px; top: ${verticalY}px; transform: translateX(-50%);`, 'left: 50%; margin-left: -4.5px;']],\r\n ['bottom-start', [`left: ${offsetX}px; top: ${verticalY}px;`, 'left: 16px;']],\r\n ['bottom-end', [`right: ${offsetX}px; top: ${verticalY}px;`, 'right: 16px;']],\r\n\r\n ['left', [`right: ${horizontalX}px; top: ${horizontalY}px; transform: translateY(-50%);`, 'top: 50%; margin-top: -4.5px;']],\r\n ['left-start', [`right: ${horizontalX}px; top: ${offsetY}px;`, 'top: 16px;']],\r\n ['left-end', [`right: ${horizontalX}px; bottom: ${offsetY}px;`, 'bottom: 16px;']],\r\n\r\n ['right', [`left: ${horizontalX}px; top: ${horizontalY}px; transform: translateY(-50%);`, 'top: 50%; margin-top: -4.5px;']],\r\n ['right-start', [`left: ${horizontalX}px; top: ${offsetY}px;`, 'top: 16px;']],\r\n ['right-end', [`left: ${horizontalX}px; bottom: ${offsetY}px;`, 'bottom: 16px;']]\r\n ])\r\n\r\n popStyle.value = placements.get(placement)?.[0] || placements.get('bottom')![0]\r\n arrowStyle.value = placements.get(placement)?.[1] || placements.get('bottom')![1]\r\n }\r\n\r\n return { popStyle, arrowStyle, showStyle, arrowSide, init, control, noop }\r\n}\r\n",
28
+ "target": "uniapp"
29
+ },
30
+ {
31
+ "path": "composables/useQueue.ts",
32
+ "content": "import { type Ref, provide, ref } from 'vue'\r\n\r\nexport const queueKey = '__QUEUE_KEY__'\r\n\r\nexport interface Queue {\r\n queue: Ref<any[]>\r\n pushToQueue: (comp: any) => void\r\n removeFromQueue: (comp: any) => void\r\n closeOther: (comp: any) => void\r\n closeOutside: () => void\r\n}\r\n\r\nexport function useQueue() {\r\n const queue = ref<any[]>([])\r\n\r\n function pushToQueue(comp: any) {\r\n queue.value.push(comp)\r\n }\r\n\r\n function removeFromQueue(comp: any) {\r\n queue.value = queue.value.filter((item) => {\r\n return item.$.uid !== comp.$.uid\r\n })\r\n }\r\n\r\n function closeOther(comp: any) {\r\n queue.value.forEach((item) => {\r\n if (item.$.uid !== comp.$.uid) {\r\n if (item.$.exposed && item.$.exposed.close) {\r\n item.$.exposed.close()\r\n }\r\n }\r\n })\r\n }\r\n\r\n function closeOutside() {\r\n queue.value.forEach((item) => {\r\n if (item.$.exposed && item.$.exposed.close) {\r\n item.$.exposed.close()\r\n }\r\n })\r\n }\r\n\r\n provide(queueKey, {\r\n queue,\r\n pushToQueue,\r\n removeFromQueue,\r\n closeOther,\r\n closeOutside\r\n })\r\n\r\n return {\r\n closeOther,\r\n closeOutside\r\n }\r\n}\r\n",
33
+ "target": "uniapp"
34
+ },
35
+ {
36
+ "path": "index.ts",
37
+ "content": "import RebornPopover from './RebornPopover.vue'\r\n\r\nexport * from './types'\r\nexport { RebornPopover }\r\nexport default RebornPopover\r\n",
38
+ "target": "uniapp"
39
+ },
40
+ {
41
+ "path": "reborn-popover.config.ts",
42
+ "content": "export default {\r\n slots: {\r\n base: 'relative inline-block',\r\n target: 'inline-block',\r\n pos: 'absolute box-border min-h-[36px] z-[500] transition-opacity duration-200 rounded-md bg-white',\r\n hidden: 'left-[-100vw] invisible',\r\n container: 'relative text-sm leading-normal shadow-[0_2px_15px_0_rgba(0,0,0,0.1)]',\r\n inner: 'relative whitespace-nowrap p-3 rounded-md bg-white dark:bg-gray-8',\r\n arrow: 'absolute w-[9px] h-[9px] bg-white dark:bg-zinc-800 pointer-events-none', // requires rotation in style\r\n closeIcon: 'absolute text-[12px] right-[-8px] top-[-10px] scale-50 p-2.5',\r\n menu: 'inline-block px-3 whitespace-nowrap relative rounded-md bg-white dark:bg-zinc-800 z-[500]',\r\n menuInner: 'relative py-3 flex items-center border-t border-solid border-gray-200 dark:border-gray-700 first:border-0',\r\n },\r\n variants: {\r\n // Arrow directions\r\n arrowSide: {\r\n top: 'top-[-4.5px] rotate-45 border-t border-l border-gray-200 dark:border-gray-700',\r\n bottom: 'bottom-[-4.5px] rotate-45 border-b border-r border-gray-200 dark:border-gray-700',\r\n left: 'left-[-4.5px] rotate-45 border-b border-l border-gray-200 dark:border-gray-700',\r\n right: 'right-[-4.5px] rotate-45 border-t border-r border-gray-200 dark:border-gray-700',\r\n none: '',\r\n }\r\n },\r\n defaultVariants: {\r\n arrowSide: 'none' as 'none' | 'top' | 'bottom' | 'left' | 'right',\r\n }\r\n}\r\n",
43
+ "target": "uniapp"
44
+ },
45
+ {
46
+ "path": "RebornPopover.vue",
47
+ "content": "<template>\r\n <view :class=\"[ui.base({ class: customClass })]\" id=\"popover\" @click.stop=\"popover.noop\">\r\n <!-- 使用插槽时无法获取正确宽高 -->\r\n <view :class=\"cn(ui.pos(), ui.hidden())\" id=\"pos\">\r\n <view :class=\"ui.container()\">\r\n <view v-if=\"!useContentSlot && displayMode === 'normal'\" :class=\"ui.inner()\">\r\n {{ title }}\r\n </view>\r\n <view v-if=\"!useContentSlot && displayMode === 'menu' && typeof title === 'object'\" :class=\"ui.menu()\">\r\n <view v-for=\"(item, index) in title\" :key=\"index\" :class=\"ui.menuInner()\" @click=\"menuClick(index)\">\r\n <text>{{ item.content || item.title }}</text>\r\n </view>\r\n </view>\r\n </view>\r\n </view>\r\n\r\n <RebornTransition :custom-class=\"ui.pos()\" :custom-style=\"popover.popStyle.value\" :show=\"showPopover\"\r\n name=\"fade\" :duration=\"200\">\r\n <view :class=\"ui.container()\">\r\n <view v-if=\"props.arrow\" :class=\"[ui.arrow(), theme.variants.arrowSide[popover.arrowSide.value]]\"\r\n :style=\"popover.arrowStyle.value\" />\r\n\r\n <!-- 普通模式 -->\r\n <view v-if=\"!useContentSlot && displayMode === 'normal'\" :class=\"ui.inner()\">\r\n {{ title }}\r\n </view>\r\n\r\n <!-- 列表模式 -->\r\n <view v-if=\"!useContentSlot && displayMode === 'menu'\" :class=\"ui.menu()\">\r\n <view v-for=\"(item, index) in title\" :key=\"index\" :class=\"ui.menuInner()\" @click=\"menuClick(index)\">\r\n <view style=\"display: inline-block\">{{ typeof item === 'object' && (item.content || item.title)\r\n ? (item.content || item.title) : '' }}</view>\r\n </view>\r\n </view>\r\n\r\n <!-- 用户自定义样式 -->\r\n <view v-if=\"useContentSlot\">\r\n <slot name=\"content\" />\r\n </view>\r\n </view>\r\n\r\n </RebornTransition>\r\n\r\n <!-- Click-away mask to handle closing -->\r\n <view v-if=\"showPopover && dismissible\" class=\"fixed inset-0 z-490 bg-transparent\" @click.stop=\"close\"\r\n @touchmove.stop.prevent=\"() => { }\"></view>\r\n\r\n <view @click.stop=\"toggle\" :class=\"ui.target()\" id=\"target\">\r\n <slot />\r\n </view>\r\n </view>\r\n</template>\r\n\r\n<script lang=\"ts\">\r\nexport default {\r\n name: 'RebornPopover',\r\n options: {\r\n virtualHost: true,\r\n addGlobalClass: true,\r\n styleIsolation: 'shared'\r\n }\r\n}\r\n</script>\r\n\r\n<script lang=\"ts\" setup>\r\nimport { computed, getCurrentInstance, inject, onBeforeMount, onBeforeUnmount, onMounted, ref, watch, useSlots } from 'vue'\r\nimport { tv } from '@/lib/tv'\r\nimport { cn } from '@/lib/utils'\r\nimport { popoverProps, type PopoverExpose } from './types'\r\nimport { usePopover } from './composables/usePopover'\r\nimport { closeOther, pushToQueue, removeFromQueue } from './composables/clickoutside'\r\nimport { type Queue, queueKey } from './composables/useQueue'\r\nimport theme from './reborn-popover.config'\r\n\r\nimport RebornTransition from '@/components/reborn-transition/RebornTransition.vue'\r\n\r\nconst props = defineProps(popoverProps)\r\nconst emit = defineEmits(['update:modelValue', 'update:open', 'menuclick', 'change', 'open', 'close'])\r\nconst slots = useSlots()\r\nconst useContentSlot = computed(() => !!slots.content)\r\n\r\nconst b = tv(theme)\r\nconst uiOverrides = computed(() => props.ui || {})\r\n\r\nconst ui = computed(() => {\r\n const styles = b({})\r\n return {\r\n base: (opts?: { class?: any }) => styles.base({ class: cn(opts?.class, uiOverrides.value.base) }),\r\n target: (opts?: { class?: any }) => styles.target({ class: cn(opts?.class, uiOverrides.value.target) }),\r\n pos: (opts?: { class?: any }) => styles.pos({ class: cn(opts?.class, uiOverrides.value.pos) }),\r\n hidden: (opts?: { class?: any }) => styles.hidden({ class: cn(opts?.class, uiOverrides.value.hidden) }),\r\n container: (opts?: { class?: any }) => styles.container({ class: cn(opts?.class, uiOverrides.value.container) }),\r\n inner: (opts?: { class?: any }) => styles.inner({ class: cn(opts?.class, uiOverrides.value.inner) }),\r\n arrow: (opts?: { class?: any }) => styles.arrow({ class: cn(opts?.class, uiOverrides.value.arrow) }),\r\n closeIcon: (opts?: { class?: any }) => styles.closeIcon({ class: cn(opts?.class, uiOverrides.value.closeIcon) }),\r\n menu: (opts?: { class?: any }) => styles.menu({ class: cn(opts?.class, uiOverrides.value.menu) }),\r\n menuInner: (opts?: { class?: any }) => styles.menuInner({ class: cn(opts?.class, uiOverrides.value.menuInner) })\r\n }\r\n})\r\n\r\nconst queue = inject<Queue | null>(queueKey, null)\r\nconst { proxy } = getCurrentInstance() as any\r\nconst popover = usePopover()\r\n\r\nconst showPopover = ref<boolean>(props.open || props.defaultOpen)\r\n\r\nconst side = computed(() => props.content?.side || 'bottom')\r\nconst align = computed(() => props.content?.align || 'center')\r\nconst sideOffset = computed(() => props.content?.sideOffset || 0)\r\n\r\nwatch(\r\n () => props.title,\r\n (newVal) => {\r\n const mode = props.displayMode\r\n if (mode === 'normal' && typeof newVal !== 'string') {\r\n console.error('The title type must be a string type in normal mode')\r\n } else if (mode === 'menu' && !isArray(newVal)) {\r\n console.error('The title type must be an Array type in menu mode')\r\n }\r\n }\r\n)\r\n\r\nwatch([side, align, () => props.arrow], () => {\r\n popover.init(side.value, align.value, props.arrow)\r\n})\r\n\r\nwatch(\r\n () => props.open,\r\n (newValue) => {\r\n showPopover.value = newValue\r\n }\r\n)\r\n\r\nwatch(\r\n () => showPopover.value,\r\n (newValue) => {\r\n if (newValue) {\r\n popover.control(side.value, align.value, sideOffset.value, props.arrow)\r\n if (queue && queue.closeOther) {\r\n queue.closeOther(proxy)\r\n } else {\r\n closeOther(proxy)\r\n }\r\n }\r\n popover.showStyle.value = newValue ? 'display: inline-block;' : 'display: none;'\r\n emit('change', { show: newValue })\r\n emit(newValue ? 'open' : 'close')\r\n }\r\n)\r\n\r\nfunction isArray(value: any): value is Array<any> {\r\n if (typeof Array.isArray === 'function') {\r\n return Array.isArray(value)\r\n }\r\n return Object.prototype.toString.call(value) === '[object Array]'\r\n}\r\n\r\nonMounted(() => {\r\n popover.init(side.value, align.value, props.arrow)\r\n})\r\n\r\nonBeforeMount(() => {\r\n if (queue && queue.pushToQueue) {\r\n queue.pushToQueue(proxy)\r\n } else {\r\n pushToQueue(proxy)\r\n }\r\n popover.showStyle.value = showPopover.value ? 'opacity: 1;' : 'opacity: 0;'\r\n})\r\n\r\nonBeforeUnmount(() => {\r\n if (queue && queue.removeFromQueue) {\r\n queue.removeFromQueue(proxy)\r\n } else {\r\n removeFromQueue(proxy)\r\n }\r\n})\r\n\r\nfunction menuClick(index: number) {\r\n updateModelValue(false)\r\n if (isArray(props.title)) {\r\n emit('menuclick', {\r\n item: props.title[index],\r\n index\r\n })\r\n }\r\n}\r\n\r\nfunction toggle() {\r\n if (props.disabled) return\r\n updateModelValue(!showPopover.value)\r\n}\r\n\r\nfunction open() {\r\n updateModelValue(true)\r\n}\r\n\r\nfunction close() {\r\n updateModelValue(false)\r\n}\r\n\r\nfunction updateModelValue(value: boolean) {\r\n showPopover.value = value\r\n emit('update:modelValue', value)\r\n emit('update:open', value)\r\n}\r\n\r\ndefineExpose<PopoverExpose>({\r\n open,\r\n close\r\n})\r\n</script>\r\n",
48
+ "target": "uniapp"
49
+ },
50
+ {
51
+ "path": "types.ts",
52
+ "content": "import type { ExtractPropTypes, PropType } from 'vue'\r\n\r\nexport interface PopoverContentProps {\r\n /** Popover 相对于触发器的显示位置 */\r\n side?: 'top' | 'right' | 'bottom' | 'left'\r\n /** Popover 沿触发器轴线的对齐方式 */\r\n align?: 'start' | 'center' | 'end'\r\n /** Popover 与触发器之间的间距 */\r\n sideOffset?: number\r\n}\r\n\r\nexport const popoverProps = {\r\n /** 内容位置与偏移配置 */\r\n content: {\r\n type: Object as PropType<PopoverContentProps>,\r\n default: () => ({ side: 'bottom', align: 'center', sideOffset: 0 })\r\n },\r\n /** 是否显示箭头 */\r\n arrow: {\r\n type: Boolean,\r\n default: true\r\n },\r\n /** 是否将 Popover 渲染到指定的 DOM 节点 (对于 UniApp 意义不大,保留作 API 一致性) */\r\n portal: {\r\n type: [Boolean, String] as PropType<boolean | string>,\r\n default: false\r\n },\r\n /** 点击外部时是否关闭 Popover */\r\n dismissible: {\r\n type: Boolean,\r\n default: true\r\n },\r\n /** 受控显示状态 */\r\n open: {\r\n type: Boolean,\r\n default: false\r\n },\r\n /** 非受控默认显示状态 */\r\n defaultOpen: {\r\n type: Boolean,\r\n default: false\r\n },\r\n /** 是否显示遮罩层并捕获焦点 */\r\n modal: {\r\n type: Boolean,\r\n default: false\r\n },\r\n /** 延迟打开时间 (ms) - 适用于 hover 模式 */\r\n openDelay: {\r\n type: Number,\r\n default: 0\r\n },\r\n /** 延迟关闭时间 (ms) - 适用于 hover 模式,防止意外关闭 */\r\n closeDelay: {\r\n type: Number,\r\n default: 0\r\n },\r\n /** 额外的类名 */\r\n customClass: {\r\n type: [String, Object, Array] as PropType<any>,\r\n default: ''\r\n },\r\n /** UI 覆盖配置 */\r\n ui: {\r\n type: Object as PropType<any>,\r\n default: () => ({})\r\n },\r\n // 以下为针对 uni-app 扩展的特有属性\r\n /**\r\n * 是否禁用 popover\r\n */\r\n disabled: {\r\n type: Boolean,\r\n default: false\r\n },\r\n /**\r\n * 显示的内容,可以通过 prop 也可以通过 slot 传入\r\n */\r\n title: {\r\n type: [String, Object] as PropType<string | Record<string, any>[]>,\r\n default: ''\r\n },\r\n useContentSlot: {\r\n type: Boolean,\r\n default: true\r\n },\r\n displayMode: {\r\n type: String as PropType<'normal' | 'menu'>,\r\n default: 'normal'\r\n }\r\n}\r\n\r\nexport type PopoverProps = ExtractPropTypes<typeof popoverProps>\r\n\r\nexport interface PopoverExpose {\r\n open: () => void\r\n close: () => void\r\n}\r\n",
53
+ "target": "uniapp"
54
+ }
55
+ ],
56
+ "fileCount": 10,
57
+ "contentHash": "427cc075e9b1985838dcdf30557b02108421d607"
58
+ }
@@ -0,0 +1,23 @@
1
+ {
2
+ "name": "reborn-popup",
3
+ "dependencies": [],
4
+ "files": [
5
+ {
6
+ "path": "index.ts",
7
+ "content": "",
8
+ "target": "uniapp"
9
+ },
10
+ {
11
+ "path": "reborn-popup.config.ts",
12
+ "content": "const PopupDirection = ['center', 'top', 'right', 'bottom', 'left'] as const\nconst PopupColors = ['primary', 'secondary', 'success', 'info', 'warning', 'error', 'neutral'] as const\nexport type PopupPosition = (typeof PopupDirection)[number]\nexport type PopupColor = (typeof PopupColors)[number]\n\nexport default {\n slots: {\n base: 'fixed bg-white',\n inner: 'relative',\n draw: 'mx-auto mt-2 h-1 w-10 rounded-full bg-gray-3',\n header: 'flex items-center justify-between px-4 py-2',\n title: 'text-30 font-medium text-gray-9',\n closeIcon: 'text-gray-5 cursor-pointer',\n },\n variants: {\n position: {\n center: {\n base: 'top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2',\n },\n top: {\n base: 'top-0 left-0 right-0',\n },\n bottom: {\n base: 'bottom-0 left-0 right-0',\n },\n left: {\n base: 'top-0 left-0 bottom-0',\n },\n right: {\n base: 'top-0 right-0 bottom-0',\n },\n },\n color: {\n primary: {\n draw: 'bg-primary/50',\n },\n secondary: {\n draw: 'bg-secondary/50',\n },\n success: {\n draw: 'bg-success/50',\n },\n info: {\n draw: 'bg-info/50',\n },\n warning: {\n draw: 'bg-warning/50',\n },\n error: {\n draw: 'bg-error/50',\n },\n neutral: {\n draw: 'bg-neutral/50',\n },\n },\n round: {\n true: {\n base: '',\n },\n false: {\n base: '',\n },\n },\n },\n compoundVariants: [\n {\n position: 'center' as const,\n round: true,\n class: { base: 'rounded-lg' },\n },\n {\n position: 'top' as const,\n round: true,\n class: { base: 'rounded-b-2xl' },\n },\n {\n position: 'bottom' as const,\n round: true,\n class: { base: 'rounded-t-2xl' },\n },\n {\n position: 'left' as const,\n round: true,\n class: { base: 'rounded-r-2xl' },\n },\n {\n position: 'right' as const,\n round: true,\n class: { base: 'rounded-l-2xl' },\n },\n ],\n defaultVariants: {\n position: 'center' as const,\n color: 'neutral' as const,\n round: true,\n },\n}\n",
13
+ "target": "uniapp"
14
+ },
15
+ {
16
+ "path": "RebornPopup.vue",
17
+ "content": "<script lang=\"ts\">\nexport default {\n name: 'reborn-popup',\n options: {\n virtualHost: true,\n addGlobalClass: true,\n styleIsolation: 'shared'\n }\n}\n</script>\n\n<script lang=\"ts\" setup>\nimport { computed, onBeforeMount, ref, reactive, type PropType } from 'vue'\nimport RebornOverlay from '../reborn-overlay/RebornOverlay.vue'\nimport RebornTransition from '../reborn-transition/RebornTransition.vue'\nimport RebornRootPortal from '../reborn-root-portal/RebornRootPortal.vue'\nimport type { TransitionName } from '../reborn-transition/RebornTransition.vue'\nimport { tv } from '@/lib/tv'\nimport { getSystemInfo } from '@/lib/device'\nimport theme, { PopupPosition, PopupColor } from './reborn-popup.config'\n\ninterface PopupProps {\n customClass?: string\n customStyle?: string\n modelValue?: boolean\n position?: PopupPosition\n direction?: PopupPosition\n transition?: TransitionName\n closeOnClickModal?: boolean\n maskClosable?: boolean\n duration?: number | boolean\n modal?: boolean\n showMask?: boolean\n zIndex?: number\n overlayZIndex?: number\n modalStyle?: string\n safeAreaInsetBottom?: boolean\n safeAreaInsetTop?: boolean\n lazyRender?: boolean\n lockScroll?: boolean\n title?: string\n showHeader?: boolean\n showClose?: boolean\n swipeClose?: boolean\n swipeCloseThreshold?: number\n ui?: any\n rootPortal?: boolean\n enablePortal?: boolean\n color?: PopupColor\n round?: boolean\n}\n\nconst props = withDefaults(defineProps<PopupProps>(), {\n customClass: '',\n customStyle: '',\n modelValue: false,\n position: 'bottom',\n direction: 'bottom',\n closeOnClickModal: true,\n maskClosable: true,\n duration: 300,\n modal: true,\n showMask: true,\n zIndex: 10,\n overlayZIndex: 10,\n modalStyle: '',\n safeAreaInsetBottom: true,\n safeAreaInsetTop: true,\n lazyRender: true,\n lockScroll: true,\n title: '',\n showHeader: true,\n showClose: true,\n swipeClose: true,\n swipeCloseThreshold: 150,\n rootPortal: false,\n enablePortal: false,\n color: 'neutral',\n round: true,\n})\n\nconst emit = defineEmits([\n 'update:modelValue',\n 'before-enter',\n 'enter',\n 'before-leave',\n 'leave',\n 'after-leave',\n 'after-enter',\n 'click-modal',\n 'close'\n])\n\n// 兼容旧参数\nconst actualPosition = computed(() => props.direction || props.position)\nconst actualModal = computed(() => props.showMask ?? props.modal)\nconst actualCloseOnClick = computed(() => props.maskClosable ?? props.closeOnClickModal)\nconst actualZIndex = computed(() => props.overlayZIndex || props.zIndex)\nconst actualRootPortal = computed(() => props.enablePortal || props.rootPortal)\n\nconst transitionName = computed<TransitionName | TransitionName[]>(() => {\n if (props.transition) return props.transition\n if (actualPosition.value === 'center') return ['zoom-in', 'fade']\n if (actualPosition.value === 'left') return 'slide-left'\n if (actualPosition.value === 'right') return 'slide-right'\n if (actualPosition.value === 'bottom') return 'slide-up'\n if (actualPosition.value === 'top') return 'slide-down'\n return 'slide-up'\n})\n\nconst safeTop = ref<number>(0)\nconst safeBottom = ref<number>(0)\n\nconst swipe = reactive({\n isTouch: false,\n startY: 0,\n offsetY: 0,\n})\n\nconst isSwipeClose = computed(() => actualPosition.value === 'bottom' && props.swipeClose)\n\nconst style = computed(() => {\n let transform = ''\n if (swipe.isTouch && swipe.offsetY > 0) {\n transform = `transform: translateY(${swipe.offsetY}px);`\n }\n return `z-index:${actualZIndex.value}; padding-top: ${safeTop.value}px; padding-bottom: ${safeBottom.value}px; ${transform} ${props.customStyle}`\n})\n\nconst b = tv(theme)\nconst rootClass = computed(() => {\n return b({ position: actualPosition.value, color: props.color, class: props.customClass, round: props.round })\n})\n\nonBeforeMount(() => {\n const { safeArea, screenHeight, safeAreaInsets } = getSystemInfo()\n if (props.safeAreaInsetTop && safeArea && actualPosition.value === 'top') {\n // #ifdef MP-WEIXIN\n safeTop.value = safeArea.top || 44\n // #endif\n // #ifndef MP-WEIXIN\n safeTop.value = safeAreaInsets?.top || 44\n // #endif\n }\n if (props.safeAreaInsetBottom && safeArea && actualPosition.value === 'bottom') {\n // #ifdef MP-WEIXIN\n safeBottom.value = screenHeight - (safeArea.bottom || 0)\n // #endif\n // #ifndef MP-WEIXIN\n safeBottom.value = safeAreaInsets ? safeAreaInsets.bottom : 0\n // #endif\n }\n})\n\nfunction handleClickModal() {\n emit('click-modal')\n if (actualCloseOnClick.value) {\n close()\n }\n}\n\nfunction close() {\n emit('close')\n emit('update:modelValue', false)\n}\n\nfunction onTouchStart(e: any) {\n if (isSwipeClose.value) {\n swipe.isTouch = true\n swipe.startY = e.touches[0].clientY\n }\n}\n\nfunction onTouchMove(e: any) {\n if (swipe.isTouch) {\n const offsetY = e.touches[0].clientY - swipe.startY\n if (offsetY > 0) {\n swipe.offsetY = offsetY\n }\n }\n}\n\nfunction onTouchEnd() {\n if (swipe.isTouch) {\n swipe.isTouch = false\n if (swipe.offsetY > props.swipeCloseThreshold) {\n close()\n }\n swipe.offsetY = 0\n }\n}\n</script>\n\n<template>\n <reborn-root-portal v-if=\"actualRootPortal\">\n <view class=\"rb-popup-wrapper\">\n <reborn-overlay v-if=\"actualModal\" :model-value=\"modelValue\" :z-index=\"actualZIndex\" :lock-scroll=\"lockScroll\"\n :duration=\"duration\" :custom-style=\"modalStyle\" @click=\"handleClickModal\" />\n <reborn-transition :lazy-render=\"lazyRender\" :custom-class=\"rootClass.base()\" :custom-style=\"style\"\n :duration=\"duration\" :show=\"modelValue\" :name=\"transitionName\" @before-enter=\"emit('before-enter')\"\n @enter=\"emit('enter')\" @after-enter=\"emit('after-enter')\" @before-leave=\"emit('before-leave')\"\n @leave=\"emit('leave')\" @after-leave=\"emit('after-leave')\" @touchstart=\"onTouchStart\" @touchmove=\"onTouchMove\"\n @touchend=\"onTouchEnd\" @touchcancel=\"onTouchEnd\">\n <view :class=\"rootClass.inner()\">\n <view v-if=\"isSwipeClose\" :class=\"rootClass.draw()\" />\n <view v-if=\"showHeader\" :class=\"rootClass.header()\">\n <slot name=\"header\">\n <text :class=\"rootClass.title()\">{{ title }}</text>\n </slot>\n <text v-if=\"showClose\" :class=\"rootClass.closeIcon()\" class=\"i-lucide-x\" @click=\"close\" />\n </view>\n <slot />\n </view>\n </reborn-transition>\n </view>\n </reborn-root-portal>\n\n <view v-else class=\"rb-popup-wrapper\">\n <reborn-overlay v-if=\"actualModal\" :model-value=\"modelValue\" :z-index=\"actualZIndex\" :lock-scroll=\"lockScroll\"\n :duration=\"duration\" :custom-style=\"modalStyle\" @click=\"handleClickModal\" />\n <reborn-transition :lazy-render=\"lazyRender\" :custom-class=\"rootClass.base()\" :custom-style=\"style\"\n :duration=\"duration\" :show=\"modelValue\" :name=\"transitionName\" @before-enter=\"emit('before-enter')\"\n @enter=\"emit('enter')\" @after-enter=\"emit('after-enter')\" @before-leave=\"emit('before-leave')\"\n @leave=\"emit('leave')\" @after-leave=\"emit('after-leave')\" @touchstart=\"onTouchStart\" @touchmove=\"onTouchMove\"\n @touchend=\"onTouchEnd\" @touchcancel=\"onTouchEnd\">\n <view :class=\"rootClass.inner()\">\n <view v-if=\"isSwipeClose\" :class=\"rootClass.draw()\" />\n <view v-if=\"showHeader\" :class=\"rootClass.header()\">\n <slot name=\"header\">\n <text :class=\"rootClass.title()\">{{ title }}</text>\n </slot>\n <text v-if=\"showClose\" :class=\"rootClass.closeIcon()\" class=\"i-lucide-x\" @click=\"close\" />\n </view>\n <slot />\n </view>\n </reborn-transition>\n </view>\n</template>\n",
18
+ "target": "uniapp"
19
+ }
20
+ ],
21
+ "fileCount": 3,
22
+ "contentHash": "1472a90d5adefa2ca51d51c543e508c323d0f5a4"
23
+ }