mikuru 1.0.39 → 1.0.40
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/CHANGELOG.md +7 -0
- package/components/MikuruComboboxMulti.mikuru +160 -0
- package/components/MikuruDataToolbar.mikuru +75 -0
- package/components/MikuruEmbedPlayer.mikuru +429 -0
- package/components/MikuruField.mikuru +46 -0
- package/components/MikuruFilterBar.mikuru +80 -0
- package/components/MikuruForm.mikuru +67 -0
- package/components/MikuruFormMessage.mikuru +33 -0
- package/components/MikuruInputOtp.mikuru +91 -0
- package/components/MikuruNumberInput.mikuru +103 -0
- package/components/MikuruPasswordInput.mikuru +107 -0
- package/components/MikuruVirtualList.mikuru +75 -0
- package/package.json +89 -1
- package/types/components/MikuruComboboxMulti.d.ts +17 -0
- package/types/components/MikuruDataToolbar.d.ts +17 -0
- package/types/components/MikuruEmbedPlayer.d.ts +61 -0
- package/types/components/MikuruField.d.ts +12 -0
- package/types/components/MikuruFilterBar.d.ts +19 -0
- package/types/components/MikuruForm.d.ts +12 -0
- package/types/components/MikuruFormMessage.d.ts +10 -0
- package/types/components/MikuruInputOtp.d.ts +12 -0
- package/types/components/MikuruNumberInput.d.ts +14 -0
- package/types/components/MikuruPasswordInput.d.ts +16 -0
- package/types/components/MikuruVirtualList.d.ts +19 -0
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<label class="mikuru-field-shell" :data-required="required ? 'true' : 'false'">
|
|
3
|
+
<span class="field-heading">
|
|
4
|
+
<span>{{ label }}</span>
|
|
5
|
+
<span m-if="required" aria-hidden="true">*</span>
|
|
6
|
+
</span>
|
|
7
|
+
<slot></slot>
|
|
8
|
+
<MikuruFormMessage m-if="error" tone="error" :message="error" />
|
|
9
|
+
<MikuruFormMessage m-else-if="help" tone="neutral" :message="help" />
|
|
10
|
+
</label>
|
|
11
|
+
</template>
|
|
12
|
+
|
|
13
|
+
<script>
|
|
14
|
+
import MikuruFormMessage from "./MikuruFormMessage.mikuru";
|
|
15
|
+
|
|
16
|
+
const {
|
|
17
|
+
label = "Field",
|
|
18
|
+
help = "",
|
|
19
|
+
error = "",
|
|
20
|
+
required = false
|
|
21
|
+
} = defineProps({
|
|
22
|
+
label: String,
|
|
23
|
+
help: String,
|
|
24
|
+
error: String,
|
|
25
|
+
required: Boolean
|
|
26
|
+
});
|
|
27
|
+
</script>
|
|
28
|
+
|
|
29
|
+
<style scoped>
|
|
30
|
+
.mikuru-field-shell {
|
|
31
|
+
display: grid;
|
|
32
|
+
gap: 6px;
|
|
33
|
+
color: #111827;
|
|
34
|
+
font: inherit;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
.field-heading {
|
|
38
|
+
display: inline-flex;
|
|
39
|
+
gap: 4px;
|
|
40
|
+
font-weight: 650;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
.field-heading span:last-child {
|
|
44
|
+
color: #dc2626;
|
|
45
|
+
}
|
|
46
|
+
</style>
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<section class="mikuru-filter-bar" :aria-label="label">
|
|
3
|
+
<MikuruSearchInput
|
|
4
|
+
:label="searchLabel"
|
|
5
|
+
:modelValue="search"
|
|
6
|
+
:placeholder="searchPlaceholder"
|
|
7
|
+
@input="setSearch"
|
|
8
|
+
@submit="submitSearch"
|
|
9
|
+
@clear="clearSearch"
|
|
10
|
+
/>
|
|
11
|
+
<div class="filter-actions">
|
|
12
|
+
<MikuruChip
|
|
13
|
+
m-for="filter in filters"
|
|
14
|
+
:key="filter.value"
|
|
15
|
+
:label="filter.label"
|
|
16
|
+
:tone="filter.value === activeFilter ? 'info' : 'neutral'"
|
|
17
|
+
@click="selectFilter(filter)"
|
|
18
|
+
/>
|
|
19
|
+
<slot></slot>
|
|
20
|
+
</div>
|
|
21
|
+
</section>
|
|
22
|
+
</template>
|
|
23
|
+
|
|
24
|
+
<script>
|
|
25
|
+
import MikuruChip from "./MikuruChip.mikuru";
|
|
26
|
+
import MikuruSearchInput from "./MikuruSearchInput.mikuru";
|
|
27
|
+
|
|
28
|
+
const {
|
|
29
|
+
label = "Filter bar",
|
|
30
|
+
search = "",
|
|
31
|
+
searchLabel = "Search",
|
|
32
|
+
searchPlaceholder = "Search...",
|
|
33
|
+
filters = [],
|
|
34
|
+
activeFilter = ""
|
|
35
|
+
} = defineProps({
|
|
36
|
+
label: String,
|
|
37
|
+
search: String,
|
|
38
|
+
searchLabel: String,
|
|
39
|
+
searchPlaceholder: String,
|
|
40
|
+
filters: Array,
|
|
41
|
+
activeFilter: String
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
const emit = defineEmits(["update:search", "search", "filter", "clear"]);
|
|
45
|
+
|
|
46
|
+
function setSearch(value) {
|
|
47
|
+
emit("update:search", value);
|
|
48
|
+
emit("search", value);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function submitSearch(value) {
|
|
52
|
+
emit("search", value);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function clearSearch() {
|
|
56
|
+
emit("update:search", "");
|
|
57
|
+
emit("clear");
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function selectFilter(filter) {
|
|
61
|
+
emit("filter", filter.value, filter);
|
|
62
|
+
}
|
|
63
|
+
</script>
|
|
64
|
+
|
|
65
|
+
<style scoped>
|
|
66
|
+
.mikuru-filter-bar {
|
|
67
|
+
display: grid;
|
|
68
|
+
gap: 10px;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
.filter-actions {
|
|
72
|
+
display: flex;
|
|
73
|
+
flex-wrap: wrap;
|
|
74
|
+
gap: 6px;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
:deep(.mikuru-chip) {
|
|
78
|
+
cursor: pointer;
|
|
79
|
+
}
|
|
80
|
+
</style>
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<form class="mikuru-form" :aria-label="label" @submit="submitForm" @reset="resetForm">
|
|
3
|
+
<header m-if="title || description">
|
|
4
|
+
<h3 m-if="title">{{ title }}</h3>
|
|
5
|
+
<p m-if="description">{{ description }}</p>
|
|
6
|
+
</header>
|
|
7
|
+
<fieldset :disabled="disabled">
|
|
8
|
+
<slot></slot>
|
|
9
|
+
</fieldset>
|
|
10
|
+
</form>
|
|
11
|
+
</template>
|
|
12
|
+
|
|
13
|
+
<script>
|
|
14
|
+
const {
|
|
15
|
+
label = "Form",
|
|
16
|
+
title = "",
|
|
17
|
+
description = "",
|
|
18
|
+
disabled = false
|
|
19
|
+
} = defineProps({
|
|
20
|
+
label: String,
|
|
21
|
+
title: String,
|
|
22
|
+
description: String,
|
|
23
|
+
disabled: Boolean
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
const emit = defineEmits(["submit", "reset"]);
|
|
27
|
+
|
|
28
|
+
function submitForm(event) {
|
|
29
|
+
event.preventDefault();
|
|
30
|
+
emit("submit", event);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function resetForm(event) {
|
|
34
|
+
emit("reset", event);
|
|
35
|
+
}
|
|
36
|
+
</script>
|
|
37
|
+
|
|
38
|
+
<style scoped>
|
|
39
|
+
.mikuru-form {
|
|
40
|
+
display: grid;
|
|
41
|
+
gap: 12px;
|
|
42
|
+
color: #111827;
|
|
43
|
+
font: inherit;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
header {
|
|
47
|
+
display: grid;
|
|
48
|
+
gap: 4px;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
h3,
|
|
52
|
+
p {
|
|
53
|
+
margin: 0;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
p {
|
|
57
|
+
color: #64748b;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
fieldset {
|
|
61
|
+
display: grid;
|
|
62
|
+
gap: 12px;
|
|
63
|
+
margin: 0;
|
|
64
|
+
border: 0;
|
|
65
|
+
padding: 0;
|
|
66
|
+
}
|
|
67
|
+
</style>
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<p class="mikuru-form-message" :data-tone="tone" role="status">{{ message }}</p>
|
|
3
|
+
</template>
|
|
4
|
+
|
|
5
|
+
<script>
|
|
6
|
+
const {
|
|
7
|
+
message = "",
|
|
8
|
+
tone = "neutral"
|
|
9
|
+
} = defineProps({
|
|
10
|
+
message: String,
|
|
11
|
+
tone: String
|
|
12
|
+
});
|
|
13
|
+
</script>
|
|
14
|
+
|
|
15
|
+
<style scoped>
|
|
16
|
+
.mikuru-form-message {
|
|
17
|
+
margin: 0;
|
|
18
|
+
color: #64748b;
|
|
19
|
+
font-size: 0.88rem;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
.mikuru-form-message[data-tone="error"] {
|
|
23
|
+
color: #b91c1c;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
.mikuru-form-message[data-tone="success"] {
|
|
27
|
+
color: #166534;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
.mikuru-form-message[data-tone="warning"] {
|
|
31
|
+
color: #92400e;
|
|
32
|
+
}
|
|
33
|
+
</style>
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<label class="mikuru-input-otp">
|
|
3
|
+
<span>{{ label }}</span>
|
|
4
|
+
<input
|
|
5
|
+
inputmode="numeric"
|
|
6
|
+
autocomplete="one-time-code"
|
|
7
|
+
:maxlength="length"
|
|
8
|
+
:value="modelValue"
|
|
9
|
+
:placeholder="placeholder"
|
|
10
|
+
:disabled="disabled"
|
|
11
|
+
@input="updateValue($event)"
|
|
12
|
+
/>
|
|
13
|
+
<span class="otp-slots" aria-hidden="true">
|
|
14
|
+
<span m-for="slot in slots" :key="slot.index" :data-filled="slot.value ? 'true' : 'false'">{{ slot.value }}</span>
|
|
15
|
+
</span>
|
|
16
|
+
</label>
|
|
17
|
+
</template>
|
|
18
|
+
|
|
19
|
+
<script>
|
|
20
|
+
import { computed } from "mikuru";
|
|
21
|
+
|
|
22
|
+
const {
|
|
23
|
+
label = "One-time code",
|
|
24
|
+
modelValue = "",
|
|
25
|
+
length = 6,
|
|
26
|
+
disabled = false
|
|
27
|
+
} = defineProps({
|
|
28
|
+
label: String,
|
|
29
|
+
modelValue: String,
|
|
30
|
+
length: Number,
|
|
31
|
+
disabled: Boolean
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
const emit = defineEmits(["update:modelValue", "input", "complete"]);
|
|
35
|
+
const placeholder = computed(() => "0".repeat(length.value));
|
|
36
|
+
const slots = computed(() => {
|
|
37
|
+
return Array.from({ length: length.value }, (_, index) => ({
|
|
38
|
+
index,
|
|
39
|
+
value: modelValue.value[index] || ""
|
|
40
|
+
}));
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
function updateValue(event) {
|
|
44
|
+
const nextValue = event.target.value.replace(/\D/g, "").slice(0, length.value);
|
|
45
|
+
emit("update:modelValue", nextValue);
|
|
46
|
+
emit("input", nextValue);
|
|
47
|
+
if (nextValue.length === length.value) emit("complete", nextValue);
|
|
48
|
+
}
|
|
49
|
+
</script>
|
|
50
|
+
|
|
51
|
+
<style scoped>
|
|
52
|
+
.mikuru-input-otp {
|
|
53
|
+
display: grid;
|
|
54
|
+
gap: 8px;
|
|
55
|
+
color: #111827;
|
|
56
|
+
font: inherit;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
.mikuru-input-otp > span:first-child {
|
|
60
|
+
font-weight: 650;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
input {
|
|
64
|
+
position: absolute;
|
|
65
|
+
width: 1px;
|
|
66
|
+
height: 1px;
|
|
67
|
+
opacity: 0;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
.otp-slots {
|
|
71
|
+
display: flex;
|
|
72
|
+
gap: 8px;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
.otp-slots span {
|
|
76
|
+
display: grid;
|
|
77
|
+
place-items: center;
|
|
78
|
+
width: 38px;
|
|
79
|
+
height: 42px;
|
|
80
|
+
border: 1px solid #cbd5e1;
|
|
81
|
+
border-radius: 8px;
|
|
82
|
+
color: #111827;
|
|
83
|
+
background: #ffffff;
|
|
84
|
+
font-weight: 800;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
.otp-slots span[data-filled="true"] {
|
|
88
|
+
border-color: #2563eb;
|
|
89
|
+
background: #eff6ff;
|
|
90
|
+
}
|
|
91
|
+
</style>
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<label class="mikuru-number-input">
|
|
3
|
+
<span>{{ label }}</span>
|
|
4
|
+
<span class="number-control">
|
|
5
|
+
<button type="button" :disabled="disabled" @click="stepDown">−</button>
|
|
6
|
+
<input
|
|
7
|
+
type="number"
|
|
8
|
+
:value="modelValue"
|
|
9
|
+
:min="min"
|
|
10
|
+
:max="max"
|
|
11
|
+
:step="step"
|
|
12
|
+
:disabled="disabled"
|
|
13
|
+
@input="updateValue($event)"
|
|
14
|
+
/>
|
|
15
|
+
<button type="button" :disabled="disabled" @click="stepUp">+</button>
|
|
16
|
+
</span>
|
|
17
|
+
</label>
|
|
18
|
+
</template>
|
|
19
|
+
|
|
20
|
+
<script>
|
|
21
|
+
const {
|
|
22
|
+
label = "Number",
|
|
23
|
+
modelValue = 0,
|
|
24
|
+
min = 0,
|
|
25
|
+
max = 100,
|
|
26
|
+
step = 1,
|
|
27
|
+
disabled = false
|
|
28
|
+
} = defineProps({
|
|
29
|
+
label: String,
|
|
30
|
+
modelValue: Number,
|
|
31
|
+
min: Number,
|
|
32
|
+
max: Number,
|
|
33
|
+
step: Number,
|
|
34
|
+
disabled: Boolean
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
const emit = defineEmits(["update:modelValue", "input", "change"]);
|
|
38
|
+
|
|
39
|
+
function clamp(value) {
|
|
40
|
+
return Math.min(max.value, Math.max(min.value, value));
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function emitValue(value) {
|
|
44
|
+
const nextValue = clamp(Number(value));
|
|
45
|
+
emit("update:modelValue", nextValue);
|
|
46
|
+
emit("input", nextValue);
|
|
47
|
+
emit("change", nextValue);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function updateValue(event) {
|
|
51
|
+
emitValue(event.target.value);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function stepDown() {
|
|
55
|
+
emitValue(modelValue.value - step.value);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function stepUp() {
|
|
59
|
+
emitValue(modelValue.value + step.value);
|
|
60
|
+
}
|
|
61
|
+
</script>
|
|
62
|
+
|
|
63
|
+
<style scoped>
|
|
64
|
+
.mikuru-number-input {
|
|
65
|
+
display: grid;
|
|
66
|
+
gap: 6px;
|
|
67
|
+
color: #111827;
|
|
68
|
+
font: inherit;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
.mikuru-number-input > span:first-child {
|
|
72
|
+
font-weight: 650;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
.number-control {
|
|
76
|
+
display: inline-grid;
|
|
77
|
+
grid-template-columns: auto minmax(72px, 1fr) auto;
|
|
78
|
+
width: fit-content;
|
|
79
|
+
border: 1px solid #cbd5e1;
|
|
80
|
+
border-radius: 8px;
|
|
81
|
+
overflow: hidden;
|
|
82
|
+
background: #ffffff;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
input,
|
|
86
|
+
button {
|
|
87
|
+
border: 0;
|
|
88
|
+
padding: 9px 11px;
|
|
89
|
+
color: #111827;
|
|
90
|
+
background: #ffffff;
|
|
91
|
+
font: inherit;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
input {
|
|
95
|
+
width: 82px;
|
|
96
|
+
border-inline: 1px solid #e5e7eb;
|
|
97
|
+
text-align: center;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
button {
|
|
101
|
+
cursor: pointer;
|
|
102
|
+
}
|
|
103
|
+
</style>
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<label class="mikuru-password-input">
|
|
3
|
+
<span>{{ label }}</span>
|
|
4
|
+
<span class="password-control">
|
|
5
|
+
<input
|
|
6
|
+
:type="visible ? 'text' : 'password'"
|
|
7
|
+
:value="modelValue"
|
|
8
|
+
:placeholder="placeholder"
|
|
9
|
+
:autocomplete="autocomplete"
|
|
10
|
+
:disabled="disabled"
|
|
11
|
+
@input="updateValue($event)"
|
|
12
|
+
/>
|
|
13
|
+
<button type="button" @click="toggleVisible">{{ visible ? hideLabel : showLabel }}</button>
|
|
14
|
+
</span>
|
|
15
|
+
<small m-if="strength">{{ strengthLabel }}</small>
|
|
16
|
+
</label>
|
|
17
|
+
</template>
|
|
18
|
+
|
|
19
|
+
<script>
|
|
20
|
+
import { computed, ref } from "mikuru";
|
|
21
|
+
|
|
22
|
+
const {
|
|
23
|
+
label = "Password",
|
|
24
|
+
modelValue = "",
|
|
25
|
+
placeholder = "",
|
|
26
|
+
autocomplete = "current-password",
|
|
27
|
+
disabled = false,
|
|
28
|
+
strength = true,
|
|
29
|
+
showLabel = "Show",
|
|
30
|
+
hideLabel = "Hide"
|
|
31
|
+
} = defineProps({
|
|
32
|
+
label: String,
|
|
33
|
+
modelValue: String,
|
|
34
|
+
placeholder: String,
|
|
35
|
+
autocomplete: String,
|
|
36
|
+
disabled: Boolean,
|
|
37
|
+
strength: Boolean,
|
|
38
|
+
showLabel: String,
|
|
39
|
+
hideLabel: String
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
const emit = defineEmits(["update:modelValue", "input"]);
|
|
43
|
+
const visible = ref(false);
|
|
44
|
+
const strengthLabel = computed(() => {
|
|
45
|
+
const value = modelValue.value || "";
|
|
46
|
+
if (value.length >= 12) return "Strong password";
|
|
47
|
+
if (value.length >= 8) return "Medium password";
|
|
48
|
+
return "Weak password";
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
function toggleVisible() {
|
|
52
|
+
visible.value = !visible.value;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function updateValue(event) {
|
|
56
|
+
emit("update:modelValue", event.target.value);
|
|
57
|
+
emit("input", event.target.value);
|
|
58
|
+
}
|
|
59
|
+
</script>
|
|
60
|
+
|
|
61
|
+
<style scoped>
|
|
62
|
+
.mikuru-password-input {
|
|
63
|
+
display: grid;
|
|
64
|
+
gap: 6px;
|
|
65
|
+
color: #111827;
|
|
66
|
+
font: inherit;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
.mikuru-password-input > span:first-child {
|
|
70
|
+
font-weight: 650;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
.password-control {
|
|
74
|
+
display: grid;
|
|
75
|
+
grid-template-columns: 1fr auto;
|
|
76
|
+
gap: 8px;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
input {
|
|
80
|
+
min-width: 0;
|
|
81
|
+
border: 1px solid #cbd5e1;
|
|
82
|
+
border-radius: 8px;
|
|
83
|
+
padding: 10px 12px;
|
|
84
|
+
color: #111827;
|
|
85
|
+
background: #ffffff;
|
|
86
|
+
font: inherit;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
input:focus {
|
|
90
|
+
border-color: #2563eb;
|
|
91
|
+
outline: 3px solid rgb(37 99 235 / 18%);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
button {
|
|
95
|
+
border: 1px solid #cbd5e1;
|
|
96
|
+
border-radius: 8px;
|
|
97
|
+
padding: 10px 12px;
|
|
98
|
+
color: #111827;
|
|
99
|
+
background: #ffffff;
|
|
100
|
+
font: inherit;
|
|
101
|
+
cursor: pointer;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
small {
|
|
105
|
+
color: #64748b;
|
|
106
|
+
}
|
|
107
|
+
</style>
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<section class="mikuru-virtual-list" :style="listStyle" @scroll="handleScroll">
|
|
3
|
+
<div :style="spacerStyle">
|
|
4
|
+
<article
|
|
5
|
+
m-for="item in visibleItems"
|
|
6
|
+
:key="item.key"
|
|
7
|
+
class="virtual-row"
|
|
8
|
+
:style="item.style"
|
|
9
|
+
>
|
|
10
|
+
<strong>{{ item.data.title || item.data.label || item.data.id }}</strong>
|
|
11
|
+
<span m-if="item.data.description">{{ item.data.description }}</span>
|
|
12
|
+
</article>
|
|
13
|
+
</div>
|
|
14
|
+
</section>
|
|
15
|
+
</template>
|
|
16
|
+
|
|
17
|
+
<script>
|
|
18
|
+
import { computed, ref } from "mikuru";
|
|
19
|
+
|
|
20
|
+
const {
|
|
21
|
+
items = [],
|
|
22
|
+
itemHeight = 48,
|
|
23
|
+
height = 240
|
|
24
|
+
} = defineProps({
|
|
25
|
+
items: Array,
|
|
26
|
+
itemHeight: Number,
|
|
27
|
+
height: Number
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
const scrollTop = ref(0);
|
|
31
|
+
const listStyle = computed(() => `height: ${height.value}px;`);
|
|
32
|
+
const spacerStyle = computed(() => `height: ${items.value.length * itemHeight.value}px; position: relative;`);
|
|
33
|
+
const visibleItems = computed(() => {
|
|
34
|
+
const start = Math.max(0, Math.floor(scrollTop.value / itemHeight.value) - 2);
|
|
35
|
+
const count = Math.ceil(height.value / itemHeight.value) + 5;
|
|
36
|
+
return items.value.slice(start, start + count).map((item, offset) => {
|
|
37
|
+
const index = start + offset;
|
|
38
|
+
return {
|
|
39
|
+
key: item.id || item.value || index,
|
|
40
|
+
data: item,
|
|
41
|
+
style: `height: ${itemHeight.value}px; transform: translateY(${index * itemHeight.value}px);`
|
|
42
|
+
};
|
|
43
|
+
});
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
function handleScroll(event) {
|
|
47
|
+
scrollTop.value = event.target.scrollTop;
|
|
48
|
+
}
|
|
49
|
+
</script>
|
|
50
|
+
|
|
51
|
+
<style scoped>
|
|
52
|
+
.mikuru-virtual-list {
|
|
53
|
+
overflow: auto;
|
|
54
|
+
border: 1px solid #e5e7eb;
|
|
55
|
+
border-radius: 8px;
|
|
56
|
+
background: #ffffff;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
.virtual-row {
|
|
60
|
+
position: absolute;
|
|
61
|
+
inset-inline: 0;
|
|
62
|
+
display: grid;
|
|
63
|
+
align-content: center;
|
|
64
|
+
gap: 2px;
|
|
65
|
+
box-sizing: border-box;
|
|
66
|
+
border-bottom: 1px solid #f1f5f9;
|
|
67
|
+
padding: 7px 12px;
|
|
68
|
+
color: #111827;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
span {
|
|
72
|
+
color: #64748b;
|
|
73
|
+
font-size: 0.86rem;
|
|
74
|
+
}
|
|
75
|
+
</style>
|