@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
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@luanlu/mk-motion",
3
- "version": "1.1.0",
3
+ "version": "1.2.1",
4
4
  "description": "A lightweight, modern frontend animation library",
5
5
  "type": "module",
6
6
  "main": "./dist/mk-motion.umd.cjs",
@@ -23,11 +23,23 @@
23
23
  "types": "./src/vue/index.ts",
24
24
  "default": "./src/vue/index.ts"
25
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
+ }
26
38
  }
27
39
  },
28
40
  "files": [
29
41
  "dist",
30
- "src/vue"
42
+ "src"
31
43
  ],
32
44
  "publishConfig": {
33
45
  "access": "public"
@@ -0,0 +1,64 @@
1
+ /**
2
+ * Focus trap for modals, drawers, and popovers.
3
+ * Keeps keyboard focus within a given container.
4
+ */
5
+ const FOCUSABLE_SELECTOR = [
6
+ 'a[href]',
7
+ 'button:not([disabled])',
8
+ 'input:not([disabled])',
9
+ 'select:not([disabled])',
10
+ 'textarea:not([disabled])',
11
+ '[tabindex]:not([tabindex="-1"])',
12
+ ].join(',')
13
+
14
+ export class FocusTrap {
15
+ private container: HTMLElement
16
+ private previousActiveElement: Element | null = null
17
+ private listeners: Array<() => void> = []
18
+
19
+ constructor(container: HTMLElement) {
20
+ this.container = container
21
+ }
22
+
23
+ activate(initialFocus?: HTMLElement): void {
24
+ this.previousActiveElement = document.activeElement
25
+ const focusable = this.getFocusableElements()
26
+ const toFocus = initialFocus || (focusable.length > 0 ? focusable[0] : null)
27
+ if (toFocus instanceof HTMLElement) {
28
+ toFocus.focus()
29
+ }
30
+
31
+ const handler = (e: KeyboardEvent) => this.handleKeyDown(e)
32
+ this.container.addEventListener('keydown', handler)
33
+ this.listeners.push(() => this.container.removeEventListener('keydown', handler))
34
+ }
35
+
36
+ deactivate(): void {
37
+ this.listeners.forEach((remove) => remove())
38
+ this.listeners = []
39
+ if (this.previousActiveElement instanceof HTMLElement) {
40
+ this.previousActiveElement.focus()
41
+ }
42
+ }
43
+
44
+ private getFocusableElements(): NodeListOf<HTMLElement> {
45
+ return this.container.querySelectorAll(FOCUSABLE_SELECTOR)
46
+ }
47
+
48
+ private handleKeyDown(e: KeyboardEvent): void {
49
+ if (e.key !== 'Tab') return
50
+ const focusable = Array.from(this.getFocusableElements())
51
+ if (focusable.length === 0) return
52
+
53
+ const first = focusable[0]
54
+ const last = focusable[focusable.length - 1]
55
+
56
+ if (e.shiftKey && document.activeElement === first) {
57
+ e.preventDefault()
58
+ last.focus()
59
+ } else if (!e.shiftKey && document.activeElement === last) {
60
+ e.preventDefault()
61
+ first.focus()
62
+ }
63
+ }
64
+ }
@@ -0,0 +1,43 @@
1
+ /**
2
+ * Keyboard navigation helpers for accessible components.
3
+ */
4
+
5
+ export interface KeyHandler {
6
+ key: string
7
+ handler: (e: KeyboardEvent) => void
8
+ preventDefault?: boolean
9
+ }
10
+
11
+ /**
12
+ * Attach keyboard handlers to an element.
13
+ * Returns a cleanup function.
14
+ */
15
+ export function onKey(el: HTMLElement, handlers: KeyHandler[]): () => void {
16
+ const listener = (e: KeyboardEvent) => {
17
+ for (const h of handlers) {
18
+ if (e.key === h.key) {
19
+ if (h.preventDefault !== false) e.preventDefault()
20
+ h.handler(e)
21
+ return
22
+ }
23
+ }
24
+ }
25
+ el.addEventListener('keydown', listener)
26
+ return () => el.removeEventListener('keydown', listener)
27
+ }
28
+
29
+ /**
30
+ * Common ARIA key patterns.
31
+ */
32
+ export const Keys = {
33
+ Enter: 'Enter',
34
+ Escape: 'Escape',
35
+ Tab: 'Tab',
36
+ ArrowUp: 'ArrowUp',
37
+ ArrowDown: 'ArrowDown',
38
+ ArrowLeft: 'ArrowLeft',
39
+ ArrowRight: 'ArrowRight',
40
+ Home: 'Home',
41
+ End: 'End',
42
+ Space: ' ',
43
+ } as const
@@ -0,0 +1,111 @@
1
+ .mk-alert {
2
+ display: flex;
3
+ align-items: flex-start;
4
+ gap: var(--mk-space-3);
5
+ padding: var(--mk-space-3) var(--mk-space-4);
6
+ font-size: var(--mk-text-sm);
7
+ line-height: var(--mk-leading-snug);
8
+ color: var(--mk-text);
9
+ background: var(--mk-surface);
10
+ border: 1px solid var(--mk-border);
11
+ border-left-width: 4px;
12
+ border-radius: var(--mk-radius);
13
+ transition: var(--mk-transition-all);
14
+ overflow: hidden;
15
+ }
16
+
17
+ .mk-alert__icon {
18
+ display: inline-flex;
19
+ align-items: center;
20
+ justify-content: center;
21
+ width: 20px;
22
+ height: 20px;
23
+ flex-shrink: 0;
24
+ font-size: 14px;
25
+ font-weight: var(--mk-font-bold);
26
+ margin-top: 1px;
27
+ }
28
+
29
+ .mk-alert__content {
30
+ flex: 1;
31
+ min-width: 0;
32
+ }
33
+
34
+ .mk-alert__title {
35
+ font-weight: var(--mk-font-semibold);
36
+ margin-bottom: 2px;
37
+ }
38
+
39
+ .mk-alert__description {
40
+ color: var(--mk-text-secondary);
41
+ font-size: var(--mk-text-sm);
42
+ }
43
+
44
+ .mk-alert__close {
45
+ display: inline-flex;
46
+ align-items: center;
47
+ justify-content: center;
48
+ width: 20px;
49
+ height: 20px;
50
+ padding: 0;
51
+ margin: 0;
52
+ font-size: 18px;
53
+ line-height: 1;
54
+ color: var(--mk-text-tertiary);
55
+ background: transparent;
56
+ border: none;
57
+ border-radius: var(--mk-radius-sm);
58
+ cursor: pointer;
59
+ flex-shrink: 0;
60
+ transition: var(--mk-transition-colors);
61
+ }
62
+
63
+ .mk-alert__close:hover {
64
+ color: var(--mk-text);
65
+ background: rgba(255, 255, 255, 0.06);
66
+ }
67
+
68
+ /* Types */
69
+ .mk-alert--info {
70
+ border-left-color: var(--mk-info);
71
+ background: var(--mk-info-soft);
72
+ }
73
+ .mk-alert--info .mk-alert__icon {
74
+ color: var(--mk-info);
75
+ }
76
+
77
+ .mk-alert--success {
78
+ border-left-color: var(--mk-success);
79
+ background: var(--mk-success-soft);
80
+ }
81
+ .mk-alert--success .mk-alert__icon {
82
+ color: var(--mk-success);
83
+ }
84
+
85
+ .mk-alert--warning {
86
+ border-left-color: var(--mk-warning);
87
+ background: var(--mk-warning-soft);
88
+ }
89
+ .mk-alert--warning .mk-alert__icon {
90
+ color: var(--mk-warning);
91
+ }
92
+
93
+ .mk-alert--danger {
94
+ border-left-color: var(--mk-danger);
95
+ background: var(--mk-danger-soft);
96
+ }
97
+ .mk-alert--danger .mk-alert__icon {
98
+ color: var(--mk-danger);
99
+ }
100
+
101
+ /* Closing animation */
102
+ .mk-alert.is-closing {
103
+ transform: scale(0.98);
104
+ opacity: 0;
105
+ max-height: 0;
106
+ padding-top: 0;
107
+ padding-bottom: 0;
108
+ margin-top: 0;
109
+ margin-bottom: 0;
110
+ border-width: 0;
111
+ }
@@ -0,0 +1,107 @@
1
+ import '../../styles/element-plus.css'
2
+ import './alert.css'
3
+
4
+ export interface AlertOptions {
5
+ type?: 'info' | 'success' | 'warning' | 'danger'
6
+ title?: string
7
+ description?: string
8
+ closable?: boolean
9
+ showIcon?: boolean
10
+ onClose?: () => void
11
+ }
12
+
13
+ const typeIcons: Record<string, string> = {
14
+ info: 'ℹ',
15
+ success: '✓',
16
+ warning: '⚠',
17
+ danger: '✕',
18
+ }
19
+
20
+ export class MkAlert {
21
+ el: HTMLDivElement
22
+ private options: AlertOptions
23
+
24
+ constructor(container: HTMLElement | string, options: AlertOptions = {}) {
25
+ const parent =
26
+ typeof container === 'string'
27
+ ? document.querySelector(container)!
28
+ : container
29
+
30
+ this.options = {
31
+ type: 'info',
32
+ showIcon: true,
33
+ ...options,
34
+ }
35
+
36
+ this.el = document.createElement('div')
37
+ this.el.className = this.buildClass()
38
+ this.el.setAttribute('role', 'alert')
39
+
40
+ if (this.options.showIcon) {
41
+ const icon = document.createElement('span')
42
+ icon.className = 'mk-alert__icon'
43
+ icon.textContent = typeIcons[this.options.type!]
44
+ this.el.appendChild(icon)
45
+ }
46
+
47
+ const content = document.createElement('div')
48
+ content.className = 'mk-alert__content'
49
+
50
+ if (this.options.title) {
51
+ const title = document.createElement('div')
52
+ title.className = 'mk-alert__title'
53
+ title.textContent = this.options.title
54
+ content.appendChild(title)
55
+ }
56
+
57
+ if (this.options.description) {
58
+ const desc = document.createElement('div')
59
+ desc.className = 'mk-alert__description'
60
+ desc.textContent = this.options.description
61
+ content.appendChild(desc)
62
+ }
63
+
64
+ this.el.appendChild(content)
65
+
66
+ if (this.options.closable) {
67
+ const closeBtn = document.createElement('button')
68
+ closeBtn.className = 'mk-alert__close'
69
+ closeBtn.setAttribute('aria-label', 'Close')
70
+ closeBtn.innerHTML = '×'
71
+ closeBtn.addEventListener('click', () => this.close())
72
+ this.el.appendChild(closeBtn)
73
+ }
74
+
75
+ parent.appendChild(this.el)
76
+ }
77
+
78
+ private buildClass(): string {
79
+ const classes = ['mk-alert', `mk-alert--${this.options.type}`]
80
+ if (this.options.closable) classes.push('is-closable')
81
+ return classes.join(' ')
82
+ }
83
+
84
+ close(): void {
85
+ this.el.classList.add('is-closing')
86
+ this.el.style.pointerEvents = 'none'
87
+ this.el.addEventListener(
88
+ 'transitionend',
89
+ () => {
90
+ this.destroy()
91
+ this.options.onClose?.()
92
+ },
93
+ { once: true }
94
+ )
95
+ }
96
+
97
+ destroy(): void {
98
+ this.el.remove()
99
+ }
100
+ }
101
+
102
+ export function createAlert(
103
+ container: HTMLElement | string,
104
+ options?: AlertOptions
105
+ ): MkAlert {
106
+ return new MkAlert(container, options)
107
+ }
@@ -0,0 +1,112 @@
1
+ .mk-avatar {
2
+ display: inline-flex;
3
+ align-items: center;
4
+ justify-content: center;
5
+ width: 40px;
6
+ height: 40px;
7
+ font-size: var(--mk-text-sm);
8
+ font-weight: var(--mk-font-medium);
9
+ color: var(--mk-text);
10
+ background: var(--mk-surface-raised);
11
+ border: 1px solid var(--mk-border);
12
+ border-radius: var(--mk-radius-full);
13
+ overflow: hidden;
14
+ flex-shrink: 0;
15
+ user-select: none;
16
+ }
17
+
18
+ .mk-avatar__image {
19
+ width: 100%;
20
+ height: 100%;
21
+ object-fit: cover;
22
+ display: block;
23
+ }
24
+
25
+ .mk-avatar__fallback {
26
+ display: flex;
27
+ align-items: center;
28
+ justify-content: center;
29
+ width: 100%;
30
+ height: 100%;
31
+ font-size: inherit;
32
+ font-weight: var(--mk-font-semibold);
33
+ color: var(--mk-text-secondary);
34
+ background: var(--mk-surface-hover);
35
+ }
36
+
37
+ /* Sizes */
38
+ .mk-avatar--small {
39
+ width: 28px;
40
+ height: 28px;
41
+ font-size: var(--mk-text-xs);
42
+ }
43
+
44
+ .mk-avatar--large {
45
+ width: 56px;
46
+ height: 56px;
47
+ font-size: var(--mk-text-md);
48
+ }
49
+
50
+ /* Shape */
51
+ .mk-avatar--square {
52
+ border-radius: var(--mk-radius);
53
+ }
54
+
55
+ /* Group */
56
+ .mk-avatar-group {
57
+ display: inline-flex;
58
+ align-items: center;
59
+ }
60
+
61
+ .mk-avatar-group__item {
62
+ margin-left: -10px;
63
+ transition: transform var(--mk-duration-fast) var(--mk-ease-default);
64
+ }
65
+
66
+ .mk-avatar-group__item:first-child {
67
+ margin-left: 0;
68
+ }
69
+
70
+ .mk-avatar-group__item:hover {
71
+ transform: translateY(-2px);
72
+ z-index: 1;
73
+ }
74
+
75
+ .mk-avatar-group__item .mk-avatar {
76
+ border: 2px solid var(--mk-bg);
77
+ }
78
+
79
+ .mk-avatar-group__counter {
80
+ display: inline-flex;
81
+ align-items: center;
82
+ justify-content: center;
83
+ width: 40px;
84
+ height: 40px;
85
+ margin-left: -10px;
86
+ font-size: var(--mk-text-xs);
87
+ font-weight: var(--mk-font-semibold);
88
+ color: var(--mk-text-secondary);
89
+ background: var(--mk-surface-raised);
90
+ border: 2px solid var(--mk-bg);
91
+ border-radius: var(--mk-radius-full);
92
+ flex-shrink: 0;
93
+ }
94
+
95
+ .mk-avatar-group--small .mk-avatar-group__counter {
96
+ width: 28px;
97
+ height: 28px;
98
+ }
99
+
100
+ .mk-avatar-group--large .mk-avatar-group__counter {
101
+ width: 56px;
102
+ height: 56px;
103
+ font-size: var(--mk-text-sm);
104
+ }
105
+
106
+ .mk-avatar-group--small .mk-avatar-group__item {
107
+ margin-left: -7px;
108
+ }
109
+
110
+ .mk-avatar-group--large .mk-avatar-group__item {
111
+ margin-left: -14px;
112
+ }
@@ -0,0 +1,175 @@
1
+ import '../../styles/element-plus.css'
2
+ import './avatar.css'
3
+
4
+ export interface AvatarOptions {
5
+ src?: string
6
+ text?: string
7
+ size?: 'small' | 'default' | 'large'
8
+ shape?: 'circle' | 'square'
9
+ icon?: string
10
+ }
11
+
12
+ export class MkAvatar {
13
+ el: HTMLDivElement
14
+ private options: AvatarOptions
15
+
16
+ constructor(container: HTMLElement | string, options: AvatarOptions = {}) {
17
+ const parent =
18
+ typeof container === 'string'
19
+ ? document.querySelector(container)!
20
+ : container
21
+
22
+ this.options = {
23
+ size: 'default',
24
+ shape: 'circle',
25
+ ...options,
26
+ }
27
+
28
+ this.el = document.createElement('div')
29
+ this.el.className = this.buildClass()
30
+
31
+ if (this.options.src) {
32
+ const img = document.createElement('img')
33
+ img.className = 'mk-avatar__image'
34
+ img.src = this.options.src
35
+ img.alt = this.options.text || ''
36
+ img.addEventListener('error', () => this.showFallback())
37
+ this.el.appendChild(img)
38
+ } else {
39
+ this.showFallback()
40
+ }
41
+
42
+ parent.appendChild(this.el)
43
+ }
44
+
45
+ private buildClass(): string {
46
+ const classes = ['mk-avatar']
47
+ if (this.options.size && this.options.size !== 'default') {
48
+ classes.push(`mk-avatar--${this.options.size}`)
49
+ }
50
+ if (this.options.shape && this.options.shape !== 'circle') {
51
+ classes.push(`mk-avatar--${this.options.shape}`)
52
+ }
53
+ return classes.join(' ')
54
+ }
55
+
56
+ private showFallback(): void {
57
+ const existing = this.el.querySelector('.mk-avatar__image')
58
+ if (existing) existing.remove()
59
+
60
+ const fallback = document.createElement('span')
61
+ fallback.className = 'mk-avatar__fallback'
62
+
63
+ if (this.options.icon) {
64
+ fallback.textContent = this.options.icon
65
+ } else if (this.options.text) {
66
+ fallback.textContent = this.options.text
67
+ .split(' ')
68
+ .map((w) => w[0])
69
+ .slice(0, 2)
70
+ .join('')
71
+ .toUpperCase()
72
+ } else {
73
+ fallback.textContent = '?'
74
+ }
75
+
76
+ this.el.appendChild(fallback)
77
+ }
78
+
79
+ setSrc(src: string): void {
80
+ this.options.src = src
81
+ const existingFallback = this.el.querySelector('.mk-avatar__fallback')
82
+ if (existingFallback) existingFallback.remove()
83
+
84
+ let img = this.el.querySelector('.mk-avatar__image') as HTMLImageElement | null
85
+ if (!img) {
86
+ img = document.createElement('img')
87
+ img.className = 'mk-avatar__image'
88
+ img.alt = this.options.text || ''
89
+ img.addEventListener('error', () => this.showFallback())
90
+ this.el.appendChild(img)
91
+ }
92
+ img.src = src
93
+ }
94
+
95
+ setText(text: string): void {
96
+ this.options.text = text
97
+ if (!this.options.src) {
98
+ this.showFallback()
99
+ }
100
+ }
101
+
102
+ destroy(): void {
103
+ this.el.remove()
104
+ }
105
+ }
106
+
107
+ export interface AvatarGroupOptions {
108
+ avatars: AvatarOptions[]
109
+ max?: number
110
+ size?: 'small' | 'default' | 'large'
111
+ }
112
+
113
+ export class MkAvatarGroup {
114
+ el: HTMLDivElement
115
+ private options: AvatarGroupOptions
116
+
117
+ constructor(container: HTMLElement | string, options: AvatarGroupOptions) {
118
+ const parent =
119
+ typeof container === 'string'
120
+ ? document.querySelector(container)!
121
+ : container
122
+
123
+ this.options = {
124
+ size: 'default',
125
+ ...options,
126
+ }
127
+
128
+ this.el = document.createElement('div')
129
+ this.el.className = 'mk-avatar-group'
130
+ if (this.options.size && this.options.size !== 'default') {
131
+ this.el.classList.add(`mk-avatar-group--${this.options.size}`)
132
+ }
133
+
134
+ const display = this.options.max
135
+ ? this.options.avatars.slice(0, this.options.max)
136
+ : this.options.avatars
137
+
138
+ display.forEach((avatarOpts) => {
139
+ const wrapper = document.createElement('div')
140
+ wrapper.className = 'mk-avatar-group__item'
141
+ new MkAvatar(wrapper, { size: this.options.size, ...avatarOpts })
142
+ this.el.appendChild(wrapper)
143
+ })
144
+
145
+ const remaining = this.options.max
146
+ ? this.options.avatars.length - this.options.max
147
+ : 0
148
+ if (remaining > 0) {
149
+ const counter = document.createElement('div')
150
+ counter.className = 'mk-avatar-group__counter'
151
+ counter.textContent = `+${remaining}`
152
+ this.el.appendChild(counter)
153
+ }
154
+
155
+ parent.appendChild(this.el)
156
+ }
157
+
158
+ destroy(): void {
159
+ this.el.remove()
160
+ }
161
+ }
162
+
163
+ export function createAvatar(
164
+ container: HTMLElement | string,
165
+ options?: AvatarOptions
166
+ ): MkAvatar {
167
+ return new MkAvatar(container, options)
168
+ }
169
+
170
+ export function createAvatarGroup(
171
+ container: HTMLElement | string,
172
+ options: AvatarGroupOptions
173
+ ): MkAvatarGroup {
174
+ return new MkAvatarGroup(container, options)
175
+ }
@@ -0,0 +1,31 @@
1
+ .mk-breadcrumb {
2
+ display: flex;
3
+ align-items: center;
4
+ flex-wrap: wrap;
5
+ font-size: var(--mk-text-sm);
6
+ color: var(--mk-text-secondary);
7
+ }
8
+
9
+ .mk-breadcrumb__item {
10
+ transition: var(--mk-transition-colors);
11
+ }
12
+
13
+ .mk-breadcrumb__item.is-link {
14
+ color: var(--mk-text-secondary);
15
+ cursor: pointer;
16
+ }
17
+
18
+ .mk-breadcrumb__item.is-link:hover {
19
+ color: var(--mk-primary);
20
+ }
21
+
22
+ .mk-breadcrumb__item.is-current {
23
+ color: var(--mk-text);
24
+ font-weight: var(--mk-font-medium);
25
+ }
26
+
27
+ .mk-breadcrumb__separator {
28
+ margin: 0 8px;
29
+ color: var(--mk-text-tertiary);
30
+ user-select: none;
31
+ }