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.
- package/dist/index.js +182 -240
- package/dist/index.js.map +1 -1
- package/package.json +53 -53
- package/registry/components/reborn-affix.json +8 -3
- package/registry/components/reborn-back-top.json +9 -4
- package/registry/components/reborn-badge.json +11 -5
- package/registry/components/reborn-button.json +5 -5
- package/registry/components/reborn-card.json +18 -0
- package/registry/components/reborn-cascader.json +18 -0
- package/registry/components/reborn-checkbox.json +4 -4
- package/registry/components/reborn-chip.json +11 -5
- package/registry/components/reborn-collapse.json +11 -5
- package/registry/components/reborn-color-picker.json +50 -0
- package/registry/components/reborn-draggable.json +32 -0
- package/registry/components/reborn-drawer.json +17 -0
- package/registry/components/reborn-dropdown-select.json +18 -0
- package/registry/components/reborn-footer.json +40 -0
- package/registry/components/reborn-form.json +11 -6
- package/registry/components/reborn-image.json +10 -5
- package/registry/components/reborn-input-number.json +12 -6
- package/registry/components/reborn-input-otp.json +40 -0
- package/registry/components/reborn-input.json +4 -4
- package/registry/components/reborn-loading.json +23 -0
- package/registry/components/reborn-loadmore.json +23 -0
- package/registry/components/reborn-overlay.json +38 -0
- package/registry/components/reborn-page.json +18 -0
- package/registry/components/reborn-picker-view.json +26 -0
- package/registry/components/reborn-popover.json +58 -0
- package/registry/components/reborn-popup.json +23 -0
- package/registry/components/reborn-qrcode.json +45 -0
- package/registry/components/reborn-radio.json +45 -0
- package/registry/components/reborn-rate.json +40 -0
- package/registry/components/reborn-root-portal.json +26 -0
- package/registry/components/reborn-select-date.json +40 -0
- package/registry/components/reborn-select-trigger.json +25 -0
- package/registry/components/reborn-select.json +41 -0
- package/registry/components/reborn-slider.json +40 -0
- package/registry/components/reborn-sticky.json +12 -6
- package/registry/components/reborn-switch.json +13 -7
- package/registry/components/reborn-tabbar.json +38 -0
- package/registry/components/reborn-tabs copy.json +46 -0
- package/registry/components/reborn-tabs-test.json +46 -0
- package/registry/components/reborn-tabs.json +12 -6
- package/registry/components/reborn-text.json +34 -0
- package/registry/components/reborn-textarea.json +5 -5
- package/registry/components/reborn-toast.json +38 -0
- package/registry/components/reborn-transition.json +38 -0
- package/registry/components/reborn-waterfall.json +18 -0
- package/registry/components/scroll-island.json +2 -2
- package/registry/registry.json +1101 -97
|
@@ -6,29 +6,35 @@
|
|
|
6
6
|
"files": [
|
|
7
7
|
{
|
|
8
8
|
"path": "index.ts",
|
|
9
|
-
"content": "export { default as RebornSwitch } from \"./RebornSwitch.vue\";\r\n"
|
|
9
|
+
"content": "export { default as RebornSwitch } from \"./RebornSwitch.vue\";\r\n",
|
|
10
|
+
"target": "web"
|
|
10
11
|
},
|
|
11
12
|
{
|
|
12
13
|
"path": "reborn-switch.config.ts",
|
|
13
|
-
"content": "const size = [\"sm\", \"md\", \"lg\"] as const;\r\nconst color = [\"primary\", \"secondary\", \"success\", \"info\", \"warning\", \"error\", \"neutral\"] as const;\r\n\r\nexport { size as switchSizes, color as switchColors };\r\n\r\nexport default {\r\n slots: {\r\n wrapper: \"inline-flex items-center gap-3 cursor-pointer select-none\",\r\n input: \"peer sr-only\",\r\n track:\r\n \"relative inline-flex items-center rounded-full bg-gray-3 transition-colors ring-1 ring-transparent peer-focus-visible:ring-2 peer-focus-visible:ring-primary/40 peer-disabled:cursor-not-allowed peer-disabled:bg-gray-2 data-[loading=true]:cursor-wait data-[loading=true]:opacity-80\",\r\n thumb:\r\n \"absolute left-0.5 top-0.5 flex items-center justify-center rounded-full bg-white shadow transition-transform duration-200\",\r\n
|
|
14
|
+
"content": "const size = [\"sm\", \"md\", \"lg\"] as const;\r\nconst color = [\"primary\", \"secondary\", \"success\", \"info\", \"warning\", \"error\", \"neutral\"] as const;\r\n\r\nexport { size as switchSizes, color as switchColors };\r\n\r\nexport default {\r\n slots: {\r\n wrapper: \"inline-flex items-center gap-3 cursor-pointer select-none\",\r\n input: \"peer sr-only\",\r\n track:\r\n \"relative inline-flex items-center rounded-full bg-gray-3 transition-colors ring-1 ring-transparent peer-focus-visible:ring-2 peer-focus-visible:ring-primary/40 peer-disabled:cursor-not-allowed peer-disabled:bg-gray-2 data-[loading=true]:cursor-wait data-[loading=true]:opacity-80\",\r\n thumb:\r\n \"absolute left-0.5 top-0.5 flex items-center justify-center rounded-full bg-white shadow transition-transform duration-200\",\r\n activeLabel: \"text-gray-8 dark:text-gray-1\",\r\n inactiveLabel: \"text-gray-8 dark:text-gray-1\",\r\n },\r\n variants: {\r\n size: {\r\n sm: {\r\n track: \"h-5 w-9 peer-checked:[&>span]:translate-x-4\",\r\n thumb: \"size-4\",\r\n activeLabel: \"text-[length:var(--text-size-24)]\",\r\n inactiveLabel: \"text-[length:var(--text-size-24)]\",\r\n },\r\n md: {\r\n track: \"h-6 w-11 peer-checked:[&>span]:translate-x-5\",\r\n thumb: \"size-5\",\r\n activeLabel: \"text-[length:var(--text-size-26)]\",\r\n inactiveLabel: \"text-[length:var(--text-size-26)]\",\r\n },\r\n lg: {\r\n track: \"h-7 w-14 peer-checked:[&>span]:translate-x-7\",\r\n thumb: \"size-6\",\r\n activeLabel: \"text-[length:var(--text-size-28)]\",\r\n inactiveLabel: \"text-[length:var(--text-size-28)]\",\r\n },\r\n },\r\n color: {\r\n primary: {\r\n track: \"peer-checked:bg-primary\",\r\n },\r\n secondary: {\r\n track: \"peer-checked:bg-secondary\",\r\n },\r\n success: {\r\n track: \"peer-checked:bg-success\",\r\n },\r\n info: {\r\n track: \"peer-checked:bg-info\",\r\n },\r\n warning: {\r\n track: \"peer-checked:bg-warning\",\r\n },\r\n error: {\r\n track: \"peer-checked:bg-error\",\r\n },\r\n neutral: {\r\n track: \"peer-checked:bg-neutral\",\r\n },\r\n },\r\n active: {\r\n true: {\r\n activeLabel: \"font-medium\",\r\n inactiveLabel: \"text-gray-4 dark:text-gray-6\",\r\n },\r\n false: {\r\n activeLabel: \"text-gray-4 dark:text-gray-6\",\r\n inactiveLabel: \"text-gray-9 dark:text-gray-1 font-medium\",\r\n },\r\n },\r\n },\r\n compoundVariants: [\r\n { color: \"primary\" as (typeof color)[number], active: true, class: { activeLabel: \"text-primary\" } },\r\n { color: \"secondary\" as (typeof color)[number], active: true, class: { activeLabel: \"text-secondary\" } },\r\n { color: \"success\" as (typeof color)[number], active: true, class: { activeLabel: \"text-success\" } },\r\n { color: \"info\" as (typeof color)[number], active: true, class: { activeLabel: \"text-info\" } },\r\n { color: \"warning\" as (typeof color)[number], active: true, class: { activeLabel: \"text-warning\" } },\r\n { color: \"error\" as (typeof color)[number], active: true, class: { activeLabel: \"text-error\" } },\r\n { color: \"neutral\" as (typeof color)[number], active: true, class: { activeLabel: \"text-neutral\" } },\r\n // Inactive states\r\n { color: \"primary\" as (typeof color)[number], active: false, class: { inactiveLabel: \"text-primary\" } },\r\n { color: \"secondary\" as (typeof color)[number], active: false, class: { inactiveLabel: \"text-secondary\" } },\r\n { color: \"success\" as (typeof color)[number], active: false, class: { inactiveLabel: \"text-success\" } },\r\n { color: \"info\" as (typeof color)[number], active: false, class: { inactiveLabel: \"text-info\" } },\r\n { color: \"warning\" as (typeof color)[number], active: false, class: { inactiveLabel: \"text-warning\" } },\r\n { color: \"error\" as (typeof color)[number], active: false, class: { inactiveLabel: \"text-error\" } },\r\n { color: \"neutral\" as (typeof color)[number], active: false, class: { inactiveLabel: \"text-neutral\" } },\r\n ],\r\n defaultVariants: {\r\n size: \"md\" as (typeof size)[number],\r\n color: \"primary\" as (typeof color)[number],\r\n },\r\n};\r\n",
|
|
14
15
|
"target": "web"
|
|
15
16
|
},
|
|
16
17
|
{
|
|
17
18
|
"path": "RebornSwitch.vue",
|
|
18
|
-
"content": "<script setup lang=\"ts\">\r\nimport { computed, ref, useAttrs, watch } from \"vue\";\r\nimport type { ClassValue } from \"clsx\";\r\nimport { cn } from \"~/lib/utils\";\r\nimport theme, { switchColors, switchSizes } from \"./reborn-switch.config\";\r\nimport { tv } from \"~/lib/tv\";\r\n\r\nconst b = tv(theme);\r\n\r\ndefineOptions({\r\n inheritAttrs: false,\r\n});\r\n\r\nexport interface SwitchProps {\r\n modelValue?:
|
|
19
|
+
"content": "<script setup lang=\"ts\">\r\nimport { computed, ref, useAttrs, watch } from \"vue\";\r\nimport type { ClassValue } from \"clsx\";\r\nimport { cn } from \"~/lib/utils\";\r\nimport theme, { switchColors, switchSizes } from \"./reborn-switch.config\";\r\nimport { tv } from \"~/lib/tv\";\r\n\r\nconst b = tv(theme);\r\n\r\ndefineOptions({\r\n inheritAttrs: false,\r\n});\r\n\r\nexport interface SwitchProps {\r\n modelValue?: any;\r\n defaultValue?: any;\r\n activeValue?: any;\r\n inactiveValue?: any;\r\n activeLabel?: string;\r\n inactiveLabel?: string;\r\n disabled?: boolean;\r\n loading?: boolean;\r\n size?: typeof switchSizes[number];\r\n color?: typeof switchColors[number];\r\n beforeChange?: () => boolean | Promise<boolean>;\r\n class?: any;\r\n ui?: Partial<{\r\n wrapper: ClassValue;\r\n input: ClassValue;\r\n track: ClassValue;\r\n thumb: ClassValue;\r\n activeLabel: ClassValue;\r\n inactiveLabel: ClassValue;\r\n }>;\r\n}\r\n\r\nconst props = withDefaults(defineProps<SwitchProps>(), {\r\n activeValue: true,\r\n inactiveValue: false,\r\n disabled: false,\r\n loading: false,\r\n size: \"md\",\r\n color: \"primary\",\r\n});\r\n\r\nconst emit = defineEmits<{\r\n (e: \"update:modelValue\", value: any): void;\r\n (e: \"change\", value: any): void;\r\n}>();\r\n\r\nconst attrs = useAttrs();\r\nconst inputRef = ref<HTMLInputElement>();\r\n\r\nconst localValue = ref(props.defaultValue ?? props.inactiveValue);\r\nconst isChecked = computed(() => {\r\n const val = props.modelValue !== undefined ? props.modelValue : localValue.value;\r\n return val === props.activeValue;\r\n});\r\n\r\nconst uiOverrides = computed(() => props.ui || {});\r\n\r\nconst ui = computed(() => {\r\n const styles = b({\r\n size: props.size,\r\n color: props.color,\r\n active: isChecked.value,\r\n });\r\n\r\n return {\r\n wrapper: (opts?: { class?: any }) => styles.wrapper({ class: cn(opts?.class, uiOverrides.value.wrapper) }),\r\n input: (opts?: { class?: any }) => styles.input({ class: cn(opts?.class, uiOverrides.value.input) }),\r\n track: (opts?: { class?: any }) => styles.track({ class: cn(opts?.class, uiOverrides.value.track) }),\r\n thumb: (opts?: { class?: any }) => styles.thumb({ class: cn(opts?.class, uiOverrides.value.thumb) }),\r\n activeLabel: (opts?: { class?: any }) => styles.activeLabel({ class: cn(opts?.class, uiOverrides.value.activeLabel) }),\r\n inactiveLabel: (opts?: { class?: any }) => styles.inactiveLabel({ class: cn(opts?.class, uiOverrides.value.inactiveLabel) }),\r\n };\r\n});\r\n\r\nconst inputAttrs = computed(() => {\r\n const { class: _class, ...rest } = attrs;\r\n return rest;\r\n});\r\n\r\nfunction updateValue(checked: boolean) {\r\n const nextValue = checked ? props.activeValue : props.inactiveValue;\r\n if (props.modelValue === undefined) {\r\n localValue.value = nextValue;\r\n }\r\n emit(\"update:modelValue\", nextValue);\r\n emit(\"change\", nextValue);\r\n}\r\n\r\nasync function handleClick(event: Event) {\r\n if (props.disabled || props.loading) return;\r\n\r\n // 阻止默认行为(防止 input 自动切换状态)\r\n event.preventDefault();\r\n\r\n const newChecked = !isChecked.value;\r\n\r\n if (props.beforeChange) {\r\n try {\r\n const result = await props.beforeChange();\r\n if (result === false) return;\r\n } catch (e) {\r\n return;\r\n }\r\n }\r\n\r\n updateValue(newChecked);\r\n}\r\n\r\nwatch(\r\n () => props.modelValue,\r\n (value: any) => {\r\n if (value !== undefined) {\r\n localValue.value = value;\r\n }\r\n },\r\n);\r\n\r\ndefineExpose({\r\n focus: () => inputRef.value?.focus(),\r\n});\r\n</script>\r\n\r\n<template>\r\n <label :class=\"ui.wrapper({ class: props.class })\" :data-disabled=\"props.disabled || props.loading\"\r\n @click=\"handleClick\">\r\n\r\n <span v-if=\"props.inactiveLabel || $slots.inactiveLabel\" :class=\"ui.inactiveLabel()\">\r\n <slot name=\"inactiveLabel\">{{ props.inactiveLabel }}</slot>\r\n </span>\r\n\r\n <!-- 增加 @click.stop 防止事件冒泡造成的双重触发 -->\r\n <input ref=\"inputRef\" v-bind=\"inputAttrs\" type=\"checkbox\" :checked=\"isChecked\"\r\n :disabled=\"props.disabled || props.loading\" :class=\"ui.input()\" @click.stop />\r\n\r\n <span :class=\"ui.track()\" :data-loading=\"props.loading\">\r\n <span :class=\"ui.thumb()\">\r\n <slot name=\"thumb\" :checked=\"isChecked\" :loading=\"props.loading\">\r\n <Icon v-if=\"props.loading\" name=\"lucide:loader-2\" class=\"size-full p-0.5 animate-spin text-gray-400\" />\r\n </slot>\r\n </span>\r\n </span>\r\n\r\n <span v-if=\"props.activeLabel || $slots.activeLabel\" :class=\"ui.activeLabel()\">\r\n <slot name=\"activeLabel\">{{ props.activeLabel }}</slot>\r\n </span>\r\n </label>\r\n</template>\r\n",
|
|
19
20
|
"target": "web"
|
|
20
21
|
},
|
|
22
|
+
{
|
|
23
|
+
"path": "index.ts",
|
|
24
|
+
"content": "export { default as RebornSwitch } from './RebornSwitch.vue'\r\n",
|
|
25
|
+
"target": "uniapp"
|
|
26
|
+
},
|
|
21
27
|
{
|
|
22
28
|
"path": "reborn-switch.config.ts",
|
|
23
|
-
"content": "const size = [
|
|
29
|
+
"content": "const size = [\"sm\", \"md\", \"lg\"] as const;\r\nconst color = [\"primary\", \"secondary\", \"success\", \"info\", \"warning\", \"error\", \"neutral\"] as const;\r\n\r\nexport { size as switchSizes, color as switchColors };\r\n\r\nexport default {\r\n slots: {\r\n wrapper: \"inline-flex items-center gap-3 cursor-pointer select-none\",\r\n input: \"sr-only\",\r\n track:\r\n \"relative inline-flex items-center rounded-full bg-gray-3 transition-colors ring-1 ring-transparent\",\r\n thumb:\r\n \"absolute left-0.5 top-0.5 flex items-center justify-center rounded-full bg-white shadow transition-transform duration-200\",\r\n loading: \"size-full p-0.5 animate-spin text-gray-400 border-2 border-current border-t-transparent rounded-full\",\r\n activeLabel: \"text-gray-8 dark:text-gray-1\",\r\n inactiveLabel: \"text-gray-8 dark:text-gray-1\",\r\n },\r\n variants: {\r\n active: {\r\n true: {\r\n activeLabel: \"font-medium\",\r\n inactiveLabel: \"text-gray-4 dark:text-gray-6\",\r\n },\r\n false: {\r\n activeLabel: \"text-gray-4 dark:text-gray-6\",\r\n inactiveLabel: \"text-gray-9 dark:text-gray-1 font-medium\",\r\n },\r\n },\r\n size: {\r\n sm: {\r\n track: \"h-5 w-9\",\r\n thumb: \"size-4\",\r\n activeLabel: \"text-24\",\r\n inactiveLabel: \"text-24\",\r\n },\r\n md: {\r\n track: \"h-6 w-11\",\r\n thumb: \"size-5\",\r\n activeLabel: \"text-26\",\r\n inactiveLabel: \"text-26\",\r\n },\r\n lg: {\r\n track: \"h-7 w-14\",\r\n thumb: \"size-6\",\r\n activeLabel: \"text-28\",\r\n inactiveLabel: \"text-28\",\r\n },\r\n },\r\n color: {\r\n primary: {},\r\n secondary: {},\r\n success: {},\r\n info: {},\r\n warning: {},\r\n error: {},\r\n neutral: {},\r\n },\r\n },\r\n compoundVariants: [\r\n // 开启状态下的轨道颜色\r\n { color: \"primary\" as (typeof color)[number], active: true, class: { track: \"bg-primary\", activeLabel: \"text-primary\" } },\r\n { color: \"secondary\" as (typeof color)[number], active: true, class: { track: \"bg-secondary\", activeLabel: \"text-secondary\" } },\r\n { color: \"success\" as (typeof color)[number], active: true, class: { track: \"bg-success\", activeLabel: \"text-success\" } },\r\n { color: \"info\" as (typeof color)[number], active: true, class: { track: \"bg-info\", activeLabel: \"text-info\" } },\r\n { color: \"warning\" as (typeof color)[number], active: true, class: { track: \"bg-warning\", activeLabel: \"text-warning\" } },\r\n { color: \"error\" as (typeof color)[number], active: true, class: { track: \"bg-error\", activeLabel: \"text-error\" } },\r\n { color: \"neutral\" as (typeof color)[number], active: true, class: { track: \"bg-neutral\", activeLabel: \"text-neutral\" } },\r\n\r\n // 关闭状态下的标签颜色\r\n { color: \"primary\" as (typeof color)[number], active: false, class: { inactiveLabel: \"text-primary\" } },\r\n { color: \"secondary\" as (typeof color)[number], active: false, class: { inactiveLabel: \"text-secondary\" } },\r\n { color: \"success\" as (typeof color)[number], active: false, class: { inactiveLabel: \"text-success\" } },\r\n { color: \"info\" as (typeof color)[number], active: false, class: { inactiveLabel: \"text-info\" } },\r\n { color: \"warning\" as (typeof color)[number], active: false, class: { inactiveLabel: \"text-warning\" } },\r\n { color: \"error\" as (typeof color)[number], active: false, class: { inactiveLabel: \"text-error\" } },\r\n { color: \"neutral\" as (typeof color)[number], active: false, class: { inactiveLabel: \"text-neutral\" } },\r\n\r\n // 开启状态下根据尺寸进行的滑块位移\r\n { size: \"sm\" as (typeof size)[number], active: true, class: { thumb: \"translate-x-4\" } },\r\n { size: \"md\" as (typeof size)[number], active: true, class: { thumb: \"translate-x-5\" } },\r\n { size: \"lg\" as (typeof size)[number], active: true, class: { thumb: \"translate-x-7\" } },\r\n ],\r\n defaultVariants: {\r\n size: \"md\" as (typeof size)[number],\r\n color: \"primary\" as (typeof color)[number],\r\n },\r\n};\r\n",
|
|
24
30
|
"target": "uniapp"
|
|
25
31
|
},
|
|
26
32
|
{
|
|
27
33
|
"path": "RebornSwitch.vue",
|
|
28
|
-
"content": "<script setup lang=\"ts\">\r\nimport type { ClassValue } from
|
|
34
|
+
"content": "<script setup lang=\"ts\">\r\nimport type { ClassValue } from 'clsx'\r\nimport type { switchColors, switchSizes } from './reborn-switch.config'\r\nimport { computed, ref, watch } from 'vue'\r\nimport { useFormInject } from '@/composables/useFieldGroup'\r\nimport { tv } from '@/lib/tv'\r\nimport { cn } from '@/lib/utils'\r\nimport theme from './reborn-switch.config'\r\n\r\ndefineOptions({\r\n name: 'RebornSwitch',\r\n inheritAttrs: false,\r\n})\r\n\r\nexport interface SwitchProps {\r\n modelValue?: any\r\n defaultValue?: any\r\n activeValue?: any\r\n inactiveValue?: any\r\n activeLabel?: string\r\n inactiveLabel?: string\r\n disabled?: boolean\r\n loading?: boolean\r\n size?: typeof switchSizes[number]\r\n color?: typeof switchColors[number]\r\n beforeChange?: () => boolean | Promise<boolean>\r\n customClass?: any\r\n ui?: Partial<{\r\n wrapper: ClassValue\r\n input: ClassValue\r\n track: ClassValue\r\n thumb: ClassValue\r\n loading: ClassValue\r\n activeLabel: ClassValue\r\n inactiveLabel: ClassValue\r\n }>\r\n}\r\n\r\nconst props = withDefaults(defineProps<SwitchProps>(), {\r\n activeValue: true,\r\n inactiveValue: false,\r\n disabled: false,\r\n loading: false,\r\n size: 'md',\r\n color: 'primary',\r\n})\r\n\r\nconst emit = defineEmits<{\r\n (e: 'update:modelValue', value: any): void\r\n (e: 'change', value: any): void\r\n}>()\r\n\r\nconst b = tv(theme as any)\r\n\r\nconst { disabled: formDisabled, size: formSize, isError, validate } = useFormInject(props)\r\n\r\nconst isDisabled = computed(() => formDisabled.value || props.disabled || props.loading)\r\n\r\nconst localValue = ref(props.defaultValue ?? props.inactiveValue)\r\nconst isChecked = computed(() => {\r\n const val = props.modelValue !== undefined ? props.modelValue : localValue.value\r\n return val === props.activeValue\r\n})\r\n\r\nconst uiOverrides = computed(() => props.ui || {})\r\n\r\nconst ui = computed(() => {\r\n const styles = (b as any)({\r\n size: formSize.value || props.size,\r\n color: props.color,\r\n active: isChecked.value,\r\n error: isError.value,\r\n loading: props.loading,\r\n })\r\n\r\n return {\r\n wrapper: (opts?: { class?: any }) => styles.wrapper({ class: cn(opts?.class, uiOverrides.value.wrapper) }),\r\n input: (opts?: { class?: any }) => styles.input({ class: cn(opts?.class, uiOverrides.value.input) }),\r\n track: (opts?: { class?: any }) => styles.track({ class: cn(opts?.class, uiOverrides.value.track) }),\r\n thumb: (opts?: { class?: any }) => styles.thumb({ class: cn(opts?.class, uiOverrides.value.thumb) }),\r\n loading: (opts?: { class?: any }) => styles.loading({ class: cn(opts?.class, uiOverrides.value.loading) }),\r\n activeLabel: (opts?: { class?: any }) => styles.activeLabel({ class: cn(opts?.class, uiOverrides.value.activeLabel) }),\r\n inactiveLabel: (opts?: { class?: any }) => styles.inactiveLabel({ class: cn(opts?.class, uiOverrides.value.inactiveLabel) }),\r\n }\r\n})\r\n\r\nfunction updateValue(checked: boolean) {\r\n const nextValue = checked ? props.activeValue : props.inactiveValue\r\n if (!props.disabled && !props.loading) {\r\n if (props.modelValue === undefined) {\r\n localValue.value = nextValue\r\n }\r\n emit('update:modelValue', nextValue)\r\n emit('change', nextValue)\r\n if (validate) { validate('change') }\r\n }\r\n}\r\n\r\nasync function onTap() {\r\n if (props.disabled || props.loading) return\r\n\r\n const originalChecked = isChecked.value\r\n const newChecked = !originalChecked\r\n\r\n if (props.beforeChange) {\r\n try {\r\n const result = await props.beforeChange()\r\n if (result === false) return\r\n } catch (e) {\r\n return\r\n }\r\n }\r\n\r\n updateValue(newChecked)\r\n}\r\n\r\nwatch(\r\n () => props.modelValue,\r\n (value) => {\r\n if (value !== undefined) {\r\n localValue.value = value\r\n }\r\n },\r\n)\r\n\r\nconst isFocused = ref(false)\r\ndefineExpose({\r\n focus: () => {\r\n isFocused.value = true\r\n },\r\n})\r\n</script>\r\n\r\n<template>\r\n <view class=\"group\" :class=\"[ui.wrapper({ class: props.customClass }), isChecked && `\r\n is-checked\r\n `, isFocused && 'is-focused']\" :data-disabled=\"isDisabled\" style=\"-webkit-tap-highlight-color: transparent;\"\r\n @tap=\"onTap\">\r\n <view v-if=\"props.inactiveLabel || $slots.inactiveLabel\" :class=\"ui.inactiveLabel()\">\r\n <slot name=\"inactiveLabel\">\r\n {{ props.inactiveLabel }}\r\n </slot>\r\n </view>\r\n\r\n <view :class=\"ui.track()\" :data-loading=\"props.loading\">\r\n <view :class=\"ui.thumb()\">\r\n <slot name=\"thumb\" :checked=\"isChecked\" :loading=\"props.loading\">\r\n <view v-if=\"props.loading\" :class=\"ui.loading()\" />\r\n </slot>\r\n </view>\r\n </view>\r\n\r\n <view v-if=\"props.activeLabel || $slots.activeLabel\" :class=\"ui.activeLabel()\">\r\n <slot name=\"activeLabel\">\r\n {{ props.activeLabel }}\r\n </slot>\r\n </view>\r\n </view>\r\n</template>\r\n\r\n<style scoped></style>\r\n",
|
|
29
35
|
"target": "uniapp"
|
|
30
36
|
}
|
|
31
37
|
],
|
|
32
|
-
"fileCount":
|
|
33
|
-
"contentHash": "
|
|
38
|
+
"fileCount": 6,
|
|
39
|
+
"contentHash": "700fad69870628008c57d3cdd5c70c16b6db53af"
|
|
34
40
|
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "reborn-tabbar",
|
|
3
|
+
"dependencies": [],
|
|
4
|
+
"files": [
|
|
5
|
+
{
|
|
6
|
+
"path": "index.ts",
|
|
7
|
+
"content": "export { default as RebornTabbar } from './RebornTabbar.vue'\r\nexport type { TabbarProps } from './RebornTabbar.vue'\r\n\r\nexport { default as RebornTabbarTrigger } from './RebornTabbarTrigger.vue'\r\nexport type { TabbarTriggerProps } from './RebornTabbarTrigger.vue'\r\n\r\nexport { TABBAR_KEY } from './types'\r\nexport type { TabbarItem, TabbarProvide } from './types'",
|
|
8
|
+
"target": "uniapp"
|
|
9
|
+
},
|
|
10
|
+
{
|
|
11
|
+
"path": "reborn-tabbar-trigger.config.ts",
|
|
12
|
+
"content": "export const triggerColors = ['primary', 'secondary', 'success', 'info', 'warning', 'error', 'neutral'] as const\r\n\r\nexport default {\r\n slots: {\r\n root: 'relative z-[2] flex-1 text-center no-underline h-full flex justify-center items-center cursor-pointer',\r\n body: 'relative z-[2] flex items-center flex-col gap-y-1 leading-none p-0',\r\n icon: 'relative grid place-items-center overflow-y-hidden transition-transform duration-300 ease-out',\r\n activeIcon: 'transition-all duration-300 ease-in-out col-start-1 row-start-1 flex items-center justify-center',\r\n inactiveIcon: 'transition-all duration-300 ease-in-out col-start-1 row-start-1 flex items-center justify-center',\r\n iconInner: 'text-40',\r\n title: 'text-24 transition-all duration-300 ease-in-out',\r\n glowLayer: 'absolute z-[1] w-full h-full rounded-[14rpx] transition-all duration-400 ease-in-out opacity-0',\r\n bodyGlowLayer: '[display:contents]',\r\n },\r\n variants: {\r\n active: {\r\n true: {\r\n title: 'opacity-100',\r\n },\r\n false: {\r\n title: 'opacity-100',\r\n },\r\n },\r\n shape: {\r\n normal: {\r\n body: 'flex-col',\r\n title: 'leading-[34rpx] mt-[4rpx]',\r\n },\r\n round: {\r\n root: 'transition-all duration-300 ease-in-out',\r\n body: 'flex-row rounded-full transition-all duration-300 ease-in-out px-[24rpx] h-[64rpx] flex items-center justify-center',\r\n title: 'transition-all duration-300 overflow-hidden whitespace-nowrap',\r\n }\r\n },\r\n animation: {\r\n fade: {},\r\n flip: {\r\n icon: '[perspective:1000px]',\r\n },\r\n reveal: {\r\n inactiveIcon: 'z-0',\r\n activeIcon: 'z-10',\r\n },\r\n creative: {},\r\n glass: {\r\n icon: 'w-[64rpx] h-[64rpx] grid !overflow-visible',\r\n activeIcon: 'absolute overflow-hidden z-[2] bottom-0 left-0 w-full h-full rounded-[12rpx] transition-all duration-400 ease-in-out box-border border border-solid border-transparent',\r\n inactiveIcon: 'absolute overflow-hidden z-[2] bottom-0 left-0 w-full h-full rounded-[12rpx] transition-all duration-400 ease-in-out bg-[#f5f3f7] box-border border border-solid border-transparent',\r\n },\r\n drop: {\r\n icon: '!overflow-visible',\r\n },\r\n 'fly-balls': {},\r\n },\r\n disabled: {\r\n true: {\r\n root: 'opacity-50 pointer-events-none',\r\n },\r\n false: '',\r\n },\r\n color: {\r\n primary: '',\r\n secondary: '',\r\n success: '',\r\n info: '',\r\n warning: '',\r\n error: '',\r\n neutral: '',\r\n },\r\n },\r\n compoundVariants: [\r\n // 基础动画 (Animations)\r\n { animation: 'fade' as const, active: true, class: { activeIcon: 'opacity-100 scale-110', inactiveIcon: 'opacity-0 scale-90' } },\r\n { animation: 'fade' as const, active: false, class: { activeIcon: 'opacity-0 scale-90', inactiveIcon: 'opacity-100 scale-100' } },\r\n\r\n { animation: 'flip' as const, active: true, class: { activeIcon: 'opacity-100 [transform:perspective(1000px)_rotateY(0deg)]', inactiveIcon: 'opacity-0 [transform:perspective(1000px)_rotateY(-180deg)]' } },\r\n { animation: 'flip' as const, active: false, class: { activeIcon: 'opacity-0 [transform:perspective(1000px)_rotateY(180deg)]', inactiveIcon: 'opacity-100 [transform:perspective(1000px)_rotateY(0deg)]' } },\r\n\r\n { animation: 'reveal' as const, active: true, class: { inactiveIcon: 'opacity-0 z-10 [clip-path:inset(0_0_0_100%)] duration-1000', activeIcon: 'opacity-100 z-0 ' } },\r\n { animation: 'reveal' as const, active: false, class: { inactiveIcon: 'opacity-100 z-10 [clip-path:inset(0_0_0_0)]', activeIcon: 'opacity-0 z-0 duration-1000' } },\r\n\r\n { animation: 'creative' as const, active: true, class: { activeIcon: 'opacity-100 translate-y-0 scale-100', inactiveIcon: 'opacity-0 translate-y-full scale-50' } },\r\n { animation: 'creative' as const, active: false, class: { activeIcon: 'opacity-0 -translate-y-full scale-50', inactiveIcon: 'opacity-100 translate-y-0 scale-100' } },\r\n\r\n // fly-balls 动画: 激活时延迟 500ms 执行渐变,未激活时立刻执行渐变复原\r\n { animation: 'fly-balls' as const, active: true, class: { activeIcon: 'opacity-100 transition-all duration-300 delay-500', inactiveIcon: 'opacity-0 transition-all duration-300 delay-500' } },\r\n { animation: 'fly-balls' as const, active: false, class: { activeIcon: 'opacity-0 transition-all duration-300', inactiveIcon: 'opacity-100 transition-all duration-300' } },\r\n\r\n // Drop 动画: 只移动 icon,不带动标题一起上浮\r\n { animation: 'drop' as const, shape: 'normal' as const, active: true, class: { icon: '-translate-y-[18px]', activeIcon: 'opacity-100 scale-100 transition-all duration-300', inactiveIcon: 'opacity-0 scale-75 transition-all duration-200' } },\r\n { animation: 'drop' as const, shape: 'normal' as const, active: false, class: { icon: 'translate-y-0', activeIcon: 'opacity-0 scale-75 transition-all duration-200', inactiveIcon: 'opacity-100 scale-100 transition-all duration-300' } },\r\n { animation: 'drop' as const, shape: 'round' as const, active: true, class: { icon: 'translate-y-0', activeIcon: 'opacity-100 scale-100 transition-all duration-300', inactiveIcon: 'opacity-0 scale-75 transition-all duration-200' } },\r\n { animation: 'drop' as const, shape: 'round' as const, active: false, class: { icon: 'translate-y-0', activeIcon: 'opacity-0 scale-75 transition-all duration-200', inactiveIcon: 'opacity-100 scale-100 transition-all duration-300' } },\r\n\r\n // Glass 动画: 激活状态 — 前景层向左下方偏移,背景产生毛玻璃效果\r\n {\r\n animation: 'glass' as const,\r\n active: true,\r\n class: {\r\n activeIcon: 'opacity-100 !bottom-[-2px] !left-[-2px] bg-white/20 [backdrop-filter:blur(3px)]',\r\n inactiveIcon: 'opacity-0 !bottom-[-2px] !left-[-2px] bg-white/20 [backdrop-filter:blur(3px)]',\r\n glowLayer: 'opacity-100 relative [transform:scale(0.96)] [transform-origin:right_top] !top-[-2px] !right-[-2px] duration-1000',\r\n },\r\n },\r\n // Glass 动画: 未激活状态\r\n {\r\n animation: 'glass' as const,\r\n active: false,\r\n class: {\r\n activeIcon: 'opacity-0',\r\n inactiveIcon: 'opacity-100 bg-[#f5f3f7]',\r\n glowLayer: 'opacity-0',\r\n },\r\n },\r\n\r\n // Glass 动画 + 激活颜色特定样式: 特定颜色边框和渐变发光层 (使用 theme.css 中的 color-4 → color-1 渐变)\r\n {\r\n animation: 'glass' as const, active: true, color: 'primary' as const, class: {\r\n activeIcon: 'border-b-primary/10 border-l-primary/10 border-t-white/20 border-r-white/20',\r\n glowLayer: '[background-image:linear-gradient(100deg,var(--color-red-8),var(--color-red-4))]',\r\n }\r\n },\r\n {\r\n animation: 'glass' as const, active: true, color: 'secondary' as const, class: {\r\n activeIcon: 'border-b-secondary/10 border-l-secondary/10 border-t-white/20 border-r-white/20',\r\n glowLayer: '[background-image:linear-gradient(100deg,var(--color-gray-8),var(--color-gray-4))]',\r\n }\r\n },\r\n {\r\n animation: 'glass' as const, active: true, color: 'success' as const, class: {\r\n activeIcon: 'border-b-success/60 border-l-success/10 border-t-white/20 border-r-white/20',\r\n glowLayer: '[background-image:linear-gradient(100deg,var(--color-green-8),var(--color-green-4))]',\r\n }\r\n },\r\n {\r\n animation: 'glass' as const, active: true, color: 'info' as const, class: {\r\n activeIcon: 'border-b-info/30 border-l-info/10 border-t-white/20 border-r-white/20',\r\n glowLayer: '[background-image:linear-gradient(100deg,var(--color-blue-8),var(--color-blue-4))]',\r\n }\r\n },\r\n {\r\n animation: 'glass' as const, active: true, color: 'warning' as const, class: {\r\n activeIcon: 'border-b-warning/10 border-l-warning/10 border-t-white/20 border-r-white/20',\r\n glowLayer: '[background-image:linear-gradient(100deg,var(--color-orange-8),var(--color-orange-4))]',\r\n }\r\n },\r\n {\r\n animation: 'glass' as const, active: true, color: 'error' as const, class: {\r\n activeIcon: 'border-b-error/30 border-l-error/10 border-t-white/20 border-r-white/20',\r\n glowLayer: '[background-image:linear-gradient(100deg,var(--color-red-8),var(--color-red-4))]',\r\n }\r\n },\r\n {\r\n animation: 'glass' as const, active: true, color: 'neutral' as const, class: {\r\n activeIcon: 'border-b-neutral/30 border-l-neutral/10 border-t-white/20 border-r-white/20',\r\n glowLayer: '[background-image:linear-gradient(100deg,var(--color-gray-6),var(--color-gray-3))]',\r\n }\r\n },\r\n\r\n // Round 形状: 基础布局 (展开与收缩表现)\r\n { shape: 'round' as const, active: true, class: { root: 'flex-[1.5]', title: 'ml-[8rpx] max-w-[200rpx] opacity-100 w-auto' } },\r\n { shape: 'round' as const, active: false, class: { root: 'flex-1', title: 'ml-0 max-w-0 opacity-0 !mx-0 w-0' } },\r\n\r\n // Round 形状: 激活状态背景颜色 (非 Glass 动画)\r\n { shape: 'round' as const, active: true, color: 'primary' as const, class: { body: 'bg-primary/20' } },\r\n { shape: 'round' as const, active: true, color: 'secondary' as const, class: { body: 'bg-secondary/10' } },\r\n { shape: 'round' as const, active: true, color: 'success' as const, class: { body: 'bg-success/10' } },\r\n { shape: 'round' as const, active: true, color: 'info' as const, class: { body: 'bg-info/10' } },\r\n { shape: 'round' as const, active: true, color: 'warning' as const, class: { body: 'bg-warning/10' } },\r\n { shape: 'round' as const, active: true, color: 'error' as const, class: { body: 'bg-error/10' } },\r\n { shape: 'round' as const, active: true, color: 'neutral' as const, class: { body: 'bg-neutral/40' } },\r\n\r\n // Drop + round: 小球动画结束后再显示 body 背景\r\n { shape: 'round' as const, animation: 'drop' as const, active: true, class: { body: 'overflow-visible transition-colors duration-200 delay-[480ms]' } },\r\n { shape: 'round' as const, animation: 'drop' as const, active: false, class: { body: 'overflow-visible' } },\r\n { shape: 'round' as const, animation: 'drop' as const, active: true, color: 'primary' as const, class: { body: '!bg-primary/30' } },\r\n { shape: 'round' as const, animation: 'drop' as const, active: true, color: 'secondary' as const, class: { body: '!bg-secondary/30' } },\r\n { shape: 'round' as const, animation: 'drop' as const, active: true, color: 'success' as const, class: { body: '!bg-success/30' } },\r\n { shape: 'round' as const, animation: 'drop' as const, active: true, color: 'info' as const, class: { body: '!bg-info/30' } },\r\n { shape: 'round' as const, animation: 'drop' as const, active: true, color: 'warning' as const, class: { body: '!bg-warning/30' } },\r\n { shape: 'round' as const, animation: 'drop' as const, active: true, color: 'error' as const, class: { body: '!bg-error/30' } },\r\n { shape: 'round' as const, animation: 'drop' as const, active: true, color: 'neutral' as const, class: { body: '!bg-neutral/30' } },\r\n\r\n // Round 形状 + Glass 动画: bodyGlowLayer 作为外层彩色背景,body 本身产生毛玻璃和悬浮效果\r\n {\r\n shape: 'round' as const, animation: 'glass' as const, active: true, class: {\r\n bodyGlowLayer: '![display:block] rounded-full transition-all duration-400 ease-in-out',\r\n body: 'relative z-[2] bg-white/45 [backdrop-filter:blur(3px)] border border-solid border-transparent rounded-full transition-all duration-400 ease-in-out [transform:translate(-4rpx,4rpx)]',\r\n icon: 'w-auto h-auto grid overflow-y-hidden',\r\n activeIcon: 'relative overflow-visible z-auto bottom-auto left-auto w-auto h-auto rounded-none transition-all duration-300 ease-in-out border-none bg-transparent opacity-100 scale-110 [backdrop-filter:none]',\r\n inactiveIcon: 'relative overflow-visible z-auto bottom-auto left-auto w-auto h-auto rounded-none transition-all duration-300 ease-in-out border-none bg-transparent opacity-0 scale-90 [backdrop-filter:none]',\r\n glowLayer: '!hidden',\r\n },\r\n },\r\n {\r\n shape: 'round' as const, animation: 'glass' as const, active: false, class: {\r\n bodyGlowLayer: '![display:block] rounded-full transition-all duration-400 ease-in-out bg-transparent',\r\n body: 'relative z-[2] bg-transparent [backdrop-filter:none] border border-solid border-transparent rounded-full transition-all duration-400 ease-in-out [transform:translate(0,0)]',\r\n icon: 'w-auto h-auto grid overflow-y-hidden',\r\n activeIcon: 'relative overflow-visible z-auto bottom-auto left-auto w-auto h-auto rounded-none transition-all duration-300 ease-in-out border-none bg-transparent opacity-0 scale-90 [backdrop-filter:none]',\r\n inactiveIcon: 'relative overflow-visible z-auto bottom-auto left-auto w-auto h-auto rounded-none transition-all duration-300 ease-in-out border-none !bg-transparent opacity-100 scale-100 [backdrop-filter:none]',\r\n glowLayer: '!hidden',\r\n },\r\n },\r\n\r\n // Round 形状 + Glass 动画 + 激活颜色特定样式: bodyGlowLayer 应用相同的渐变背景,body 应用同色系边框\r\n { shape: 'round' as const, animation: 'glass' as const, active: true, color: 'primary' as const, class: { bodyGlowLayer: '[background-image:linear-gradient(100deg,var(--color-red-8),var(--color-red-4))]', body: 'border-b-primary/10 border-l-primary/10 border-t-white/20 border-r-white/20' } },\r\n { shape: 'round' as const, animation: 'glass' as const, active: true, color: 'secondary' as const, class: { bodyGlowLayer: '[background-image:linear-gradient(100deg,var(--color-gray-8),var(--color-gray-4))]', body: 'border-b-secondary/10 border-l-secondary/10 border-t-white/20 border-r-white/20' } },\r\n { shape: 'round' as const, animation: 'glass' as const, active: true, color: 'success' as const, class: { bodyGlowLayer: '[background-image:linear-gradient(100deg,var(--color-green-8),var(--color-green-4))]', body: 'border-b-success/10 border-l-success/10 border-t-white/20 border-r-white/20' } },\r\n { shape: 'round' as const, animation: 'glass' as const, active: true, color: 'info' as const, class: { bodyGlowLayer: '[background-image:linear-gradient(100deg,var(--color-blue-8),var(--color-blue-4))]', body: 'border-b-info/10 border-l-info/10 border-t-white/20 border-r-white/20' } },\r\n { shape: 'round' as const, animation: 'glass' as const, active: true, color: 'warning' as const, class: { bodyGlowLayer: '[background-image:linear-gradient(100deg,var(--color-orange-8),var(--color-orange-4))]', body: 'border-b-warning/10 border-l-warning/10 border-t-white/20 border-r-white/20' } },\r\n { shape: 'round' as const, animation: 'glass' as const, active: true, color: 'error' as const, class: { bodyGlowLayer: '[background-image:linear-gradient(100deg,var(--color-red-8),var(--color-red-4))]', body: 'border-b-error/10 border-l-error/10 border-t-white/20 border-r-white/20' } },\r\n { shape: 'round' as const, animation: 'glass' as const, active: true, color: 'neutral' as const, class: { bodyGlowLayer: '[background-image:linear-gradient(100deg,var(--color-gray-6),var(--color-gray-4))]', body: 'border-b-neutral/10 border-l-neutral/10 border-t-white/20 border-r-white/20' } },\r\n\r\n // 激活状态文字与图标颜色\r\n { active: true, color: 'primary' as const, class: { title: 'text-primary', activeIcon: 'text-primary' } },\r\n { active: true, color: 'secondary' as const, class: { title: 'text-secondary', activeIcon: 'text-secondary' } },\r\n { active: true, color: 'success' as const, class: { title: 'text-success', activeIcon: 'text-success' } },\r\n { active: true, color: 'info' as const, class: { title: 'text-info', activeIcon: 'text-info' } },\r\n { active: true, color: 'warning' as const, class: { title: 'text-warning', activeIcon: 'text-warning' } },\r\n { active: true, color: 'error' as const, class: { title: 'text-error', activeIcon: 'text-error' } },\r\n { active: true, color: 'neutral' as const, class: { title: 'text-neutral', activeIcon: 'text-neutral' } },\r\n\r\n // 未激活状态文字与图标颜色\r\n { active: false, class: { title: 'text-gray-5', inactiveIcon: 'text-gray-5' } },\r\n ],\r\n defaultVariants: {\r\n active: false,\r\n disabled: false,\r\n color: 'primary' as const,\r\n shape: 'normal' as const,\r\n animation: 'fade' as const,\r\n },\r\n}\r\n",
|
|
13
|
+
"target": "uniapp"
|
|
14
|
+
},
|
|
15
|
+
{
|
|
16
|
+
"path": "reborn-tabbar.config.ts",
|
|
17
|
+
"content": "export const tabbarColors = ['primary', 'secondary', 'success', 'info', 'warning', 'error', 'neutral'] as const\r\nexport const tabbarShapes = ['normal', 'round'] as const\r\nexport const tabbarAnimations = ['reveal', 'flip', 'creative', 'glass', 'fly-balls', 'drop'] as const\r\n\r\nexport default {\r\n slots: {\r\n root: 'box-content',\r\n base: 'flex items-center flex-nowrap relative bg-white dark:bg-black h-[110rpx] box-border overflow-visible',\r\n dropBall: 'absolute z-1 top-[-16px] w-[44px] h-[44px] rounded-full pointer-events-none transform-gpu will-change-transform [backface-visibility:hidden] shadow-none',\r\n flyBallsContainer: 'absolute left-0 top-0 w-full h-full pointer-events-none z-10',\r\n flyBallItem: 'absolute pointer-events-none z-10',\r\n },\r\n variants: {\r\n shape: {\r\n normal: '',\r\n round: {\r\n base: 'mx-[32rpx] rounded-full shadow-[0_6px_30px_5px_rgba(0,0,0,0.05),0_16px_24px_2px_rgba(0,0,0,0.04),0_8px_10px_-5px_rgba(0,0,0,0.08)] dark:shadow-[0_6px_30px_5px_rgba(0,0,0,0.5),0_16px_24px_2px_rgba(0,0,0,0.4),0_8px_10px_-5px_rgba(0,0,0,0.6)]',\r\n },\r\n },\r\n pureIcon: {\r\n true: '',\r\n false: '',\r\n },\r\n fixed: {\r\n true: {\r\n base: 'fixed left-0 bottom-0 right-0 !z-[150]',\r\n },\r\n false: '',\r\n },\r\n bordered: {\r\n true: '',\r\n false: '',\r\n },\r\n safeAreaInsetBottom: {\r\n true: '',\r\n false: '',\r\n },\r\n animation: {\r\n reveal: '',\r\n flip: '',\r\n creative: '',\r\n glass: '',\r\n 'fly-balls': '',\r\n drop: '',\r\n },\r\n color: {\r\n primary: '',\r\n secondary: '',\r\n success: '',\r\n info: '',\r\n warning: '',\r\n error: '',\r\n neutral: '',\r\n },\r\n },\r\n compoundVariants: [\r\n // 默认形状 + 边框\r\n {\r\n shape: 'normal' as const,\r\n bordered: true,\r\n class: {\r\n base: 'border-t border-t-gray-3 border-solid',\r\n },\r\n },\r\n // 固定在底部 + 安全区适配 (适应 normal 和 round 的基础逻辑)\r\n {\r\n fixed: true,\r\n safeAreaInsetBottom: true,\r\n class: {\r\n base: 'box-content bottom-[env(safe-area-inset-bottom)]',\r\n },\r\n },\r\n // round 形状特有的安全区 padding (用于撑起空间)\r\n {\r\n shape: 'round' as const,\r\n fixed: true,\r\n safeAreaInsetBottom: true,\r\n class: {\r\n root: 'pb-[env(safe-area-inset-bottom)]',\r\n },\r\n },\r\n // normal 形状下使用 glass 动画增加默认高度\r\n {\r\n shape: 'normal' as const,\r\n animation: 'glass' as const,\r\n class: {\r\n base: 'h-[130rpx]',\r\n },\r\n },\r\n {\r\n pureIcon: true,\r\n class: {\r\n base: 'h-[90rpx]',\r\n },\r\n },\r\n\r\n // Drop 动画: 球体背景色跟随 color\r\n { animation: 'drop' as const, color: 'primary' as const, class: { dropBall: 'bg-primary/30' } },\r\n { animation: 'drop' as const, color: 'secondary' as const, class: { dropBall: 'bg-secondary/30' } },\r\n { animation: 'drop' as const, color: 'success' as const, class: { dropBall: 'bg-success/30' } },\r\n { animation: 'drop' as const, color: 'info' as const, class: { dropBall: 'bg-info/30' } },\r\n { animation: 'drop' as const, color: 'warning' as const, class: { dropBall: 'bg-warning/30' } },\r\n { animation: 'drop' as const, color: 'error' as const, class: { dropBall: 'bg-error/30' } },\r\n { animation: 'drop' as const, color: 'neutral' as const, class: { dropBall: 'bg-neutral/30' } },\r\n\r\n // round 模式下使用更小的共享指示球,避免顶部白层闪烁\r\n { shape: 'round' as const, animation: 'drop' as const, class: { dropBall: '!w-8 !h-8 !top-[-10px] shadow-none' } },\r\n ],\r\n defaultVariants: {\r\n shape: 'normal' as const,\r\n fixed: false,\r\n bordered: true,\r\n safeAreaInsetBottom: false,\r\n color: 'primary' as const,\r\n },\r\n}\r\n",
|
|
18
|
+
"target": "uniapp"
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
"path": "RebornTabbar.vue",
|
|
22
|
+
"content": "<script lang=\"ts\">\r\nexport default {\r\n name: 'reborn-tabbar',\r\n options: {\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, getCurrentInstance, nextTick, onMounted, onUnmounted, ref, watch, type CSSProperties } from 'vue'\r\nimport { tv } from '@/lib/tv'\r\nimport { cn } from '@/lib/utils'\r\nimport theme, { tabbarAnimations, tabbarColors, tabbarShapes } from './reborn-tabbar.config'\r\nimport { useChildren } from '@/composables/useChildren'\r\nimport { TABBAR_KEY } from './types'\r\nimport type { TabbarItem } from './types'\r\n\r\nexport interface TabbarProps {\r\n modelValue?: number | string\r\n fixed?: boolean\r\n bordered?: boolean\r\n safeAreaInsetBottom?: boolean\r\n pureIcon?: boolean\r\n shape?: (typeof tabbarShapes)[number]\r\n animation?: (typeof tabbarAnimations)[number] | null\r\n ballColors?: string[]\r\n activeColor?: string\r\n inactiveColor?: string\r\n placeholder?: boolean\r\n zIndex?: number\r\n color?: (typeof tabbarColors)[number]\r\n customClass?: any\r\n customStyle?: string\r\n ui?: Partial<Record<'root' | 'base' | 'dropBall' | 'flyBallsContainer' | 'flyBallItem', string>>\r\n ballShiftY?: number\r\n beforeChange?: (params: { name: string | number }, done: (shouldProceed?: boolean) => void) => boolean | Promise<boolean> | void\r\n}\r\n\r\nconst props = withDefaults(defineProps<TabbarProps>(), {\r\n modelValue: 0,\r\n fixed: false,\r\n bordered: true,\r\n safeAreaInsetBottom: false,\r\n pureIcon: false,\r\n shape: 'normal',\r\n animation: null,\r\n placeholder: true,\r\n zIndex: 99,\r\n color: 'primary',\r\n customStyle: '',\r\n ballColors: () => ['#ff6675', '#ffb03b', '#35b6f2', '#3ac29e']\r\n})\r\n\r\nconst emit = defineEmits(['change', 'update:modelValue'])\r\n\r\nconst b = tv(theme)\r\nconst { proxy } = getCurrentInstance() as any\r\nconst { linkChildren, children } = useChildren(TABBAR_KEY)\r\n\r\nconst height = ref<number | string>('')\r\nconst locked = ref(false)\r\nconst isTransition = ref(false)\r\nconst oldIndexRef = ref(0)\r\nconst newIndexRef = ref(0)\r\nconst dropBallLeft = ref(0)\r\nconst dropBallStartLeft = ref(0)\r\nconst dropBallTop = ref(-16)\r\nconst dropBallSize = ref(44)\r\nconst dropBallReady = ref(false)\r\nconst dropBallAnimating = ref(false)\r\nconst pendingDropAnimation = ref(false)\r\nconst dropBallVisible = computed(() => {\r\n if (props.animation !== 'drop' || !dropBallReady.value) {\r\n return false\r\n }\r\n if (props.shape === 'round') {\r\n return dropBallAnimating.value\r\n }\r\n return true\r\n})\r\n\r\nlet transitionTimer: ReturnType<typeof setTimeout> | null = null\r\nlet layoutTimer: ReturnType<typeof setTimeout> | null = null\r\nlet unlockTimer: ReturnType<typeof setTimeout> | null = null\r\n\r\nconst DROP_ANIMATION_DURATION = 480\r\n\r\nlinkChildren({\r\n props,\r\n setChange,\r\n locked,\r\n})\r\n\r\nconst uiOverrides = computed(() => props.ui || {})\r\n\r\nconst ui = computed(() => {\r\n const styles = b({\r\n shape: props.shape as any,\r\n pureIcon: props.pureIcon,\r\n fixed: props.fixed,\r\n bordered: props.bordered,\r\n animation: props.animation as any,\r\n safeAreaInsetBottom: props.safeAreaInsetBottom,\r\n color: props.color as any,\r\n })\r\n\r\n return {\r\n root: (opts?: { class?: any }) => styles.root({ class: cn(opts?.class, uiOverrides.value.root) }),\r\n base: (opts?: { class?: any }) => styles.base({ class: cn(opts?.class, uiOverrides.value.base) }),\r\n dropBall: (opts?: { class?: any }) => styles.dropBall({ class: cn(opts?.class, uiOverrides.value.dropBall) }),\r\n flyBallsContainer: (opts?: { class?: any }) => styles.flyBallsContainer({ class: cn(opts?.class, uiOverrides.value.flyBallsContainer) }),\r\n flyBallItem: (opts?: { class?: any }) => styles.flyBallItem({ class: cn(opts?.class, uiOverrides.value.flyBallItem) }),\r\n }\r\n})\r\n\r\nconst rootStyle = computed(() => {\r\n const style: CSSProperties = {}\r\n if (props.zIndex !== undefined) {\r\n style['z-index'] = props.zIndex\r\n }\r\n return style\r\n})\r\n\r\nconst placeholderStyle = computed(() => {\r\n if (props.fixed && props.placeholder && height.value) {\r\n return { height: `${height.value}px` }\r\n }\r\n return {}\r\n})\r\n\r\nconst presets = computed(() => {\r\n return props.ballColors.map((color, index) => ({\r\n top: 35 - index * 2,\r\n width: 12,\r\n height: 12,\r\n offsetXStart: index,\r\n shiftY: props.ballShiftY || -5,\r\n backgroundColor: color,\r\n }))\r\n})\r\n\r\nconst ballCount = computed(() => props.ballColors.length || 3)\r\n\r\nconst activeIndex = computed(() => {\r\n const index = children.findIndex((child: any, idx: number) => getChildName(child, idx) === props.modelValue)\r\n return index >= 0 ? index : 0\r\n})\r\n\r\nfunction getChildName(child: any, index: number) {\r\n const childProps = child?.$props || {}\r\n return childProps.name !== undefined ? childProps.name : index\r\n}\r\n\r\nfunction getDropMetrics(itemRect: any, baseRect: any) {\r\n const size = props.shape === 'round' ? 32 : 44\r\n const top = props.shape === 'round' ? -10 : -22\r\n return {\r\n left: itemRect.left - baseRect.left + itemRect.width / 2 - size / 2,\r\n top,\r\n size,\r\n }\r\n}\r\n\r\nfunction getFallbackRect(baseRect: any, index: number, count: number) {\r\n const safeCount = Math.max(count, 1)\r\n const width = baseRect.width / safeCount\r\n return {\r\n left: baseRect.left + width * index,\r\n width,\r\n height: baseRect.height,\r\n }\r\n}\r\n\r\nfunction syncDropIndicator(animate = false) {\r\n if (props.animation !== 'drop') {\r\n dropBallReady.value = false\r\n dropBallAnimating.value = false\r\n pendingDropAnimation.value = false\r\n return\r\n }\r\n\r\n const query = uni.createSelectorQuery().in(proxy)\r\n query.select('.reborn-tabbar-base').boundingClientRect()\r\n query.selectAll('.reborn-tabbar-trigger').boundingClientRect()\r\n query.exec((res: any[]) => {\r\n const baseRect = res?.[0]\r\n const rects = (res?.[1] || []).filter(Boolean)\r\n if (!baseRect) return\r\n\r\n const targetRect = rects[activeIndex.value] || getFallbackRect(baseRect, activeIndex.value, children.length || 1)\r\n const metrics = getDropMetrics(targetRect, baseRect)\r\n\r\n dropBallTop.value = metrics.top\r\n dropBallSize.value = metrics.size\r\n\r\n if (!dropBallReady.value) {\r\n dropBallReady.value = true\r\n dropBallStartLeft.value = metrics.left\r\n dropBallLeft.value = metrics.left\r\n return\r\n }\r\n\r\n if (!animate) {\r\n dropBallAnimating.value = false\r\n dropBallStartLeft.value = metrics.left\r\n dropBallLeft.value = metrics.left\r\n return\r\n }\r\n\r\n dropBallStartLeft.value = dropBallLeft.value\r\n dropBallAnimating.value = false\r\n setTimeout(() => {\r\n dropBallLeft.value = metrics.left\r\n dropBallAnimating.value = true\r\n }, 16)\r\n })\r\n}\r\n\r\nfunction scheduleLayoutSync(animate = false, delay = 0) {\r\n if (layoutTimer) clearTimeout(layoutTimer)\r\n layoutTimer = setTimeout(() => {\r\n nextTick(() => {\r\n syncDropIndicator(animate)\r\n setPlaceholderHeight()\r\n })\r\n }, delay)\r\n}\r\n\r\nwatch(\r\n () => [props.fixed, props.placeholder, props.shape, props.safeAreaInsetBottom, props.animation, props.bordered, props.pureIcon],\r\n () => {\r\n scheduleLayoutSync(false, 16)\r\n }\r\n)\r\n\r\nwatch(\r\n () => children.length,\r\n () => {\r\n scheduleLayoutSync(false, 16)\r\n }\r\n)\r\n\r\nwatch(\r\n () => props.modelValue,\r\n () => {\r\n const shouldAnimate = props.animation === 'drop' && pendingDropAnimation.value\r\n scheduleLayoutSync(shouldAnimate, shouldAnimate ? 16 : 0)\r\n pendingDropAnimation.value = false\r\n\r\n if (shouldAnimate) {\r\n if (unlockTimer) clearTimeout(unlockTimer)\r\n unlockTimer = setTimeout(() => {\r\n dropBallAnimating.value = false\r\n locked.value = false\r\n }, DROP_ANIMATION_DURATION)\r\n }\r\n }\r\n)\r\n\r\nonMounted(() => {\r\n scheduleLayoutSync(false, 32)\r\n})\r\n\r\nonUnmounted(() => {\r\n if (transitionTimer) clearTimeout(transitionTimer)\r\n if (layoutTimer) clearTimeout(layoutTimer)\r\n if (unlockTimer) clearTimeout(unlockTimer)\r\n})\r\n\r\nfunction setChange(child: TabbarItem) {\r\n const active = child.name\r\n\r\n if (active === props.modelValue) return\r\n if (locked.value) return\r\n\r\n const done = () => {\r\n const currentIndex = children.findIndex((c: any, i: number) => getChildName(c, i) === props.modelValue)\r\n oldIndexRef.value = currentIndex >= 0 ? currentIndex : 0\r\n\r\n emit('update:modelValue', active)\r\n emit('change', { value: active })\r\n\r\n if (props.animation === 'fly-balls') {\r\n const nextIdx = children.findIndex((c: any, i: number) => getChildName(c, i) === active)\r\n newIndexRef.value = nextIdx >= 0 ? nextIdx : 0\r\n\r\n locked.value = true\r\n isTransition.value = false\r\n setTimeout(() => {\r\n isTransition.value = true\r\n if (transitionTimer) clearTimeout(transitionTimer)\r\n transitionTimer = setTimeout(() => {\r\n isTransition.value = false\r\n locked.value = false\r\n }, 600)\r\n }, 20)\r\n } else if (props.animation === 'drop') {\r\n locked.value = true\r\n pendingDropAnimation.value = true\r\n } else {\r\n locked.value = false\r\n }\r\n }\r\n\r\n if (props.beforeChange && typeof props.beforeChange === 'function') {\r\n locked.value = true\r\n const wrappedDone = (shouldProceed: boolean = true) => {\r\n if (shouldProceed) {\r\n done()\r\n } else {\r\n locked.value = false\r\n }\r\n }\r\n const result = props.beforeChange({ name: active }, wrappedDone)\r\n if (result === true) {\r\n wrappedDone()\r\n } else if (result && typeof (result as Promise<any>).then === 'function') {\r\n ; (result as Promise<any>).then((isPass: boolean | void) => {\r\n if (isPass !== false) {\r\n wrappedDone()\r\n } else {\r\n locked.value = false\r\n }\r\n }).catch(() => {\r\n locked.value = false\r\n })\r\n } else if (result === false) {\r\n locked.value = false\r\n }\r\n } else {\r\n done()\r\n }\r\n}\r\n\r\nconst getBallStyle = (ballIndex: number) => {\r\n const count = children.length || 1\r\n const renderOldIndex = oldIndexRef.value\r\n const renderNewIndex = newIndexRef.value\r\n const startLeftPercent = ((renderOldIndex + 0.5) / count) * 100\r\n const endLeftPercent = ((renderNewIndex + 0.5) / count) * 100\r\n const p = presets.value[ballIndex % presets.value.length]\r\n const duration = ballCount.value * 0.2 + 0.1\r\n\r\n return {\r\n '--fly-ball-left': `calc(${endLeftPercent}%)`,\r\n '--fly-ball-start-left': `calc(${startLeftPercent}% + ${p.offsetXStart}px)`,\r\n '--fly-ball-top': `${p.top}rpx`,\r\n '--fly-ball-shift': `${p.shiftY}rpx`,\r\n '--fly-ball-jump': `-${40 + (ballIndex % 3) * 5}px`,\r\n '--fly-ball-duration': `${duration - ballIndex * 0.15}s`,\r\n left: 'var(--fly-ball-start-left)',\r\n top: 'var(--fly-ball-top)',\r\n width: `${p.width}rpx`,\r\n height: `${p.height}rpx`,\r\n borderRadius: '50%',\r\n backgroundColor: props.ballColors?.[ballIndex % count] || '#000',\r\n opacity: 0,\r\n }\r\n}\r\n\r\nfunction setPlaceholderHeight() {\r\n if (!props.fixed || !props.placeholder) {\r\n height.value = ''\r\n return\r\n }\r\n\r\n const query = uni.createSelectorQuery().in(proxy)\r\n query.select('.reborn-tabbar-base').boundingClientRect((res: any) => {\r\n if (res) {\r\n const extra = props.animation === 'drop' ? Math.max(-dropBallTop.value, 0) : 0\r\n height.value = Number(res.height) + extra\r\n }\r\n }).exec()\r\n}\r\n</script>\r\n\r\n<template>\r\n <view :class=\"ui.root({ class: cn(customClass) })\" :style=\"placeholderStyle\">\r\n <view class=\"reborn-tabbar-base\" :class=\"ui.base()\" :style=\"[rootStyle, customStyle]\">\r\n <view v-if=\"dropBallVisible\" :class=\"[ui.dropBall(), { 'drop-ball-bounce': dropBallAnimating }]\" :style=\"{\r\n left: '0px',\r\n top: `${dropBallTop}px`,\r\n width: `${dropBallSize}px`,\r\n height: `${dropBallSize}px`,\r\n transform: `translate3d(${dropBallLeft}px, 0, 0)`,\r\n opacity: props.shape === 'round' ? (dropBallAnimating ? 1 : 0) : 1,\r\n '--drop-ball-start-x': `${dropBallStartLeft}px`,\r\n '--drop-ball-end-x': `${dropBallLeft}px`,\r\n '--drop-ball-peak': props.shape === 'round' ? '-26px' : '-34px'\r\n }\">\r\n </view>\r\n\r\n <view v-if=\"animation === 'fly-balls'\" :class=\"ui.flyBallsContainer()\">\r\n <view v-for=\"(color, index) in ballColors\" :key=\"index\"\r\n :class=\"[ui.flyBallItem(), isTransition ? 'fly-ball-anim-dynamic' : '']\"\r\n :style=\"getBallStyle(index)\">\r\n </view>\r\n </view>\r\n\r\n <slot />\r\n </view>\r\n </view>\r\n</template>\r\n\r\n<style scoped>\r\n@keyframes flyBallsJump {\r\n 0% {\r\n transform: translateY(0) scale(1) translateX(0);\r\n opacity: 0;\r\n left: var(--fly-ball-start-left);\r\n }\r\n\r\n 10% {\r\n opacity: 1;\r\n left: var(--fly-ball-start-left);\r\n }\r\n\r\n 50% {\r\n transform: translateY(var(--fly-ball-jump)) scale(1.1);\r\n opacity: 1;\r\n left: calc(var(--fly-ball-start-left) / 2 + var(--fly-ball-left) / 2);\r\n }\r\n\r\n 80% {\r\n transform: translateY(var(--fly-ball-shift)) scale(1.2) translateX(0);\r\n opacity: 1;\r\n left: var(--fly-ball-left);\r\n }\r\n\r\n 100% {\r\n transform: translateY(calc(var(--fly-ball-shift) + 15px)) scale(0) translateX(0);\r\n opacity: 0;\r\n left: var(--fly-ball-left);\r\n }\r\n}\r\n\r\n.fly-ball-anim-dynamic {\r\n animation: flyBallsJump var(--fly-ball-duration) ease-in-out forwards;\r\n}\r\n\r\n.drop-ball-bounce {\r\n animation: dropBallParabola 0.58s cubic-bezier(0.25, 0.9, 0.3, 1) both;\r\n}\r\n\r\n@keyframes dropBallParabola {\r\n 0% {\r\n transform: translate3d(var(--drop-ball-start-x), 0, 0);\r\n }\r\n\r\n 50% {\r\n transform: translate3d(calc((var(--drop-ball-start-x) + var(--drop-ball-end-x)) / 2), var(--drop-ball-peak), 0);\r\n }\r\n\r\n 100% {\r\n transform: translate3d(var(--drop-ball-end-x), 0, 0);\r\n }\r\n}\r\n</style>\r\n",
|
|
23
|
+
"target": "uniapp"
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
"path": "RebornTabbarTrigger.vue",
|
|
27
|
+
"content": "<script lang=\"ts\">\r\nexport default {\r\n name: 'reborn-tabbar-trigger',\r\n options: {\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, ref, watch } from 'vue'\r\nimport { tv } from '@/lib/tv'\r\nimport { cn } from '@/lib/utils'\r\nimport theme from './reborn-tabbar-trigger.config'\r\nimport RebornImage from '@/components/reborn-image/RebornImage.vue'\r\nimport { useParent } from '@/composables/useChildren'\r\nimport { TABBAR_KEY } from '@/components/reborn-tabbar/types'\r\nimport type { tabbarShapes } from '@/components/reborn-tabbar/reborn-tabbar.config'\r\n\r\nexport interface TabbarTriggerProps {\r\n /** 标签页标题 */\r\n title?: string\r\n /** 唯一标识符 */\r\n name?: number | string\r\n /** 图标名称或图片链接 */\r\n icon?: string\r\n /** 未选中时的图标名称或图片链接 */\r\n inactive?: string\r\n /** 是否禁用 */\r\n disabled?: boolean\r\n /** 自定义样式类 */\r\n customClass?: any\r\n /** 自定义内联样式 */\r\n customStyle?: string\r\n /** 颜色 */\r\n color?: string\r\n /** 图片大小 */\r\n imageSize?: number\r\n /** UI 样式覆盖 */\r\n ui?: Partial<Record<'root' | 'body' | 'icon' | 'activeIcon' | 'inactiveIcon' | 'iconInner' | 'title' | 'glowLayer' | 'bodyGlowLayer', string>>\r\n}\r\n\r\nconst props = withDefaults(defineProps<TabbarTriggerProps>(), {\r\n disabled: false,\r\n customStyle: '',\r\n})\r\n\r\nconst { parent: tabbar, index } = useParent(TABBAR_KEY)\r\n\r\nconst active = computed(() => {\r\n const itemName = props.name !== undefined ? props.name : index.value\r\n if (tabbar) {\r\n return tabbar.props.modelValue === itemName\r\n }\r\n return false\r\n})\r\n\r\nconst parentColor = computed(() => {\r\n if (props.color) {\r\n return props.color\r\n }\r\n if (tabbar && tabbar.props.color) {\r\n return tabbar.props.color\r\n }\r\n return 'primary'\r\n})\r\n\r\nconst parentShape = computed(() => {\r\n if (tabbar && tabbar.props.shape) {\r\n return tabbar.props.shape as (typeof tabbarShapes)[number]\r\n }\r\n return 'normal'\r\n})\r\n\r\nconst parentAnimation = computed(() => {\r\n if (tabbar && tabbar.props.animation) {\r\n return tabbar.props.animation as 'fade' | 'flip' | 'reveal' | 'creative' | 'glass' | 'fly-balls' | 'drop'\r\n }\r\n return 'fade'\r\n})\r\n\r\nconst pureIcon = computed(() => Boolean(tabbar?.props.pureIcon))\r\nconst hasTitle = computed(() => Boolean(props.title))\r\nconst isNormalDrop = computed(() => parentShape.value === 'normal' && parentAnimation.value === 'drop')\r\nconst isRoundDropPureIcon = computed(() =>\r\n parentShape.value === 'round' && parentAnimation.value === 'drop' && pureIcon.value\r\n)\r\n\r\nconst rootExtraClass = computed(() => {\r\n if (parentShape.value === 'round' && pureIcon.value) {\r\n return '!flex-1'\r\n }\r\n return ''\r\n})\r\n\r\nconst bodyExtraClass = computed(() => {\r\n const classes: string[] = []\r\n if (parentShape.value === 'round' && pureIcon.value) {\r\n classes.push('!px-0 !w-[64rpx] !min-w-[64rpx]')\r\n }\r\n if (isRoundDropPureIcon.value) {\r\n if (active.value) {\r\n classes.push('shadow-[0_8px_20px_rgba(15,23,42,0.08)]')\r\n } else {\r\n classes.push('!bg-transparent shadow-none')\r\n }\r\n }\r\n return classes.join(' ')\r\n})\r\n\r\nconst iconExtraClass = computed(() => {\r\n if (!active.value) return ''\r\n if (isNormalDrop.value) {\r\n return pureIcon.value ? '!-translate-y-[22px]' : '!-translate-y-[18px]'\r\n }\r\n return ''\r\n})\r\n\r\nconst b = tv(theme)\r\n\r\nconst uiOverrides = computed(() => props.ui || {})\r\n\r\nconst ui = computed(() => {\r\n const styles = b({\r\n active: active.value,\r\n disabled: props.disabled,\r\n color: parentColor.value as any,\r\n shape: parentShape.value as any,\r\n animation: parentAnimation.value as any,\r\n })\r\n\r\n return {\r\n root: (opts?: { class?: any }) => styles.root({ class: cn(opts?.class, uiOverrides.value.root) }),\r\n body: (opts?: { class?: any }) => styles.body({ class: cn(opts?.class, uiOverrides.value.body) }),\r\n icon: (opts?: { class?: any }) => styles.icon({ class: cn(opts?.class, uiOverrides.value.icon) }),\r\n activeIcon: (opts?: { class?: any }) => styles.activeIcon({ class: cn(opts?.class, uiOverrides.value.activeIcon) }),\r\n inactiveIcon: (opts?: { class?: any }) => styles.inactiveIcon({ class: cn(opts?.class, uiOverrides.value.inactiveIcon) }),\r\n iconInner: (opts?: { class?: any }) => styles.iconInner({ class: cn(opts?.class, uiOverrides.value.iconInner) }),\r\n title: (opts?: { class?: any }) => styles.title({ class: cn(opts?.class, uiOverrides.value.title) }),\r\n glowLayer: (opts?: { class?: any }) => styles.glowLayer({ class: cn(opts?.class, uiOverrides.value.glowLayer) }),\r\n bodyGlowLayer: (opts?: { class?: any }) => styles.bodyGlowLayer({ class: cn(opts?.class, uiOverrides.value.bodyGlowLayer) }),\r\n }\r\n})\r\n\r\nconst textStyle = computed(() => {\r\n if (tabbar) {\r\n if (active.value && tabbar.props.activeColor) {\r\n return { color: tabbar.props.activeColor }\r\n }\r\n if (!active.value && tabbar.props.inactiveColor) {\r\n return { color: tabbar.props.inactiveColor }\r\n }\r\n }\r\n return {}\r\n})\r\n\r\nconst isImage = (name?: string) => {\r\n if (!name) return false\r\n return name.includes('/') || name.includes('.') || name.startsWith('http') || name.startsWith('data:image')\r\n}\r\n\r\nconst isShaking = ref(false)\r\nlet shakeTimer: any = null\r\n\r\nconst isJelly = ref(false)\r\n\r\nwatch(() => active.value, (newVal) => {\r\n if (parentAnimation.value === 'fly-balls') {\r\n if (newVal) {\r\n isJelly.value = true\r\n setTimeout(() => {\r\n isJelly.value = false\r\n }, 500)\r\n }\r\n }\r\n}, { immediate: true })\r\n\r\n/**\r\n * 点击 tabbar 选项\r\n */\r\nfunction handleClick() {\r\n if (props.disabled) return\r\n if (tabbar && tabbar.locked.value) return\r\n const itemName: string | number = props.name !== undefined ? props.name : index.value\r\n\r\n if (active.value) {\r\n if (shakeTimer) clearTimeout(shakeTimer)\r\n isShaking.value = false\r\n setTimeout(() => {\r\n isShaking.value = true\r\n shakeTimer = setTimeout(() => {\r\n isShaking.value = false\r\n }, 300)\r\n }, 10)\r\n return\r\n }\r\n\r\n if (tabbar) {\r\n tabbar.setChange({ name: itemName })\r\n }\r\n}\r\n</script>\r\n\r\n<template>\r\n <view class=\"reborn-tabbar-trigger\" :class=\"ui.root({ class: cn(customClass, rootExtraClass) })\" :style=\"customStyle\"\r\n @click=\"handleClick\">\r\n <slot :active=\"active\" :ui=\"ui\">\r\n <!-- round + glass: bodyGlowLayer 包裹 body,提供颜色背景;默认 display:contents 不影响布局 -->\r\n <view :class=\"ui.bodyGlowLayer()\">\r\n <view :class=\"ui.body({ class: bodyExtraClass })\">\r\n <!-- Icon area with animation -->\r\n <view v-if=\"$slots.icon || icon\"\r\n :class=\"[ui.icon({ class: iconExtraClass }), isShaking ? 'animate-[shake_0.3s_ease-in-out]' : '', isJelly ? 'fly-balls-jelly' : '']\">\r\n <slot name=\"icon\" :active=\"active\" :ui=\"ui\">\r\n <!-- 选中时 -->\r\n <view :class=\"ui.activeIcon()\" :style=\"textStyle\">\r\n <RebornImage v-if=\"isImage(icon)\" :width=\"imageSize || 40\" :height=\"imageSize || 40\"\r\n :src=\"icon!\" mode=\"scaleToFill\" />\r\n <view v-else :class=\"['text-40', icon]\" />\r\n </view>\r\n <!-- 未选中时 -->\r\n <view :class=\"ui.inactiveIcon()\" :style=\"textStyle\">\r\n <RebornImage v-if=\"isImage(inactive || icon)\" :width=\"imageSize || 40\"\r\n :height=\"imageSize || 40\" :src=\"inactive! || icon!\" mode=\"scaleToFill\" />\r\n <view v-else :class=\"['text-40', inactive || icon]\" />\r\n </view>\r\n </slot>\r\n <!-- Glass 动画: icon 级别渐变光影背景层 (normal + glass) -->\r\n <view :class=\"ui.glowLayer()\" />\r\n </view>\r\n\r\n <!-- 标题 -->\r\n <view v-if=\"title && !pureIcon\" :class=\"ui.title()\" :style=\"textStyle\">\r\n {{ title }}\r\n </view>\r\n </view>\r\n </view>\r\n </slot>\r\n </view>\r\n</template>\r\n\r\n<style scoped>\r\n.fly-balls-jelly {\r\n animation: jelly 0.5s ease-in-out;\r\n}\r\n\r\n@keyframes jelly {\r\n\r\n 0%,\r\n 100% {\r\n transform: scale(1, 1);\r\n }\r\n\r\n 25% {\r\n transform: scale(1, 0.5);\r\n }\r\n\r\n 50% {\r\n transform: scale(0.75, 1);\r\n }\r\n\r\n 75% {\r\n transform: scale(1, 0.75);\r\n }\r\n}\r\n</style>\r\n",
|
|
28
|
+
"target": "uniapp"
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
"path": "types.ts",
|
|
32
|
+
"content": "import type { InjectionKey } from 'vue'\r\nimport type { tabbarColors, tabbarShapes } from './reborn-tabbar.config'\r\n\r\n/**\r\n * TabbarItem 子项数据结构\r\n */\r\nexport interface TabbarItem {\r\n name: string | number\r\n}\r\n\r\n/**\r\n * TabbarProvide - 提供给子组件的数据\r\n */\r\nexport type TabbarProvide = {\r\n props: {\r\n modelValue?: number | string\r\n fixed?: boolean\r\n safeAreaInsetBottom?: boolean\r\n bordered?: boolean\r\n pureIcon?: boolean\r\n shape?: (typeof tabbarShapes)[number] | null\r\n animation?: 'fade' | 'flip' | 'reveal' | 'creative' | 'glass' | 'fly-balls' | 'drop' | null\r\n activeColor?: string\r\n inactiveColor?: string\r\n placeholder?: boolean\r\n zIndex?: number\r\n color?: (typeof tabbarColors)[number]\r\n }\r\n setChange: (child: TabbarItem) => void\r\n locked: import('vue').Ref<boolean>\r\n}\r\n\r\nexport const TABBAR_KEY: InjectionKey<TabbarProvide> = Symbol('reborn-tabbar')\r\n",
|
|
33
|
+
"target": "uniapp"
|
|
34
|
+
}
|
|
35
|
+
],
|
|
36
|
+
"fileCount": 6,
|
|
37
|
+
"contentHash": "10498929e89fbd81f96ab672eea95f5a5687bd70"
|
|
38
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "reborn-tabs copy",
|
|
3
|
+
"dependencies": [
|
|
4
|
+
"@vueuse/core",
|
|
5
|
+
"clsx"
|
|
6
|
+
],
|
|
7
|
+
"files": [
|
|
8
|
+
{
|
|
9
|
+
"path": "index.ts",
|
|
10
|
+
"content": "export { default as TabsContent } from './TabsContent.vue'\r\nexport { default as TabsList } from './TabsList.vue'\r\nexport { default as TabsRoot } from './TabsRoot.vue'\r\nexport { default as TabsTrigger } from './TabsTrigger.vue'\r\n",
|
|
11
|
+
"target": "uniapp"
|
|
12
|
+
},
|
|
13
|
+
{
|
|
14
|
+
"path": "reborn-tabs.config.ts",
|
|
15
|
+
"content": "export const tabsTypes = ['line', 'card'] as const\r\nexport const tabsVariants = ['primary', 'info', 'success', 'warning', 'neutral'] as const\r\nexport const tabsSizes = ['sm', 'md', 'lg'] as const\r\nexport const tabsOrientations = ['horizontal', 'vertical'] as const\r\n\r\nexport default {\r\n slots: {\r\n root: 'flex flex-col gap-2 min-w-0',\r\n list: 'relative flex max-w-full box-border gap-2 [&::-webkit-scrollbar]:hidden [-ms-overflow-style:none] [scrollbar-width:none]',\r\n trigger:\r\n 'relative z-10 inline-flex items-center justify-center gap-2 whitespace-nowrap px-3 py-2 text-32 font-medium text-gray-7 ring-offset-background transition-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-20 flex-1',\r\n leadingIcon: 'flex items-center text-28',\r\n leadingAvatar: 'flex items-center overflow-hidden rounded-full',\r\n leadingAvatarSize: 'h-6 w-6',\r\n label: 'relative z-10',\r\n trailingBadge: 'flex items-center rounded-full bg-gray-2 px-2 py-0.5 text-24 text-gray-7',\r\n trailingBadgeSize: 'text-24',\r\n content:\r\n 'mt-2 ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 scroll-mt-24',\r\n indicator:\r\n 'absolute bottom-1 left-0 h-1.5 w-0 rounded-full transition-all duration-300 ease-in-out z-0 opacity-90 -translate-y-1',\r\n },\r\n variants: {\r\n type: {\r\n line: {\r\n list: 'border-b border-gray-2',\r\n trigger: 'bg-transparent shadow-none rounded-none !text-gray-7 hover:text-gray-8 data-[state=active]:font-bold data-[state=active]:!text-gray-8',\r\n indicator: 'block',\r\n },\r\n card: {\r\n list: 'inline-flex items-center justify-start rounded-[var(--radius-ui-base)] bg-gray-2/70 p-1',\r\n trigger: 'rounded-[var(--radius-ui-sm)] px-3 py-2 shadow-none !text-gray-7 data-[state=active]:!text-white',\r\n indicator: 'hidden',\r\n },\r\n },\r\n variant: {\r\n primary: {\r\n trigger: 'data-[state=active]:text-primary',\r\n indicator:\r\n 'bg-[linear-gradient(90deg,var(--color-red-6),var(--color-red-3),var(--color-blue-1),var(--color-orange-1))] bg-[length:200%]',\r\n },\r\n info: {\r\n trigger: 'data-[state=active]:text-info',\r\n indicator: 'bg-info',\r\n },\r\n success: {\r\n trigger: 'data-[state=active]:text-success',\r\n indicator: 'bg-success',\r\n },\r\n warning: {\r\n trigger: 'data-[state=active]:text-warning',\r\n indicator: 'bg-warning',\r\n },\r\n neutral: {\r\n trigger: 'data-[state=active]:text-gray-8',\r\n indicator: 'bg-gray-8',\r\n },\r\n },\r\n orientation: {\r\n horizontal: {\r\n root: 'flex-col w-full h-full min-w-0 max-w-full relative overflow-hidden',\r\n list: 'flex-row flex-nowrap w-full min-w-0 max-w-full overflow-x-auto overflow-y-hidden shrink-0',\r\n trigger: 'shrink-0',\r\n indicator:\r\n 'h-1.5 w-[var(--radix-tabs-indicator-width)] translate-x-[var(--radix-tabs-indicator-position)] bottom-1',\r\n content: 'flex-1 w-full min-h-0 overflow-y-auto',\r\n },\r\n vertical: {\r\n root: 'flex-row items-start gap-4 h-full [&>*:not([role=tablist])]:flex-1 [&>*:not([role=tablist])]:w-full [&>*:not([role=tablist])]:h-full relative overflow-hidden',\r\n list: 'flex-col w-auto h-full overflow-y-auto overflow-x-hidden border-b-0 border-r border-gray-2',\r\n trigger: 'flex-initial w-full justify-start border-b-0 border-r-0 rounded-none',\r\n indicator: 'hidden',\r\n content:\r\n 'flex-1 w-full h-full overflow-y-auto mt-0 [&::-webkit-scrollbar]:hidden [-ms-overflow-style:none] [scrollbar-width:none]',\r\n },\r\n },\r\n size: {\r\n sm: {\r\n trigger: 'text-28 px-2.5 py-1.5',\r\n leadingAvatarSize: 'h-5 w-5',\r\n trailingBadgeSize: 'text-20',\r\n },\r\n md: {\r\n trigger: 'text-32 px-3 py-2',\r\n leadingAvatarSize: 'h-6 w-6',\r\n trailingBadgeSize: 'text-22',\r\n },\r\n lg: {\r\n trigger: 'text-36 px-4 py-2.5',\r\n leadingAvatarSize: 'h-7 w-7',\r\n trailingBadgeSize: 'text-24',\r\n },\r\n },\r\n sticky: {\r\n true: {\r\n root: 'h-auto block overflow-visible',\r\n list: 'sticky top-0 z-40 bg-background/95 backdrop-blur',\r\n content: 'h-auto block overflow-visible',\r\n },\r\n },\r\n shrink: {\r\n true: {\r\n list: 'justify-start',\r\n trigger: 'flex-none',\r\n },\r\n },\r\n scrollspy: {\r\n true: {\r\n // Base scrollspy styles\r\n },\r\n },\r\n },\r\n compoundVariants: [\r\n {\r\n orientation: 'horizontal' as (typeof tabsOrientations)[number],\r\n scrollspy: true,\r\n class: {\r\n root: 'block h-auto overflow-visible relative',\r\n list: 'sticky top-0 z-10 bg-background shadow-sm',\r\n content: 'h-auto block',\r\n },\r\n },\r\n {\r\n orientation: 'vertical' as (typeof tabsOrientations)[number],\r\n scrollspy: true,\r\n class: {\r\n root: 'flex flex-row items-start gap-0 h-full min-h-0 overflow-hidden',\r\n list: 'h-full min-h-0 overflow-y-auto overflow-x-hidden border-r border-gray-2 shrink-0 w-auto',\r\n content: 'flex-1 min-w-0 w-full',\r\n },\r\n },\r\n {\r\n type: 'card' as (typeof tabsTypes)[number],\r\n variant: 'primary' as (typeof tabsVariants)[number],\r\n class: {\r\n trigger: 'data-[state=active]:bg-primary data-[state=active]:text-white',\r\n },\r\n },\r\n {\r\n type: 'card' as (typeof tabsTypes)[number],\r\n variant: 'info' as (typeof tabsVariants)[number],\r\n class: {\r\n trigger: 'data-[state=active]:bg-info data-[state=active]:text-white',\r\n },\r\n },\r\n {\r\n type: 'card' as (typeof tabsTypes)[number],\r\n variant: 'success' as (typeof tabsVariants)[number],\r\n class: {\r\n trigger: 'data-[state=active]:bg-success data-[state=active]:text-white',\r\n },\r\n },\r\n {\r\n type: 'card' as (typeof tabsTypes)[number],\r\n variant: 'warning' as (typeof tabsVariants)[number],\r\n class: {\r\n trigger: 'data-[state=active]:bg-warning data-[state=active]:text-white',\r\n },\r\n },\r\n {\r\n type: 'card' as (typeof tabsTypes)[number],\r\n variant: 'neutral' as (typeof tabsVariants)[number],\r\n class: {\r\n trigger: 'data-[state=active]:bg-gray-8 data-[state=active]:text-white',\r\n },\r\n },\r\n {\r\n type: 'card' as (typeof tabsTypes)[number],\r\n orientation: 'vertical' as (typeof tabsOrientations)[number],\r\n class: {\r\n list: 'border-r-0',\r\n trigger: 'border-r-0',\r\n },\r\n },\r\n ],\r\n defaultVariants: {\r\n type: 'line' as (typeof tabsTypes)[number],\r\n variant: 'primary' as (typeof tabsVariants)[number],\r\n orientation: 'horizontal' as (typeof tabsOrientations)[number],\r\n size: 'md' as (typeof tabsSizes)[number],\r\n sticky: false,\r\n shrink: false,\r\n },\r\n}\r\n",
|
|
16
|
+
"target": "uniapp"
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
"path": "TabsContent.vue",
|
|
20
|
+
"content": "<script setup lang=\"ts\">\r\nimport { computed, getCurrentInstance, inject, nextTick, onBeforeUnmount, ref, watch } from 'vue'\r\nimport { cn } from '@/lib/utils'\r\n\r\nconst props = defineProps<{\r\n index?: number\r\n customClass?: any\r\n}>()\r\n\r\nconst context = inject('TabsContext') as any\r\nconst localIndex = ref<number>(context.registerContent(props.index))\r\nconst instance = getCurrentInstance()\r\n\r\n// Register ID for scroll handling (if page scroll logic is used)\r\nconst contentId = computed(() => `${context.rootId}-content-${localIndex.value}`)\r\nwatch(contentId, (id) => {\r\n if (localIndex.value !== undefined) {\r\n context.registerContentId(localIndex.value, id)\r\n }\r\n}, { immediate: true })\r\n\r\n// Direction logic\r\nconst transitionName = computed(() => {\r\n if (isScrollspy.value) { return undefined }\r\n\r\n const isVertical = context.orientation.value === 'vertical'\r\n const dir = context.direction.value\r\n\r\n if (isVertical) {\r\n return dir === 'next' ? 'tabs-slide-up' : 'tabs-slide-down'\r\n }\r\n return dir === 'next' ? 'tabs-slide-left' : 'tabs-slide-right'\r\n})\r\n\r\nconst isActive = computed(() => context.activeIndex.value === localIndex.value)\r\nconst isScrollspy = computed(() => context.scrollspy.value)\r\nlet observer: UniApp.IntersectionObserver | null = null\r\n\r\n// --- Swipe Logic ---\r\nlet startX = 0\r\nlet startY = 0\r\n\r\nfunction onTouchStart(e: any) {\r\n if (!context.swipeable.value) { return }\r\n if (e.changedTouches && e.changedTouches.length > 0) {\r\n startX = e.changedTouches[0].clientX\r\n startY = e.changedTouches[0].clientY\r\n }\r\n}\r\n\r\nfunction onTouchEnd(e: any) {\r\n if (!context.swipeable.value) { return }\r\n if (e.changedTouches && e.changedTouches.length > 0) {\r\n const endX = e.changedTouches[0].clientX\r\n const endY = e.changedTouches[0].clientY\r\n const dx = endX - startX\r\n const dy = endY - startY\r\n const threshold = 50\r\n\r\n const isHorizontal = context.orientation.value === 'horizontal'\r\n const isVertical = context.orientation.value === 'vertical'\r\n const maxIndex = context.contentCounter.value\r\n const currentIndex = context.activeIndex.value\r\n\r\n if (isHorizontal) {\r\n if (Math.abs(dx) > threshold && Math.abs(dy) < threshold) { // horizontal swipe\r\n if (dx < 0) {\r\n // Swipe Left -> Next\r\n if (currentIndex < maxIndex - 1) { context.setActiveIndex(currentIndex + 1) }\r\n }\r\n else {\r\n // Swipe Right -> Prev\r\n if (currentIndex > 0) { context.setActiveIndex(currentIndex - 1) }\r\n }\r\n }\r\n }\r\n else if (isVertical) {\r\n if (Math.abs(dy) > threshold && Math.abs(dx) < threshold) {\r\n if (dy < 0) {\r\n // Swipe Up -> Next\r\n if (currentIndex < maxIndex - 1) { context.setActiveIndex(currentIndex + 1) }\r\n }\r\n else {\r\n // Swipe Down -> Prev\r\n if (currentIndex > 0) { context.setActiveIndex(currentIndex - 1) }\r\n }\r\n }\r\n }\r\n }\r\n}\r\n\r\nwatch(\r\n isScrollspy,\r\n async (enabled) => {\r\n if (!enabled) {\r\n if (observer) {\r\n observer.disconnect()\r\n observer = null\r\n }\r\n return\r\n }\r\n\r\n await nextTick()\r\n\r\n // In UniApp, we create observer from the instance\r\n if (observer) { observer.disconnect() }\r\n\r\n observer = uni.createIntersectionObserver(instance)\r\n\r\n // We observe relative to viewport (or a scroll view if we knew it).\r\n // Since we don't know the scroll parent, observing relative to viewport is safest default for scrollspy.\r\n observer.relativeToViewport({ bottom: -100 }) // Adjust margins as needed\r\n observer.observe(`#${contentId.value}`, (res) => {\r\n if (res.intersectionRatio > 0) {\r\n // intersection\r\n // If we have multiple contents intersecting, the last one usually wins or the first one?\r\n // Simple logic: if intersecting, set active.\r\n // Debounce might be needed if scrolling fast.\r\n context.setActiveIndex(localIndex.value)\r\n }\r\n })\r\n },\r\n { immediate: true },\r\n)\r\n\r\nonBeforeUnmount(() => {\r\n if (observer) { observer.disconnect() }\r\n if (localIndex.value !== undefined) {\r\n context.unregisterContentId(localIndex.value)\r\n }\r\n})\r\n</script>\r\n\r\n<template>\r\n <view\r\n v-show=\"isScrollspy || isActive\" :id=\"contentId\" role=\"tabpanel\"\r\n :data-state=\"isActive ? 'active' : 'inactive'\" :data-index=\"localIndex\"\r\n :class=\"context.ui.value.content({ class: cn(props.customClass, context.uiOverrides.value?.content) })\"\r\n @touchstart=\"onTouchStart\" @touchend=\"onTouchEnd\"\r\n >\r\n <slot />\r\n </view>\r\n</template>\r\n",
|
|
21
|
+
"target": "uniapp"
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
"path": "TabsList.vue",
|
|
25
|
+
"content": "<script setup lang=\"ts\">\r\nimport { getCurrentInstance, inject, nextTick, onMounted, ref, watch } from 'vue'\r\n// import { useResizeObserver } from \"@vueuse/core\"; // Removed non-uniapp compatible observer\r\nimport { cn } from '@/lib/utils'\r\n\r\nconst props = defineProps<{\r\n customClass?: any\r\n}>()\r\n\r\nconst context = inject('TabsContext') as any\r\n\r\nconst instance = getCurrentInstance()\r\nconst indicatorStyle = ref({})\r\nconst scrollIntoId = ref('')\r\n\r\n// --- Indicator logic ---\r\nfunction updateIndicator() {\r\n // In UniApp, we must use createSelectorQuery\r\n // We need to measure the List relative to the active Trigger.\r\n // Or rather the Active Trigger relative to the List.\r\n const activeIdx = context.activeIndex.value\r\n const triggerId = `#${context.rootId}-trigger-${activeIdx}`\r\n\r\n // We assume the root of TabsList has a specific class or ID we can target if needed,\r\n // but better to target the trigger directly and the list container.\r\n // However, in UniApp customized component, selecting 'in(instance)' is best.\r\n\r\n const query = uni.createSelectorQuery().in(instance)\r\n\r\n // Select the List container (this component's root or the scroll-view)\r\n // Select the List container (this component's root or the scroll-view)\r\n // query.select(`#${context.rootId}-list`).boundingClientRect(); // legacy check\r\n\r\n // Select the active trigger (inside the slot)\r\n // Note: Since Trigger is in a SLOT, selection might be tricky if it's not part of this component's shadow DOM.\r\n // But in Vue/UniApp MP, slots are part of the page/parent usually.\r\n // Wait, if TabsList is a component, and Trigger is passed in slot,\r\n // `query.in(instance)` might NOT find the trigger if it is not in `TabsList`'s template?\r\n // Actually, in default slot, it IS children.\r\n // If it fails, we might need to query from the Page context or ask `TabsRoot` to query?\r\n // Let's try `in(instance)` first, if that fails, we might need a workaround.\r\n // Another approach: coordinate via TabsRoot. Note that Refs don't work across slots comfortably in MP.\r\n // Trying global selection (without .in(instance)) might work if IDs are unique.\r\n\r\n const globalQuery = uni.createSelectorQuery() // Try global first or in root?\r\n // actually `uni.createSelectorQuery()` in a component is scoped to component? No.\r\n // Without `in`, it is global in H5, but page-scoped in MP.\r\n\r\n // Select the active trigger content wrapper if available, otherwise fallback to trigger.\r\n // In UniApp, we can try selecting the descendant.\r\n // If we select `#id .class`, it should work.\r\n\r\n // Changing logic: Query BOTH the trigger and the inner content.\r\n // If inner content exists, use it. If not, use trigger.\r\n // But `exec` returns array order.\r\n\r\n // Let's select the inner content primarily.\r\n globalQuery.select(`${triggerId} .rb-tabs__trigger-inner`).boundingClientRect()\r\n globalQuery.select(triggerId).boundingClientRect() // Fallback\r\n globalQuery.select(`#${context.rootId}-list`).boundingClientRect()\r\n\r\n globalQuery.exec((res) => {\r\n if (!res || res.length < 3) {\r\n // If array length is small, maybe inner query failed or elements not ready.\r\n // We expect [innerRect, triggerRect, listRect].\r\n // If innerRect is null, we use triggerRect.\r\n return\r\n }\r\n\r\n const innerRect = res[0] as UniApp.NodeInfo\r\n const fallbackTriggerRect = res[1] as UniApp.NodeInfo // Was triggerRect\r\n const listRect = res[2] as UniApp.NodeInfo\r\n\r\n if (!fallbackTriggerRect || !listRect) { return } // Critical elements missing\r\n\r\n // Prefer innerRect if valid (width > 0), else use triggerRect\r\n const triggerRect = (innerRect && (innerRect.width ?? 0) > 0) ? innerRect : fallbackTriggerRect\r\n\r\n const isHorizontal = context.orientation.value === 'horizontal'\r\n\r\n if (isHorizontal) {\r\n // Relative left: trigger.left - list.left\r\n // However, if list is scrolled, `list.left` is visible window.\r\n // `trigger.left` is also visible window.\r\n // So `trigger.left - list.left` is the visual offset from left edge.\r\n // But for `indicator` (which is likely inside the scroll-view?),\r\n // we want position relative to the CONTENT start.\r\n // If indicator is `absolute` inside `relative` list, it moves with scroll.\r\n // So we need `visual offset + current scroll-left`.\r\n\r\n // To get current scroll-left? We can listen to scroll event or query it.\r\n // Let's query scroll offset of the list.\r\n\r\n uni.createSelectorQuery().in(instance).select(`#${context.rootId}-list`).scrollOffset((scrollRes) => {\r\n const res = scrollRes as any\r\n const scrollLeft = res.scrollLeft || 0\r\n const scrollTop = res.scrollTop || 0\r\n\r\n const left = (triggerRect.left || 0) - (listRect.left || 0) + scrollLeft\r\n const top = (triggerRect.top || 0) - (listRect.top || 0) + scrollTop\r\n\r\n if (isHorizontal) {\r\n indicatorStyle.value = {\r\n '--radix-tabs-indicator-position': `${left}px`,\r\n '--radix-tabs-indicator-width': `${triggerRect.width}px`,\r\n 'opacity': 1,\r\n }\r\n }\r\n else {\r\n indicatorStyle.value = {\r\n '--radix-tabs-indicator-position': `${top}px`,\r\n '--radix-tabs-indicator-height': `${triggerRect.height}px`,\r\n 'opacity': 1,\r\n }\r\n }\r\n }).exec()\r\n }\r\n else {\r\n // Vertical logic similar\r\n uni.createSelectorQuery().in(instance).select(`#${context.rootId}-list`).scrollOffset((scrollRes) => {\r\n const res = scrollRes as any\r\n const scrollTop = res.scrollTop || 0\r\n const top = (triggerRect.top || 0) - (listRect.top || 0) + scrollTop\r\n\r\n indicatorStyle.value = {\r\n '--radix-tabs-indicator-position': `${top}px`,\r\n '--radix-tabs-indicator-height': `${triggerRect.height}px`,\r\n 'opacity': 1,\r\n }\r\n }).exec()\r\n }\r\n })\r\n}\r\n\r\n// --- Scrolling Logic ---\r\nfunction scrollToActiveTab() {\r\n // Use scroll-into-view\r\n // The ID to scroll to is the trigger ID\r\n const activeIdx = context.activeIndex.value\r\n scrollIntoId.value = `${context.rootId}-trigger-${activeIdx}`\r\n}\r\n\r\n// Watchers\r\nwatch(() => context.activeIndex.value, async () => {\r\n // Wait for DOM update\r\n await nextTick()\r\n // In MP, sometimes need double tick or slight delay\r\n setTimeout(() => {\r\n scrollToActiveTab()\r\n updateIndicator()\r\n }, 50)\r\n})\r\n\r\nonMounted(async () => {\r\n await nextTick()\r\n setTimeout(() => {\r\n scrollToActiveTab()\r\n updateIndicator()\r\n }, 100)\r\n})\r\n\r\n// Since we removed ResizeObserver, we might want to listen to window resize?\r\n// Or just rely on updates.\r\n</script>\r\n\r\n<template>\r\n <scroll-view\r\n :id=\"`${context.rootId}-list`\" :scroll-x=\"context.orientation.value === 'horizontal'\"\r\n :scroll-y=\"context.orientation.value === 'vertical'\" :scroll-into-view=\"scrollIntoId\" scroll-with-animation\r\n role=\"tablist\" :enable-flex=\"true\"\r\n :data-sticky-tabs=\"context.sticky.value || context.scrollspy.value ? 'true' : undefined\"\r\n :class=\"context.ui.value.list({ class: cn(props.customClass, context.uiOverrides.value?.list) })\"\r\n :style=\"indicatorStyle\"\r\n >\r\n <slot />\r\n <slot\r\n name=\"indicator\" :style=\"indicatorStyle\"\r\n :class=\"context.ui.value.indicator({ class: context.uiOverrides.value?.indicator })\"\r\n >\r\n <text\r\n :class=\"context.ui.value.indicator({ class: context.uiOverrides.value?.indicator })\"\r\n aria-hidden=\"true\" :style=\"indicatorStyle\"\r\n />\r\n </slot>\r\n </scroll-view>\r\n</template>\r\n\r\n<style scoped>\r\n:deep(::-webkit-scrollbar) {\r\n display: none;\r\n width: 0;\r\n height: 0;\r\n}\r\n\r\n/* Ensure scroll view content container behaves correctly for flex items */\r\n:deep(.uni-scroll-view-content) {\r\n display: flex;\r\n}\r\n</style>\r\n",
|
|
26
|
+
"target": "uniapp"
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
"path": "TabsRoot.vue",
|
|
30
|
+
"content": "<script setup lang=\"ts\">\r\nimport type { TabsProps } from './types'\r\nimport { useVModel } from '@vueuse/core'\r\nimport { computed, provide, ref, toRef } from 'vue'\r\nimport { tv } from '@/lib/tv'\r\nimport { cn } from '@/lib/utils'\r\n\r\nimport theme from './reborn-tabs.config'\r\n\r\nexport type { TabsProps }\r\n\r\nconst props = withDefaults(defineProps<TabsProps>(), {\r\n defaultActive: 0,\r\n type: 'line',\r\n variant: 'primary',\r\n size: 'md',\r\n orientation: 'horizontal',\r\n sticky: false,\r\n swipeable: false,\r\n shrink: false,\r\n scrollspy: false,\r\n activationMode: 'automatic',\r\n ignorePageScroll: false,\r\n})\r\n\r\nconst emit = defineEmits<{\r\n (e: 'update:active', value: number): void\r\n (e: 'click-tab', value: number, event: any): void\r\n}>()\r\n\r\nconst activeIndex = useVModel(props, 'active', emit, {\r\n defaultValue: props.defaultActive,\r\n passive: true,\r\n})\r\n\r\nconst b = tv(theme)\r\nconst type = toRef(props, 'type')\r\nconst variant = toRef(props, 'variant')\r\nconst size = toRef(props, 'size')\r\nconst orientation = toRef(props, 'orientation')\r\nconst sticky = toRef(props, 'sticky')\r\nconst swipeable = toRef(props, 'swipeable')\r\nconst shrink = toRef(props, 'shrink')\r\nconst scrollspy = toRef(props, 'scrollspy')\r\nconst uiOverrides = computed(() => props.ui ?? {})\r\n\r\nconst ui = computed(() => b({\r\n type: type.value,\r\n variant: variant.value,\r\n size: size.value,\r\n orientation: orientation.value,\r\n sticky: sticky.value,\r\n shrink: shrink.value,\r\n scrollspy: scrollspy.value,\r\n}))\r\n\r\nconst triggerCounter = ref(0)\r\nconst contentCounter = ref(0)\r\n\r\n// Simple ID generator for coordination\r\nconst rootId = `tabs-${Math.random().toString(36).substring(2, 9)}`\r\n\r\nfunction registerTrigger(index?: number) {\r\n if (typeof index === 'number') {\r\n triggerCounter.value = Math.max(triggerCounter.value, index + 1)\r\n return index\r\n }\r\n const value = triggerCounter.value\r\n triggerCounter.value += 1\r\n return value\r\n}\r\n\r\nfunction registerContent(index?: number) {\r\n if (typeof index === 'number') {\r\n contentCounter.value = Math.max(contentCounter.value, index + 1)\r\n return index\r\n }\r\n const value = contentCounter.value\r\n contentCounter.value += 1\r\n return value\r\n}\r\n\r\n// Map of index -> unique ID string (needed for scroll-into-view or selector queries)\r\nconst contentIds = ref<Record<number, string>>({})\r\n\r\nfunction registerContentId(index: number, id: string) {\r\n contentIds.value[index] = id\r\n}\r\n\r\nfunction unregisterContentId(index: number) {\r\n delete contentIds.value[index]\r\n}\r\n\r\nconst currentScrollToId = ref('')\r\n\r\nfunction scrollToContent(index: number) {\r\n const id = contentIds.value[index]\r\n if (id) {\r\n currentScrollToId.value = id\r\n\r\n if (!props.ignorePageScroll) {\r\n // In UniApp, we can try to scroll the page to the content\r\n uni.pageScrollTo({\r\n selector: `#${id}`,\r\n duration: 300,\r\n fail: () => {\r\n // If ID not found or in a scroll-view, this might fail or do nothing.\r\n // We could emit an event for manual handling\r\n // console.warn(`[TabsRoot] Could not scroll to content #${id}. If content is inside a scroll-view, please handle scrolling manually.`);\r\n },\r\n })\r\n }\r\n }\r\n}\r\n\r\nconst direction = ref<'next' | 'prev'>('next')\r\n\r\nfunction setActiveIndex(value: number) {\r\n const currentDefault = activeIndex.value ?? 0\r\n if (value > currentDefault) {\r\n direction.value = 'next'\r\n }\r\n else if (value < currentDefault) {\r\n direction.value = 'prev'\r\n }\r\n activeIndex.value = value\r\n}\r\n\r\nfunction onTabClick(value: number, event: any) {\r\n if (props.activationMode === 'manual') {\r\n // Manual mode handled by Trigger\r\n }\r\n emit('click-tab', value, event)\r\n}\r\n\r\nprovide('TabsContext', {\r\n rootId,\r\n activeIndex,\r\n type,\r\n variant,\r\n size,\r\n orientation,\r\n swipeable,\r\n contentCounter,\r\n activationMode: toRef(props, 'activationMode'),\r\n sticky,\r\n scrollspy,\r\n registerTrigger,\r\n registerContent,\r\n registerContentId,\r\n unregisterContentId,\r\n scrollToContent,\r\n setActiveIndex,\r\n onTabClick,\r\n ui,\r\n uiOverrides,\r\n direction, // Provide direction to context\r\n})\r\n\r\ndefineExpose({\r\n activeIndex,\r\n})\r\n</script>\r\n\r\n<template>\r\n <view :class=\"cn(ui.root(), uiOverrides.root, props.customClass)\">\r\n <slot :ui=\"ui\" :ui-overrides=\"uiOverrides\" :current-scroll-to-id=\"currentScrollToId\" />\r\n </view>\r\n</template>\r\n",
|
|
31
|
+
"target": "uniapp"
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
"path": "TabsTrigger.vue",
|
|
35
|
+
"content": "<script setup lang=\"ts\">\r\nimport { computed, inject, ref } from 'vue'\r\nimport { cn } from '@/lib/utils'\r\n\r\nconst props = defineProps<{\r\n index?: number\r\n disabled?: boolean\r\n label?: string\r\n customClass?: any\r\n}>()\r\n\r\nconst context = inject('TabsContext') as any\r\nconst localIndex = ref<number>(context.registerTrigger(props.index))\r\n\r\n// Generate unique ID for this trigger to be selected by TabsList\r\nconst triggerId = computed(() => `${context.rootId}-trigger-${localIndex.value}`)\r\n\r\nconst isActive = computed(() => context.activeIndex.value === localIndex.value)\r\n\r\nfunction handleClick(event: any) {\r\n if (props.disabled) { return }\r\n\r\n if (context.scrollspy.value) {\r\n context.scrollToContent(localIndex.value)\r\n // We still set active index, but scroll handling might trigger intersection observer\r\n // which sets active index again. That's fine.\r\n context.setActiveIndex(localIndex.value)\r\n }\r\n else {\r\n context.setActiveIndex(localIndex.value)\r\n }\r\n context.onTabClick(localIndex.value, event)\r\n}\r\n</script>\r\n\r\n<template>\r\n <view\r\n :id=\"triggerId\" role=\"tab\" :aria-selected=\"isActive\"\r\n :class=\"context.ui.value.trigger({ class: cn(props.customClass, context.uiOverrides.value?.trigger) })\"\r\n :data-state=\"isActive ? 'active' : 'inactive'\" :data-orientation=\"context.orientation.value\"\r\n :data-disabled=\"props.disabled ? '' : undefined\" :data-index=\"localIndex\" @tap=\"handleClick\"\r\n >\r\n <view\r\n class=\"\r\n rb-tabs__trigger-inner inline-flex items-center justify-center gap-2\r\n \"\r\n >\r\n <text\r\n v-if=\"$slots['leading-icon']\"\r\n :class=\"context.ui.value.leadingIcon({ class: context.uiOverrides.value?.leadingIcon })\"\r\n >\r\n <slot name=\"leading-icon\" />\r\n </text>\r\n <text\r\n v-if=\"$slots['leading-avatar']\"\r\n :class=\"context.ui.value.leadingAvatar({ class: context.uiOverrides.value?.leadingAvatar })\"\r\n >\r\n <text\r\n :class=\"context.ui.value.leadingAvatarSize({ class: context.uiOverrides.value?.leadingAvatarSize })\"\r\n >\r\n <slot name=\"leading-avatar\" />\r\n </text>\r\n </text>\r\n <view\r\n data-tab-label :class=\"context.ui.value.label({ class: context.uiOverrides.value?.label })\"\r\n class=\"relative inline-flex items-center justify-center\"\r\n >\r\n <!-- Hidden bold text to reserve space -->\r\n <text\r\n class=\"\r\n pointer-events-none invisible select-none overflow-hidden\r\n whitespace-pre font-bold opacity-0\r\n \"\r\n >\r\n <slot name=\"label\">\r\n {{ props.label }}\r\n <slot />\r\n </slot>\r\n </text>\r\n <!-- Visible text -->\r\n <text\r\n class=\"\r\n absolute left-1/2 top-1/2 z-10 -translate-x-1/2 -translate-y-1/2\r\n whitespace-pre\r\n \"\r\n >\r\n <slot name=\"label\">\r\n {{ props.label }}\r\n <slot />\r\n </slot>\r\n </text>\r\n </view>\r\n <text\r\n v-if=\"$slots['trailing-badge']\"\r\n :class=\"context.ui.value.trailingBadge({ class: context.uiOverrides.value?.trailingBadge })\"\r\n >\r\n <text\r\n :class=\"context.ui.value.trailingBadgeSize({ class: context.uiOverrides.value?.trailingBadgeSize })\"\r\n >\r\n <slot name=\"trailing-badge\" />\r\n </text>\r\n </text>\r\n </view>\r\n </view>\r\n</template>\r\n",
|
|
36
|
+
"target": "uniapp"
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
"path": "types.ts",
|
|
40
|
+
"content": "import type { ClassValue } from 'clsx'\r\nimport type { tabsOrientations, tabsSizes, tabsTypes, tabsVariants } from './reborn-tabs.config'\r\n\r\nexport interface TabsProps {\r\n active?: number\r\n defaultActive?: number\r\n type?: typeof tabsTypes[number]\r\n variant?: typeof tabsVariants[number]\r\n size?: typeof tabsSizes[number]\r\n orientation?: typeof tabsOrientations[number]\r\n sticky?: boolean\r\n swipeable?: boolean\r\n shrink?: boolean\r\n scrollspy?: boolean\r\n ignorePageScroll?: boolean\r\n activationMode?: 'automatic' | 'manual'\r\n customClass?: ClassValue\r\n ui?: Partial<{\r\n root: ClassValue\r\n list: ClassValue\r\n indicator: ClassValue\r\n trigger: ClassValue\r\n leadingIcon: ClassValue\r\n leadingAvatar: ClassValue\r\n leadingAvatarSize: ClassValue\r\n label: ClassValue\r\n trailingBadge: ClassValue\r\n trailingBadgeSize: ClassValue\r\n content: ClassValue\r\n }>\r\n}\r\n",
|
|
41
|
+
"target": "uniapp"
|
|
42
|
+
}
|
|
43
|
+
],
|
|
44
|
+
"fileCount": 7,
|
|
45
|
+
"contentHash": "a568c4c50c7e5a1dffa944de035dd04566fb436e"
|
|
46
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "reborn-tabs-test",
|
|
3
|
+
"dependencies": [
|
|
4
|
+
"@vueuse/core",
|
|
5
|
+
"clsx"
|
|
6
|
+
],
|
|
7
|
+
"files": [
|
|
8
|
+
{
|
|
9
|
+
"path": "index.ts",
|
|
10
|
+
"content": "export { default as TabsContent } from './TabsContent.vue'\r\nexport { default as TabsList } from './TabsList.vue'\r\nexport { default as TabsRoot } from './TabsRoot.vue'\r\nexport { default as TabsTrigger } from './TabsTrigger.vue'\r\n",
|
|
11
|
+
"target": "uniapp"
|
|
12
|
+
},
|
|
13
|
+
{
|
|
14
|
+
"path": "reborn-tabs.config.ts",
|
|
15
|
+
"content": "export const tabsTypes = ['line', 'card'] as const\r\nexport const tabsVariants = ['primary', 'info', 'success', 'warning', 'neutral'] as const\r\nexport const tabsSizes = ['sm', 'md', 'lg'] as const\r\nexport const tabsOrientations = ['horizontal', 'vertical'] as const\r\n\r\nexport default {\r\n slots: {\r\n root: 'flex flex-col gap-2 min-w-0',\r\n list: 'relative flex max-w-full box-border gap-2 [&::-webkit-scrollbar]:hidden [-ms-overflow-style:none] [scrollbar-width:none]',\r\n trigger:\r\n 'relative z-10 inline-flex items-center justify-center gap-2 whitespace-nowrap px-3 py-2 text-32 font-medium text-gray-7 ring-offset-background transition-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none data-[disabled=true]:pointer-events-none data-[disabled=true]:!text-gray-4 flex-1',\r\n leadingIcon: 'flex items-center text-28',\r\n leadingAvatar: 'flex items-center overflow-hidden rounded-full',\r\n leadingAvatarSize: 'h-6 w-6',\r\n label: 'relative z-10',\r\n trailingBadge: 'flex items-center rounded-full bg-gray-2 px-2 py-0.5 text-24 text-gray-7',\r\n trailingBadgeSize: 'text-24',\r\n content:\r\n 'mt-2 ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 scroll-mt-24',\r\n indicator:\r\n 'absolute bottom-1 left-0 h-1.5 w-0 rounded-full transition-all duration-300 ease-in-out z-0 opacity-90 -translate-y-1 pointer-events-none',\r\n },\r\n variants: {\r\n type: {\r\n line: {\r\n list: 'border-b border-gray-2',\r\n trigger: 'bg-transparent shadow-none rounded-none !text-gray-7 hover:text-gray-8 [&.active]:font-bold [&.active]:!text-gray-8',\r\n indicator: 'block',\r\n },\r\n card: {\r\n list: 'inline-flex items-center justify-start rounded-[var(--radius-ui-base)] bg-gray-2/70 p-1',\r\n trigger: 'rounded-[var(--radius-ui-sm)] px-3 py-2 shadow-none !text-gray-7 [&.active]:!text-white',\r\n indicator: 'hidden',\r\n },\r\n },\r\n variant: {\r\n primary: {\r\n trigger: '[&.active]:text-primary',\r\n indicator:\r\n 'bg-[linear-gradient(90deg,var(--color-red-6),var(--color-red-3),var(--color-blue-1),var(--color-orange-1))] bg-[length:200%]',\r\n },\r\n info: {\r\n trigger: '[&.active]:text-info',\r\n indicator: 'bg-info',\r\n },\r\n success: {\r\n trigger: '[&.active]:text-success',\r\n indicator: 'bg-success',\r\n },\r\n warning: {\r\n trigger: '[&.active]:text-warning',\r\n indicator: 'bg-warning',\r\n },\r\n neutral: {\r\n trigger: '[&.active]:text-gray-8',\r\n indicator: 'bg-gray-8',\r\n },\r\n },\r\n orientation: {\r\n horizontal: {\r\n root: 'flex-col w-full h-full min-w-0 max-w-full relative overflow-hidden',\r\n list: 'flex-row flex-nowrap w-full min-w-0 max-w-full overflow-x-auto overflow-y-hidden shrink-0',\r\n trigger: 'shrink-0',\r\n indicator:\r\n 'h-1.5 w-[var(--radix-tabs-indicator-width)] translate-x-[var(--radix-tabs-indicator-position)] bottom-1',\r\n content: 'flex-1 w-full min-h-0 overflow-y-auto',\r\n },\r\n vertical: {\r\n root: 'flex-row items-start gap-4 h-full [&>*:not([role=tablist])]:flex-1 [&>*:not([role=tablist])]:w-full [&>*:not([role=tablist])]:h-full relative overflow-hidden',\r\n list: 'flex-col w-auto h-full overflow-y-auto overflow-x-hidden border-b-0 border-r border-gray-2',\r\n trigger: 'flex-initial w-full justify-start border-b-0 border-r-0 rounded-none',\r\n indicator: 'hidden',\r\n content:\r\n 'flex-1 w-full h-full overflow-y-auto mt-0 [&::-webkit-scrollbar]:hidden [-ms-overflow-style:none] [scrollbar-width:none]',\r\n },\r\n },\r\n size: {\r\n sm: {\r\n trigger: 'text-28 px-2.5 py-1.5',\r\n leadingAvatarSize: 'h-5 w-5',\r\n trailingBadgeSize: 'text-20',\r\n },\r\n md: {\r\n trigger: 'text-32 px-3 py-2',\r\n leadingAvatarSize: 'h-6 w-6',\r\n trailingBadgeSize: 'text-22',\r\n },\r\n lg: {\r\n trigger: 'text-36 px-4 py-2.5',\r\n leadingAvatarSize: 'h-7 w-7',\r\n trailingBadgeSize: 'text-24',\r\n },\r\n },\r\n sticky: {\r\n true: {\r\n root: 'h-auto block overflow-visible',\r\n list: 'sticky top-0 z-40 bg-background/95 backdrop-blur',\r\n content: 'h-auto block overflow-visible',\r\n },\r\n },\r\n shrink: {\r\n true: {\r\n list: 'justify-start',\r\n trigger: 'flex-none',\r\n },\r\n },\r\n scrollspy: {\r\n true: {\r\n // Base scrollspy styles\r\n },\r\n },\r\n },\r\n compoundVariants: [\r\n {\r\n orientation: 'horizontal' as (typeof tabsOrientations)[number],\r\n scrollspy: true,\r\n class: {\r\n root: 'block h-auto overflow-visible relative',\r\n list: 'sticky top-0 z-10 bg-background shadow-sm',\r\n content: 'h-auto block',\r\n },\r\n },\r\n {\r\n orientation: 'vertical' as (typeof tabsOrientations)[number],\r\n scrollspy: true,\r\n class: {\r\n root: 'flex flex-row items-start gap-0 h-full min-h-0 overflow-hidden',\r\n list: 'h-full min-h-0 overflow-y-auto overflow-x-hidden border-r border-gray-2 shrink-0 w-auto',\r\n content: 'flex-1 min-w-0 w-full',\r\n },\r\n },\r\n {\r\n type: 'card' as (typeof tabsTypes)[number],\r\n variant: 'primary' as (typeof tabsVariants)[number],\r\n class: {\r\n trigger: '[&.active]:bg-primary [&.active]:text-white',\r\n },\r\n },\r\n {\r\n type: 'card' as (typeof tabsTypes)[number],\r\n variant: 'info' as (typeof tabsVariants)[number],\r\n class: {\r\n trigger: '[&.active]:bg-info [&.active]:text-white',\r\n },\r\n },\r\n {\r\n type: 'card' as (typeof tabsTypes)[number],\r\n variant: 'success' as (typeof tabsVariants)[number],\r\n class: {\r\n trigger: '[&.active]:bg-success [&.active]:text-white',\r\n },\r\n },\r\n {\r\n type: 'card' as (typeof tabsTypes)[number],\r\n variant: 'warning' as (typeof tabsVariants)[number],\r\n class: {\r\n trigger: '[&.active]:bg-warning [&.active]:text-white',\r\n },\r\n },\r\n {\r\n type: 'card' as (typeof tabsTypes)[number],\r\n variant: 'neutral' as (typeof tabsVariants)[number],\r\n class: {\r\n trigger: '[&.active]:bg-gray-8 [&.active]:text-white',\r\n },\r\n },\r\n {\r\n type: 'card' as (typeof tabsTypes)[number],\r\n orientation: 'vertical' as (typeof tabsOrientations)[number],\r\n class: {\r\n list: 'border-r-0',\r\n trigger: 'border-r-0',\r\n },\r\n },\r\n ],\r\n defaultVariants: {\r\n type: 'line' as (typeof tabsTypes)[number],\r\n variant: 'primary' as (typeof tabsVariants)[number],\r\n orientation: 'horizontal' as (typeof tabsOrientations)[number],\r\n size: 'md' as (typeof tabsSizes)[number],\r\n sticky: false,\r\n shrink: false,\r\n },\r\n}\r\n",
|
|
16
|
+
"target": "uniapp"
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
"path": "TabsContent.vue",
|
|
20
|
+
"content": "<script setup lang=\"ts\">\r\nimport { computed, getCurrentInstance, inject, nextTick, onBeforeUnmount, ref, watch } from 'vue'\r\nimport { cn } from '@/lib/utils'\r\n\r\nconst props = defineProps<{\r\n index?: number\r\n customClass?: any\r\n}>()\r\n\r\nconst context = inject('TabsContext') as any\r\nconst localIndex = ref<number>(context.registerContent(props.index))\r\nconst instance = getCurrentInstance()\r\n\r\n// Register ID for scroll handling (if page scroll logic is used)\r\nconst contentId = computed(() => `${context.rootId}-content-${localIndex.value}`)\r\nwatch(contentId, (id) => {\r\n if (localIndex.value !== undefined) {\r\n context.registerContentId(localIndex.value, id)\r\n }\r\n}, { immediate: true })\r\n\r\n// Direction logic\r\nconst transitionName = computed(() => {\r\n if (isScrollspy.value) { return undefined }\r\n\r\n const isVertical = context.orientation.value === 'vertical'\r\n const dir = context.direction.value\r\n\r\n if (isVertical) {\r\n return dir === 'next' ? 'tabs-slide-up' : 'tabs-slide-down'\r\n }\r\n return dir === 'next' ? 'tabs-slide-left' : 'tabs-slide-right'\r\n})\r\n\r\nconst isActive = computed(() => context.activeIndex.value === localIndex.value)\r\nconst isScrollspy = computed(() => context.scrollspy.value)\r\nlet observer: UniApp.IntersectionObserver | null = null\r\n\r\n// --- Swipe Logic ---\r\nlet startX = 0\r\nlet startY = 0\r\n\r\nfunction onTouchStart(e: any) {\r\n if (!context.swipeable.value) { return }\r\n if (e.changedTouches && e.changedTouches.length > 0) {\r\n startX = e.changedTouches[0].clientX\r\n startY = e.changedTouches[0].clientY\r\n }\r\n}\r\n\r\nfunction onTouchEnd(e: any) {\r\n if (!context.swipeable.value) { return }\r\n if (e.changedTouches && e.changedTouches.length > 0) {\r\n const endX = e.changedTouches[0].clientX\r\n const endY = e.changedTouches[0].clientY\r\n const dx = endX - startX\r\n const dy = endY - startY\r\n const threshold = 50\r\n\r\n const isHorizontal = context.orientation.value === 'horizontal'\r\n const isVertical = context.orientation.value === 'vertical'\r\n const maxIndex = context.contentCounter.value\r\n const currentIndex = context.activeIndex.value\r\n\r\n if (isHorizontal) {\r\n if (Math.abs(dx) > threshold && Math.abs(dy) < threshold) { // horizontal swipe\r\n if (dx < 0) {\r\n // Swipe Left -> Next\r\n if (currentIndex < maxIndex - 1) {\r\n const nextIndex = currentIndex + 1\r\n context.setActiveIndex(nextIndex)\r\n if (context.scrollspy.value) {\r\n context.scrollToContent(nextIndex)\r\n }\r\n }\r\n }\r\n else {\r\n // Swipe Right -> Prev\r\n if (currentIndex > 0) {\r\n const prevIndex = currentIndex - 1\r\n context.setActiveIndex(prevIndex)\r\n if (context.scrollspy.value) {\r\n context.scrollToContent(prevIndex)\r\n }\r\n }\r\n }\r\n }\r\n }\r\n else if (isVertical) {\r\n if (Math.abs(dy) > threshold && Math.abs(dx) < threshold) {\r\n if (dy < 0) {\r\n // Swipe Up -> Next\r\n if (currentIndex < maxIndex - 1) {\r\n const nextIndex = currentIndex + 1\r\n context.setActiveIndex(nextIndex)\r\n if (context.scrollspy.value) {\r\n context.scrollToContent(nextIndex)\r\n }\r\n }\r\n }\r\n else {\r\n // Swipe Down -> Prev\r\n if (currentIndex > 0) {\r\n const prevIndex = currentIndex - 1\r\n context.setActiveIndex(prevIndex)\r\n if (context.scrollspy.value) {\r\n context.scrollToContent(prevIndex)\r\n }\r\n }\r\n }\r\n }\r\n }\r\n }\r\n}\r\n\r\nwatch(\r\n isScrollspy,\r\n async (enabled) => {\r\n if (!enabled) {\r\n if (observer) {\r\n observer.disconnect()\r\n observer = null\r\n }\r\n return\r\n }\r\n\r\n await nextTick()\r\n\r\n // In UniApp, we create observer from the instance\r\n if (observer) { observer.disconnect() }\r\n\r\n observer = uni.createIntersectionObserver(instance)\r\n\r\n // We observe relative to viewport (or a scroll view if we knew it).\r\n // Since we don't know the scroll parent, observing relative to viewport is safest default for scrollspy.\r\n observer.relativeToViewport({ bottom: -100 }) // Adjust margins as needed\r\n observer.observe(`#${contentId.value}`, (res) => {\r\n if (res.intersectionRatio > 0) {\r\n // intersection\r\n // If we have multiple contents intersecting, the last one usually wins or the first one?\r\n // Simple logic: if intersecting, set active.\r\n // Debounce might be needed if scrolling fast.\r\n context.setActiveIndex(localIndex.value)\r\n }\r\n })\r\n },\r\n { immediate: true },\r\n)\r\n\r\nonBeforeUnmount(() => {\r\n if (observer) { observer.disconnect() }\r\n if (localIndex.value !== undefined) {\r\n context.unregisterContentId(localIndex.value)\r\n }\r\n})\r\n</script>\r\n\r\n<template>\r\n <view\r\n v-if=\"isScrollspy || isActive\" :id=\"contentId\" role=\"tabpanel\" :data-state=\"isActive ? 'active' : 'inactive'\"\r\n :data-index=\"localIndex\"\r\n :class=\"context.ui.value.content({ class: cn(props.customClass, context.uiOverrides.value?.content) })\"\r\n @touchstart=\"onTouchStart\" @touchend=\"onTouchEnd\"\r\n >\r\n <slot />\r\n </view>\r\n</template>\r\n",
|
|
21
|
+
"target": "uniapp"
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
"path": "TabsList.vue",
|
|
25
|
+
"content": "<script setup lang=\"ts\">\r\nimport { getCurrentInstance, inject, nextTick, onMounted, ref, watch } from 'vue'\r\n// import { useResizeObserver } from \"@vueuse/core\"; // Removed non-uniapp compatible observer\r\nimport { cn } from '@/lib/utils'\r\n\r\nconst props = defineProps<{\r\n customClass?: any\r\n}>()\r\n\r\nconst context = inject('TabsContext') as any\r\n\r\nconst instance = getCurrentInstance()\r\nconst indicatorStyle = ref({})\r\nconst scrollIntoId = ref('')\r\n\r\n// --- Indicator logic ---\r\nfunction updateIndicator() {\r\n const activeIdx = context.activeIndex.value\r\n const triggerId = `#${context.rootId}-trigger-${activeIdx}`\r\n const query = uni.createSelectorQuery().in(instance)\r\n query.select(`${triggerId} .rb-tabs__trigger-inner`).boundingClientRect()\r\n query.select(triggerId).boundingClientRect() // Fallback\r\n query.select(`#${context.rootId}-list`).boundingClientRect()\r\n query.select(`#${context.rootId}-list`).scrollOffset()\r\n\r\n query.exec((res) => {\r\n if (!res || res.length < 4) { return }\r\n\r\n const innerRect = res[0] as UniApp.NodeInfo\r\n const fallbackTriggerRect = res[1] as UniApp.NodeInfo\r\n const listRect = res[2] as UniApp.NodeInfo\r\n const scrollRes = res[3] as any\r\n\r\n if (!fallbackTriggerRect || !listRect) { return }\r\n const triggerRect = (innerRect && (innerRect.width ?? 0) > 0) ? innerRect : fallbackTriggerRect\r\n\r\n const isHorizontal = context.orientation.value === 'horizontal'\r\n const scrollLeft = scrollRes?.scrollLeft || 0\r\n const scrollTop = scrollRes?.scrollTop || 0\r\n\r\n if (isHorizontal) {\r\n const left = (triggerRect.left || 0) - (listRect.left || 0) + scrollLeft\r\n indicatorStyle.value = {\r\n '--radix-tabs-indicator-position': `${left}px`,\r\n '--radix-tabs-indicator-width': `${triggerRect.width}px`,\r\n 'opacity': 1,\r\n }\r\n }\r\n else {\r\n const top = (triggerRect.top || 0) - (listRect.top || 0) + scrollTop\r\n indicatorStyle.value = {\r\n '--radix-tabs-indicator-position': `${top}px`,\r\n '--radix-tabs-indicator-height': `${triggerRect.height}px`,\r\n 'opacity': 1,\r\n }\r\n }\r\n })\r\n}\r\n\r\n// --- Scrolling Logic ---\r\nfunction scrollToActiveTab() {\r\n const activeIdx = context.activeIndex.value\r\n const nextId = `${context.rootId}-trigger-${activeIdx}`\r\n if (scrollIntoId.value === nextId) {\r\n scrollIntoId.value = ''\r\n nextTick(() => {\r\n scrollIntoId.value = nextId\r\n })\r\n return\r\n }\r\n scrollIntoId.value = nextId\r\n}\r\n\r\n// Watchers\r\nwatch(() => context.activeIndex.value, async () => {\r\n await nextTick()\r\n setTimeout(() => {\r\n scrollToActiveTab()\r\n updateIndicator()\r\n }, 50)\r\n})\r\n\r\nonMounted(async () => {\r\n await nextTick()\r\n setTimeout(() => {\r\n scrollToActiveTab()\r\n updateIndicator()\r\n }, 100)\r\n})\r\n</script>\r\n\r\n<template>\r\n <scroll-view\r\n :id=\"`${context.rootId}-list`\" :scroll-x=\"context.orientation.value === 'horizontal'\"\r\n :scroll-y=\"context.orientation.value === 'vertical'\" :scroll-into-view=\"scrollIntoId\" scroll-with-animation\r\n role=\"tablist\" :enable-flex=\"true\"\r\n :data-sticky-tabs=\"context.sticky.value || context.scrollspy.value ? 'true' : undefined\"\r\n :class=\"context.ui.value.list({ class: cn(props.customClass, context.uiOverrides.value?.list) })\"\r\n :style=\"indicatorStyle\"\r\n >\r\n <slot />\r\n <slot\r\n name=\"indicator\" :style=\"indicatorStyle\"\r\n :class=\"context.ui.value.indicator({ class: context.uiOverrides.value?.indicator })\"\r\n >\r\n <view\r\n :class=\"context.ui.value.indicator({ class: context.uiOverrides.value?.indicator })\"\r\n aria-hidden=\"true\" :style=\"indicatorStyle\"\r\n />\r\n </slot>\r\n </scroll-view>\r\n</template>\r\n\r\n<style scoped>\r\n:deep(::-webkit-scrollbar) {\r\n display: none;\r\n width: 0;\r\n height: 0;\r\n}\r\n\r\n:deep(.uni-scroll-view-content) {\r\n display: flex;\r\n}\r\n</style>\r\n",
|
|
26
|
+
"target": "uniapp"
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
"path": "TabsRoot.vue",
|
|
30
|
+
"content": "<script setup lang=\"ts\">\r\nimport type { TabsProps } from './types'\r\nimport { useVModel } from '@vueuse/core'\r\nimport { computed, provide, ref, toRef } from 'vue'\r\nimport { tv } from '@/lib/tv'\r\nimport { cn } from '@/lib/utils'\r\n\r\nimport theme from './reborn-tabs.config'\r\n\r\nexport type { TabsProps }\r\n\r\nconst props = withDefaults(defineProps<TabsProps>(), {\r\n defaultActive: 0,\r\n type: 'line',\r\n variant: 'primary',\r\n size: 'md',\r\n orientation: 'horizontal',\r\n sticky: false,\r\n swipeable: false,\r\n shrink: false,\r\n scrollspy: false,\r\n activationMode: 'automatic',\r\n ignorePageScroll: false,\r\n})\r\n\r\nconst emit = defineEmits<{\r\n (e: 'update:active', value: number): void\r\n (e: 'click-tab', value: number, event: any): void\r\n}>()\r\n\r\nconst activeIndex = useVModel(props, 'active', emit, {\r\n defaultValue: props.defaultActive,\r\n passive: true,\r\n})\r\n\r\nconst b = tv(theme)\r\nconst type = toRef(props, 'type')\r\nconst variant = toRef(props, 'variant')\r\nconst size = toRef(props, 'size')\r\nconst orientation = toRef(props, 'orientation')\r\nconst sticky = toRef(props, 'sticky')\r\nconst swipeable = toRef(props, 'swipeable')\r\nconst shrink = toRef(props, 'shrink')\r\nconst scrollspy = toRef(props, 'scrollspy')\r\nconst uiOverrides = computed(() => props.ui ?? {})\r\n\r\nconst ui = computed(() => b({\r\n type: type.value,\r\n variant: variant.value,\r\n size: size.value,\r\n orientation: orientation.value,\r\n sticky: sticky.value,\r\n shrink: shrink.value,\r\n scrollspy: scrollspy.value,\r\n}))\r\n\r\nconst triggerCounter = ref(0)\r\nconst contentCounter = ref(0)\r\n\r\n// Simple ID generator for coordination\r\nconst rootId = `tabs-${Math.random().toString(36).substring(2, 9)}`\r\n\r\nfunction registerTrigger(index?: number) {\r\n if (typeof index === 'number') {\r\n triggerCounter.value = Math.max(triggerCounter.value, index + 1)\r\n return index\r\n }\r\n const value = triggerCounter.value\r\n triggerCounter.value += 1\r\n return value\r\n}\r\n\r\nfunction registerContent(index?: number) {\r\n if (typeof index === 'number') {\r\n contentCounter.value = Math.max(contentCounter.value, index + 1)\r\n return index\r\n }\r\n const value = contentCounter.value\r\n contentCounter.value += 1\r\n return value\r\n}\r\n\r\n// Map of index -> unique ID string (needed for scroll-into-view or selector queries)\r\nconst contentIds = ref<Record<number, string>>({})\r\n\r\nfunction registerContentId(index: number, id: string) {\r\n contentIds.value[index] = id\r\n}\r\n\r\nfunction unregisterContentId(index: number) {\r\n delete contentIds.value[index]\r\n}\r\n\r\nconst currentScrollToId = ref('')\r\n\r\nfunction scrollToContent(index: number) {\r\n const id = contentIds.value[index]\r\n if (id) {\r\n currentScrollToId.value = id\r\n\r\n if (!props.ignorePageScroll) {\r\n // In UniApp, we can try to scroll the page to the content\r\n uni.pageScrollTo({\r\n selector: `#${id}`,\r\n duration: 300,\r\n fail: () => {\r\n // If ID not found or in a scroll-view, this might fail or do nothing.\r\n // We could emit an event for manual handling\r\n // console.warn(`[TabsRoot] Could not scroll to content #${id}. If content is inside a scroll-view, please handle scrolling manually.`);\r\n },\r\n })\r\n }\r\n }\r\n}\r\n\r\nconst direction = ref<'next' | 'prev'>('next')\r\n\r\nfunction setActiveIndex(value: number) {\r\n const currentDefault = activeIndex.value ?? 0\r\n if (value > currentDefault) {\r\n direction.value = 'next'\r\n }\r\n else if (value < currentDefault) {\r\n direction.value = 'prev'\r\n }\r\n activeIndex.value = value\r\n}\r\n\r\nfunction onTabClick(value: number, event: any) {\r\n if (props.activationMode === 'manual') {\r\n // Manual mode handled by Trigger\r\n }\r\n emit('click-tab', value, event)\r\n}\r\n\r\nprovide('TabsContext', {\r\n rootId,\r\n activeIndex,\r\n type,\r\n variant,\r\n size,\r\n orientation,\r\n swipeable,\r\n contentCounter,\r\n activationMode: toRef(props, 'activationMode'),\r\n sticky,\r\n scrollspy,\r\n registerTrigger,\r\n registerContent,\r\n registerContentId,\r\n unregisterContentId,\r\n scrollToContent,\r\n setActiveIndex,\r\n onTabClick,\r\n ui,\r\n uiOverrides,\r\n direction, // Provide direction to context\r\n})\r\n\r\ndefineExpose({\r\n activeIndex,\r\n})\r\n</script>\r\n\r\n<template>\r\n <view :class=\"cn(ui.root(), uiOverrides.root, props.customClass)\">\r\n <slot :ui=\"ui\" :ui-overrides=\"uiOverrides\" :current-scroll-to-id=\"currentScrollToId\" :root-id=\"rootId\" />\r\n </view>\r\n</template>\r\n",
|
|
31
|
+
"target": "uniapp"
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
"path": "TabsTrigger.vue",
|
|
35
|
+
"content": "<script setup lang=\"ts\">\r\nimport { computed, inject, ref, useSlots } from 'vue'\r\nimport { cn } from '@/lib/utils'\r\n\r\nconst props = defineProps<{\r\n index?: number\r\n disabled?: boolean\r\n label?: string\r\n customClass?: any\r\n}>()\r\n\r\nconst slots = useSlots()\r\nconst hasLabelSlot = computed(() => !!slots.label)\r\n\r\nconst context = inject('TabsContext') as any\r\nconst localIndex = ref<number>(context.registerTrigger(props.index))\r\n\r\n// Generate unique ID for this trigger to be selected by TabsList\r\nconst triggerId = computed(() => `${context.rootId}-trigger-${localIndex.value}`)\r\n\r\nconst isActive = computed(() => context.activeIndex.value === localIndex.value)\r\n\r\nfunction handleClick(event: any) {\r\n if (props.disabled) { return }\r\n // console.log('[TabsTrigger] Clicked:', localIndex.value, 'Current Active:', context.activeIndex.value);\r\n\r\n if (context.scrollspy.value) {\r\n context.scrollToContent(localIndex.value)\r\n // We still set active index, but scroll handling might trigger intersection observer\r\n // which sets active index again. That's fine.\r\n context.setActiveIndex(localIndex.value)\r\n }\r\n else {\r\n context.setActiveIndex(localIndex.value)\r\n }\r\n context.onTabClick(localIndex.value, event)\r\n}\r\n</script>\r\n\r\n<template>\r\n <view\r\n :id=\"triggerId\" role=\"tab\" :aria-selected=\"isActive\"\r\n :class=\"[context.ui.value.trigger({ class: cn(props.customClass, context.uiOverrides.value?.trigger) }), { active: isActive }]\"\r\n :data-state=\"isActive ? 'active' : 'inactive'\" :data-orientation=\"context.orientation.value\"\r\n :data-disabled=\"props.disabled ? 'true' : 'false'\" :data-index=\"localIndex\" @tap=\"handleClick\"\r\n >\r\n <view\r\n class=\"\r\n rb-tabs__trigger-inner inline-flex items-center justify-center gap-2\r\n \"\r\n >\r\n <text\r\n v-if=\"$slots['leading-icon']\"\r\n :class=\"context.ui.value.leadingIcon({ class: context.uiOverrides.value?.leadingIcon })\"\r\n >\r\n <slot name=\"leading-icon\" />\r\n </text>\r\n <text\r\n v-if=\"$slots['leading-avatar']\"\r\n :class=\"context.ui.value.leadingAvatar({ class: context.uiOverrides.value?.leadingAvatar })\"\r\n >\r\n <text\r\n :class=\"context.ui.value.leadingAvatarSize({ class: context.uiOverrides.value?.leadingAvatarSize })\"\r\n >\r\n <slot name=\"leading-avatar\" />\r\n </text>\r\n </text>\r\n <view\r\n data-tab-label :class=\"context.ui.value.label({ class: context.uiOverrides.value?.label })\"\r\n class=\"relative inline-flex items-center justify-center\"\r\n >\r\n <!-- Direct render for debugging MP click issues -->\r\n <text class=\"whitespace-pre\">\r\n <slot name=\"label\">\r\n {{ props.label }}\r\n <slot />\r\n </slot>\r\n </text>\r\n </view>\r\n <text\r\n v-if=\"$slots['trailing-badge']\"\r\n :class=\"context.ui.value.trailingBadge({ class: context.uiOverrides.value?.trailingBadge })\"\r\n >\r\n <text\r\n :class=\"context.ui.value.trailingBadgeSize({ class: context.uiOverrides.value?.trailingBadgeSize })\"\r\n >\r\n <slot name=\"trailing-badge\" />\r\n </text>\r\n </text>\r\n </view>\r\n </view>\r\n</template>\r\n",
|
|
36
|
+
"target": "uniapp"
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
"path": "types.ts",
|
|
40
|
+
"content": "import type { ClassValue } from 'clsx'\r\nimport type { tabsOrientations, tabsSizes, tabsTypes, tabsVariants } from './reborn-tabs.config'\r\n\r\nexport interface TabsProps {\r\n active?: number\r\n defaultActive?: number\r\n type?: typeof tabsTypes[number]\r\n variant?: typeof tabsVariants[number]\r\n size?: typeof tabsSizes[number]\r\n orientation?: typeof tabsOrientations[number]\r\n sticky?: boolean\r\n swipeable?: boolean\r\n shrink?: boolean\r\n scrollspy?: boolean\r\n ignorePageScroll?: boolean\r\n activationMode?: 'automatic' | 'manual'\r\n customClass?: ClassValue\r\n ui?: Partial<{\r\n root: ClassValue\r\n list: ClassValue\r\n indicator: ClassValue\r\n trigger: ClassValue\r\n leadingIcon: ClassValue\r\n leadingAvatar: ClassValue\r\n leadingAvatarSize: ClassValue\r\n label: ClassValue\r\n trailingBadge: ClassValue\r\n trailingBadgeSize: ClassValue\r\n content: ClassValue\r\n }>\r\n}\r\n",
|
|
41
|
+
"target": "uniapp"
|
|
42
|
+
}
|
|
43
|
+
],
|
|
44
|
+
"fileCount": 7,
|
|
45
|
+
"contentHash": "361cd898f152f4956c05770297f06f794d38e0b6"
|
|
46
|
+
}
|