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
@@ -7,7 +7,8 @@
7
7
  "files": [
8
8
  {
9
9
  "path": "index.ts",
10
- "content": "export { default as TabsRoot } from './TabsRoot.vue'\r\nexport { default as TabsList } from './TabsList.vue'\r\nexport { default as TabsTrigger } from './TabsTrigger.vue'\r\nexport { default as TabsContent } from './TabsContent.vue'\r\n"
10
+ "content": "export { default as TabsRoot } from './TabsRoot.vue'\r\nexport { default as TabsList } from './TabsList.vue'\r\nexport { default as TabsTrigger } from './TabsTrigger.vue'\r\nexport { default as TabsContent } from './TabsContent.vue'\r\n",
11
+ "target": "web"
11
12
  },
12
13
  {
13
14
  "path": "reborn-tabs.config.ts",
@@ -36,20 +37,25 @@
36
37
  },
37
38
  {
38
39
  "path": "context.ts",
39
- "content": "import type { InjectionKey, Ref } from 'vue'\r\n\r\nexport interface TabsContext {\r\n activeValue: Ref<string | number>\r\n upateValue: (value: string | number) => void\r\n}\r\n\r\nexport const TABS_INJECTION_KEY: InjectionKey<TabsContext> = Symbol('RebornTabs')\r\n",
40
+ "content": "import type { InjectionKey, Ref } from 'vue'\r\n\r\nexport interface TabsContext {\r\n activeValue: Ref<string | number>\r\n upateValue: (value: string | number) => void\r\n}\r\n\r\nexport const TABS_INJECTION_KEY: InjectionKey<TabsContext> = Symbol('RebornTabs')\r\n",
41
+ "target": "uniapp"
42
+ },
43
+ {
44
+ "path": "index.ts",
45
+ "content": "export { default as RebornText } from './RebornText.vue'\r\n",
40
46
  "target": "uniapp"
41
47
  },
42
48
  {
43
49
  "path": "reborn-tabs.config.ts",
44
- "content": "const size = ['xs', 'sm', 'md', 'lg', 'xl'] as const\r\nconst color = ['primary', 'secondary', 'success', 'info', 'warning', 'error', 'neutral'] as const\r\nconst variant = ['line', 'card'] as const\r\n\r\nexport { size as TabsSizes, color as TabsColors, variant as TabsVariants }\r\n\r\nexport default {\r\n slots: {\r\n tabs: '',\r\n scrollbar: 'flex flex-row w-full h-full',\r\n inner: 'flex flex-row relative',\r\n item: 'flex flex-row items-center justify-center relative z-10 transition-colors group',\r\n text: 'font-light whitespace-nowrap',\r\n line: 'absolute left-0 bottom-0 h-[4px] rounded-full bg-[length:200%] transition-all duration-300 ease-[cubic-bezier(0.4,0,0.2,1)] will-change-transform',\r\n slider: 'absolute top-0 left-0 h-full w-full bg-primary text-white rounded-md transition-all duration-300 ease-[cubic-bezier(0.4,0,0.2,1)] will-change-transform z-0',\r\n active: 'font-medium whitespace-nowrap'\r\n },\r\n variants: {\r\n variant: {\r\n line: {\r\n inner: '',\r\n item: 'hover:text-gray-900 dark:hover:text-gray-100', // Removed static border-b-2\r\n text: 'text-gray-7',\r\n active: 'font-semibold text-gray-8'\r\n },\r\n card: {\r\n inner: '',\r\n item: 'rounded-md',\r\n text: 'text-gray-7',\r\n active: 'text-white'\r\n }\r\n },\r\n color: {\r\n primary: {\r\n // item: \"data-[state=active]:text-primary\",\r\n line: 'bg-[linear-gradient(90deg,var(--color-red-4),var(--color-red-2),var(--color-orange-3),var(--color-orange-1))]',\r\n slider: \"bg-primary\",\r\n },\r\n secondary: {\r\n // item: \"data-[state=active]:text-secondary\",\r\n line: 'bg-[linear-gradient(90deg,var(--color-gray-4),var(--color-gray-2),var(--color-orange-3),var(--color-orange-1))]',\r\n slider: \"bg-secondary\",\r\n },\r\n success: {\r\n // item: \"data-[state=active]:text-success\",\r\n line: 'bg-[linear-gradient(90deg,var(--color-green-4),var(--color-green-2),var(--color-orange-3),var(--color-orange-1))]',\r\n slider: \"bg-success\",\r\n },\r\n info: {\r\n // item: \"data-[state=active]:text-info\",\r\n line: 'bg-[linear-gradient(90deg,var(--color-blue-4),var(--color-blue-2),var(--color-orange-3),var(--color-orange-1))]',\r\n slider: \"bg-info\",\r\n },\r\n warning: {\r\n // item: \"data-[state=active]:text-warning\",\r\n line: 'bg-[linear-gradient(90deg,var(--color-orange-4),var(--color-orange-2),var(--color-red-3),var(--color-red-1))]',\r\n slider: \"bg-warning\",\r\n },\r\n error: {\r\n // item: \"data-[state=active]:text-error\",\r\n line: 'bg-[linear-gradient(90deg,var(--color-red-4),var(--color-red-2),var(--color-orange-3),var(--color-orange-1))]',\r\n slider: \"bg-error\",\r\n },\r\n neutral: {\r\n // item: \"data-[state=active]:text-neutral\",\r\n line: 'bg-[linear-gradient(90deg,var(--color-gray-4),var(--color-gray-2),var(--color-orange-3),var(--color-orange-1))]',\r\n slider: \"bg-neutral\",\r\n }\r\n },\r\n size: {\r\n xs: {\r\n scrollbar: 'py-1.5',\r\n item: 'px-2 text-xs',\r\n text: 'text-xs',\r\n },\r\n sm: {\r\n scrollbar: 'py-2',\r\n item: 'px-3 text-sm',\r\n text: 'text-sm',\r\n },\r\n md: {\r\n scrollbar: 'py-2.5',\r\n item: 'px-4 text-base',\r\n text: 'text-base',\r\n },\r\n lg: {\r\n scrollbar: 'py-3',\r\n item: 'px-5 text-lg',\r\n text: 'text-lg',\r\n },\r\n xl: {\r\n scrollbar: 'py-3.5',\r\n item: 'px-6 text-xl',\r\n text: 'text-xl',\r\n }\r\n },\r\n fill: {\r\n true: {\r\n inner: 'w-full',\r\n item: 'flex-1'\r\n }\r\n },\r\n justify: {\r\n start: {\r\n inner: 'justify-start'\r\n },\r\n center: {\r\n inner: 'justify-center'\r\n },\r\n end: {\r\n inner: 'justify-end'\r\n }\r\n }\r\n },\r\n // compoundVariants: [\r\n // {\r\n // variant: 'line' as (typeof variant)[number],\r\n // color: 'primary' as (typeof color)[number],\r\n // class: {\r\n // item: 'data-[state=active]:text-primary', // Removed static border-primary\r\n // active: 'text-primary'\r\n // }\r\n // },\r\n // {\r\n // variant: 'line' as (typeof variant)[number],\r\n // color: 'secondary' as (typeof color)[number],\r\n // class: {\r\n // item: 'data-[state=active]:text-secondary', // Removed static border-primary\r\n // active: 'text-secondary'\r\n // }\r\n // },\r\n // {\r\n // variant: 'line' as (typeof variant)[number],\r\n // color: 'success' as (typeof color)[number],\r\n // class: {\r\n // item: 'data-[state=active]:text-success', // Removed static border-primary\r\n // active: 'text-success'\r\n // }\r\n // },\r\n // {\r\n // variant: 'line' as (typeof variant)[number],\r\n // color: 'info' as (typeof color)[number],\r\n // class: {\r\n // item: 'data-[state=active]:text-info', // Removed static border-primary\r\n // active: 'text-info'\r\n // }\r\n // },\r\n // {\r\n // variant: 'line' as (typeof variant)[number],\r\n // color: 'warning' as (typeof color)[number],\r\n // class: {\r\n // item: 'data-[state=active]:text-warning', // Removed static border-primary\r\n // active: 'text-warning'\r\n // }\r\n // },\r\n // {\r\n // variant: 'line' as (typeof variant)[number],\r\n // color: 'error' as (typeof color)[number],\r\n // class: {\r\n // item: 'data-[state=active]:text-error', // Removed static border-primary\r\n // active: 'text-error'\r\n // }\r\n // },\r\n // {\r\n // variant: 'line' as (typeof variant)[number],\r\n // color: 'neutral' as (typeof color)[number],\r\n // class: {\r\n // item: 'data-[state=active]:text-neutral', // Removed static border-primary\r\n // active: 'text-neutral'\r\n // }\r\n // },\r\n // ],\r\n defaultVariants: {\r\n variant: 'line' as (typeof variant)[number],\r\n color: 'primary' as (typeof color)[number],\r\n size: 'xs' as (typeof size)[number],\r\n fill: false,\r\n justify: 'start' as 'start' | 'center' | 'end'\r\n }\r\n}\r\n",
50
+ "content": "const size = ['xs', 'sm', 'md', 'lg', 'xl'] as const\r\nconst color = ['primary', 'secondary', 'success', 'info', 'warning', 'error', 'neutral'] as const\r\nconst variant = ['line', 'card'] as const\r\n\r\nexport { color as TabsColors, size as TabsSizes, variant as TabsVariants }\r\n\r\nexport default {\r\n slots: {\r\n tabs: '',\r\n scrollbar: 'flex flex-row w-full h-full overflow-x-scroll',\r\n inner: 'flex flex-row relative',\r\n item: 'flex flex-row items-center justify-center relative z-10 transition-colors group',\r\n text: 'font-light whitespace-nowrap',\r\n line: 'absolute left-0 bottom-0 h-[4px] rounded-full bg-[length:200%] transition-all duration-300 ease-[cubic-bezier(0.4,0,0.2,1)] will-change-transform',\r\n slider: 'absolute top-0 left-0 h-full w-full bg-primary text-white rounded-md transition-all duration-300 ease-[cubic-bezier(0.4,0,0.2,1)] will-change-transform z-0',\r\n active: 'font-medium whitespace-nowrap',\r\n },\r\n variants: {\r\n variant: {\r\n line: {\r\n inner: '',\r\n item: 'hover:text-gray-900 dark:hover:text-gray-100', // Removed static border-b-2\r\n text: 'text-gray-7',\r\n active: 'font-semibold text-gray-8',\r\n },\r\n card: {\r\n inner: '',\r\n item: 'rounded-md',\r\n text: 'text-gray-7',\r\n active: 'text-white',\r\n },\r\n },\r\n color: {\r\n primary: {\r\n // item: \"data-[state=active]:text-primary\",\r\n line: 'bg-[linear-gradient(90deg,var(--color-red-4),var(--color-red-2),var(--color-orange-3),var(--color-orange-1))]',\r\n slider: 'bg-primary',\r\n },\r\n secondary: {\r\n // item: \"data-[state=active]:text-secondary\",\r\n line: 'bg-[linear-gradient(90deg,var(--color-gray-4),var(--color-gray-2),var(--color-orange-3),var(--color-orange-1))]',\r\n slider: 'bg-secondary',\r\n },\r\n success: {\r\n // item: \"data-[state=active]:text-success\",\r\n line: 'bg-[linear-gradient(90deg,var(--color-green-4),var(--color-green-2),var(--color-orange-3),var(--color-orange-1))]',\r\n slider: 'bg-success',\r\n },\r\n info: {\r\n // item: \"data-[state=active]:text-info\",\r\n line: 'bg-[linear-gradient(90deg,var(--color-blue-4),var(--color-blue-2),var(--color-orange-3),var(--color-orange-1))]',\r\n slider: 'bg-info',\r\n },\r\n warning: {\r\n // item: \"data-[state=active]:text-warning\",\r\n line: 'bg-[linear-gradient(90deg,var(--color-orange-4),var(--color-orange-2),var(--color-red-3),var(--color-red-1))]',\r\n slider: 'bg-warning',\r\n },\r\n error: {\r\n // item: \"data-[state=active]:text-error\",\r\n line: 'bg-[linear-gradient(90deg,var(--color-red-4),var(--color-red-2),var(--color-orange-3),var(--color-orange-1))]',\r\n slider: 'bg-error',\r\n },\r\n neutral: {\r\n // item: \"data-[state=active]:text-neutral\",\r\n line: 'bg-[linear-gradient(90deg,var(--color-gray-4),var(--color-gray-2),var(--color-orange-3),var(--color-orange-1))]',\r\n slider: 'bg-neutral',\r\n },\r\n },\r\n size: {\r\n xs: {\r\n scrollbar: 'py-1.5',\r\n item: 'px-2 text-xs',\r\n text: 'text-xs',\r\n },\r\n sm: {\r\n scrollbar: 'py-2',\r\n item: 'px-3 text-sm',\r\n text: 'text-sm',\r\n },\r\n md: {\r\n scrollbar: 'py-2.5',\r\n item: 'px-4 text-base',\r\n text: 'text-base',\r\n },\r\n lg: {\r\n scrollbar: 'py-3',\r\n item: 'px-5 text-lg',\r\n text: 'text-lg',\r\n },\r\n xl: {\r\n scrollbar: 'py-3.5',\r\n item: 'px-6 text-xl',\r\n text: 'text-xl',\r\n },\r\n },\r\n fill: {\r\n true: {\r\n inner: 'w-full',\r\n item: 'flex-1',\r\n },\r\n },\r\n justify: {\r\n start: {\r\n inner: 'justify-start',\r\n },\r\n center: {\r\n inner: 'justify-center',\r\n },\r\n end: {\r\n inner: 'justify-end',\r\n },\r\n },\r\n },\r\n defaultVariants: {\r\n variant: 'line' as (typeof variant)[number],\r\n color: 'primary' as (typeof color)[number],\r\n size: 'xs' as (typeof size)[number],\r\n fill: false,\r\n justify: 'start' as 'start' | 'center' | 'end',\r\n },\r\n}\r\n",
45
51
  "target": "uniapp"
46
52
  },
47
53
  {
48
54
  "path": "RebornTabs.vue",
49
- "content": "<script lang=\"ts\">\r\n// Tabs组件的单项类型\r\nexport interface TabsItem {\r\n label: string; // 标签文本\r\n value: string | number; // 对应值\r\n disabled?: boolean; // 是否禁用\r\n};\r\n\r\n</script>\r\n<script setup lang=\"ts\">\r\nimport { computed, getCurrentInstance, nextTick, onMounted, ref, watch } from \"vue\";\r\nimport { tv } from '@/lib/tv'\r\nimport { cn } from '@/lib/utils'\r\nimport theme, { TabsColors, TabsSizes, TabsVariants } from \"./reborn-tabs.config\";\r\n\r\n\r\ninterface Item extends TabsItem {\r\n isActive: boolean;\r\n}\r\n\r\ninterface Props {\r\n customClass?: string;\r\n // modelValue: string | number;\r\n list: TabsItem[];\r\n fill?: boolean; // 是否填充标签\r\n color?: typeof TabsColors[number];\r\n variant?: typeof TabsVariants[number]; // 标签类型\r\n size?: typeof TabsSizes[number]; // 标签大小\r\n disabled?: boolean; // 是否禁用\r\n justify?: 'start' | 'center' | 'end'; // 对齐方式\r\n ui?: {\r\n tabs?: string;\r\n scrollbar?: string;\r\n inner?: string;\r\n item?: string;\r\n text?: string;\r\n line?: string;\r\n slider?: string;\r\n active?: string;\r\n }\r\n}\r\n\r\ndefineSlots<{\r\n item(props: { item: Item; active: boolean }): any;\r\n}>();\r\n\r\n// 定义事件发射器\r\nconst emit = defineEmits([\"update:modelValue\", \"change\"]);\r\n\r\nconst props = withDefaults(defineProps<Props>(), {\r\n list: () => [],\r\n fill: false,\r\n color: TabsColors[0],\r\n variant: TabsVariants[0],\r\n size: TabsSizes[0],\r\n disabled: false,\r\n justify: 'start'\r\n});\r\nconst active = defineModel<string | number>('modelValue')\r\n\r\n// 获取当前组件实例的proxy对象\r\nconst { proxy } = getCurrentInstance()!;\r\nconst b = tv(theme)\r\n\r\nconst color = toRef(props, 'color')\r\nconst variant = toRef(props, 'variant')\r\nconst size = toRef(props, 'size')\r\nconst uiOverrides = computed(() => props.ui || {});\r\nconst ui = computed(() => b({\r\n color: color.value,\r\n variant: variant.value,\r\n size: size.value,\r\n fill: props.fill,\r\n justify: props.justify\r\n}))\r\n\r\n// 当前选中的标签值\r\n// const active = ref(props.modelValue);\r\n\r\n// 计算标签列表,增加isActive和disabled属性,便于渲染和状态判断\r\nconst list = computed(() =>\r\n props.list.map((e) => {\r\n return {\r\n label: e.label,\r\n value: e.value,\r\n // 如果未传disabled则默认为false\r\n disabled: e.disabled ?? false,\r\n // 判断当前标签是否为激活状态\r\n isActive: e.value == active.value\r\n } as Item;\r\n })\r\n);\r\n// 切换标签时触发,参数为索引\r\nasync function change(index: number) {\r\n // 如果整个Tabs被禁用,则不响应点击\r\n if (props.disabled) {\r\n return false;\r\n }\r\n\r\n // 获取当前点击标签的值\r\n const { value, disabled } = list.value[index];\r\n\r\n // 如果标签被禁用,则不响应点击\r\n if (disabled) {\r\n return false;\r\n }\r\n // 触发v-model的更新\r\n emit(\"update:modelValue\", value, list.value[index], index);\r\n // 触发change事件\r\n emit(\"change\", value, list.value[index], index);\r\n}\r\n\r\n// tab区域宽度\r\nconst tabWidth = ref(0);\r\n// tab区域左侧偏移\r\nconst tabLeft = ref(0);\r\n// 下划线左侧偏移\r\nconst lineLeft = ref(0);\r\n// 下划线宽度\r\nconst lineWidth = ref(0);\r\n// 滑块左侧偏移\r\nconst sliderLeft = ref(0);\r\n// 滑块宽度\r\nconst sliderWidth = ref(0);\r\n// 滚动条左侧偏移\r\nconst scrollLeft = ref(0);\r\n\r\n// 单个标签的位置信息类型,包含left和width\r\ntype ItemRect = {\r\n left: number;\r\n width: number;\r\n};\r\n\r\n// 所有标签的位置信息,响应式数组\r\nconst itemRects = ref<ItemRect[]>([]);\r\nconst textRects = ref<ItemRect[]>([]);\r\n\r\n// 获取当前选中标签的下标,未找到则返回0\r\nfunction getIndex() {\r\n const index = list.value.findIndex((e) => e.isActive);\r\n return index == -1 ? 0 : index;\r\n}\r\n\r\n// 更新下划线、滑块、滚动条等位置\r\n// 更新下划线、滑块、滚动条等位置\r\nfunction updatePosition() {\r\n nextTick(() => {\r\n if (itemRects.value?.length) {\r\n const index = getIndex();\r\n // 获取当前选中标签的位置信息\r\n const item = itemRects.value[index];\r\n const text = textRects.value?.[index];\r\n\r\n if (!!item) {\r\n // 计算滚动条偏移,使选中项居中\r\n // x = Content_Center - Container_Half_Width\r\n // 由于 item.left 已经标准化(相对于内容区域),公式简化为:\r\n let x = item.left - (tabWidth.value - item.width) / 2;\r\n // 防止滚动条偏移为负\r\n if (x < 0) {\r\n x = 0;\r\n }\r\n // 设置滚动条偏移\r\n scrollLeft.value = x;\r\n\r\n // 设置下划线偏移 (Text based)\r\n if (text) {\r\n // text.left 已经是 relative to content\r\n lineLeft.value = text.left;\r\n lineWidth.value = text.width;\r\n } else {\r\n // Fallback\r\n lineLeft.value = item.left + item.width / 2 - 20 / 2;\r\n lineWidth.value = 20;\r\n }\r\n\r\n // 设置滑块左侧偏移\r\n sliderLeft.value = item.left;\r\n // 设置滑块宽度\r\n sliderWidth.value = item.width;\r\n }\r\n }\r\n });\r\n}\r\n\r\n// 获取所有标签的位置信息,便于后续计算\r\nfunction getRects() {\r\n // 创建选择器查询\r\n const query = uni.createSelectorQuery().in(proxy);\r\n\r\n query.selectAll(\".reborn-tabs__item\").boundingClientRect();\r\n query.selectAll(\".reborn-tabs__text\").boundingClientRect();\r\n query.select(\".reborn-tabs__scroll-view\").scrollOffset(() => { });\r\n\r\n query.exec((nodes: any) => {\r\n const scrollNode = nodes?.[2];\r\n const currentScrollLeft = scrollNode?.scrollLeft ?? 0;\r\n\r\n // 解析查询结果, 转换为相对于内容区域的坐标\r\n // Relative_Left = Viewport_Left - Tab_Viewport_Left + Current_Scroll\r\n itemRects.value = nodes?.[0]?.map((e: any) => ({\r\n left: (e.left ?? 0) - tabLeft.value + currentScrollLeft,\r\n width: e.width ?? 0\r\n } as ItemRect)) ?? [];\r\n\r\n textRects.value = nodes?.[1]?.map((e: any) => ({\r\n left: (e.left ?? 0) - tabLeft.value + currentScrollLeft,\r\n width: e.width ?? 0\r\n } as ItemRect)) ?? [];\r\n\r\n // 更新下划线、滑块等位置\r\n updatePosition();\r\n });\r\n}\r\n\r\n// 刷新tab区域的宽度和位置信息\r\nfunction refresh() {\r\n // 使用 setTimeout 替代 nextTick,确保在视图层渲染完成后再获取布局信息\r\n // 尤其是在修改 size 等导致几何变化的属性时,需要一定延迟\r\n setTimeout(() => {\r\n // 创建选择器查询\r\n uni.createSelectorQuery()\r\n // 作用域限定为当前组件\r\n .in(proxy)\r\n // 选择tab容器\r\n .select(\".reborn-tabs\")\r\n // 获取容器的left和width\r\n .boundingClientRect((node: any) => {\r\n // 设置tab左侧偏移\r\n tabLeft.value = node?.left ?? 0;\r\n // 设置tab宽度\r\n tabWidth.value = node?.width ?? 0;\r\n\r\n // 获取所有标签的位置信息\r\n getRects();\r\n })\r\n .exec();\r\n }, 50);\r\n}\r\n\r\n\r\n\r\nonMounted(() => {\r\n watch(\r\n () => active.value,\r\n () => {\r\n // 更新下划线、滑块等位置\r\n updatePosition();\r\n },\r\n {\r\n // 立即执行一次\r\n immediate: true\r\n }\r\n );\r\n\r\n // 监听标签列表变化,刷新布局\r\n watch(\r\n computed(() => props.list),\r\n () => {\r\n refresh();\r\n },\r\n {\r\n immediate: true,\r\n deep: true\r\n }\r\n );\r\n\r\n // 监听其他影响布局的属性的变化\r\n watch(\r\n [\r\n computed(() => props.size),\r\n computed(() => props.variant),\r\n computed(() => props.fill),\r\n computed(() => props.justify)\r\n ],\r\n () => {\r\n refresh();\r\n }\r\n );\r\n});\r\n</script>\r\n\r\n<template>\r\n <view class=\"reborn-tabs\" :class=\"ui.tabs({ class: cn(props.customClass, uiOverrides.tabs) })\">\r\n <scroll-view class=\"reborn-tabs__scroll-view\" :class=\"ui.scrollbar({ class: uiOverrides.scrollbar })\"\r\n :scroll-with-animation=\"true\" :scroll-x=\"true\" direction=\"horizontal\" :scroll-left=\"scrollLeft\"\r\n :show-scrollbar=\"false\">\r\n <view :class=\"ui.inner({ class: uiOverrides.inner })\">\r\n <view class=\"reborn-tabs__item\" :class=\"ui.item({ class: uiOverrides.item })\"\r\n :data-state=\"item.isActive ? 'active' : 'inactive'\"\r\n :data-disabled=\"item.disabled ? 'true' : undefined\" v-for=\"(item, index) in list\" :key=\"index\"\r\n @tap=\"change(index)\">\r\n <slot name=\"item\" :item=\"item\" :active=\"item.isActive\">\r\n <text v-if=\"item.isActive\" class=\"reborn-tabs__text\"\r\n :class=\"ui.active({ class: cn(uiOverrides.active, item.disabled || props.disabled ? 'text-gray-5 cursor-not-allowed' : '') })\">\r\n {{ item.label }}\r\n </text>\r\n <text v-else class=\"reborn-tabs__text\"\r\n :class=\"ui.text({ class: cn(uiOverrides.text, item.disabled || props.disabled ? 'text-gray-5 cursor-not-allowed' : '') })\">\r\n {{ item.label }}\r\n </text>\r\n </slot>\r\n </view>\r\n <template v-if=\"lineLeft > 0\">\r\n <view :class=\"ui.line({ class: uiOverrides.line })\"\r\n :style=\"{ transform: `translateX(${lineLeft}px)`, width: `${lineWidth}px` }\"\r\n v-if=\"variant == 'line'\"></view>\r\n <view :class=\"ui.slider({ class: uiOverrides.slider })\"\r\n :style=\"{ transform: `translateX(${sliderLeft}px)`, width: `${sliderWidth}px` }\"\r\n v-if=\"variant == 'card'\"></view>\r\n </template>\r\n </view>\r\n </scroll-view>\r\n </view>\r\n</template>",
55
+ "content": "<script lang=\"ts\">\r\n// Tabs组件的单项类型\r\n</script>\r\n\r\n<script setup lang=\"ts\">\r\nimport { computed, getCurrentInstance, nextTick, onMounted, ref, watch } from 'vue'\r\nimport { tv } from '@/lib/tv'\r\nimport { cn } from '@/lib/utils'\r\nimport theme, { TabsColors, TabsSizes, TabsVariants } from './reborn-tabs.config'\r\n\r\nexport interface TabsItem {\r\n label: string // 标签文本\r\n value: string | number // 对应值\r\n disabled?: boolean // 是否禁用\r\n};\r\n\r\ninterface Item extends TabsItem {\r\n isActive: boolean\r\n}\r\n\r\ninterface Props {\r\n customClass?: string\r\n // modelValue: string | number;\r\n list: TabsItem[]\r\n fill?: boolean // 是否填充标签\r\n color?: typeof TabsColors[number]\r\n variant?: typeof TabsVariants[number] // 标签类型\r\n size?: typeof TabsSizes[number] // 标签大小\r\n disabled?: boolean // 是否禁用\r\n justify?: 'start' | 'center' | 'end' // 对齐方式\r\n ui?: {\r\n tabs?: string\r\n scrollbar?: string\r\n inner?: string\r\n item?: string\r\n text?: string\r\n line?: string\r\n slider?: string\r\n active?: string\r\n }\r\n}\r\n\r\nconst props = withDefaults(defineProps<Props>(), {\r\n list: () => [],\r\n fill: false,\r\n color: TabsColors[0],\r\n variant: TabsVariants[0],\r\n size: TabsSizes[0],\r\n disabled: false,\r\n justify: 'start',\r\n})\r\n\r\n// 定义事件发射器\r\nconst emit = defineEmits(['update:modelValue', 'change'])\r\n\r\ndefineSlots<{\r\n item: (props: { item: Item, active: boolean }) => any\r\n}>()\r\n\r\nconst active = defineModel<string | number>('modelValue')\r\n\r\n// 获取当前组件实例的proxy对象\r\nconst { proxy } = getCurrentInstance()!\r\nconst b = tv(theme)\r\n\r\nconst color = toRef(props, 'color')\r\nconst variant = toRef(props, 'variant')\r\nconst size = toRef(props, 'size')\r\nconst uiOverrides = computed(() => props.ui || {})\r\nconst ui = computed(() => b({\r\n color: color.value,\r\n variant: variant.value,\r\n size: size.value,\r\n fill: props.fill,\r\n justify: props.justify,\r\n}))\r\n\r\n// 当前选中的标签值\r\n// const active = ref(props.modelValue);\r\n\r\n// 计算标签列表,增加isActive和disabled属性,便于渲染和状态判断\r\nconst list = computed(() =>\r\n props.list.map((e) => {\r\n return {\r\n label: e.label,\r\n value: e.value,\r\n // 如果未传disabled则默认为false\r\n disabled: e.disabled ?? false,\r\n // 判断当前标签是否为激活状态\r\n isActive: e.value == active.value,\r\n } as Item\r\n }),\r\n)\r\n// 切换标签时触发,参数为索引\r\nasync function change(index: number) {\r\n // 如果整个Tabs被禁用,则不响应点击\r\n if (props.disabled) {\r\n return false\r\n }\r\n\r\n // 获取当前点击标签的值\r\n const { value, disabled } = list.value[index]\r\n\r\n // 如果标签被禁用,则不响应点击\r\n if (disabled) {\r\n return false\r\n }\r\n // 触发v-model的更新\r\n emit('update:modelValue', value, list.value[index], index)\r\n // 触发change事件\r\n emit('change', value, list.value[index], index)\r\n}\r\n\r\n// tab区域宽度\r\nconst tabWidth = ref(0)\r\n// tab区域左侧偏移\r\nconst tabLeft = ref(0)\r\n// 下划线左侧偏移\r\nconst lineLeft = ref(0)\r\n// 下划线宽度\r\nconst lineWidth = ref(0)\r\n// 滑块左侧偏移\r\nconst sliderLeft = ref(0)\r\n// 滑块宽度\r\nconst sliderWidth = ref(0)\r\n// 滚动条左侧偏移\r\nconst scrollLeft = ref(0)\r\n\r\n// 单个标签的位置信息类型,包含left和width\r\ninterface ItemRect {\r\n left: number\r\n width: number\r\n}\r\n\r\n// 所有标签的位置信息,响应式数组\r\nconst itemRects = ref<ItemRect[]>([])\r\nconst textRects = ref<ItemRect[]>([])\r\n\r\n// 获取当前选中标签的下标,未找到则返回0\r\nfunction getIndex() {\r\n const index = list.value.findIndex(e => e.isActive)\r\n return index == -1 ? 0 : index\r\n}\r\n\r\n// 更新下划线、滑块、滚动条等位置\r\n// 更新下划线、滑块、滚动条等位置\r\nfunction updatePosition() {\r\n nextTick(() => {\r\n if (itemRects.value?.length) {\r\n const index = getIndex()\r\n // 获取当前选中标签的位置信息\r\n const item = itemRects.value[index]\r\n const text = textRects.value?.[index]\r\n\r\n if (item) {\r\n // 计算滚动条偏移,使选中项居中\r\n // x = Content_Center - Container_Half_Width\r\n // 由于 item.left 已经标准化(相对于内容区域),公式简化为:\r\n let x = item.left - (tabWidth.value - item.width) / 2\r\n // 防止滚动条偏移为负\r\n if (x < 0) {\r\n x = 0\r\n }\r\n // 设置滚动条偏移\r\n scrollLeft.value = x\r\n\r\n // 设置下划线偏移 (Text based)\r\n if (text) {\r\n // text.left 已经是 relative to content\r\n lineLeft.value = text.left\r\n lineWidth.value = text.width\r\n }\r\n else {\r\n // Fallback\r\n lineLeft.value = item.left + item.width / 2 - 20 / 2\r\n lineWidth.value = 20\r\n }\r\n\r\n // 设置滑块左侧偏移\r\n sliderLeft.value = item.left\r\n // 设置滑块宽度\r\n sliderWidth.value = item.width\r\n }\r\n }\r\n })\r\n}\r\n\r\n// 获取所有标签的位置信息,便于后续计算\r\nfunction getRects() {\r\n // 创建选择器查询\r\n const query = uni.createSelectorQuery().in(proxy)\r\n\r\n query.selectAll('.reborn-tabs__item').boundingClientRect()\r\n query.selectAll('.reborn-tabs__text').boundingClientRect()\r\n query.select('.reborn-tabs__scroll-view').scrollOffset(() => { })\r\n\r\n query.exec((nodes: any) => {\r\n const scrollNode = nodes?.[2]\r\n const currentScrollLeft = scrollNode?.scrollLeft ?? 0\r\n\r\n // 解析查询结果, 转换为相对于内容区域的坐标\r\n // Relative_Left = Viewport_Left - Tab_Viewport_Left + Current_Scroll\r\n itemRects.value = nodes?.[0]?.map((e: any) => ({\r\n left: (e.left ?? 0) - tabLeft.value + currentScrollLeft,\r\n width: e.width ?? 0,\r\n } as ItemRect)) ?? []\r\n\r\n textRects.value = nodes?.[1]?.map((e: any) => ({\r\n left: (e.left ?? 0) - tabLeft.value + currentScrollLeft,\r\n width: e.width ?? 0,\r\n } as ItemRect)) ?? []\r\n\r\n // 更新下划线、滑块等位置\r\n updatePosition()\r\n })\r\n}\r\n\r\n// 刷新tab区域的宽度和位置信息\r\nfunction refresh() {\r\n // 使用 setTimeout 替代 nextTick,确保在视图层渲染完成后再获取布局信息\r\n // 尤其是在修改 size 等导致几何变化的属性时,需要一定延迟\r\n setTimeout(() => {\r\n // 创建选择器查询\r\n uni.createSelectorQuery()\r\n // 作用域限定为当前组件\r\n .in(proxy)\r\n // 选择tab容器\r\n .select('.reborn-tabs')\r\n // 获取容器的left和width\r\n .boundingClientRect((node: any) => {\r\n // 设置tab左侧偏移\r\n tabLeft.value = node?.left ?? 0\r\n // 设置tab宽度\r\n tabWidth.value = node?.width ?? 0\r\n\r\n // 获取所有标签的位置信息\r\n getRects()\r\n })\r\n .exec()\r\n }, 50)\r\n}\r\n\r\nonMounted(() => {\r\n watch(\r\n () => active.value,\r\n () => {\r\n // 更新下划线、滑块等位置\r\n updatePosition()\r\n },\r\n {\r\n // 立即执行一次\r\n immediate: true,\r\n },\r\n )\r\n\r\n // 监听标签列表变化,刷新布局\r\n watch(\r\n computed(() => props.list),\r\n () => {\r\n refresh()\r\n },\r\n {\r\n immediate: true,\r\n deep: true,\r\n },\r\n )\r\n\r\n // 监听其他影响布局的属性的变化\r\n watch(\r\n [\r\n computed(() => props.size),\r\n computed(() => props.variant),\r\n computed(() => props.fill),\r\n computed(() => props.justify),\r\n ],\r\n () => {\r\n refresh()\r\n },\r\n )\r\n})\r\n</script>\r\n\r\n<template>\r\n <view class=\"reborn-tabs\" :class=\"ui.tabs({ class: cn(props.customClass, uiOverrides.tabs) })\">\r\n <scroll-view\r\n class=\"reborn-tabs__scroll-view\" :class=\"ui.scrollbar({ class: uiOverrides.scrollbar })\"\r\n :scroll-with-animation=\"true\" :scroll-x=\"true\" direction=\"horizontal\" :scroll-left=\"scrollLeft\"\r\n :show-scrollbar=\"false\"\r\n >\r\n <view :class=\"ui.inner({ class: uiOverrides.inner })\">\r\n <view\r\n v-for=\"(item, index) in list\" :key=\"index\"\r\n class=\"reborn-tabs__item\"\r\n :class=\"ui.item({ class: uiOverrides.item })\" :data-state=\"item.isActive ? 'active' : 'inactive'\" :data-disabled=\"item.disabled ? 'true' : undefined\"\r\n @tap=\"change(index)\"\r\n >\r\n <slot name=\"item\" :item=\"item\" :active=\"item.isActive\">\r\n <text\r\n v-if=\"item.isActive\" class=\"reborn-tabs__text\"\r\n :class=\"ui.active({ class: cn(uiOverrides.active, item.disabled || props.disabled ? `\r\n cursor-not-allowed text-gray-5\r\n ` : '') })\"\r\n >\r\n {{ item.label }}\r\n </text>\r\n <text\r\n v-else class=\"reborn-tabs__text\"\r\n :class=\"ui.text({ class: cn(uiOverrides.text, item.disabled || props.disabled ? `\r\n cursor-not-allowed text-gray-5\r\n ` : '') })\"\r\n >\r\n {{ item.label }}\r\n </text>\r\n </slot>\r\n </view>\r\n <template v-if=\"lineLeft > 0\">\r\n <view\r\n v-if=\"variant == 'line'\"\r\n :class=\"ui.line({ class: uiOverrides.line })\"\r\n :style=\"{ transform: `translateX(${lineLeft}px)`, width: `${lineWidth}px` }\"\r\n />\r\n <view\r\n v-if=\"variant == 'card'\"\r\n :class=\"ui.slider({ class: uiOverrides.slider })\"\r\n :style=\"{ transform: `translateX(${sliderLeft}px)`, width: `${sliderWidth}px` }\"\r\n />\r\n </template>\r\n </view>\r\n </scroll-view>\r\n </view>\r\n</template>\r\n",
50
56
  "target": "uniapp"
51
57
  }
52
58
  ],
53
- "fileCount": 9,
54
- "contentHash": "ad1936ad60b8df5d8bff2b63a2fe834d0fb58441"
59
+ "fileCount": 10,
60
+ "contentHash": "15c9fdbb559a9061499de2ca7c2f7f7f4e746a77"
55
61
  }
@@ -0,0 +1,34 @@
1
+ {
2
+ "name": "reborn-text",
3
+ "dependencies": [
4
+ "clsx"
5
+ ],
6
+ "files": [
7
+ {
8
+ "path": "index.ts",
9
+ "content": "export { default as RebornText } from \"./RebornText.vue\";\r\n"
10
+ },
11
+ {
12
+ "path": "reborn-text.config.ts",
13
+ "content": "const colors = [\"primary\", \"secondary\", \"success\", \"info\", \"warning\", \"error\", \"neutral\"] as const;\r\n\r\nexport { colors as textColors };\r\n\r\nexport default {\r\n slots: {\r\n base: \"inline [flex-shrink:unset]\",\r\n },\r\n variants: {\r\n preWrap: {\r\n true: {\r\n base: \"whitespace-pre-wrap\",\r\n },\r\n },\r\n color: {\r\n primary: { base: \"text-primary\" },\r\n secondary: { base: \"text-secondary\" },\r\n success: { base: \"text-success\" },\r\n info: { base: \"text-info\" },\r\n warning: { base: \"text-warning\" },\r\n error: { base: \"text-error\" },\r\n neutral: { base: \"text-neutral\" },\r\n },\r\n ellipsis: {\r\n true: {\r\n base: \"text-ellipsis overflow-hidden [display:-webkit-box] [-webkit-box-orient:vertical] [line-break:anywhere]\",\r\n },\r\n },\r\n },\r\n};\r\n",
14
+ "target": "web"
15
+ },
16
+ {
17
+ "path": "RebornText.vue",
18
+ "content": "<script setup lang=\"ts\">\r\nimport { computed } from \"vue\";\r\nimport type { ClassValue } from \"clsx\";\r\nimport { cn } from \"~/lib/utils\";\r\nimport theme, { textColors } from \"./reborn-text.config\";\r\nimport { tv } from \"~/lib/tv\";\r\n\r\nconst b = tv(theme);\r\n\r\ndefineOptions({ inheritAttrs: false });\r\n\r\nexport interface RebornTextProps {\r\n /** 文本颜色 */\r\n color?: (typeof textColors)[number];\r\n /** 字体大小 px */\r\n size?: number;\r\n /** 显示的值 */\r\n value?: string | number | null;\r\n /** 文本类型: default | phone | name | amount | card | email */\r\n type?: string;\r\n /** 是否开启脱敏 */\r\n mask?: boolean;\r\n /** 金额货币符号 */\r\n currency?: string;\r\n /** 金额小数位数 */\r\n precision?: number;\r\n /** 脱敏起始位置 */\r\n maskStart?: number;\r\n /** 脱敏结束位置 */\r\n maskEnd?: number;\r\n /** 脱敏替换字符 */\r\n maskChar?: string;\r\n /** 是否省略号 */\r\n ellipsis?: boolean;\r\n /** 最大行数 */\r\n lines?: number;\r\n /** 是否保留空白 */\r\n preWrap?: boolean;\r\n class?: any;\r\n ui?: Partial<{ base: ClassValue }>;\r\n}\r\n\r\nconst props = withDefaults(defineProps<RebornTextProps>(), {\r\n value: null,\r\n type: \"default\",\r\n mask: false,\r\n currency: \"¥\",\r\n precision: 2,\r\n maskStart: 3,\r\n maskEnd: 4,\r\n maskChar: \"*\",\r\n ellipsis: false,\r\n lines: 1,\r\n preWrap: false,\r\n});\r\n\r\nconst uiOverrides = computed(() => props.ui || {});\r\n\r\nconst ui = computed(() => {\r\n const styles = b({\r\n color: props.color,\r\n preWrap: props.preWrap,\r\n ellipsis: props.ellipsis,\r\n });\r\n return {\r\n base: (opts?: { class?: any }) =>\r\n styles.base({ class: cn(opts?.class, uiOverrides.value.base) }),\r\n };\r\n});\r\n\r\nconst textStyle = computed(() => {\r\n const style: Record<string, any> = {};\r\n if (props.ellipsis) {\r\n style[\"-webkit-line-clamp\"] = props.lines;\r\n style[\"line-clamp\"] = props.lines;\r\n style[\"-webkit-box-orient\"] = \"vertical\";\r\n style[\"display\"] = \"-webkit-box\";\r\n style[\"overflow\"] = \"hidden\";\r\n style[\"word-break\"] = \"break-all\";\r\n }\r\n if (props.size) {\r\n style.fontSize = `${props.size}px`;\r\n }\r\n return style;\r\n});\r\n\r\nfunction formatPhone(phone: string): string {\r\n if (phone.length !== 11 || !props.mask) return phone;\r\n return phone.replace(/(\\d{3})\\d{4}(\\d{4})/, `$1${props.maskChar.repeat(4)}$2`);\r\n}\r\n\r\nfunction formatName(name: string): string {\r\n if (name.length <= 1 || !props.mask) return name;\r\n if (name.length === 2) return name[0] + props.maskChar;\r\n return name[0] + props.maskChar.repeat(name.length - 2) + name[name.length - 1];\r\n}\r\n\r\nfunction formatAmount(amount: string | number): string {\r\n const num = typeof amount === \"number\" ? amount : parseFloat(amount);\r\n const formatted = num.toFixed(props.precision);\r\n const parts = formatted.split(\".\");\r\n parts[0] = parts[0].replace(/\\B(?=(\\d{3})+(?!\\d))/g, \",\");\r\n return props.currency + parts.join(\".\");\r\n}\r\n\r\nfunction formatCard(card: string): string {\r\n if (card.length < 8 || !props.mask) return card;\r\n const start = card.substring(0, props.maskStart);\r\n const end = card.substring(card.length - props.maskEnd);\r\n const middle = props.maskChar.repeat(card.length - props.maskStart - props.maskEnd);\r\n return start + middle + end;\r\n}\r\n\r\nfunction formatEmail(email: string): string {\r\n if (!props.mask) return email;\r\n const atIndex = email.indexOf(\"@\");\r\n if (atIndex === -1) return email;\r\n const username = email.substring(0, atIndex);\r\n const domain = email.substring(atIndex);\r\n if (username.length <= 2) return email;\r\n return username[0] + props.maskChar.repeat(username.length - 2) + username[username.length - 1] + domain;\r\n}\r\n\r\nconst content = computed(() => {\r\n const val = props.value ?? \"\";\r\n switch (props.type) {\r\n case \"phone\": return formatPhone(val as string);\r\n case \"name\": return formatName(val as string);\r\n case \"amount\": return formatAmount(val as number);\r\n case \"card\": return formatCard(val as string);\r\n case \"email\": return formatEmail(val as string);\r\n default: return val;\r\n }\r\n});\r\n</script>\r\n\r\n<template>\r\n <span :class=\"ui.base({ class: props.class })\" :style=\"textStyle\">\r\n <slot>{{ content }}</slot>\r\n </span>\r\n</template>\r\n",
19
+ "target": "web"
20
+ },
21
+ {
22
+ "path": "reborn-text.config.ts",
23
+ "content": "export const textColors = ['primary', 'secondary', 'success', 'info', 'warning', 'error', 'neutral'] as const\r\n\r\nexport default {\r\n slots: {\r\n // #ifndef APP\r\n base: 'reborn-text [flex-shrink:unset] text-28',\r\n // #endif\r\n // #ifdef APP\r\n // @ts-ignore\r\n base: 'reborn-text text-28',\r\n // #endif\r\n },\r\n variants: {\r\n preWrap: {\r\n true: {\r\n // #ifndef APP\r\n base: 'whitespace-pre-wrap',\r\n // #endif\r\n },\r\n },\r\n color: {\r\n primary: {\r\n base: 'text-primary',\r\n },\r\n secondary: {\r\n base: 'text-secondary',\r\n },\r\n success: {\r\n base: 'text-success',\r\n },\r\n info: {\r\n base: 'text-info',\r\n },\r\n warning: {\r\n base: 'text-warning',\r\n },\r\n error: {\r\n base: 'text-error',\r\n },\r\n neutral: {\r\n base: 'text-neutral',\r\n },\r\n },\r\n ellipsis: {\r\n true: {\r\n base: [\r\n 'text-ellipsis overflow-hidden [display:-webkit-box] [-webkit-box-orient:vertical] [line-break:anywhere]',\r\n ],\r\n },\r\n },\r\n },\r\n}\r\n",
24
+ "target": "uniapp"
25
+ },
26
+ {
27
+ "path": "RebornText.vue",
28
+ "content": "<script setup lang=\"ts\">\r\nimport type { textColors } from './reborn-text.config'\r\nimport { computed } from 'vue'\r\nimport { tv } from '@/lib/tv'\r\nimport { cn } from '@/lib/utils'\r\nimport theme from './reborn-text.config'\r\n\r\ndefineOptions({\r\n name: 'RebornText',\r\n})\r\n\r\nconst props = withDefaults(defineProps<RebornTextProps>(), {\r\n ui: () => ({\r\n base: '',\r\n }),\r\n customClass: '',\r\n value: null,\r\n type: 'default',\r\n mask: false,\r\n currency: '¥',\r\n precision: 2,\r\n maskStart: 3,\r\n maskEnd: 4,\r\n maskChar: '*',\r\n ellipsis: false,\r\n lines: 1,\r\n selectable: false,\r\n space: '',\r\n decode: false,\r\n preWrap: false,\r\n})\r\nexport interface RebornTextProps {\r\n // 透传样式\r\n ui?: {\r\n base: string\r\n }\r\n customClass?: string\r\n // 文本颜色\r\n color?: typeof textColors[number]\r\n // 字体大小\r\n size?: number\r\n // 显示的值\r\n value?: string | number | null\r\n // 文本类型\r\n type?: string\r\n // 是否开启脱敏/加密\r\n mask?: boolean\r\n // 金额货币符号\r\n currency?: string\r\n // 金额小数位数\r\n precision?: number\r\n // 脱敏起始位置\r\n maskStart?: number\r\n // 脱敏结束位置\r\n maskEnd?: number\r\n // 脱敏替换字符\r\n maskChar?: string\r\n // 是否省略号\r\n ellipsis?: boolean\r\n // 最大行数,仅在ellipsis时生效\r\n lines?: number\r\n // 是否可选择\r\n selectable?: boolean\r\n // 显示连续空格\r\n space?: string\r\n // 是否解码 (app平台如需解析字符实体,需要配置为 true)\r\n decode?: boolean\r\n // 是否保留单词\r\n preWrap?: boolean\r\n}\r\nconst cache = { key: 1 }\r\ninterface PassThrough { className?: string }\r\n\r\n// 样式生成\r\nconst b = tv(theme)\r\nconst ui = computed(() => {\r\n const styles = b({\r\n color: props.color,\r\n preWrap: props.preWrap,\r\n ellipsis: props.ellipsis,\r\n })\r\n\r\n return {\r\n base: (opts?: { class?: any }) => styles.base({ class: cn(opts?.class, props.ui?.base) }),\r\n }\r\n})\r\n\r\n// 文本样式\r\nconst textStyle = computed(() => {\r\n const style: Record<string, any> = {}\r\n\r\n // 省略号\r\n if (props.ellipsis) {\r\n style['-webkit-line-clamp'] = props.lines\r\n style['line-clamp'] = props.lines\r\n }\r\n\r\n // 字号\r\n if (props.size) {\r\n style.fontSize = `${props.size}rpx`\r\n }\r\n\r\n return style\r\n})\r\n\r\n/**\r\n * 手机号脱敏处理\r\n * 保留前3位和后4位,中间4位替换为掩码\r\n */\r\nfunction formatPhone(phone: string): string {\r\n if (phone.length != 11 || !props.mask) { return phone }\r\n return phone.replace(/(\\d{3})\\d{4}(\\d{4})/, `$1${props.maskChar.repeat(4)}$2`)\r\n}\r\n\r\n/**\r\n * 姓名脱敏处理\r\n * 2个字时保留第1个字\r\n * 大于2个字时保留首尾字\r\n */\r\nfunction formatName(name: string): string {\r\n if (name.length <= 1 || !props.mask) { return name }\r\n if (name.length == 2) {\r\n return name[0] + props.maskChar\r\n }\r\n return name[0] + props.maskChar.repeat(name.length - 2) + name[name.length - 1]\r\n}\r\n\r\n/**\r\n * 金额格式化\r\n * 1. 处理小数位数\r\n * 2. 添加千分位分隔符\r\n * 3. 添加货币符号\r\n */\r\nfunction formatAmount(amount: string | number): string {\r\n let num: number\r\n\r\n if (typeof amount == 'number') {\r\n num = amount\r\n }\r\n else {\r\n num = Number.parseFloat(amount)\r\n }\r\n\r\n const formatted = num.toFixed(props.precision)\r\n const parts = formatted.split('.')\r\n parts[0] = parts[0].replace(/\\B(?=(\\d{3})+(?!\\d))/g, ',')\r\n\r\n return props.currency + parts.join('.')\r\n}\r\n\r\n/**\r\n * 银行卡号脱敏\r\n * 保留开头和结尾指定位数,中间用掩码替换\r\n */\r\nfunction formatCard(card: string): string {\r\n if (card.length < 8 || !props.mask) { return card }\r\n\r\n const start = card.substring(0, props.maskStart)\r\n const end = card.substring(card.length - props.maskEnd)\r\n const middle = props.maskChar.repeat(card.length - props.maskStart - props.maskEnd)\r\n return start + middle + end\r\n}\r\n\r\n/**\r\n * 邮箱脱敏处理\r\n * 保留用户名首尾字符和完整域名\r\n */\r\nfunction formatEmail(email: string): string {\r\n if (!props.mask) { return email }\r\n\r\n const atIndex = email.indexOf('@')\r\n if (atIndex == -1) { return email }\r\n\r\n const username = email.substring(0, atIndex)\r\n const domain = email.substring(atIndex)\r\n\r\n if (username.length <= 2) { return email }\r\n\r\n const maskedUsername\r\n = username[0] + props.maskChar.repeat(username.length - 2) + username[username.length - 1]\r\n return maskedUsername + domain\r\n}\r\n\r\n/**\r\n * 根据不同类型格式化显示\r\n */\r\nconst content = computed(() => {\r\n const val = props.value ?? ''\r\n\r\n switch (props.type) {\r\n case 'phone':\r\n return formatPhone(val as string)\r\n case 'name':\r\n return formatName(val as string)\r\n case 'amount':\r\n return formatAmount(val as number)\r\n case 'card':\r\n return formatCard(val as string)\r\n case 'email':\r\n return formatEmail(val as string)\r\n default:\r\n return val\r\n }\r\n})\r\n</script>\r\n\r\n<template>\r\n <!-- #ifdef MP -->\r\n <view :key=\"cache.key\" :class=\"ui.base({ class: customClass })\" :style=\"textStyle\" :selectable=\"selectable\"\r\n :space=\"space\" :decode=\"decode\">\r\n <slot>{{ content }}</slot>\r\n </view>\r\n <!-- #endif -->\r\n\r\n <!-- #ifndef MP -->\r\n <text :key=\"cache.key\" :class=\"ui.base({ class: customClass })\" :style=\"textStyle\" :selectable=\"selectable\"\r\n :space=\"space\" :decode=\"decode\">\r\n <slot>{{ content }}</slot>\r\n </text>\r\n <!-- #endif -->\r\n</template>\r\n\r\n<style lang=\"scss\" scoped></style>\r\n",
29
+ "target": "uniapp"
30
+ }
31
+ ],
32
+ "fileCount": 5,
33
+ "contentHash": "2b699ab869a5d952cebc2d26123d6f4553a37e62"
34
+ }
@@ -4,7 +4,7 @@
4
4
  "files": [
5
5
  {
6
6
  "path": "reborn-textarea.config.ts",
7
- "content": "const sizes = [\"sm\", \"md\", \"lg\"] as const;\r\nconst colors = [\"primary\", \"secondary\", \"success\", \"info\", \"warning\", \"error\", \"neutral\"] as const;\r\n\r\nconst config = {\r\n slots: {\r\n root: \"relative box-border shrink-0 grow-0 basis-auto min-h-0 min-w-0 flex flex-row items-center bg-white rounded-lg p-2 transition-all duration-200\",\r\n inner: \"h-full flex-1 text-sm text-surface-700 bg-transparent disabled:cursor-not-allowed disabled:opacity-50 outline-none border-none\",\r\n text: \"absolute right-4 bottom-2 text-xs text-gray-500 pointer-events-none\",\r\n },\r\n variants: {\r\n size: {\r\n sm: {\r\n inner: \"text-xs\",\r\n },\r\n md: {\r\n inner: \"text-sm\",\r\n },\r\n lg: {\r\n inner: \"text-base\",\r\n },\r\n },\r\n border: {\r\n true: {\r\n root: \"ring-1 ring-gray-4 dark:ring-gray-1\",\r\n },\r\n },\r\n focused: {\r\n true: {},\r\n },\r\n disabled: {\r\n true: {\r\n root: \"bg-gray-3 text-gray-5\",\r\n inner: \"cursor-not-allowed\",\r\n },\r\n },\r\n error: {\r\n true: {\r\n root: \"border-red-500 ring-red-500\",\r\n },\r\n },\r\n hasCount: {\r\n true: {\r\n inner: \"pb-6\",\r\n },\r\n },\r\n color: {\r\n primary: {\r\n root: \"\",\r\n },\r\n secondary: {\r\n root: \"\",\r\n },\r\n success: {\r\n root: \"\",\r\n },\r\n info: {\r\n root: \"\",\r\n },\r\n warning: {\r\n root: \"\",\r\n },\r\n error: {\r\n root: \"\",\r\n },\r\n neutral: {\r\n root: \"\",\r\n },\r\n },\r\n },\r\n compoundVariants: [\r\n {\r\n focused: true,\r\n color: \"primary\" as (typeof colors)[number],\r\n class: {\r\n root: \"ring-2 ring-primary\",\r\n },\r\n },\r\n {\r\n focused: true,\r\n color: \"secondary\" as (typeof colors)[number],\r\n class: {\r\n root: \"ring-2 ring-secondary\",\r\n },\r\n },\r\n {\r\n focused: true,\r\n color: \"success\" as (typeof colors)[number],\r\n class: {\r\n root: \"ring-2 ring-success\",\r\n },\r\n },\r\n {\r\n focused: true,\r\n color: \"info\" as (typeof colors)[number],\r\n class: {\r\n root: \"ring-2 ring-info\",\r\n },\r\n },\r\n {\r\n focused: true,\r\n color: \"warning\" as (typeof colors)[number],\r\n class: {\r\n root: \"ring-2 ring-warning\",\r\n },\r\n },\r\n {\r\n focused: true,\r\n color: \"error\" as (typeof colors)[number],\r\n class: {\r\n root: \"ring-2 ring-error\",\r\n },\r\n },\r\n {\r\n focused: true,\r\n color: \"neutral\" as (typeof colors)[number],\r\n class: {\r\n root: \"ring-2 ring-neutral\",\r\n },\r\n },\r\n ],\r\n defaultVariants: {\r\n size: \"md\" as (typeof sizes)[number],\r\n border: true,\r\n color: \"primary\" as (typeof colors)[number],\r\n },\r\n};\r\n\r\nexport { sizes as textareaSizes, colors as textareaColors };\r\nexport default config;\r\n",
7
+ "content": "const sizes = [\"sm\", \"md\", \"lg\"] as const;\r\nconst colors = [\"primary\", \"secondary\", \"success\", \"info\", \"warning\", \"error\", \"neutral\"] as const;\r\n\r\nconst config = {\r\n slots: {\r\n root: \"relative box-border shrink-0 grow-0 basis-auto min-h-0 min-w-0 flex flex-row items-center bg-white rounded-lg p-2 transition-all duration-200\",\r\n inner: \"h-full flex-1 text-sm text-gray-7 bg-transparent disabled:cursor-not-allowed disabled:opacity-50 outline-none border-none\",\r\n text: \"absolute right-4 bottom-2 text-xs text-gray-500 pointer-events-none\",\r\n },\r\n variants: {\r\n size: {\r\n sm: {\r\n inner: \"text-xs\",\r\n },\r\n md: {\r\n inner: \"text-sm\",\r\n },\r\n lg: {\r\n inner: \"text-base\",\r\n },\r\n },\r\n border: {\r\n true: {\r\n root: \"ring-1 ring-gray-4 dark:ring-gray-1\",\r\n },\r\n },\r\n focused: {\r\n true: {},\r\n },\r\n disabled: {\r\n true: {\r\n root: \"bg-gray-3 text-gray-5\",\r\n inner: \"cursor-not-allowed\",\r\n },\r\n },\r\n error: {\r\n true: {\r\n root: \"border-red-500 ring-red-500\",\r\n },\r\n },\r\n hasCount: {\r\n true: {\r\n inner: \"pb-6\",\r\n },\r\n },\r\n color: {\r\n primary: {\r\n root: \"\",\r\n },\r\n secondary: {\r\n root: \"\",\r\n },\r\n success: {\r\n root: \"\",\r\n },\r\n info: {\r\n root: \"\",\r\n },\r\n warning: {\r\n root: \"\",\r\n },\r\n error: {\r\n root: \"\",\r\n },\r\n neutral: {\r\n root: \"\",\r\n },\r\n },\r\n },\r\n compoundVariants: [\r\n {\r\n focused: true,\r\n color: \"primary\" as (typeof colors)[number],\r\n class: {\r\n root: \"ring-2 ring-primary\",\r\n },\r\n },\r\n {\r\n focused: true,\r\n color: \"secondary\" as (typeof colors)[number],\r\n class: {\r\n root: \"ring-2 ring-secondary\",\r\n },\r\n },\r\n {\r\n focused: true,\r\n color: \"success\" as (typeof colors)[number],\r\n class: {\r\n root: \"ring-2 ring-success\",\r\n },\r\n },\r\n {\r\n focused: true,\r\n color: \"info\" as (typeof colors)[number],\r\n class: {\r\n root: \"ring-2 ring-info\",\r\n },\r\n },\r\n {\r\n focused: true,\r\n color: \"warning\" as (typeof colors)[number],\r\n class: {\r\n root: \"ring-2 ring-warning\",\r\n },\r\n },\r\n {\r\n focused: true,\r\n color: \"error\" as (typeof colors)[number],\r\n class: {\r\n root: \"ring-2 ring-error\",\r\n },\r\n },\r\n {\r\n focused: true,\r\n color: \"neutral\" as (typeof colors)[number],\r\n class: {\r\n root: \"ring-2 ring-neutral\",\r\n },\r\n },\r\n ],\r\n defaultVariants: {\r\n size: \"md\" as (typeof sizes)[number],\r\n border: true,\r\n color: \"primary\" as (typeof colors)[number],\r\n },\r\n};\r\n\r\nexport { sizes as textareaSizes, colors as textareaColors };\r\nexport default config;\r\n",
8
8
  "target": "web"
9
9
  },
10
10
  {
@@ -14,20 +14,20 @@
14
14
  },
15
15
  {
16
16
  "path": "index.ts",
17
- "content": "export { default as RebornTextarea } from \"./RebornTextarea.vue\";\r\n",
17
+ "content": "export { default as RebornTextarea } from './RebornTextarea.vue'\r\n",
18
18
  "target": "uniapp"
19
19
  },
20
20
  {
21
21
  "path": "reborn-textarea.config.ts",
22
- "content": "const sizes = [\"sm\", \"md\", \"lg\"] as const;\r\nconst colors = [\"primary\", \"secondary\", \"success\", \"info\", \"warning\", \"error\", \"neutral\"] as const;\r\n\r\nconst config = {\r\n slots: {\r\n root: \"relative box-border shrink-0 grow-0 basis-auto min-h-0 min-w-0 flex flex-row items-center bg-white rounded-lg p-2 transition-all duration-200\",\r\n inner: \"h-full flex-1 text-28 text-gray-7 bg-transparent disabled:cursor-not-allowed disabled:opacity-50 w-full\",\r\n text: \"absolute right-2 bottom-2 text-xs text-gray-5 pointer-events-none\",\r\n },\r\n variants: {\r\n size: {\r\n sm: {\r\n inner: \"text-26\",\r\n },\r\n md: {\r\n inner: \"text-28\",\r\n },\r\n lg: {\r\n inner: \"text-32\",\r\n },\r\n },\r\n border: {\r\n true: {\r\n root: \"ring-1 ring-gray-4 dark:ring-gray-1\",\r\n },\r\n },\r\n focused: {\r\n true: {},\r\n },\r\n disabled: {\r\n true: {\r\n root: \"bg-gray-3 text-gray-5\",\r\n inner: \"cursor-not-allowed\",\r\n },\r\n },\r\n error: {\r\n true: {\r\n root: \"ring-error text-error\",\r\n inner: \"placeholder:text-error/50\"\r\n },\r\n },\r\n hasCount: {\r\n true: {\r\n inner: \"pb-6\",\r\n },\r\n },\r\n color: {\r\n primary: {\r\n root: \"\",\r\n },\r\n secondary: {\r\n root: \"\",\r\n },\r\n success: {\r\n root: \"\",\r\n },\r\n info: {\r\n root: \"\",\r\n },\r\n warning: {\r\n root: \"\",\r\n },\r\n error: {\r\n root: \"\",\r\n },\r\n neutral: {\r\n root: \"\",\r\n },\r\n },\r\n },\r\n compoundVariants: [\r\n {\r\n focused: true,\r\n color: \"primary\" as (typeof colors)[number],\r\n class: {\r\n root: \"ring-2 ring-primary\",\r\n },\r\n },\r\n {\r\n focused: true,\r\n color: \"secondary\" as (typeof colors)[number],\r\n class: {\r\n root: \"ring-2 ring-secondary\",\r\n },\r\n },\r\n {\r\n focused: true,\r\n color: \"success\" as (typeof colors)[number],\r\n class: {\r\n root: \"ring-2 ring-success\",\r\n },\r\n },\r\n {\r\n focused: true,\r\n color: \"info\" as (typeof colors)[number],\r\n class: {\r\n root: \"ring-2 ring-info\",\r\n },\r\n },\r\n {\r\n focused: true,\r\n color: \"warning\" as (typeof colors)[number],\r\n class: {\r\n root: \"ring-2 ring-warning\",\r\n },\r\n },\r\n {\r\n focused: true,\r\n color: \"error\" as (typeof colors)[number],\r\n class: {\r\n root: \"ring-2 ring-error\",\r\n },\r\n },\r\n {\r\n focused: true,\r\n color: \"neutral\" as (typeof colors)[number],\r\n class: {\r\n root: \"ring-2 ring-neutral\",\r\n },\r\n },\r\n ],\r\n defaultVariants: {\r\n size: \"md\" as (typeof sizes)[number],\r\n border: true,\r\n color: \"primary\" as (typeof colors)[number],\r\n },\r\n}\r\n\r\nexport { sizes as textareaSizes, colors as textareaColors };\r\nexport default config;\r\n",
22
+ "content": "const sizes = ['sm', 'md', 'lg'] as const\r\nconst colors = ['primary', 'secondary', 'success', 'info', 'warning', 'error', 'neutral'] as const\r\n\r\nconst config = {\r\n slots: {\r\n root: 'relative box-border shrink-0 grow-0 basis-auto min-h-0 min-w-0 flex flex-row items-center bg-white rounded-lg p-2 transition-all duration-200',\r\n inner: 'h-full min-h-0 flex-1 text-28 text-gray-7 bg-transparent disabled:cursor-not-allowed disabled:opacity-50 w-full',\r\n text: 'absolute right-2 bottom-2 text-xs text-gray-5 pointer-events-none',\r\n },\r\n variants: {\r\n size: {\r\n sm: {\r\n inner: 'text-26',\r\n },\r\n md: {\r\n inner: 'text-28',\r\n },\r\n lg: {\r\n inner: 'text-32',\r\n },\r\n },\r\n border: {\r\n true: {\r\n root: 'ring-1 ring-gray-4 dark:ring-gray-1',\r\n },\r\n },\r\n focused: {\r\n true: {},\r\n },\r\n disabled: {\r\n true: {\r\n root: 'bg-gray-3 text-gray-5',\r\n inner: 'cursor-not-allowed',\r\n },\r\n },\r\n error: {\r\n true: {\r\n root: 'ring-error text-error',\r\n inner: 'placeholder:text-error/50',\r\n },\r\n },\r\n hasCount: {\r\n true: {\r\n inner: 'pb-6',\r\n },\r\n },\r\n color: {\r\n primary: {\r\n root: '',\r\n },\r\n secondary: {\r\n root: '',\r\n },\r\n success: {\r\n root: '',\r\n },\r\n info: {\r\n root: '',\r\n },\r\n warning: {\r\n root: '',\r\n },\r\n error: {\r\n root: '',\r\n },\r\n neutral: {\r\n root: '',\r\n },\r\n },\r\n },\r\n compoundVariants: [\r\n {\r\n focused: true,\r\n color: 'primary' as (typeof colors)[number],\r\n class: {\r\n root: 'ring-2 ring-primary',\r\n },\r\n },\r\n {\r\n focused: true,\r\n color: 'secondary' as (typeof colors)[number],\r\n class: {\r\n root: 'ring-2 ring-secondary',\r\n },\r\n },\r\n {\r\n focused: true,\r\n color: 'success' as (typeof colors)[number],\r\n class: {\r\n root: 'ring-2 ring-success',\r\n },\r\n },\r\n {\r\n focused: true,\r\n color: 'info' as (typeof colors)[number],\r\n class: {\r\n root: 'ring-2 ring-info',\r\n },\r\n },\r\n {\r\n focused: true,\r\n color: 'warning' as (typeof colors)[number],\r\n class: {\r\n root: 'ring-2 ring-warning',\r\n },\r\n },\r\n {\r\n focused: true,\r\n color: 'error' as (typeof colors)[number],\r\n class: {\r\n root: 'ring-2 ring-error',\r\n },\r\n },\r\n {\r\n focused: true,\r\n color: 'neutral' as (typeof colors)[number],\r\n class: {\r\n root: 'ring-2 ring-neutral',\r\n },\r\n },\r\n ],\r\n defaultVariants: {\r\n size: 'md' as (typeof sizes)[number],\r\n border: true,\r\n color: 'primary' as (typeof colors)[number],\r\n },\r\n}\r\n\r\nexport { colors as textareaColors, sizes as textareaSizes }\r\nexport default config\r\n",
23
23
  "target": "uniapp"
24
24
  },
25
25
  {
26
26
  "path": "RebornTextarea.vue",
27
- "content": "<script setup lang=\"ts\">\r\nimport { computed, nextTick, ref, watch, type PropType } from \"vue\";\r\nimport { tv } from \"@/lib/tv\";\r\nimport { cn } from \"@/lib/utils\";\r\n\r\nimport { useFormInject } from \"@/composables/useFieldGroup\";\r\nimport theme, { textareaSizes, textareaColors } from \"./reborn-textarea.config\";\r\n\r\ndefineOptions({\r\n\tname: \"cl-textarea\",\r\n});\r\n\r\n// 组件属性定义\r\nconst props = defineProps({\r\n\t// ... (props are fine, keeping them implied by context or unchanged)\r\n\t// 颜色\r\n\tcolor: {\r\n\t\ttype: String as PropType<typeof textareaColors[number]>,\r\n\t\tdefault: \"primary\",\r\n\t},\r\n\t// 自定义样式类\r\n\tcustomClass: {\r\n\t\ttype: [String, Object, Array] as PropType<any>,\r\n\t\tdefault: \"\",\r\n\t},\r\n\t// UI 覆盖\r\n\tui: {\r\n\t\ttype: Object as PropType<Record<string, any>>,\r\n\t\tdefault: () => ({}),\r\n\t},\r\n\t// 绑定值\r\n\tmodelValue: {\r\n\t\ttype: String,\r\n\t\tdefault: \"\",\r\n\t},\r\n\t// 尺寸\r\n\tsize: {\r\n\t\ttype: String as PropType<typeof textareaSizes[number]>,\r\n\t\tdefault: \"md\",\r\n\t},\r\n\t// 是否显示边框\r\n\tborder: {\r\n\t\ttype: Boolean,\r\n\t\tdefault: true,\r\n\t},\r\n\t// 是否禁用\r\n\tdisabled: {\r\n\t\ttype: Boolean,\r\n\t\tdefault: false,\r\n\t},\r\n\t// 是否只读\r\n\treadonly: {\r\n\t\ttype: Boolean,\r\n\t\tdefault: null,\r\n\t},\r\n\t// 是否显示字数统计\r\n\tshowWordLimit: {\r\n\t\ttype: Boolean,\r\n\t\tdefault: true,\r\n\t},\r\n\t// 名称\r\n\tname: {\r\n\t\ttype: String,\r\n\t\tdefault: \"\",\r\n\t},\r\n\t// 占位符\r\n\tplaceholder: {\r\n\t\ttype: String,\r\n\t\tdefault: () => \"请输入\",\r\n\t},\r\n\t// 占位符样式类\r\n\tplaceholderClass: {\r\n\t\ttype: String,\r\n\t\tdefault: \"\",\r\n\t},\r\n\t// 占位符样式\r\n\tplaceholderStyle: {\r\n\t\ttype: String,\r\n\t\tdefault: \"\",\r\n\t},\r\n\t// 最大输入长度\r\n\tmaxlength: {\r\n\t\ttype: Number,\r\n\t\tdefault: 100,\r\n\t},\r\n\t// 是否自动聚焦\r\n\tautofocus: {\r\n\t\ttype: Boolean,\r\n\t\tdefault: false,\r\n\t},\r\n\t// 设置键盘右下角按钮的文字\r\n\tconfirmType: {\r\n\t\ttype: String as PropType<\"done\" | \"go\" | \"next\" | \"search\" | \"send\">,\r\n\t\tdefault: \"done\",\r\n\t},\r\n\t// 指定focus时的光标位置\r\n\tcursor: {\r\n\t\ttype: Number,\r\n\t\tdefault: 0,\r\n\t},\r\n\t// 点击键盘确认按钮时是否保持键盘不收起\r\n\tconfirmHold: {\r\n\t\ttype: Boolean,\r\n\t\tdefault: false,\r\n\t},\r\n\t// 高度\r\n\theight: {\r\n\t\ttype: [Number, String],\r\n\t\tdefault: 140,\r\n\t},\r\n\t// 是否自动增高\r\n\tautoHeight: {\r\n\t\ttype: Boolean,\r\n\t\tdefault: false,\r\n\t},\r\n\t// 如果 textarea 是在一个 position:fixed 的区域,需要显示指定属性 fixed 为 true\r\n\tfixed: {\r\n\t\ttype: Boolean,\r\n\t\tdefault: false,\r\n\t},\r\n\t// 光标与键盘的距离\r\n\tcursorSpacing: {\r\n\t\ttype: Number,\r\n\t\tdefault: 5,\r\n\t},\r\n\t// 指定光标颜色\r\n\tcursorColor: {\r\n\t\ttype: String,\r\n\t\tdefault: \"\",\r\n\t},\r\n\t// 是否显示键盘上方带有”完成“按钮那一栏\r\n\tshowConfirmBar: {\r\n\t\ttype: Boolean,\r\n\t\tdefault: true,\r\n\t},\r\n\t// 光标起始位置\r\n\tselectionStart: {\r\n\t\ttype: Number,\r\n\t\tdefault: -1,\r\n\t},\r\n\t// 光标结束位置\r\n\tselectionEnd: {\r\n\t\ttype: Number,\r\n\t\tdefault: -1,\r\n\t},\r\n\t// 盘弹起时,是否自动上推页面\r\n\tadjustPosition: {\r\n\t\ttype: Boolean,\r\n\t\tdefault: true,\r\n\t},\r\n\t// 它提供了用户在编辑元素或其内容时可能输入的数据类型的提示。\r\n\tinputmode: {\r\n\t\ttype: String as PropType<\r\n\t\t\t\"none\" | \"text\" | \"decimal\" | \"numeric\" | \"tel\" | \"search\" | \"email\" | \"url\"\r\n\t\t>,\r\n\t\tdefault: \"text\",\r\n\t},\r\n\t// focus时,点击页面的时候不收起键盘\r\n\tholdKeyboard: {\r\n\t\ttype: Boolean,\r\n\t\tdefault: false,\r\n\t},\r\n\t// 是否禁用默认内边距\r\n\tdisableDefaultPadding: {\r\n\t\ttype: Boolean,\r\n\t\tdefault: true,\r\n\t},\r\n\t// 键盘对齐位置\r\n\tadjustKeyboardTo: {\r\n\t\ttype: String as PropType<\"cursor\" | \"bottom\">,\r\n\t\tdefault: \"cursor\",\r\n\t},\r\n});\r\n\r\n// 事件定义\r\nconst emit = defineEmits([\r\n\t\"update:modelValue\",\r\n\t\"input\",\r\n\t\"change\",\r\n\t\"focus\",\r\n\t\"blur\",\r\n\t\"confirm\",\r\n\t\"linechange\",\r\n\t\"keyboardheightchange\",\r\n]);\r\n\r\nconst { disabled: isDisabled, isError } = useFormInject(props);\r\n\r\n\r\n// 绑定值\r\nconst value = ref(props.modelValue);\r\n\r\n// 是否聚焦(样式作用)\r\nconst isFocus = ref<boolean>(props.autofocus);\r\n\r\n// 是否聚焦(输入框作用)\r\nconst isFocusing = ref<boolean>(props.autofocus);\r\n\r\nconst b = tv(theme);\r\n\r\nconst ui = computed(() =>\r\n\tb({\r\n\t\tsize: props.size,\r\n\t\tborder: props.border,\r\n\t\tfocused: isFocus.value,\r\n\t\tdisabled: isDisabled.value,\r\n\t\terror: isError.value,\r\n\t\thasCount: props.showWordLimit,\r\n\t\tcolor: props.color,\r\n\t})\r\n);\r\n\r\n// 文本框样式\r\nconst textareaStyle = computed(() => {\r\n\tconst style = {\r\n\t\theight: typeof props.height === 'number' ? `${props.height}rpx` : props.height,\r\n\t};\r\n\r\n\treturn style;\r\n});\r\n\r\nconst placeholderStyle = computed(() => {\r\n\treturn `${props.placeholderStyle}`;\r\n});\r\n\r\n// 点击事件\r\nfunction onTap() {\r\n\tisFocus.value = true;\r\n}\r\n\r\n// 获取焦点事件\r\nfunction onFocus(e: any) {\r\n\tisFocus.value = true;\r\n\temit(\"focus\", e);\r\n}\r\n\r\n// 失去焦点事件\r\nfunction onBlur(e: any) {\r\n\temit(\"blur\", e);\r\n\r\n\tsetTimeout(() => {\r\n\t\tisFocus.value = false;\r\n\t}, 0);\r\n}\r\n\r\n// 输入事件\r\nfunction onInput(e: any) {\r\n\tconst v1 = e.detail.value;\r\n\tconst v2 = value.value;\r\n\r\n\tvalue.value = v1;\r\n\r\n\temit(\"update:modelValue\", v1);\r\n\temit(\"input\", e);\r\n\r\n\tif (v1 != v2) {\r\n\t\temit(\"change\", v1);\r\n\t}\r\n}\r\n\r\n// 点击确认按钮事件\r\nfunction onConfirm(e: any) {\r\n\temit(\"confirm\", e);\r\n}\r\n\r\n// 键盘高度变化事件\r\nfunction onKeyboardheightchange(e: any) {\r\n\temit(\"keyboardheightchange\", e);\r\n}\r\n\r\n// 行数变化事件\r\nfunction onLineChange(e: any) {\r\n\temit(\"linechange\", e);\r\n}\r\n\r\n// 聚焦方法\r\nfunction focus() {\r\n\tsetTimeout(() => {\r\n\t\tisFocusing.value = false;\r\n\r\n\t\tnextTick(() => {\r\n\t\t\tisFocusing.value = true;\r\n\t\t});\r\n\t}, 0);\r\n}\r\n\r\nwatch(\r\n\tcomputed(() => props.modelValue),\r\n\t(val: string) => {\r\n\t\tvalue.value = val;\r\n\t}\r\n);\r\n\r\ndefineExpose({\r\n\tisFocus,\r\n\tfocus,\r\n});\r\n</script>\r\n\r\n<template>\r\n\t<view :class=\"ui.root({ class: cn(props.customClass, props.ui?.root) })\" @tap=\"onTap\">\r\n\t\t<textarea :class=\"ui.inner({ class: props.ui?.inner })\" :style=\"textareaStyle\" :value=\"value\" :name=\"name\"\r\n\t\t\t:disabled=\"readonly ?? isDisabled\" :placeholder=\"placeholder\"\r\n\t\t\t:placeholder-class=\"`text-surface-400 ${placeholderClass}`\" :placeholder-style=\"placeholderStyle\"\r\n\t\t\t:maxlength=\"maxlength\" :focus=\"isFocusing\" :cursor=\"cursor\" :cursor-spacing=\"cursorSpacing\"\r\n\t\t\t:cursor-color=\"cursorColor\" :show-confirm-bar=\"showConfirmBar\" :confirm-hold=\"confirmHold\"\r\n\t\t\t:auto-height=\"autoHeight\" :fixed=\"fixed\" :adjust-position=\"adjustPosition\" :hold-keyboard=\"holdKeyboard\"\r\n\t\t\t:inputmode=\"inputmode\" :disable-default-padding=\"disableDefaultPadding\"\r\n\t\t\t:adjust-keyboard-to=\"adjustKeyboardTo\" @confirm=\"onConfirm\" @input=\"onInput\" @linechange=\"onLineChange\"\r\n\t\t\t@blur=\"onBlur\" @focus=\"onFocus\" @keyboardheightchange=\"onKeyboardheightchange\" />\r\n\r\n\t\t<slot v-if=\"showWordLimit\" name=\"limit\" :length=\"value.length\" :max=\"maxlength\">\r\n\t\t\t<text :size=\"12\" :class=\"ui.text({ class: props.ui?.text })\">{{\r\n\t\t\t\tvalue.length }} / {{ maxlength }}</text>\r\n\t\t</slot>\r\n\t</view>\r\n</template>\r\n\r\n\r\n\r\n<style scoped>\r\n:deep(.uni-textarea-compute) {\r\n\topacity: 0;\r\n}\r\n</style>\r\n",
27
+ "content": "<script setup lang=\"ts\">\r\nimport type { textareaColors, textareaSizes } from './reborn-textarea.config'\r\nimport { computed, nextTick, ref, watch } from 'vue'\r\nimport { useFormInject } from '@/composables/useFieldGroup'\r\n\r\nimport { tv } from '@/lib/tv'\r\nimport { cn } from '@/lib/utils'\r\nimport theme from './reborn-textarea.config'\r\n\r\ndefineOptions({\r\n name: 'ClTextarea',\r\n})\r\n\r\nconst props = withDefaults(defineProps<RebornTextareaProps>(), {\r\n color: 'primary',\r\n customClass: '',\r\n ui: () => ({}),\r\n modelValue: '',\r\n size: 'md',\r\n border: true,\r\n disabled: false,\r\n readonly: false,\r\n showWordLimit: true,\r\n name: '',\r\n placeholder: () => '请输入',\r\n placeholderClass: '',\r\n placeholderStyle: '',\r\n maxlength: 100,\r\n autofocus: false,\r\n confirmType: 'done',\r\n cursor: 0,\r\n confirmHold: false,\r\n height: 140,\r\n autoHeight: false,\r\n fixed: false,\r\n cursorSpacing: 5,\r\n cursorColor: '',\r\n showConfirmBar: true,\r\n selectionStart: -1,\r\n selectionEnd: -1,\r\n adjustPosition: true,\r\n inputmode: 'text',\r\n holdKeyboard: false,\r\n disableDefaultPadding: true,\r\n adjustKeyboardTo: 'cursor',\r\n})\r\n// 事件定义\r\nconst emit = defineEmits([\r\n 'update:modelValue',\r\n 'input',\r\n 'change',\r\n 'focus',\r\n 'blur',\r\n 'confirm',\r\n 'linechange',\r\n 'keyboardheightchange',\r\n])\r\nexport interface RebornTextareaProps {\r\n // 颜色\r\n color?: typeof textareaColors[number]\r\n // 自定义样式类\r\n customClass?: string\r\n // UI 覆盖\r\n ui?: Record<string, any>\r\n // 绑定值\r\n modelValue: string\r\n // 尺寸\r\n size?: typeof textareaSizes[number]\r\n // 是否显示边框\r\n border?: boolean\r\n // 是否禁用\r\n disabled?: boolean\r\n // 是否只读\r\n readonly?: boolean\r\n // 是否显示字数统计\r\n showWordLimit?: boolean\r\n // 名称\r\n name?: string\r\n // 占位符\r\n placeholder?: string\r\n // 占位符样式类\r\n placeholderClass?: string\r\n // 占位符样式\r\n placeholderStyle?: string\r\n // 最大输入长度\r\n maxlength?: number\r\n // 是否自动聚焦\r\n autofocus?: boolean\r\n // 设置键盘右下角按钮的文字\r\n confirmType?: string\r\n // 指定focus时的光标位置\r\n cursor?: number\r\n // 点击键盘确认按钮时是否保持键盘不收起\r\n confirmHold?: boolean\r\n // 高度\r\n height?: number | string\r\n // 是否自动增高\r\n autoHeight?: boolean\r\n // 如果 textarea 是在一个 position:fixed 的区域,需要显示指定属性 fixed 为 true\r\n fixed?: boolean\r\n // 光标与键盘的距离\r\n cursorSpacing?: number\r\n // 指定光标颜色\r\n cursorColor?: string\r\n // 是否显示键盘上方带有”完成“按钮那一栏\r\n showConfirmBar?: boolean\r\n // 光标起始位置\r\n selectionStart?: number\r\n // 光标结束位置\r\n selectionEnd?: number\r\n // 盘弹起时,是否自动上推页面\r\n adjustPosition?: boolean\r\n // 它提供了用户在编辑元素或其内容时可能输入的数据类型的提示。\r\n inputmode?: 'none' | 'text' | 'decimal' | 'numeric' | 'tel' | 'search' | 'email' | 'url'\r\n // focus时,点击页面的时候不收起键盘\r\n holdKeyboard?: boolean\r\n // 是否禁用默认内边距\r\n disableDefaultPadding?: boolean\r\n // 键盘对齐位置\r\n adjustKeyboardTo?: string\r\n}\r\nconst { disabled: isDisabled, isError, validate } = useFormInject(props)\r\n\r\n// 绑定值\r\nconst value = ref(props.modelValue)\r\n\r\n// 是否聚焦(样式作用)\r\nconst isFocus = ref<boolean>(props.autofocus)\r\n\r\n// 是否聚焦(输入框作用)\r\nconst isFocusing = ref<boolean>(props.autofocus)\r\n\r\nconst b = tv(theme)\r\n\r\nconst ui = computed(() => {\r\n const styles = b({\r\n size: props.size,\r\n border: props.border,\r\n focused: isFocus.value,\r\n disabled: isDisabled.value,\r\n error: isError.value,\r\n hasCount: props.showWordLimit,\r\n color: props.color,\r\n })\r\n return {\r\n root: (opts?: { class?: any }) =>\r\n styles.root({ class: cn(opts?.class, props.ui?.root) }),\r\n inner: (opts?: { class?: any }) =>\r\n styles.inner({ class: cn(opts?.class, props.ui?.inner) }),\r\n text: (opts?: { class?: any }) =>\r\n styles.text({ class: cn(opts?.class, props.ui?.text) }),\r\n }\r\n})\r\n\r\n// 文本框样式\r\nconst textareaStyle = computed(() => {\r\n const style: any = {}\r\n\r\n if (!props.autoHeight) {\r\n style.height = typeof props.height === 'number' ? `${props.height}rpx` : props.height\r\n }\r\n else {\r\n style.minHeight = '48rpx'\r\n }\r\n\r\n return style\r\n})\r\n\r\nconst placeholderStyle = computed(() => {\r\n return `${props.placeholderStyle}`\r\n})\r\n\r\n// 点击事件\r\nfunction onTap() {\r\n if (!isFocus.value) {\r\n focus()\r\n }\r\n}\r\n\r\n// 获取焦点事件\r\nfunction onFocus(e: any) {\r\n isFocus.value = true\r\n emit('focus', e)\r\n}\r\n\r\n// 失去焦点事件\r\nfunction onBlur(e: any) {\r\n emit('blur', e)\r\n if (validate) { validate('blur') }\r\n\r\n setTimeout(() => {\r\n isFocus.value = false\r\n }, 0)\r\n}\r\n\r\n// 输入事件\r\nfunction onInput(e: any) {\r\n const v1 = e.detail.value\r\n const v2 = value.value\r\n\r\n value.value = v1\r\n\r\n emit('update:modelValue', v1)\r\n emit('input', e)\r\n\r\n if (v1 != v2) {\r\n emit('change', v1)\r\n if (validate) { validate('change') }\r\n }\r\n}\r\n\r\n// 点击确认按钮事件\r\nfunction onConfirm(e: any) {\r\n emit('confirm', e)\r\n}\r\n\r\n// 键盘高度变化事件\r\nfunction onKeyboardheightchange(e: any) {\r\n emit('keyboardheightchange', e)\r\n}\r\n\r\n// 行数变化事件\r\nfunction onLineChange(e: any) {\r\n emit('linechange', e)\r\n}\r\n\r\n// 聚焦方法\r\nfunction focus() {\r\n setTimeout(() => {\r\n isFocusing.value = false\r\n\r\n nextTick(() => {\r\n isFocusing.value = true\r\n })\r\n }, 0)\r\n}\r\n\r\nwatch(\r\n computed(() => props.modelValue),\r\n (val: string) => {\r\n value.value = val\r\n },\r\n)\r\n\r\ndefineExpose({\r\n isFocus,\r\n focus,\r\n})\r\n</script>\r\n\r\n<template>\r\n <view :class=\"ui.root({ class: cn(props.customClass) })\" @tap=\"onTap\">\r\n <textarea :class=\"ui.inner()\" :style=\"textareaStyle\" :value=\"value\" :name=\"name\" :disabled=\"readonly || isDisabled\"\r\n :placeholder=\"placeholder\" :placeholder-class=\"`text-gray-4 ${placeholderClass}`\"\r\n :placeholder-style=\"placeholderStyle\" :maxlength=\"maxlength\" :focus=\"isFocusing\" :cursor=\"cursor\"\r\n :cursor-spacing=\"cursorSpacing\" :cursor-color=\"cursorColor\" :show-confirm-bar=\"showConfirmBar\"\r\n :confirm-hold=\"confirmHold\" :auto-height=\"autoHeight\" :fixed=\"fixed\" :adjust-position=\"adjustPosition\"\r\n :hold-keyboard=\"holdKeyboard\" :inputmode=\"inputmode\" :disable-default-padding=\"disableDefaultPadding\"\r\n :adjust-keyboard-to=\"adjustKeyboardTo\" @confirm=\"onConfirm\" @input=\"onInput\" @linechange=\"onLineChange\"\r\n @blur=\"onBlur\" @focus=\"onFocus\" @keyboardheightchange=\"onKeyboardheightchange\" />\r\n\r\n <slot v-if=\"showWordLimit\" name=\"limit\" :length=\"value.length\" :max=\"maxlength\">\r\n <text :size=\"12\" :class=\"ui.text()\">\r\n {{\r\n value.length }} / {{ maxlength }}\r\n </text>\r\n </slot>\r\n </view>\r\n</template>\r\n\r\n<style scoped>\r\n:deep(.uni-textarea-compute) {\r\n opacity: 0;\r\n}\r\n</style>\r\n",
28
28
  "target": "uniapp"
29
29
  }
30
30
  ],
31
31
  "fileCount": 5,
32
- "contentHash": "fedc1900eade691f74c88d4accd6290b2098fa0a"
32
+ "contentHash": "c092c74f3a17fcc23ba07e940936f7d2a9357fca"
33
33
  }
@@ -0,0 +1,38 @@
1
+ {
2
+ "name": "reborn-toast",
3
+ "dependencies": [],
4
+ "files": [
5
+ {
6
+ "path": "index.ts",
7
+ "content": "import type { Ref } from 'vue';\r\nimport { createVNode, getCurrentInstance, inject, provide, ref, render } from 'vue';\r\nimport RebornToast from './RebornToast.vue';\r\n\r\nexport interface ToastOptions {\r\n msg?: string;\r\n duration?: number;\r\n iconName?: 'success' | 'error' | 'warning' | 'loading' | 'info';\r\n position?: 'top' | 'middle-top' | 'middle' | 'bottom';\r\n show?: boolean;\r\n zIndex?: number;\r\n cover?: boolean;\r\n color?: 'primary' | 'secondary' | 'success' | 'info' | 'warning' | 'error' | 'neutral' | '';\r\n direction?: 'horizontal' | 'vertical';\r\n}\r\n\r\nconst toastDefaultOptionKey = '__REBORN_TOAST_OPTION__';\r\nexport const defaultOptions: ToastOptions = { duration: 2000, show: false };\r\nexport const getToastOptionKey = (selector = '') => (selector ? `${toastDefaultOptionKey}${selector}` : toastDefaultOptionKey);\r\n\r\nlet toastContainer: HTMLElement | null = null;\r\nlet globalTimer: ReturnType<typeof setTimeout> | null = null;\r\nconst globalOptionRef = ref<ToastOptions>({ ...defaultOptions });\r\n\r\nexport function useToast(selector = '') {\r\n const key = getToastOptionKey(selector);\r\n const instance = getCurrentInstance();\r\n const optionRef = inject<Ref<ToastOptions>>(key, globalOptionRef);\r\n\r\n if (typeof document !== 'undefined' && !toastContainer) {\r\n toastContainer = document.createElement('div');\r\n document.body.appendChild(toastContainer);\r\n\r\n const vnode = createVNode(RebornToast);\r\n if (instance) {\r\n vnode.appContext = instance.appContext;\r\n }\r\n if (vnode.appContext) {\r\n vnode.appContext.provides[key] = optionRef;\r\n }\r\n render(vnode, toastContainer);\r\n }\r\n\r\n provide(key, optionRef);\r\n\r\n const close = () => {\r\n if (globalTimer) clearTimeout(globalTimer);\r\n optionRef.value = { ...optionRef.value, show: false };\r\n };\r\n\r\n const show = (option: ToastOptions | string) => {\r\n const next = typeof option === 'string' ? { msg: option } : option;\r\n optionRef.value = { ...defaultOptions, ...next, show: true };\r\n\r\n if (globalTimer) clearTimeout(globalTimer);\r\n if ((optionRef.value.duration || 0) > 0) {\r\n globalTimer = setTimeout(close, optionRef.value.duration);\r\n }\r\n };\r\n\r\n const build = (base: ToastOptions) => (options: ToastOptions | string) => show({ ...base, ...(typeof options === 'string' ? { msg: options } : options) });\r\n\r\n return {\r\n show,\r\n close,\r\n loading: build({ iconName: 'loading', duration: 0, cover: true }),\r\n success: build({ iconName: 'success', duration: 1500 }),\r\n error: build({ iconName: 'error' }),\r\n warning: build({ iconName: 'warning' }),\r\n info: build({ iconName: 'info' }),\r\n };\r\n}\r\n\r\nexport { RebornToast };\r\n",
8
+ "target": "web"
9
+ },
10
+ {
11
+ "path": "reborn-toast.config.ts",
12
+ "content": "export const toastTheme = {\r\n root: 'inline-flex max-w-[70%] items-center justify-center rounded-xl bg-black/80 px-4 py-3 text-white shadow-lg transition-all duration-300',\r\n positions: { top: '-translate-y-[40vh]', 'middle-top': '-translate-y-[18vh]', middle: '', bottom: 'translate-y-[40vh]' },\r\n colors: {\r\n primary: 'bg-primary text-primary-foreground',\r\n secondary: 'bg-secondary text-secondary-foreground',\r\n success: 'bg-green-500 text-white',\r\n info: 'bg-blue-500 text-white',\r\n warning: 'bg-orange-500 text-white',\r\n error: 'bg-red-500 text-white',\r\n neutral: 'bg-gray-500 text-white',\r\n },\r\n msg: 'text-sm break-all text-center',\r\n};\r\n",
13
+ "target": "web"
14
+ },
15
+ {
16
+ "path": "RebornToast.vue",
17
+ "content": "<script setup lang=\"ts\">\r\nimport type { ToastOptions } from './index';\r\nimport { computed, inject, ref, watch } from 'vue';\r\nimport RebornOverlay from '../reborn-overlay/RebornOverlay.vue';\r\nimport RebornTransition from '../reborn-transition/RebornTransition.vue';\r\nimport { defaultOptions, getToastOptionKey } from './index';\r\nimport { toastTheme } from './reborn-toast.config';\r\n\r\nconst props = withDefaults(defineProps<{ selector?: string }>(), { selector: '' });\r\nconst key = getToastOptionKey(props.selector);\r\nconst optionRef = inject(key, ref<ToastOptions>(defaultOptions));\r\nconst state = ref<ToastOptions>({ ...defaultOptions });\r\nwatch(() => optionRef.value, (v) => { state.value = { ...v }; }, { immediate: true, deep: true });\r\n\r\nconst rootClass = computed(() => {\r\n const base = toastTheme.root;\r\n const pos = toastTheme.positions[state.value.position || 'top'];\r\n const customConfigColor = state.value.color ? toastTheme.colors[state.value.color as keyof typeof toastTheme.colors] : '';\r\n const finalBase = customConfigColor ? base.replace('bg-black/80', '').replace('text-white', '') + ` ${customConfigColor}` : base;\r\n return `${finalBase} ${pos}`;\r\n});\r\n\r\nconst getIconName = (name: string) => {\r\n if (name === 'loading') return 'lucide:loader-2';\r\n if (name === 'success') return 'lucide:circle-check';\r\n if (name === 'error') return 'lucide:circle-x';\r\n if (name === 'warning') return 'lucide:triangle-alert';\r\n if (name === 'info') return 'lucide:info';\r\n return name;\r\n};\r\n</script>\r\n<template>\r\n <RebornOverlay v-if=\"state.cover\" :model-value=\"!!state.show\" :z-index=\"state.zIndex || 100\"\r\n custom-style=\"background-color:transparent;pointer-events:auto;\" />\r\n <RebornTransition name=\"fade\" :show=\"!!state.show\"\r\n :custom-style=\"`z-index:${state.zIndex || 100};position:fixed;left:0;top:50%;width:100%;transform:translateY(-50%);text-align:center;pointer-events:none;`\">\r\n <div :class=\"rootClass\">\r\n <Transition name=\"fade-content\" mode=\"out-in\">\r\n <div :key=\"state.msg + (state.iconName || '') + (state.color || '')\"\r\n class=\"flex items-center justify-center transition-all duration-300\"\r\n :class=\"{ 'flex-col': state.direction === 'vertical' }\">\r\n <Icon v-if=\"state.iconName\" :name=\"getIconName(state.iconName)\" :class=\"[\r\n state.iconName === 'loading' ? 'animate-spin' : '',\r\n state.iconName === 'success' ? 'text-success' : '',\r\n state.iconName === 'error' ? 'text-error' : '',\r\n state.iconName === 'warning' ? 'text-warning' : '',\r\n state.iconName === 'info' ? 'text-info' : '',\r\n state.direction === 'vertical' ? 'mb-2 text-3xl' : 'mr-2 text-xl'\r\n ]\" />\r\n <span :class=\"toastTheme.msg\">{{ state.msg }}</span>\r\n </div>\r\n </Transition>\r\n </div>\r\n </RebornTransition>\r\n</template>\r\n\r\n<style scoped>\r\n.fade-content-enter-active,\r\n.fade-content-leave-active {\r\n transition: opacity 0.2s ease, transform 0.2s ease;\r\n}\r\n\r\n.fade-content-enter-from,\r\n.fade-content-leave-to {\r\n opacity: 0;\r\n transform: scale(0.95);\r\n}\r\n</style>\r\n",
18
+ "target": "web"
19
+ },
20
+ {
21
+ "path": "index.ts",
22
+ "content": "import { inject, provide, ref, render, createVNode, getCurrentInstance } from 'vue'\r\nimport { deepMerge } from '@/lib/util'\r\n\r\nexport type ToastIconType = 'success' | 'error' | 'warning' | 'loading' | 'info'\r\nexport type ToastPositionType = 'top' | 'middle-top' | 'middle' | 'bottom'\r\nexport type ToastDirection = 'vertical' | 'horizontal'\r\nexport type ToastLoadingType = 'outline' | 'ring' | 'spinner' | 'bars-scale' | 'blocks-shuffle' | 'blocks-wave' | 'gooey-balls'\r\n\r\nexport type ToastColorType = 'primary' | 'secondary' | 'success' | 'info' | 'warning' | 'error' | 'neutral'\r\n\r\nexport type ToastOptions = {\r\n msg?: string\r\n duration?: number\r\n direction?: ToastDirection\r\n iconName?: ToastIconType\r\n iconSize?: number\r\n loadingType?: ToastLoadingType\r\n loadingColor?: string\r\n loadingSize?: number\r\n position?: ToastPositionType\r\n show?: boolean\r\n zIndex?: number\r\n cover?: boolean\r\n iconClass?: string\r\n classPrefix?: string\r\n color?: ToastColorType\r\n opened?: () => void\r\n closed?: () => void\r\n}\r\n\r\nconst toastDefaultOptionKey = '__REBORN_TOAST_OPTION__'\r\nexport const defaultOptions: ToastOptions = { duration: 2000, show: false }\r\n\r\nexport function getToastOptionKey(selector: string) {\r\n return selector ? `${toastDefaultOptionKey}${selector}` : toastDefaultOptionKey\r\n}\r\n\r\nimport RebornToast from './RebornToast.vue'\r\n\r\n// 全局唯一的组件挂载节点与实例状态\r\nlet toastContainer: HTMLElement | null = null\r\nlet globalTimer: ReturnType<typeof setTimeout> | null = null\r\n\r\n// 全局唯一的配置,保证应用中同一时刻只有一个 Toast 实际在工作\r\nexport const globalOptionRef = ref<ToastOptions>({ ...defaultOptions })\r\n\r\nexport function useToast(selector = '') {\r\n const key = getToastOptionKey(selector)\r\n const instance = getCurrentInstance()\r\n\r\n // 尝试在当前上下文中挂载或获取,以兼容不同端\r\n let optionRef = inject(key, globalOptionRef)\r\n\r\n // #ifdef H5\r\n if (!toastContainer) {\r\n toastContainer = document.createElement('div')\r\n toastContainer.id = 'reborn-toast-container'\r\n document.body.appendChild(toastContainer)\r\n\r\n const vnode = createVNode(RebornToast)\r\n\r\n // 桥接当前上下文,以支持 router, pinia 等\r\n if (instance) {\r\n vnode.appContext = instance.appContext\r\n } else if (!vnode.appContext) {\r\n vnode.appContext = { provides: {} } as any\r\n }\r\n\r\n // 极重要:在这里 provide key 使得动态渲染的 RebornToast 能 inject 到这个 Ref\r\n if (vnode.appContext) {\r\n if (!vnode.appContext.provides) vnode.appContext.provides = {}\r\n vnode.appContext.provides[key] = optionRef\r\n }\r\n\r\n render(vnode, toastContainer)\r\n }\r\n // #endif\r\n\r\n // 向下提供引用给可能写在页面里的 <RebornToast />,保证内外使用的 optionRef 永远映射到同一个实例\r\n provide(key, optionRef)\r\n\r\n const close = () => {\r\n if (globalTimer) {\r\n clearTimeout(globalTimer)\r\n globalTimer = null\r\n }\r\n optionRef.value = { ...optionRef.value, show: false }\r\n }\r\n\r\n const show = (option: ToastOptions | string) => {\r\n const next = deepMerge(defaultOptions, typeof option === 'string' ? { msg: option } : option)\r\n\r\n // 保证每次 show 时都是立刻覆盖当前唯一的提示\r\n optionRef.value = deepMerge(next, { show: true })\r\n\r\n if (globalTimer) {\r\n clearTimeout(globalTimer)\r\n globalTimer = null\r\n }\r\n\r\n if (next.duration !== undefined && next.duration > 0) {\r\n globalTimer = setTimeout(close, next.duration)\r\n }\r\n }\r\n\r\n const build = (base: ToastOptions) => (options: ToastOptions | string) => show(deepMerge(base, typeof options === 'string' ? { msg: options } : options))\r\n return {\r\n show,\r\n close,\r\n loading: build({ iconName: 'loading', duration: 0, cover: true }),\r\n success: build({ iconName: 'success', duration: 1500 }),\r\n error: build({ iconName: 'error' }),\r\n warning: build({ iconName: 'warning' }),\r\n info: build({ iconName: 'info' }),\r\n }\r\n}\r\n\r\nexport const toastIcon = {\r\n success() {\r\n return '<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"2 2 44 44\" width=\"48\" height=\"48\"><circle cx=\"24\" cy=\"26\" r=\"22\" fill=\"#000\" opacity=\".1\"/><circle cx=\"24\" cy=\"24\" r=\"20\" fill=\"#34D19D\" opacity=\".4\"/><circle cx=\"24\" cy=\"24\" r=\"16\" fill=\"#34D19D\"/><path d=\"M19 24l4 4 8-8\" stroke=\"#FFF\" stroke-width=\"2.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\" fill=\"none\"/></svg>'\r\n },\r\n warning() {\r\n return '<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"2 2 44 44\" width=\"48\" height=\"48\"><circle cx=\"24\" cy=\"26\" r=\"22\" fill=\"#000\" opacity=\".1\"/><circle cx=\"24\" cy=\"24\" r=\"20\" fill=\"#F0883A\" opacity=\".4\"/><circle cx=\"24\" cy=\"24\" r=\"16\" fill=\"#F0883A\"/><rect x=\"22.5\" y=\"14\" width=\"3\" height=\"12\" fill=\"#FFF\" rx=\"1.5\"/><circle cx=\"24\" cy=\"30\" r=\"2\" fill=\"#FFF\"/></svg>'\r\n },\r\n info() {\r\n return '<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"2 2 44 44\" width=\"48\" height=\"48\"><circle cx=\"24\" cy=\"26\" r=\"22\" fill=\"#000\" opacity=\".1\"/><circle cx=\"24\" cy=\"24\" r=\"20\" fill=\"#909CB7\" opacity=\".4\"/><circle cx=\"24\" cy=\"24\" r=\"16\" fill=\"#909CB7\"/><circle cx=\"24\" cy=\"18\" r=\"2\" fill=\"#FFF\"/><rect x=\"22.5\" y=\"22\" width=\"3\" height=\"12\" fill=\"#FFF\" rx=\"1.5\"/></svg>'\r\n },\r\n error() {\r\n return '<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"2 2 44 44\" width=\"48\" height=\"48\"><circle cx=\"24\" cy=\"26\" r=\"22\" fill=\"#000\" opacity=\".1\"/><circle cx=\"24\" cy=\"24\" r=\"20\" fill=\"#fa4350\" opacity=\".4\"/><circle cx=\"24\" cy=\"24\" r=\"16\" fill=\"#fa4350\"/><path d=\"M18 18l12 12M30 18L18 30\" stroke=\"#FFF\" stroke-width=\"2.5\" stroke-linecap=\"round\"/></svg>'\r\n }\r\n}\r\nexport { RebornToast }\r\n",
23
+ "target": "uniapp"
24
+ },
25
+ {
26
+ "path": "reborn-toast.config.ts",
27
+ "content": "export const toastColors = ['default', 'primary', 'secondary', 'success', 'info', 'warning', 'error', 'neutral'] as const\r\nexport const toastPositions = ['top', 'middle-top', 'middle', 'bottom'] as const\r\nexport const toastDirections = ['horizontal', 'vertical'] as const\r\n\r\nconst config = {\r\n slots: {\r\n root: 'rb-toast inline-block max-w-[70%] rounded-xl px-4 py-3 shadow-lg transition-all duration-300',\r\n msg: 'text-left text-24 break-all',\r\n },\r\n variants: {\r\n position: {\r\n top: { root: '-translate-y-[40vh]' },\r\n 'middle-top': { root: '-translate-y-[18.8vh]' },\r\n middle: { root: '' },\r\n bottom: { root: 'translate-y-[40vh]' },\r\n },\r\n direction: {\r\n horizontal: { root: '' },\r\n vertical: { root: 'flex-col' },\r\n },\r\n withIcon: {\r\n true: { root: 'inline-flex items-center' },\r\n false: { root: '' },\r\n },\r\n color: {\r\n default: { root: 'bg-black/80 text-white' },\r\n primary: { root: 'bg-primary text-primary-foreground' },\r\n secondary: { root: 'bg-secondary text-secondary-foreground' },\r\n success: { root: 'bg-success text-success-foreground' },\r\n info: { root: 'bg-info text-info-foreground' },\r\n warning: { root: 'bg-warning text-warning-foreground' },\r\n error: { root: 'bg-error text-error-foreground' },\r\n neutral: { root: 'bg-neutral text-neutral-foreground' },\r\n },\r\n },\r\n defaultVariants: {\r\n color: 'default',\r\n position: 'middle-top',\r\n direction: 'horizontal',\r\n withIcon: false,\r\n },\r\n} as const\r\n\r\nexport default config\r\n",
28
+ "target": "uniapp"
29
+ },
30
+ {
31
+ "path": "RebornToast.vue",
32
+ "content": "<script lang=\"ts\">\r\nexport default {\r\n name: 'reborn-toast',\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 setup lang=\"ts\">\r\nimport { computed, inject, ref, watch } from 'vue'\r\nimport RebornLoading from '@/components/reborn-loading/RebornLoading.vue'\r\nimport RebornOverlay from '@/components/reborn-overlay/RebornOverlay.vue'\r\nimport RebornTransition from '@/components/reborn-transition/RebornTransition.vue'\r\nimport base64 from '@/lib/base64'\r\nimport { addUnit, isDef, isFunction } from '@/lib/util'\r\nimport { tv } from '@/lib/tv'\r\nimport { cn } from '@/lib/utils'\r\nimport { globalOptionRef, getToastOptionKey, toastIcon, type ToastDirection, type ToastOptions } from './index'\r\nimport theme, { toastColors, toastDirections, toastPositions } from './reborn-toast.config'\r\nimport type { ToastLoadingType, ToastIconType, ToastPositionType } from './index'\r\n\r\ninterface ToastProps {\r\n selector?: string\r\n msg?: string\r\n direction?: ToastDirection\r\n iconName?: ToastIconType | ''\r\n iconSize?: number\r\n loadingType?: ToastLoadingType\r\n loadingColor?: string\r\n loadingSize?: number\r\n position?: ToastPositionType\r\n zIndex?: number\r\n cover?: boolean\r\n iconClass?: string\r\n classPrefix?: string\r\n color?: Exclude<typeof toastColors[number], 'default'> | ''\r\n opened?: () => void\r\n closed?: () => void\r\n customClass?: string\r\n}\r\n\r\nconst props = withDefaults(defineProps<ToastProps>(), {\r\n selector: '',\r\n msg: '',\r\n direction: 'horizontal',\r\n iconName: '',\r\n loadingType: 'outline',\r\n loadingColor: '#4D80F0',\r\n position: 'middle-top',\r\n zIndex: 100,\r\n cover: false,\r\n iconClass: '',\r\n classPrefix: 'rb-icon',\r\n customClass: '',\r\n color: '',\r\n loadingSize: 40,\r\n iconSize: 40,\r\n})\r\nconst show = ref(false)\r\nconst msg = ref('')\r\nconst iconName = ref<ToastProps['iconName']>('')\r\nconst svgStr = ref('')\r\nconst cover = ref(false)\r\nconst position = ref(props.position)\r\nconst zIndex = ref(props.zIndex)\r\nconst direction = ref(props.direction)\r\nconst loadingType = ref(props.loadingType)\r\nconst loadingColor = ref(props.loadingColor)\r\nconst loadingSize = ref<string | undefined>(undefined)\r\nconst iconSize = ref<string | undefined>(undefined)\r\nconst color = ref<ToastProps['color']>(props.color)\r\nlet opened: (() => void) | null = null\r\nlet closed: (() => void) | null = null\r\n\r\nconst key = getToastOptionKey(props.selector)\r\nconst toastOption = inject(key, globalOptionRef)\r\nwatch(() => toastOption.value, (val) => reset(val), { immediate: true, deep: true })\r\nwatch(() => iconName.value, buildSvg, { immediate: true })\r\n\r\nconst b = tv(theme)\r\nconst transitionStyle = computed(() => `z-index:${zIndex.value};position:fixed;left:0;top:50%;width:100%;transform:translate(0,-50%);text-align:center;pointer-events:none;`)\r\nconst ui = computed(() => {\r\n const styles = b({\r\n position: position.value as typeof toastPositions[number],\r\n direction: direction.value as typeof toastDirections[number],\r\n withIcon: Boolean(iconName.value && (iconName.value !== 'loading' || msg.value)),\r\n color: (color.value || 'default') as typeof toastColors[number],\r\n })\r\n\r\n return {\r\n root: (opts?: { class?: any }) => styles.root({ class: cn(opts?.class) }),\r\n msg: (opts?: { class?: any }) => styles.msg({ class: cn(opts?.class) }),\r\n }\r\n})\r\nconst svgStyle = computed(() => {\r\n const size = iconSize.value || '30rpx'\r\n return `background-image:url(${svgStr.value});width:${size};height:${size};`\r\n})\r\n\r\nconst onAfterEnter = () => isFunction(opened) && opened()\r\nconst onAfterLeave = () => isFunction(closed) && closed()\r\n\r\nfunction buildSvg() {\r\n if (!iconName.value || iconName.value === 'loading') return\r\n svgStr.value = `data:image/svg+xml;base64,${base64(toastIcon[iconName.value as keyof typeof toastIcon]())}`\r\n}\r\n\r\nfunction reset(option: ToastOptions) {\r\n show.value = !!option.show\r\n if (!show.value) return\r\n iconName.value = option.iconName ?? props.iconName\r\n msg.value = option.msg ?? props.msg\r\n position.value = option.position ?? props.position\r\n zIndex.value = option.zIndex ?? props.zIndex\r\n cover.value = option.cover ?? props.cover\r\n direction.value = option.direction ?? props.direction\r\n loadingType.value = option.loadingType ?? props.loadingType\r\n loadingColor.value = option.loadingColor ?? props.loadingColor\r\n color.value = option.color ?? props.color\r\n iconSize.value = isDef(option.iconSize) ? addUnit(option.iconSize) : isDef(props.iconSize) ? addUnit(props.iconSize) : undefined\r\n loadingSize.value = isDef(option.loadingSize) ? addUnit(option.loadingSize) : isDef(props.loadingSize) ? addUnit(props.loadingSize) : undefined\r\n closed = option.closed ?? props.closed ?? null\r\n opened = option.opened ?? props.opened ?? null\r\n}\r\n</script>\r\n\r\n<template>\r\n <RebornOverlay v-if=\"cover\" :show=\"show\" :z-index=\"zIndex\" lock-scroll\r\n custom-style=\"background-color:transparent;pointer-events:auto;\" />\r\n <RebornTransition name=\"fade\" :show=\"show\" :custom-style=\"transitionStyle\" @after-enter=\"onAfterEnter\"\r\n @after-leave=\"onAfterLeave\">\r\n <view :class=\"ui.root({ class: props.customClass })\">\r\n <RebornLoading v-if=\"iconName === 'loading'\" :type=\"loadingType\" :color=\"loadingColor as any\"\r\n :size=\"loadingSize\" :custom-class=\"direction === 'vertical' ? 'mb-2' : 'mr-2'\" />\r\n <view v-else-if=\"iconName\"\r\n :class=\"direction === 'vertical' ? 'mb-2 mx-auto inline-block bg-cover bg-no-repeat' : 'mr-2 inline-block bg-cover bg-no-repeat'\"\r\n :style=\"svgStyle\" />\r\n <view v-if=\"msg\" :class=\"ui.msg()\">{{ msg }}</view>\r\n </view>\r\n </RebornTransition>\r\n</template>\r\n",
33
+ "target": "uniapp"
34
+ }
35
+ ],
36
+ "fileCount": 6,
37
+ "contentHash": "649c786f4bd2b1445cee9952c7c5b3ef197500b3"
38
+ }
@@ -0,0 +1,38 @@
1
+ {
2
+ "name": "reborn-transition",
3
+ "dependencies": [],
4
+ "files": [
5
+ {
6
+ "path": "index.ts",
7
+ "content": "export { default as RebornTransition } from './RebornTransition.vue';\r\n",
8
+ "target": "web"
9
+ },
10
+ {
11
+ "path": "reborn-transition.config.ts",
12
+ "content": "export const transitionStyles: Record<string, Record<string, string>> = {\r\n fade: {\r\n enter: 'opacity-0',\r\n 'enter-active': 'transition-opacity',\r\n 'enter-to': 'opacity-100',\r\n leave: 'opacity-100',\r\n 'leave-active': 'transition-opacity',\r\n 'leave-to': 'opacity-0',\r\n },\r\n 'fade-up': {\r\n enter: 'translate-y-full opacity-0',\r\n 'enter-active': 'transition-[opacity,transform]',\r\n 'enter-to': 'translate-y-0 opacity-100',\r\n leave: 'translate-y-0 opacity-100',\r\n 'leave-active': 'transition-[opacity,transform]',\r\n 'leave-to': 'translate-y-full opacity-0',\r\n },\r\n 'fade-down': {\r\n enter: '-translate-y-full opacity-0',\r\n 'enter-active': 'transition-[opacity,transform]',\r\n 'enter-to': 'translate-y-0 opacity-100',\r\n leave: 'translate-y-0 opacity-100',\r\n 'leave-active': 'transition-[opacity,transform]',\r\n 'leave-to': '-translate-y-full opacity-0',\r\n },\r\n 'fade-left': {\r\n enter: 'translate-x-full opacity-0',\r\n 'enter-active': 'transition-[opacity,transform]',\r\n 'enter-to': 'translate-x-0 opacity-100',\r\n leave: 'translate-x-0 opacity-100',\r\n 'leave-active': 'transition-[opacity,transform]',\r\n 'leave-to': 'translate-x-full opacity-0',\r\n },\r\n 'fade-right': {\r\n enter: '-translate-x-full opacity-0',\r\n 'enter-active': 'transition-[opacity,transform]',\r\n 'enter-to': 'translate-x-0 opacity-100',\r\n leave: 'translate-x-0 opacity-100',\r\n 'leave-active': 'transition-[opacity,transform]',\r\n 'leave-to': '-translate-x-full opacity-0',\r\n },\r\n 'slide-up': {\r\n enter: 'translate-y-full',\r\n 'enter-active': 'transition-transform',\r\n 'enter-to': 'translate-y-0',\r\n leave: 'translate-y-0',\r\n 'leave-active': 'transition-transform',\r\n 'leave-to': 'translate-y-full',\r\n },\r\n 'slide-right': {\r\n enter: 'translate-x-[100%]',\r\n 'enter-active': 'transition-transform',\r\n 'enter-to': 'translate-x-0',\r\n leave: 'translate-x-0',\r\n 'leave-active': 'transition-transform',\r\n 'leave-to': 'translate-x-[100%]',\r\n },\r\n 'slide-down': {\r\n enter: '-translate-y-[100%]',\r\n 'enter-active': 'transition-transform',\r\n 'enter-to': 'translate-y-0',\r\n leave: 'translate-y-0',\r\n 'leave-active': 'transition-transform',\r\n 'leave-to': '-translate-y-[100%]',\r\n },\r\n 'slide-left': {\r\n enter: '-translate-x-[100%]',\r\n 'enter-active': 'transition-transform',\r\n 'enter-to': 'translate-x-0',\r\n leave: 'translate-x-0',\r\n 'leave-active': 'transition-transform',\r\n 'leave-to': '-translate-x-[100%]',\r\n },\r\n 'zoom-in': {\r\n enter: 'opacity-0 scale-[0.8]',\r\n 'enter-active': 'transition-[opacity,transform]',\r\n 'enter-to': 'opacity-100 scale-100',\r\n leave: 'opacity-100 scale-100',\r\n 'leave-active': 'transition-[opacity,transform]',\r\n 'leave-to': 'opacity-0 scale-[0.8]',\r\n },\r\n 'zoom-out': {\r\n enter: 'opacity-0 scale-[1.2]',\r\n 'enter-active': 'transition-[opacity,transform]',\r\n 'enter-to': 'opacity-100 scale-100',\r\n leave: 'opacity-100 scale-100',\r\n 'leave-active': 'transition-[opacity,transform]',\r\n 'leave-to': 'opacity-0 scale-[1.2]',\r\n },\r\n};\r\n\r\nexport type TransitionName = keyof typeof transitionStyles;\r\n",
13
+ "target": "web"
14
+ },
15
+ {
16
+ "path": "RebornTransition.vue",
17
+ "content": "<script setup lang=\"ts\">\r\nimport type { TransitionName } from './reborn-transition.config';\r\nimport { computed, ref, watch } from 'vue';\r\nimport { transitionStyles } from './reborn-transition.config';\r\n\r\nconst props = withDefaults(defineProps<{\r\n show?: boolean;\r\n duration?: number | { enter?: number; leave?: number } | boolean;\r\n lazyRender?: boolean;\r\n name?: TransitionName | TransitionName[];\r\n destroy?: boolean;\r\n customClass?: string;\r\n customStyle?: string;\r\n disableTouchMove?: boolean;\r\n}>(), {\r\n show: false,\r\n duration: 300,\r\n lazyRender: false,\r\n destroy: true,\r\n customClass: '',\r\n customStyle: '',\r\n disableTouchMove: false,\r\n});\r\n\r\nconst emit = defineEmits(['click', 'beforeEnter', 'enter', 'beforeLeave', 'afterLeave', 'leave', 'afterEnter']);\r\n\r\nconst durationOf = (type: 'enter' | 'leave') => (typeof props.duration === 'object'\r\n ? (props.duration[type] ?? 300)\r\n : (props.duration === false ? 0 : Number(props.duration)));\r\n\r\nconst style = computed(() => `${props.customStyle}`);\r\n\r\nfunction getClassNames(name = props.name ?? \"fade\") {\r\n const names = Array.isArray(name) ? name : [name];\r\n const picked = names.map(n => transitionStyles[n]).filter((it): it is Record<string, string> => !!it);\r\n const join = (key: string) => picked.map(it => it[key]).join(' ');\r\n return {\r\n enterFrom: join('enter'),\r\n enterActive: join('enter-active'),\r\n enterTo: join('enter-to'),\r\n leaveFrom: join('leave'),\r\n leaveActive: join('leave-active'),\r\n leaveTo: join('leave-to'),\r\n };\r\n}\r\n\r\nconst transitionClasses = computed(() => getClassNames());\r\nconst renderReady = ref(!props.lazyRender);\r\n\r\nwatch(() => props.show, (val) => {\r\n if (val && props.lazyRender) renderReady.value = true;\r\n}, { immediate: true });\r\n</script>\r\n\r\n<template>\r\n <Transition :enter-from-class=\"transitionClasses.enterFrom\" :enter-active-class=\"transitionClasses.enterActive\"\r\n :enter-to-class=\"transitionClasses.enterTo\" :leave-from-class=\"transitionClasses.leaveFrom\"\r\n :leave-active-class=\"transitionClasses.leaveActive\" :leave-to-class=\"transitionClasses.leaveTo\"\r\n @before-enter=\"emit('beforeEnter')\" @enter=\"emit('enter')\" @after-enter=\"emit('afterEnter')\"\r\n @before-leave=\"emit('beforeLeave')\" @leave=\"emit('leave')\" @after-leave=\"emit('afterLeave')\">\r\n <div v-if=\"renderReady && (props.destroy ? props.show : true)\" v-show=\"props.destroy ? true : props.show\"\r\n :class=\"`rb-transition ease-in-out ${props.customClass}`\"\r\n :style=\"`transition-duration: ${durationOf(props.show ? 'enter' : 'leave')}ms; ${style}`\" @click=\"emit('click')\">\r\n <slot />\r\n </div>\r\n </Transition>\r\n</template>\r\n",
18
+ "target": "web"
19
+ },
20
+ {
21
+ "path": "index.ts",
22
+ "content": "// @ts-ignore\r\nimport RebornTransition from './RebornTransition.vue'\r\n\r\nexport default RebornTransition\r\nexport { RebornTransition }\r\nexport type { TransitionName } from './RebornTransition.vue'\r\n",
23
+ "target": "uniapp"
24
+ },
25
+ {
26
+ "path": "reborn-transition.config.ts",
27
+ "content": "export const transitionStyles: Record<string, Record<string, string>> = {\r\n fade: {\r\n enter: 'opacity-0',\r\n 'enter-active': 'transition-opacity',\r\n 'enter-to': 'opacity-100',\r\n leave: 'opacity-100',\r\n 'leave-active': 'transition-opacity',\r\n 'leave-to': 'opacity-0'\r\n },\r\n 'fade-up': {\r\n enter: 'translate-y-full opacity-0',\r\n 'enter-active': 'transition-[opacity,transform]',\r\n 'enter-to': 'translate-y-0 opacity-100',\r\n leave: 'translate-y-0 opacity-100',\r\n 'leave-active': 'transition-[opacity,transform]',\r\n 'leave-to': 'translate-y-full opacity-0'\r\n },\r\n 'fade-down': {\r\n enter: '-translate-y-full opacity-0',\r\n 'enter-active': 'transition-[opacity,transform]',\r\n 'enter-to': 'translate-y-0 opacity-100',\r\n leave: 'translate-y-0 opacity-100',\r\n 'leave-active': 'transition-[opacity,transform]',\r\n 'leave-to': '-translate-y-full opacity-0'\r\n },\r\n 'fade-left': {\r\n enter: '-translate-x-full opacity-0',\r\n 'enter-active': 'transition-[opacity,transform]',\r\n 'enter-to': 'translate-x-0 opacity-100',\r\n leave: 'translate-x-0 opacity-100',\r\n 'leave-active': 'transition-[opacity,transform]',\r\n 'leave-to': '-translate-x-full opacity-0'\r\n },\r\n 'fade-right': {\r\n enter: 'translate-x-full opacity-0',\r\n 'enter-active': 'transition-[opacity,transform]',\r\n 'enter-to': 'translate-x-0 opacity-100',\r\n leave: 'translate-x-0 opacity-100',\r\n 'leave-active': 'transition-[opacity,transform]',\r\n 'leave-to': 'translate-x-full opacity-0'\r\n },\r\n 'slide-up': {\r\n enter: 'translate-y-full',\r\n 'enter-active': 'transition-transform',\r\n 'enter-to': 'translate-y-0',\r\n leave: 'translate-y-0',\r\n 'leave-active': 'transition-transform',\r\n 'leave-to': 'translate-y-full'\r\n },\r\n 'slide-down': {\r\n enter: '-translate-y-full',\r\n 'enter-active': 'transition-transform',\r\n 'enter-to': 'translate-y-0',\r\n leave: 'translate-y-0',\r\n 'leave-active': 'transition-transform',\r\n 'leave-to': '-translate-y-full'\r\n },\r\n 'slide-left': {\r\n enter: '-translate-x-full',\r\n 'enter-active': 'transition-transform',\r\n 'enter-to': 'translate-x-0',\r\n leave: 'translate-x-0',\r\n 'leave-active': 'transition-transform',\r\n 'leave-to': '-translate-x-full'\r\n },\r\n 'slide-right': {\r\n enter: 'translate-x-full',\r\n 'enter-active': 'transition-transform',\r\n 'enter-to': 'translate-x-0',\r\n leave: 'translate-x-0',\r\n 'leave-active': 'transition-transform',\r\n 'leave-to': 'translate-x-full'\r\n },\r\n 'zoom-in': {\r\n enter: 'opacity-0 scale-[0.8]',\r\n 'enter-active': 'transition-[opacity,transform]',\r\n 'enter-to': 'opacity-100 scale-100',\r\n leave: 'opacity-100 scale-100',\r\n 'leave-active': 'transition-[opacity,transform]',\r\n 'leave-to': 'opacity-0 scale-[0.8]'\r\n },\r\n 'zoom-out': {\r\n enter: 'opacity-0 scale-125',\r\n 'enter-active': 'transition-[opacity,transform]',\r\n 'enter-to': 'opacity-100 scale-100',\r\n leave: 'opacity-100 scale-100',\r\n 'leave-active': 'transition-[opacity,transform]',\r\n 'leave-to': 'opacity-0 scale-125'\r\n }\r\n}\r\n",
28
+ "target": "uniapp"
29
+ },
30
+ {
31
+ "path": "RebornTransition.vue",
32
+ "content": "<script lang=\"ts\">\r\nexport default {\r\n name: 'reborn-transition',\r\n options: {\r\n addGlobalClass: true,\r\n virtualHost: true,\r\n styleIsolation: 'shared'\r\n }\r\n}\r\n</script>\r\n\r\n<script lang=\"ts\" setup>\r\nimport { computed, onBeforeMount, ref, watch, type PropType } from 'vue'\r\nimport { isObj, isPromise, pause } from '../../lib/util'\r\nimport { AbortablePromise } from '../../lib/AbortablePromise'\r\nimport { transitionStyles } from './reborn-transition.config'\r\n\r\nexport type TransitionName =\r\n | 'fade'\r\n | 'fade-down'\r\n | 'fade-left'\r\n | 'fade-right'\r\n | 'fade-up'\r\n | 'slide-down'\r\n | 'slide-left'\r\n | 'slide-right'\r\n | 'slide-up'\r\n | 'zoom-in'\r\n | 'zoom-out'\r\n\r\nconst props = defineProps({\r\n customClass: { type: String, default: '' },\r\n customStyle: { type: String, default: '' },\r\n show: { type: Boolean, default: false },\r\n duration: { type: [Object, Number, Boolean] as PropType<Record<string, number> | number | boolean>, default: 300 },\r\n lazyRender: { type: Boolean, default: false },\r\n name: { type: [String, Array] as PropType<TransitionName | TransitionName[]>, default: 'fade' },\r\n destroy: { type: Boolean, default: true },\r\n enterClass: { type: String, default: '' },\r\n enterActiveClass: { type: String, default: '' },\r\n enterToClass: { type: String, default: '' },\r\n leaveClass: { type: String, default: '' },\r\n leaveActiveClass: { type: String, default: '' },\r\n leaveToClass: { type: String, default: '' },\r\n disableTouchMove: { type: Boolean, default: false }\r\n})\r\n\r\nconst emit = defineEmits(['click', 'before-enter', 'enter', 'before-leave', 'leave', 'after-leave', 'after-enter'])\r\n\r\nconst inited = ref<boolean>(false)\r\nconst display = ref<boolean>(false)\r\nconst status = ref<string>('')\r\nconst transitionEnded = ref<boolean>(false)\r\nconst currentDuration = ref<number>(300)\r\nconst classes = ref<string>('')\r\nconst enterPromise = ref<AbortablePromise<void> | null>(null)\r\nconst enterLifeCyclePromises = ref<AbortablePromise<unknown> | null>(null)\r\nconst leaveLifeCyclePromises = ref<AbortablePromise<unknown> | null>(null)\r\n\r\nconst getClassNames = (name?: TransitionName | TransitionName[]) => {\r\n let enter: string = `${props.enterClass} ${props.enterActiveClass}`\r\n let enterTo: string = `${props.enterToClass} ${props.enterActiveClass}`\r\n let leave: string = `${props.leaveClass} ${props.leaveActiveClass}`\r\n let leaveTo: string = `${props.leaveToClass} ${props.leaveActiveClass}`\r\n\r\n const addClass = (n: string) => {\r\n const config = transitionStyles[n]\r\n if (config) {\r\n enter = `rb-transition-${n}-enter ${config.enter} rb-transition-${n}-enter-active ${config['enter-active']} ${enter}`\r\n enterTo = `rb-transition-${n}-enter-to ${config['enter-to']} rb-transition-${n}-enter-active ${config['enter-active']} ${enterTo}`\r\n leave = `rb-transition-${n}-leave ${config.leave} rb-transition-${n}-leave-active ${config['leave-active']} ${leave}`\r\n leaveTo = `rb-transition-${n}-leave-to ${config['leave-to']} rb-transition-${n}-leave-active ${config['leave-active']} ${leaveTo}`\r\n } else {\r\n enter = `rb-${n}-enter rb-${n}-enter-active ${enter}`\r\n enterTo = `rb-${n}-enter-to rb-${n}-enter-active ${enterTo}`\r\n leave = `rb-${n}-leave rb-${n}-leave-active ${leave}`\r\n leaveTo = `rb-${n}-leave-to rb-${n}-leave-active ${leaveTo}`\r\n }\r\n }\r\n\r\n if (Array.isArray(name)) {\r\n name.forEach(n => addClass(n))\r\n } else if (name) {\r\n addClass(name as string)\r\n }\r\n\r\n return { enter, 'enter-to': enterTo, leave, 'leave-to': leaveTo }\r\n}\r\n\r\nconst style = computed(() => {\r\n return `-webkit-transition-duration:${currentDuration.value}ms;transition-duration:${currentDuration.value}ms;${display.value || !props.destroy ? '' : 'display: none;'\r\n }${props.customStyle}`\r\n})\r\n\r\nconst rootClass = computed(() => `rb-transition ease-in-out ${props.customClass} ${classes.value}`)\r\n\r\nconst isShow = computed(() => !props.lazyRender || inited.value)\r\n\r\nonBeforeMount(() => { if (props.show) enter() })\r\nwatch(() => props.show, (newVal) => handleShow(newVal), { deep: true })\r\nfunction handleClick() { emit('click') }\r\nfunction handleShow(value: boolean) {\r\n if (value) { handleAbortPromise(); enter() } else leave()\r\n}\r\nfunction handleAbortPromise() {\r\n isPromise(enterPromise.value) && enterPromise.value.abort()\r\n isPromise(enterLifeCyclePromises.value) && enterLifeCyclePromises.value.abort()\r\n isPromise(leaveLifeCyclePromises.value) && leaveLifeCyclePromises.value.abort()\r\n enterPromise.value = null\r\n enterLifeCyclePromises.value = null\r\n leaveLifeCyclePromises.value = null\r\n}\r\nfunction enter() {\r\n enterPromise.value = new AbortablePromise(async (resolve) => {\r\n try {\r\n const classNames = getClassNames(props.name)\r\n const duration = isObj(props.duration) ? (props.duration as any).enter : props.duration\r\n status.value = 'enter'\r\n emit('before-enter')\r\n enterLifeCyclePromises.value = pause()\r\n await enterLifeCyclePromises.value\r\n emit('enter')\r\n classes.value = classNames.enter\r\n currentDuration.value = duration\r\n enterLifeCyclePromises.value = pause()\r\n await enterLifeCyclePromises.value\r\n inited.value = true\r\n display.value = true\r\n enterLifeCyclePromises.value = pause()\r\n await enterLifeCyclePromises.value\r\n enterLifeCyclePromises.value = null\r\n transitionEnded.value = false\r\n classes.value = classNames['enter-to']\r\n resolve()\r\n } catch (error) { }\r\n })\r\n}\r\nasync function leave() {\r\n if (!enterPromise.value) {\r\n transitionEnded.value = false\r\n return onTransitionEnd()\r\n }\r\n try {\r\n await enterPromise.value\r\n if (!display.value) return\r\n const classNames = getClassNames(props.name)\r\n const duration = isObj(props.duration) ? (props.duration as any).leave : props.duration\r\n status.value = 'leave'\r\n emit('before-leave')\r\n currentDuration.value = duration\r\n leaveLifeCyclePromises.value = pause()\r\n await leaveLifeCyclePromises.value\r\n emit('leave')\r\n classes.value = classNames.leave\r\n leaveLifeCyclePromises.value = pause()\r\n await leaveLifeCyclePromises.value\r\n transitionEnded.value = false\r\n classes.value = classNames['leave-to']\r\n leaveLifeCyclePromises.value = setPromise(currentDuration.value)\r\n await leaveLifeCyclePromises.value\r\n leaveLifeCyclePromises.value = null\r\n onTransitionEnd()\r\n enterPromise.value = null\r\n } catch (error) { }\r\n}\r\nfunction setPromise(duration: number) {\r\n return new AbortablePromise<void>((resolve) => {\r\n const timer = setTimeout(() => { clearTimeout(timer); resolve() }, duration)\r\n })\r\n}\r\nfunction onTransitionEnd() {\r\n if (transitionEnded.value) return\r\n transitionEnded.value = true\r\n if (status.value === 'leave') {\r\n emit('after-leave')\r\n } else if (status.value === 'enter') { emit('after-enter') }\r\n if (!props.show && display.value) display.value = false\r\n}\r\nfunction noop() { }\r\n</script>\r\n\r\n<template>\r\n <view :class=\"rootClass\" :style=\"style\" @transitionend=\"onTransitionEnd\" @click=\"handleClick\"\r\n @touchmove.stop.prevent=\"noop\" v-if=\"isShow && disableTouchMove\">\r\n <slot />\r\n </view>\r\n <view :class=\"rootClass\" :style=\"style\" @transitionend=\"onTransitionEnd\" @click=\"handleClick\"\r\n v-else-if=\"isShow && !disableTouchMove\">\r\n <slot />\r\n </view>\r\n</template>",
33
+ "target": "uniapp"
34
+ }
35
+ ],
36
+ "fileCount": 6,
37
+ "contentHash": "9ec17bf11142b179d0338577102b6c2671ed4251"
38
+ }
@@ -0,0 +1,18 @@
1
+ {
2
+ "name": "reborn-waterfall",
3
+ "dependencies": [],
4
+ "files": [
5
+ {
6
+ "path": "reborn-waterfall.config.ts",
7
+ "content": "export default {\r\n slots: {\r\n root: 'flex flex-row w-full relative',\r\n column: 'flex-1',\r\n item: '',\r\n inner: '',\r\n virtual: 'absolute top-0 w-full -left-full opacity-0',\r\n },\r\n} as const\r\n",
8
+ "target": "uniapp"
9
+ },
10
+ {
11
+ "path": "RebornWaterfall.vue",
12
+ "content": "<script lang=\"ts\">\r\nexport type Props = {\r\n\tui?: any;\r\n\tcustomClass?: string;\r\n\tcolumn?: number; // 瀑布流列数,默认为2列\r\n\tgutter?: number; // 列间距,单位为px\r\n\tnodeKey?: string; // 数据项的唯一标识字段名,默认为\"id\"\r\n};\r\n</script>\r\n<script setup lang=\"ts\">\r\nimport { computed, getCurrentInstance, nextTick, onMounted, ref, watch } from \"vue\";\r\nimport { tv } from \"@/lib/tv\";\r\nimport { cn } from \"@/lib/utils\";\r\nimport theme from \"./reborn-waterfall.config\";\r\n\r\ndefineOptions({\r\n\tname: \"reborn-waterfall\"\r\n});\r\n\r\n\r\n// 定义插槽类型,item插槽接收item和index参数\r\ndefineSlots<{\r\n\titem(props: { item: any; index: number }): any;\r\n}>();\r\n\r\nconst props = withDefaults(defineProps<Props>(), {\r\n\tcolumn: 2,\r\n\tgutter: 16,\r\n\tnodeKey: \"id\",\r\n\tui: () => ({}),\r\n});\r\n\r\n// 获取当前组件实例的代理对象\r\nconst { proxy } = getCurrentInstance()!;\r\n\r\nconst b = tv(theme)\r\n\r\n// 存储每列的当前高度,用于计算最短列\r\nconst heights = ref<number[]>([]);\r\n\r\n// 存储瀑布流数据,二维数组,每个子数组代表一列\r\nconst columns = ref<any[][]>([]);\r\n\r\nconst uiOverrides = computed(() => props.ui || {})\r\nconst uiClasses = computed(() => {\r\n\tconst styles = b({\r\n\t\tcolumn: props.column,\r\n\t\tgutter: props.gutter,\r\n\t\tnodeKey: props.nodeKey\r\n\t})\r\n\treturn {\r\n\t\troot: (opts?: { class?: any }) => styles.root({ class: cn(opts?.class, uiOverrides.value.root) }),\r\n\t\tcolumn: (opts?: { class?: any }) => styles.column({ class: cn(opts?.class, uiOverrides.value.column) }),\r\n\t\titem: (opts?: { class?: any }) => styles.item({ class: cn(opts?.class, uiOverrides.value.item) }),\r\n\t\tinner: (opts?: { class?: any }) => styles.inner({ class: cn(opts?.class, uiOverrides.value.inner) }),\r\n\t\tvirtual: (opts?: { class?: any }) => styles.virtual({ class: cn(opts?.class, uiOverrides.value.virtual) })\r\n\t}\r\n})\r\n/**\r\n * 获取各列的当前高度\r\n * 通过uni.createSelectorQuery查询DOM元素的实际高度\r\n * @returns Promise<> 返回Promise对象\r\n */\r\nasync function getHeight(): Promise<void> {\r\n\t// 等待DOM更新完成\r\n\tawait nextTick();\r\n\r\n\treturn new Promise((resolve) => {\r\n\t\t// 创建选择器查询,获取所有列容器的边界信息\r\n\t\tuni.createSelectorQuery()\r\n\t\t\t.in(proxy)\r\n\t\t\t.selectAll(\".cl-waterfall__column-inner\")\r\n\t\t\t.boundingClientRect()\r\n\t\t\t.exec((rect) => {\r\n\t\t\t\tconst nodes = rect[0] as any[];\r\n\r\n\t\t\t\tif (!(nodes == null || nodes == undefined)) {\r\n\t\t\t\t\t// 提取每列的高度信息,如果获取失败则默认为0\r\n\t\t\t\t\theights.value = nodes.map((e) => e.height ?? 0);\r\n\t\t\t\t}\r\n\r\n\t\t\t\tresolve();\r\n\t\t\t});\r\n\t});\r\n}\r\n\r\n/**\r\n * 向瀑布流添加新数据\r\n * 使用虚拟定位技术计算每个项目的高度,然后分配到最短的列\r\n * @param data 要添加的数据数组\r\n */\r\nasync function append(data: any[]) {\r\n\t// 首先获取当前各列高度\r\n\tawait getHeight();\r\n\r\n\t// 将新数据作为虚拟项目添加到第一列,用于计算高度\r\n\tcolumns.value[0].push(\r\n\t\t...data.map((e) => {\r\n\t\t\treturn {\r\n\t\t\t\t...e,\r\n\t\t\t\tisVirtual: true // 标记为虚拟项目,会在CSS中隐藏\r\n\t\t\t} as any;\r\n\t\t})\r\n\t);\r\n\r\n\t// 等待DOM更新\r\n\tawait nextTick();\r\n\r\n\t// 延迟300ms后计算虚拟项目的高度并重新分配\r\n\tsetTimeout(() => {\r\n\t\tuni.createSelectorQuery()\r\n\t\t\t.in(proxy)\r\n\t\t\t.selectAll(\".is-virtual\")\r\n\t\t\t.boundingClientRect()\r\n\t\t\t.exec((rect) => {\r\n\t\t\t\t// 遍历每个虚拟项目\r\n\t\t\t\t(rect[0] as any[]).forEach((e, i) => {\r\n\t\t\t\t\t// 找到当前高度最小的列\r\n\t\t\t\t\tconst min = Math.min(...heights.value);\r\n\t\t\t\t\tconst index = heights.value.indexOf(min);\r\n\r\n\t\t\t\t\t// 将实际数据添加到最短列\r\n\t\t\t\t\tcolumns.value[index].push(data[i]);\r\n\r\n\t\t\t\t\t// 更新该列的高度\r\n\t\t\t\t\theights.value[index] += e.height ?? 0;\r\n\r\n\t\t\t\t\t// 清除第一列中的虚拟项目(临时用于计算高度的项目)\r\n\t\t\t\t\tcolumns.value[0] = columns.value[0].filter((e) => e.isVirtual != true);\r\n\t\t\t\t});\r\n\t\t\t});\r\n\t}, 300);\r\n}\r\n/**\r\n * 根据ID移除指定项目\r\n * @param id 要移除的项目ID\r\n */\r\nfunction remove(id: string | number) {\r\n\tcolumns.value.forEach((column, columnIndex) => {\r\n\t\t// 过滤掉指定ID的项目\r\n\t\tcolumns.value[columnIndex] = column.filter((e) => e[props.nodeKey] != id);\r\n\t});\r\n}\r\n\r\n/**\r\n * 根据ID更新指定项目的数据\r\n * @param id 要更新的项目ID\r\n * @param data 新的数据对象\r\n */\r\nfunction update(id: string | number, data: any) {\r\n\tcolumns.value.forEach((column) => {\r\n\t\tcolumn.forEach((e) => {\r\n\t\t\t// 找到指定ID的项目并更新数据\r\n\t\t\tif (e[props.nodeKey] == id) {\r\n\t\t\t\tObject.assign(e, data);\r\n\t\t\t}\r\n\t\t});\r\n\t});\r\n}\r\n/**\r\n * 清空瀑布流数据\r\n * 重新初始化列数组\r\n */\r\nfunction clear() {\r\n\tcolumns.value = [];\r\n\r\n\t// 根据列数创建空的列数组\r\n\tfor (let i = 0; i < props.column; i++) {\r\n\t\tcolumns.value.push([]);\r\n\t}\r\n}\r\n\r\n// 组件挂载时的初始化逻辑\r\nonMounted(() => {\r\n\t// 监听列数变化,当列数改变时重新初始化\r\n\twatch(\r\n\t\tcomputed(() => props.column),\r\n\t\t() => {\r\n\t\t\tclear(); // 清空现有数据\r\n\t\t\tgetHeight(); // 重新获取高度\r\n\t\t},\r\n\t\t{\r\n\t\t\timmediate: true // 立即执行一次\r\n\t\t}\r\n\t);\r\n});\r\n\r\ndefineExpose({\r\n\tappend,\r\n\tremove,\r\n\tupdate,\r\n\tclear\r\n});\r\n</script>\r\n<template>\r\n\t<view :class=\"uiClasses.root({ class: customClass })\" :style=\"{\r\n\t\tpadding: `0 ${props.gutter / 2}px`\r\n\t}\">\r\n\t\t<view :class=\"uiClasses.column()\" v-for=\"(column, columnIndex) in columns\" :key=\"columnIndex\" :style=\"{\r\n\t\t\tmargin: `0 ${props.gutter / 2}px`\r\n\t\t}\">\r\n\t\t\t<view :class=\"[uiClasses.inner(), 'cl-waterfall__column-inner']\">\r\n\t\t\t\t<view v-for=\"(item, index) in column\" :key=\"`${columnIndex}-${index}-${item[props.nodeKey]}`\"\r\n\t\t\t\t\t:class=\"[uiClasses.item({ class: item.isVirtual ? uiClasses.virtual() : '' }), item.isVirtual ? 'is-virtual' : '']\">\r\n\t\t\t\t\t<slot name=\"item\" :item=\"item\" :index=\"index\"></slot>\r\n\t\t\t\t</view>\r\n\t\t\t</view>\r\n\t\t</view>\r\n\t</view>\r\n</template>",
13
+ "target": "uniapp"
14
+ }
15
+ ],
16
+ "fileCount": 2,
17
+ "contentHash": "85ca74c51327f7f2a94a1c2bc6e02aa19b0ada09"
18
+ }
@@ -12,10 +12,10 @@
12
12
  },
13
13
  {
14
14
  "path": "ScrollIsland.vue",
15
- "content": "<script lang=\"ts\" setup>\r\nimport NumberFlow from \"@number-flow/vue\";\r\nimport { useColorMode } from \"@vueuse/core\";\r\nimport { motion, MotionConfig } from \"motion-v\";\r\nimport { computed, onMounted, onUnmounted, ref, useSlots } from \"vue\";\r\n\r\ninterface Props {\r\n class?: string;\r\n title?: string;\r\n height?: number;\r\n}\r\n\r\nconst props = withDefaults(defineProps<Props>(), {\r\n class: \"\",\r\n title: \"Progress\",\r\n height: 44,\r\n});\r\n\r\nconst open = ref(false);\r\nconst slots = useSlots();\r\n\r\nconst scrollPercentage = ref(0);\r\n\r\nconst isDark = computed(() => useColorMode().value == \"dark\");\r\nconst isSlotAvailable = computed(() => !!slots.default);\r\nconst borderRadius = computed(() => `${props.height / 2}px`);\r\n\r\nonMounted(() => {\r\n if (window === undefined) return;\r\n\r\n window.addEventListener(\"scroll\", updatePageScroll);\r\n updatePageScroll();\r\n});\r\n\r\nfunction updatePageScroll() {\r\n scrollPercentage.value = window.scrollY / (document.body.scrollHeight - window.innerHeight);\r\n}\r\n\r\nonUnmounted(() => {\r\n window.removeEventListener(\"scroll\", updatePageScroll);\r\n});\r\n</script>\r\n\r\n<template>\r\n <MotionConfig\r\n :transition=\"{\r\n duration: 0.7,\r\n type: 'spring',\r\n bounce: 0.5,\r\n }\"\r\n >\r\n <div\r\n class=\"bg-primary/90 border-radius fixed top-12 left-1/2 z-[999] -translate-x-1/2 backdrop-blur-lg\"\r\n :class=\"[$props.class]\"\r\n @click=\"() => (open = !open)\"\r\n >\r\n <motion.div\r\n id=\"motion-id\"\r\n layout\r\n :initial=\"{\r\n height: props.height,\r\n width: 0,\r\n }\"\r\n :animate=\"{\r\n height: open && isSlotAvailable ? 'auto' : props.height,\r\n width: open && isSlotAvailable ? 320 : 260,\r\n }\"\r\n class=\"bg-natural-900 text-secondary relative cursor-pointer overflow-hidden\"\r\n >\r\n <header class=\"gray- flex h-11 cursor-pointer items-center gap-2 px-4\">\r\n <AnimatedCircularProgressBar\r\n :value=\"scrollPercentage * 100\"\r\n :min=\"0\"\r\n :max=\"100\"\r\n :circle-stroke-width=\"10\"\r\n class=\"w-6\"\r\n :show-percentage=\"false\"\r\n :duration=\"0.3\"\r\n :gauge-secondary-color=\"isDark ? '#6b728055' : '#6b728099'\"\r\n :gauge-primary-color=\"isDark ? 'black' : 'white'\"\r\n />\r\n <h1 class=\"grow text-center font-bold\">{{ title }}</h1>\r\n <NumberFlow\r\n :value=\"scrollPercentage\"\r\n :format=\"{ style: 'percent' }\"\r\n locales=\"en-US\"\r\n />\r\n </header>\r\n <motion.div\r\n v-if=\"isSlotAvailable\"\r\n class=\"mb-2 flex h-full max-h-60 flex-col gap-1 overflow-y-auto px-4 text-sm\"\r\n >\r\n <slot />\r\n </motion.div>\r\n </motion.div>\r\n </div>\r\n </MotionConfig>\r\n</template>\r\n\r\n<style scoped>\r\n.border-radius {\r\n border-radius: v-bind(borderRadius);\r\n}\r\n</style>\r\n",
15
+ "content": "<script lang=\"ts\" setup>\r\nimport NumberFlow from \"@number-flow/vue\";\r\nimport { useColorMode } from \"@vueuse/core\";\r\nimport { motion, MotionConfig } from \"motion-v\";\r\nimport { computed, onMounted, onUnmounted, ref, useSlots } from \"vue\";\r\n\r\ninterface Props {\r\n class?: string;\r\n title?: string;\r\n height?: number;\r\n}\r\n\r\nconst props = withDefaults(defineProps<Props>(), {\r\n class: \"\",\r\n title: \"Progress\",\r\n height: 44,\r\n});\r\n\r\nconst open = ref(false);\r\nconst slots = useSlots();\r\n\r\nconst scrollPercentage = ref(0);\r\n\r\nconst isDark = computed(() => useColorMode().value == \"dark\");\r\nconst isSlotAvailable = computed(() => !!slots.default);\r\nconst borderRadius = computed(() => `${props.height / 2}px`);\r\n\r\nonMounted(() => {\r\n if (window === undefined) return;\r\n\r\n window.addEventListener(\"scroll\", updatePageScroll);\r\n updatePageScroll();\r\n});\r\n\r\nfunction updatePageScroll() {\r\n scrollPercentage.value = window.scrollY / (document.body.scrollHeight - window.innerHeight);\r\n}\r\n\r\nonUnmounted(() => {\r\n window.removeEventListener(\"scroll\", updatePageScroll);\r\n});\r\n</script>\r\n\r\n<template>\r\n <MotionConfig :transition=\"{\r\n duration: 0.7,\r\n type: 'spring',\r\n bounce: 0.5,\r\n }\">\r\n <div class=\"bg-primary/90 border-radius fixed top-12 left-1/2 z-[999] -translate-x-1/2 backdrop-blur-lg\"\r\n :class=\"[$props.class]\" @click=\"() => (open = !open)\">\r\n <motion.div id=\"motion-id\" layout :initial=\"{\r\n height: props.height,\r\n width: 0,\r\n }\" :animate=\"{\r\n height: open && isSlotAvailable ? 'auto' : props.height,\r\n width: open && isSlotAvailable ? 320 : 260,\r\n }\" class=\"bg-natural-900 text-secondary relative cursor-pointer overflow-hidden\">\r\n <header class=\"gray- flex h-11 cursor-pointer items-center gap-2 px-4\">\r\n <AnimatedCircularProgressBar :value=\"scrollPercentage * 100\" :min=\"0\" :max=\"100\" :circle-stroke-width=\"10\"\r\n class=\"w-6\" :show-percentage=\"false\" :duration=\"0.3\"\r\n :gauge-secondary-color=\"isDark ? '#6b728055' : '#6b728099'\"\r\n :gauge-primary-color=\"isDark ? 'black' : 'white'\" />\r\n <h1 class=\"grow text-center font-bold text-white\">{{ title }}</h1>\r\n <NumberFlow :value=\"scrollPercentage\" :format=\"{ style: 'percent' }\" locales=\"en-US\" />\r\n </header>\r\n <motion.div v-if=\"isSlotAvailable\"\r\n class=\"mb-2 flex h-full max-h-60 flex-col gap-1 overflow-y-auto px-4 text-sm\">\r\n <slot />\r\n </motion.div>\r\n </motion.div>\r\n </div>\r\n </MotionConfig>\r\n</template>\r\n\r\n<style scoped>\r\n.border-radius {\r\n border-radius: v-bind(borderRadius);\r\n}\r\n</style>\r\n",
16
16
  "target": "web"
17
17
  }
18
18
  ],
19
19
  "fileCount": 2,
20
- "contentHash": "57219406e0767c81db57cf29f5ed741b42170004"
20
+ "contentHash": "e85c31284e98107c8fdafa0e2b300ca4b58239f0"
21
21
  }