@kungal/editor-nuxt 0.11.0 → 0.13.0

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 CHANGED
@@ -1,5 +1,68 @@
1
1
  # @kungal/editor-nuxt
2
2
 
3
+ ## 0.13.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 8ce8210: The Nuxt layer now preconfigures Vite's dev dependency optimizer, so consumers
8
+ never hit the micromark/`debug` dev error.
9
+
10
+ Under `nuxt dev`, Milkdown's tokenizer (micromark) resolves to its DEV build,
11
+ which does `import debug from 'debug'`. `debug` is CommonJS; unless Vite
12
+ pre-bundles it, its browser build is served raw with no `default` export and the
13
+ editor throws at load — `SyntaxError: 'debug' does not provide an export named
14
+ 'default'` (create-tokenizer.js), which also aborts app init. Whether it fires is
15
+ non-deterministic per app (it depends on whether Vite's dep scan reaches the
16
+ editor before it evaluates).
17
+
18
+ The layer now sets `vite.optimizeDeps.include` for the editor + Milkdown entry
19
+ points, which Nuxt merges into every consuming app — so esbuild pre-bundles that
20
+ subgraph and resolves micromark's CJS `debug` inside the bundle. No app needs to
21
+ configure this itself. Dev-only: the production build strips micromark's debug
22
+ calls, so `nuxt build` is unaffected. (Plain non-Nuxt Vite apps that use
23
+ `@kungal/editor-vue` directly still add the same `optimizeDeps.include` manually —
24
+ a layer can inject Vite config, a plain package cannot.)
25
+
26
+ ### Patch Changes
27
+
28
+ - @kungal/editor-core@0.13.0
29
+ - @kungal/editor-vue@0.13.0
30
+
31
+ ## 0.12.0
32
+
33
+ ### Minor Changes
34
+
35
+ - 5786594: Add a `#toolbar` slot and a KunUI toolbar — the toolbar is now a swappable layer.
36
+
37
+ `<KunEditor>` exposes a `#toolbar` scoped slot whose props are the command API
38
+ (`KunEditorToolbarApi`: `run(cmdKey, payload)` / `insertText` / `insertQuote` /
39
+ `insertMention` / `focus` / `adapters` / `features` / `locale`). The API is
40
+ computed inside the Milkdown providers and handed down, so a custom toolbar can
41
+ live in the consumer's tree without `useInstance`/`inject`. The hand-rolled
42
+ default toolbar is the slot's fallback — omit the slot and nothing changes.
43
+
44
+ `@kungal/editor-nuxt` ships `<KunEditorToolbar>` (auto-imported): the same
45
+ toolbar built with **KunButton / KunIcon / KunTooltip / KunPopover** (real tooltip
46
+ delay, popover focus/collision, a11y):
47
+
48
+ ```vue
49
+ <KunEditor v-model="md" :adapters="adapters">
50
+ <template #toolbar="api"><KunEditorToolbar v-bind="api" /></template>
51
+ </KunEditor>
52
+ ```
53
+
54
+ This keeps `@kungal/editor-vue` **headless / zero UI-kit dependency** (any Vue app
55
+ can use it with the default toolbar) while the KunUI ecosystem (forum, moyu) gets
56
+ native chrome — the "headless core + optional UI layer" pattern (TipTap UI,
57
+ BlockNote's swappable toolbar). Also exports `EMOJI` from `@kungal/editor-vue` for
58
+ building custom pickers.
59
+
60
+ ### Patch Changes
61
+
62
+ - Updated dependencies [5786594]
63
+ - @kungal/editor-vue@0.12.0
64
+ - @kungal/editor-core@0.12.0
65
+
3
66
  ## 0.11.0
4
67
 
5
68
  ### Patch Changes
package/nuxt.config.ts CHANGED
@@ -1,28 +1,66 @@
1
- import { defineNuxtModule, addComponent } from '@nuxt/kit'
1
+ import { defineNuxtModule, addComponent, createResolver } from '@nuxt/kit'
2
2
 
3
- // KunEditor Nuxt layer. The component lives (already compiled) in the
4
- // framework-agnostic-friendly @kungal/editor-vue package; this layer just
5
- // registers it as a Nuxt auto-import so downstream templates can use
6
- // `<KunEditor>` with no import — and Nuxt generates its types, so the tag stays
7
- // type-checked in consumer templates.
3
+ // KunEditor Nuxt layer. <KunEditor> itself lives in the framework-agnostic,
4
+ // headless @kungal/editor-vue; this layer registers it as a Nuxt auto-import so
5
+ // downstream templates use `<KunEditor>` with no import (and Nuxt generates its
6
+ // types, keeping the tag type-checked in consumer templates).
7
+ //
8
+ // It also ships <KunEditorToolbar>: the KunUI-built toolbar for <KunEditor>'s
9
+ // #toolbar slot. It belongs HERE (this layer already assumes @kungal/ui-nuxt),
10
+ // not in headless @kungal/editor-vue — so editor-vue stays UI-kit-free while the
11
+ // KunUI ecosystem gets native chrome. Same shape as TipTap's optional UI pkg.
8
12
  export default defineNuxtConfig({
9
13
  modules: [
10
14
  defineNuxtModule({
11
15
  meta: { name: 'kun-editor-components' },
12
16
  setup() {
17
+ const { resolve } = createResolver(import.meta.url)
13
18
  addComponent({
14
19
  name: 'KunEditor',
15
20
  export: 'KunEditor',
16
21
  filePath: '@kungal/editor-vue'
17
22
  })
23
+ addComponent({
24
+ name: 'KunEditorToolbar',
25
+ filePath: resolve('./runtime/KunEditorToolbar.vue')
26
+ })
18
27
  }
19
28
  })
20
- ]
29
+ ],
30
+
31
+ // Under `nuxt dev`, Milkdown's markdown tokenizer (micromark) resolves to its
32
+ // DEV build, which does `import debug from 'debug'`. `debug` is CommonJS, so
33
+ // unless Vite pre-bundles it, its browser.js is served raw with no `default`
34
+ // export and the editor throws at load:
35
+ // SyntaxError: … 'debug' does not provide an export named 'default'
36
+ // (create-tokenizer.js)
37
+ // which also aborts app init. Whether it fires is non-deterministic per app —
38
+ // it depends on whether Vite's dep scan reaches the editor before it evaluates
39
+ // (the docs happens to get auto-scanned; other consumers don't). So pin it
40
+ // HERE, in the layer: force-including the editor/milkdown ENTRY points makes
41
+ // esbuild pre-bundle their whole subgraph and resolve micromark's CJS `debug`
42
+ // INSIDE the bundle. Merged into every consumer that extends this layer, so no
43
+ // app configures it. Dev-only: the prod build strips micromark's debug calls.
44
+ //
45
+ // NB: include the resolvable ENTRY points, not `'debug'` — under pnpm's
46
+ // non-hoisted layout `debug` isn't resolvable from the app root, so Vite would
47
+ // silently skip it. Unresolvable entries below are skipped too (harmless).
48
+ vite: {
49
+ optimizeDeps: {
50
+ include: [
51
+ '@kungal/editor-vue',
52
+ '@kungal/editor-core',
53
+ '@kungal/editor-core/preset',
54
+ '@milkdown/kit/preset/commonmark',
55
+ '@milkdown/kit/preset/gfm'
56
+ ]
57
+ }
58
+ }
21
59
  })
22
60
 
23
61
  // NOTE: this layer does NOT own a Tailwind entry or import editor styles. The
24
62
  // consuming app owns one stylesheet and adds `@kungal/editor-vue/style.css`
25
63
  // alongside its @kungal/ui-tokens + @kungal/ui-vue setup — the @source scan
26
64
  // path is node_modules-layout-specific, so only the app can write it. It also
27
- // assumes @kungal/ui-nuxt is already extended (KunEditor renders KunUI chrome).
28
- // See this package's README.
65
+ // assumes @kungal/ui-nuxt is already extended: <KunEditorToolbar> (and the
66
+ // reference editor styles) render KunUI chrome. See this package's README.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kungal/editor-nuxt",
3
- "version": "0.11.0",
3
+ "version": "0.13.0",
4
4
  "description": "KunEditor Nuxt Layer — wraps @kungal/editor-vue and auto-imports <KunEditor> so an existing Nuxt app keeps its exact DX (no import needed).",
5
5
  "type": "module",
6
6
  "keywords": [
@@ -26,13 +26,16 @@
26
26
  "files": [
27
27
  "CHANGELOG.md",
28
28
  "app",
29
+ "runtime",
29
30
  "nuxt.config.ts"
30
31
  ],
31
32
  "dependencies": {
32
- "@kungal/editor-vue": "0.11.0"
33
+ "@kungal/editor-core": "0.13.0",
34
+ "@kungal/editor-vue": "0.13.0"
33
35
  },
34
36
  "peerDependencies": {
35
37
  "@kungal/ui-vue": "^2.7.0",
38
+ "@milkdown/kit": "^7.21.2",
36
39
  "nuxt": "^4.0.0",
37
40
  "vue": "^3.5.0"
38
41
  },
@@ -0,0 +1,246 @@
1
+ <script setup lang="ts">
2
+ // A KunUI-built toolbar for <KunEditor>, dropped into its #toolbar scoped slot:
3
+ //
4
+ // <KunEditor v-model="md" :adapters="a">
5
+ // <template #toolbar="api"><KunEditorToolbar v-bind="api" /></template>
6
+ // </KunEditor>
7
+ //
8
+ // This lives in @kungal/editor-nuxt (the layer that already assumes @kungal/
9
+ // ui-nuxt), NOT in the headless @kungal/editor-vue — so editor-vue keeps its
10
+ // zero-UI-dep promise while the KunUI ecosystem (forum, moyu) gets native
11
+ // KunButton / KunIcon / KunTooltip / KunPopover chrome. It drives the editor
12
+ // purely through the slot API props (KunEditorToolbarApi) — no useInstance.
13
+ //
14
+ // KunButton/KunIcon/KunTooltip/KunPopover are auto-imported by @kungal/ui-nuxt in
15
+ // the consuming app; icons use the app's iconify set (e.g. lucide via @nuxt/icon).
16
+ import { computed, ref } from 'vue'
17
+ import {
18
+ createCodeBlockCommand,
19
+ insertHrCommand,
20
+ insertImageCommand,
21
+ toggleEmphasisCommand,
22
+ toggleInlineCodeCommand,
23
+ toggleStrongCommand,
24
+ wrapInBlockquoteCommand,
25
+ wrapInBulletListCommand,
26
+ wrapInHeadingCommand,
27
+ wrapInOrderedListCommand
28
+ } from '@milkdown/kit/preset/commonmark'
29
+ import { toggleStrikethroughCommand } from '@milkdown/kit/preset/gfm'
30
+ import {
31
+ insertKunSpoilerCommand,
32
+ toggleLatexCommand
33
+ } from '@kungal/editor-core/preset'
34
+ import { EMOJI } from '@kungal/editor-vue'
35
+ import type { KunEditorToolbarApi, StickerPack } from '@kungal/editor-vue'
36
+
37
+ const props = defineProps<KunEditorToolbarApi>()
38
+
39
+ const en = computed(() => props.locale.toLowerCase().startsWith('en'))
40
+ const t = computed(() => ({
41
+ bold: en.value ? 'Bold' : '加粗',
42
+ italic: en.value ? 'Italic' : '斜体',
43
+ strike: en.value ? 'Strikethrough' : '删除线',
44
+ code: en.value ? 'Inline code' : '行内代码',
45
+ h1: en.value ? 'Heading 1' : '一级标题',
46
+ h2: en.value ? 'Heading 2' : '二级标题',
47
+ h3: en.value ? 'Heading 3' : '三级标题',
48
+ bulletList: en.value ? 'Bullet list' : '无序列表',
49
+ orderedList: en.value ? 'Ordered list' : '有序列表',
50
+ quote: en.value ? 'Blockquote' : '引用块',
51
+ codeBlock: en.value ? 'Code block' : '代码块',
52
+ hr: en.value ? 'Divider' : '分隔线',
53
+ spoiler: en.value ? 'Spoiler' : '隐藏文本',
54
+ latex: en.value ? 'Math (LaTeX)' : '公式',
55
+ image: en.value ? 'Upload image' : '上传图片',
56
+ picker: en.value ? 'Emoji & stickers' : '表情与贴纸',
57
+ emoji: en.value ? 'Emoji' : '表情',
58
+ sticker: en.value ? 'Stickers' : '贴纸',
59
+ empty: en.value ? 'No stickers' : '暂无贴纸',
60
+ failed: en.value ? 'Failed to load stickers' : '贴纸加载失败'
61
+ }))
62
+
63
+ interface Tool {
64
+ icon: string
65
+ title: string
66
+ run: () => void
67
+ }
68
+
69
+ // Same command set + grouping as the default toolbar; `.key` is read at click
70
+ // time via api.run (populated once the editor is created).
71
+ const groups = computed<Tool[][]>(() => [
72
+ [
73
+ { icon: 'lucide:bold', title: t.value.bold, run: () => props.run(toggleStrongCommand.key) },
74
+ { icon: 'lucide:italic', title: t.value.italic, run: () => props.run(toggleEmphasisCommand.key) },
75
+ { icon: 'lucide:strikethrough', title: t.value.strike, run: () => props.run(toggleStrikethroughCommand.key) },
76
+ { icon: 'lucide:code', title: t.value.code, run: () => props.run(toggleInlineCodeCommand.key) }
77
+ ],
78
+ [
79
+ { icon: 'lucide:heading-1', title: t.value.h1, run: () => props.run(wrapInHeadingCommand.key, 1) },
80
+ { icon: 'lucide:heading-2', title: t.value.h2, run: () => props.run(wrapInHeadingCommand.key, 2) },
81
+ { icon: 'lucide:heading-3', title: t.value.h3, run: () => props.run(wrapInHeadingCommand.key, 3) }
82
+ ],
83
+ [
84
+ { icon: 'lucide:list', title: t.value.bulletList, run: () => props.run(wrapInBulletListCommand.key) },
85
+ { icon: 'lucide:list-ordered', title: t.value.orderedList, run: () => props.run(wrapInOrderedListCommand.key) },
86
+ { icon: 'lucide:text-quote', title: t.value.quote, run: () => props.run(wrapInBlockquoteCommand.key) },
87
+ { icon: 'lucide:square-code', title: t.value.codeBlock, run: () => props.run(createCodeBlockCommand.key, '') },
88
+ { icon: 'lucide:minus', title: t.value.hr, run: () => props.run(insertHrCommand.key) }
89
+ ],
90
+ [
91
+ { icon: 'lucide:eye-off', title: t.value.spoiler, run: () => props.run(insertKunSpoilerCommand.key) },
92
+ { icon: 'lucide:sigma', title: t.value.latex, run: () => props.run(toggleLatexCommand.key) }
93
+ ]
94
+ ])
95
+
96
+ // Image upload — only when the host supplies uploadImage. Same adapter as
97
+ // paste/drop; on success inserts an image node at the cursor.
98
+ const uploadImage = computed(() => props.adapters.uploadImage)
99
+ const fileInput = ref<HTMLInputElement | null>(null)
100
+ const pickImage = () => fileInput.value?.click()
101
+ const onFileChange = async (e: Event) => {
102
+ const input = e.target as HTMLInputElement
103
+ const file = input.files?.[0]
104
+ input.value = ''
105
+ const upload = uploadImage.value
106
+ if (!file || !upload) return
107
+ try {
108
+ const src = await upload(file)
109
+ props.run(insertImageCommand.key, { src, title: file.name, alt: file.name })
110
+ } catch {
111
+ props.adapters.notify?.(en.value ? 'Image upload failed' : '图片上传失败', 'error')
112
+ }
113
+ }
114
+
115
+ // Emoji (built-in) + sticker (adapter) picker in a KunPopover.
116
+ const showPicker = computed(() => props.features.sticker !== false)
117
+ const stickerSource = computed(() => props.adapters.stickerSource)
118
+ const hasSticker = computed(() => !!stickerSource.value)
119
+ const tab = ref<'emoji' | 'sticker'>('emoji')
120
+ const packs = ref<StickerPack[]>([])
121
+ const loaded = ref(false)
122
+ const loadFailed = ref(false)
123
+ const loadStickers = async () => {
124
+ const source = stickerSource.value
125
+ if (loaded.value || !source) return
126
+ loaded.value = true
127
+ try {
128
+ packs.value = await source()
129
+ } catch {
130
+ loadFailed.value = true
131
+ }
132
+ }
133
+ const insertSticker = (src: string, name: string) =>
134
+ props.run(insertImageCommand.key, { src, title: name, alt: name })
135
+ </script>
136
+
137
+ <template>
138
+ <div class="flex flex-wrap items-center gap-0.5">
139
+ <template v-for="(group, gi) in groups" :key="gi">
140
+ <span v-if="gi > 0" class="bg-default-200 mx-1 h-5 w-px" aria-hidden="true" />
141
+ <KunTooltip v-for="(b, bi) in group" :key="bi" :text="b.title" :delay-show="300">
142
+ <KunButton
143
+ variant="light"
144
+ size="sm"
145
+ :is-icon-only="true"
146
+ :aria-label="b.title"
147
+ @click="b.run()"
148
+ >
149
+ <KunIcon :name="b.icon" />
150
+ </KunButton>
151
+ </KunTooltip>
152
+ </template>
153
+
154
+ <template v-if="uploadImage">
155
+ <span class="bg-default-200 mx-1 h-5 w-px" aria-hidden="true" />
156
+ <KunTooltip :text="t.image" :delay-show="300">
157
+ <KunButton
158
+ variant="light"
159
+ size="sm"
160
+ :is-icon-only="true"
161
+ :aria-label="t.image"
162
+ @click="pickImage"
163
+ >
164
+ <KunIcon name="lucide:image" />
165
+ </KunButton>
166
+ </KunTooltip>
167
+ <input
168
+ ref="fileInput"
169
+ type="file"
170
+ accept=".jpg,.jpeg,.png,.webp,.gif"
171
+ hidden
172
+ @change="onFileChange"
173
+ />
174
+ </template>
175
+
176
+ <template v-if="showPicker">
177
+ <span class="bg-default-200 mx-1 h-5 w-px" aria-hidden="true" />
178
+ <KunPopover position="bottom-start" :opaque="true" inner-class="w-72 p-2">
179
+ <template #trigger>
180
+ <KunButton
181
+ variant="light"
182
+ size="sm"
183
+ :is-icon-only="true"
184
+ :aria-label="t.picker"
185
+ @click="hasSticker && loadStickers()"
186
+ >
187
+ <KunIcon name="lucide:smile-plus" />
188
+ </KunButton>
189
+ </template>
190
+
191
+ <div v-if="hasSticker" class="mb-2 flex gap-1">
192
+ <KunButton
193
+ :variant="tab === 'emoji' ? 'flat' : 'light'"
194
+ size="sm"
195
+ @click="tab = 'emoji'"
196
+ >{{ t.emoji }}</KunButton
197
+ >
198
+ <KunButton
199
+ :variant="tab === 'sticker' ? 'flat' : 'light'"
200
+ size="sm"
201
+ @click="tab = 'sticker'"
202
+ >{{ t.sticker }}</KunButton
203
+ >
204
+ </div>
205
+
206
+ <div
207
+ v-show="!hasSticker || tab === 'emoji'"
208
+ class="grid max-h-56 grid-cols-8 gap-0.5 overflow-y-auto"
209
+ >
210
+ <button
211
+ v-for="(e, i) in EMOJI"
212
+ :key="i"
213
+ type="button"
214
+ class="hover:bg-default-100 rounded p-1 text-lg leading-none"
215
+ @click="props.insertText(e)"
216
+ >
217
+ {{ e }}
218
+ </button>
219
+ </div>
220
+
221
+ <div v-if="hasSticker" v-show="tab === 'sticker'" class="max-h-56 overflow-y-auto">
222
+ <template v-if="packs.length">
223
+ <div v-for="pack in packs" :key="pack.name" class="mb-2">
224
+ <div class="text-default-500 mb-1 text-xs">{{ pack.name }}</div>
225
+ <div class="grid grid-cols-4 gap-1">
226
+ <button
227
+ v-for="(s, i) in pack.stickers"
228
+ :key="i"
229
+ type="button"
230
+ class="hover:bg-default-100 rounded p-1"
231
+ :title="s.name"
232
+ @click="insertSticker(s.src, s.name)"
233
+ >
234
+ <img :src="s.src" :alt="s.name" loading="lazy" class="h-14 w-14 object-contain" />
235
+ </button>
236
+ </div>
237
+ </div>
238
+ </template>
239
+ <div v-else class="text-default-500 p-4 text-center text-sm">
240
+ {{ loadFailed ? t.failed : t.empty }}
241
+ </div>
242
+ </div>
243
+ </KunPopover>
244
+ </template>
245
+ </div>
246
+ </template>