reborn-ui 0.1.76 → 0.1.78

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (50) hide show
  1. package/dist/index.js +182 -240
  2. package/dist/index.js.map +1 -1
  3. package/package.json +53 -53
  4. package/registry/components/reborn-affix.json +8 -3
  5. package/registry/components/reborn-back-top.json +9 -4
  6. package/registry/components/reborn-badge.json +11 -5
  7. package/registry/components/reborn-button.json +5 -5
  8. package/registry/components/reborn-card.json +18 -0
  9. package/registry/components/reborn-cascader.json +18 -0
  10. package/registry/components/reborn-checkbox.json +4 -4
  11. package/registry/components/reborn-chip.json +11 -5
  12. package/registry/components/reborn-collapse.json +11 -5
  13. package/registry/components/reborn-color-picker.json +50 -0
  14. package/registry/components/reborn-draggable.json +32 -0
  15. package/registry/components/reborn-drawer.json +17 -0
  16. package/registry/components/reborn-dropdown-select.json +18 -0
  17. package/registry/components/reborn-footer.json +40 -0
  18. package/registry/components/reborn-form.json +11 -6
  19. package/registry/components/reborn-image.json +10 -5
  20. package/registry/components/reborn-input-number.json +12 -6
  21. package/registry/components/reborn-input-otp.json +40 -0
  22. package/registry/components/reborn-input.json +4 -4
  23. package/registry/components/reborn-loading.json +23 -0
  24. package/registry/components/reborn-loadmore.json +23 -0
  25. package/registry/components/reborn-overlay.json +38 -0
  26. package/registry/components/reborn-page.json +18 -0
  27. package/registry/components/reborn-picker-view.json +26 -0
  28. package/registry/components/reborn-popover.json +58 -0
  29. package/registry/components/reborn-popup.json +23 -0
  30. package/registry/components/reborn-qrcode.json +45 -0
  31. package/registry/components/reborn-radio.json +45 -0
  32. package/registry/components/reborn-rate.json +40 -0
  33. package/registry/components/reborn-root-portal.json +26 -0
  34. package/registry/components/reborn-select-date.json +40 -0
  35. package/registry/components/reborn-select-trigger.json +25 -0
  36. package/registry/components/reborn-select.json +41 -0
  37. package/registry/components/reborn-slider.json +40 -0
  38. package/registry/components/reborn-sticky.json +12 -6
  39. package/registry/components/reborn-switch.json +13 -7
  40. package/registry/components/reborn-tabbar.json +38 -0
  41. package/registry/components/reborn-tabs copy.json +46 -0
  42. package/registry/components/reborn-tabs-test.json +46 -0
  43. package/registry/components/reborn-tabs.json +12 -6
  44. package/registry/components/reborn-text.json +34 -0
  45. package/registry/components/reborn-textarea.json +5 -5
  46. package/registry/components/reborn-toast.json +38 -0
  47. package/registry/components/reborn-transition.json +38 -0
  48. package/registry/components/reborn-waterfall.json +18 -0
  49. package/registry/components/scroll-island.json +2 -2
  50. package/registry/registry.json +1101 -97
@@ -0,0 +1,40 @@
1
+ {
2
+ "name": "reborn-footer",
3
+ "dependencies": [
4
+ "clsx"
5
+ ],
6
+ "files": [
7
+ {
8
+ "path": "index.ts",
9
+ "content": "export { default as RebornFooter } from './RebornFooter.vue'\r\n",
10
+ "target": "web"
11
+ },
12
+ {
13
+ "path": "RebornFooter.vue",
14
+ "content": "<template>\r\n <div class=\"rounded-lg border border-dashed border-gray-300 p-4 text-sm text-gray-500 dark:border-gray-700 dark:text-gray-300\">\r\n web端暂未开发\r\n </div>\r\n</template>\r\n",
15
+ "target": "web"
16
+ },
17
+ {
18
+ "path": "index.ts",
19
+ "content": "export { rebornFooterOffset } from './offset'\r\nexport { default as RebornFooter } from './RebornFooter.vue'\r\n",
20
+ "target": "uniapp"
21
+ },
22
+ {
23
+ "path": "offset.ts",
24
+ "content": "import { ref } from 'vue'\r\n\r\nconst height = ref(0)\r\n\r\nexport const rebornFooterOffset = {\r\n height,\r\n set(val: number) {\r\n height.value = val\r\n },\r\n}\r\n",
25
+ "target": "uniapp"
26
+ },
27
+ {
28
+ "path": "reborn-footer.config.ts",
29
+ "content": "export default {\r\n slots: {\r\n placeholder: 'w-full',\r\n wrapper: 'fixed bottom-0 left-0 z-70 w-full overflow-visible',\r\n base: 'overflow-visible pb-[env(safe-area-inset-bottom)] bg-white dark:bg-gray-8',\r\n content: 'overflow-visible px-3 py-3',\r\n },\r\n} as const\r\n",
30
+ "target": "uniapp"
31
+ },
32
+ {
33
+ "path": "RebornFooter.vue",
34
+ "content": "<script setup lang=\"ts\">\r\nimport type { ClassValue } from 'clsx'\r\nimport { computed, getCurrentInstance, nextTick, onMounted, ref, watch } from 'vue'\r\nimport { isHarmony } from '@/lib/device'\r\nimport { tv } from '@/lib/tv'\r\nimport { cn } from '@/lib/utils'\r\nimport { rebornFooterOffset } from './offset'\r\nimport theme from './reborn-footer.config'\r\n\r\nexport interface RebornFooterProps {\r\n // 最小高度,小于该高度时,不显示\r\n minHeight?: number\r\n // 监听值,触发更新\r\n vt?: number\r\n // 内容高度\r\n height?: number | null\r\n ui?: {\r\n placeholder?: ClassValue\r\n wrapper?: ClassValue\r\n base?: ClassValue\r\n content?: ClassValue\r\n }\r\n}\r\n\r\ndefineOptions({\r\n name: 'RebornFooter',\r\n})\r\nconst props = withDefaults(defineProps<RebornFooterProps>(), {\r\n minHeight: 30,\r\n vt: 0,\r\n height: null,\r\n})\r\n\r\nconst { proxy } = getCurrentInstance()!\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\r\n return {\r\n placeholder: (opts?: { class?: any }) => styles.placeholder({ class: cn(opts?.class, uiOverrides.value.placeholder) }),\r\n wrapper: (opts?: { class?: any }) => styles.wrapper({ class: cn(opts?.class, uiOverrides.value.wrapper) }),\r\n base: (opts?: { class?: any }) => styles.base({ class: cn(opts?.class, uiOverrides.value.base) }),\r\n content: (opts?: { class?: any }) => styles.content({ class: cn(opts?.class, uiOverrides.value.content) }),\r\n }\r\n})\r\n\r\nconst placeholderHeight = ref(0)\r\nconst visible = ref(true)\r\n\r\nconst contentStyle = computed(() => {\r\n const style: Record<string, string> = {}\r\n\r\n if (props.height != null) {\r\n style.height = `${props.height}px`\r\n }\r\n\r\n return style\r\n})\r\n\r\nfunction getSafeAreaHeight(type: 'top' | 'bottom') {\r\n const { safeAreaInsets } = uni.getWindowInfo()\r\n\r\n if (type === 'top') {\r\n return safeAreaInsets.top\r\n }\r\n\r\n let h = safeAreaInsets.bottom\r\n\r\n // #ifdef APP-ANDROID\r\n if (h === 0) {\r\n h = 16\r\n }\r\n // #endif\r\n\r\n return h\r\n}\r\n\r\nfunction setHeight(val: number) {\r\n placeholderHeight.value = val + 5\r\n visible.value = val > props.minHeight + getSafeAreaHeight('bottom')\r\n rebornFooterOffset.set(visible.value ? val : 0)\r\n}\r\n\r\nfunction getHeight() {\r\n if (props.height != null) {\r\n setHeight(props.height + getSafeAreaHeight('bottom'))\r\n return\r\n }\r\n\r\n nextTick(() => {\r\n setTimeout(() => {\r\n uni.createSelectorQuery()\r\n .in(proxy)\r\n .select('#reborn-footer-node')\r\n .boundingClientRect((res) => {\r\n setHeight(Math.floor((res as NodeInfo).height ?? 0))\r\n })\r\n .exec()\r\n }, isHarmony() ? 50 : 0)\r\n })\r\n}\r\n\r\nonMounted(() => {\r\n watch(\r\n () => props.vt,\r\n () => {\r\n visible.value = true\r\n getHeight()\r\n },\r\n { immediate: true },\r\n )\r\n})\r\n</script>\r\n\r\n<template>\r\n <view v-if=\"visible\" :class=\"ui.placeholder()\" :style=\"{ height: `${placeholderHeight}px` }\" />\r\n\r\n <view :class=\"ui.wrapper()\">\r\n <view v-if=\"visible\" id=\"reborn-footer-node\" :class=\"ui.base()\">\r\n <view :class=\"ui.content()\" :style=\"contentStyle\">\r\n <slot />\r\n </view>\r\n </view>\r\n </view>\r\n</template>\r\n",
35
+ "target": "uniapp"
36
+ }
37
+ ],
38
+ "fileCount": 6,
39
+ "contentHash": "0e9b2239ccef32f01c9938b10111927987488fe7"
40
+ }
@@ -26,27 +26,32 @@
26
26
  "content": "<template>\r\n <div class=\"mb-4\">\r\n RebornFormItem (Web) - Under Development\r\n <slot />\r\n </div>\r\n</template>\r\n\r\n<script setup lang=\"ts\">\r\ndefineOptions({\r\n name: \"RebornFormItem\"\r\n})\r\n</script>\r\n",
27
27
  "target": "web"
28
28
  },
29
+ {
30
+ "path": "index.ts",
31
+ "content": "export { default as RebornForm } from './RebornForm.vue'\r\nexport { default as RebornFormItem } from './RebornFormItem.vue'\r\n",
32
+ "target": "uniapp"
33
+ },
29
34
  {
30
35
  "path": "reborn-form-item.config.ts",
31
- "content": "const size = [\"sm\", \"md\", \"lg\"] as const;\r\nconst labelPositions = [\"left\", \"top\", \"right\"] as const;\r\nexport default {\r\n slots: {\r\n root: \"flex gap-2 mb-4\",\r\n wrapper: \"flex-1\",\r\n label: \"text-gray-8 dark:text-gray-1 font-medium flex items-center shrink-0\",\r\n content: \"relative w-full flex-1\",\r\n error: \"text-xs text-red-500 mt-1 animate-in slide-in-from-top-1 fade-in duration-200\"\r\n },\r\n variants: {\r\n size: {\r\n sm: {\r\n label: \"text-26\",\r\n },\r\n md: {\r\n label: \"text-28\",\r\n },\r\n lg: {\r\n label: \"text-30\",\r\n },\r\n },\r\n error: {\r\n true: {\r\n root: \"re-form-item--error\"\r\n }\r\n },\r\n labelPosition: {\r\n left: {\r\n root: \"flex-row\",\r\n label: \"justify-start text-left\",\r\n },\r\n right: {\r\n root: \"flex-row\",\r\n label: \"justify-end text-right\",\r\n },\r\n top: {\r\n root: \"flex-col items-stretch\",\r\n label: \"justify-start text-left w-full\",\r\n }\r\n }\r\n },\r\n defaultVariants: {\r\n size: \"sm\",\r\n error: false,\r\n labelPosition: \"left\"\r\n }\r\n} as const;\r\n\r\nexport { labelPositions as formItemLabelPositions };",
36
+ "content": "const size = ['sm', 'md', 'lg'] as const\r\nconst labelPositions = ['left', 'top', 'right'] as const\r\nexport default {\r\n slots: {\r\n root: 'flex gap-2 mb-4',\r\n wrapper: 'flex-1',\r\n label: 'text-gray-8 dark:text-gray-1 font-medium flex items-center shrink-0',\r\n content: 'relative w-full flex-1',\r\n error: 'text-xs text-red-500 mt-1 animate-in slide-in-from-top-1 fade-in duration-200',\r\n },\r\n variants: {\r\n size: {\r\n sm: {\r\n label: 'text-26',\r\n },\r\n md: {\r\n label: 'text-28',\r\n },\r\n lg: {\r\n label: 'text-30',\r\n },\r\n },\r\n error: {\r\n true: {\r\n root: 're-form-item--error',\r\n },\r\n },\r\n labelPosition: {\r\n left: {\r\n root: 'flex-row',\r\n label: 'justify-start text-left',\r\n },\r\n right: {\r\n root: 'flex-row',\r\n label: 'justify-end text-right',\r\n },\r\n top: {\r\n root: 'flex-col items-stretch',\r\n label: 'justify-start text-left w-full',\r\n },\r\n },\r\n },\r\n defaultVariants: {\r\n size: 'sm',\r\n error: false,\r\n labelPosition: 'left',\r\n },\r\n} as const\r\n\r\nexport { size as formItemLabelSize, labelPositions as formItemLabelPositions }\r\n",
32
37
  "target": "uniapp"
33
38
  },
34
39
  {
35
40
  "path": "reborn-form.config.ts",
36
- "content": "const labelPositions = [\"left\", \"top\", \"right\"] as const;\r\n\r\nexport { labelPositions as formLabelPositions };\r\n\r\nexport default {\r\n slots: {\r\n root: \"w-full\",\r\n },\r\n};",
41
+ "content": "const labelPositions = ['left', 'top', 'right'] as const\r\n\r\nexport { labelPositions as formLabelPositions }\r\n\r\nexport default {\r\n slots: {\r\n root: 'w-full',\r\n },\r\n}\r\n",
37
42
  "target": "uniapp"
38
43
  },
39
44
  {
40
45
  "path": "RebornForm.vue",
41
- "content": "<script setup lang=\"ts\">\r\nimport type { ClassValue } from \"clsx\";\r\nimport { computed, provide, ref, watch, onMounted } from \"vue\";\r\nimport * as z from 'zod'\r\nimport { tv } from \"@/lib/tv\";\r\nimport { cn } from '@/lib/utils';\r\nimport theme, { formLabelPositions } from \"./reborn-form.config\";\r\nimport { useFieldGroup, type FormValidateError } from '@/composables/useFieldGroup';\r\n\r\nexport type { FormValidateError };\r\n\r\nexport interface FormRule {\r\n\trequired?: boolean;\r\n\tmessage?: string;\r\n\tvalidator: (value: any) => boolean | string;\r\n\ttrigger?: string;\r\n}\r\n\r\nexport interface FromProps {\r\n\tcustomClass?: ClassValue;\r\n\tmodelValue: any;\r\n\trules?: z.ZodObject<{ [key: string]: any }, any>;\r\n\tlabelPosition?: typeof formLabelPositions[number]; // 标签位置\r\n\tlabelWidth?: string | number; // 标签宽度\r\n\thideRequiredAsterisk?: boolean; // 是否隐藏必填符号\r\n\trequireAsteriskPosition?: \"left\" | \"right\"; // 必填符号位置\r\n\tshowMessage?: boolean; // 是否显示错误信息\r\n\tinlineMessage?: boolean; // 是否内联显示错误信息\r\n\tstatusIcon?: boolean; // 是否在输入框中显示校验结果反馈图标\r\n\tvalidateOnRuleChange?: boolean; // 是否在规则改变时重新验证\r\n\tsize?: \"\" | \"sm\" | \"md\" | \"lg\"; // 表单大小\r\n\tdisabled?: boolean; // 是否禁用\r\n\tscrollToError?: boolean; // 是否滚动到错误信息\r\n\ttrigger?: 'blur' | 'change'; // 触发验证\r\n\tui?: Partial<{\r\n\t\troot: ClassValue;\r\n\t}>;\r\n}\r\n\r\nconst props = withDefaults(defineProps<FromProps>(), {\r\n\tmodelValue: () => ({}),\r\n\tlabelPosition: \"left\",\r\n\tlabelWidth: \"140rpx\",\r\n\thideRequiredAsterisk: false,\r\n\trequireAsteriskPosition: \"left\",\r\n\tshowMessage: true,\r\n\tinlineMessage: false,\r\n\tstatusIcon: false,\r\n\tvalidateOnRuleChange: true,\r\n\tsize: \"\",\r\n\tdisabled: false,\r\n\tscrollToError: true,\r\n\ttrigger: 'blur',\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\tconst styles = b();\r\n\treturn {\r\n\t\troot: (opts?: { class?: any }) => styles.root({ class: cn(opts?.class, uiOverrides.value.root) }),\r\n\t};\r\n});\r\n\r\n// 使用 Composable获取状态管理能力\r\nconst {\r\n\tfields,\r\n\tfieldInstances,\r\n\terrors,\r\n\taddField,\r\n\tremoveField,\r\n\tsetError,\r\n\tremoveError,\r\n\tgetError,\r\n\tgetErrors,\r\n\tclearValidate\r\n} = useFieldGroup();\r\n\r\nconsole.log('RebornForm: setup called');\r\n// --- Business Logic ---\r\n\r\nconst initialModel = ref<any>({});\r\nconst data = ref({} as any);\r\n\r\nfunction parseToObject<T>(val: T) {\r\n\treturn JSON.parse(JSON.stringify(val || {}));\r\n}\r\n\r\n// 设置初始值\r\nfunction setInitialValues(values: any) {\r\n\tinitialModel.value = parseToObject(values);\r\n}\r\n\r\n// 验证单个字段\r\nasync function validateField(prop: string): Promise<string | null> {\r\n\tlet error = null as string | null;\r\n\r\n\tif (prop != \"\") {\r\n\t\t// Zod check\r\n\t\tconst parts = prop.split('-');\r\n\r\n\t\t// Nested logic: contacts-0-name\r\n\t\tif (parts.length >= 3 && !isNaN(Number(parts[1]))) {\r\n\t\t\tconst [key, indexStr, fieldName] = parts;\r\n\t\t\tconst index = Number(indexStr);\r\n\r\n\t\t\tif (fieldName && props.rules && props.rules.shape[key]) {\r\n\t\t\t\tconst itemSchema = props.rules.shape[key].element;\r\n\t\t\t\tconst schema = itemSchema?.pick({ [fieldName]: true });\r\n\r\n\t\t\t\tconst list = data.value[key];\r\n\t\t\t\tif (Array.isArray(list) && list[index]) {\r\n\t\t\t\t\tconst result = await schema.safeParseAsync(list[index]);\r\n\t\t\t\t\tif (!result.success) {\r\n\t\t\t\t\t\tconst issue = result.error.issues.find((i: any) => i.path.length === 1 && i.path[0] === fieldName);\r\n\t\t\t\t\t\tif (issue) error = issue.message;\r\n\t\t\t\t\t}\r\n\t\t\t\t}\r\n\t\t\t}\r\n\t\t} else {\r\n\t\t\tif (props.rules) {\r\n\t\t\t\tconst schema = props.rules.pick({ [prop]: true });\r\n\t\t\t\tif (schema) {\r\n\t\t\t\t\tconst result = await schema.safeParseAsync(data.value);\r\n\t\t\t\t\tif (!result.success) {\r\n\t\t\t\t\t\tconst issue = result.error.issues.find((i: any) => i.path.length === 1 && i.path[0] === prop);\r\n\t\t\t\t\t\tif (issue) error = issue.message;\r\n\t\t\t\t\t}\r\n\t\t\t\t}\r\n\t\t\t}\r\n\t\t}\r\n\r\n\t\tremoveError(prop);\r\n\t}\r\n\r\n\tif (error != null) {\r\n\t\tsetError(prop, error!);\r\n\t}\r\n\r\n\treturn error;\r\n}\r\n\r\n// 滚动到字段\r\nfunction scrollToField(prop: string) {\r\n\tconst field = fieldInstances.value.find(f => f.prop === prop);\r\n\tif (field && field.getBoundingClientRect) {\r\n\t\tfield.getBoundingClientRect((res: any) => {\r\n\t\t\tif (res) {\r\n\t\t\t\tuni.pageScrollTo({\r\n\t\t\t\t\tscrollTop: res.top + (fields.value.size > 0 ? 0 : 0) + uni.getSystemInfoSync().windowTop - 44,\r\n\t\t\t\t\tduration: 300\r\n\t\t\t\t});\r\n\t\t\t}\r\n\t\t});\r\n\t}\r\n}\r\n\r\n// 验证整个表单\r\nfunction validate(callback?: (valid: boolean, errors: FormValidateError[]) => void): Promise<boolean> {\r\n\treturn new Promise(async (resolve) => {\r\n\t\tconsole.log(fields.value);\r\n\t\tconst promises = Array.from(fields.value).map(prop => validateField(prop));\r\n\t\tawait Promise.all(promises);\r\n\r\n\t\tconst currentErrors = await getErrors();\r\n\r\n\t\tif (currentErrors.length > 0 && props.scrollToError) {\r\n\t\t\tconst errorInstances = fieldInstances.value.filter(f => errors.value.has(f.prop));\r\n\t\t\tif (errorInstances.length > 0) {\r\n\t\t\t\tscrollToField(errorInstances[0].prop);\r\n\t\t\t}\r\n\t\t}\r\n\r\n\t\tconst isValid = currentErrors.length === 0;\r\n\t\tif (callback) callback(isValid, currentErrors);\r\n\t\tresolve(isValid);\r\n\t});\r\n}\r\n\r\n// 重置表单\r\nfunction resetFields() {\r\n\tif (!props.modelValue) return;\r\n\tclearValidate();\r\n\tconst initData = initialModel.value;\r\n\r\n\tObject.keys(props.modelValue).forEach(key => {\r\n\t\tif (key in initData) {\r\n\t\t\tprops.modelValue[key] = initData[key];\r\n\t\t}\r\n\t});\r\n}\r\n\r\n// 监听数据变化\r\nwatch(() => props.modelValue, (val) => {\r\n\tdata.value = val;\r\n}, { immediate: true, deep: true });\r\n\r\nonMounted(() => {\r\n\tif (props.modelValue) {\r\n\t\tsetInitialValues(props.modelValue);\r\n\t}\r\n});\r\n\r\n// 提供 Context 给子组件 (包括 Props)\r\nprovide('rebornForm', {\r\n\tprops,\r\n\taddField: (f: any) => {\r\n\t\tconsole.log('RebornForm: addField received', f.prop);\r\n\t\taddField(f);\r\n\t},\r\n\tremoveField,\r\n\tgetError\r\n});\r\n\r\nfunction getField(prop: string) {\r\n\treturn fieldInstances.value.find(f => f.prop === prop);\r\n}\r\n\r\ndefineExpose({\r\n\tvalidate,\r\n\tvalidateField,\r\n\tclearValidate,\r\n\tresetFields,\r\n\tscrollToField,\r\n\tfields,\r\n\tgetField,\r\n\tsetInitialValues\r\n})\r\n</script>\r\n<template>\r\n\t<view :class=\"ui.root({ class: props.customClass })\">\r\n\t\t<slot></slot>\r\n\t</view>\r\n</template>",
46
+ "content": "<script setup lang=\"ts\">\r\nimport type { ClassValue } from 'clsx'\r\nimport type * as z from 'zod'\r\nimport type { formLabelPositions } from './reborn-form.config'\r\nimport type { FormValidateError } from '@/composables/useFieldGroup'\r\nimport { computed, onMounted, provide, ref, watch } from 'vue'\r\nimport { useFieldGroup } from '@/composables/useFieldGroup'\r\nimport { tv } from '@/lib/tv'\r\nimport { cn } from '@/lib/utils'\r\nimport theme from './reborn-form.config'\r\n\r\nexport type { FormValidateError }\r\n\r\nexport interface FormRule {\r\n required?: boolean\r\n message?: string\r\n validator: (value: any) => boolean | string\r\n trigger?: string\r\n}\r\n\r\nexport interface FromProps {\r\n customClass?: ClassValue\r\n modelValue: any\r\n rules?: z.ZodObject<{ [key: string]: any }, any>\r\n labelPosition?: typeof formLabelPositions[number] // 标签位置\r\n labelWidth?: string | number // 标签宽度\r\n hideRequiredAsterisk?: boolean // 是否隐藏必填符号\r\n requireAsteriskPosition?: 'left' | 'right' // 必填符号位置\r\n showMessage?: boolean // 是否显示错误信息\r\n inlineMessage?: boolean // 是否内联显示错误信息\r\n statusIcon?: boolean // 是否在输入框中显示校验结果反馈图标\r\n validateOnRuleChange?: boolean // 是否在规则改变时重新验证\r\n size?: '' | 'sm' | 'md' | 'lg' // 表单大小\r\n disabled?: boolean // 是否禁用\r\n scrollToError?: boolean // 是否滚动到错误信息\r\n trigger?: 'blur' | 'change' | 'none' | Array<'blur' | 'change'> // 触发验证\r\n ui?: Partial<{\r\n root: ClassValue\r\n }>\r\n}\r\n\r\nconst props = withDefaults(defineProps<FromProps>(), {\r\n modelValue: () => ({}),\r\n labelPosition: 'left',\r\n labelWidth: '140rpx',\r\n hideRequiredAsterisk: false,\r\n requireAsteriskPosition: 'left',\r\n showMessage: true,\r\n inlineMessage: false,\r\n statusIcon: false,\r\n validateOnRuleChange: true,\r\n size: '',\r\n disabled: false,\r\n scrollToError: true,\r\n trigger: 'none',\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 return {\r\n root: (opts?: { class?: any }) => styles.root({ class: cn(opts?.class, uiOverrides.value.root) }),\r\n }\r\n})\r\n\r\n// 使用 Composable获取状态管理能力\r\nconst {\r\n fields,\r\n fieldInstances,\r\n errors,\r\n addField,\r\n removeField,\r\n setError,\r\n removeError,\r\n getError,\r\n getErrors,\r\n clearValidate,\r\n} = useFieldGroup()\r\n\r\nconst initialModel = ref<any>({})\r\nconst data = ref({} as any)// 滚动距离\r\nconst scrollTop = ref(0)\r\n\r\n// 滚动到指定位置\r\nfunction scrollTo(top: number) {\r\n // #ifdef H5\r\n window.scrollTo({ top, behavior: 'smooth' })\r\n // #endif\r\n\r\n // #ifdef MP\r\n uni.pageScrollTo({\r\n scrollTop: top,\r\n duration: 300,\r\n })\r\n // #endif\r\n\r\n // #ifdef APP\r\n uni.pageScrollTo({\r\n scrollTop: top,\r\n duration: 0,\r\n })\r\n // #endif\r\n}\r\n\r\n// 回到顶部\r\nfunction scrollToTop() {\r\n scrollTo(0 + Math.random() / 1000)\r\n}\r\n\r\nfunction parseToObject<T>(val: T) {\r\n return JSON.parse(JSON.stringify(val || {}))\r\n}\r\n\r\n// 设置初始值\r\nfunction setInitialValues(values: any) {\r\n initialModel.value = parseToObject(values)\r\n}\r\n\r\n// 验证单个字段\r\nasync function validateField(prop: string): Promise<string | null> {\r\n let error = null as string | null\r\n\r\n if (prop != '') {\r\n // Zod check\r\n const parts = prop.split('-')\r\n\r\n // Nested logic: contacts-0-name\r\n if (parts.length >= 3 && !isNaN(Number(parts[1]))) {\r\n const [key, indexStr, fieldName] = parts\r\n const index = Number(indexStr)\r\n\r\n if (fieldName && props.rules && props.rules.shape[key]) {\r\n const itemSchema = props.rules.shape[key].element\r\n const schema = itemSchema?.pick({ [fieldName]: true })\r\n\r\n const list = data.value[key]\r\n if (Array.isArray(list) && list[index]) {\r\n const result = await schema.safeParseAsync(list[index])\r\n if (!result.success) {\r\n const issue = result.error.issues.find((i: any) => i.path.length === 1 && i.path[0] === fieldName)\r\n if (issue) { error = issue.message }\r\n }\r\n }\r\n }\r\n }\r\n else {\r\n if (props.rules) {\r\n const schema = props.rules.pick({ [prop]: true })\r\n if (schema) {\r\n const result = await schema.safeParseAsync(data.value)\r\n if (!result.success) {\r\n const issue = result.error.issues.find((i: any) => i.path.length === 1 && i.path[0] === prop)\r\n if (issue) { error = issue.message }\r\n }\r\n }\r\n }\r\n }\r\n\r\n removeError(prop)\r\n }\r\n\r\n if (error != null) {\r\n setError(prop, error!)\r\n }\r\n\r\n return error\r\n}\r\n\r\n// 滚动到字段\r\nfunction scrollToField(prop: string) {\r\n const field = fieldInstances.value.find(f => f.prop === prop)\r\n if (field && field.getBoundingClientRect) {\r\n field.getBoundingClientRect((res: any) => {\r\n if (res) {\r\n scrollTo(res.top + (fields.value.size > 0 ? 0 : 0) + scrollTop.value)\r\n }\r\n })\r\n }\r\n}\r\n\r\n// 验证整个表单\r\nfunction validate(callback?: (valid: boolean, errors: FormValidateError[]) => void): Promise<boolean> {\r\n return new Promise(async (resolve) => {\r\n const promises = Array.from(fields.value).map(prop => validateField(prop))\r\n await Promise.all(promises)\r\n\r\n const currentErrors = await getErrors()\r\n\r\n if (currentErrors.length > 0 && props.scrollToError) {\r\n const errorInstances = fieldInstances.value.filter(f => errors.value[f.prop] !== undefined)\r\n if (errorInstances.length > 0) {\r\n scrollToField(errorInstances[0].prop)\r\n }\r\n }\r\n\r\n const isValid = currentErrors.length === 0\r\n if (callback) { callback(isValid, currentErrors) }\r\n resolve(isValid)\r\n })\r\n}\r\n\r\n// 重置表单\r\nfunction resetFields() {\r\n if (!props.modelValue) { return }\r\n clearValidate()\r\n const initData = initialModel.value\r\n\r\n Object.keys(props.modelValue).forEach((key) => {\r\n if (key in initData) {\r\n props.modelValue[key] = initData[key]\r\n }\r\n })\r\n}\r\n\r\n// 监听数据变化\r\nwatch(() => props.modelValue, (val) => {\r\n data.value = val\r\n}, { immediate: true, deep: true })\r\n\r\nonMounted(() => {\r\n if (props.modelValue) {\r\n setInitialValues(props.modelValue)\r\n }\r\n})\r\n\r\n// 提供 Context 给子组件 (包括 Props)\r\nprovide('rebornForm', {\r\n props,\r\n addField: (f: any) => {\r\n addField(f)\r\n },\r\n removeField,\r\n getError,\r\n validateField,\r\n})\r\n\r\nfunction getField(prop: string) {\r\n return fieldInstances.value.find(f => f.prop === prop)\r\n}\r\n\r\ndefineExpose({\r\n validate,\r\n validateField,\r\n clearValidate,\r\n resetFields,\r\n scrollToField,\r\n fields,\r\n getField,\r\n setInitialValues,\r\n})\r\n</script>\r\n\r\n<template>\r\n <view :class=\"ui.root({ class: props.customClass })\">\r\n <slot />\r\n </view>\r\n</template>\r\n",
42
47
  "target": "uniapp"
43
48
  },
44
49
  {
45
50
  "path": "RebornFormItem.vue",
46
- "content": "<template>\r\n <view class=\"re-form-item\" :class=\"ui.root({ class: customClass })\">\r\n <view v-if=\"label || $slots.label\" :class=\"ui.label()\" :style=\"labelStyle\">\r\n <slot name=\"label\">\r\n <text v-if=\"required && requireAsteriskPosition === 'left'\" class=\"text-red-500 mr-1\">*</text>\r\n {{ label }}\r\n <text v-if=\"required && requireAsteriskPosition === 'right'\" class=\"text-red-500 ml-1\">*</text>\r\n </slot>\r\n </view>\r\n\r\n <view :class=\"ui.wrapper()\">\r\n <view :class=\"ui.content()\">\r\n <slot></slot>\r\n </view>\r\n\r\n <view v-if=\"error\" :class=\"ui.error()\">\r\n {{ error }}\r\n </view>\r\n </view>\r\n </view>\r\n</template>\r\n\r\n<script setup lang=\"ts\">\r\nimport { computed, provide, type PropType } from 'vue';\r\nimport type { ClassValue } from \"clsx\";\r\nimport { tv } from \"@/lib/tv\";\r\nimport { cn } from \"@/lib/utils\";\r\nimport theme, { formItemLabelPositions } from \"./reborn-form-item.config\";\r\nimport { useFieldGroupItem } from '@/composables/useFieldGroup';\r\n\r\ndefineOptions({\r\n name: 're-form-item'\r\n});\r\n\r\nexport interface FormItemProps {\r\n prop?: string;\r\n label?: string;\r\n required?: boolean;\r\n requireAsteriskPosition?: 'left' | 'right'; // 标签位置\r\n labelWidth?: string | number; // 标签宽度\r\n labelPosition?: typeof formItemLabelPositions[number]; // 标签位置\r\n customClass?: ClassValue;\r\n ui?: Partial<{\r\n root: ClassValue;\r\n label: ClassValue;\r\n wrapper: ClassValue;\r\n content: ClassValue;\r\n error: ClassValue;\r\n }>;\r\n}\r\n\r\nconst props = withDefaults(defineProps<FormItemProps>(), {\r\n prop: \"\",\r\n label: \"\",\r\n customClass: \"\",\r\n required: false,\r\n requireAsteriskPosition: 'right',\r\n});\r\n\r\nconsole.log('RebornFormItem: props', props.prop, props);\r\n\r\nconst {\r\n error,\r\n labelPosition,\r\n labelWidth,\r\n size,\r\n getBoundingClientRect\r\n} = useFieldGroupItem({\r\n prop: props.prop,\r\n labelPosition: props.labelPosition,\r\n labelWidth: props.labelWidth\r\n});\r\n\r\nprovide('rebornFormItem', {\r\n isError: computed(() => !!error.value)\r\n});\r\n\r\nconst labelStyle = computed(() => {\r\n return {\r\n width: labelWidth.value\r\n };\r\n});\r\n\r\n// Configure Component Styles\r\nconst b = tv(theme);\r\nconst uiOverrides = computed(() => props.ui || {});\r\n\r\nconst ui = computed(() => {\r\n const styles = b({\r\n error: !!error.value,\r\n labelPosition: labelPosition.value as any, // Cast to match config variants\r\n size: size.value as any\r\n });\r\n return {\r\n root: (opts?: { class?: any }) => styles.root({ class: cn(opts?.class, uiOverrides.value.root) }),\r\n wrapper: (opts?: { class?: any }) => styles.wrapper({ class: cn(opts?.class, uiOverrides.value.wrapper) }),\r\n label: (opts?: { class?: any }) => styles.label({ class: cn(opts?.class, uiOverrides.value.label) }),\r\n content: (opts?: { class?: any }) => styles.content({ class: cn(opts?.class, uiOverrides.value.content) }),\r\n error: (opts?: { class?: any }) => styles.error({ class: cn(opts?.class, uiOverrides.value.error) }),\r\n };\r\n});\r\n\r\ndefineExpose({\r\n prop: props.prop,\r\n getBoundingClientRect\r\n});\r\n</script>\r\n",
51
+ "content": "<script setup lang=\"ts\">\r\nimport type { ClassValue } from 'clsx'\r\nimport type { formItemLabelPositions } from './reborn-form-item.config'\r\nimport { computed, provide } from 'vue'\r\nimport { useFieldGroupItem } from '@/composables/useFieldGroup'\r\nimport { tv } from '@/lib/tv'\r\nimport { cn } from '@/lib/utils'\r\nimport theme from './reborn-form-item.config'\r\n\r\ndefineOptions({\r\n name: 'ReFormItem',\r\n})\r\n\r\nconst props = withDefaults(defineProps<FormItemProps>(), {\r\n prop: '',\r\n label: '',\r\n customClass: '',\r\n required: false,\r\n requireAsteriskPosition: 'right',\r\n})\r\n\r\nexport interface FormItemProps {\r\n prop?: string\r\n label?: string\r\n required?: boolean\r\n requireAsteriskPosition?: 'left' | 'right' // 标签位置\r\n labelWidth?: string | number // 标签宽度\r\n labelPosition?: typeof formItemLabelPositions[number] // 标签位置\r\n trigger?: 'blur' | 'change' | 'none' | Array<'blur' | 'change'> // 触发验证\r\n customClass?: ClassValue\r\n ui?: Partial<{\r\n root: ClassValue\r\n label: ClassValue\r\n wrapper: ClassValue\r\n content: ClassValue\r\n error: ClassValue\r\n }>\r\n}\r\n\r\nconst {\r\n error,\r\n labelPosition,\r\n labelWidth,\r\n size,\r\n getBoundingClientRect,\r\n validate,\r\n} = useFieldGroupItem({\r\n prop: props.prop,\r\n labelPosition: props.labelPosition,\r\n labelWidth: props.labelWidth,\r\n trigger: props.trigger,\r\n})\r\n\r\nprovide('rebornFormItem', {\r\n isError: computed(() => !!error.value),\r\n validate,\r\n})\r\n\r\nconst labelStyle = computed(() => {\r\n return {\r\n width: labelWidth.value,\r\n }\r\n})\r\n\r\n// Configure Component Styles\r\nconst b = tv(theme)\r\nconst uiOverrides = computed(() => props.ui || {})\r\n\r\nconst ui = computed(() => {\r\n const styles = b({\r\n error: !!error.value,\r\n labelPosition: labelPosition.value as any, // Cast to match config variants\r\n size: size.value as any,\r\n })\r\n return {\r\n root: (opts?: { class?: any }) => styles.root({ class: cn(opts?.class, uiOverrides.value.root) }),\r\n wrapper: (opts?: { class?: any }) => styles.wrapper({ class: cn(opts?.class, uiOverrides.value.wrapper) }),\r\n label: (opts?: { class?: any }) => styles.label({ class: cn(opts?.class, uiOverrides.value.label) }),\r\n content: (opts?: { class?: any }) => styles.content({ class: cn(opts?.class, uiOverrides.value.content) }),\r\n error: (opts?: { class?: any }) => styles.error({ class: cn(opts?.class, uiOverrides.value.error) }),\r\n }\r\n})\r\n\r\ndefineExpose({\r\n prop: props.prop,\r\n getBoundingClientRect,\r\n})\r\n</script>\r\n\r\n<template>\r\n <view class=\"re-form-item\" :class=\"ui.root({ class: customClass })\">\r\n <view v-if=\"label || $slots.label\" :class=\"ui.label()\" :style=\"labelStyle\">\r\n <slot name=\"label\">\r\n <text\r\n v-if=\"required && requireAsteriskPosition === 'left'\" class=\"\r\n mr-1 text-red-500\r\n \"\r\n >\r\n *\r\n </text>\r\n {{ label }}\r\n <text\r\n v-if=\"required && requireAsteriskPosition === 'right'\" class=\"\r\n ml-1 text-red-500\r\n \"\r\n >\r\n *\r\n </text>\r\n </slot>\r\n </view>\r\n\r\n <view :class=\"ui.wrapper()\">\r\n <view :class=\"ui.content()\">\r\n <slot />\r\n </view>\r\n\r\n <view v-if=\"error\" :class=\"ui.error()\">\r\n {{ error }}\r\n </view>\r\n </view>\r\n </view>\r\n</template>\r\n",
47
52
  "target": "uniapp"
48
53
  }
49
54
  ],
50
- "fileCount": 8,
51
- "contentHash": "7ed4704d3c47dea28a275904c34150198e739a7d"
55
+ "fileCount": 9,
56
+ "contentHash": "8fff822e89e3b8ebae6daf503ec21a7fb16cbefd"
52
57
  }
@@ -8,7 +8,7 @@
8
8
  "files": [
9
9
  {
10
10
  "path": "reborn-image.config.ts",
11
- "content": "const mode = [\"scaleToFill\", \"aspectFit\", \"aspectFill\", \"widthFix\", \"heightFix\", \"top\", \"bottom\", \"center\", \"left\", \"right\", \"top left\", \"top right\", \"bottom left\", \"bottom right\"]\r\nexport default {\r\n slots: {\r\n root: \"relative flex flex-row items-center justify-center rounded-xl overflow-hidden\",\r\n error: \"absolute h-full w-full bg-gray-2 dark:bg-gray-7 flex flex-col items-center justify-center\",\r\n errorIcon: \"text-gray-4 size-8 icon-[lucide--image-off]\",\r\n loading: \"absolute h-full w-full bg-gray-2 dark:bg-gray-7 flex flex-col items-center justify-center\",\r\n loadingIcon: \"text-surface-400 size-8 border-2 border-gray-3 border-t-blue-500 rounded-full animate-spin\",\r\n inner: \"w-full h-full\",\r\n },\r\n}\r\n\r\nexport { mode as imageMode }",
11
+ "content": "const mode = [\"scaleToFill\", \"aspectFit\", \"aspectFill\", \"widthFix\", \"heightFix\", \"top\", \"bottom\", \"center\", \"left\", \"right\", \"top left\", \"top right\", \"bottom left\", \"bottom right\"]\r\nexport default {\r\n slots: {\r\n root: \"relative flex flex-row items-center justify-center rounded-xl overflow-hidden\",\r\n error: \"absolute h-full w-full bg-gray-2 dark:bg-gray-7 flex flex-col items-center justify-center\",\r\n errorIcon: \"text-gray-4 size-8 icon-[lucide--image-off]\",\r\n loading: \"absolute h-full w-full bg-gray-2 dark:bg-gray-7 flex flex-col items-center justify-center\",\r\n loadingIcon: \"text-gray-4 size-8 border-2 border-gray-3 border-t-blue-500 rounded-full animate-spin\",\r\n inner: \"w-full h-full\",\r\n },\r\n}\r\n\r\nexport { mode as imageMode }",
12
12
  "target": "web"
13
13
  },
14
14
  {
@@ -20,17 +20,22 @@
20
20
  "path": "RefactorPlan.md",
21
21
  "content": "# RebornImage State Handling Refactor\r\n\r\n## Problem\r\n`vue-lazyload` manages image loading internally. When an image fails, it sets the `lazy=\"error\"` attribute on the `img` element but does not necessarily bubble a standard `@error` event to the Vue component, causing `isError` state to remain `false`.\r\n\r\n## Solution\r\nUse CSS-based state control via Tailwind's `peer` utility for `v-lazy`, while maintaining JS-based control for standard loading.\r\n\r\n### Changes\r\n1. **Reorder DOM**: Move `<img>` to the beginning of the container so it can be a `peer` to the overlays.\r\n2. **Add `peer` class**: Add `peer` to both standard and lazy `img` tags.\r\n3. **Update Overlays**:\r\n - Change `v-if` to `v-show` or class binding to keep elements in DOM (required for peer).\r\n - Add Tailwind peer modifiers:\r\n - Error Overlay: `peer-[lazy=error]:flex` (and ensure it's hidden by default/controlled by `isError`).\r\n - Loading Overlay: `peer-[lazy=loading]:flex` (and controlled by `isLoading`).\r\n4. **Compatibility**: Ensure `isError`/`isLoading` refs still work for non-lazy mode.\r\n\r\n## Implementation Details\r\n- `img` tag needs `z-0` or similar? Default stacking: later siblings on top. So `img` first is perfect for `absolute` overlays on top.\r\n- Error Overlay Class: `hidden peer-[lazy=error]:flex` (if relying purely on CSS for lazy) + `flex` if `isError` is true.\r\n - Combined: `cn(..., { 'flex': isError, 'hidden': !isError && !lazyLoad, 'peer-[lazy=error]:flex': lazyLoad })`\r\n - Simplest: Always render, default hidden. Show if `isError` OR `peer-lazy=error`.\r\n - Class: `absolute ... hidden peer-[lazy=error]:flex` ?\r\n - Vue logic: `:class=\"{ '!flex': isError }\"` (using `!` to override hidden).\r\n\r\n### Plan\r\n1. Move `img` tags to top of `div`.\r\n2. Add `peer` to `img`.\r\n3. Update `error` and `loading` divs to use peer classes.\r\n"
22
22
  },
23
+ {
24
+ "path": "index.ts",
25
+ "content": "export { default as RebornImage } from './RebornImage.vue'\r\n",
26
+ "target": "uniapp"
27
+ },
23
28
  {
24
29
  "path": "reborn-image.config.ts",
25
- "content": "const mode = [\"scaleToFill\", \"aspectFit\", \"aspectFill\", \"widthFix\", \"heightFix\", \"top\", \"bottom\", \"center\", \"left\", \"right\", \"top left\", \"top right\", \"bottom left\", \"bottom right\"]\r\nexport default {\r\n slots: {\r\n root: \"relative flex flex-row items-center justify-center\",\r\n error: \"absolute h-full w-full bg-gray-2 dark:bg-gray-7 rounded-xl flex flex-col items-center justify-center\",\r\n errorIcon: \"text-gray-4\",\r\n loading: \"absolute h-full w-full bg-gray-2 dark:bg-gray-7 rounded-xl flex flex-col items-center justify-center\",\r\n loadingIcon: \"text-surface-400\",\r\n inner: \"w-full h-full rounded-xl\",\r\n },\r\n}\r\n\r\nexport { mode as imageMode }",
30
+ "content": "const mode = ['scaleToFill', 'aspectFit', 'aspectFill', 'widthFix', 'heightFix', 'top', 'bottom', 'center', 'left', 'right', 'top left', 'top right', 'bottom left', 'bottom right']\r\nexport default {\r\n slots: {\r\n root: 'relative flex flex-row items-center justify-center overflow-hidden',\r\n error: 'absolute h-full w-full bg-gray-2 dark:bg-gray-7 flex flex-col items-center justify-center',\r\n errorIcon: 'text-gray-4',\r\n loading: 'absolute h-full w-full bg-gray-2 dark:bg-gray-7 flex flex-col items-center justify-center',\r\n loadingIcon: 'text-gray-4',\r\n inner: 'w-full h-full',\r\n },\r\n variants: {\r\n round: {\r\n true: {\r\n root: 'rounded-xl',\r\n },\r\n },\r\n },\r\n}\r\n\r\nexport { mode as imageMode }\r\n",
26
31
  "target": "uniapp"
27
32
  },
28
33
  {
29
34
  "path": "RebornImage.vue",
30
- "content": "<script setup lang=\"ts\">\r\nimport type { ClassValue } from \"clsx\";\r\nimport { computed, ref, type PropType } from \"vue\";\r\nimport { tv } from '@/lib/tv';\r\nimport { cn } from '@/lib/utils';\r\nimport theme, { imageMode } from './reborn-image.config'\r\n\r\nexport interface ImageProps {\r\n\tcustomClass?: ClassValue;\r\n\tsrc: string;\r\n\tmode?: typeof imageMode[number]; // 图片裁剪、缩放的模式\r\n\tpreview?: boolean; // 是否显示边框\r\n\tpreviewList?: string[];\r\n\theight?: string | number;\r\n\twidth?: string | number;\r\n\tshowLoading?: boolean; // 是否显示加载状态\r\n\tlazyLoad?: boolean; // 是否懒加载\r\n\tfadeShow?: boolean; // 图片显示动画效果\r\n\twebp?: boolean; // 是否解码webp格式\r\n\tshowMenuByLongpress?: boolean; // 是否长按显示菜单\r\n\tui?: Partial<{\r\n\t\troot: ClassValue;\r\n\t\terror: ClassValue;\r\n\t\terrorIcon: ClassValue;\r\n\t\tloading: ClassValue;\r\n\t\tloadingIcon: ClassValue;\r\n\t\tinner: ClassValue;\r\n\t}>;\r\n}\r\n\r\nconst props = withDefaults(defineProps<ImageProps>(), {\r\n\tmode: \"aspectFill\",\r\n\tpreview: false,\r\n\theight: 120,\r\n\twidth: 120,\r\n\tshowLoading: true,\r\n\tlazyLoad: false,\r\n\twebp: false,\r\n\tshowMenuByLongpress: false,\r\n})\r\n\r\n// 事件定义\r\nconst emit = defineEmits([\"load\", \"error\"]);\r\n\r\nconst b = tv(theme);\r\n\r\nconst uiOverrides = computed(() => props.ui || {});\r\n\r\nconst ui = computed(() => {\r\n\tconst styles = b();\r\n\treturn {\r\n\t\troot: (opts?: { class?: any }) => styles.root({ class: cn(opts?.class, uiOverrides.value.root) }),\r\n\t\terror: (opts?: { class?: any }) => styles.error({ class: cn(opts?.class, uiOverrides.value.error) }),\r\n\t\terrorIcon: (opts?: { class?: any }) => styles.errorIcon({ class: cn(opts?.class, uiOverrides.value.errorIcon) }),\r\n\t\tloading: (opts?: { class?: any }) => styles.loading({ class: cn(opts?.class, uiOverrides.value.loading) }),\r\n\t\tloadingIcon: (opts?: { class?: any }) => styles.loadingIcon({ class: cn(opts?.class, uiOverrides.value.loadingIcon) }),\r\n\t\tinner: (opts?: { class?: any }) => styles.inner({ class: cn(opts?.class, uiOverrides.value.inner) }),\r\n\t};\r\n})\r\n\r\n// 加载状态\r\nconst isLoading = ref(true);\r\n// 加载失败状态\r\nconst isError = ref(false);\r\n\r\nfunction getUnit(val: string | number | undefined | null): string | undefined {\r\n\tif (val == null || val === \"\") return undefined;\r\n\r\n\tif (typeof val === \"string\") {\r\n\t\t// 如果包含单位则直接返回,否则补 rpx\r\n\t\tconst hasUnit = /px|rpx|%|vw|vh$/.test(val);\r\n\t\treturn hasUnit ? val : `${val}rpx`;\r\n\t}\r\n\treturn `${val}rpx`;\r\n}\r\n\r\n// 图片加载成功\r\nfunction onLoad(e: any) {\r\n\tisLoading.value = false;\r\n\tisError.value = false;\r\n\temit(\"load\", e);\r\n}\r\n\r\n// 图片加载失败\r\nfunction onError(e: any) {\r\n\tisLoading.value = false;\r\n\tisError.value = true;\r\n\temit(\"error\", e);\r\n}\r\n\r\n// 图片点击\r\nfunction onTap() {\r\n\tif (props.preview) {\r\n\t\t// 修正逻辑:优先使用 previewList,如果没有则用当前 src 组成数组\r\n\t\tconst urls = (props.previewList && props.previewList.length > 0)\r\n\t\t\t? props.previewList\r\n\t\t\t: [props.src];\r\n\r\n\t\tuni.previewImage({\r\n\t\t\turls: urls, // 此时 urls 必定是 string[]\r\n\t\t\tcurrent: props.src\r\n\t\t});\r\n\t}\r\n}\r\n</script>\r\n<template>\r\n\t<view :class=\"ui.root({ class: props.customClass })\" :style=\"{\r\n\t\twidth: getUnit(width),\r\n\t\theight: getUnit(height)\r\n\t}\">\r\n\t\t<view :class=\"ui.error()\" v-if=\"isError\">\r\n\t\t\t<slot name=\"error\">\r\n\t\t\t\t<!-- <view :class=\"absolute w-[2px] h-full bg-current rotate-45\"></view>\r\n\t\t\t\t<view :class=\"absolute w-[2px] h-full bg-current -rotate-45\"></view> -->\r\n\t\t\t\t<view :class=\"ui.errorIcon()\">\r\n\t\t\t\t\t<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\">\r\n\t\t\t\t\t\t<g fill=\"none\" stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\"\r\n\t\t\t\t\t\t\tstroke-width=\"1.5\">\r\n\t\t\t\t\t\t\t<path\r\n\t\t\t\t\t\t\t\td=\"m3 16l7-3l4 1.818M16 10a2 2 0 1 1 0-4a2 2 0 0 1 0 4m.879 11.121L19 19m2.121-2.121L19 19m0 0l-2.121-2.121M19 19l2.121 2.121\" />\r\n\t\t\t\t\t\t\t<path d=\"M13 21H3.6a.6.6 0 0 1-.6-.6V3.6a.6.6 0 0 1 .6-.6h16.8a.6.6 0 0 1 .6.6V13\" />\r\n\t\t\t\t\t\t</g>\r\n\t\t\t\t\t</svg>\r\n\t\t\t\t</view>\r\n\t\t\t</slot>\r\n\t\t</view>\r\n\t\t<view :class=\"ui.loading()\" v-else-if=\"isLoading && showLoading\">\r\n\t\t\t<slot name=\"loading\">\r\n\t\t\t\t<view :class=\"ui.loadingIcon()\"\r\n\t\t\t\t\tclass=\"w-6 h-6 border-2 border-gray-3 border-t-blue-500 rounded-full animate-spin\">\r\n\t\t\t\t</view>\r\n\t\t\t</slot>\r\n\t\t</view>\r\n\t\t<image :class=\"ui.inner()\" :src=\"src\" :mode=\"mode\" :lazy-load=\"lazyLoad\" :webp=\"webp\"\r\n\t\t\t:show-menu-by-longpress=\"showMenuByLongpress\" @load=\"onLoad\" @error=\"onError\" @tap=\"onTap\" />\r\n\t\t<slot></slot>\r\n\t</view>\r\n</template>",
35
+ "content": "<script setup lang=\"ts\">\r\nimport type { ClassValue } from 'clsx'\r\nimport type { imageMode } from './reborn-image.config'\r\nimport { computed, ref } from 'vue'\r\nimport { tv } from '@/lib/tv'\r\nimport { cn } from '@/lib/utils'\r\nimport theme from './reborn-image.config'\r\n\r\nexport interface ImageProps {\r\n customClass?: ClassValue\r\n src: string\r\n mode?: typeof imageMode[number] // 图片裁剪、缩放的模式\r\n preview?: boolean // 是否显示边框\r\n previewList?: string[]\r\n height?: string | number\r\n width?: string | number\r\n showLoading?: boolean // 是否显示加载状态\r\n lazyLoad?: boolean // 是否懒加载\r\n fadeShow?: boolean // 图片显示动画效果\r\n webp?: boolean // 是否解码webp格式\r\n showMenuByLongpress?: boolean // 是否长按显示菜单\r\n round?: boolean // 是否显示圆角\r\n ui?: Partial<{\r\n root: ClassValue\r\n error: ClassValue\r\n errorIcon: ClassValue\r\n loading: ClassValue\r\n loadingIcon: ClassValue\r\n inner: ClassValue\r\n }>\r\n}\r\n\r\nconst props = withDefaults(defineProps<ImageProps>(), {\r\n mode: 'aspectFill',\r\n preview: false,\r\n height: 120,\r\n width: 120,\r\n showLoading: true,\r\n lazyLoad: false,\r\n webp: false,\r\n showMenuByLongpress: false,\r\n round: false,\r\n})\r\n\r\n// 事件定义\r\nconst emit = defineEmits(['load', 'error'])\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 round: props.round,\r\n })\r\n return {\r\n root: (opts?: { class?: any }) => styles.root({ class: cn(opts?.class, uiOverrides.value.root) }),\r\n error: (opts?: { class?: any }) => styles.error({ class: cn(opts?.class, uiOverrides.value.error) }),\r\n errorIcon: (opts?: { class?: any }) => styles.errorIcon({ class: cn(opts?.class, uiOverrides.value.errorIcon) }),\r\n loading: (opts?: { class?: any }) => styles.loading({ class: cn(opts?.class, uiOverrides.value.loading) }),\r\n loadingIcon: (opts?: { class?: any }) => styles.loadingIcon({ class: cn(opts?.class, uiOverrides.value.loadingIcon) }),\r\n inner: (opts?: { class?: any }) => styles.inner({ class: cn(opts?.class, uiOverrides.value.inner) }),\r\n }\r\n})\r\n\r\n// 加载状态\r\nconst isLoading = ref(true)\r\n// 加载失败状态\r\nconst isError = ref(false)\r\n\r\nfunction getUnit(val: string | number | undefined | null): string | undefined {\r\n if (val == null || val === '') { return undefined }\r\n\r\n if (typeof val === 'string') {\r\n // 如果包含单位则直接返回,否则补 rpx\r\n const hasUnit = /px|rpx|%|vw|vh$/.test(val)\r\n return hasUnit ? val : `${val}rpx`\r\n }\r\n return `${val}rpx`\r\n}\r\n\r\n// 图片加载成功\r\nfunction onLoad(e: any) {\r\n isLoading.value = false\r\n isError.value = false\r\n emit('load', e)\r\n}\r\n\r\n// 图片加载失败\r\nfunction onError(e: any) {\r\n isLoading.value = false\r\n isError.value = true\r\n emit('error', e)\r\n}\r\n\r\n// 图片点击\r\nfunction onTap() {\r\n if (props.preview) {\r\n // 修正逻辑:优先使用 previewList,如果没有则用当前 src 组成数组\r\n const urls = (props.previewList && props.previewList.length > 0)\r\n ? props.previewList\r\n : [props.src]\r\n\r\n uni.previewImage({\r\n urls, // 此时 urls 必定是 string[]\r\n current: props.src,\r\n })\r\n }\r\n}\r\n</script>\r\n\r\n<template>\r\n <view :class=\"ui.root({ class: props.customClass })\" :style=\"{\r\n width: getUnit(width),\r\n height: getUnit(height),\r\n }\">\r\n <view v-if=\"isError\" :class=\"ui.error()\">\r\n <slot name=\"error\">\r\n <!-- <view :class=\"absolute w-[2px] h-full bg-current rotate-45\"></view>\r\n\t\t\t\t<view :class=\"absolute w-[2px] h-full bg-current -rotate-45\"></view> -->\r\n <view :class=\"ui.errorIcon()\">\r\n <svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\">\r\n <g fill=\"none\" stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"1.5\">\r\n <path\r\n d=\"m3 16l7-3l4 1.818M16 10a2 2 0 1 1 0-4a2 2 0 0 1 0 4m.879 11.121L19 19m2.121-2.121L19 19m0 0l-2.121-2.121M19 19l2.121 2.121\" />\r\n <path d=\"M13 21H3.6a.6.6 0 0 1-.6-.6V3.6a.6.6 0 0 1 .6-.6h16.8a.6.6 0 0 1 .6.6V13\" />\r\n </g>\r\n </svg>\r\n </view>\r\n </slot>\r\n </view>\r\n <view v-else-if=\"isLoading && showLoading\" :class=\"ui.loading()\">\r\n <slot name=\"loading\">\r\n <view :class=\"ui.loadingIcon()\" class=\"\r\n size-6 animate-spin rounded-full border-2 border-gray-3\r\n border-t-blue-500\r\n \" />\r\n </slot>\r\n </view>\r\n <image :class=\"ui.inner()\" :src=\"src\" :mode=\"mode\" :lazy-load=\"lazyLoad\" :webp=\"webp\"\r\n :show-menu-by-longpress=\"showMenuByLongpress\" @load=\"onLoad\" @error=\"onError\" @tap=\"onTap\" />\r\n <slot />\r\n </view>\r\n</template>\r\n",
31
36
  "target": "uniapp"
32
37
  }
33
38
  ],
34
- "fileCount": 5,
35
- "contentHash": "44e8bb7dcb8557a86f7135a78c744ec9634fd097"
39
+ "fileCount": 6,
40
+ "contentHash": "f2051a9679f1a0001b645fe818ce75cfb09c5e88"
36
41
  }
@@ -6,7 +6,8 @@
6
6
  "files": [
7
7
  {
8
8
  "path": "index.ts",
9
- "content": "export { default as RebornInputNumber } from \"./RebornInputNumber.vue\";\r\n"
9
+ "content": "export { default as RebornInputNumber } from \"./RebornInputNumber.vue\";\r\n",
10
+ "target": "web"
10
11
  },
11
12
  {
12
13
  "path": "reborn-input-number.config.ts",
@@ -18,22 +19,27 @@
18
19
  "content": "<script setup lang=\"ts\">\r\nimport { computed, ref, toRef, useAttrs, watch } from \"vue\";\r\nimport type { ClassValue } from \"clsx\";\r\nimport { cn } from \"~/lib/utils\";\r\nimport theme, { inputNumberColors, inputNumberSizes } from \"./reborn-input-number.config\";\r\nimport { useFieldGroup } from \"~/composables/useFieldGroup\";\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 InputNumberProps {\r\n modelValue?: number;\r\n defaultValue?: number;\r\n min?: number;\r\n max?: number;\r\n step?: number;\r\n disabled?: boolean;\r\n size?: typeof inputNumberSizes[number];\r\n color?: typeof inputNumberColors[number];\r\n class?: any;\r\n ui?: Partial<{\r\n wrapper: ClassValue;\r\n button: ClassValue;\r\n input: ClassValue;\r\n divider: ClassValue;\r\n icon: ClassValue;\r\n }>;\r\n}\r\n\r\nconst props = withDefaults(defineProps<InputNumberProps>(), {\r\n disabled: false,\r\n step: 1,\r\n size: \"md\",\r\n color: \"primary\",\r\n});\r\n\r\nconst emit = defineEmits<{\r\n (e: \"update:modelValue\", value: number): void;\r\n}>();\r\n\r\nconst attrs = useAttrs();\r\n\r\nconst localValue = ref(props.defaultValue ?? props.min ?? 0);\r\nconst currentValue = computed(() => (props.modelValue !== undefined ? props.modelValue : localValue.value));\r\n\r\nconst { orientation, size: fieldGroupSize } = useFieldGroup(props);\r\n\r\nconst size = toRef(props, \"size\");\r\n\r\nconst uiOverrides = computed(() => props.ui || {});\r\n\r\nconst ui = computed(() => {\r\n const styles = b({\r\n size: (fieldGroupSize.value || size.value) as any,\r\n color: props.color,\r\n fieldGroup: orientation.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 button: (opts?: { class?: any }) => styles.button({ class: cn(opts?.class, uiOverrides.value.button) }),\r\n input: (opts?: { class?: any }) => styles.input({ class: cn(opts?.class, uiOverrides.value.input) }),\r\n divider: (opts?: { class?: any }) => styles.divider({ class: cn(opts?.class, uiOverrides.value.divider) }),\r\n icon: (opts?: { class?: any }) => styles.icon({ class: cn(opts?.class, uiOverrides.value.icon) }),\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\nconst isDecrementDisabled = computed(() =>\r\n props.disabled || (props.min !== undefined && currentValue.value <= props.min),\r\n);\r\n\r\nconst isIncrementDisabled = computed(() =>\r\n props.disabled || (props.max !== undefined && currentValue.value >= props.max),\r\n);\r\n\r\nfunction clampValue(value: number) {\r\n let nextValue = value;\r\n if (props.min !== undefined) {\r\n nextValue = Math.max(nextValue, props.min);\r\n }\r\n if (props.max !== undefined) {\r\n nextValue = Math.min(nextValue, props.max);\r\n }\r\n return nextValue;\r\n}\r\n\r\nfunction updateValue(value: number) {\r\n const nextValue = clampValue(value);\r\n if (props.modelValue === undefined) {\r\n localValue.value = nextValue;\r\n }\r\n emit(\"update:modelValue\", nextValue);\r\n}\r\n\r\nfunction handleInput(event: Event) {\r\n const target = event.target as HTMLInputElement;\r\n const parsed = Number(target.value);\r\n\r\n if (Number.isNaN(parsed)) {\r\n return;\r\n }\r\n\r\n updateValue(parsed);\r\n}\r\n\r\nfunction increase() {\r\n if (isIncrementDisabled.value) return;\r\n updateValue(currentValue.value + props.step);\r\n}\r\n\r\nfunction decrease() {\r\n if (isDecrementDisabled.value) return;\r\n updateValue(currentValue.value - props.step);\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</script>\r\n\r\n<template>\r\n <div :class=\"ui.wrapper({ class: props.class })\" :data-disabled=\"props.disabled\">\r\n <button type=\"button\" :class=\"ui.button()\" :disabled=\"isDecrementDisabled\" @click=\"decrease\">\r\n <slot name=\"decrement\" :icon-class=\"ui.icon()\">\r\n <Icon name=\"lucide:minus\" :class=\"ui.icon()\" />\r\n </slot>\r\n </button>\r\n\r\n <span :class=\"ui.divider()\" aria-hidden=\"true\" />\r\n\r\n <input v-bind=\"inputAttrs\" type=\"number\" inputmode=\"decimal\" :min=\"props.min\" :max=\"props.max\" :step=\"props.step\"\r\n :value=\"currentValue\" :disabled=\"props.disabled\" :class=\"ui.input()\" @input=\"handleInput\" />\r\n\r\n <span :class=\"ui.divider()\" aria-hidden=\"true\" />\r\n\r\n <button type=\"button\" :class=\"ui.button()\" :disabled=\"isIncrementDisabled\" @click=\"increase\">\r\n <slot name=\"increment\" :icon-class=\"ui.icon()\">\r\n <Icon name=\"lucide:plus\" :class=\"ui.icon()\" />\r\n </slot>\r\n </button>\r\n </div>\r\n</template>\r\n",
19
20
  "target": "web"
20
21
  },
22
+ {
23
+ "path": "index.ts",
24
+ "content": "export { default as RebornInputNumber } from './RebornInputNumber.vue'\r\n",
25
+ "target": "uniapp"
26
+ },
21
27
  {
22
28
  "path": "long-press.ts",
23
- "content": "import { onUnmounted, ref, type Ref } from \"vue\";\r\n\r\n// 长按触发延迟时间,单位毫秒\r\nconst DELAY = 500;\r\n// 长按重复执行间隔时间,单位毫秒\r\nconst REPEAT = 100;\r\n\r\n/**\r\n * 长按操作钩子函数返回类型\r\n */\r\ntype UseLongPress = {\r\n\t// 开始长按\r\n\tstart: (cb: () => void) => void;\r\n\t// 停止长按\r\n\tstop: () => void;\r\n\t// 清除定时器\r\n\tclear: () => void;\r\n\t// 是否正在长按中\r\n\tisPressing: Ref<boolean>;\r\n};\r\n\r\n/**\r\n * 长按操作钩子函数\r\n * 支持长按持续触发,可用于数字输入框等需要连续操作的场景\r\n */\r\nexport const useLongPress = (): UseLongPress => {\r\n\t// 是否正在长按中\r\n\tconst isPressing = ref(false);\r\n\t// 长按延迟定时器\r\n\tlet pressTimer: number = 0;\r\n\t// 重复执行定时器\r\n\tlet repeatTimer: number = 0;\r\n\r\n\t/**\r\n\t * 清除所有定时器\r\n\t * 重置长按状态\r\n\t */\r\n\tconst clear = () => {\r\n\t\t// 清除长按延迟定时器\r\n\t\tif (pressTimer != 0) {\r\n\t\t\tclearTimeout(pressTimer);\r\n\t\t\tpressTimer = 0;\r\n\t\t}\r\n\t\t// 清除重复执行定时器\r\n\t\tif (repeatTimer != 0) {\r\n\t\t\tclearInterval(repeatTimer);\r\n\t\t\trepeatTimer = 0;\r\n\t\t}\r\n\t\t// 重置长按状态\r\n\t\tisPressing.value = false;\r\n\t};\r\n\r\n\t/**\r\n\t * 开始长按操作\r\n\t * @param cb 长按时重复执行的回调函数\r\n\t */\r\n\tconst start = (cb: () => void) => {\r\n\t\t// 清除已有定时器\r\n\t\tclear();\r\n\r\n\t\t// 立即执行一次回调\r\n\t\tcb();\r\n\r\n\t\t// 延迟500ms后开始长按\r\n\t\t// @ts-ignore\r\n\t\tpressTimer = setTimeout(() => {\r\n\r\n\t\t\t// 设置长按状态\r\n\t\t\tisPressing.value = true;\r\n\t\t\t// 每100ms重复执行回调\r\n\t\t\t// @ts-ignore\r\n\t\t\trepeatTimer = setInterval(() => {\r\n\t\t\t\tcb();\r\n\t\t\t}, REPEAT);\r\n\t\t}, DELAY);\r\n\t};\r\n\r\n\t/**\r\n\t * 停止长按操作\r\n\t * 清除定时器并重置状态\r\n\t */\r\n\tconst stop = () => {\r\n\t\tclear();\r\n\t};\r\n\r\n\t// 组件卸载时清理定时器\r\n\tonUnmounted(() => {\r\n\t\tclear();\r\n\t});\r\n\r\n\treturn {\r\n\t\tstart,\r\n\t\tstop,\r\n\t\tclear,\r\n\t\tisPressing\r\n\t};\r\n};\r\n",
29
+ "content": "import type { Ref } from 'vue'\r\nimport { onUnmounted, ref } from 'vue'\r\n\r\n// 长按触发延迟时间,单位毫秒\r\nconst DELAY = 500\r\n// 长按重复执行间隔时间,单位毫秒\r\nconst REPEAT = 100\r\n\r\n/**\r\n * 长按操作钩子函数返回类型\r\n */\r\ninterface UseLongPress {\r\n // 开始长按\r\n start: (cb: () => void) => void\r\n // 停止长按\r\n stop: () => void\r\n // 清除定时器\r\n clear: () => void\r\n // 是否正在长按中\r\n isPressing: Ref<boolean>\r\n}\r\n\r\n/**\r\n * 长按操作钩子函数\r\n * 支持长按持续触发,可用于数字输入框等需要连续操作的场景\r\n */\r\nexport function useLongPress(): UseLongPress {\r\n // 是否正在长按中\r\n const isPressing = ref(false)\r\n // 长按延迟定时器\r\n let pressTimer: number = 0\r\n // 重复执行定时器\r\n let repeatTimer: number = 0\r\n\r\n /**\r\n * 清除所有定时器\r\n * 重置长按状态\r\n */\r\n const clear = () => {\r\n // 清除长按延迟定时器\r\n if (pressTimer != 0) {\r\n clearTimeout(pressTimer)\r\n pressTimer = 0\r\n }\r\n // 清除重复执行定时器\r\n if (repeatTimer != 0) {\r\n clearInterval(repeatTimer)\r\n repeatTimer = 0\r\n }\r\n // 重置长按状态\r\n isPressing.value = false\r\n }\r\n\r\n /**\r\n * 开始长按操作\r\n * @param cb 长按时重复执行的回调函数\r\n */\r\n const start = (cb: () => void) => {\r\n // 清除已有定时器\r\n clear()\r\n\r\n // 立即执行一次回调\r\n cb()\r\n\r\n // 延迟500ms后开始长按\r\n // @ts-ignore\r\n pressTimer = setTimeout(() => {\r\n // 设置长按状态\r\n isPressing.value = true\r\n // 每100ms重复执行回调\r\n // @ts-ignore\r\n repeatTimer = setInterval(() => {\r\n cb()\r\n }, REPEAT)\r\n }, DELAY)\r\n }\r\n\r\n /**\r\n * 停止长按操作\r\n * 清除定时器并重置状态\r\n */\r\n const stop = () => {\r\n clear()\r\n }\r\n\r\n // 组件卸载时清理定时器\r\n onUnmounted(() => {\r\n clear()\r\n })\r\n\r\n return {\r\n start,\r\n stop,\r\n clear,\r\n isPressing,\r\n }\r\n}\r\n",
24
30
  "target": "uniapp"
25
31
  },
26
32
  {
27
33
  "path": "reborn-input-number.config.ts",
28
- "content": "const size = [\"sm\", \"md\", \"lg\"] as const;\r\nconst color = [\"primary\", \"secondary\", \"success\", \"info\", \"warning\", \"error\", \"neutral\"] as const;\r\nconst shape = [\"circle\", \"square\"] as const;\r\n\r\nexport { size as inputNumberSizes, color as inputNumberColors, shape as inputNumberShapes };\r\n\r\nexport default {\r\n slots: {\r\n wrapper:\r\n \"group relative inline-flex items-center overflow-hidden bg-white text-gray-8 ring-[1.5px] ring-gray-4 transition-colors focus-within:ring-2 data-[disabled=true]:cursor-not-allowed data-[disabled=true]:bg-gray-1 data-[disabled=true]:text-gray-4 dark:bg-gray-800 dark:text-gray-200 dark:data-[disabled=true]:bg-gray-900 dark:data-[disabled=true]:text-gray-600\",\r\n button:\r\n \"flex h-full items-center justify-center text-gray-8 transition-colors disabled:cursor-not-allowed disabled:text-gray-4 dark:text-gray-400 dark:hover:text-gray-200 dark:disabled:text-gray-600\",\r\n input:\r\n \"min-w-0 flex-1 bg-transparent text-center text-gray-8 outline-none placeholder:text-gray-4 [appearance:textfield] [&::-webkit-inner-spin-button]:appearance-none [&::-webkit-outer-spin-button]:appearance-none dark:text-gray-200 dark:placeholder:text-gray-500\",\r\n divider: \"h-full w-[1.5px] bg-gray-4\",\r\n icon: \"shrink-0\",\r\n },\r\n variants: {\r\n size: {\r\n sm: {\r\n wrapper: \"h-[calc(var(--text-size-26)*2)] text-[length:var(--text-size-26)]\",\r\n input: \"w-[calc(var(--text-size-26)*2.5)] text-[length:var(--text-size-26)]\",\r\n button: \"p-1.5\",\r\n icon: \"size-3.5\",\r\n },\r\n md: {\r\n wrapper: \"h-[calc(var(--text-size-28)*2)] text-[length:var(--text-size-28)]\",\r\n input: \"w-[calc(var(--text-size-28)*2.5)] text-[length:var(--text-size-28)]\",\r\n button: \"p-2\",\r\n icon: \"size-4\",\r\n },\r\n lg: {\r\n wrapper: \"h-[calc(var(--text-size-32)*2)] text-[length:var(--text-size-32)]\",\r\n input: \"w-[calc(var(--text-size-32)*5)] text-[length:var(--text-size-32)]\",\r\n button: \"p-2\",\r\n icon: \"size-5\",\r\n },\r\n },\r\n color: {\r\n primary: {\r\n wrapper: \"focus-within:ring-primary\",\r\n button: \"hover:text-primary\",\r\n divider: \"group-focus-within:bg-primary\",\r\n },\r\n secondary: {\r\n wrapper: \"focus-within:ring-secondary\",\r\n button: \"hover:text-secondary\",\r\n divider: \"group-focus-within:bg-secondary\",\r\n },\r\n success: {\r\n wrapper: \"focus-within:ring-success\",\r\n button: \"hover:text-success\",\r\n divider: \"group-focus-within:bg-success\",\r\n },\r\n info: {\r\n wrapper: \"focus-within:ring-info\",\r\n button: \"hover:text-info\",\r\n divider: \"group-focus-within:bg-info\",\r\n },\r\n warning: {\r\n wrapper: \"focus-within:ring-warning\",\r\n button: \"hover:text-warning\",\r\n divider: \"group-focus-within:bg-warning\",\r\n },\r\n error: {\r\n wrapper: \"focus-within:ring-error\",\r\n button: \"hover:text-error\",\r\n divider: \"group-focus-within:bg-error\",\r\n },\r\n neutral: {\r\n wrapper: \"focus-within:ring-gray-4\",\r\n button: \"hover:text-neutral\",\r\n divider: \"group-focus-within:bg-gray-4\",\r\n },\r\n },\r\n shape: {\r\n circle: {\r\n wrapper: \"rounded-full\",\r\n },\r\n square: {\r\n wrapper: \"rounded-md\",\r\n },\r\n },\r\n fieldGroup: {\r\n horizontal:\r\n \"not-only:first:rounded-e-none not-only:last:rounded-s-none not-last:not-first:rounded-none focus-within:z-[1]\",\r\n vertical:\r\n \"not-only:first:rounded-b-none not-only:last:rounded-t-none not-last:not-first:rounded-none focus-within:z-[1]\",\r\n },\r\n },\r\n defaultVariants: {\r\n size: \"sm\" as (typeof size)[number],\r\n color: \"neutral\" as (typeof color)[number],\r\n shape: \"circle\" as (typeof shape)[number],\r\n },\r\n};\r\n",
34
+ "content": "const size = ['sm', 'md', 'lg'] as const\r\nconst color = ['primary', 'secondary', 'success', 'info', 'warning', 'error', 'neutral'] as const\r\nconst shape = ['circle', 'square'] as const\r\n\r\nexport { color as inputNumberColors, shape as inputNumberShapes, size as inputNumberSizes }\r\n\r\nexport default {\r\n slots: {\r\n wrapper:\r\n 'group relative inline-flex items-center overflow-hidden bg-white text-gray-8 ring-1 ring-gray-4 transition-colors focus-within:ring-2 data-[disabled=true]:cursor-not-allowed data-[disabled=true]:bg-gray-1 data-[disabled=true]:text-gray-4 dark:bg-gray-800 dark:text-gray-200 dark:data-[disabled=true]:bg-gray-900 dark:data-[disabled=true]:text-gray-600',\r\n button:\r\n 'flex h-full items-center justify-center text-gray-8 transition-colors disabled:cursor-not-allowed disabled:text-gray-4 dark:text-gray-4 dark:hover:text-gray-2 dark:disabled:text-gray-6',\r\n input:\r\n 'min-w-0 flex-1 bg-transparent text-center text-gray-8 outline-none placeholder:text-gray-4 [appearance:textfield] [&::-webkit-inner-spin-button]:appearance-none [&::-webkit-outer-spin-button]:appearance-none dark:text-gray-200 dark:placeholder:text-gray-500',\r\n divider: 'h-full w-[1px] bg-gray-4',\r\n icon: 'shrink-0',\r\n },\r\n variants: {\r\n size: {\r\n sm: {\r\n wrapper: 'h-[calc(var(--text-size-26)*2)] text-[length:var(--text-size-26)]',\r\n input: 'w-[calc(var(--text-size-26)*2.5)] text-[length:var(--text-size-26)]',\r\n button: 'p-1.5',\r\n icon: 'size-3.5',\r\n },\r\n md: {\r\n wrapper: 'h-[calc(var(--text-size-28)*2)] text-[length:var(--text-size-28)]',\r\n input: 'w-[calc(var(--text-size-28)*2.5)] text-[length:var(--text-size-28)]',\r\n button: 'p-2',\r\n icon: 'size-4',\r\n },\r\n lg: {\r\n wrapper: 'h-[calc(var(--text-size-32)*2)] text-[length:var(--text-size-32)]',\r\n input: 'w-[calc(var(--text-size-32)*5)] text-[length:var(--text-size-32)]',\r\n button: 'p-2',\r\n icon: 'size-5',\r\n },\r\n },\r\n color: {\r\n primary: {\r\n wrapper: 'focus-within:ring-primary',\r\n button: 'hover:text-primary',\r\n divider: 'group-focus-within:bg-primary',\r\n },\r\n secondary: {\r\n wrapper: 'focus-within:ring-secondary',\r\n button: 'hover:text-secondary',\r\n divider: 'group-focus-within:bg-secondary',\r\n },\r\n success: {\r\n wrapper: 'focus-within:ring-success',\r\n button: 'hover:text-success',\r\n divider: 'group-focus-within:bg-success',\r\n },\r\n info: {\r\n wrapper: 'focus-within:ring-info',\r\n button: 'hover:text-info',\r\n divider: 'group-focus-within:bg-info',\r\n },\r\n warning: {\r\n wrapper: 'focus-within:ring-warning',\r\n button: 'hover:text-warning',\r\n divider: 'group-focus-within:bg-warning',\r\n },\r\n error: {\r\n wrapper: 'focus-within:ring-error',\r\n button: 'hover:text-error',\r\n divider: 'group-focus-within:bg-error',\r\n },\r\n neutral: {\r\n wrapper: 'focus-within:ring-gray-4',\r\n button: 'hover:text-neutral',\r\n divider: 'group-focus-within:bg-gray-4',\r\n },\r\n },\r\n shape: {\r\n circle: {\r\n wrapper: 'rounded-full',\r\n },\r\n square: {\r\n wrapper: 'rounded-md',\r\n },\r\n },\r\n error: {\r\n true: {\r\n wrapper: 'ring-error',\r\n divider: 'bg-error',\r\n },\r\n },\r\n fieldGroup: {\r\n horizontal:\r\n 'not-only:first:rounded-e-none not-only:last:rounded-s-none not-last:not-first:rounded-none focus-within:z-[1]',\r\n vertical:\r\n 'not-only:first:rounded-b-none not-only:last:rounded-t-none not-last:not-first:rounded-none focus-within:z-[1]',\r\n },\r\n },\r\n defaultVariants: {\r\n size: 'sm' as (typeof size)[number],\r\n color: 'neutral' as (typeof color)[number],\r\n shape: 'circle' as (typeof shape)[number],\r\n },\r\n}\r\n",
29
35
  "target": "uniapp"
30
36
  },
31
37
  {
32
38
  "path": "RebornInputNumber.vue",
33
- "content": "<template>\r\n <view :class=\"ui.wrapper({ class: props.customClass })\" :data-disabled=\"fieldGroupDisabled\">\r\n <view :class=\"ui.button()\" @touchstart=\"onMinus\" @touchend=\"longPress.stop\" @touchcancel=\"longPress.stop\">\r\n <slot name=\"decrease-icon\">\r\n <view :class=\"ui.icon()\" class=\"i-lucide-minus\" />\r\n </slot>\r\n </view>\r\n\r\n <view :class=\"ui.divider()\" />\r\n\r\n <input :class=\"ui.input()\" :type=\"inputType\" :value=\"value\" :disabled=\"fieldGroupDisabled\" :readonly=\"!readonly\"\r\n :placeholder=\"placeholder\" @input=\"onInput\" @blur=\"onBlur\" />\r\n\r\n <view :class=\"ui.divider()\" />\r\n\r\n <view :class=\"ui.button()\" @touchstart=\"onPlus\" @touchend=\"longPress.stop\" @touchcancel=\"longPress.stop\">\r\n <slot name=\"increase-icon\">\r\n <view :class=\"ui.icon()\" class=\"i-lucide-plus\" />\r\n </slot>\r\n </view>\r\n </view>\r\n</template>\r\n\r\n<script lang=\"ts\" setup>\r\nimport { computed, nextTick, ref, watch, useAttrs, toRef } from \"vue\";\r\nimport type { ClassValue } from \"clsx\";\r\nimport theme, { inputNumberColors, inputNumberSizes, inputNumberShapes } from \"./reborn-input-number.config\";\r\n\r\nimport { useFormInject } from \"@/composables/useFieldGroup\";\r\nimport { useLongPress } from \"./long-press\";\r\nimport { cn } from \"@/lib/utils\";\r\nimport { tv } from \"@/lib/tv\";\r\n\r\ndefineOptions({\r\n name: \"re-input-number\",\r\n inheritAttrs: false\r\n});\r\n\r\nexport interface InputNumberProps {\r\n modelValue?: number;\r\n defaultValue?: number;\r\n min?: number;\r\n max?: number;\r\n step?: number;\r\n disabled?: boolean;\r\n size?: typeof inputNumberSizes[number];\r\n color?: typeof inputNumberColors[number];\r\n shape?: typeof inputNumberShapes[number];\r\n ui?: Partial<{\r\n wrapper: ClassValue;\r\n button: ClassValue;\r\n input: ClassValue;\r\n divider: ClassValue;\r\n icon: ClassValue;\r\n }>;\r\n readonly?: boolean;\r\n placeholder?: string;\r\n inputType?: \"digit\" | \"number\";\r\n customClass?: any;\r\n}\r\n\r\nconst props = withDefaults(defineProps<InputNumberProps>(), {\r\n modelValue: 0,\r\n defaultValue: 0,\r\n min: 0,\r\n max: 200,\r\n step: 1,\r\n disabled: false,\r\n ui: () => ({}),\r\n size: \"md\",\r\n color: \"primary\",\r\n shape: \"square\",\r\n placeholder: \"\",\r\n inputType: \"number\",\r\n readonly: true,\r\n});\r\n\r\nconst emit = defineEmits([\"update:modelValue\", \"change\", \"input\", \"blur\", \"focus\"]);\r\n\r\nconst longPress = useLongPress();\r\nconst { orientation, size: fieldGroupSize, disabled: fieldGroupDisabled } = useFormInject(props);\r\n\r\nconst uiOverrides = computed(() => props.ui || {});\r\nconst b = tv(theme);\r\nconst size = toRef(props, \"size\");\r\nconst ui = computed(() => {\r\n const styles = b({\r\n size: (fieldGroupSize.value || size.value) as any,\r\n color: props.color,\r\n shape: props.shape,\r\n fieldGroup: orientation.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 button: (opts?: { class?: any }) => styles.button({ class: cn(opts?.class, uiOverrides.value.button) }),\r\n input: (opts?: { class?: any }) => styles.input({ class: cn(opts?.class, uiOverrides.value.input) }),\r\n divider: (opts?: { class?: any }) => styles.divider({ class: cn(opts?.class, uiOverrides.value.divider) }),\r\n icon: (opts?: { class?: any }) => styles.icon({ class: cn(opts?.class, uiOverrides.value.icon) }),\r\n };\r\n});\r\n\r\nconst value = ref(props.modelValue);\r\n\r\nconst isPlus = computed(() => !fieldGroupDisabled.value && value.value < props.max);\r\nconst isMinus = computed(() => !fieldGroupDisabled.value && value.value > props.min);\r\n\r\nfunction update() {\r\n nextTick(() => {\r\n let val = value.value;\r\n\r\n if (val < props.min) val = props.min;\r\n if (val > props.max) val = props.max;\r\n if (props.min > props.max) val = props.max;\r\n\r\n if (props.inputType == \"digit\") {\r\n val = parseFloat(val.toFixed(2));\r\n }\r\n\r\n value.value = val;\r\n\r\n if (val != props.modelValue) {\r\n emit(\"update:modelValue\", val);\r\n emit(\"change\", val);\r\n }\r\n });\r\n}\r\n\r\nfunction onInput(e: any) {\r\n const val = parseFloat(e.detail.value);\r\n if (!isNaN(val)) {\r\n // We update local value but don't force emit update:modelValue instantly \r\n // to allow typing (e.g. typing \"10\" not being clamped to max \"5\" immediately if undesired)\r\n // However, for strict input number checks, we might want to clamp or just update local ref.\r\n value.value = val;\r\n }\r\n emit(\"input\", e);\r\n}\r\n\r\nfunction onPlus() {\r\n if (fieldGroupDisabled.value || !isPlus.value) return;\r\n\r\n longPress.start(() => {\r\n if (isPlus.value) {\r\n const val = props.max - value.value;\r\n value.value += val > props.step ? props.step : val;\r\n update();\r\n }\r\n });\r\n}\r\n\r\nfunction onMinus() {\r\n if (fieldGroupDisabled.value || !isMinus.value) return;\r\n\r\n longPress.start(() => {\r\n if (isMinus.value) {\r\n const val = value.value - props.min;\r\n value.value -= val > props.step ? props.step : val;\r\n update();\r\n }\r\n });\r\n}\r\n\r\nfunction onBlur(e: any) {\r\n if (e.detail.value == \"\") {\r\n value.value = props.min || 0;\r\n } else {\r\n value.value = parseFloat(e.detail.value);\r\n }\r\n update();\r\n emit(\"blur\", e);\r\n}\r\n\r\nwatch(\r\n () => props.modelValue,\r\n (val) => {\r\n if (val !== undefined && val !== value.value) {\r\n value.value = val;\r\n }\r\n },\r\n { immediate: true }\r\n);\r\n\r\nwatch(() => props.max, update);\r\nwatch(() => props.min, update);\r\n</script>\r\n",
39
+ "content": "<script lang=\"ts\" setup>\r\nimport type { ClassValue } from 'clsx'\r\nimport type { inputNumberColors, inputNumberShapes, inputNumberSizes } from './reborn-input-number.config'\r\nimport { computed, nextTick, ref, toRef, watch } from 'vue'\r\n\r\nimport { useFormInject } from '@/composables/useFieldGroup'\r\nimport { tv } from '@/lib/tv'\r\nimport { cn } from '@/lib/utils'\r\nimport { useLongPress } from './long-press'\r\nimport theme from './reborn-input-number.config'\r\n\r\ndefineOptions({\r\n name: 'ReInputNumber',\r\n inheritAttrs: false,\r\n})\r\n\r\nconst props = withDefaults(defineProps<InputNumberProps>(), {\r\n modelValue: 0,\r\n defaultValue: 0,\r\n min: 0,\r\n max: 200,\r\n step: 1,\r\n disabled: false,\r\n ui: () => ({}),\r\n size: 'md',\r\n color: 'primary',\r\n shape: 'square',\r\n placeholder: '',\r\n inputType: 'number',\r\n readonly: true,\r\n})\r\n\r\nconst emit = defineEmits(['update:modelValue', 'change', 'input', 'blur', 'focus'])\r\n\r\nexport interface InputNumberProps {\r\n modelValue?: number\r\n defaultValue?: number\r\n min?: number\r\n max?: number\r\n step?: number\r\n disabled?: boolean\r\n size?: typeof inputNumberSizes[number]\r\n color?: typeof inputNumberColors[number]\r\n shape?: typeof inputNumberShapes[number]\r\n ui?: Partial<{\r\n wrapper: ClassValue\r\n button: ClassValue\r\n input: ClassValue\r\n divider: ClassValue\r\n icon: ClassValue\r\n }>\r\n readonly?: boolean\r\n placeholder?: string\r\n inputType?: 'digit' | 'number'\r\n customClass?: any\r\n}\r\n\r\nconst longPress = useLongPress()\r\nconst { orientation, size: fieldGroupSize, disabled: fieldGroupDisabled, isError, validate } = useFormInject(props)\r\n\r\nconst uiOverrides = computed(() => props.ui || {})\r\nconst b = tv(theme)\r\nconst size = toRef(props, 'size')\r\nconst ui = computed(() => {\r\n const styles = b({\r\n size: (fieldGroupSize.value || size.value) as any,\r\n color: props.color,\r\n shape: props.shape,\r\n fieldGroup: orientation.value,\r\n error: isError.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 button: (opts?: { class?: any }) => styles.button({ class: cn(opts?.class, uiOverrides.value.button) }),\r\n input: (opts?: { class?: any }) => styles.input({ class: cn(opts?.class, uiOverrides.value.input) }),\r\n divider: (opts?: { class?: any }) => styles.divider({ class: cn(opts?.class, uiOverrides.value.divider) }),\r\n icon: (opts?: { class?: any }) => styles.icon({ class: cn(opts?.class, uiOverrides.value.icon) }),\r\n }\r\n})\r\n\r\nconst value = ref(props.modelValue)\r\n\r\nconst isPlus = computed(() => !fieldGroupDisabled.value && value.value < props.max)\r\nconst isMinus = computed(() => !fieldGroupDisabled.value && value.value > props.min)\r\n\r\nfunction update() {\r\n nextTick(() => {\r\n let val = value.value\r\n\r\n if (val < props.min) { val = props.min }\r\n if (val > props.max) { val = props.max }\r\n if (props.min > props.max) { val = props.max }\r\n\r\n if (props.inputType == 'digit') {\r\n val = Number.parseFloat(val.toFixed(2))\r\n }\r\n\r\n value.value = val\r\n\r\n if (val != props.modelValue) {\r\n emit('update:modelValue', val)\r\n emit('change', val)\r\n if (validate) { validate('change') }\r\n }\r\n })\r\n}\r\n\r\nfunction onInput(e: any) {\r\n const val = Number.parseFloat(e.detail.value)\r\n if (!isNaN(val)) {\r\n value.value = val\r\n }\r\n emit('input', e)\r\n}\r\n\r\nfunction onPlus() {\r\n if (fieldGroupDisabled.value || !isPlus.value) { return }\r\n\r\n longPress.start(() => {\r\n if (isPlus.value) {\r\n const val = props.max - value.value\r\n value.value += val > props.step ? props.step : val\r\n update()\r\n }\r\n })\r\n}\r\n\r\nfunction onMinus() {\r\n if (fieldGroupDisabled.value || !isMinus.value) { return }\r\n\r\n longPress.start(() => {\r\n if (isMinus.value) {\r\n const val = value.value - props.min\r\n value.value -= val > props.step ? props.step : val\r\n update()\r\n }\r\n })\r\n}\r\n\r\nfunction onBlur(e: any) {\r\n if (e.detail.value == '') {\r\n value.value = props.min || 0\r\n }\r\n else {\r\n value.value = Number.parseFloat(e.detail.value)\r\n }\r\n update()\r\n emit('blur', e)\r\n if (validate) { validate('blur') }\r\n}\r\n\r\nwatch(\r\n () => props.modelValue,\r\n (val) => {\r\n if (val !== undefined && val !== value.value) {\r\n value.value = val\r\n }\r\n },\r\n { immediate: true },\r\n)\r\n\r\nwatch(() => props.max, update)\r\nwatch(() => props.min, update)\r\n</script>\r\n\r\n<template>\r\n <view :class=\"ui.wrapper({ class: props.customClass })\" :data-disabled=\"fieldGroupDisabled\">\r\n <view :class=\"ui.button()\" @touchstart=\"onMinus\" @touchend=\"longPress.stop\" @touchcancel=\"longPress.stop\">\r\n <slot name=\"decrease-icon\">\r\n <view :class=\"ui.icon()\" class=\"i-lucide-minus\" />\r\n </slot>\r\n </view>\r\n\r\n <view :class=\"ui.divider()\" />\r\n\r\n <input :class=\"ui.input()\" :type=\"inputType\" :value=\"value\" :disabled=\"fieldGroupDisabled\" :readonly=\"!readonly\"\r\n :placeholder=\"placeholder\" @input=\"onInput\" @blur=\"onBlur\">\r\n\r\n <view :class=\"ui.divider()\" />\r\n\r\n <view :class=\"ui.button()\" @touchstart=\"onPlus\" @touchend=\"longPress.stop\" @touchcancel=\"longPress.stop\">\r\n <slot name=\"increase-icon\">\r\n <view :class=\"ui.icon()\" class=\"i-lucide-plus\" />\r\n </slot>\r\n </view>\r\n </view>\r\n</template>\r\n",
34
40
  "target": "uniapp"
35
41
  }
36
42
  ],
37
- "fileCount": 6,
38
- "contentHash": "9a0a60703a5d6db6426fa6f559eac3b307b93b90"
43
+ "fileCount": 7,
44
+ "contentHash": "c1c1ddca9b78b84819d23be221823b3311d19e2e"
39
45
  }
@@ -0,0 +1,40 @@
1
+ {
2
+ "name": "reborn-input-otp",
3
+ "dependencies": [
4
+ "clsx"
5
+ ],
6
+ "files": [
7
+ {
8
+ "path": "index.ts",
9
+ "content": "export { default as RebornInputOtp } from \"./RebornInputOtp.vue\";\r\n",
10
+ "target": "web"
11
+ },
12
+ {
13
+ "path": "reborn-input-otp.config.ts",
14
+ "content": "const sizes = [\"sm\", \"md\", \"lg\"] as const;\r\nconst colors = [\"primary\", \"secondary\", \"success\", \"info\", \"warning\", \"error\", \"neutral\"] as const;\r\n\r\nexport { sizes as inputOtpSizes, colors as inputOtpColors };\r\n\r\nexport default {\r\n slots: {\r\n root: \"relative inline-flex items-center\",\r\n inner: \"absolute top-0 h-full z-10 opacity-0 w-full left-0\",\r\n list: \"flex flex-row relative gap-1\",\r\n item: \"flex flex-row items-center justify-center duration-100 border border-solid border-gray-3 dark:border-gray-6 rounded-lg bg-gray-1 dark:bg-gray-8\",\r\n value: \"text-inherit font-medium\",\r\n cursor: \"absolute w-[1px] h-[60%]\",\r\n },\r\n variants: {\r\n size: {\r\n sm: { item: \"h-8 w-8 text-xs\" },\r\n md: { item: \"h-10 w-10 text-sm\" },\r\n lg: { item: \"h-12 w-12 text-base\" },\r\n },\r\n color: {\r\n primary: {\r\n item: \"data-[active=true]:border-primary data-[active=true]:ring-2 data-[active=true]:ring-primary/20 data-[active=true]:text-primary\",\r\n cursor: \"bg-primary\",\r\n },\r\n secondary: {\r\n item: \"data-[active=true]:border-secondary data-[active=true]:ring-2 data-[active=true]:ring-secondary/20 data-[active=true]:text-secondary\",\r\n cursor: \"bg-secondary\",\r\n },\r\n success: {\r\n item: \"data-[active=true]:border-success data-[active=true]:ring-2 data-[active=true]:ring-success/20 data-[active=true]:text-success\",\r\n cursor: \"bg-success\",\r\n },\r\n info: {\r\n item: \"data-[active=true]:border-info data-[active=true]:ring-2 data-[active=true]:ring-info/20 data-[active=true]:text-info\",\r\n cursor: \"bg-info\",\r\n },\r\n warning: {\r\n item: \"data-[active=true]:border-warning data-[active=true]:ring-2 data-[active=true]:ring-warning/20 data-[active=true]:text-warning\",\r\n cursor: \"bg-warning\",\r\n },\r\n error: {\r\n item: \"data-[active=true]:border-error data-[active=true]:ring-2 data-[active=true]:ring-error/20 data-[active=true]:text-error\",\r\n cursor: \"bg-error\",\r\n },\r\n neutral: {\r\n item: \"data-[active=true]:border-neutral data-[active=true]:ring-2 data-[active=true]:ring-neutral/20 data-[active=true]:text-neutral\",\r\n cursor: \"bg-neutral\",\r\n },\r\n },\r\n disabled: {\r\n true: {\r\n root: \"opacity-50 pointer-events-none\",\r\n item: \"bg-gray-100 dark:bg-gray-700\",\r\n },\r\n },\r\n },\r\n defaultVariants: {\r\n size: \"md\" as (typeof sizes)[number],\r\n color: \"primary\" as (typeof colors)[number],\r\n },\r\n};\r\n",
15
+ "target": "web"
16
+ },
17
+ {
18
+ "path": "RebornInputOtp.vue",
19
+ "content": "<script setup lang=\"ts\">\r\nimport { computed, nextTick, onMounted, ref, watch } from \"vue\";\r\nimport type { ClassValue } from \"clsx\";\r\nimport { cn } from \"~/lib/utils\";\r\nimport theme, { inputOtpColors, inputOtpSizes } from \"./reborn-input-otp.config\";\r\nimport { tv } from \"~/lib/tv\";\r\n\r\nconst b = tv(theme);\r\n\r\ndefineOptions({ inheritAttrs: false });\r\n\r\nexport interface InputOtpProps {\r\n modelValue?: string;\r\n autofocus?: boolean;\r\n length?: number;\r\n disabled?: boolean;\r\n inputType?: \"text\" | \"number\";\r\n size?: (typeof inputOtpSizes)[number];\r\n color?: (typeof inputOtpColors)[number];\r\n class?: any;\r\n ui?: Partial<{\r\n root: ClassValue;\r\n inner: ClassValue;\r\n list: ClassValue;\r\n item: ClassValue;\r\n value: ClassValue;\r\n cursor: ClassValue;\r\n }>;\r\n}\r\n\r\nconst props = withDefaults(defineProps<InputOtpProps>(), {\r\n modelValue: \"\",\r\n autofocus: false,\r\n length: 4,\r\n disabled: false,\r\n inputType: \"number\",\r\n size: \"md\",\r\n color: \"primary\",\r\n});\r\n\r\nconst emit = defineEmits<{\r\n (e: \"update:modelValue\", value: string): void;\r\n (e: \"done\", value: string): void;\r\n (e: \"focus\", event: FocusEvent): void;\r\n (e: \"blur\", event: FocusEvent): void;\r\n}>();\r\n\r\nconst inputRef = ref<HTMLInputElement | null>(null);\r\nconst isFocus = ref(false);\r\nconst value = ref(props.modelValue);\r\n\r\nconst uiOverrides = computed(() => props.ui || {});\r\n\r\nconst ui = computed(() => {\r\n const styles = b({\r\n color: props.color,\r\n size: props.size,\r\n disabled: props.disabled,\r\n });\r\n return {\r\n root: (opts?: { class?: any }) => styles.root({ class: cn(opts?.class, uiOverrides.value.root) }),\r\n inner: (opts?: { class?: any }) => styles.inner({ class: cn(opts?.class, uiOverrides.value.inner) }),\r\n list: (opts?: { class?: any }) => styles.list({ class: cn(opts?.class, uiOverrides.value.list) }),\r\n item: (opts?: { class?: any }) => styles.item({ class: cn(opts?.class, uiOverrides.value.item) }),\r\n value: (opts?: { class?: any }) => styles.value({ class: cn(opts?.class, uiOverrides.value.value) }),\r\n cursor: (opts?: { class?: any }) => styles.cursor({ class: cn(opts?.class, uiOverrides.value.cursor) }),\r\n };\r\n});\r\n\r\nconst list = computed<string[]>(() => {\r\n const arr: string[] = [];\r\n for (let i = 0; i < props.length; i++) {\r\n arr.push(value.value.charAt(i));\r\n }\r\n return arr;\r\n});\r\n\r\nfunction onInput(e: Event) {\r\n const target = e.target as HTMLInputElement;\r\n let val = target.value;\r\n if (props.inputType === \"number\") {\r\n val = val.replace(/\\D/g, \"\");\r\n }\r\n val = val.slice(0, props.length);\r\n value.value = val;\r\n target.value = val;\r\n emit(\"update:modelValue\", val);\r\n if (val.length === props.length) {\r\n emit(\"done\", val);\r\n inputRef.value?.blur();\r\n }\r\n}\r\n\r\nfunction onFocus(e: FocusEvent) {\r\n isFocus.value = true;\r\n emit(\"focus\", e);\r\n}\r\n\r\nfunction onBlur(e: FocusEvent) {\r\n isFocus.value = false;\r\n emit(\"blur\", e);\r\n}\r\n\r\nfunction onClick() {\r\n inputRef.value?.focus();\r\n}\r\n\r\nonMounted(() => {\r\n if (props.autofocus) {\r\n nextTick(() => inputRef.value?.focus());\r\n }\r\n});\r\n\r\nwatch(\r\n () => props.modelValue,\r\n (val) => {\r\n value.value = val;\r\n },\r\n);\r\n</script>\r\n\r\n<template>\r\n <div :class=\"ui.root({ class: props.class })\" @click=\"onClick\">\r\n <div :class=\"ui.inner()\">\r\n <input ref=\"inputRef\" :value=\"value\" :type=\"inputType === 'number' ? 'tel' : 'text'\" :maxlength=\"length\"\r\n :disabled=\"disabled\" :autofocus=\"autofocus\" autocomplete=\"one-time-code\" inputmode=\"numeric\"\r\n class=\"h-full w-full opacity-0\" @input=\"onInput\" @focus=\"onFocus\" @blur=\"onBlur\" />\r\n </div>\r\n <div :class=\"ui.list()\">\r\n <div v-for=\"(item, index) in list\" :key=\"index\" :class=\"ui.item()\"\r\n :data-active=\"value.length === index && isFocus\" :data-disabled=\"disabled\" @click=\"onClick\">\r\n <span :class=\"ui.value()\">{{ item }}</span>\r\n <span v-if=\"value.length === index && isFocus && item === ''\" class=\"otp-cursor\" :class=\"ui.cursor()\" />\r\n </div>\r\n </div>\r\n </div>\r\n</template>\r\n\r\n<style scoped>\r\n.otp-cursor {\r\n animation: blink 1s infinite;\r\n}\r\n\r\n@keyframes blink {\r\n 0% {\r\n opacity: 1;\r\n }\r\n\r\n 50% {\r\n opacity: 0;\r\n }\r\n}\r\n</style>\r\n",
20
+ "target": "web"
21
+ },
22
+ {
23
+ "path": "index.ts",
24
+ "content": "export { default as RebornInputOtp } from './RebornInputOtp.vue'\r\n",
25
+ "target": "uniapp"
26
+ },
27
+ {
28
+ "path": "reborn-input-otp.config.ts",
29
+ "content": "const sizes = ['sm', 'md', 'lg'] as const\r\nconst colors = ['primary', 'secondary', 'success', 'info', 'warning', 'error', 'neutral'] as const\r\n\r\nexport default {\r\n slots: {\r\n root: 'relative inline-flex items-center',\r\n inner: 'absolute top-0 h-full z-10 opacity-0 w-[200%] -left-full',\r\n list: 'flex flex-row relative gap-1',\r\n item: 'flex flex-row items-center justify-center duration-100 border border-solid border-gray-4 rounded-lg bg-gray-1 dark:bg-gray-8 ',\r\n value: 'text-inherit font-medium',\r\n cursor: 'absolute w-[1px] h-[60%]',\r\n },\r\n variants: {\r\n size: {\r\n sm: {\r\n item: 'h-8 w-8 text-xs',\r\n },\r\n md: {\r\n item: 'h-10 w-10 text-sm',\r\n },\r\n lg: {\r\n item: 'h-12 w-12 text-base',\r\n },\r\n },\r\n color: {\r\n primary: {\r\n item: 'data-[active=true]:border-primary data-[active=true]:ring-2 data-[active=true]:ring-primary/20 data-[active=true]:text-primary',\r\n cursor: 'bg-primary',\r\n },\r\n secondary: {\r\n item: 'data-[active=true]:border-secondary data-[active=true]:ring-2 data-[active=true]:ring-secondary/20 data-[active=true]:text-secondary',\r\n cursor: 'bg-secondary',\r\n },\r\n success: {\r\n item: 'data-[active=true]:border-success data-[active=true]:ring-2 data-[active=true]:ring-success/20 data-[active=true]:text-success',\r\n cursor: 'bg-success',\r\n },\r\n info: {\r\n item: 'data-[active=true]:border-info data-[active=true]:ring-2 data-[active=true]:ring-info/20 data-[active=true]:text-info',\r\n cursor: 'bg-info',\r\n },\r\n warning: {\r\n item: 'data-[active=true]:border-warning data-[active=true]:ring-2 data-[active=true]:ring-warning/20 data-[active=true]:text-warning',\r\n cursor: 'bg-warning',\r\n },\r\n error: {\r\n item: 'data-[active=true]:border-error data-[active=true]:ring-2 data-[active=true]:ring-error/20 data-[active=true]:text-error',\r\n cursor: 'bg-error',\r\n },\r\n neutral: {\r\n item: 'data-[active=true]:border-neutral data-[active=true]:ring-2 data-[active=true]:ring-neutral/20 data-[active=true]:text-neutral',\r\n cursor: 'bg-neutral',\r\n },\r\n },\r\n disabled: {\r\n true: {\r\n root: 'opacity-50 pointer-events-none',\r\n item: 'bg-muted',\r\n },\r\n },\r\n },\r\n defaultVariants: {\r\n size: 'md',\r\n color: 'primary',\r\n },\r\n} as const\r\n\r\nexport { colors as inputOtpColors, sizes as inputOtpSizes }\r\n",
30
+ "target": "uniapp"
31
+ },
32
+ {
33
+ "path": "RebornInputOtp.vue",
34
+ "content": "<script setup lang=\"ts\">\r\nimport type { inputOtpColors, inputOtpSizes } from './reborn-input-otp.config'\r\nimport type { AnimationEngine } from '@/lib/animation'\r\nimport { computed, nextTick, onMounted, ref, watch } from 'vue'\r\nimport RebornInput from '@/components/reborn-input/RebornInput.vue'\r\nimport { createAnimation } from '@/lib/animation'\r\nimport { tv } from '@/lib/tv'\r\n\r\nimport { cn } from '@/lib/utils'\r\nimport theme from './reborn-input-otp.config'\r\n\r\ndefineOptions({\r\n name: 'RebornInputOtp',\r\n})\r\n\r\nconst props = withDefaults(defineProps<{\r\n ui?: any\r\n customClass?: any\r\n modelValue?: string\r\n autofocus?: boolean\r\n length?: number\r\n disabled?: boolean\r\n inputType?: 'text' | 'number' | 'digit'\r\n size?: typeof inputOtpSizes[number]\r\n color?: typeof inputOtpColors[number]\r\n}>(), {\r\n modelValue: '',\r\n autofocus: false,\r\n length: 4,\r\n disabled: false,\r\n inputType: 'number',\r\n size: 'md',\r\n color: 'primary',\r\n ui: () => ({}),\r\n})\r\nconst emit = defineEmits(['update:modelValue', 'done', 'focus', 'blur'])\r\nconst b = tv(theme)\r\nconst inputRef = ref<InstanceType<typeof RebornInput> | null>(null)\r\n\r\nconst cursorRef = ref<any[]>([])\r\n\r\nconst value = ref(props.modelValue)\r\n\r\nconst ui = computed(() => {\r\n const style = b({\r\n color: props.color,\r\n size: props.size,\r\n disabled: props.disabled,\r\n })\r\n\r\n return {\r\n root: (opts?: { class?: any }) => style.root({ class: cn(opts?.class, props.ui?.root) }),\r\n inner: (opts?: { class?: any }) => style.inner({ class: cn(opts?.class, props.ui?.inner) }),\r\n list: (opts?: { class?: any }) => style.list({ class: cn(opts?.class, props.ui?.list) }),\r\n item: (opts?: { class?: any }) => style.item({ class: cn(opts?.class, props.ui?.item) }),\r\n value: (opts?: { class?: any }) => style.value({ class: cn(opts?.class, props.ui?.value) }),\r\n cursor: (opts?: { class?: any }) => style.cursor({ class: cn(opts?.class, props.ui?.cursor) }),\r\n }\r\n})\r\n\r\nconst isFocus = ref(false)\r\n\r\nconst list = computed<string[]>(() => {\r\n const arr = [] as string[]\r\n for (let i = 0; i < props.length; i++) {\r\n arr.push(value.value.charAt(i))\r\n }\r\n return arr\r\n})\r\n\r\nlet animationEngine: AnimationEngine | null = null\r\n\r\nfunction last<T>(array: T[]): T | null {\r\n return Array.isArray(array) && array.length > 0 ? array[array.length - 1] : null\r\n}\r\n\r\nasync function onCursor() {\r\n await nextTick()\r\n\r\n if (!cursorRef.value) {\r\n return\r\n }\r\n\r\n // #ifdef APP\r\n if (animationEngine != null) {\r\n animationEngine.stop()\r\n }\r\n\r\n const target = last(cursorRef.value)\r\n if (target) {\r\n animationEngine = createAnimation(target, {\r\n duration: 600,\r\n loop: -1,\r\n alternate: true,\r\n })\r\n .opacity('0', '1')\r\n .play()\r\n }\r\n // #endif\r\n}\r\n\r\nfunction onChange(val: string) {\r\n emit('update:modelValue', val)\r\n\r\n // 输入完成时触发done事件\r\n if (val.length == props.length) {\r\n uni.hideKeyboard()\r\n emit('done', val)\r\n }\r\n\r\n // 更新光标动画\r\n onCursor()\r\n}\r\n\r\nfunction onFocus(e: any) {\r\n isFocus.value = true\r\n emit('focus', e)\r\n onCursor()\r\n}\r\n\r\nfunction onBlur(e: any) {\r\n isFocus.value = false\r\n emit('blur', e)\r\n if (animationEngine) {\r\n animationEngine.stop()\r\n }\r\n}\r\n\r\nfunction onTap() {\r\n if (inputRef.value) {\r\n inputRef.value.focus()\r\n }\r\n onCursor()\r\n}\r\n\r\nonMounted(() => {\r\n if (props.autofocus) {\r\n isFocus.value = true\r\n nextTick(() => {\r\n onTap()\r\n })\r\n }\r\n\r\n watch(\r\n () => props.modelValue,\r\n (val: string) => {\r\n value.value = val\r\n if (val && isFocus.value) {\r\n onCursor()\r\n }\r\n },\r\n {\r\n immediate: true,\r\n },\r\n )\r\n})\r\n</script>\r\n\r\n<template>\r\n <view :class=\"ui.root({ class: props.customClass })\" @tap=\"onTap()\">\r\n <view :class=\"ui.inner()\">\r\n <RebornInput\r\n ref=\"inputRef\" v-model=\"value\" :type=\"inputType\" :maxlength=\"length\" :disabled=\"disabled\"\r\n :autofocus=\"autofocus\" :hold-keyboard=\"false\" :clearable=\"false\" customClass=\"!h-full\" @input=\"onChange\"\r\n @focus=\"onFocus\" @blur=\"onBlur\"\r\n />\r\n </view>\r\n <view :class=\"ui.list()\">\r\n <view\r\n v-for=\"(item, index) in list\" :key=\"index\" :class=\"ui.item()\"\r\n :data-active=\"value.length >= index && isFocus\" :data-disabled=\"disabled\" @tap=\"onTap\"\r\n >\r\n <text :class=\"ui.value()\" :style=\"{ color: value.length >= index && isFocus ? props.color : '' }\">\r\n {{ item }}\r\n </text>\r\n <view\r\n v-if=\"value.length == index && isFocus && item == ''\" ref=\"cursorRef\" class=\"\r\n cursor\r\n \"\r\n :class=\"ui.cursor()\"\r\n />\r\n </view>\r\n </view>\r\n </view>\r\n</template>\r\n\r\n<style lang=\"scss\" scoped>\r\n.cursor {\r\n // #ifndef APP\r\n animation: blink 1s infinite;\r\n\r\n @keyframes blink {\r\n 0% {\r\n opacity: 1;\r\n }\r\n\r\n 50% {\r\n opacity: 0;\r\n }\r\n }\r\n\r\n // #endif\r\n}\r\n</style>\r\n",
35
+ "target": "uniapp"
36
+ }
37
+ ],
38
+ "fileCount": 6,
39
+ "contentHash": "da75aa23d5452f08b22b47c6254586463a7a9c3c"
40
+ }
@@ -19,20 +19,20 @@
19
19
  },
20
20
  {
21
21
  "path": "index.ts",
22
- "content": "export { default as RebornInput } from \"./RebornInput.vue\";\r\n",
22
+ "content": "export { default as RebornInput } from './RebornInput.vue'\r\n",
23
23
  "target": "uniapp"
24
24
  },
25
25
  {
26
26
  "path": "reborn-input.config.ts",
27
- "content": "const sizes = [\"sm\", \"md\", \"lg\"] as const;\r\nconst colors = [\"primary\", \"secondary\", \"success\", \"info\", \"warning\", \"error\", \"neutral\"] as const;\r\n\r\nconst config = {\r\n slots: {\r\n wrapper: \"relative flex w-full items-center\",\r\n input:\r\n \"flex h-10 w-full rounded-md border border-gray-2 bg-gray-2 px-3 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50\",\r\n leading: \"absolute left-3 top-0 bottom-0 flex items-center justify-center text-muted-foreground\",\r\n trailing: \"absolute right-3 top-0 bottom-0 flex items-center justify-center text-muted-foreground\",\r\n // Styles for internal icons like clear and password toggle\r\n icon: \"absolute right-3 top-0 bottom-0 flex items-center justify-center text-muted-foreground transition-opacity hover:opacity-80 cursor-pointer z-10\",\r\n },\r\n variants: {\r\n size: {\r\n sm: {\r\n input: \"h-input-sm px-3\",\r\n },\r\n md: {\r\n input: \"h-input-md px-3\",\r\n },\r\n lg: {\r\n input: \"h-input-lg px-8 rounded-md\",\r\n },\r\n },\r\n color: {\r\n primary: {\r\n input: \"focus-within:ring-[1.5px] focus-within:ring-primary\",\r\n },\r\n secondary: {\r\n input: \"focus-within:ring-[1.5px] focus-within:ring-secondary\",\r\n },\r\n success: {\r\n input: \"focus-within:ring-[1.5px] focus-within:ring-success\",\r\n },\r\n info: {\r\n input: \"focus-within:ring-[1.5px] focus-within:ring-info\",\r\n },\r\n warning: {\r\n input: \"focus-within:ring-[1.5px] focus-within:ring-warning\",\r\n },\r\n error: {\r\n input: \"focus-within:ring-[1.5px] focus-within:ring-error\",\r\n },\r\n neutral: {\r\n input: \"focus-within:ring-[1.5px] focus-within:ring-gray-4\",\r\n },\r\n },\r\n multiline: {\r\n true: {\r\n input: \"h-auto\",\r\n },\r\n },\r\n fieldGroup: {\r\n horizontal: {\r\n wrapper: \"first:rounded-r-none last:rounded-l-none\",\r\n input: \"first:rounded-r-none last:rounded-l-none focus:z-10\",\r\n },\r\n vertical: {\r\n wrapper: \"first:rounded-b-none last:rounded-t-none\",\r\n input: \"first:rounded-b-none last:rounded-t-none focus:z-10\",\r\n },\r\n },\r\n hasLeading: {\r\n true: {\r\n input: \"pl-9\",\r\n },\r\n },\r\n hasTrailing: {\r\n true: {\r\n input: \"pr-9\",\r\n },\r\n },\r\n rounded: {\r\n true: {\r\n input: \"rounded-full\",\r\n },\r\n false: {\r\n input: \"rounded-md\",\r\n },\r\n },\r\n error: {\r\n true: {\r\n input: \"border-error text-error placeholder:text-error/50 focus-visible:ring-error focus-within:ring-error focus-within:border-error ring-error\",\r\n },\r\n },\r\n },\r\n defaultVariants: {\r\n size: \"md\",\r\n color: \"neutral\",\r\n rounded: true,\r\n },\r\n} as const;\r\n\r\nexport { sizes as inputSizes, colors as inputColors };\r\nexport default config;\r\n",
27
+ "content": "const sizes = ['sm', 'md', 'lg'] as const\r\nconst colors = ['primary', 'secondary', 'success', 'info', 'warning', 'error', 'neutral'] as const\r\n\r\nconst config = {\r\n slots: {\r\n wrapper: 'relative flex w-full items-center',\r\n input:\r\n 'flex h-10 w-full rounded-md border border-gray-2 dark:border-gray-7 bg-gray-2 dark:bg-gray-8 px-3 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50',\r\n leading: 'absolute left-3 top-0 bottom-0 flex items-center justify-center text-muted-foreground',\r\n trailing: 'absolute right-3 top-0 bottom-0 flex items-center justify-center text-muted-foreground',\r\n // Styles for internal icons like clear and password toggle\r\n icon: 'absolute right-3 top-0 bottom-0 flex items-center justify-center text-muted-foreground transition-opacity hover:opacity-80 cursor-pointer z-10',\r\n iconBox: 'absolute bottom-0 right-3 top-0 z-20 flex items-center gap-2',\r\n iconSection: 'flex cursor-pointer items-center justify-center p-1 text-muted-foreground transition-opacity hover:opacity-80',\r\n },\r\n variants: {\r\n size: {\r\n sm: {\r\n input: 'h-input-sm px-3',\r\n },\r\n md: {\r\n input: 'h-input-md px-3',\r\n },\r\n lg: {\r\n input: 'h-input-lg px-8 rounded-md',\r\n },\r\n },\r\n color: {\r\n primary: {\r\n input: 'focus-within:ring-[1.5px] focus-within:ring-primary',\r\n },\r\n secondary: {\r\n input: 'focus-within:ring-[1.5px] focus-within:ring-secondary',\r\n },\r\n success: {\r\n input: 'focus-within:ring-[1.5px] focus-within:ring-success',\r\n },\r\n info: {\r\n input: 'focus-within:ring-[1.5px] focus-within:ring-info',\r\n },\r\n warning: {\r\n input: 'focus-within:ring-[1.5px] focus-within:ring-warning',\r\n },\r\n error: {\r\n input: 'focus-within:ring-[1.5px] focus-within:ring-error',\r\n },\r\n neutral: {\r\n input: 'focus-within:ring-[1.5px] focus-within:ring-gray-4',\r\n },\r\n },\r\n multiline: {\r\n true: {\r\n input: 'h-auto',\r\n },\r\n },\r\n fieldGroup: {\r\n horizontal: {\r\n wrapper: 'first:rounded-r-none last:rounded-l-none',\r\n input: 'first:rounded-r-none last:rounded-l-none focus:z-10',\r\n },\r\n vertical: {\r\n wrapper: 'first:rounded-b-none last:rounded-t-none',\r\n input: 'first:rounded-b-none last:rounded-t-none focus:z-10',\r\n },\r\n },\r\n hasLeading: {\r\n true: {\r\n input: 'pl-9',\r\n },\r\n },\r\n hasTrailing: {\r\n true: {\r\n input: 'pr-9',\r\n },\r\n },\r\n rounded: {\r\n true: {\r\n input: 'rounded-full',\r\n },\r\n false: {\r\n input: 'rounded-md',\r\n },\r\n },\r\n error: {\r\n true: {\r\n input: 'border-error text-error placeholder:text-error/50 focus-visible:ring-error focus-within:ring-error focus-within:border-error ring-error',\r\n },\r\n },\r\n },\r\n defaultVariants: {\r\n size: 'md',\r\n color: 'neutral',\r\n rounded: true,\r\n },\r\n} as const\r\n\r\nexport { colors as inputColors, sizes as inputSizes }\r\nexport default config\r\n",
28
28
  "target": "uniapp"
29
29
  },
30
30
  {
31
31
  "path": "RebornInput.vue",
32
- "content": "<script lang=\"ts\">\r\nimport { inputSizes, inputColors } from \"./reborn-input.config\";\r\nexport type InputType =\r\n | \"text\"\r\n | \"number\"\r\n | \"idcard\"\r\n | \"digit\"\r\n | \"tel\"\r\n | \"safe-password\"\r\n | \"nickname\"\r\n | \"none\"\r\n | \"decimal\"\r\n | \"numeric\"\r\n | \"search\"\r\n | \"email\"\r\n | \"url\";\r\n\r\nexport interface InputProps {\r\n modelValue?: string | number;\r\n defaultValue?: string | number;\r\n placeholder?: string;\r\n disabled?: boolean;\r\n readonly?: boolean;\r\n type?: InputType;\r\n size?: typeof inputSizes[number];\r\n rows?: number;\r\n customClass?: any;\r\n password?: boolean;\r\n clearable?: boolean;\r\n focus?: boolean;\r\n maxlength?: number;\r\n cursorSpacing?: number;\r\n confirmHold?: boolean;\r\n confirmType?: string;\r\n adjustPosition?: boolean;\r\n holdKeyboard?: boolean;\r\n placeholderClass?: string;\r\n autofocus?: boolean;\r\n rounded?: boolean;\r\n color?: typeof inputColors[number];\r\n}\r\n</script>\r\n\r\n<script setup lang=\"ts\">\r\nimport { computed, nextTick, ref, toRef, useSlots, watch } from \"vue\";\r\nimport { tv } from \"@/lib/tv\";\r\nimport theme from \"./reborn-input.config\";\r\nimport { useFormInject } from \"@/composables/useFieldGroup\";\r\n\r\nconst b = tv(theme);\r\nconst slots = useSlots();\r\n\r\ndefineOptions({\r\n inheritAttrs: false,\r\n});\r\n\r\n\r\nconst props = withDefaults(defineProps<InputProps>(), {\r\n disabled: false,\r\n readonly: false,\r\n type: \"text\",\r\n size: \"md\",\r\n rows: 4,\r\n focus: false,\r\n password: false,\r\n maxlength: 140,\r\n cursorSpacing: 5,\r\n confirmHold: false,\r\n confirmType: \"done\",\r\n adjustPosition: true,\r\n holdKeyboard: false,\r\n placeholderClass: \"\",\r\n autofocus: false,\r\n clearable: false,\r\n rounded: true,\r\n color: \"primary\",\r\n});\r\n\r\nconst emit = defineEmits([\r\n \"update:modelValue\",\r\n \"input\",\r\n \"change\",\r\n \"focus\",\r\n \"blur\",\r\n \"confirm\",\r\n \"clear\",\r\n \"keyboardheightchange\"\r\n]);\r\n\r\nconst localValue = ref(props.defaultValue ?? \"\");\r\n\r\n// 是否聚焦(样式作用)\r\nconst isFocus = ref<boolean>(props.autofocus);\r\n\r\n// 是否聚焦(输入框作用)\r\nconst isFocusing = ref<boolean>(props.autofocus);\r\n\r\n// 是否显示密码\r\nconst isPassword = ref(props.password);\r\n\r\nconst inputValue = computed(() =>\r\n props.modelValue !== undefined ? props.modelValue : localValue.value,\r\n);\r\n\r\n// 是否显示清除按钮\r\nconst showClear = computed(() => {\r\n return props.clearable && !fieldGroupDisabled.value && !props.readonly && `${inputValue.value}` !== \"\";\r\n});\r\n\r\n\r\nconst isFilled = computed(() => `${inputValue.value ?? \"\"}`.length > 0);\r\n\r\nconst { orientation, size: fieldGroupSize, disabled: fieldGroupDisabled, isError } = useFormInject(props);\r\n\r\nconst size = toRef(props, \"size\");\r\n\r\nconst ui = computed(() =>\r\n b({\r\n size: (fieldGroupSize.value || size.value) as any,\r\n fieldGroup: orientation.value,\r\n hasLeading: !!slots.leading,\r\n hasTrailing: !!slots.trailing || showClear.value || props.password,\r\n rounded: props.rounded,\r\n color: props.color,\r\n error: isError.value,\r\n }),\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\n// 输入事件\r\nfunction onInput(e: any) {\r\n const v1 = e.detail.value;\r\n localValue.value = v1; // Update local value for uncontrolled usage\r\n\r\n emit(\"update:modelValue\", v1);\r\n emit(\"input\", e);\r\n emit(\"change\", v1);\r\n}\r\n\r\n// 点击确认按钮事件\r\nfunction onConfirm(e: any) {\r\n emit(\"confirm\", e);\r\n}\r\n\r\n// 键盘高度变化事件\r\nfunction onKeyboardheightchange(e: any) {\r\n emit(\"keyboardheightchange\", e);\r\n}\r\n\r\n// 聚焦方法\r\nfunction focus() {\r\n if (fieldGroupDisabled.value || props.readonly) {\r\n isFocusing.value = false;\r\n return\r\n };\r\n\r\n setTimeout(() => {\r\n isFocusing.value = false;\r\n\r\n nextTick(() => {\r\n isFocusing.value = true;\r\n });\r\n }, 0);\r\n}\r\n\r\n// 获取焦点事件\r\nfunction onFocus(e: any) {\r\n if (fieldGroupDisabled.value || props.readonly) return;\r\n isFocus.value = true;\r\n emit(\"focus\", e);\r\n}\r\n\r\nfunction onBlur(e: any) {\r\n isFocus.value = false;\r\n emit(\"blur\", e);\r\n}\r\n// 切换密码显示状态\r\nfunction showPassword() {\r\n if (fieldGroupDisabled.value || props.readonly) return;\r\n isPassword.value = !isPassword.value;\r\n}\r\n// 清除方法\r\nfunction clear() {\r\n localValue.value = \"\";\r\n emit(\"update:modelValue\", \"\");\r\n emit(\"change\", \"\");\r\n emit(\"clear\");\r\n\r\n // #ifdef H5\r\n focus();\r\n // #endif\r\n}\r\n\r\ndefineExpose({\r\n isFocus,\r\n focus,\r\n clear\r\n});\r\n</script>\r\n\r\n<template>\r\n <view :class=\"ui.wrapper({ class: props.customClass })\" :data-disabled=\"fieldGroupDisabled\" :data-filled=\"isFilled\"\r\n @click=\"focus\">\r\n <view v-if=\"$slots.leading\" :class=\"ui.leading()\">\r\n <slot name=\"leading\" :ui=\"ui\" />\r\n </view>\r\n\r\n <input :type=\"isPassword ? 'password' : props.type\" :disabled=\"fieldGroupDisabled || props.readonly\"\r\n :readonly=\"props.readonly\" :placeholder=\"props.placeholder\" :value=\"inputValue\" :class=\"ui.input()\"\r\n :password=\"isPassword\" :focus=\"props.focus && !fieldGroupDisabled && !props.readonly\"\r\n :placeholder-class=\"`text-gart-4 ${props.placeholderClass}`\" :maxlength=\"props.maxlength\"\r\n :cursor-spacing=\"props.cursorSpacing\" :confirm-type=\"props.confirmType\" :confirm-hold=\"props.confirmHold\"\r\n :adjust-position=\"props.adjustPosition\" :hold-keyboard=\"props.holdKeyboard\" @input=\"onInput\" @focus=\"onFocus\"\r\n @blur=\"onBlur\" @confirm=\"onConfirm\" @keyboardheightchange=\"onKeyboardheightchange\" />\r\n\r\n <view v-if=\"$slots.trailing\" :class=\"ui.trailing()\">\r\n <slot name=\"trailing\" :ui=\"ui\" />\r\n </view>\r\n\r\n <!-- Icons Section -->\r\n <view class=\"absolute right-3 top-0 bottom-0 flex items-center gap-2 z-20\">\r\n <view v-if=\"showClear\"\r\n class=\"flex items-center justify-center text-muted-foreground transition-opacity hover:opacity-80 cursor-pointer p-1\"\r\n @tap.stop=\"clear\">\r\n <view class=\"i-lucide-x-circle size-4\" />\r\n </view>\r\n\r\n <view v-if=\"password\"\r\n class=\"flex items-center justify-center text-muted-foreground transition-opacity hover:opacity-80 cursor-pointer p-1\"\r\n @tap.stop=\"showPassword\">\r\n <view :class=\"[isPassword ? 'i-lucide-eye' : 'i-lucide-eye-off', 'size-4']\" />\r\n </view>\r\n </view>\r\n </view>\r\n</template>\r\n",
32
+ "content": "<script lang=\"ts\">\r\nimport type { inputColors, inputSizes } from './reborn-input.config'\r\n</script>\r\n\r\n<script setup lang=\"ts\">\r\nimport { computed, nextTick, ref, toRef, useSlots, 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-input.config'\r\n\r\nexport type InputType\r\n = | 'text'\r\n | 'number'\r\n | 'idcard'\r\n | 'digit'\r\n | 'tel'\r\n | 'safe-password'\r\n | 'nickname'\r\n | 'none'\r\n | 'decimal'\r\n | 'numeric'\r\n | 'search'\r\n | 'email'\r\n | 'url'\r\n\r\nexport type InputUI = {\r\n wrapper?: string\r\n input?: string\r\n leading?: string\r\n trailing?: string\r\n iconBox?: string\r\n clear?: string\r\n password?: string\r\n}\r\n\r\nexport interface InputProps {\r\n modelValue?: string | number\r\n defaultValue?: string | number\r\n placeholder?: string\r\n disabled?: boolean\r\n readonly?: boolean\r\n type?: InputType\r\n size?: typeof inputSizes[number]\r\n rows?: number\r\n customClass?: any\r\n password?: boolean\r\n clearable?: boolean\r\n focus?: boolean\r\n maxlength?: number\r\n cursorSpacing?: number\r\n confirmHold?: boolean\r\n confirmType?: string\r\n adjustPosition?: boolean\r\n holdKeyboard?: boolean\r\n placeholderClass?: string\r\n autofocus?: boolean\r\n rounded?: boolean\r\n color?: typeof inputColors[number]\r\n ui?: InputUI\r\n}\r\n\r\ndefineOptions({\r\n inheritAttrs: false,\r\n})\r\nconst props = withDefaults(defineProps<InputProps>(), {\r\n disabled: false,\r\n readonly: false,\r\n type: 'text',\r\n size: 'md',\r\n rows: 4,\r\n focus: false,\r\n password: false,\r\n maxlength: 140,\r\n cursorSpacing: 5,\r\n confirmHold: false,\r\n confirmType: 'done',\r\n adjustPosition: true,\r\n holdKeyboard: false,\r\n placeholderClass: '',\r\n autofocus: false,\r\n clearable: false,\r\n rounded: true,\r\n color: 'primary',\r\n})\r\nconst emit = defineEmits([\r\n 'update:modelValue',\r\n 'input',\r\n 'change',\r\n 'focus',\r\n 'blur',\r\n 'confirm',\r\n 'clear',\r\n 'keyboardheightchange',\r\n])\r\nconst slots = useSlots()\r\n\r\nconst inputRef = ref<HTMLInputElement | null>(null)\r\nconst localValue = ref(props.defaultValue ?? '')\r\n\r\n// 是否聚焦(样式作用)\r\nconst isFocus = ref<boolean>(props.autofocus)\r\n\r\n// 是否聚焦(输入框作用)\r\nconst isFocusing = ref<boolean>(props.autofocus)\r\n\r\n// 是否显示密码\r\nconst isPassword = ref(props.password)\r\n\r\nconst inputValue = computed(() =>\r\n props.modelValue !== undefined ? props.modelValue : localValue.value,\r\n)\r\n\r\n// 是否显示清除按钮\r\nconst showClear = computed(() => {\r\n return props.clearable && !fieldGroupDisabled.value && !props.readonly && `${inputValue.value}` !== ''\r\n})\r\n\r\nconst isFilled = computed(() => `${inputValue.value ?? ''}`.length > 0)\r\n\r\nconst { orientation, size: fieldGroupSize, disabled: fieldGroupDisabled, isError, validate } = useFormInject(props)\r\n\r\nconst size = toRef(props, 'size')\r\n\r\nconst b = tv(theme)\r\nconst uiOverrides = computed(() => props.ui || {})\r\nconst ui = computed(() => {\r\n const styles = b({\r\n size: (fieldGroupSize.value || size.value) as any,\r\n fieldGroup: orientation.value,\r\n hasLeading: !!slots.leading,\r\n hasTrailing: !!slots.trailing || showClear.value || props.password,\r\n rounded: props.rounded,\r\n color: props.color,\r\n error: isError.value,\r\n })\r\n return {\r\n wrapper: (opts?: { class?: any }) =>\r\n styles.wrapper({ class: cn(opts?.class, uiOverrides.value.wrapper) }),\r\n input: (opts?: { class?: any }) =>\r\n styles.input({ class: cn(opts?.class, uiOverrides.value.input) }),\r\n leading: (opts?: { class?: any }) =>\r\n styles.leading({ class: cn(opts?.class, uiOverrides.value.leading) }),\r\n trailing: (opts?: { class?: any }) =>\r\n styles.trailing({ class: cn(opts?.class, uiOverrides.value.trailing) }),\r\n iconBox: (opts?: { class?: any }) =>\r\n styles.iconBox({ class: cn(opts?.class, uiOverrides.value.iconBox) }),\r\n clear: (opts?: { class?: any }) =>\r\n styles.iconSection({ class: cn(opts?.class, uiOverrides.value.clear) }),\r\n password: (opts?: { class?: any }) =>\r\n styles.iconSection({ class: cn(opts?.class, uiOverrides.value.password) }),\r\n }\r\n}\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\n// 输入事件\r\nfunction onInput(e: any) {\r\n const v1 = e.detail.value\r\n localValue.value = v1 // Update local value for uncontrolled usage\r\n emit('update:modelValue', v1)\r\n emit('input', v1)\r\n emit('change', v1)\r\n if (validate) { validate('change') }\r\n}\r\n\r\n// 点击确认按钮事件\r\nfunction onConfirm(e: any) {\r\n emit('confirm', e)\r\n}\r\n\r\n// 键盘高度变化事件\r\nfunction onKeyboardheightchange(e: any) {\r\n emit('keyboardheightchange', e)\r\n}\r\n\r\n// 聚焦方法\r\nfunction focus() {\r\n if (fieldGroupDisabled.value || props.readonly) {\r\n isFocusing.value = false\r\n return\r\n };\r\n\r\n setTimeout(() => {\r\n isFocusing.value = false\r\n\r\n nextTick(() => {\r\n isFocusing.value = true\r\n })\r\n }, 0)\r\n}\r\n\r\n// 获取焦点事件\r\nfunction onFocus(e: any) {\r\n if (fieldGroupDisabled.value || props.readonly) { return }\r\n isFocus.value = true\r\n emit('focus', e)\r\n}\r\n\r\nfunction onBlur(e: any) {\r\n isFocus.value = false\r\n emit('blur', e)\r\n if (validate) { validate('blur') }\r\n}\r\n// 切换密码显示状态\r\nfunction showPassword() {\r\n if (fieldGroupDisabled.value || props.readonly) { return }\r\n isPassword.value = !isPassword.value\r\n nextTick(() => focus())\r\n}\r\n// 清除方法\r\nfunction clear() {\r\n localValue.value = ''\r\n emit('update:modelValue', '')\r\n emit('change', '')\r\n emit('clear')\r\n\r\n // #ifdef H5\r\n focus()\r\n // #endif\r\n}\r\n\r\ndefineExpose({\r\n isFocus,\r\n focus,\r\n clear,\r\n})\r\n</script>\r\n\r\n<template>\r\n <view :class=\"ui.wrapper({ class: props.customClass })\" :data-disabled=\"fieldGroupDisabled\" :data-filled=\"isFilled\"\r\n @click=\"focus\">\r\n <view v-if=\"$slots.leading\" :class=\"ui.leading()\">\r\n <slot name=\"leading\" :ui=\"ui\" />\r\n </view>\r\n\r\n <input ref=\"inputRef\" :type=\"props.type\" :disabled=\"fieldGroupDisabled || props.readonly\" :readonly=\"props.readonly\"\r\n :placeholder=\"props.placeholder\" :value=\"inputValue\" :class=\"ui.input()\" :password=\"isPassword\"\r\n :focus=\"isFocusing && !fieldGroupDisabled && !props.readonly\"\r\n :placeholder-class=\"`text-gart-4 ${props.placeholderClass}`\" :maxlength=\"props.maxlength\"\r\n :cursor-spacing=\"props.cursorSpacing\" :confirm-type=\"props.confirmType\" :confirm-hold=\"props.confirmHold\"\r\n :adjust-position=\"props.adjustPosition\" :hold-keyboard=\"props.holdKeyboard\" @input=\"onInput\" @focus=\"onFocus\"\r\n @blur=\"onBlur\" @confirm=\"onConfirm\" @keyboardheightchange=\"onKeyboardheightchange\">\r\n\r\n <view v-if=\"$slots.trailing\" :class=\"ui.trailing()\">\r\n <slot name=\"trailing\" :ui=\"ui\" />\r\n </view>\r\n\r\n <!-- Icons Section -->\r\n <view :class=\"ui.iconBox()\" @tap.stop>\r\n <view v-if=\"showClear\" :class=\"ui.clear()\" @tap.stop=\"clear\">\r\n <view class=\"i-lucide-x-circle size-4\" />\r\n </view>\r\n\r\n <view v-if=\"password\" :class=\"ui.password()\" @tap.stop=\"showPassword\">\r\n <view class=\"size-4\" :class=\"[isPassword ? 'i-lucide-eye' : `\r\n i-lucide-eye-off\r\n `]\" />\r\n </view>\r\n </view>\r\n </view>\r\n</template>\r\n",
33
33
  "target": "uniapp"
34
34
  }
35
35
  ],
36
36
  "fileCount": 6,
37
- "contentHash": "3f2547459ad965e09e06bef96f71c151bd0f7b4c"
37
+ "contentHash": "63c8b77acfe4dc1976ec45ca738587c073393f89"
38
38
  }