@luanlu/mk-motion 1.2.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.
- package/package.json +2 -4
- package/src/a11y/focus-trap.ts +64 -0
- package/src/a11y/keyboard.ts +43 -0
- package/src/components/alert/alert.css +111 -0
- package/src/components/alert/alert.ts +107 -0
- package/src/components/avatar/avatar.css +112 -0
- package/src/components/avatar/avatar.ts +175 -0
- package/src/components/breadcrumb/breadcrumb.css +31 -0
- package/src/components/breadcrumb/breadcrumb.ts +71 -0
- package/src/components/button/button.css +108 -0
- package/src/components/button/button.ts +140 -0
- package/src/components/card/card.css +52 -0
- package/src/components/card/card.ts +87 -0
- package/src/components/collapse/collapse.css +76 -0
- package/src/components/collapse/collapse.ts +168 -0
- package/src/components/dialog/dialog.css +78 -0
- package/src/components/dialog/dialog.ts +164 -0
- package/src/components/drawer/drawer.css +73 -0
- package/src/components/drawer/drawer.ts +131 -0
- package/src/components/empty/empty.css +36 -0
- package/src/components/empty/empty.ts +85 -0
- package/src/components/form/checkbox.css +56 -0
- package/src/components/form/checkbox.ts +119 -0
- package/src/components/form/radio.css +57 -0
- package/src/components/form/radio.ts +153 -0
- package/src/components/form/select.css +91 -0
- package/src/components/form/select.ts +174 -0
- package/src/components/form/slider.css +56 -0
- package/src/components/form/slider.ts +148 -0
- package/src/components/input/input.css +92 -0
- package/src/components/input/input.ts +162 -0
- package/src/components/layout/divider.css +32 -0
- package/src/components/layout/divider.ts +42 -0
- package/src/components/layout/row.css +64 -0
- package/src/components/layout/row.ts +57 -0
- package/src/components/layout/space.css +14 -0
- package/src/components/layout/space.ts +48 -0
- package/src/components/loading/loading.css +37 -0
- package/src/components/loading/loading.ts +46 -0
- package/src/components/menu/menu.css +121 -0
- package/src/components/menu/menu.ts +187 -0
- package/src/components/message/message.css +64 -0
- package/src/components/message/message.ts +96 -0
- package/src/components/popover/popover.css +73 -0
- package/src/components/popover/popover.ts +279 -0
- package/src/components/progress/progress.css +112 -0
- package/src/components/progress/progress.ts +171 -0
- package/src/components/steps/steps.css +127 -0
- package/src/components/steps/steps.ts +102 -0
- package/src/components/styles/components.css +28 -0
- package/src/components/styles/reset.css +24 -0
- package/src/components/styles/tokens.css +248 -0
- package/src/components/styles/variables.css +24 -0
- package/src/components/switch/switch.css +53 -0
- package/src/components/switch/switch.ts +103 -0
- package/src/components/table/table.css +192 -0
- package/src/components/table/table.ts +370 -0
- package/src/components/tabs/tabs.css +138 -0
- package/src/components/tabs/tabs.ts +211 -0
- package/src/components/tag/tag.css +123 -0
- package/src/components/tag/tag.ts +112 -0
- package/src/components/tooltip/tooltip.css +66 -0
- package/src/components/tooltip/tooltip.ts +185 -0
- package/src/core/animator.ts +124 -0
- package/src/core/timeline.ts +128 -0
- package/src/core/utils.ts +47 -0
- package/src/effects/glitch.ts +99 -0
- package/src/effects/particle.ts +134 -0
- package/src/effects/text-split.ts +95 -0
- package/src/effects/wave-text.ts +88 -0
- package/src/gesture/draggable.ts +130 -0
- package/src/gesture/spring.ts +152 -0
- package/src/index.ts +162 -0
- package/src/interactive/coverflow.ts +100 -0
- package/src/interactive/cursor-trail.ts +113 -0
- package/src/interactive/flip-card.ts +114 -0
- package/src/interactive/magnetic.ts +121 -0
- package/src/micro/hover-lift.ts +94 -0
- package/src/micro/ripple.ts +130 -0
- package/src/motion/component-motion.ts +177 -0
- package/src/presets/index.ts +69 -0
- package/src/scroll/scroll-trigger.ts +104 -0
- package/src/styles/animations.css +135 -0
- package/src/styles/element-plus.css +174 -0
- package/src/text/count-up.ts +108 -0
- package/src/text/typewriter.ts +109 -0
- package/src/theme/dark.css +19 -0
- package/src/theme/light.css +19 -0
- package/src/theme/theme.ts +65 -0
- package/src/transitions/blur-reveal.ts +92 -0
- package/src/transitions/collapse.ts +112 -0
- package/src/transitions/lazy-image.ts +87 -0
- package/src/transitions/list.ts +75 -0
- package/src/transitions/loading.ts +95 -0
- package/src/transitions/parallax.ts +60 -0
- package/src/transitions/shimmer.ts +105 -0
- package/src/transitions/toast.ts +151 -0
- package/src/types.d.ts +4 -0
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import './checkbox.css'
|
|
2
|
+
import { onKey, Keys } from '../../a11y/keyboard.ts'
|
|
3
|
+
|
|
4
|
+
export interface CheckboxOptions {
|
|
5
|
+
label?: string
|
|
6
|
+
checked?: boolean
|
|
7
|
+
disabled?: boolean
|
|
8
|
+
onChange?: (checked: boolean) => void
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export class MkCheckbox {
|
|
12
|
+
el: HTMLLabelElement
|
|
13
|
+
private options: CheckboxOptions
|
|
14
|
+
private _checked: boolean
|
|
15
|
+
private _cleanupKey?: () => void
|
|
16
|
+
|
|
17
|
+
constructor(container: HTMLElement | string, options: CheckboxOptions = {}) {
|
|
18
|
+
const parent =
|
|
19
|
+
typeof container === 'string'
|
|
20
|
+
? document.querySelector(container)!
|
|
21
|
+
: container
|
|
22
|
+
|
|
23
|
+
this.options = { checked: false, ...options }
|
|
24
|
+
this._checked = this.options.checked!
|
|
25
|
+
|
|
26
|
+
this.el = document.createElement('label')
|
|
27
|
+
this.el.className = 'mk-checkbox'
|
|
28
|
+
if (this._checked) this.el.classList.add('is-checked')
|
|
29
|
+
if (this.options.disabled) this.el.classList.add('is-disabled')
|
|
30
|
+
this.el.setAttribute('role', 'checkbox')
|
|
31
|
+
this.el.setAttribute('aria-checked', String(this._checked))
|
|
32
|
+
if (!this.options.disabled) {
|
|
33
|
+
this.el.setAttribute('tabindex', '0')
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const box = document.createElement('span')
|
|
37
|
+
box.className = 'mk-checkbox__input'
|
|
38
|
+
const check = document.createElement('span')
|
|
39
|
+
check.className = 'mk-checkbox__check'
|
|
40
|
+
check.textContent = '✓'
|
|
41
|
+
box.appendChild(check)
|
|
42
|
+
this.el.appendChild(box)
|
|
43
|
+
|
|
44
|
+
if (this.options.label) {
|
|
45
|
+
const text = document.createElement('span')
|
|
46
|
+
text.textContent = this.options.label
|
|
47
|
+
this.el.appendChild(text)
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
this.el.addEventListener('click', () => {
|
|
51
|
+
if (this.options.disabled) return
|
|
52
|
+
this.toggle()
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
this._cleanupKey = onKey(this.el, [
|
|
56
|
+
{ key: Keys.Space, handler: () => this.toggle() },
|
|
57
|
+
])
|
|
58
|
+
|
|
59
|
+
parent.appendChild(this.el)
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
get checked(): boolean {
|
|
63
|
+
return this._checked
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
set checked(v: boolean) {
|
|
67
|
+
this._checked = v
|
|
68
|
+
this.el.classList.toggle('is-checked', v)
|
|
69
|
+
this.el.setAttribute('aria-checked', String(v))
|
|
70
|
+
this.options.onChange?.(v)
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
toggle(): void {
|
|
74
|
+
this.checked = !this._checked
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
destroy(): void {
|
|
78
|
+
this._cleanupKey?.()
|
|
79
|
+
this.el.remove()
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export function createCheckbox(
|
|
84
|
+
container: HTMLElement | string,
|
|
85
|
+
options?: CheckboxOptions
|
|
86
|
+
): MkCheckbox {
|
|
87
|
+
return new MkCheckbox(container, options)
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/* ===== Checkbox Group ===== */
|
|
91
|
+
export class MkCheckboxGroup {
|
|
92
|
+
el: HTMLDivElement
|
|
93
|
+
private checkboxes: MkCheckbox[] = []
|
|
94
|
+
|
|
95
|
+
constructor(container: HTMLElement | string) {
|
|
96
|
+
const parent =
|
|
97
|
+
typeof container === 'string'
|
|
98
|
+
? document.querySelector(container)!
|
|
99
|
+
: container
|
|
100
|
+
|
|
101
|
+
this.el = document.createElement('div')
|
|
102
|
+
this.el.className = 'mk-checkbox-group'
|
|
103
|
+
parent.appendChild(this.el)
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
add(options: CheckboxOptions): MkCheckbox {
|
|
107
|
+
const cb = new MkCheckbox(this.el, options)
|
|
108
|
+
this.checkboxes.push(cb)
|
|
109
|
+
return cb
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
getValues(): boolean[] {
|
|
113
|
+
return this.checkboxes.map((cb) => cb.checked)
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
destroy(): void {
|
|
117
|
+
this.el.remove()
|
|
118
|
+
}
|
|
119
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
.mk-radio {
|
|
2
|
+
display: inline-flex;
|
|
3
|
+
align-items: center;
|
|
4
|
+
gap: 8px;
|
|
5
|
+
cursor: pointer;
|
|
6
|
+
user-select: none;
|
|
7
|
+
font-size: 13px;
|
|
8
|
+
color: var(--mk-text);
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
.mk-radio__input {
|
|
12
|
+
width: 16px;
|
|
13
|
+
height: 16px;
|
|
14
|
+
border: 1.5px solid var(--mk-border);
|
|
15
|
+
border-radius: 50%;
|
|
16
|
+
display: flex;
|
|
17
|
+
align-items: center;
|
|
18
|
+
justify-content: center;
|
|
19
|
+
transition: var(--mk-transition);
|
|
20
|
+
flex-shrink: 0;
|
|
21
|
+
background: var(--mk-surface);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
.mk-radio:hover .mk-radio__input {
|
|
25
|
+
border-color: var(--mk-border-hover);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
.mk-radio.is-checked .mk-radio__input {
|
|
29
|
+
border-color: var(--mk-primary);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
.mk-radio__dot {
|
|
33
|
+
width: 8px;
|
|
34
|
+
height: 8px;
|
|
35
|
+
border-radius: 50%;
|
|
36
|
+
background: var(--mk-primary);
|
|
37
|
+
opacity: 0;
|
|
38
|
+
transform: scale(0);
|
|
39
|
+
transition: all 0.15s ease;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
.mk-radio.is-checked .mk-radio__dot {
|
|
43
|
+
opacity: 1;
|
|
44
|
+
transform: scale(1);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
.mk-radio.is-disabled {
|
|
48
|
+
opacity: 0.4;
|
|
49
|
+
cursor: not-allowed;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/* Group */
|
|
53
|
+
.mk-radio-group {
|
|
54
|
+
display: flex;
|
|
55
|
+
flex-wrap: wrap;
|
|
56
|
+
gap: 16px;
|
|
57
|
+
}
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
import './radio.css'
|
|
2
|
+
import { onKey, Keys } from '../../a11y/keyboard.ts'
|
|
3
|
+
|
|
4
|
+
export interface RadioOptions {
|
|
5
|
+
label?: string
|
|
6
|
+
value: string | number
|
|
7
|
+
checked?: boolean
|
|
8
|
+
disabled?: boolean
|
|
9
|
+
onChange?: (value: string | number) => void
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export class MkRadio {
|
|
13
|
+
el: HTMLLabelElement
|
|
14
|
+
private options: RadioOptions
|
|
15
|
+
private _checked: boolean
|
|
16
|
+
|
|
17
|
+
constructor(container: HTMLElement | string, options: RadioOptions) {
|
|
18
|
+
const parent =
|
|
19
|
+
typeof container === 'string'
|
|
20
|
+
? document.querySelector(container)!
|
|
21
|
+
: container
|
|
22
|
+
|
|
23
|
+
this.options = { checked: false, ...options }
|
|
24
|
+
this._checked = this.options.checked!
|
|
25
|
+
|
|
26
|
+
this.el = document.createElement('label')
|
|
27
|
+
this.el.className = 'mk-radio'
|
|
28
|
+
if (this._checked) this.el.classList.add('is-checked')
|
|
29
|
+
if (this.options.disabled) this.el.classList.add('is-disabled')
|
|
30
|
+
this.el.setAttribute('role', 'radio')
|
|
31
|
+
this.el.setAttribute('aria-checked', String(this._checked))
|
|
32
|
+
|
|
33
|
+
const box = document.createElement('span')
|
|
34
|
+
box.className = 'mk-radio__input'
|
|
35
|
+
const dot = document.createElement('span')
|
|
36
|
+
dot.className = 'mk-radio__dot'
|
|
37
|
+
box.appendChild(dot)
|
|
38
|
+
this.el.appendChild(box)
|
|
39
|
+
|
|
40
|
+
if (this.options.label) {
|
|
41
|
+
const text = document.createElement('span')
|
|
42
|
+
text.textContent = this.options.label
|
|
43
|
+
this.el.appendChild(text)
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
this.el.addEventListener('click', () => {
|
|
47
|
+
if (this.options.disabled) return
|
|
48
|
+
this.options.onChange?.(this.options.value)
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
parent.appendChild(this.el)
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
setChecked(checked: boolean): void {
|
|
55
|
+
this._checked = checked
|
|
56
|
+
this.el.classList.toggle('is-checked', checked)
|
|
57
|
+
this.el.setAttribute('aria-checked', String(checked))
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
getValue(): string | number {
|
|
61
|
+
return this.options.value
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
destroy(): void {
|
|
65
|
+
this.el.remove()
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/* ===== Radio Group ===== */
|
|
70
|
+
export class MkRadioGroup {
|
|
71
|
+
el: HTMLDivElement
|
|
72
|
+
private radios: MkRadio[] = []
|
|
73
|
+
private _value: string | number | undefined
|
|
74
|
+
private _cleanupKey?: () => void
|
|
75
|
+
|
|
76
|
+
constructor(
|
|
77
|
+
container: HTMLElement | string,
|
|
78
|
+
options: { value?: string | number; onChange?: (value: string | number) => void } = {}
|
|
79
|
+
) {
|
|
80
|
+
const parent =
|
|
81
|
+
typeof container === 'string'
|
|
82
|
+
? document.querySelector(container)!
|
|
83
|
+
: container
|
|
84
|
+
|
|
85
|
+
this._value = options.value
|
|
86
|
+
|
|
87
|
+
this.el = document.createElement('div')
|
|
88
|
+
this.el.className = 'mk-radio-group'
|
|
89
|
+
this.el.setAttribute('role', 'radiogroup')
|
|
90
|
+
parent.appendChild(this.el)
|
|
91
|
+
|
|
92
|
+
this._cleanupKey = onKey(this.el, [
|
|
93
|
+
{ key: Keys.ArrowUp, handler: () => this.focusRadio(-1) },
|
|
94
|
+
{ key: Keys.ArrowLeft, handler: () => this.focusRadio(-1) },
|
|
95
|
+
{ key: Keys.ArrowDown, handler: () => this.focusRadio(1) },
|
|
96
|
+
{ key: Keys.ArrowRight, handler: () => this.focusRadio(1) },
|
|
97
|
+
])
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
add(radioOptions: Omit<RadioOptions, 'onChange'>): MkRadio {
|
|
101
|
+
const radio = new MkRadio(this.el, {
|
|
102
|
+
...radioOptions,
|
|
103
|
+
onChange: (value) => {
|
|
104
|
+
this.setValue(value)
|
|
105
|
+
},
|
|
106
|
+
})
|
|
107
|
+
const checked = radioOptions.value === this._value
|
|
108
|
+
radio.setChecked(checked)
|
|
109
|
+
if (this._value === undefined && this.radios.length === 0) {
|
|
110
|
+
radio.el.setAttribute('tabindex', '0')
|
|
111
|
+
} else {
|
|
112
|
+
radio.el.setAttribute('tabindex', checked ? '0' : '-1')
|
|
113
|
+
}
|
|
114
|
+
this.radios.push(radio)
|
|
115
|
+
return radio
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
setValue(value: string | number): void {
|
|
119
|
+
this._value = value
|
|
120
|
+
this.radios.forEach((r) => {
|
|
121
|
+
const checked = r.getValue() === value
|
|
122
|
+
r.setChecked(checked)
|
|
123
|
+
r.el.setAttribute('tabindex', checked ? '0' : '-1')
|
|
124
|
+
})
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
getValue(): string | number | undefined {
|
|
128
|
+
return this._value
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
private focusRadio(dir: number): void {
|
|
132
|
+
let current = this.radios.findIndex((r) => r.el.getAttribute('tabindex') === '0')
|
|
133
|
+
if (current === -1) current = this.radios.findIndex((r) => r.el.classList.contains('is-checked'))
|
|
134
|
+
if (current === -1) current = 0
|
|
135
|
+
let next = current
|
|
136
|
+
for (let i = 0; i < this.radios.length; i++) {
|
|
137
|
+
next = (next + dir + this.radios.length) % this.radios.length
|
|
138
|
+
if (!this.radios[next].el.classList.contains('is-disabled')) {
|
|
139
|
+
break
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
if (next !== current || this.radios.length === 1) {
|
|
143
|
+
this.radios.forEach((r, i) => r.el.setAttribute('tabindex', i === next ? '0' : '-1'))
|
|
144
|
+
this.radios[next].el.focus()
|
|
145
|
+
this.setValue(this.radios[next].getValue())
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
destroy(): void {
|
|
150
|
+
this._cleanupKey?.()
|
|
151
|
+
this.el.remove()
|
|
152
|
+
}
|
|
153
|
+
}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
.mk-select {
|
|
2
|
+
position: relative;
|
|
3
|
+
display: inline-flex;
|
|
4
|
+
width: 100%;
|
|
5
|
+
max-width: 320px;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
.mk-select__trigger {
|
|
9
|
+
width: 100%;
|
|
10
|
+
height: 36px;
|
|
11
|
+
padding: 0 32px 0 12px;
|
|
12
|
+
font-size: 13px;
|
|
13
|
+
color: var(--mk-text);
|
|
14
|
+
background: var(--mk-surface);
|
|
15
|
+
border: 1px solid var(--mk-border);
|
|
16
|
+
border-radius: var(--mk-radius);
|
|
17
|
+
cursor: pointer;
|
|
18
|
+
display: flex;
|
|
19
|
+
align-items: center;
|
|
20
|
+
outline: none;
|
|
21
|
+
transition: var(--mk-transition);
|
|
22
|
+
user-select: none;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
.mk-select__trigger:hover {
|
|
26
|
+
border-color: var(--mk-border-hover);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
.mk-select__trigger.is-open {
|
|
30
|
+
border-color: var(--mk-primary);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
.mk-select__arrow {
|
|
34
|
+
position: absolute;
|
|
35
|
+
right: 10px;
|
|
36
|
+
color: var(--mk-text-tertiary);
|
|
37
|
+
font-size: 10px;
|
|
38
|
+
transition: transform 0.2s;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
.mk-select__trigger.is-open .mk-select__arrow {
|
|
42
|
+
transform: rotate(180deg);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
.mk-select__dropdown {
|
|
46
|
+
position: absolute;
|
|
47
|
+
top: calc(100% + 4px);
|
|
48
|
+
left: 0;
|
|
49
|
+
right: 0;
|
|
50
|
+
background: var(--mk-surface);
|
|
51
|
+
border: 1px solid var(--mk-border);
|
|
52
|
+
border-radius: var(--mk-radius);
|
|
53
|
+
z-index: 100;
|
|
54
|
+
max-height: 240px;
|
|
55
|
+
overflow-y: auto;
|
|
56
|
+
opacity: 0;
|
|
57
|
+
transform: translateY(-4px);
|
|
58
|
+
pointer-events: none;
|
|
59
|
+
transition: all 0.15s ease;
|
|
60
|
+
box-shadow: var(--mk-shadow-lg);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
.mk-select__dropdown.is-open {
|
|
64
|
+
opacity: 1;
|
|
65
|
+
transform: translateY(0);
|
|
66
|
+
pointer-events: auto;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
.mk-select__option {
|
|
70
|
+
padding: 8px 12px;
|
|
71
|
+
font-size: 13px;
|
|
72
|
+
color: var(--mk-text);
|
|
73
|
+
cursor: pointer;
|
|
74
|
+
transition: var(--mk-transition);
|
|
75
|
+
display: flex;
|
|
76
|
+
align-items: center;
|
|
77
|
+
gap: 8px;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
.mk-select__option:hover {
|
|
81
|
+
background: var(--mk-surface-hover);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
.mk-select__option.is-selected {
|
|
85
|
+
background: rgba(99,102,241,0.1);
|
|
86
|
+
color: var(--mk-primary);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
.mk-select__placeholder {
|
|
90
|
+
color: var(--mk-text-tertiary);
|
|
91
|
+
}
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
import './select.css'
|
|
2
|
+
import { onKey, Keys } from '../../a11y/keyboard.ts'
|
|
3
|
+
|
|
4
|
+
export interface SelectOption {
|
|
5
|
+
label: string
|
|
6
|
+
value: string | number
|
|
7
|
+
disabled?: boolean
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export interface SelectOptions {
|
|
11
|
+
placeholder?: string
|
|
12
|
+
options: SelectOption[]
|
|
13
|
+
value?: string | number
|
|
14
|
+
onChange?: (value: string | number) => void
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export class MkSelect {
|
|
18
|
+
el: HTMLDivElement
|
|
19
|
+
private trigger: HTMLDivElement
|
|
20
|
+
private dropdown: HTMLDivElement
|
|
21
|
+
private options: SelectOptions
|
|
22
|
+
private _value: string | number | undefined
|
|
23
|
+
private isOpen = false
|
|
24
|
+
private _cleanupKey?: () => void
|
|
25
|
+
|
|
26
|
+
constructor(container: HTMLElement | string, options: SelectOptions) {
|
|
27
|
+
const parent =
|
|
28
|
+
typeof container === 'string'
|
|
29
|
+
? document.querySelector(container)!
|
|
30
|
+
: container
|
|
31
|
+
|
|
32
|
+
this.options = options
|
|
33
|
+
this._value = options.value
|
|
34
|
+
|
|
35
|
+
this.el = document.createElement('div')
|
|
36
|
+
this.el.className = 'mk-select'
|
|
37
|
+
|
|
38
|
+
this.trigger = document.createElement('div')
|
|
39
|
+
this.trigger.className = 'mk-select__trigger'
|
|
40
|
+
this.trigger.setAttribute('role', 'combobox')
|
|
41
|
+
this.trigger.setAttribute('aria-haspopup', 'listbox')
|
|
42
|
+
this.trigger.setAttribute('aria-expanded', 'false')
|
|
43
|
+
this.trigger.setAttribute('tabindex', '0')
|
|
44
|
+
this.trigger.addEventListener('click', () => this.toggle())
|
|
45
|
+
|
|
46
|
+
const label = document.createElement('span')
|
|
47
|
+
label.className = 'mk-select__label'
|
|
48
|
+
this.updateLabel(label)
|
|
49
|
+
this.trigger.appendChild(label)
|
|
50
|
+
|
|
51
|
+
const arrow = document.createElement('span')
|
|
52
|
+
arrow.className = 'mk-select__arrow'
|
|
53
|
+
arrow.textContent = '▼'
|
|
54
|
+
this.trigger.appendChild(arrow)
|
|
55
|
+
|
|
56
|
+
this.dropdown = document.createElement('div')
|
|
57
|
+
this.dropdown.className = 'mk-select__dropdown'
|
|
58
|
+
this.dropdown.setAttribute('role', 'listbox')
|
|
59
|
+
|
|
60
|
+
options.options.forEach((opt) => {
|
|
61
|
+
const item = document.createElement('div')
|
|
62
|
+
item.className = 'mk-select__option'
|
|
63
|
+
item.textContent = opt.label
|
|
64
|
+
item.setAttribute('role', 'option')
|
|
65
|
+
item.setAttribute('aria-selected', String(opt.value === this._value))
|
|
66
|
+
if (opt.disabled) {
|
|
67
|
+
item.setAttribute('aria-disabled', 'true')
|
|
68
|
+
item.style.opacity = '0.4'
|
|
69
|
+
item.style.pointerEvents = 'none'
|
|
70
|
+
}
|
|
71
|
+
item.addEventListener('click', () => {
|
|
72
|
+
this.setValue(opt.value)
|
|
73
|
+
this.close()
|
|
74
|
+
})
|
|
75
|
+
this.dropdown.appendChild(item)
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
this.el.appendChild(this.trigger)
|
|
79
|
+
this.el.appendChild(this.dropdown)
|
|
80
|
+
parent.appendChild(this.el)
|
|
81
|
+
|
|
82
|
+
this._cleanupKey = onKey(this.trigger, [
|
|
83
|
+
{ key: Keys.ArrowDown, handler: () => this.moveSelection(1) },
|
|
84
|
+
{ key: Keys.ArrowUp, handler: () => this.moveSelection(-1) },
|
|
85
|
+
{ key: Keys.Enter, handler: () => {
|
|
86
|
+
if (this.isOpen) this.close()
|
|
87
|
+
else this.open()
|
|
88
|
+
}},
|
|
89
|
+
{ key: Keys.Escape, handler: () => this.close() },
|
|
90
|
+
])
|
|
91
|
+
|
|
92
|
+
document.addEventListener('click', (e) => {
|
|
93
|
+
if (!this.el.contains(e.target as Node)) this.close()
|
|
94
|
+
})
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
private updateLabel(labelEl: HTMLSpanElement): void {
|
|
98
|
+
const selected = this.options.options.find((o) => o.value === this._value)
|
|
99
|
+
if (selected) {
|
|
100
|
+
labelEl.textContent = selected.label
|
|
101
|
+
labelEl.classList.remove('mk-select__placeholder')
|
|
102
|
+
} else {
|
|
103
|
+
labelEl.textContent = this.options.placeholder || '请选择'
|
|
104
|
+
labelEl.classList.add('mk-select__placeholder')
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
get value(): string | number | undefined {
|
|
109
|
+
return this._value
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
setValue(v: string | number): void {
|
|
113
|
+
this._value = v
|
|
114
|
+
const label = this.trigger.querySelector('.mk-select__label') as HTMLSpanElement
|
|
115
|
+
this.updateLabel(label)
|
|
116
|
+
this.renderOptions()
|
|
117
|
+
this.options.onChange?.(v)
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
private renderOptions(): void {
|
|
121
|
+
const items = this.dropdown.querySelectorAll('.mk-select__option')
|
|
122
|
+
items.forEach((item, i) => {
|
|
123
|
+
const opt = this.options.options[i]
|
|
124
|
+
item.classList.toggle('is-selected', opt.value === this._value)
|
|
125
|
+
item.setAttribute('aria-selected', String(opt.value === this._value))
|
|
126
|
+
})
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
private toggle(): void {
|
|
130
|
+
this.isOpen ? this.close() : this.open()
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
open(): void {
|
|
134
|
+
this.isOpen = true
|
|
135
|
+
this.trigger.setAttribute('aria-expanded', 'true')
|
|
136
|
+
this.trigger.classList.add('is-open')
|
|
137
|
+
this.dropdown.classList.add('is-open')
|
|
138
|
+
this.renderOptions()
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
close(): void {
|
|
142
|
+
this.isOpen = false
|
|
143
|
+
this.trigger.setAttribute('aria-expanded', 'false')
|
|
144
|
+
this.trigger.classList.remove('is-open')
|
|
145
|
+
this.dropdown.classList.remove('is-open')
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
private moveSelection(dir: number): void {
|
|
149
|
+
if (!this.isOpen) {
|
|
150
|
+
this.open()
|
|
151
|
+
}
|
|
152
|
+
const currentIndex = this.options.options.findIndex((o) => o.value === this._value)
|
|
153
|
+
let next = currentIndex
|
|
154
|
+
for (let i = 0; i < this.options.options.length; i++) {
|
|
155
|
+
next = (next + dir + this.options.options.length) % this.options.options.length
|
|
156
|
+
if (!this.options.options[next].disabled) {
|
|
157
|
+
this.setValue(this.options.options[next].value)
|
|
158
|
+
return
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
destroy(): void {
|
|
164
|
+
this._cleanupKey?.()
|
|
165
|
+
this.el.remove()
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
export function createSelect(
|
|
170
|
+
container: HTMLElement | string,
|
|
171
|
+
options: SelectOptions
|
|
172
|
+
): MkSelect {
|
|
173
|
+
return new MkSelect(container, options)
|
|
174
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
.mk-slider {
|
|
2
|
+
display: flex;
|
|
3
|
+
align-items: center;
|
|
4
|
+
gap: 12px;
|
|
5
|
+
width: 100%;
|
|
6
|
+
max-width: 320px;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
.mk-slider__track {
|
|
10
|
+
position: relative;
|
|
11
|
+
flex: 1;
|
|
12
|
+
height: 4px;
|
|
13
|
+
background: var(--mk-border);
|
|
14
|
+
border-radius: 2px;
|
|
15
|
+
cursor: pointer;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
.mk-slider__fill {
|
|
19
|
+
position: absolute;
|
|
20
|
+
left: 0;
|
|
21
|
+
top: 0;
|
|
22
|
+
height: 100%;
|
|
23
|
+
background: var(--mk-primary);
|
|
24
|
+
border-radius: 2px;
|
|
25
|
+
pointer-events: none;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
.mk-slider__thumb {
|
|
29
|
+
position: absolute;
|
|
30
|
+
top: 50%;
|
|
31
|
+
width: 16px;
|
|
32
|
+
height: 16px;
|
|
33
|
+
background: var(--mk-primary);
|
|
34
|
+
border: 2px solid var(--mk-surface);
|
|
35
|
+
border-radius: 50%;
|
|
36
|
+
transform: translate(-50%, -50%);
|
|
37
|
+
cursor: grab;
|
|
38
|
+
transition: transform 0.15s, box-shadow 0.15s;
|
|
39
|
+
box-shadow: var(--mk-shadow-sm);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
.mk-slider__thumb:hover {
|
|
43
|
+
transform: translate(-50%, -50%) scale(1.2);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
.mk-slider__thumb:active {
|
|
47
|
+
cursor: grabbing;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
.mk-slider__value {
|
|
51
|
+
font-size: 13px;
|
|
52
|
+
color: var(--mk-text-secondary);
|
|
53
|
+
min-width: 32px;
|
|
54
|
+
text-align: right;
|
|
55
|
+
font-variant-numeric: tabular-nums;
|
|
56
|
+
}
|