mikuru 1.0.34 → 1.0.36

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 (44) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/README.md +25 -1
  3. package/components/MikuruAccordion.mikuru +156 -0
  4. package/components/MikuruCarousel.mikuru +144 -6
  5. package/components/MikuruCheckbox.mikuru +84 -0
  6. package/components/MikuruCodeBlock.mikuru +238 -4
  7. package/components/MikuruCombobox.mikuru +226 -0
  8. package/components/MikuruFooter.mikuru +121 -0
  9. package/components/MikuruHeader.mikuru +165 -0
  10. package/components/MikuruSelect.mikuru +106 -0
  11. package/components/MikuruSideMenu.mikuru +188 -0
  12. package/components/MikuruTabs.mikuru +163 -0
  13. package/components/MikuruTextInput.mikuru +83 -0
  14. package/components/MikuruTextarea.mikuru +86 -0
  15. package/components/MikuruVideoPlayer.mikuru +8 -4
  16. package/dist/cli/create.js +5 -1
  17. package/dist/cli/create.js.map +1 -1
  18. package/dist/cli/templates.d.ts +1 -1
  19. package/dist/cli/templates.js +3 -2
  20. package/dist/cli/templates.js.map +1 -1
  21. package/dist/cli.js +1 -1
  22. package/package.json +81 -1
  23. package/templates/video-player/_gitignore +3 -0
  24. package/templates/video-player/index.html +13 -0
  25. package/templates/video-player/package.json +19 -0
  26. package/templates/video-player/public/favicon.svg +4 -0
  27. package/templates/video-player/src/App.mikuru +92 -0
  28. package/templates/video-player/src/css-env.d.ts +1 -0
  29. package/templates/video-player/src/main.ts +10 -0
  30. package/templates/video-player/src/mikuru-env.d.ts +1 -0
  31. package/templates/video-player/src/style.css +132 -0
  32. package/templates/video-player/tsconfig.json +11 -0
  33. package/templates/video-player/vite.config.ts +6 -0
  34. package/types/components/MikuruAccordion.d.ts +18 -0
  35. package/types/components/MikuruCarousel.d.ts +2 -0
  36. package/types/components/MikuruCheckbox.d.ts +13 -0
  37. package/types/components/MikuruCombobox.d.ts +21 -0
  38. package/types/components/MikuruFooter.d.ts +19 -0
  39. package/types/components/MikuruHeader.d.ts +22 -0
  40. package/types/components/MikuruSelect.d.ts +21 -0
  41. package/types/components/MikuruSideMenu.d.ts +22 -0
  42. package/types/components/MikuruTabs.d.ts +18 -0
  43. package/types/components/MikuruTextInput.d.ts +16 -0
  44. package/types/components/MikuruTextarea.d.ts +16 -0
@@ -4,7 +4,7 @@
4
4
  <span>{{ languageLabel }}</span>
5
5
  <button type="button" @click="copyCode">{{ copyLabel }}</button>
6
6
  </figcaption>
7
- <pre><code><span m-for="line in lines" :key="line.number" class="code-line"><span m-if="showLineNumbers" class="line-number">{{ line.number }}</span><span>{{ line.text }}</span>
7
+ <pre><code><span m-for="line in lines" :key="line.number" class="code-line"><span m-if="showLineNumbers" class="line-number">{{ line.number }}</span><span m-html="line.html"></span>
8
8
  </span></code></pre>
9
9
  </figure>
10
10
  </template>
@@ -27,10 +27,10 @@ const lines = ref([]);
27
27
  const languageLabel = computed(() => language.value || "text");
28
28
  const copyLabel = computed(() => copied.value ? "Copied" : "Copy");
29
29
 
30
- watch(code, () => {
30
+ watch([code, language], () => {
31
31
  lines.value = code.value.split("\n").map((text, index) => ({
32
32
  number: index + 1,
33
- text
33
+ html: highlightLine(text, language.value)
34
34
  }));
35
35
  }, { immediate: true });
36
36
 
@@ -42,6 +42,207 @@ async function copyCode() {
42
42
  copied.value = false;
43
43
  }, 1400);
44
44
  }
45
+
46
+ function highlightLine(line, lang) {
47
+ const normalizedLanguage = String(lang || "text").toLowerCase();
48
+ if (["mikuru", "html", "xml", "svg"].includes(normalizedLanguage)) {
49
+ return highlightMarkup(line);
50
+ }
51
+ if (["css", "scss"].includes(normalizedLanguage)) {
52
+ return highlightCss(line);
53
+ }
54
+ if (["json"].includes(normalizedLanguage)) {
55
+ return highlightJson(line);
56
+ }
57
+ if (["js", "jsx", "javascript", "ts", "tsx", "typescript"].includes(normalizedLanguage)) {
58
+ return highlightScript(line);
59
+ }
60
+ return escapeHtml(line);
61
+ }
62
+
63
+ function highlightScript(line) {
64
+ const keywords = new Set([
65
+ "async", "await", "break", "case", "catch", "class", "const", "continue", "default", "do", "else",
66
+ "export", "extends", "finally", "for", "from", "function", "if", "import", "in", "let", "new",
67
+ "of", "return", "switch", "throw", "try", "typeof", "var", "while"
68
+ ]);
69
+ const literals = new Set(["false", "null", "true", "undefined"]);
70
+ let output = "";
71
+ let index = 0;
72
+
73
+ while (index < line.length) {
74
+ const rest = line.slice(index);
75
+ const comment = rest.match(/^\/\/.*/);
76
+ if (comment) {
77
+ output += token("comment", comment[0]);
78
+ break;
79
+ }
80
+ const blockComment = rest.match(/^\/\*.*?\*\//);
81
+ if (blockComment) {
82
+ output += token("comment", blockComment[0]);
83
+ index += blockComment[0].length;
84
+ continue;
85
+ }
86
+ const string = readString(line, index);
87
+ if (string) {
88
+ output += token("string", string);
89
+ index += string.length;
90
+ continue;
91
+ }
92
+ const number = rest.match(/^\b\d+(?:\.\d+)?\b/);
93
+ if (number) {
94
+ output += token("number", number[0]);
95
+ index += number[0].length;
96
+ continue;
97
+ }
98
+ const word = rest.match(/^[A-Za-z_$][\w$]*/);
99
+ if (word) {
100
+ const value = word[0];
101
+ const after = line.slice(index + value.length).trimStart();
102
+ if (keywords.has(value)) {
103
+ output += token("keyword", value);
104
+ } else if (literals.has(value)) {
105
+ output += token("literal", value);
106
+ } else if (after.startsWith("(")) {
107
+ output += token("function", value);
108
+ } else {
109
+ output += escapeHtml(value);
110
+ }
111
+ index += value.length;
112
+ continue;
113
+ }
114
+ const operator = rest.match(/^[{}()[\].,;:+\-*/%=!<>?&|]+/);
115
+ if (operator) {
116
+ output += token("operator", operator[0]);
117
+ index += operator[0].length;
118
+ continue;
119
+ }
120
+ output += escapeHtml(line[index]);
121
+ index += 1;
122
+ }
123
+
124
+ return output;
125
+ }
126
+
127
+ function highlightJson(line) {
128
+ return escapeHtml(line)
129
+ .replace(/(&quot;[^&]*?&quot;)(\s*:)?/g, (_, value, colon) => {
130
+ return colon ? `${tokenEscaped("property", value)}${colon}` : tokenEscaped("string", value);
131
+ })
132
+ .replace(/\b(true|false|null)\b/g, (_, value) => token("literal", value))
133
+ .replace(/\b\d+(?:\.\d+)?\b/g, (_, value) => token("number", value));
134
+ }
135
+
136
+ function highlightCss(line) {
137
+ return escapeHtml(line)
138
+ .replace(/\/\*.*?\*\//g, (_, value) => token("comment", value))
139
+ .replace(/(@[\w-]+)/g, (_, value) => token("keyword", value))
140
+ .replace(/(#(?:[0-9a-fA-F]{3}){1,2})\b/g, (_, value) => token("number", value))
141
+ .replace(/([\w-]+)(\s*:)/g, (_, name, colon) => `${token("property", name)}${colon}`)
142
+ .replace(/(&quot;[^&]*?&quot;|'[^']*?')/g, (_, value) => tokenEscaped("string", value));
143
+ }
144
+
145
+ function highlightMarkup(line) {
146
+ let output = "";
147
+ let index = 0;
148
+ const tagPattern = /<!--.*?-->|<\/?[A-Za-z][^>]*?>/g;
149
+ let match;
150
+
151
+ while ((match = tagPattern.exec(line)) !== null) {
152
+ output += highlightScript(line.slice(index, match.index));
153
+ output += match[0].startsWith("<!--") ? token("comment", match[0]) : highlightTag(match[0]);
154
+ index = match.index + match[0].length;
155
+ }
156
+
157
+ output += highlightScript(line.slice(index));
158
+ return output;
159
+ }
160
+
161
+ function highlightTag(tagSource) {
162
+ let output = "";
163
+ let index = 0;
164
+ const open = tagSource.match(/^<\/?/);
165
+ if (open) {
166
+ output += token("operator", open[0]);
167
+ index += open[0].length;
168
+ }
169
+
170
+ const tagName = tagSource.slice(index).match(/^[A-Za-z][\w:-]*/);
171
+ if (tagName) {
172
+ output += token("tag", tagName[0]);
173
+ index += tagName[0].length;
174
+ }
175
+
176
+ while (index < tagSource.length) {
177
+ const rest = tagSource.slice(index);
178
+ const close = rest.match(/^\/?>/);
179
+ if (close) {
180
+ output += token("operator", close[0]);
181
+ index += close[0].length;
182
+ continue;
183
+ }
184
+ const whitespace = rest.match(/^\s+/);
185
+ if (whitespace) {
186
+ output += escapeHtml(whitespace[0]);
187
+ index += whitespace[0].length;
188
+ continue;
189
+ }
190
+ const attribute = rest.match(/^[:@#]?[A-Za-z_][\w:.-]*/);
191
+ if (attribute) {
192
+ output += token("property", attribute[0]);
193
+ index += attribute[0].length;
194
+ continue;
195
+ }
196
+ if (tagSource[index] === "=") {
197
+ output += token("operator", "=");
198
+ index += 1;
199
+ continue;
200
+ }
201
+ const string = readString(tagSource, index);
202
+ if (string) {
203
+ output += token("string", string);
204
+ index += string.length;
205
+ continue;
206
+ }
207
+ output += escapeHtml(tagSource[index]);
208
+ index += 1;
209
+ }
210
+
211
+ return output;
212
+ }
213
+
214
+ function readString(line, start) {
215
+ const quote = line[start];
216
+ if (quote !== "\"" && quote !== "'" && quote !== "`") return "";
217
+ let index = start + 1;
218
+ while (index < line.length) {
219
+ if (line[index] === "\\") {
220
+ index += 2;
221
+ continue;
222
+ }
223
+ if (line[index] === quote) {
224
+ return line.slice(start, index + 1);
225
+ }
226
+ index += 1;
227
+ }
228
+ return line.slice(start);
229
+ }
230
+
231
+ function token(kind, value) {
232
+ return `<span class="syntax-${kind}">${escapeHtml(value)}</span>`;
233
+ }
234
+
235
+ function tokenEscaped(kind, value) {
236
+ return `<span class="syntax-${kind}">${value}</span>`;
237
+ }
238
+
239
+ function escapeHtml(value) {
240
+ return String(value)
241
+ .replaceAll("&", "&amp;")
242
+ .replaceAll("<", "&lt;")
243
+ .replaceAll(">", "&gt;")
244
+ .replaceAll('"', "&quot;");
245
+ }
45
246
  </script>
46
247
 
47
248
  <style scoped>
@@ -78,7 +279,7 @@ pre {
78
279
  margin: 0;
79
280
  overflow: auto;
80
281
  padding: 12px 0;
81
- color: #e5e7eb;
282
+ color: #d4d4d4;
82
283
  font: 0.9rem/1.55 ui-monospace, SFMono-Regular, Consolas, "Liberation Mono", monospace;
83
284
  }
84
285
 
@@ -97,4 +298,37 @@ pre {
97
298
  text-align: right;
98
299
  user-select: none;
99
300
  }
301
+
302
+ :deep(.syntax-comment) {
303
+ color: #6a9955;
304
+ }
305
+
306
+ :deep(.syntax-function) {
307
+ color: #dcdcaa;
308
+ }
309
+
310
+ :deep(.syntax-keyword) {
311
+ color: #c586c0;
312
+ }
313
+
314
+ :deep(.syntax-literal),
315
+ :deep(.syntax-number) {
316
+ color: #b5cea8;
317
+ }
318
+
319
+ :deep(.syntax-operator) {
320
+ color: #d4d4d4;
321
+ }
322
+
323
+ :deep(.syntax-property) {
324
+ color: #9cdcfe;
325
+ }
326
+
327
+ :deep(.syntax-string) {
328
+ color: #ce9178;
329
+ }
330
+
331
+ :deep(.syntax-tag) {
332
+ color: #569cd6;
333
+ }
100
334
  </style>
@@ -0,0 +1,226 @@
1
+ <template>
2
+ <div class="mikuru-combobox" ref="rootEl" @keydown="handleKeydown">
3
+ <label class="field-label">{{ label }}</label>
4
+ <input
5
+ class="combo-input"
6
+ type="text"
7
+ :value="query"
8
+ :placeholder="placeholder"
9
+ :disabled="disabled"
10
+ role="combobox"
11
+ aria-autocomplete="list"
12
+ :aria-label="label"
13
+ :aria-expanded="open ? 'true' : 'false'"
14
+ @focus="openList"
15
+ @input="updateQuery($event)"
16
+ />
17
+ <div m-if="open" class="combo-list" role="listbox">
18
+ <button
19
+ m-for="item in filteredOptions"
20
+ :key="item.value"
21
+ class="combo-option"
22
+ :class="{ active: Object.is(item.value, modelValue) }"
23
+ type="button"
24
+ role="option"
25
+ :aria-selected="Object.is(item.value, modelValue) ? 'true' : 'false'"
26
+ :disabled="item.disabled"
27
+ @click="selectOption(item)"
28
+ >
29
+ <span>{{ item.label }}</span>
30
+ <small>{{ item.description }}</small>
31
+ </button>
32
+ <p m-if="filteredOptions.length === 0" class="combo-empty">{{ emptyText }}</p>
33
+ </div>
34
+ </div>
35
+ </template>
36
+
37
+ <script>
38
+ import { computed, onMounted, onUnmounted, ref, watch } from "mikuru";
39
+
40
+ const {
41
+ label = "Combobox",
42
+ modelValue = "",
43
+ options = [],
44
+ placeholder = "Search...",
45
+ emptyText = "No options",
46
+ disabled = false
47
+ } = defineProps();
48
+
49
+ const emit = defineEmits(["update:modelValue", "change", "query"]);
50
+ const rootEl = ref(null);
51
+ const open = ref(false);
52
+ const query = ref("");
53
+ const normalizedOptions = ref([]);
54
+ let optionsSignature = "";
55
+
56
+ watch(options, syncOptions, { immediate: true });
57
+ watch(modelValue, syncQuery, { immediate: true });
58
+
59
+ const filteredOptions = computed(() => {
60
+ const needle = query.value.trim().toLowerCase();
61
+ if (!needle) return normalizedOptions.value;
62
+ return normalizedOptions.value.filter((item) => {
63
+ return item.label.toLowerCase().includes(needle) || item.description.toLowerCase().includes(needle);
64
+ });
65
+ });
66
+
67
+ onMounted(() => {
68
+ document.addEventListener("pointerdown", handleDocumentPointer);
69
+ });
70
+
71
+ onUnmounted(() => {
72
+ document.removeEventListener("pointerdown", handleDocumentPointer);
73
+ });
74
+
75
+ function syncOptions() {
76
+ const source = Array.isArray(options.value) ? options.value : [];
77
+ const nextOptions = source.map((item, index) => {
78
+ if (typeof item === "string") {
79
+ return { label: item, value: item, description: "", disabled: false };
80
+ }
81
+ return {
82
+ label: item.label || `Option ${index + 1}`,
83
+ value: item.value ?? item.label ?? index,
84
+ description: item.description || "",
85
+ disabled: Boolean(item.disabled)
86
+ };
87
+ });
88
+ const nextSignature = nextOptions
89
+ .map((item) => `${item.value}\u0000${item.label}\u0000${item.description}\u0000${item.disabled}`)
90
+ .join("\u0001");
91
+ if (nextSignature === optionsSignature) return;
92
+ optionsSignature = nextSignature;
93
+ normalizedOptions.value = nextOptions;
94
+ syncQuery();
95
+ }
96
+
97
+ function syncQuery() {
98
+ const selected = normalizedOptions.value.find((item) => Object.is(item.value, modelValue.value));
99
+ if (selected) {
100
+ query.value = selected.label;
101
+ }
102
+ }
103
+
104
+ function openList() {
105
+ if (!disabled.value) {
106
+ open.value = true;
107
+ }
108
+ }
109
+
110
+ function closeList() {
111
+ open.value = false;
112
+ }
113
+
114
+ function updateQuery(event) {
115
+ query.value = event.target.value;
116
+ emit("query", query.value);
117
+ openList();
118
+ }
119
+
120
+ function selectOption(item) {
121
+ if (item.disabled) return;
122
+ query.value = item.label;
123
+ emit("update:modelValue", item.value);
124
+ emit("change", item.value);
125
+ closeList();
126
+ }
127
+
128
+ function handleDocumentPointer(event) {
129
+ const root = rootEl.value;
130
+ if (!root || root.contains(event.target)) return;
131
+ closeList();
132
+ }
133
+
134
+ function handleKeydown(event) {
135
+ if (event.key === "Escape") {
136
+ closeList();
137
+ return;
138
+ }
139
+ if (event.key !== "Enter") return;
140
+ if (!open.value || filteredOptions.value.length === 0) return;
141
+ event.preventDefault();
142
+ const firstEnabled = filteredOptions.value.find((item) => !item.disabled);
143
+ if (firstEnabled) {
144
+ selectOption(firstEnabled);
145
+ }
146
+ }
147
+ </script>
148
+
149
+ <style scoped>
150
+ .mikuru-combobox {
151
+ position: relative;
152
+ display: grid;
153
+ gap: 6px;
154
+ color: #0f172a;
155
+ font: inherit;
156
+ }
157
+
158
+ .field-label {
159
+ font-weight: 650;
160
+ }
161
+
162
+ .combo-input {
163
+ width: 100%;
164
+ box-sizing: border-box;
165
+ border: 1px solid #cbd5e1;
166
+ border-radius: 8px;
167
+ padding: 10px 12px;
168
+ color: #111827;
169
+ background: #ffffff;
170
+ font: inherit;
171
+ }
172
+
173
+ .combo-input:focus {
174
+ border-color: #2563eb;
175
+ outline: 3px solid rgb(37 99 235 / 18%);
176
+ }
177
+
178
+ .combo-list {
179
+ position: absolute;
180
+ top: calc(100% + 6px);
181
+ left: 0;
182
+ right: 0;
183
+ z-index: 40;
184
+ display: grid;
185
+ max-height: 240px;
186
+ overflow: auto;
187
+ border: 1px solid #e2e8f0;
188
+ border-radius: 8px;
189
+ background: #ffffff;
190
+ box-shadow: 0 18px 48px rgb(15 23 42 / 16%);
191
+ }
192
+
193
+ .combo-option {
194
+ display: grid;
195
+ gap: 2px;
196
+ border: 0;
197
+ padding: 10px 12px;
198
+ color: #0f172a;
199
+ background: transparent;
200
+ text-align: left;
201
+ font: inherit;
202
+ cursor: pointer;
203
+ }
204
+
205
+ .combo-option:hover,
206
+ .combo-option:focus-visible,
207
+ .combo-option.active {
208
+ background: #eff6ff;
209
+ outline: none;
210
+ }
211
+
212
+ .combo-option:disabled {
213
+ color: #94a3b8;
214
+ cursor: not-allowed;
215
+ }
216
+
217
+ .combo-option small {
218
+ color: #64748b;
219
+ }
220
+
221
+ .combo-empty {
222
+ margin: 0;
223
+ padding: 12px;
224
+ color: #64748b;
225
+ }
226
+ </style>
@@ -0,0 +1,121 @@
1
+ <template>
2
+ <footer class="mikuru-footer">
3
+ <div>
4
+ <strong>{{ title }}</strong>
5
+ <p m-if="description">{{ description }}</p>
6
+ </div>
7
+
8
+ <nav m-if="normalizedLinks.length > 0" class="footer-links" :aria-label="navLabel">
9
+ <a
10
+ m-for="link in normalizedLinks"
11
+ :key="link.value"
12
+ :href="link.href"
13
+ @click="selectLink(link, $event)"
14
+ >
15
+ {{ link.label }}
16
+ </a>
17
+ </nav>
18
+
19
+ <small>{{ note }}</small>
20
+ </footer>
21
+ </template>
22
+
23
+ <script>
24
+ import { ref, watch } from "mikuru";
25
+
26
+ const {
27
+ title = "Mikuru",
28
+ description = "",
29
+ note = "",
30
+ navLabel = "Footer navigation",
31
+ links = []
32
+ } = defineProps();
33
+
34
+ const emit = defineEmits(["select"]);
35
+ const normalizedLinks = ref([]);
36
+ let linksSignature = "";
37
+
38
+ watch(links, syncLinks, { immediate: true });
39
+
40
+ function syncLinks() {
41
+ const source = Array.isArray(links.value) ? links.value : [];
42
+ const nextLinks = source.map((link, index) => {
43
+ if (typeof link === "string") {
44
+ return { label: link, value: link, href: "#" };
45
+ }
46
+ return {
47
+ label: link.label || `Link ${index + 1}`,
48
+ value: link.value ?? link.label ?? index,
49
+ href: link.href || "#"
50
+ };
51
+ });
52
+ const nextSignature = nextLinks
53
+ .map((link) => `${link.value}\u0000${link.label}\u0000${link.href}`)
54
+ .join("\u0001");
55
+ if (nextSignature === linksSignature) return;
56
+ linksSignature = nextSignature;
57
+ normalizedLinks.value = nextLinks;
58
+ }
59
+
60
+ function selectLink(link, event) {
61
+ if (link.href === "#") {
62
+ event.preventDefault();
63
+ }
64
+ emit("select", link.value);
65
+ }
66
+ </script>
67
+
68
+ <style scoped>
69
+ .mikuru-footer {
70
+ display: grid;
71
+ grid-template-columns: 1fr auto;
72
+ gap: 12px 18px;
73
+ align-items: center;
74
+ border: 1px solid #e2e8f0;
75
+ border-radius: 8px;
76
+ padding: 14px;
77
+ color: #334155;
78
+ background: #ffffff;
79
+ }
80
+
81
+ .mikuru-footer strong {
82
+ color: #0f172a;
83
+ }
84
+
85
+ .mikuru-footer p {
86
+ margin: 4px 0 0;
87
+ }
88
+
89
+ .footer-links {
90
+ display: flex;
91
+ flex-wrap: wrap;
92
+ justify-content: flex-end;
93
+ gap: 8px 12px;
94
+ }
95
+
96
+ .footer-links a {
97
+ color: #2563eb;
98
+ text-decoration: none;
99
+ }
100
+
101
+ .footer-links a:hover,
102
+ .footer-links a:focus-visible {
103
+ text-decoration: underline;
104
+ outline: none;
105
+ }
106
+
107
+ .mikuru-footer small {
108
+ grid-column: 1 / -1;
109
+ color: #64748b;
110
+ }
111
+
112
+ @media (max-width: 720px) {
113
+ .mikuru-footer {
114
+ grid-template-columns: 1fr;
115
+ }
116
+
117
+ .footer-links {
118
+ justify-content: flex-start;
119
+ }
120
+ }
121
+ </style>