@luanlu/mk-motion 1.0.0 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@luanlu/mk-motion",
3
- "version": "1.0.0",
3
+ "version": "1.2.0",
4
4
  "description": "A lightweight, modern frontend animation library",
5
5
  "type": "module",
6
6
  "main": "./dist/mk-motion.umd.cjs",
@@ -17,10 +17,31 @@
17
17
  "default": "./dist/mk-motion.umd.cjs"
18
18
  }
19
19
  },
20
- "./css": "./dist/style.css"
20
+ "./css": "./dist/style.css",
21
+ "./vue": {
22
+ "import": {
23
+ "types": "./src/vue/index.ts",
24
+ "default": "./src/vue/index.ts"
25
+ }
26
+ },
27
+ "./nuxt": {
28
+ "import": {
29
+ "types": "./src/nuxt/module.ts",
30
+ "default": "./src/nuxt/module.ts"
31
+ }
32
+ },
33
+ "./vite": {
34
+ "import": {
35
+ "types": "./src/vite/plugin.ts",
36
+ "default": "./src/vite/plugin.ts"
37
+ }
38
+ }
21
39
  },
22
40
  "files": [
23
- "dist"
41
+ "dist",
42
+ "src/vue",
43
+ "src/nuxt",
44
+ "src/vite"
24
45
  ],
25
46
  "publishConfig": {
26
47
  "access": "public"
@@ -49,9 +70,13 @@
49
70
  "url": "https://github.com/luanluuu/mk-motion/issues"
50
71
  },
51
72
  "homepage": "https://github.com/luanluuu/mk-motion#readme",
73
+ "peerDependencies": {
74
+ "vue": "^3.0.0"
75
+ },
52
76
  "devDependencies": {
53
77
  "typescript": "^5.4.0",
54
78
  "vite": "^5.2.0",
55
- "vite-plugin-dts": "^3.8.0"
79
+ "vite-plugin-dts": "^3.8.0",
80
+ "vue": "^3.5.0"
56
81
  }
57
82
  }
@@ -0,0 +1,46 @@
1
+ import { defineNuxtModule, addComponent, addImportsDir, createResolver } from '@nuxt/kit'
2
+
3
+ export interface ModuleOptions {
4
+ prefix?: string
5
+ }
6
+
7
+ export default defineNuxtModule<ModuleOptions>({
8
+ meta: {
9
+ name: 'mk-motion',
10
+ configKey: 'mkMotion',
11
+ compatibility: {
12
+ nuxt: '^3.0.0',
13
+ },
14
+ },
15
+ defaults: {
16
+ prefix: 'Mk',
17
+ },
18
+ setup(options, nuxt) {
19
+ const resolver = createResolver(import.meta.url)
20
+ const runtimeDir = resolver.resolve('./runtime')
21
+
22
+ // Auto-import components from src/vue/
23
+ const components = [
24
+ 'Button',
25
+ 'Card',
26
+ 'Dialog',
27
+ 'Input',
28
+ 'Radio',
29
+ 'Slider',
30
+ 'Switch',
31
+ ]
32
+
33
+ for (const name of components) {
34
+ addComponent({
35
+ name: `${options.prefix}${name}`,
36
+ filePath: resolver.resolve('../../vue', `${name.toLowerCase()}.ts`),
37
+ })
38
+ }
39
+
40
+ // Auto-import composables
41
+ addImportsDir(resolver.resolve('../../vue/composables'))
42
+
43
+ // Inject CSS
44
+ nuxt.options.css.push('@luanlu/mk-motion/css')
45
+ },
46
+ })
@@ -0,0 +1,45 @@
1
+ import type { Plugin } from 'vite'
2
+
3
+ export interface MkMotionOptions {
4
+ components?: string[]
5
+ motion?: boolean
6
+ theme?: 'dark' | 'light' | 'auto'
7
+ }
8
+
9
+ export function mkMotion(options: MkMotionOptions = {}): Plugin {
10
+ const components = options.components || [
11
+ 'Button', 'Card', 'Dialog', 'Input', 'Radio', 'Slider', 'Switch',
12
+ ]
13
+
14
+ const cssImports: string[] = []
15
+ if (options.theme === 'dark') {
16
+ cssImports.push(`import '@luanlu/mk-motion/css'`)
17
+ }
18
+
19
+ return {
20
+ name: 'mk-motion',
21
+ enforce: 'pre',
22
+ transform(code, id) {
23
+ // Only process Vue SFC script blocks
24
+ if (!id.endsWith('.vue')) return code
25
+
26
+ // If component is used but not imported, add auto-import
27
+ const used = components.filter((name) => {
28
+ const tag = `Mk${name}`
29
+ return code.includes(`<${tag}`) || code.includes(`<${tag} `)
30
+ })
31
+
32
+ if (used.length === 0) return code
33
+
34
+ const imports = used
35
+ .map((name) => `import { Mk${name} } from '@luanlu/mk-motion/vue'`)
36
+ .join('\n')
37
+
38
+ if (code.includes('import { ' + used.map((n) => `Mk${n}`).join(', ') + ' }')) {
39
+ return code
40
+ }
41
+
42
+ return imports + '\n' + code
43
+ },
44
+ }
45
+ }
@@ -0,0 +1,54 @@
1
+ import { defineComponent, h, ref, onMounted, onUnmounted, watch } from 'vue'
2
+ import { createButton } from '../components/button/button.js'
3
+ import type { ButtonOptions } from '../components/button/button.js'
4
+
5
+ export const MkButton = defineComponent({
6
+ name: 'MkButton',
7
+ props: {
8
+ type: { type: String as () => ButtonOptions['type'], default: 'default' },
9
+ size: { type: String as () => ButtonOptions['size'], default: 'default' },
10
+ plain: { type: Boolean, default: false },
11
+ round: { type: Boolean, default: false },
12
+ circle: { type: Boolean, default: false },
13
+ disabled: { type: Boolean, default: false },
14
+ loading: { type: Boolean, default: false },
15
+ icon: { type: String, default: '' },
16
+ motion: { type: Object as () => ButtonOptions['motion'], default: undefined },
17
+ },
18
+ emits: ['click'],
19
+ setup(props, { emit, slots }) {
20
+ const container = ref<HTMLDivElement>()
21
+ let instance: ReturnType<typeof createButton> | null = null
22
+
23
+ const getText = () => {
24
+ const slotContent = slots.default?.()[0]?.children
25
+ return (typeof slotContent === 'string' ? slotContent : '') || ''
26
+ }
27
+
28
+ const create = () => {
29
+ if (!container.value) return
30
+ instance?.destroy()
31
+ instance = createButton(container.value, {
32
+ type: props.type,
33
+ size: props.size,
34
+ plain: props.plain,
35
+ round: props.round,
36
+ circle: props.circle,
37
+ disabled: props.disabled,
38
+ loading: props.loading,
39
+ text: getText(),
40
+ icon: props.icon || undefined,
41
+ motion: props.motion,
42
+ onClick: (e) => emit('click', e),
43
+ })
44
+ }
45
+
46
+ onMounted(create)
47
+ watch(() => [props.type, props.size, props.plain, props.round, props.circle, props.icon, props.motion], create, { deep: true })
48
+ watch(() => props.loading, (v) => instance?.setLoading(v))
49
+ watch(() => props.disabled, (v) => instance?.setDisabled(v))
50
+ onUnmounted(() => instance?.destroy())
51
+
52
+ return () => h('div', { ref: container, style: 'display:inline-block' })
53
+ },
54
+ })
@@ -0,0 +1,53 @@
1
+ import { defineComponent, h, ref, onMounted, onUnmounted, watch } from 'vue'
2
+ import { createCard } from '../components/card/card.js'
3
+ import type { CardOptions } from '../components/card/card.js'
4
+
5
+ export const MkCard = defineComponent({
6
+ name: 'MkCard',
7
+ props: {
8
+ shadow: { type: String as () => CardOptions['shadow'], default: undefined },
9
+ loading: { type: Boolean, default: false },
10
+ image: { type: String, default: '' },
11
+ motion: { type: Object as () => CardOptions['motion'], default: undefined },
12
+ },
13
+ setup(props, { slots }) {
14
+ const container = ref<HTMLDivElement>()
15
+ let instance: ReturnType<typeof createCard> | null = null
16
+
17
+ const getTitle = () => {
18
+ const slot = slots.header?.()[0]?.children
19
+ return typeof slot === 'string' ? slot : ''
20
+ }
21
+
22
+ const getBody = () => {
23
+ const slot = slots.default?.()[0]?.children
24
+ return typeof slot === 'string' ? slot : ''
25
+ }
26
+
27
+ const getFooter = () => {
28
+ const slot = slots.footer?.()[0]?.children
29
+ return typeof slot === 'string' ? slot : ''
30
+ }
31
+
32
+ const create = () => {
33
+ if (!container.value) return
34
+ instance?.destroy()
35
+ instance = createCard(container.value, {
36
+ title: getTitle(),
37
+ body: getBody(),
38
+ footer: getFooter(),
39
+ image: props.image || undefined,
40
+ shadow: props.shadow,
41
+ loading: props.loading,
42
+ motion: props.motion,
43
+ })
44
+ }
45
+
46
+ onMounted(create)
47
+ watch(() => [props.shadow, props.image, props.loading, props.motion], create, { deep: true })
48
+ watch(() => props.loading, (v) => instance?.setLoading(v))
49
+ onUnmounted(() => instance?.destroy())
50
+
51
+ return () => h('div', { ref: container })
52
+ },
53
+ })
@@ -0,0 +1,4 @@
1
+ export { useMkTheme } from './useTheme.js'
2
+ export { useMkMotion } from './useMotion.js'
3
+ export { useMkLoading } from './useLoading.js'
4
+ export { useMkMessage } from './useMessage.js'
@@ -0,0 +1,12 @@
1
+ import {
2
+ showLoading,
3
+ showFullscreenLoading,
4
+ } from '../../components/loading/loading.js'
5
+ import type { LoadingOptions } from '../../components/loading/loading.js'
6
+
7
+ export function useMkLoading() {
8
+ return {
9
+ showLoading: (options?: LoadingOptions) => showLoading(options),
10
+ showFullscreenLoading: (text?: string) => showFullscreenLoading(text),
11
+ }
12
+ }
@@ -0,0 +1,16 @@
1
+ import {
2
+ message,
3
+ messageSuccess,
4
+ messageError,
5
+ messageWarning,
6
+ } from '../../components/message/message.js'
7
+ import type { MessageOptions } from '../../components/message/message.js'
8
+
9
+ export function useMkMessage() {
10
+ return {
11
+ message: (msg: string, options?: MessageOptions) => message(msg, options),
12
+ messageSuccess: (msg: string, options?: Omit<MessageOptions, 'type'>) => messageSuccess(msg, options),
13
+ messageError: (msg: string, options?: Omit<MessageOptions, 'type'>) => messageError(msg, options),
14
+ messageWarning: (msg: string, options?: Omit<MessageOptions, 'type'>) => messageWarning(msg, options),
15
+ }
16
+ }
@@ -0,0 +1,19 @@
1
+ import { onMounted, onUnmounted } from 'vue'
2
+ import type { Ref } from 'vue'
3
+ import { Animator } from '../../core/animator.js'
4
+ import type { MotionOptions } from '../../motion/component-motion.js'
5
+
6
+ export function useMkMotion(elRef: Ref<HTMLElement | undefined>, _options?: MotionOptions) {
7
+ let animator: Animator | null = null
8
+ onMounted(() => {
9
+ if (!elRef.value) return
10
+ animator = new Animator(elRef.value)
11
+ })
12
+ onUnmounted(() => {
13
+ animator?.reset()
14
+ })
15
+ return {
16
+ animate: (name: string, opts?: any) => animator?.animate(name, opts),
17
+ reset: () => animator?.reset(),
18
+ }
19
+ }
@@ -0,0 +1,12 @@
1
+ import { ref } from 'vue'
2
+
3
+ const isDark = ref(false)
4
+
5
+ export function useMkTheme() {
6
+ const setTheme = (dark: boolean) => {
7
+ isDark.value = dark
8
+ document.documentElement.setAttribute('data-mk-theme', dark ? 'dark' : 'light')
9
+ }
10
+ const toggle = () => setTheme(!isDark.value)
11
+ return { isDark, setTheme, toggle }
12
+ }
@@ -0,0 +1,85 @@
1
+ import { defineComponent, h, ref, watch, onMounted, onUnmounted, Teleport, Transition } from 'vue'
2
+
3
+ export const MkDialog = defineComponent({
4
+ name: 'MkDialog',
5
+ props: {
6
+ modelValue: { type: Boolean, default: false },
7
+ title: { type: String, default: '' },
8
+ showClose: { type: Boolean, default: true },
9
+ showCancel: { type: Boolean, default: true },
10
+ center: { type: Boolean, default: false },
11
+ cancelText: { type: String, default: '取消' },
12
+ confirmText: { type: String, default: '确定' },
13
+ },
14
+ emits: ['update:modelValue', 'confirm', 'cancel', 'close'],
15
+ setup(props, { emit, slots }) {
16
+ const visible = ref(props.modelValue)
17
+ const overlayRef = ref<HTMLDivElement>()
18
+
19
+ watch(() => props.modelValue, (v) => {
20
+ visible.value = v
21
+ })
22
+
23
+ const close = () => {
24
+ visible.value = false
25
+ emit('update:modelValue', false)
26
+ emit('close')
27
+ }
28
+
29
+ const onConfirm = () => {
30
+ emit('confirm')
31
+ close()
32
+ }
33
+
34
+ const onCancel = () => {
35
+ emit('cancel')
36
+ close()
37
+ }
38
+
39
+ const onOverlayClick = (e: MouseEvent) => {
40
+ if (e.target === overlayRef.value) close()
41
+ }
42
+
43
+ const onKeydown = (e: KeyboardEvent) => {
44
+ if (e.key === 'Escape') close()
45
+ }
46
+
47
+ onMounted(() => {
48
+ if (props.modelValue) visible.value = true
49
+ })
50
+
51
+ return () => h(Teleport, { to: 'body' }, [
52
+ h(Transition, { name: 'mk-dialog' }, {
53
+ default: () => visible.value
54
+ ? h('div', {
55
+ ref: overlayRef,
56
+ class: 'mk-dialog-overlay',
57
+ tabindex: -1,
58
+ onClick: onOverlayClick,
59
+ onKeydown,
60
+ }, [
61
+ h('div', {
62
+ class: ['mk-dialog', { 'is-center': props.center }],
63
+ role: 'dialog',
64
+ 'aria-modal': 'true',
65
+ }, [
66
+ h('div', { class: 'mk-dialog__header' }, [
67
+ h('span', { class: 'mk-dialog__title' }, props.title),
68
+ props.showClose
69
+ ? h('span', { class: 'mk-dialog__close', onClick: close }, '✕')
70
+ : null,
71
+ ]),
72
+ h('div', { class: 'mk-dialog__body' }, slots.default?.()),
73
+ h('div', { class: 'mk-dialog__footer' }, [
74
+ props.showCancel
75
+ ? h('button', { class: 'mk-button', onClick: onCancel }, props.cancelText)
76
+ : null,
77
+ h('button', { class: 'mk-button mk-button--primary', onClick: onConfirm }, props.confirmText),
78
+ ]),
79
+ ]),
80
+ ])
81
+ : null,
82
+ }),
83
+ ])
84
+ },
85
+ })
@@ -0,0 +1,11 @@
1
+ // Components
2
+ export { MkButton } from './button.js'
3
+ export { MkCard } from './card.js'
4
+ export { MkDialog } from './dialog.js'
5
+ export { MkInput } from './input.js'
6
+ export { MkRadio } from './radio.js'
7
+ export { MkSlider } from './slider.js'
8
+ export { MkSwitch } from './switch.js'
9
+
10
+ // Composables
11
+ export { useMkTheme, useMkMotion, useMkLoading, useMkMessage } from './composables/index.js'
@@ -0,0 +1,56 @@
1
+ import { defineComponent, h, ref, onMounted, onUnmounted, watch } from 'vue'
2
+ import { createInput } from '../components/input/input.js'
3
+ import type { InputOptions } from '../components/input/input.js'
4
+
5
+ export const MkInput = defineComponent({
6
+ name: 'MkInput',
7
+ props: {
8
+ modelValue: { type: String, default: '' },
9
+ type: { type: String, default: 'text' },
10
+ placeholder: { type: String, default: '' },
11
+ disabled: { type: Boolean, default: false },
12
+ clearable: { type: Boolean, default: false },
13
+ showPassword: { type: Boolean, default: false },
14
+ maxlength: { type: Number, default: undefined },
15
+ rows: { type: Number, default: undefined },
16
+ motion: { type: Object as () => InputOptions['motion'], default: undefined },
17
+ },
18
+ emits: ['update:modelValue', 'enter', 'focus', 'blur'],
19
+ setup(props, { emit }) {
20
+ const container = ref<HTMLDivElement>()
21
+ let instance: ReturnType<typeof createInput> | null = null
22
+
23
+ onMounted(() => {
24
+ if (!container.value) return
25
+ instance = createInput(container.value, {
26
+ type: props.type as InputOptions['type'],
27
+ placeholder: props.placeholder,
28
+ value: props.modelValue,
29
+ disabled: props.disabled,
30
+ clearable: props.clearable,
31
+ showPassword: props.showPassword,
32
+ maxlength: props.maxlength,
33
+ rows: props.rows,
34
+ motion: props.motion,
35
+ onInput: (v) => emit('update:modelValue', v),
36
+ onEnter: (v) => emit('enter', v),
37
+ onFocus: () => emit('focus'),
38
+ onBlur: () => emit('blur'),
39
+ })
40
+ })
41
+
42
+ watch(() => props.modelValue, (v) => {
43
+ if (instance && instance.input.value !== v) {
44
+ instance.input.value = v
45
+ }
46
+ })
47
+
48
+ watch(() => props.disabled, (v) => {
49
+ if (instance) instance.input.disabled = v
50
+ })
51
+
52
+ onUnmounted(() => instance?.destroy())
53
+
54
+ return () => h('div', { ref: container })
55
+ },
56
+ })
@@ -0,0 +1,23 @@
1
+ import { showLoading, showFullscreenLoading } from '../components/loading/loading.js'
2
+ import type { LoadingOptions } from '../components/loading/loading.js'
3
+
4
+ export function useMkLoading() {
5
+ let cleanup: (() => void) | null = null
6
+
7
+ const start = (options: LoadingOptions = {}) => {
8
+ cleanup?.()
9
+ cleanup = showLoading(options)
10
+ }
11
+
12
+ const startFullscreen = (text?: string) => {
13
+ cleanup?.()
14
+ cleanup = showFullscreenLoading(text)
15
+ }
16
+
17
+ const stop = () => {
18
+ cleanup?.()
19
+ cleanup = null
20
+ }
21
+
22
+ return { start, startFullscreen, stop }
23
+ }
@@ -0,0 +1,11 @@
1
+ import { message, messageSuccess, messageError, messageWarning } from '../components/message/message.js'
2
+ import type { MessageOptions } from '../components/message/message.js'
3
+
4
+ export function useMkMessage() {
5
+ return {
6
+ show: message,
7
+ success: messageSuccess,
8
+ error: messageError,
9
+ warning: messageWarning,
10
+ }
11
+ }
@@ -0,0 +1,37 @@
1
+ import { defineComponent, h, ref, onMounted, onUnmounted, watch } from 'vue'
2
+ import { MkRadio as MkRadioClass } from '../components/form/radio.js'
3
+ import type { RadioOptions } from '../components/form/radio.js'
4
+
5
+ export const MkRadio = defineComponent({
6
+ name: 'MkRadio',
7
+ props: {
8
+ label: { type: String, default: '' },
9
+ value: { type: [String, Number], required: true },
10
+ checked: { type: Boolean, default: false },
11
+ disabled: { type: Boolean, default: false },
12
+ },
13
+ emits: ['change'],
14
+ setup(props, { emit }) {
15
+ const container = ref<HTMLDivElement>()
16
+ let instance: InstanceType<typeof MkRadioClass> | null = null
17
+
18
+ const create = () => {
19
+ if (!container.value) return
20
+ instance?.destroy()
21
+ instance = new MkRadioClass(container.value, {
22
+ label: props.label,
23
+ value: props.value,
24
+ checked: props.checked,
25
+ disabled: props.disabled,
26
+ onChange: (v) => emit('change', v),
27
+ })
28
+ }
29
+
30
+ onMounted(create)
31
+ watch(() => props.checked, (v) => instance?.setChecked(v))
32
+ watch(() => [props.label, props.value, props.disabled], create, { deep: true })
33
+ onUnmounted(() => instance?.destroy())
34
+
35
+ return () => h('div', { ref: container })
36
+ },
37
+ })
@@ -0,0 +1,60 @@
1
+ import { defineComponent, h, ref, onMounted, onUnmounted, watch } from 'vue'
2
+ import { MkSlider as MkSliderClass } from '../components/form/slider.js'
3
+ import type { SliderOptions } from '../components/form/slider.js'
4
+
5
+ export const MkSlider = defineComponent({
6
+ name: 'MkSlider',
7
+ props: {
8
+ modelValue: { type: Number, default: 0 },
9
+ min: { type: Number, default: 0 },
10
+ max: { type: Number, default: 100 },
11
+ step: { type: Number, default: 1 },
12
+ showValue: { type: Boolean, default: true },
13
+ },
14
+ emits: ['update:modelValue', 'change'],
15
+ setup(props, { emit }) {
16
+ const container = ref<HTMLDivElement>()
17
+ let instance: InstanceType<typeof MkSliderClass> | null = null
18
+
19
+ onMounted(() => {
20
+ if (!container.value) return
21
+ instance = new MkSliderClass(container.value, {
22
+ min: props.min,
23
+ max: props.max,
24
+ step: props.step,
25
+ value: props.modelValue,
26
+ showValue: props.showValue,
27
+ onChange: (v) => {
28
+ emit('update:modelValue', v)
29
+ emit('change', v)
30
+ },
31
+ })
32
+ })
33
+
34
+ watch(() => props.modelValue, (v) => {
35
+ if (instance && instance.value !== v) {
36
+ instance.value = v
37
+ }
38
+ })
39
+
40
+ watch(() => [props.min, props.max, props.step], () => {
41
+ if (!container.value) return
42
+ instance?.destroy()
43
+ instance = new MkSliderClass(container.value, {
44
+ min: props.min,
45
+ max: props.max,
46
+ step: props.step,
47
+ value: props.modelValue,
48
+ showValue: props.showValue,
49
+ onChange: (v) => {
50
+ emit('update:modelValue', v)
51
+ emit('change', v)
52
+ },
53
+ })
54
+ })
55
+
56
+ onUnmounted(() => instance?.destroy())
57
+
58
+ return () => h('div', { ref: container })
59
+ },
60
+ })
@@ -0,0 +1,39 @@
1
+ import { defineComponent, h, ref, onMounted, onUnmounted, watch } from 'vue'
2
+ import { createSwitch } from '../components/switch/switch.js'
3
+ import type { SwitchOptions } from '../components/switch/switch.js'
4
+
5
+ export const MkSwitch = defineComponent({
6
+ name: 'MkSwitch',
7
+ props: {
8
+ modelValue: { type: Boolean, default: false },
9
+ disabled: { type: Boolean, default: false },
10
+ activeText: { type: String, default: '' },
11
+ inactiveText: { type: String, default: '' },
12
+ },
13
+ emits: ['update:modelValue', 'change'],
14
+ setup(props, { emit }) {
15
+ const container = ref<HTMLDivElement>()
16
+ let instance: ReturnType<typeof createSwitch> | null = null
17
+
18
+ const create = () => {
19
+ if (!container.value) return
20
+ instance?.destroy()
21
+ instance = createSwitch(container.value, {
22
+ value: props.modelValue,
23
+ disabled: props.disabled,
24
+ activeText: props.activeText,
25
+ inactiveText: props.inactiveText,
26
+ onChange: (v) => {
27
+ emit('update:modelValue', v)
28
+ emit('change', v)
29
+ },
30
+ })
31
+ }
32
+
33
+ onMounted(create)
34
+ watch(() => [props.modelValue, props.disabled, props.activeText, props.inactiveText], create, { deep: true })
35
+ onUnmounted(() => instance?.destroy())
36
+
37
+ return () => h('div', { ref: container, style: 'display:inline-block' })
38
+ },
39
+ })