mikuru 1.0.37 → 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 (76) hide show
  1. package/CHANGELOG.md +12 -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/MikuruVideoPlayer.mikuru +44 -13
  39. package/components/MikuruWysiwygEditor.mikuru +259 -0
  40. package/package.json +289 -1
  41. package/types/components/MikuruAlertDialog.d.ts +16 -0
  42. package/types/components/MikuruAvatar.d.ts +12 -0
  43. package/types/components/MikuruAvatarGroup.d.ts +19 -0
  44. package/types/components/MikuruBadge.d.ts +11 -0
  45. package/types/components/MikuruBreadcrumb.d.ts +16 -0
  46. package/types/components/MikuruCalendar.d.ts +11 -0
  47. package/types/components/MikuruChip.d.ts +12 -0
  48. package/types/components/MikuruCodeView.d.ts +11 -0
  49. package/types/components/MikuruColorPicker.d.ts +11 -0
  50. package/types/components/MikuruCommandPalette.d.ts +20 -0
  51. package/types/components/MikuruContextMenu.d.ts +18 -0
  52. package/types/components/MikuruDataList.d.ts +17 -0
  53. package/types/components/MikuruDatePicker.d.ts +12 -0
  54. package/types/components/MikuruDrawer.d.ts +14 -0
  55. package/types/components/MikuruEmptyState.d.ts +12 -0
  56. package/types/components/MikuruFileUpload.d.ts +14 -0
  57. package/types/components/MikuruKbd.d.ts +9 -0
  58. package/types/components/MikuruMarkdownEditor.d.ts +15 -0
  59. package/types/components/MikuruPagination.d.ts +12 -0
  60. package/types/components/MikuruPopover.d.ts +13 -0
  61. package/types/components/MikuruRadioGroup.d.ts +21 -0
  62. package/types/components/MikuruRangeSlider.d.ts +15 -0
  63. package/types/components/MikuruRating.d.ts +13 -0
  64. package/types/components/MikuruSearchInput.d.ts +12 -0
  65. package/types/components/MikuruSegmentedControl.d.ts +18 -0
  66. package/types/components/MikuruSkeleton.d.ts +13 -0
  67. package/types/components/MikuruSlider.d.ts +15 -0
  68. package/types/components/MikuruStatCard.d.ts +12 -0
  69. package/types/components/MikuruStepper.d.ts +19 -0
  70. package/types/components/MikuruSwitch.d.ts +12 -0
  71. package/types/components/MikuruTable.d.ts +27 -0
  72. package/types/components/MikuruTagInput.d.ts +13 -0
  73. package/types/components/MikuruTimePicker.d.ts +12 -0
  74. package/types/components/MikuruTimeline.d.ts +17 -0
  75. package/types/components/MikuruTreeView.d.ts +17 -0
  76. package/types/components/MikuruWysiwygEditor.d.ts +12 -0
@@ -0,0 +1,561 @@
1
+ <template>
2
+ <section class="mikuru-markdown-editor">
3
+ <header class="editor-header">
4
+ <label :for="editorId">{{ label }}</label>
5
+ <div class="editor-actions" role="group" aria-label="Markdown editor view">
6
+ <button type="button" :class="{ active: mode === 'write' }" @click="setMode('write')">Write</button>
7
+ <button type="button" :class="{ active: mode === 'preview' }" @click="setMode('preview')">Preview</button>
8
+ <button type="button" :class="{ active: mode === 'split' }" @click="setMode('split')">Split</button>
9
+ </div>
10
+ </header>
11
+
12
+ <div class="editor-body" :class="bodyClass">
13
+ <textarea
14
+ m-if="mode !== 'preview'"
15
+ :id="editorId"
16
+ class="markdown-input"
17
+ :value="textValue"
18
+ :placeholder="placeholder"
19
+ :rows="rows"
20
+ :disabled="disabled"
21
+ @input="updateValue($event)"
22
+ ></textarea>
23
+ <article m-if="mode !== 'write'" class="markdown-preview">
24
+ <template m-for="block in previewBlocks" :key="block.key">
25
+ <MikuruCodeView
26
+ m-if="block.type === 'code'"
27
+ class="markdown-code-view"
28
+ :code="block.code"
29
+ :language="block.language"
30
+ />
31
+ <div
32
+ m-else
33
+ class="markdown-html-block"
34
+ m-html="block.html"
35
+ ></div>
36
+ </template>
37
+ </article>
38
+ </div>
39
+
40
+ <small m-if="help">{{ help }}</small>
41
+ </section>
42
+ </template>
43
+
44
+ <script>
45
+ import { computed, ref, watch } from "mikuru";
46
+ import MikuruCodeView from "./MikuruCodeBlock.mikuru";
47
+
48
+ const {
49
+ label = "Markdown",
50
+ modelValue = "",
51
+ placeholder = "Write markdown...",
52
+ rows = 8,
53
+ help = "",
54
+ preview = true,
55
+ disabled = false
56
+ } = defineProps({
57
+ label: String,
58
+ modelValue: String,
59
+ placeholder: String,
60
+ rows: Number,
61
+ help: String,
62
+ preview: Boolean,
63
+ disabled: Boolean
64
+ });
65
+
66
+ const emit = defineEmits(["update:modelValue", "input", "change"]);
67
+ const editorId = `mikuru-markdown-${Math.random().toString(36).slice(2)}`;
68
+ const mode = ref(preview.value ? "split" : "write");
69
+ const textValue = ref("");
70
+
71
+ watch(modelValue, () => {
72
+ textValue.value = String(modelValue.value ?? "");
73
+ }, { immediate: true });
74
+
75
+ const previewBlocks = computed(() => renderMarkdown(textValue.value));
76
+ const bodyClass = computed(() => mode.value === "split" ? "split" : "single");
77
+
78
+ function setMode(nextMode) {
79
+ mode.value = nextMode;
80
+ }
81
+
82
+ function updateValue(event) {
83
+ const nextValue = event.target.value;
84
+ textValue.value = nextValue;
85
+ emit("update:modelValue", nextValue);
86
+ emit("input", nextValue);
87
+ emit("change", nextValue);
88
+ }
89
+
90
+ function renderMarkdown(source) {
91
+ const lines = String(source || "").split("\n");
92
+ const blocks = [];
93
+ let html = [];
94
+ let listType = "";
95
+ let paragraph = [];
96
+ let index = 0;
97
+ let blockIndex = 0;
98
+
99
+ const pushHtml = (value) => {
100
+ html.push(value);
101
+ };
102
+ const flushHtml = () => {
103
+ if (html.length === 0) return;
104
+ blocks.push({
105
+ key: `html-${blockIndex}`,
106
+ type: "html",
107
+ html: html.join("")
108
+ });
109
+ blockIndex += 1;
110
+ html = [];
111
+ };
112
+ const pushCode = (language, code) => {
113
+ flushHtml();
114
+ blocks.push({
115
+ key: `code-${blockIndex}`,
116
+ type: "code",
117
+ language,
118
+ code
119
+ });
120
+ blockIndex += 1;
121
+ };
122
+
123
+ const flushParagraph = () => {
124
+ if (paragraph.length === 0) return;
125
+ pushHtml(`<p>${inlineMarkdown(paragraph.join("\n"))}</p>`);
126
+ paragraph = [];
127
+ };
128
+ const closeList = (nextType = "") => {
129
+ if (!listType || listType === nextType) return;
130
+ pushHtml(`</${listType}>`);
131
+ listType = "";
132
+ };
133
+
134
+ while (index < lines.length) {
135
+ const line = lines[index];
136
+ const trimmed = line.trim();
137
+ if (!trimmed) {
138
+ flushParagraph();
139
+ closeList();
140
+ index += 1;
141
+ continue;
142
+ }
143
+
144
+ const fence = trimmed.match(/^```(\w+)?\s*$/);
145
+ if (fence) {
146
+ flushParagraph();
147
+ closeList();
148
+ const language = fence[1] || "text";
149
+ const codeLines = [];
150
+ index += 1;
151
+ while (index < lines.length && !lines[index].trim().startsWith("```")) {
152
+ codeLines.push(lines[index]);
153
+ index += 1;
154
+ }
155
+ pushCode(language, codeLines.join("\n"));
156
+ index += 1;
157
+ continue;
158
+ }
159
+
160
+ if (trimmed === "$$") {
161
+ flushParagraph();
162
+ closeList();
163
+ const mathLines = [];
164
+ index += 1;
165
+ while (index < lines.length && lines[index].trim() !== "$$") {
166
+ mathLines.push(lines[index]);
167
+ index += 1;
168
+ }
169
+ pushHtml(`<div class="math-block">${escapeHtml(mathLines.join("\n"))}</div>`);
170
+ index += 1;
171
+ continue;
172
+ }
173
+
174
+ if (trimmed === "@startuml") {
175
+ flushParagraph();
176
+ closeList();
177
+ const umlLines = [trimmed];
178
+ index += 1;
179
+ while (index < lines.length) {
180
+ umlLines.push(lines[index]);
181
+ if (lines[index].trim() === "@enduml") break;
182
+ index += 1;
183
+ }
184
+ pushHtml(`<pre class="diagram-block plantuml"><code>${escapeHtml(umlLines.join("\n"))}</code></pre>`);
185
+ index += 1;
186
+ continue;
187
+ }
188
+
189
+ if (/^---+$|^\*\*\*+$|^___+$/.test(trimmed)) {
190
+ flushParagraph();
191
+ closeList();
192
+ pushHtml("<hr>");
193
+ index += 1;
194
+ continue;
195
+ }
196
+
197
+ if (isTableStart(lines, index)) {
198
+ flushParagraph();
199
+ closeList();
200
+ const table = collectTable(lines, index);
201
+ pushHtml(renderTable(table.rows, table.alignments));
202
+ index = table.nextIndex;
203
+ continue;
204
+ }
205
+
206
+ const heading = trimmed.match(/^(#{1,6})\s+(.+)$/);
207
+ if (heading) {
208
+ flushParagraph();
209
+ closeList();
210
+ const level = heading[1].length;
211
+ pushHtml(`<h${level}>${inlineMarkdown(heading[2])}</h${level}>`);
212
+ index += 1;
213
+ continue;
214
+ }
215
+
216
+ const quote = trimmed.match(/^(>+)\s*(.+)$/);
217
+ if (quote) {
218
+ flushParagraph();
219
+ closeList();
220
+ const level = Math.min(quote[1].length, 3);
221
+ pushHtml(`${"<blockquote>".repeat(level)}${inlineMarkdown(quote[2])}${"</blockquote>".repeat(level)}`);
222
+ index += 1;
223
+ continue;
224
+ }
225
+
226
+ const task = trimmed.match(/^[-+*]\s+\[([ xX])\]\s+(.+)$/);
227
+ if (task) {
228
+ flushParagraph();
229
+ closeList("ul");
230
+ if (!listType) {
231
+ pushHtml('<ul class="task-list">');
232
+ listType = "ul";
233
+ }
234
+ const checked = task[1].toLowerCase() === "x" ? " checked" : "";
235
+ pushHtml(`<li class="task-list-item"><input type="checkbox" disabled${checked}> ${inlineMarkdown(task[2])}</li>`);
236
+ index += 1;
237
+ continue;
238
+ }
239
+
240
+ const unorderedList = trimmed.match(/^[-+*]\s+(.+)$/);
241
+ if (unorderedList) {
242
+ flushParagraph();
243
+ closeList("ul");
244
+ if (!listType) {
245
+ pushHtml("<ul>");
246
+ listType = "ul";
247
+ }
248
+ pushHtml(`<li>${inlineMarkdown(unorderedList[1])}</li>`);
249
+ index += 1;
250
+ continue;
251
+ }
252
+
253
+ const orderedList = trimmed.match(/^\d+\.\s+(.+)$/);
254
+ if (orderedList) {
255
+ flushParagraph();
256
+ closeList("ol");
257
+ if (!listType) {
258
+ pushHtml("<ol>");
259
+ listType = "ol";
260
+ }
261
+ pushHtml(`<li>${inlineMarkdown(orderedList[1])}</li>`);
262
+ index += 1;
263
+ continue;
264
+ }
265
+
266
+ paragraph.push(line);
267
+ index += 1;
268
+ }
269
+
270
+ flushParagraph();
271
+ closeList();
272
+ flushHtml();
273
+ return blocks;
274
+ }
275
+
276
+ function isTableStart(lines, index) {
277
+ const current = lines[index]?.trim() || "";
278
+ const next = lines[index + 1]?.trim() || "";
279
+ return current.includes("|") && /^\|?\s*:?-{3,}:?\s*(\|\s*:?-{3,}:?\s*)+\|?$/.test(next);
280
+ }
281
+
282
+ function collectTable(lines, startIndex) {
283
+ const rows = [splitTableRow(lines[startIndex])];
284
+ const alignments = splitTableRow(lines[startIndex + 1]).map((cell) => {
285
+ const trimmed = cell.trim();
286
+ if (trimmed.startsWith(":") && trimmed.endsWith(":")) return "center";
287
+ if (trimmed.endsWith(":")) return "right";
288
+ return "left";
289
+ });
290
+ let index = startIndex + 2;
291
+ while (index < lines.length && lines[index].includes("|") && lines[index].trim()) {
292
+ rows.push(splitTableRow(lines[index]));
293
+ index += 1;
294
+ }
295
+ return { rows, alignments, nextIndex: index };
296
+ }
297
+
298
+ function splitTableRow(line) {
299
+ return line.trim().replace(/^\|/, "").replace(/\|$/, "").split("|").map((cell) => cell.trim());
300
+ }
301
+
302
+ function renderTable(rows, alignments) {
303
+ const [head, ...body] = rows;
304
+ const headers = head.map((cell, index) => `<th style="text-align: ${alignments[index] || "left"}">${inlineMarkdown(cell)}</th>`).join("");
305
+ const bodyRows = body.map((row) => {
306
+ const cells = row.map((cell, index) => `<td style="text-align: ${alignments[index] || "left"}">${inlineMarkdown(cell)}</td>`).join("");
307
+ return `<tr>${cells}</tr>`;
308
+ }).join("");
309
+ return `<table><thead><tr>${headers}</tr></thead><tbody>${bodyRows}</tbody></table>`;
310
+ }
311
+
312
+ function inlineMarkdown(value) {
313
+ return restoreSafeHtml(escapeHtml(value))
314
+ .replace(/ {2}\n/g, "<br>")
315
+ .replace(/\n/g, " ")
316
+ .replace(/!\[([^\]]*)\]\(([^)\s]+)(?:\s+=([0-9]+)x([0-9]+)?)?\)/g, (_, alt, src, width, height) => imageHtml(alt, src, width, height))
317
+ .replace(/\[([^\]]+)\]\((https?:\/\/[^)\s]+)\)/g, '<a href="$2" target="_blank" rel="noreferrer">$1</a>')
318
+ .replace(/`([^`]+)`/g, "<code>$1</code>")
319
+ .replace(/\$\$?([^$]+)\$\$?/g, '<span class="math-inline">$1</span>')
320
+ .replace(/\*\*([^*]+)\*\*/g, "<strong>$1</strong>")
321
+ .replace(/__([^_]+)__/g, "<strong>$1</strong>")
322
+ .replace(/\*([^*]+)\*/g, "<em>$1</em>")
323
+ .replace(/_([^_]+)_/g, "<em>$1</em>")
324
+ .replace(/~~([^~]+)~~/g, "<del>$1</del>")
325
+ .replace(/:(sparkles|smile|tada|rocket):/g, (_, name) => emoji(name));
326
+ }
327
+
328
+ function imageHtml(alt, src, width, height) {
329
+ if (!isSafeAssetUrl(src)) return "";
330
+ const size = width ? ` width="${escapeAttribute(width)}"${height ? ` height="${escapeAttribute(height)}"` : ""}` : "";
331
+ return `<img src="${escapeAttribute(src)}" alt="${escapeAttribute(alt)}"${size}>`;
332
+ }
333
+
334
+ function emoji(name) {
335
+ const map = {
336
+ rocket: "🚀",
337
+ smile: "😄",
338
+ sparkles: "✨",
339
+ tada: "🎉"
340
+ };
341
+ return map[name] || `:${name}:`;
342
+ }
343
+
344
+ function restoreSafeHtml(value) {
345
+ return value
346
+ .replace(/&lt;br\s*\/?&gt;/gi, "<br>")
347
+ .replace(/&lt;(\/?)(b|i)&gt;/gi, "<$1$2>")
348
+ .replace(/&lt;div\s+style=&quot;color:\s*([#\w\s(),.-]+);?&quot;&gt;/gi, (_, color) => `<div style="color: ${escapeAttribute(color.trim())}">`)
349
+ .replace(/&lt;\/div&gt;/gi, "</div>");
350
+ }
351
+
352
+ function isSafeAssetUrl(value) {
353
+ const trimmed = String(value || "").trim();
354
+ if (trimmed.startsWith("/") || trimmed.startsWith("./") || trimmed.startsWith("../")) return true;
355
+ try {
356
+ const parsed = new URL(trimmed);
357
+ return parsed.protocol === "http:" || parsed.protocol === "https:";
358
+ } catch {
359
+ return false;
360
+ }
361
+ }
362
+
363
+ function escapeAttribute(value) {
364
+ return String(value)
365
+ .replaceAll("&", "&amp;")
366
+ .replaceAll('"', "&quot;")
367
+ .replaceAll("<", "&lt;")
368
+ .replaceAll(">", "&gt;");
369
+ }
370
+
371
+ function escapeHtml(value) {
372
+ return String(value)
373
+ .replaceAll("&", "&amp;")
374
+ .replaceAll("<", "&lt;")
375
+ .replaceAll(">", "&gt;")
376
+ .replaceAll('"', "&quot;");
377
+ }
378
+ </script>
379
+
380
+ <style scoped>
381
+ .mikuru-markdown-editor {
382
+ display: grid;
383
+ gap: 8px;
384
+ color: #111827;
385
+ font: inherit;
386
+ }
387
+
388
+ .editor-header {
389
+ display: flex;
390
+ align-items: center;
391
+ justify-content: space-between;
392
+ gap: 12px;
393
+ }
394
+
395
+ label {
396
+ font-weight: 650;
397
+ }
398
+
399
+ .editor-actions {
400
+ display: flex;
401
+ gap: 4px;
402
+ }
403
+
404
+ button {
405
+ border: 1px solid #cbd5e1;
406
+ border-radius: 8px;
407
+ padding: 7px 10px;
408
+ color: #111827;
409
+ background: #ffffff;
410
+ font: inherit;
411
+ cursor: pointer;
412
+ }
413
+
414
+ button.active {
415
+ border-color: #2563eb;
416
+ color: #ffffff;
417
+ background: #2563eb;
418
+ }
419
+
420
+ .editor-body {
421
+ display: grid;
422
+ gap: 10px;
423
+ }
424
+
425
+ .editor-body.split {
426
+ grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
427
+ }
428
+
429
+ .markdown-input,
430
+ .markdown-preview {
431
+ min-height: 220px;
432
+ box-sizing: border-box;
433
+ border: 1px solid #cbd5e1;
434
+ border-radius: 8px;
435
+ padding: 12px;
436
+ background: #ffffff;
437
+ }
438
+
439
+ .markdown-input {
440
+ resize: vertical;
441
+ color: #111827;
442
+ font: 0.95rem/1.55 ui-monospace, SFMono-Regular, Consolas, "Liberation Mono", monospace;
443
+ }
444
+
445
+ .markdown-input:focus {
446
+ border-color: #2563eb;
447
+ outline: 3px solid rgb(37 99 235 / 18%);
448
+ }
449
+
450
+ .markdown-preview {
451
+ overflow: auto;
452
+ color: #334155;
453
+ line-height: 1.6;
454
+ }
455
+
456
+ small {
457
+ color: #64748b;
458
+ }
459
+
460
+ :deep(.markdown-preview h1),
461
+ :deep(.markdown-preview h2),
462
+ :deep(.markdown-preview h3),
463
+ :deep(.markdown-preview p),
464
+ :deep(.markdown-preview ul),
465
+ :deep(.markdown-preview blockquote) {
466
+ margin: 0 0 10px;
467
+ }
468
+
469
+ :deep(.markdown-preview blockquote) {
470
+ border-left: 3px solid #93c5fd;
471
+ padding-left: 10px;
472
+ color: #475569;
473
+ }
474
+
475
+ :deep(.markdown-preview hr) {
476
+ border: 0;
477
+ border-top: 1px solid #cbd5e1;
478
+ margin: 14px 0;
479
+ }
480
+
481
+ :deep(.markdown-preview table) {
482
+ width: 100%;
483
+ margin: 0 0 12px;
484
+ border-collapse: collapse;
485
+ }
486
+
487
+ :deep(.markdown-preview th),
488
+ :deep(.markdown-preview td) {
489
+ border: 1px solid #cbd5e1;
490
+ padding: 7px 9px;
491
+ }
492
+
493
+ :deep(.markdown-preview th) {
494
+ background: #f8fafc;
495
+ }
496
+
497
+ :deep(.markdown-preview img) {
498
+ max-width: 100%;
499
+ height: auto;
500
+ border-radius: 8px;
501
+ }
502
+
503
+ .markdown-html-block {
504
+ display: contents;
505
+ }
506
+
507
+ .markdown-code-view {
508
+ margin: 0 0 12px;
509
+ }
510
+
511
+ :deep(.markdown-preview code) {
512
+ border-radius: 6px;
513
+ padding: 2px 5px;
514
+ background: #f1f5f9;
515
+ font-family: ui-monospace, SFMono-Regular, Consolas, "Liberation Mono", monospace;
516
+ }
517
+
518
+ :deep(.markdown-preview pre),
519
+ :deep(.markdown-preview .math-block),
520
+ :deep(.markdown-preview .diagram-block) {
521
+ overflow: auto;
522
+ margin: 0 0 12px;
523
+ border-radius: 8px;
524
+ padding: 12px;
525
+ color: #e2e8f0;
526
+ background: #0f172a;
527
+ white-space: pre;
528
+ }
529
+
530
+ :deep(.markdown-preview .math-inline) {
531
+ border-radius: 6px;
532
+ padding: 2px 5px;
533
+ color: #1e3a8a;
534
+ background: #dbeafe;
535
+ font-family: ui-monospace, SFMono-Regular, Consolas, "Liberation Mono", monospace;
536
+ }
537
+
538
+ :deep(.markdown-preview .task-list) {
539
+ padding-left: 0;
540
+ list-style: none;
541
+ }
542
+
543
+ :deep(.markdown-preview .task-list-item input) {
544
+ margin-right: 6px;
545
+ }
546
+
547
+ :deep(.markdown-preview a) {
548
+ color: #2563eb;
549
+ }
550
+
551
+ @media (max-width: 760px) {
552
+ .editor-header {
553
+ align-items: stretch;
554
+ flex-direction: column;
555
+ }
556
+
557
+ .editor-body.split {
558
+ grid-template-columns: 1fr;
559
+ }
560
+ }
561
+ </style>
@@ -0,0 +1,109 @@
1
+ <template>
2
+ <nav class="mikuru-pagination" :aria-label="label">
3
+ <button type="button" :disabled="currentPage <= 1" @click="goPrevious">Previous</button>
4
+ <button
5
+ m-for="pageNumber in visiblePages"
6
+ :key="pageNumber"
7
+ type="button"
8
+ :class="{ active: pageNumber === currentPage }"
9
+ :aria-current="pageNumber === currentPage ? 'page' : undefined"
10
+ @click="goTo(pageNumber)"
11
+ >
12
+ {{ pageNumber }}
13
+ </button>
14
+ <button type="button" :disabled="currentPage >= totalPages" @click="goNext">Next</button>
15
+ </nav>
16
+ </template>
17
+
18
+ <script>
19
+ import { computed } from "mikuru";
20
+
21
+ const {
22
+ page = 1,
23
+ total = 1,
24
+ siblingCount = 1,
25
+ label = "Pagination"
26
+ } = defineProps({
27
+ page: Number,
28
+ total: Number,
29
+ siblingCount: Number,
30
+ label: String
31
+ });
32
+
33
+ const emit = defineEmits(["update:page", "change"]);
34
+
35
+ const totalPages = computed(() => Math.max(1, Number(total.value) || 1));
36
+ const currentPage = computed(() => clampPage(page.value));
37
+ const visiblePages = computed(() => {
38
+ const radius = Math.max(0, Number(siblingCount.value) || 0);
39
+ const start = Math.max(1, currentPage.value - radius);
40
+ const end = Math.min(totalPages.value, currentPage.value + radius);
41
+ const pages = [];
42
+ for (let index = start; index <= end; index += 1) {
43
+ pages.push(index);
44
+ }
45
+ if (!pages.includes(1)) pages.unshift(1);
46
+ if (!pages.includes(totalPages.value)) pages.push(totalPages.value);
47
+ return pages;
48
+ });
49
+
50
+ function clampPage(value) {
51
+ const nextPage = Number(value) || 1;
52
+ return Math.min(Math.max(Math.round(nextPage), 1), totalPages.value);
53
+ }
54
+
55
+ function emitPage(nextPage) {
56
+ const clamped = clampPage(nextPage);
57
+ emit("update:page", clamped);
58
+ emit("change", clamped);
59
+ }
60
+
61
+ function goTo(nextPage) {
62
+ emitPage(nextPage);
63
+ }
64
+
65
+ function goPrevious() {
66
+ emitPage(currentPage.value - 1);
67
+ }
68
+
69
+ function goNext() {
70
+ emitPage(currentPage.value + 1);
71
+ }
72
+ </script>
73
+
74
+ <style scoped>
75
+ .mikuru-pagination {
76
+ display: flex;
77
+ align-items: center;
78
+ gap: 6px;
79
+ flex-wrap: wrap;
80
+ }
81
+
82
+ button {
83
+ min-width: 38px;
84
+ border: 1px solid #cbd5e1;
85
+ border-radius: 8px;
86
+ padding: 8px 10px;
87
+ color: #111827;
88
+ background: #ffffff;
89
+ font: inherit;
90
+ cursor: pointer;
91
+ }
92
+
93
+ button:hover:not(:disabled),
94
+ button:focus-visible {
95
+ border-color: #2563eb;
96
+ outline: 3px solid rgb(37 99 235 / 16%);
97
+ }
98
+
99
+ button.active {
100
+ border-color: #2563eb;
101
+ color: #ffffff;
102
+ background: #2563eb;
103
+ }
104
+
105
+ button:disabled {
106
+ color: #94a3b8;
107
+ cursor: not-allowed;
108
+ }
109
+ </style>