mikuru 1.0.38 → 1.0.39

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 (75) hide show
  1. package/CHANGELOG.md +8 -0
  2. package/components/MikuruAlertDialog.mikuru +183 -0
  3. package/components/MikuruAvatar.mikuru +60 -0
  4. package/components/MikuruAvatarGroup.mikuru +66 -0
  5. package/components/MikuruBadge.mikuru +62 -0
  6. package/components/MikuruBreadcrumb.mikuru +86 -0
  7. package/components/MikuruCalendar.mikuru +142 -0
  8. package/components/MikuruChip.mikuru +64 -0
  9. package/components/MikuruCodeBlock.mikuru +20 -13
  10. package/components/MikuruCodeView.mikuru +21 -0
  11. package/components/MikuruColorPicker.mikuru +63 -0
  12. package/components/MikuruCommandPalette.mikuru +197 -0
  13. package/components/MikuruContextMenu.mikuru +137 -0
  14. package/components/MikuruDataList.mikuru +61 -0
  15. package/components/MikuruDatePicker.mikuru +293 -0
  16. package/components/MikuruDrawer.mikuru +115 -0
  17. package/components/MikuruEmptyState.mikuru +72 -0
  18. package/components/MikuruFileUpload.mikuru +161 -0
  19. package/components/MikuruKbd.mikuru +28 -0
  20. package/components/MikuruMarkdownEditor.mikuru +561 -0
  21. package/components/MikuruPagination.mikuru +109 -0
  22. package/components/MikuruPopover.mikuru +152 -0
  23. package/components/MikuruRadioGroup.mikuru +111 -0
  24. package/components/MikuruRangeSlider.mikuru +96 -0
  25. package/components/MikuruRating.mikuru +72 -0
  26. package/components/MikuruSearchInput.mikuru +97 -0
  27. package/components/MikuruSegmentedControl.mikuru +70 -0
  28. package/components/MikuruSkeleton.mikuru +74 -0
  29. package/components/MikuruSlider.mikuru +77 -0
  30. package/components/MikuruStatCard.mikuru +63 -0
  31. package/components/MikuruStepper.mikuru +123 -0
  32. package/components/MikuruSwitch.mikuru +104 -0
  33. package/components/MikuruTable.mikuru +242 -0
  34. package/components/MikuruTagInput.mikuru +127 -0
  35. package/components/MikuruTimePicker.mikuru +61 -0
  36. package/components/MikuruTimeline.mikuru +93 -0
  37. package/components/MikuruTreeView.mikuru +72 -0
  38. package/components/MikuruWysiwygEditor.mikuru +259 -0
  39. package/package.json +289 -1
  40. package/types/components/MikuruAlertDialog.d.ts +16 -0
  41. package/types/components/MikuruAvatar.d.ts +12 -0
  42. package/types/components/MikuruAvatarGroup.d.ts +19 -0
  43. package/types/components/MikuruBadge.d.ts +11 -0
  44. package/types/components/MikuruBreadcrumb.d.ts +16 -0
  45. package/types/components/MikuruCalendar.d.ts +11 -0
  46. package/types/components/MikuruChip.d.ts +12 -0
  47. package/types/components/MikuruCodeView.d.ts +11 -0
  48. package/types/components/MikuruColorPicker.d.ts +11 -0
  49. package/types/components/MikuruCommandPalette.d.ts +20 -0
  50. package/types/components/MikuruContextMenu.d.ts +18 -0
  51. package/types/components/MikuruDataList.d.ts +17 -0
  52. package/types/components/MikuruDatePicker.d.ts +12 -0
  53. package/types/components/MikuruDrawer.d.ts +14 -0
  54. package/types/components/MikuruEmptyState.d.ts +12 -0
  55. package/types/components/MikuruFileUpload.d.ts +14 -0
  56. package/types/components/MikuruKbd.d.ts +9 -0
  57. package/types/components/MikuruMarkdownEditor.d.ts +15 -0
  58. package/types/components/MikuruPagination.d.ts +12 -0
  59. package/types/components/MikuruPopover.d.ts +13 -0
  60. package/types/components/MikuruRadioGroup.d.ts +21 -0
  61. package/types/components/MikuruRangeSlider.d.ts +15 -0
  62. package/types/components/MikuruRating.d.ts +13 -0
  63. package/types/components/MikuruSearchInput.d.ts +12 -0
  64. package/types/components/MikuruSegmentedControl.d.ts +18 -0
  65. package/types/components/MikuruSkeleton.d.ts +13 -0
  66. package/types/components/MikuruSlider.d.ts +15 -0
  67. package/types/components/MikuruStatCard.d.ts +12 -0
  68. package/types/components/MikuruStepper.d.ts +19 -0
  69. package/types/components/MikuruSwitch.d.ts +12 -0
  70. package/types/components/MikuruTable.d.ts +27 -0
  71. package/types/components/MikuruTagInput.d.ts +13 -0
  72. package/types/components/MikuruTimePicker.d.ts +12 -0
  73. package/types/components/MikuruTimeline.d.ts +17 -0
  74. package/types/components/MikuruTreeView.d.ts +17 -0
  75. package/types/components/MikuruWysiwygEditor.d.ts +12 -0
@@ -0,0 +1,127 @@
1
+ <template>
2
+ <section class="mikuru-tag-input">
3
+ <label :for="inputId">{{ label }}</label>
4
+ <div class="tag-box">
5
+ <MikuruChip
6
+ m-for="tag in tags"
7
+ :key="tag"
8
+ :label="tag"
9
+ tone="info"
10
+ removable
11
+ @remove="removeTag(tag)"
12
+ />
13
+ <input
14
+ :id="inputId"
15
+ :value="draft"
16
+ :placeholder="placeholder"
17
+ :disabled="disabled"
18
+ @input="updateDraft($event)"
19
+ @keydown="handleKeydown"
20
+ />
21
+ </div>
22
+ <small m-if="help">{{ help }}</small>
23
+ </section>
24
+ </template>
25
+
26
+ <script>
27
+ import { ref, watch } from "mikuru";
28
+ import MikuruChip from "./MikuruChip.mikuru";
29
+
30
+ const {
31
+ label = "Tags",
32
+ modelValue = [],
33
+ placeholder = "Add tag...",
34
+ help = "",
35
+ disabled = false
36
+ } = defineProps({
37
+ label: String,
38
+ modelValue: Array,
39
+ placeholder: String,
40
+ help: String,
41
+ disabled: Boolean
42
+ });
43
+
44
+ const emit = defineEmits(["update:modelValue", "change"]);
45
+ const inputId = `mikuru-tags-${Math.random().toString(36).slice(2)}`;
46
+ const tags = ref([]);
47
+ const draft = ref("");
48
+
49
+ watch(modelValue, () => {
50
+ tags.value = Array.from(modelValue.value || []);
51
+ }, { immediate: true });
52
+
53
+ function updateDraft(event) {
54
+ draft.value = event.target.value;
55
+ }
56
+
57
+ function handleKeydown(event) {
58
+ if (event.key === "Enter" || event.key === ",") {
59
+ event.preventDefault();
60
+ addTag();
61
+ }
62
+ if (event.key === "Backspace" && !draft.value && tags.value.length) {
63
+ removeTag(tags.value[tags.value.length - 1]);
64
+ }
65
+ }
66
+
67
+ function addTag() {
68
+ const nextTag = draft.value.trim().replace(/,$/, "");
69
+ if (!nextTag || tags.value.includes(nextTag)) return;
70
+ updateTags([...tags.value, nextTag]);
71
+ draft.value = "";
72
+ }
73
+
74
+ function removeTag(tag) {
75
+ updateTags(tags.value.filter((item) => item !== tag));
76
+ }
77
+
78
+ function updateTags(nextTags) {
79
+ tags.value = nextTags;
80
+ emit("update:modelValue", nextTags);
81
+ emit("change", nextTags);
82
+ }
83
+ </script>
84
+
85
+ <style scoped>
86
+ .mikuru-tag-input {
87
+ display: grid;
88
+ gap: 6px;
89
+ color: #111827;
90
+ font: inherit;
91
+ }
92
+
93
+ label {
94
+ font-weight: 650;
95
+ }
96
+
97
+ .tag-box {
98
+ display: flex;
99
+ flex-wrap: wrap;
100
+ align-items: center;
101
+ gap: 6px;
102
+ border: 1px solid #cbd5e1;
103
+ border-radius: 8px;
104
+ padding: 7px;
105
+ background: #ffffff;
106
+ }
107
+
108
+ .tag-box:focus-within {
109
+ border-color: #2563eb;
110
+ outline: 3px solid rgb(37 99 235 / 18%);
111
+ }
112
+
113
+ input {
114
+ min-width: 120px;
115
+ flex: 1 1 120px;
116
+ border: 0;
117
+ padding: 5px;
118
+ color: #111827;
119
+ background: transparent;
120
+ font: inherit;
121
+ outline: none;
122
+ }
123
+
124
+ small {
125
+ color: #64748b;
126
+ }
127
+ </style>
@@ -0,0 +1,61 @@
1
+ <template>
2
+ <label class="mikuru-time-picker">
3
+ <span>{{ label }}</span>
4
+ <input
5
+ type="time"
6
+ :value="modelValue"
7
+ :step="step"
8
+ :disabled="disabled"
9
+ @input="updateValue($event)"
10
+ />
11
+ </label>
12
+ </template>
13
+
14
+ <script>
15
+ const {
16
+ label = "Time",
17
+ modelValue = "09:00",
18
+ step = 60,
19
+ disabled = false
20
+ } = defineProps({
21
+ label: String,
22
+ modelValue: String,
23
+ step: Number,
24
+ disabled: Boolean
25
+ });
26
+
27
+ const emit = defineEmits(["update:modelValue", "change"]);
28
+
29
+ function updateValue(event) {
30
+ emit("update:modelValue", event.target.value);
31
+ emit("change", event.target.value);
32
+ }
33
+ </script>
34
+
35
+ <style scoped>
36
+ .mikuru-time-picker {
37
+ display: grid;
38
+ gap: 6px;
39
+ color: #111827;
40
+ font: inherit;
41
+ }
42
+
43
+ span {
44
+ font-weight: 650;
45
+ }
46
+
47
+ input {
48
+ width: fit-content;
49
+ border: 1px solid #cbd5e1;
50
+ border-radius: 8px;
51
+ padding: 9px 10px;
52
+ color: #111827;
53
+ background: #ffffff;
54
+ font: inherit;
55
+ }
56
+
57
+ input:focus {
58
+ border-color: #2563eb;
59
+ outline: 3px solid rgb(37 99 235 / 18%);
60
+ }
61
+ </style>
@@ -0,0 +1,93 @@
1
+ <template>
2
+ <ol class="mikuru-timeline">
3
+ <li m-for="item in items" :key="itemKey(item)" :data-tone="item.tone || 'neutral'">
4
+ <span class="timeline-dot" aria-hidden="true"></span>
5
+ <div class="timeline-content">
6
+ <time m-if="item.time">{{ item.time }}</time>
7
+ <strong>{{ item.title }}</strong>
8
+ <p m-if="item.description">{{ item.description }}</p>
9
+ </div>
10
+ </li>
11
+ </ol>
12
+ </template>
13
+
14
+ <script>
15
+ const {
16
+ items = []
17
+ } = defineProps({
18
+ items: Array
19
+ });
20
+
21
+ function itemKey(item) {
22
+ return item.id || item.title;
23
+ }
24
+ </script>
25
+
26
+ <style scoped>
27
+ .mikuru-timeline {
28
+ display: grid;
29
+ gap: 0;
30
+ margin: 0;
31
+ padding: 0;
32
+ list-style: none;
33
+ }
34
+
35
+ li {
36
+ position: relative;
37
+ display: grid;
38
+ grid-template-columns: auto 1fr;
39
+ gap: 12px;
40
+ padding-bottom: 14px;
41
+ }
42
+
43
+ li:not(:last-child)::before {
44
+ position: absolute;
45
+ top: 16px;
46
+ bottom: 0;
47
+ left: 6px;
48
+ width: 2px;
49
+ background: #e2e8f0;
50
+ content: "";
51
+ }
52
+
53
+ .timeline-dot {
54
+ position: relative;
55
+ z-index: 1;
56
+ width: 14px;
57
+ height: 14px;
58
+ margin-top: 4px;
59
+ border-radius: 999px;
60
+ background: #64748b;
61
+ }
62
+
63
+ li[data-tone="success"] .timeline-dot {
64
+ background: #16a34a;
65
+ }
66
+
67
+ li[data-tone="info"] .timeline-dot {
68
+ background: #2563eb;
69
+ }
70
+
71
+ li[data-tone="warning"] .timeline-dot {
72
+ background: #f59e0b;
73
+ }
74
+
75
+ .timeline-content {
76
+ display: grid;
77
+ gap: 2px;
78
+ }
79
+
80
+ time {
81
+ color: #64748b;
82
+ font-size: 0.82rem;
83
+ }
84
+
85
+ strong {
86
+ color: #111827;
87
+ }
88
+
89
+ p {
90
+ margin: 0;
91
+ color: #475569;
92
+ }
93
+ </style>
@@ -0,0 +1,72 @@
1
+ <template>
2
+ <nav class="mikuru-tree-view" :aria-label="label">
3
+ <ul>
4
+ <li m-for="node in nodes" :key="nodeKey(node)">
5
+ <button type="button" @click="selectNode(node)">{{ node.label }}</button>
6
+ <ul m-if="node.children && node.children.length">
7
+ <li m-for="child in node.children" :key="nodeKey(child)">
8
+ <button type="button" @click="selectNode(child)">{{ child.label }}</button>
9
+ </li>
10
+ </ul>
11
+ </li>
12
+ </ul>
13
+ </nav>
14
+ </template>
15
+
16
+ <script>
17
+ const {
18
+ label = "Tree view",
19
+ nodes = []
20
+ } = defineProps({
21
+ label: String,
22
+ nodes: Array
23
+ });
24
+
25
+ const emit = defineEmits(["select"]);
26
+
27
+ function nodeKey(node) {
28
+ return node.id || node.value || node.label;
29
+ }
30
+
31
+ function selectNode(node) {
32
+ emit("select", node);
33
+ }
34
+ </script>
35
+
36
+ <style scoped>
37
+ .mikuru-tree-view {
38
+ color: #111827;
39
+ font: inherit;
40
+ }
41
+
42
+ ul {
43
+ display: grid;
44
+ gap: 4px;
45
+ margin: 0;
46
+ padding: 0;
47
+ list-style: none;
48
+ }
49
+
50
+ ul ul {
51
+ margin-top: 4px;
52
+ padding-left: 18px;
53
+ }
54
+
55
+ button {
56
+ width: 100%;
57
+ border: 0;
58
+ border-radius: 6px;
59
+ padding: 7px 8px;
60
+ color: #334155;
61
+ background: transparent;
62
+ font: inherit;
63
+ text-align: left;
64
+ cursor: pointer;
65
+ }
66
+
67
+ button:hover,
68
+ button:focus-visible {
69
+ background: #eff6ff;
70
+ outline: none;
71
+ }
72
+ </style>
@@ -0,0 +1,259 @@
1
+ <template>
2
+ <section class="mikuru-wysiwyg-editor">
3
+ <header class="editor-header">
4
+ <label>{{ label }}</label>
5
+ <div class="toolbar" role="toolbar" aria-label="WYSIWYG formatting">
6
+ <button type="button" title="Bold" @click="format('bold')"><strong>B</strong></button>
7
+ <button type="button" title="Italic" @click="format('italic')"><em>I</em></button>
8
+ <button type="button" title="Heading" @click="toggleHeading">H</button>
9
+ <button type="button" title="Bulleted list" @click="format('insertUnorderedList')">•</button>
10
+ <button type="button" title="Link" @click="createLink">🔗</button>
11
+ <button type="button" title="Clear formatting" @click="format('removeFormat')">Tx</button>
12
+ </div>
13
+ </header>
14
+
15
+ <div
16
+ ref="editorEl"
17
+ class="wysiwyg-surface"
18
+ contenteditable="true"
19
+ role="textbox"
20
+ aria-multiline="true"
21
+ :aria-label="label"
22
+ :data-placeholder="placeholder"
23
+ @input="handleInput"
24
+ @blur="handleBlur"
25
+ @paste="handlePaste"
26
+ ></div>
27
+
28
+ <small m-if="help">{{ help }}</small>
29
+ </section>
30
+ </template>
31
+
32
+ <script>
33
+ import { onMounted, ref, watch } from "mikuru";
34
+
35
+ const {
36
+ label = "Editor",
37
+ modelValue = "",
38
+ placeholder = "Start writing...",
39
+ help = ""
40
+ } = defineProps({
41
+ label: String,
42
+ modelValue: String,
43
+ placeholder: String,
44
+ help: String
45
+ });
46
+
47
+ const emit = defineEmits(["update:modelValue", "input", "change"]);
48
+ const editorEl = ref(null);
49
+ let internalUpdate = false;
50
+
51
+ onMounted(() => {
52
+ syncHtml();
53
+ });
54
+
55
+ watch(modelValue, syncHtml);
56
+
57
+ function syncHtml() {
58
+ if (internalUpdate) return;
59
+ const editor = editorEl.value;
60
+ if (!editor) return;
61
+ const nextHtml = sanitizeHtml(modelValue.value || "");
62
+ if (editor.innerHTML !== nextHtml) {
63
+ editor.innerHTML = nextHtml;
64
+ }
65
+ }
66
+
67
+ function emitInputHtml() {
68
+ const editor = editorEl.value;
69
+ if (!editor) return;
70
+ const nextHtml = sanitizeHtml(editor.innerHTML);
71
+ if (editor.innerHTML !== nextHtml) {
72
+ editor.innerHTML = nextHtml;
73
+ }
74
+ internalUpdate = true;
75
+ emit("update:modelValue", nextHtml);
76
+ emit("input", nextHtml);
77
+ internalUpdate = false;
78
+ }
79
+
80
+ function handleInput() {
81
+ emitInputHtml();
82
+ }
83
+
84
+ function handleBlur() {
85
+ emitChangeHtml();
86
+ }
87
+
88
+ function emitChangeHtml() {
89
+ const editor = editorEl.value;
90
+ if (!editor) return;
91
+ const nextHtml = sanitizeHtml(editor.innerHTML);
92
+ if (editor.innerHTML !== nextHtml) {
93
+ editor.innerHTML = nextHtml;
94
+ }
95
+ internalUpdate = true;
96
+ emit("update:modelValue", nextHtml);
97
+ emit("change", nextHtml);
98
+ internalUpdate = false;
99
+ }
100
+
101
+ function format(command) {
102
+ focusEditor();
103
+ document.execCommand(command, false);
104
+ emitInputHtml();
105
+ }
106
+
107
+ function toggleHeading() {
108
+ focusEditor();
109
+ document.execCommand("formatBlock", false, "h2");
110
+ emitInputHtml();
111
+ }
112
+
113
+ function createLink() {
114
+ focusEditor();
115
+ const url = window.prompt("Link URL");
116
+ if (!url) return;
117
+ const safeUrl = sanitizeUrl(url);
118
+ if (!safeUrl) return;
119
+ document.execCommand("createLink", false, safeUrl);
120
+ emitInputHtml();
121
+ }
122
+
123
+ function handlePaste(event) {
124
+ event.preventDefault();
125
+ const text = event.clipboardData?.getData("text/plain") || "";
126
+ document.execCommand("insertText", false, text);
127
+ emitInputHtml();
128
+ }
129
+
130
+ function focusEditor() {
131
+ editorEl.value?.focus();
132
+ }
133
+
134
+ function sanitizeHtml(html) {
135
+ const template = document.createElement("template");
136
+ template.innerHTML = String(html || "");
137
+ template.content.querySelectorAll("script, style, iframe, object, embed").forEach((node) => node.remove());
138
+ template.content.querySelectorAll("*").forEach((node) => {
139
+ for (const attribute of [...node.attributes]) {
140
+ const name = attribute.name.toLowerCase();
141
+ if (name.startsWith("on")) {
142
+ node.removeAttribute(attribute.name);
143
+ continue;
144
+ }
145
+ if ((name === "href" || name === "src") && !sanitizeUrl(attribute.value)) {
146
+ node.removeAttribute(attribute.name);
147
+ }
148
+ }
149
+ if (node.tagName === "A") {
150
+ node.setAttribute("rel", "noreferrer");
151
+ node.setAttribute("target", "_blank");
152
+ }
153
+ });
154
+ return template.innerHTML;
155
+ }
156
+
157
+ function sanitizeUrl(value) {
158
+ const trimmed = String(value || "").trim();
159
+ if (!trimmed) return "";
160
+ if (trimmed.startsWith("#") || trimmed.startsWith("/")) return trimmed;
161
+ try {
162
+ const parsed = new URL(trimmed);
163
+ return parsed.protocol === "http:" || parsed.protocol === "https:" || parsed.protocol === "mailto:"
164
+ ? parsed.toString()
165
+ : "";
166
+ } catch {
167
+ return "";
168
+ }
169
+ }
170
+ </script>
171
+
172
+ <style scoped>
173
+ .mikuru-wysiwyg-editor {
174
+ display: grid;
175
+ gap: 8px;
176
+ color: #111827;
177
+ font: inherit;
178
+ }
179
+
180
+ .editor-header {
181
+ display: flex;
182
+ align-items: center;
183
+ justify-content: space-between;
184
+ gap: 12px;
185
+ }
186
+
187
+ label {
188
+ font-weight: 650;
189
+ }
190
+
191
+ .toolbar {
192
+ display: flex;
193
+ gap: 4px;
194
+ flex-wrap: wrap;
195
+ }
196
+
197
+ button {
198
+ display: inline-flex;
199
+ align-items: center;
200
+ justify-content: center;
201
+ min-width: 34px;
202
+ border: 1px solid #cbd5e1;
203
+ border-radius: 8px;
204
+ padding: 7px 9px;
205
+ color: #111827;
206
+ background: #ffffff;
207
+ font: inherit;
208
+ cursor: pointer;
209
+ }
210
+
211
+ button:hover,
212
+ button:focus-visible {
213
+ border-color: #2563eb;
214
+ outline: 3px solid rgb(37 99 235 / 16%);
215
+ }
216
+
217
+ .wysiwyg-surface {
218
+ min-height: 220px;
219
+ overflow: auto;
220
+ border: 1px solid #cbd5e1;
221
+ border-radius: 8px;
222
+ padding: 12px;
223
+ color: #111827;
224
+ background: #ffffff;
225
+ line-height: 1.6;
226
+ outline: none;
227
+ }
228
+
229
+ .wysiwyg-surface:focus {
230
+ border-color: #2563eb;
231
+ outline: 3px solid rgb(37 99 235 / 18%);
232
+ }
233
+
234
+ .wysiwyg-surface:empty::before {
235
+ content: attr(data-placeholder);
236
+ color: #94a3b8;
237
+ }
238
+
239
+ small {
240
+ color: #64748b;
241
+ }
242
+
243
+ :deep(.wysiwyg-surface h2),
244
+ :deep(.wysiwyg-surface p),
245
+ :deep(.wysiwyg-surface ul) {
246
+ margin: 0 0 10px;
247
+ }
248
+
249
+ :deep(.wysiwyg-surface a) {
250
+ color: #2563eb;
251
+ }
252
+
253
+ @media (max-width: 760px) {
254
+ .editor-header {
255
+ align-items: stretch;
256
+ flex-direction: column;
257
+ }
258
+ }
259
+ </style>