@luanlu/mk-motion 1.1.0 → 1.2.1

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 (131) hide show
  1. package/package.json +14 -2
  2. package/src/a11y/focus-trap.ts +64 -0
  3. package/src/a11y/keyboard.ts +43 -0
  4. package/src/components/alert/alert.css +111 -0
  5. package/src/components/alert/alert.ts +107 -0
  6. package/src/components/avatar/avatar.css +112 -0
  7. package/src/components/avatar/avatar.ts +175 -0
  8. package/src/components/breadcrumb/breadcrumb.css +31 -0
  9. package/src/components/breadcrumb/breadcrumb.ts +71 -0
  10. package/src/components/button/button.css +108 -0
  11. package/src/components/button/button.ts +140 -0
  12. package/src/components/card/card.css +52 -0
  13. package/src/components/card/card.ts +87 -0
  14. package/src/components/collapse/collapse.css +76 -0
  15. package/src/components/collapse/collapse.ts +168 -0
  16. package/src/components/dialog/dialog.css +78 -0
  17. package/src/components/dialog/dialog.ts +164 -0
  18. package/src/components/drawer/drawer.css +73 -0
  19. package/src/components/drawer/drawer.ts +131 -0
  20. package/src/components/empty/empty.css +36 -0
  21. package/src/components/empty/empty.ts +85 -0
  22. package/src/components/form/checkbox.css +56 -0
  23. package/src/components/form/checkbox.ts +119 -0
  24. package/src/components/form/radio.css +57 -0
  25. package/src/components/form/radio.ts +153 -0
  26. package/src/components/form/select.css +91 -0
  27. package/src/components/form/select.ts +174 -0
  28. package/src/components/form/slider.css +56 -0
  29. package/src/components/form/slider.ts +148 -0
  30. package/src/components/input/input.css +92 -0
  31. package/src/components/input/input.ts +162 -0
  32. package/src/components/layout/divider.css +32 -0
  33. package/src/components/layout/divider.ts +42 -0
  34. package/src/components/layout/row.css +64 -0
  35. package/src/components/layout/row.ts +57 -0
  36. package/src/components/layout/space.css +14 -0
  37. package/src/components/layout/space.ts +48 -0
  38. package/src/components/loading/loading.css +37 -0
  39. package/src/components/loading/loading.ts +46 -0
  40. package/src/components/menu/menu.css +121 -0
  41. package/src/components/menu/menu.ts +187 -0
  42. package/src/components/message/message.css +64 -0
  43. package/src/components/message/message.ts +96 -0
  44. package/src/components/popover/popover.css +73 -0
  45. package/src/components/popover/popover.ts +279 -0
  46. package/src/components/progress/progress.css +112 -0
  47. package/src/components/progress/progress.ts +171 -0
  48. package/src/components/steps/steps.css +127 -0
  49. package/src/components/steps/steps.ts +102 -0
  50. package/src/components/styles/components.css +28 -0
  51. package/src/components/styles/reset.css +24 -0
  52. package/src/components/styles/tokens.css +248 -0
  53. package/src/components/styles/variables.css +24 -0
  54. package/src/components/switch/switch.css +53 -0
  55. package/src/components/switch/switch.ts +103 -0
  56. package/src/components/table/table.css +192 -0
  57. package/src/components/table/table.ts +370 -0
  58. package/src/components/tabs/tabs.css +138 -0
  59. package/src/components/tabs/tabs.ts +211 -0
  60. package/src/components/tag/tag.css +123 -0
  61. package/src/components/tag/tag.ts +112 -0
  62. package/src/components/tooltip/tooltip.css +66 -0
  63. package/src/components/tooltip/tooltip.ts +185 -0
  64. package/src/core/animator.ts +124 -0
  65. package/src/core/timeline.ts +128 -0
  66. package/src/core/utils.ts +47 -0
  67. package/src/effects/glitch.ts +99 -0
  68. package/src/effects/particle.ts +134 -0
  69. package/src/effects/text-split.ts +95 -0
  70. package/src/effects/wave-text.ts +88 -0
  71. package/src/gesture/draggable.ts +130 -0
  72. package/src/gesture/spring.ts +152 -0
  73. package/src/index.ts +162 -0
  74. package/src/interactive/coverflow.ts +100 -0
  75. package/src/interactive/cursor-trail.ts +113 -0
  76. package/src/interactive/flip-card.ts +114 -0
  77. package/src/interactive/magnetic.ts +121 -0
  78. package/src/micro/hover-lift.ts +94 -0
  79. package/src/micro/ripple.ts +130 -0
  80. package/src/motion/component-motion.ts +177 -0
  81. package/src/nuxt/module.ts +46 -0
  82. package/src/presets/index.ts +69 -0
  83. package/src/scroll/scroll-trigger.ts +104 -0
  84. package/src/styles/animations.css +135 -0
  85. package/src/styles/element-plus.css +174 -0
  86. package/src/text/count-up.ts +108 -0
  87. package/src/text/typewriter.ts +109 -0
  88. package/src/theme/dark.css +19 -0
  89. package/src/theme/light.css +19 -0
  90. package/src/theme/theme.ts +65 -0
  91. package/src/transitions/blur-reveal.ts +92 -0
  92. package/src/transitions/collapse.ts +112 -0
  93. package/src/transitions/lazy-image.ts +87 -0
  94. package/src/transitions/list.ts +75 -0
  95. package/src/transitions/loading.ts +95 -0
  96. package/src/transitions/parallax.ts +60 -0
  97. package/src/transitions/shimmer.ts +105 -0
  98. package/src/transitions/toast.ts +151 -0
  99. package/src/types.d.ts +4 -0
  100. package/src/vite/plugin.ts +45 -0
  101. package/src/vue/button.ts +28 -9
  102. package/src/vue/card.ts +28 -8
  103. package/src/vue/composables/index.ts +4 -0
  104. package/src/vue/composables/useLoading.ts +12 -0
  105. package/src/vue/composables/useMessage.ts +16 -0
  106. package/src/vue/composables/useMotion.ts +19 -0
  107. package/src/vue/composables/useTheme.ts +12 -0
  108. package/src/vue/dialog.ts +69 -17
  109. package/src/vue/index.ts +4 -21
  110. package/src/vue/input.ts +35 -11
  111. package/src/vue/slider.ts +22 -4
  112. package/src/vue/switch.ts +16 -9
  113. package/src/vue/alert.ts +0 -32
  114. package/src/vue/avatar.ts +0 -34
  115. package/src/vue/breadcrumb.ts +0 -32
  116. package/src/vue/checkbox.ts +0 -32
  117. package/src/vue/collapse.ts +0 -33
  118. package/src/vue/divider.ts +0 -32
  119. package/src/vue/drawer.ts +0 -33
  120. package/src/vue/empty.ts +0 -33
  121. package/src/vue/menu.ts +0 -33
  122. package/src/vue/popover.ts +0 -34
  123. package/src/vue/progress.ts +0 -33
  124. package/src/vue/row.ts +0 -32
  125. package/src/vue/select.ts +0 -33
  126. package/src/vue/space.ts +0 -32
  127. package/src/vue/steps.ts +0 -33
  128. package/src/vue/table.ts +0 -33
  129. package/src/vue/tabs.ts +0 -33
  130. package/src/vue/tag.ts +0 -33
  131. package/src/vue/tooltip.ts +0 -34
@@ -0,0 +1,71 @@
1
+ import './breadcrumb.css'
2
+
3
+ export interface BreadcrumbItem {
4
+ label: string
5
+ href?: string
6
+ onClick?: () => void
7
+ }
8
+
9
+ export interface BreadcrumbOptions {
10
+ items: BreadcrumbItem[]
11
+ separator?: string | HTMLElement
12
+ }
13
+
14
+ export class MkBreadcrumb {
15
+ el: HTMLElement
16
+
17
+ constructor(container: HTMLElement | string, options: BreadcrumbOptions) {
18
+ const parent = typeof container === 'string' ? document.querySelector(container)! : container
19
+
20
+ this.el = document.createElement('nav')
21
+ this.el.className = 'mk-breadcrumb'
22
+ this.el.setAttribute('aria-label', 'breadcrumb')
23
+
24
+ const sep = options.separator ?? '/'
25
+
26
+ options.items.forEach((item, index) => {
27
+ const isLast = index === options.items.length - 1
28
+
29
+ if (index > 0) {
30
+ const separatorEl = document.createElement('span')
31
+ separatorEl.className = 'mk-breadcrumb__separator'
32
+ if (typeof sep === 'string') {
33
+ separatorEl.textContent = sep
34
+ } else if (sep instanceof HTMLElement) {
35
+ separatorEl.appendChild(sep)
36
+ }
37
+ this.el.appendChild(separatorEl)
38
+ }
39
+
40
+ if (isLast) {
41
+ const span = document.createElement('span')
42
+ span.className = 'mk-breadcrumb__item is-current'
43
+ span.setAttribute('aria-current', 'page')
44
+ span.textContent = item.label
45
+ this.el.appendChild(span)
46
+ } else {
47
+ const link = document.createElement(item.href ? 'a' : 'span')
48
+ link.className = 'mk-breadcrumb__item is-link'
49
+ if (item.href) {
50
+ (link as HTMLAnchorElement).href = item.href
51
+ }
52
+ link.textContent = item.label
53
+ link.style.cursor = item.onClick || item.href ? 'pointer' : 'default'
54
+ if (item.onClick) {
55
+ link.addEventListener('click', item.onClick)
56
+ }
57
+ this.el.appendChild(link)
58
+ }
59
+ })
60
+
61
+ parent.appendChild(this.el)
62
+ }
63
+
64
+ destroy(): void {
65
+ this.el.remove()
66
+ }
67
+ }
68
+
69
+ export function createBreadcrumb(container: HTMLElement | string, options: BreadcrumbOptions): MkBreadcrumb {
70
+ return new MkBreadcrumb(container, options)
71
+ }
@@ -0,0 +1,108 @@
1
+ .mk-button {
2
+ display: inline-flex;
3
+ align-items: center;
4
+ justify-content: center;
5
+ gap: 6px;
6
+ padding: 0 16px;
7
+ height: 36px;
8
+ font-size: 13px;
9
+ font-weight: 500;
10
+ white-space: nowrap;
11
+ cursor: pointer;
12
+ background: var(--mk-surface);
13
+ border: 1px solid var(--mk-border);
14
+ border-radius: var(--mk-radius);
15
+ color: var(--mk-text);
16
+ outline: none;
17
+ transition: var(--mk-transition);
18
+ user-select: none;
19
+ margin-right: 8px;
20
+ margin-bottom: 4px;
21
+ }
22
+ .mk-button:last-child {
23
+ margin-right: 0;
24
+ }
25
+
26
+ .mk-button:hover {
27
+ background: var(--mk-surface-hover);
28
+ border-color: var(--mk-border-hover);
29
+ }
30
+
31
+ .mk-button:active {
32
+ transform: translateY(0.5px);
33
+ }
34
+
35
+ .mk-button.is-disabled {
36
+ opacity: 0.4;
37
+ cursor: not-allowed;
38
+ }
39
+
40
+ /* Primary */
41
+ .mk-button--primary {
42
+ background: var(--mk-primary);
43
+ border-color: var(--mk-primary);
44
+ color: #fff;
45
+ }
46
+ .mk-button--primary:hover {
47
+ background: var(--mk-primary-hover);
48
+ border-color: var(--mk-primary-hover);
49
+ }
50
+ .mk-button--primary:active {
51
+ background: var(--mk-primary-active);
52
+ border-color: var(--mk-primary-active);
53
+ }
54
+
55
+ /* Success / Warning / Danger */
56
+ .mk-button--success { background: var(--mk-success); border-color: var(--mk-success); color: #fff; }
57
+ .mk-button--success:hover { opacity: 0.9; }
58
+
59
+ .mk-button--warning { background: var(--mk-warning); border-color: var(--mk-warning); color: #000; }
60
+ .mk-button--warning:hover { opacity: 0.9; }
61
+
62
+ .mk-button--danger { background: var(--mk-danger); border-color: var(--mk-danger); color: #fff; }
63
+ .mk-button--danger:hover { opacity: 0.9; }
64
+
65
+ /* Text */
66
+ .mk-button--text {
67
+ background: transparent;
68
+ border-color: transparent;
69
+ color: var(--mk-primary);
70
+ padding: 0 8px;
71
+ }
72
+ .mk-button--text:hover {
73
+ background: rgba(99,102,241,0.08);
74
+ }
75
+
76
+ /* Sizes */
77
+ .mk-button--small { padding: 0 12px; height: 28px; font-size: 12px; border-radius: 6px; }
78
+ .mk-button--large { padding: 0 20px; height: 44px; font-size: 14px; }
79
+
80
+ /* Round / Circle */
81
+ .mk-button.is-round { border-radius: var(--mk-radius-full); }
82
+ .mk-button.is-circle { border-radius: 50%; padding: 0; width: 36px; height: 36px; }
83
+ .mk-button--small.is-circle { width: 28px; height: 28px; }
84
+ .mk-button--large.is-circle { width: 44px; height: 44px; }
85
+
86
+ /* Loading */
87
+ .mk-button.is-loading { pointer-events: none; }
88
+ .mk-button__spinner {
89
+ width: 14px; height: 14px;
90
+ border: 2px solid rgba(255,255,255,0.2);
91
+ border-top-color: #fff;
92
+ border-radius: 50%;
93
+ animation: mk-spin 0.6s linear infinite;
94
+ }
95
+ @keyframes mk-spin { to { transform: rotate(360deg); } }
96
+
97
+ /* Ripple */
98
+ .mk-button__ripple {
99
+ position: absolute;
100
+ border-radius: 50%;
101
+ background: rgba(255,255,255,0.2);
102
+ pointer-events: none;
103
+ transform: scale(0);
104
+ animation: mk-ripple 0.4s ease-out forwards;
105
+ }
106
+ @keyframes mk-ripple {
107
+ to { transform: scale(2.5); opacity: 0; }
108
+ }
@@ -0,0 +1,140 @@
1
+ import '../../styles/element-plus.css'
2
+ import './button.css'
3
+ import { withMotion, type MotionOptions } from '../../motion/component-motion.ts'
4
+
5
+ export interface ButtonOptions {
6
+ type?: 'default' | 'primary' | 'success' | 'warning' | 'danger' | 'text'
7
+ size?: 'default' | 'small' | 'large'
8
+ plain?: boolean
9
+ round?: boolean
10
+ circle?: boolean
11
+ disabled?: boolean
12
+ loading?: boolean
13
+ text?: string
14
+ icon?: string
15
+ onClick?: (e: MouseEvent) => void
16
+ motion?: MotionOptions
17
+ }
18
+
19
+ export class MkButton {
20
+ el: HTMLButtonElement
21
+ private options: ButtonOptions
22
+ private motion: ReturnType<typeof withMotion> | null = null
23
+
24
+ constructor(container: HTMLElement | string, options: ButtonOptions = {}) {
25
+ const parent =
26
+ typeof container === 'string'
27
+ ? document.querySelector(container)!
28
+ : container
29
+
30
+ this.options = {
31
+ type: 'default',
32
+ size: 'default',
33
+ ...options,
34
+ }
35
+
36
+ this.el = document.createElement('button')
37
+ this.el.type = 'button'
38
+ this.el.className = this.buildClass()
39
+ this.el.disabled = !!this.options.disabled || !!this.options.loading
40
+ if (this.options.icon && !this.options.text) {
41
+ this.el.setAttribute('aria-label', 'Button')
42
+ }
43
+ this.updateAriaDisabled()
44
+
45
+ if (this.options.loading) {
46
+ this.el.classList.add('is-loading')
47
+ const spinner = document.createElement('span')
48
+ spinner.className = 'mk-button__spinner'
49
+ this.el.appendChild(spinner)
50
+ }
51
+
52
+ if (this.options.icon && !this.options.loading) {
53
+ const icon = document.createElement('span')
54
+ icon.textContent = this.options.icon
55
+ this.el.appendChild(icon)
56
+ }
57
+
58
+ if (this.options.text) {
59
+ const span = document.createElement('span')
60
+ span.textContent = this.options.text
61
+ this.el.appendChild(span)
62
+ }
63
+
64
+ this.el.addEventListener('click', (e) => {
65
+ this.createRipple(e)
66
+ this.options.onClick?.(e)
67
+ })
68
+
69
+ parent.appendChild(this.el)
70
+
71
+ this.motion = withMotion(this.el, options.motion || { hover: 'lift', active: 'press', focus: 'ring', enter: 'fadeIn', duration: 200 })
72
+ }
73
+
74
+ private buildClass(): string {
75
+ const classes = ['mk-button']
76
+ if (this.options.type && this.options.type !== 'default') {
77
+ classes.push(`mk-button--${this.options.type}`)
78
+ }
79
+ if (this.options.size && this.options.size !== 'default') {
80
+ classes.push(`mk-button--${this.options.size}`)
81
+ }
82
+ if (this.options.plain) classes.push('is-plain')
83
+ if (this.options.round) classes.push('is-round')
84
+ if (this.options.circle) classes.push('is-circle')
85
+ if (this.options.loading) classes.push('is-loading')
86
+ if (this.options.disabled) classes.push('is-disabled')
87
+ return classes.join(' ')
88
+ }
89
+
90
+ private createRipple(e: MouseEvent): void {
91
+ const rect = this.el.getBoundingClientRect()
92
+ const size = Math.max(rect.width, rect.height)
93
+ const ripple = document.createElement('span')
94
+ ripple.className = 'mk-button__ripple'
95
+ ripple.style.width = ripple.style.height = `${size}px`
96
+ ripple.style.left = `${e.clientX - rect.left - size / 2}px`
97
+ ripple.style.top = `${e.clientY - rect.top - size / 2}px`
98
+ this.el.appendChild(ripple)
99
+ setTimeout(() => ripple.remove(), 600)
100
+ }
101
+
102
+ setLoading(loading: boolean): void {
103
+ this.options.loading = loading
104
+ this.el.classList.toggle('is-loading', loading)
105
+ this.el.disabled = loading || !!this.options.disabled
106
+ this.updateAriaDisabled()
107
+
108
+ const spinner = this.el.querySelector('.mk-button__spinner')
109
+ if (loading && !spinner) {
110
+ const s = document.createElement('span')
111
+ s.className = 'mk-button__spinner'
112
+ this.el.insertBefore(s, this.el.firstChild)
113
+ } else if (!loading && spinner) {
114
+ spinner.remove()
115
+ }
116
+ }
117
+
118
+ setDisabled(disabled: boolean): void {
119
+ this.options.disabled = disabled
120
+ this.el.classList.toggle('is-disabled', disabled)
121
+ this.el.disabled = disabled || !!this.options.loading
122
+ this.updateAriaDisabled()
123
+ }
124
+
125
+ private updateAriaDisabled(): void {
126
+ this.el.setAttribute('aria-disabled', String(this.el.disabled))
127
+ }
128
+
129
+ destroy(): void {
130
+ this.motion?.destroy()
131
+ this.el.remove()
132
+ }
133
+ }
134
+
135
+ export function createButton(
136
+ container: HTMLElement | string,
137
+ options?: ButtonOptions
138
+ ): MkButton {
139
+ return new MkButton(container, options)
140
+ }
@@ -0,0 +1,52 @@
1
+ .mk-card {
2
+ background: var(--mk-surface);
3
+ border: 1px solid var(--mk-border);
4
+ border-radius: var(--mk-radius-lg);
5
+ overflow: hidden;
6
+ transition: var(--mk-transition);
7
+ }
8
+
9
+ .mk-card:hover {
10
+ border-color: var(--mk-border-hover);
11
+ }
12
+
13
+ .mk-card__header {
14
+ padding: 16px 20px 12px;
15
+ display: flex;
16
+ align-items: center;
17
+ justify-content: space-between;
18
+ }
19
+
20
+ .mk-card__title {
21
+ font-size: 14px;
22
+ font-weight: 600;
23
+ color: var(--mk-text);
24
+ }
25
+
26
+ .mk-card__body {
27
+ padding: 0 20px 16px;
28
+ color: var(--mk-text-secondary);
29
+ font-size: 13px;
30
+ line-height: 1.6;
31
+ }
32
+
33
+ .mk-card__footer {
34
+ padding: 10px 20px;
35
+ border-top: 1px solid var(--mk-border);
36
+ color: var(--mk-text-tertiary);
37
+ font-size: 12px;
38
+ }
39
+
40
+ .mk-card__image {
41
+ width: 100%;
42
+ height: 160px;
43
+ object-fit: cover;
44
+ display: block;
45
+ border-bottom: 1px solid var(--mk-border);
46
+ }
47
+
48
+ /* Loading */
49
+ .mk-card.is-loading .mk-card__body {
50
+ opacity: 0.4;
51
+ pointer-events: none;
52
+ }
@@ -0,0 +1,87 @@
1
+ import './card.css'
2
+ import { withMotion, type MotionOptions } from '../../motion/component-motion.ts'
3
+
4
+ export interface CardOptions {
5
+ title?: string
6
+ body?: string
7
+ footer?: string
8
+ image?: string
9
+ shadow?: 'always' | 'hover' | 'never'
10
+ loading?: boolean
11
+ motion?: MotionOptions
12
+ }
13
+
14
+ export class MkCard {
15
+ el: HTMLDivElement
16
+ private motion: ReturnType<typeof withMotion> | null = null
17
+
18
+ constructor(container: HTMLElement | string, options: CardOptions = {}) {
19
+ const parent =
20
+ typeof container === 'string'
21
+ ? document.querySelector(container)!
22
+ : container
23
+
24
+ this.el = document.createElement('div')
25
+ this.el.className = 'mk-card'
26
+
27
+ if (options.shadow) {
28
+ if (options.shadow === 'always') this.el.classList.add('is-always-shadow')
29
+ else if (options.shadow === 'hover') this.el.classList.add('is-hover-shadow')
30
+ }
31
+ if (options.loading) {
32
+ this.el.classList.add('is-loading')
33
+ }
34
+
35
+ if (options.image) {
36
+ const img = document.createElement('img')
37
+ img.className = 'mk-card__image'
38
+ img.src = options.image
39
+ img.alt = ''
40
+ this.el.appendChild(img)
41
+ }
42
+
43
+ if (options.title) {
44
+ const header = document.createElement('div')
45
+ header.className = 'mk-card__header'
46
+ const title = document.createElement('span')
47
+ title.className = 'mk-card__title'
48
+ title.textContent = options.title
49
+ header.appendChild(title)
50
+ this.el.appendChild(header)
51
+ }
52
+
53
+ const body = document.createElement('div')
54
+ body.className = 'mk-card__body'
55
+ if (options.body) {
56
+ body.innerHTML = options.body
57
+ }
58
+ this.el.appendChild(body)
59
+
60
+ if (options.footer) {
61
+ const footer = document.createElement('div')
62
+ footer.className = 'mk-card__footer'
63
+ footer.textContent = options.footer
64
+ this.el.appendChild(footer)
65
+ }
66
+
67
+ parent.appendChild(this.el)
68
+
69
+ this.motion = withMotion(this.el, options.motion || { hover: 'lift', duration: 300 })
70
+ }
71
+
72
+ setLoading(loading: boolean): void {
73
+ this.el.classList.toggle('is-loading', loading)
74
+ }
75
+
76
+ destroy(): void {
77
+ this.motion?.destroy()
78
+ this.el.remove()
79
+ }
80
+ }
81
+
82
+ export function createCard(
83
+ container: HTMLElement | string,
84
+ options?: CardOptions
85
+ ): MkCard {
86
+ return new MkCard(container, options)
87
+ }
@@ -0,0 +1,76 @@
1
+ .mk-collapse {
2
+ border: 1px solid var(--mk-border);
3
+ border-radius: var(--mk-radius);
4
+ background: var(--mk-surface);
5
+ overflow: hidden;
6
+ }
7
+
8
+ .mk-collapse__item {
9
+ border-bottom: 1px solid var(--mk-border);
10
+ }
11
+
12
+ .mk-collapse__item:last-child {
13
+ border-bottom: none;
14
+ }
15
+
16
+ .mk-collapse__header {
17
+ display: flex;
18
+ align-items: center;
19
+ justify-content: space-between;
20
+ gap: var(--mk-space-3);
21
+ padding: var(--mk-space-3) var(--mk-space-4);
22
+ font-size: var(--mk-text-sm);
23
+ font-weight: var(--mk-font-medium);
24
+ color: var(--mk-text);
25
+ cursor: pointer;
26
+ user-select: none;
27
+ transition: background-color var(--mk-duration-fast) var(--mk-ease-default);
28
+ outline: none;
29
+ }
30
+
31
+ .mk-collapse__header:hover {
32
+ background: var(--mk-surface-hover);
33
+ }
34
+
35
+ .mk-collapse__header:focus-visible {
36
+ box-shadow: inset 0 0 0 2px var(--mk-primary-soft);
37
+ }
38
+
39
+ .mk-collapse__item.is-disabled .mk-collapse__header {
40
+ color: var(--mk-text-disabled);
41
+ cursor: not-allowed;
42
+ background: transparent;
43
+ }
44
+
45
+ .mk-collapse__title {
46
+ flex: 1;
47
+ min-width: 0;
48
+ }
49
+
50
+ .mk-collapse__arrow {
51
+ display: inline-flex;
52
+ align-items: center;
53
+ justify-content: center;
54
+ width: 16px;
55
+ height: 16px;
56
+ color: var(--mk-text-tertiary);
57
+ transition: transform var(--mk-duration-normal) var(--mk-ease-out);
58
+ flex-shrink: 0;
59
+ }
60
+
61
+ .mk-collapse__arrow.is-expanded {
62
+ transform: rotate(90deg);
63
+ }
64
+
65
+ .mk-collapse__content {
66
+ max-height: 0;
67
+ overflow: hidden;
68
+ transition: max-height var(--mk-duration-normal) var(--mk-ease-out);
69
+ }
70
+
71
+ .mk-collapse__inner {
72
+ padding: 0 var(--mk-space-4) var(--mk-space-3);
73
+ font-size: var(--mk-text-sm);
74
+ line-height: var(--mk-leading-normal);
75
+ color: var(--mk-text-secondary);
76
+ }
@@ -0,0 +1,168 @@
1
+ import '../../styles/element-plus.css'
2
+ import './collapse.css'
3
+
4
+ export interface CollapseItem {
5
+ title: string
6
+ content: string | HTMLElement
7
+ disabled?: boolean
8
+ }
9
+
10
+ export interface CollapsePanelOptions {
11
+ items: CollapseItem[]
12
+ accordion?: boolean
13
+ activeKeys?: number[]
14
+ }
15
+
16
+ let collapseIdCounter = 0
17
+
18
+ export class MkCollapse {
19
+ el: HTMLDivElement
20
+ private options: CollapsePanelOptions
21
+ private activeKeys: Set<number>
22
+ private itemEls: Array<{
23
+ header: HTMLDivElement
24
+ content: HTMLDivElement
25
+ inner: HTMLDivElement
26
+ arrow: HTMLSpanElement
27
+ contentId: string
28
+ }> = []
29
+
30
+ constructor(container: HTMLElement | string, options: CollapsePanelOptions) {
31
+ const parent =
32
+ typeof container === 'string'
33
+ ? document.querySelector(container)!
34
+ : container
35
+
36
+ this.options = {
37
+ accordion: false,
38
+ activeKeys: [],
39
+ ...options,
40
+ }
41
+
42
+ this.activeKeys = new Set(this.options.activeKeys)
43
+
44
+ this.el = document.createElement('div')
45
+ this.el.className = 'mk-collapse'
46
+
47
+ this.options.items.forEach((item, index) => {
48
+ const contentId = `mk-collapse-content-${++collapseIdCounter}`
49
+
50
+ const itemEl = document.createElement('div')
51
+ itemEl.className = 'mk-collapse__item'
52
+ if (item.disabled) itemEl.classList.add('is-disabled')
53
+
54
+ const header = document.createElement('div')
55
+ header.className = 'mk-collapse__header'
56
+ header.setAttribute('role', 'button')
57
+ header.setAttribute('tabindex', item.disabled ? '-1' : '0')
58
+ header.setAttribute('aria-expanded', 'false')
59
+ header.setAttribute('aria-controls', contentId)
60
+
61
+ const title = document.createElement('span')
62
+ title.className = 'mk-collapse__title'
63
+ title.textContent = item.title
64
+
65
+ const arrow = document.createElement('span')
66
+ arrow.className = 'mk-collapse__arrow'
67
+ arrow.innerHTML =
68
+ '<svg width="12" height="12" viewBox="0 0 12 12" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M4 2l4 4-4 4"/></svg>'
69
+
70
+ header.appendChild(title)
71
+ header.appendChild(arrow)
72
+ itemEl.appendChild(header)
73
+
74
+ const content = document.createElement('div')
75
+ content.className = 'mk-collapse__content'
76
+ content.id = contentId
77
+
78
+ const inner = document.createElement('div')
79
+ inner.className = 'mk-collapse__inner'
80
+ if (typeof item.content === 'string') {
81
+ inner.innerHTML = item.content
82
+ } else {
83
+ inner.appendChild(item.content)
84
+ }
85
+ content.appendChild(inner)
86
+ itemEl.appendChild(content)
87
+
88
+ this.itemEls.push({ header, content, inner, arrow, contentId })
89
+
90
+ this.el.appendChild(itemEl)
91
+
92
+ if (!item.disabled) {
93
+ header.addEventListener('click', () => this.toggle(index))
94
+ header.addEventListener('keydown', (e) => {
95
+ if (e.key === 'Enter' || e.key === ' ') {
96
+ e.preventDefault()
97
+ this.toggle(index)
98
+ }
99
+ })
100
+ }
101
+ })
102
+
103
+ parent.appendChild(this.el)
104
+
105
+ requestAnimationFrame(() => {
106
+ this.itemEls.forEach((_, index) => {
107
+ this.updateItemState(index)
108
+ })
109
+ })
110
+ }
111
+
112
+ private toggle(index: number): void {
113
+ const isActive = this.activeKeys.has(index)
114
+
115
+ if (this.options.accordion) {
116
+ const wasActive = Array.from(this.activeKeys)
117
+ this.activeKeys.clear()
118
+ if (!isActive) {
119
+ this.activeKeys.add(index)
120
+ }
121
+ wasActive.forEach((key) => this.updateItemState(key))
122
+ this.updateItemState(index)
123
+ } else {
124
+ if (isActive) {
125
+ this.activeKeys.delete(index)
126
+ } else {
127
+ this.activeKeys.add(index)
128
+ }
129
+ this.updateItemState(index)
130
+ }
131
+ }
132
+
133
+ private updateItemState(index: number): void {
134
+ const item = this.itemEls[index]
135
+ if (!item) return
136
+
137
+ const isActive = this.activeKeys.has(index)
138
+ const itemEl = item.header.parentElement as HTMLDivElement
139
+
140
+ item.header.setAttribute('aria-expanded', String(isActive))
141
+ item.arrow.classList.toggle('is-expanded', isActive)
142
+ itemEl.classList.toggle('is-active', isActive)
143
+
144
+ if (isActive) {
145
+ item.content.style.maxHeight = `${item.inner.scrollHeight}px`
146
+ } else {
147
+ item.content.style.maxHeight = '0px'
148
+ }
149
+ }
150
+
151
+ setActiveKeys(keys: number[]): void {
152
+ const prev = new Set(this.activeKeys)
153
+ this.activeKeys = new Set(keys)
154
+ prev.forEach((key) => this.updateItemState(key))
155
+ keys.forEach((key) => this.updateItemState(key))
156
+ }
157
+
158
+ destroy(): void {
159
+ this.el.remove()
160
+ }
161
+ }
162
+
163
+ export function createCollapse(
164
+ container: HTMLElement | string,
165
+ options: CollapsePanelOptions
166
+ ): MkCollapse {
167
+ return new MkCollapse(container, options)
168
+ }