quasar-ui-danx 0.5.1 → 0.5.2
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/.claude/settings.local.json +8 -0
- package/dist/danx.es.js +11359 -10497
- package/dist/danx.es.js.map +1 -1
- package/dist/danx.umd.js +138 -131
- package/dist/danx.umd.js.map +1 -1
- package/dist/style.css +1 -1
- package/package.json +1 -1
- package/src/components/Utility/Buttons/ActionButton.vue +15 -5
- package/src/components/Utility/Code/CodeViewer.vue +10 -2
- package/src/components/Utility/Code/CodeViewerFooter.vue +2 -0
- package/src/components/Utility/Code/MarkdownContent.vue +31 -163
- package/src/components/Utility/Markdown/MarkdownEditor.vue +7 -2
- package/src/components/Utility/Markdown/MarkdownEditorContent.vue +69 -8
- package/src/components/Utility/Widgets/LabelPillWidget.vue +20 -0
- package/src/composables/markdown/features/useCodeBlocks.spec.ts +59 -33
- package/src/composables/markdown/features/useLinks.spec.ts +29 -10
- package/src/composables/markdown/useMarkdownEditor.ts +16 -7
- package/src/composables/useCodeFormat.ts +17 -10
- package/src/helpers/formats/highlightCSS.ts +236 -0
- package/src/helpers/formats/highlightHTML.ts +483 -0
- package/src/helpers/formats/highlightJavaScript.ts +346 -0
- package/src/helpers/formats/highlightSyntax.ts +15 -4
- package/src/helpers/formats/index.ts +3 -0
- package/src/helpers/formats/markdown/htmlToMarkdown/index.ts +15 -2
- package/src/helpers/formats/markdown/linePatterns.spec.ts +7 -4
- package/src/styles/danx.scss +3 -3
- package/src/styles/index.scss +5 -5
- package/src/styles/themes/danx/code.scss +257 -1
- package/src/styles/themes/danx/index.scss +10 -10
- package/src/styles/themes/danx/markdown.scss +59 -0
- package/src/test/highlighters.test.ts +153 -0
- package/src/types/widgets.d.ts +2 -2
- package/vite.config.js +5 -1
package/package.json
CHANGED
|
@@ -8,14 +8,14 @@
|
|
|
8
8
|
>
|
|
9
9
|
<div class="flex items-center flex-nowrap">
|
|
10
10
|
<component
|
|
11
|
+
v-if="icon || typeOptions.icon"
|
|
11
12
|
:is="icon || typeOptions.icon"
|
|
12
13
|
class="transition-all"
|
|
13
14
|
:class="resolvedIconClass"
|
|
14
15
|
/>
|
|
15
16
|
<div
|
|
16
17
|
v-if="label || label === 0"
|
|
17
|
-
class="ml-2"
|
|
18
|
-
:class="labelClass"
|
|
18
|
+
:class="[labelClass, { 'ml-2': icon || typeOptions.icon }]"
|
|
19
19
|
>
|
|
20
20
|
{{ label }}
|
|
21
21
|
</div>
|
|
@@ -87,7 +87,7 @@ import { ActionTarget, ResourceAction } from "../../../types";
|
|
|
87
87
|
|
|
88
88
|
export interface ActionButtonProps {
|
|
89
89
|
type?: "save" | "trash" | "back" | "create" | "edit" | "copy" | "folder" | "document" | "play" | "stop" | "pause" | "refresh" | "restart" | "confirm" | "cancel" | "export" | "import" | "minus" | "merge" | "check" | "clock" | "view" | "database" | "users" | "close";
|
|
90
|
-
color?: "red" | "blue" | "blue-invert" | "sky" | "sky-invert" | "green" | "green-invert" | "lime" | "white" | "gray" | "slate" | "slate-invert" | "yellow" | "orange" | "amber" | "purple" | "teal" | "teal-invert";
|
|
90
|
+
color?: "red" | "blue" | "blue-invert" | "sky" | "sky-invert" | "sky-soft" | "green" | "green-invert" | "green-soft" | "lime" | "white" | "gray" | "slate" | "slate-invert" | "slate-soft" | "yellow" | "orange" | "amber" | "purple" | "blue-soft" | "red-soft" | "teal" | "teal-invert";
|
|
91
91
|
size?: "xxs" | "xs" | "sm" | "md" | "lg";
|
|
92
92
|
icon?: object | string;
|
|
93
93
|
iconClass?: string;
|
|
@@ -126,7 +126,7 @@ const props = withDefaults(defineProps<ActionButtonProps>(), {
|
|
|
126
126
|
const mappedSizeClass = {
|
|
127
127
|
xxs: {
|
|
128
128
|
icon: "w-2",
|
|
129
|
-
button: "px
|
|
129
|
+
button: "px-1 h-4 text-[10px]"
|
|
130
130
|
},
|
|
131
131
|
xs: {
|
|
132
132
|
icon: "w-3",
|
|
@@ -157,16 +157,22 @@ const colorClass = computed(() => {
|
|
|
157
157
|
switch (props.color) {
|
|
158
158
|
case "red":
|
|
159
159
|
return "text-red-900 bg-red-300 hover:bg-red-400";
|
|
160
|
+
case "red-soft":
|
|
161
|
+
return "text-red-700 bg-red-100 hover:bg-red-200";
|
|
160
162
|
case "lime":
|
|
161
163
|
return "text-lime-900 bg-lime-300 hover:bg-lime-400";
|
|
162
164
|
case "green":
|
|
163
165
|
return "text-green-900 bg-green-300 hover:bg-green-400";
|
|
164
166
|
case "green-invert":
|
|
165
167
|
return "text-green-300 bg-green-900 hover:bg-green-800";
|
|
168
|
+
case "green-soft":
|
|
169
|
+
return "text-green-700 bg-green-100 hover:bg-green-200";
|
|
166
170
|
case "blue":
|
|
167
171
|
return "text-blue-900 bg-blue-300 hover:bg-blue-400";
|
|
168
172
|
case "blue-invert":
|
|
169
173
|
return "text-blue-300 bg-blue-900 hover:bg-blue-800";
|
|
174
|
+
case "blue-soft":
|
|
175
|
+
return "text-blue-700 bg-blue-100 hover:bg-blue-200";
|
|
170
176
|
case "teal":
|
|
171
177
|
return "text-teal-800 bg-teal-200 hover:bg-teal-400";
|
|
172
178
|
case "teal-invert":
|
|
@@ -175,6 +181,8 @@ const colorClass = computed(() => {
|
|
|
175
181
|
return "text-sky-900 bg-sky-300 hover:bg-sky-400";
|
|
176
182
|
case "sky-invert":
|
|
177
183
|
return "text-sky-400 bg-sky-800 hover:bg-sky-900";
|
|
184
|
+
case "sky-soft":
|
|
185
|
+
return "text-sky-700 bg-sky-100 hover:bg-sky-200";
|
|
178
186
|
case "white":
|
|
179
187
|
return "text-white bg-gray-800 hover:bg-gray-200";
|
|
180
188
|
case "yellow":
|
|
@@ -189,6 +197,8 @@ const colorClass = computed(() => {
|
|
|
189
197
|
return "text-slate-900 bg-slate-300 hover:bg-slate-400";
|
|
190
198
|
case "slate-invert":
|
|
191
199
|
return "text-slate-300 bg-slate-900 hover:bg-slate-800";
|
|
200
|
+
case "slate-soft":
|
|
201
|
+
return "text-slate-600 bg-slate-100 hover:bg-slate-200";
|
|
192
202
|
case "purple":
|
|
193
203
|
return "text-purple-300 bg-purple-900 hover:bg-purple-800";
|
|
194
204
|
default:
|
|
@@ -251,7 +261,7 @@ const typeOptions = computed(() => {
|
|
|
251
261
|
case "close":
|
|
252
262
|
return { icon: CloseIcon };
|
|
253
263
|
default:
|
|
254
|
-
return { icon:
|
|
264
|
+
return { icon: null };
|
|
255
265
|
}
|
|
256
266
|
});
|
|
257
267
|
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
<template>
|
|
2
2
|
<div
|
|
3
3
|
class="dx-code-viewer group flex flex-col"
|
|
4
|
-
:class="{ 'is-collapsed': isCollapsed }"
|
|
4
|
+
:class="[{ 'is-collapsed': isCollapsed }, props.theme === 'light' ? 'theme-light' : '']"
|
|
5
5
|
>
|
|
6
6
|
<FieldLabel
|
|
7
7
|
v-if="label"
|
|
@@ -84,10 +84,12 @@
|
|
|
84
84
|
|
|
85
85
|
<!-- Footer with char count and edit toggle -->
|
|
86
86
|
<CodeViewerFooter
|
|
87
|
+
v-if="!hideFooter"
|
|
87
88
|
:char-count="editor.charCount.value"
|
|
88
89
|
:validation-error="editor.validationError.value"
|
|
89
90
|
:can-edit="canEdit && currentFormat !== 'markdown'"
|
|
90
91
|
:is-editing="editor.isEditing.value"
|
|
92
|
+
:show-version="showVersion"
|
|
91
93
|
@toggle-edit="editor.toggleEdit"
|
|
92
94
|
/>
|
|
93
95
|
</div>
|
|
@@ -118,6 +120,9 @@ export interface CodeViewerProps {
|
|
|
118
120
|
defaultCollapsed?: boolean;
|
|
119
121
|
defaultCodeFormat?: "json" | "yaml";
|
|
120
122
|
allowAnyLanguage?: boolean;
|
|
123
|
+
theme?: "dark" | "light";
|
|
124
|
+
showVersion?: boolean;
|
|
125
|
+
hideFooter?: boolean;
|
|
121
126
|
}
|
|
122
127
|
|
|
123
128
|
const props = withDefaults(defineProps<CodeViewerProps>(), {
|
|
@@ -128,7 +133,10 @@ const props = withDefaults(defineProps<CodeViewerProps>(), {
|
|
|
128
133
|
canEdit: false,
|
|
129
134
|
editable: false,
|
|
130
135
|
collapsible: false,
|
|
131
|
-
defaultCollapsed: true
|
|
136
|
+
defaultCollapsed: true,
|
|
137
|
+
theme: "dark",
|
|
138
|
+
showVersion: false,
|
|
139
|
+
hideFooter: false
|
|
132
140
|
});
|
|
133
141
|
|
|
134
142
|
const emit = defineEmits<{
|
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
</template>
|
|
13
13
|
<template v-else>
|
|
14
14
|
{{ charCount.toLocaleString() }} chars
|
|
15
|
+
<span v-if="showVersion" class="ml-2 text-sky-400">[v1.0.5]</span>
|
|
15
16
|
</template>
|
|
16
17
|
</div>
|
|
17
18
|
<!-- Edit toggle button -->
|
|
@@ -41,6 +42,7 @@ export interface CodeViewerFooterProps {
|
|
|
41
42
|
validationError: ValidationError | null;
|
|
42
43
|
canEdit: boolean;
|
|
43
44
|
isEditing: boolean;
|
|
45
|
+
showVersion?: boolean;
|
|
44
46
|
}
|
|
45
47
|
|
|
46
48
|
const props = defineProps<CodeViewerFooterProps>();
|
|
@@ -9,29 +9,17 @@
|
|
|
9
9
|
/>
|
|
10
10
|
|
|
11
11
|
<!-- Code blocks with syntax highlighting -->
|
|
12
|
-
<
|
|
12
|
+
<CodeViewer
|
|
13
13
|
v-else-if="token.type === 'code_block'"
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
/>
|
|
24
|
-
<LanguageBadge
|
|
25
|
-
v-else-if="token.language"
|
|
26
|
-
:format="token.language"
|
|
27
|
-
:available-formats="[]"
|
|
28
|
-
:toggleable="false"
|
|
29
|
-
/>
|
|
30
|
-
<pre><code
|
|
31
|
-
:class="'language-' + getCodeBlockFormat(index, token.language)"
|
|
32
|
-
v-html="highlightCodeBlock(index, token.content, token.language)"
|
|
33
|
-
></code></pre>
|
|
34
|
-
</div>
|
|
14
|
+
:model-value="token.content"
|
|
15
|
+
:format="normalizeLanguage(token.language)"
|
|
16
|
+
:default-code-format="defaultCodeFormat"
|
|
17
|
+
:can-edit="false"
|
|
18
|
+
:collapsible="false"
|
|
19
|
+
hide-footer
|
|
20
|
+
allow-any-language
|
|
21
|
+
class="markdown-code-block"
|
|
22
|
+
/>
|
|
35
23
|
|
|
36
24
|
<!-- Blockquotes (recursive) -->
|
|
37
25
|
<blockquote
|
|
@@ -186,12 +174,10 @@
|
|
|
186
174
|
</template>
|
|
187
175
|
|
|
188
176
|
<script setup lang="ts">
|
|
189
|
-
import { computed
|
|
190
|
-
import { parse as parseYAML, stringify as stringifyYAML } from "yaml";
|
|
177
|
+
import { computed } from "vue";
|
|
191
178
|
import { tokenizeBlocks, parseInline, renderMarkdown, getFootnotes, resetParserState } from "../../../helpers/formats/markdown";
|
|
192
179
|
import type { BlockToken, ListItem } from "../../../helpers/formats/markdown";
|
|
193
|
-
import
|
|
194
|
-
import LanguageBadge from "./LanguageBadge.vue";
|
|
180
|
+
import CodeViewer from "./CodeViewer.vue";
|
|
195
181
|
|
|
196
182
|
export interface MarkdownContentProps {
|
|
197
183
|
content: string;
|
|
@@ -202,10 +188,21 @@ const props = withDefaults(defineProps<MarkdownContentProps>(), {
|
|
|
202
188
|
content: ""
|
|
203
189
|
});
|
|
204
190
|
|
|
205
|
-
//
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
const
|
|
191
|
+
// Normalize language aliases to standard names
|
|
192
|
+
function normalizeLanguage(lang?: string): string {
|
|
193
|
+
if (!lang) return "text";
|
|
194
|
+
const aliases: Record<string, string> = {
|
|
195
|
+
js: "javascript",
|
|
196
|
+
ts: "typescript",
|
|
197
|
+
py: "python",
|
|
198
|
+
rb: "ruby",
|
|
199
|
+
yml: "yaml",
|
|
200
|
+
md: "markdown",
|
|
201
|
+
sh: "bash",
|
|
202
|
+
shell: "bash"
|
|
203
|
+
};
|
|
204
|
+
return aliases[lang.toLowerCase()] || lang.toLowerCase();
|
|
205
|
+
}
|
|
209
206
|
|
|
210
207
|
// Tokenize the markdown content
|
|
211
208
|
const tokens = computed<BlockToken[]>(() => {
|
|
@@ -234,124 +231,6 @@ const sortedFootnotes = computed(() => {
|
|
|
234
231
|
.map(([id, fn]) => ({ id, content: fn.content, index: fn.index }));
|
|
235
232
|
});
|
|
236
233
|
|
|
237
|
-
// Check if a language is toggleable (json or yaml)
|
|
238
|
-
function isToggleableLanguage(language: string): boolean {
|
|
239
|
-
if (!language) return false;
|
|
240
|
-
const lang = language.toLowerCase();
|
|
241
|
-
return lang === "json" || lang === "yaml";
|
|
242
|
-
}
|
|
243
|
-
|
|
244
|
-
// Get the current format for a code block (respecting user toggle, then default override, then original)
|
|
245
|
-
function getCodeBlockFormat(index: number, originalLanguage: string): string {
|
|
246
|
-
// If user has toggled this block, use their choice
|
|
247
|
-
if (codeBlockFormats[index]) {
|
|
248
|
-
return codeBlockFormats[index];
|
|
249
|
-
}
|
|
250
|
-
|
|
251
|
-
// If a default is set and this is a toggleable language, use the default
|
|
252
|
-
const lang = originalLanguage?.toLowerCase();
|
|
253
|
-
if (props.defaultCodeFormat && (lang === "json" || lang === "yaml")) {
|
|
254
|
-
return props.defaultCodeFormat;
|
|
255
|
-
}
|
|
256
|
-
|
|
257
|
-
// Otherwise use the original language
|
|
258
|
-
return lang || "text";
|
|
259
|
-
}
|
|
260
|
-
|
|
261
|
-
// Get converted content for a code block (handles initial conversion for defaultCodeFormat)
|
|
262
|
-
function getConvertedContent(index: number, originalContent: string, originalLang: string): string {
|
|
263
|
-
const format = getCodeBlockFormat(index, originalLang);
|
|
264
|
-
|
|
265
|
-
// If format matches original, no conversion needed
|
|
266
|
-
if (format === originalLang || !isToggleableLanguage(originalLang)) {
|
|
267
|
-
return originalContent;
|
|
268
|
-
}
|
|
269
|
-
|
|
270
|
-
// Convert from original to target format
|
|
271
|
-
try {
|
|
272
|
-
let parsed: unknown;
|
|
273
|
-
|
|
274
|
-
if (originalLang === "json") {
|
|
275
|
-
parsed = JSON.parse(originalContent);
|
|
276
|
-
} else if (originalLang === "yaml") {
|
|
277
|
-
parsed = parseYAML(originalContent);
|
|
278
|
-
}
|
|
279
|
-
|
|
280
|
-
if (parsed !== undefined) {
|
|
281
|
-
if (format === "json") {
|
|
282
|
-
return JSON.stringify(parsed, null, 2);
|
|
283
|
-
} else if (format === "yaml") {
|
|
284
|
-
return stringifyYAML(parsed as object);
|
|
285
|
-
}
|
|
286
|
-
}
|
|
287
|
-
} catch {
|
|
288
|
-
// Conversion failed, return original
|
|
289
|
-
}
|
|
290
|
-
|
|
291
|
-
return originalContent;
|
|
292
|
-
}
|
|
293
|
-
|
|
294
|
-
// Set format for a code block (converts content to the new format)
|
|
295
|
-
function setCodeBlockFormat(index: number, newFormat: string) {
|
|
296
|
-
const token = tokens.value[index];
|
|
297
|
-
if (token?.type !== "code_block") return;
|
|
298
|
-
|
|
299
|
-
const originalLang = token.language?.toLowerCase() || "json";
|
|
300
|
-
const current = getCodeBlockFormat(index, originalLang);
|
|
301
|
-
|
|
302
|
-
// No change needed if already in target format
|
|
303
|
-
if (current === newFormat) return;
|
|
304
|
-
|
|
305
|
-
// Convert the content
|
|
306
|
-
try {
|
|
307
|
-
// Use the currently displayed content (which may already be converted due to defaultCodeFormat)
|
|
308
|
-
const sourceContent = convertedContent[index] || getConvertedContent(index, token.content, originalLang);
|
|
309
|
-
let parsed: unknown;
|
|
310
|
-
|
|
311
|
-
// Parse from current format
|
|
312
|
-
if (current === "json") {
|
|
313
|
-
parsed = JSON.parse(sourceContent);
|
|
314
|
-
} else {
|
|
315
|
-
parsed = parseYAML(sourceContent);
|
|
316
|
-
}
|
|
317
|
-
|
|
318
|
-
// Convert to new format
|
|
319
|
-
if (newFormat === "json") {
|
|
320
|
-
convertedContent[index] = JSON.stringify(parsed, null, 2);
|
|
321
|
-
} else {
|
|
322
|
-
convertedContent[index] = stringifyYAML(parsed as object);
|
|
323
|
-
}
|
|
324
|
-
|
|
325
|
-
codeBlockFormats[index] = newFormat;
|
|
326
|
-
} catch {
|
|
327
|
-
// If conversion fails, just set the format without converting
|
|
328
|
-
codeBlockFormats[index] = newFormat;
|
|
329
|
-
}
|
|
330
|
-
}
|
|
331
|
-
|
|
332
|
-
// Highlight code block content based on format
|
|
333
|
-
function highlightCodeBlock(index: number, originalContent: string, originalLanguage: string): string {
|
|
334
|
-
const format = getCodeBlockFormat(index, originalLanguage);
|
|
335
|
-
const originalLang = originalLanguage?.toLowerCase() || "text";
|
|
336
|
-
|
|
337
|
-
// Get the content (converted if needed, or from cache if user toggled)
|
|
338
|
-
const content = convertedContent[index] || getConvertedContent(index, originalContent, originalLang);
|
|
339
|
-
|
|
340
|
-
// Apply syntax highlighting
|
|
341
|
-
switch (format) {
|
|
342
|
-
case "json":
|
|
343
|
-
return highlightJSON(content);
|
|
344
|
-
case "yaml":
|
|
345
|
-
return highlightYAML(content);
|
|
346
|
-
default:
|
|
347
|
-
// For other languages, just escape HTML
|
|
348
|
-
return content
|
|
349
|
-
.replace(/&/g, "&")
|
|
350
|
-
.replace(/</g, "<")
|
|
351
|
-
.replace(/>/g, ">");
|
|
352
|
-
}
|
|
353
|
-
}
|
|
354
|
-
|
|
355
234
|
// Parse inline markdown (bold, italic, links, etc.)
|
|
356
235
|
function parseInlineContent(text: string): string {
|
|
357
236
|
return parseInline(text, true);
|
|
@@ -383,23 +262,12 @@ function renderBlockquote(content: string): string {
|
|
|
383
262
|
</script>
|
|
384
263
|
|
|
385
264
|
<style lang="scss">
|
|
386
|
-
.
|
|
387
|
-
position: relative;
|
|
265
|
+
.markdown-code-block {
|
|
388
266
|
margin: 1em 0;
|
|
389
267
|
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
padding: 1em;
|
|
394
|
-
border-radius: 6px;
|
|
395
|
-
overflow-x: auto;
|
|
396
|
-
|
|
397
|
-
code {
|
|
398
|
-
background: transparent;
|
|
399
|
-
padding: 0;
|
|
400
|
-
font-size: 0.875em;
|
|
401
|
-
font-family: 'Fira Code', 'Monaco', monospace;
|
|
402
|
-
}
|
|
268
|
+
// Ensure auto-height instead of 100%
|
|
269
|
+
&.dx-code-viewer {
|
|
270
|
+
height: auto;
|
|
403
271
|
}
|
|
404
272
|
}
|
|
405
273
|
</style>
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
<template>
|
|
2
|
-
<div class="dx-markdown-editor" :class="{ 'is-readonly': readonly }">
|
|
2
|
+
<div class="dx-markdown-editor" :class="[{ 'is-readonly': readonly }, props.theme === 'light' ? 'theme-light' : '']">
|
|
3
3
|
<div class="dx-markdown-editor-body" @contextmenu="contextMenu.show">
|
|
4
4
|
<!-- Floating line type menu positioned next to current block -->
|
|
5
5
|
<div
|
|
@@ -25,6 +25,7 @@
|
|
|
25
25
|
</div>
|
|
26
26
|
|
|
27
27
|
<MarkdownEditorFooter
|
|
28
|
+
v-if="!hideFooter"
|
|
28
29
|
:char-count="editor.charCount.value"
|
|
29
30
|
@show-hotkeys="editor.showHotkeyHelp"
|
|
30
31
|
/>
|
|
@@ -81,6 +82,8 @@ export interface MarkdownEditorProps {
|
|
|
81
82
|
readonly?: boolean;
|
|
82
83
|
minHeight?: string;
|
|
83
84
|
maxHeight?: string;
|
|
85
|
+
theme?: "dark" | "light";
|
|
86
|
+
hideFooter?: boolean;
|
|
84
87
|
}
|
|
85
88
|
|
|
86
89
|
const props = withDefaults(defineProps<MarkdownEditorProps>(), {
|
|
@@ -88,7 +91,9 @@ const props = withDefaults(defineProps<MarkdownEditorProps>(), {
|
|
|
88
91
|
placeholder: "Start typing...",
|
|
89
92
|
readonly: false,
|
|
90
93
|
minHeight: "100px",
|
|
91
|
-
maxHeight: "none"
|
|
94
|
+
maxHeight: "none",
|
|
95
|
+
theme: "dark",
|
|
96
|
+
hideFooter: false
|
|
92
97
|
});
|
|
93
98
|
|
|
94
99
|
const emit = defineEmits<{
|
|
@@ -2,10 +2,10 @@
|
|
|
2
2
|
<div
|
|
3
3
|
ref="containerRef"
|
|
4
4
|
class="dx-markdown-editor-content dx-markdown-content"
|
|
5
|
-
:class="{ 'is-readonly': readonly, 'is-empty':
|
|
5
|
+
:class="{ 'is-readonly': readonly, 'is-empty': isContentEmpty }"
|
|
6
6
|
:contenteditable="!readonly"
|
|
7
7
|
:data-placeholder="placeholder"
|
|
8
|
-
@input="
|
|
8
|
+
@input="onInput"
|
|
9
9
|
@keydown="$emit('keydown', $event)"
|
|
10
10
|
@blur="$emit('blur')"
|
|
11
11
|
@click="handleClick"
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
</template>
|
|
15
15
|
|
|
16
16
|
<script setup lang="ts">
|
|
17
|
-
import {
|
|
17
|
+
import { nextTick, ref, watch } from "vue";
|
|
18
18
|
|
|
19
19
|
export interface MarkdownEditorContentProps {
|
|
20
20
|
html: string;
|
|
@@ -27,13 +27,38 @@ const props = withDefaults(defineProps<MarkdownEditorContentProps>(), {
|
|
|
27
27
|
placeholder: "Start typing..."
|
|
28
28
|
});
|
|
29
29
|
|
|
30
|
-
defineEmits<{
|
|
30
|
+
const emit = defineEmits<{
|
|
31
31
|
input: [];
|
|
32
32
|
keydown: [event: KeyboardEvent];
|
|
33
33
|
blur: [];
|
|
34
34
|
}>();
|
|
35
35
|
|
|
36
36
|
const containerRef = ref<HTMLElement | null>(null);
|
|
37
|
+
const isContentEmpty = ref(true);
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Check if the editor content is empty by examining the actual DOM text content.
|
|
41
|
+
* This is needed because contenteditable changes the DOM directly without updating props.
|
|
42
|
+
*/
|
|
43
|
+
function checkIfEmpty(): void {
|
|
44
|
+
if (containerRef.value) {
|
|
45
|
+
const textContent = containerRef.value.textContent?.trim() || "";
|
|
46
|
+
isContentEmpty.value = textContent.length === 0;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Handle input events - check if content is empty and emit the input event.
|
|
52
|
+
*/
|
|
53
|
+
function onInput(): void {
|
|
54
|
+
checkIfEmpty();
|
|
55
|
+
emit("input");
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Watch for external HTML changes (e.g., from parent component)
|
|
59
|
+
watch(() => props.html, () => {
|
|
60
|
+
nextTick(() => checkIfEmpty());
|
|
61
|
+
}, { immediate: true });
|
|
37
62
|
|
|
38
63
|
/**
|
|
39
64
|
* Find the anchor element if the click target is inside one
|
|
@@ -74,10 +99,6 @@ function handleClick(event: MouseEvent): void {
|
|
|
74
99
|
window.open(href, "_blank", "noopener,noreferrer");
|
|
75
100
|
}
|
|
76
101
|
|
|
77
|
-
const isEmpty = computed(() => {
|
|
78
|
-
return !props.html || props.html === "<p></p>" || props.html === "<p><br></p>";
|
|
79
|
-
});
|
|
80
|
-
|
|
81
102
|
// Expose containerRef for parent component
|
|
82
103
|
defineExpose({ containerRef });
|
|
83
104
|
</script>
|
|
@@ -231,5 +252,45 @@ defineExpose({ containerRef });
|
|
|
231
252
|
}
|
|
232
253
|
}
|
|
233
254
|
}
|
|
255
|
+
|
|
256
|
+
// ==========================================
|
|
257
|
+
// LIGHT THEME VARIANT
|
|
258
|
+
// ==========================================
|
|
259
|
+
.dx-markdown-editor.theme-light & {
|
|
260
|
+
background-color: #f8fafc;
|
|
261
|
+
color: #1e293b;
|
|
262
|
+
caret-color: #1e293b;
|
|
263
|
+
|
|
264
|
+
&:focus {
|
|
265
|
+
border-color: rgba(14, 165, 233, 0.6);
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
&:hover:not(:focus):not(.is-readonly) {
|
|
269
|
+
border-color: rgba(14, 165, 233, 0.3);
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
// Placeholder styling - light theme
|
|
273
|
+
&.is-empty::before {
|
|
274
|
+
color: #94a3b8;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
// Link tooltip - light theme
|
|
278
|
+
a:hover::after {
|
|
279
|
+
background: #e2e8f0;
|
|
280
|
+
color: #1e293b;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
// Code block wrapper - light theme
|
|
284
|
+
.code-block-wrapper {
|
|
285
|
+
background: #f1f5f9;
|
|
286
|
+
border-color: #e2e8f0;
|
|
287
|
+
|
|
288
|
+
.dx-code-viewer {
|
|
289
|
+
.code-footer {
|
|
290
|
+
background: #e2e8f0;
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
}
|
|
234
295
|
}
|
|
235
296
|
</style>
|
|
@@ -14,6 +14,7 @@ const props = withDefaults(defineProps<LabelPillWidgetProps>(), {
|
|
|
14
14
|
});
|
|
15
15
|
|
|
16
16
|
const colorClasses = {
|
|
17
|
+
// Dark theme colors (dark backgrounds with bright text)
|
|
17
18
|
sky: "bg-sky-950 text-sky-400",
|
|
18
19
|
green: "bg-green-950 text-green-400",
|
|
19
20
|
red: "bg-red-950 text-red-400",
|
|
@@ -33,6 +34,25 @@ const colorClasses = {
|
|
|
33
34
|
indigo: "bg-indigo-950 text-indigo-400",
|
|
34
35
|
violet: "bg-violet-950 text-violet-400",
|
|
35
36
|
fuchsia: "bg-fuchsia-950 text-fuchsia-400",
|
|
37
|
+
// Soft/light theme colors (light backgrounds with darker text)
|
|
38
|
+
"sky-soft": "bg-sky-100 text-sky-700",
|
|
39
|
+
"green-soft": "bg-green-100 text-green-700",
|
|
40
|
+
"red-soft": "bg-red-100 text-red-700",
|
|
41
|
+
"amber-soft": "bg-amber-100 text-amber-700",
|
|
42
|
+
"yellow-soft": "bg-yellow-100 text-yellow-700",
|
|
43
|
+
"blue-soft": "bg-blue-100 text-blue-700",
|
|
44
|
+
"purple-soft": "bg-purple-100 text-purple-700",
|
|
45
|
+
"slate-soft": "bg-slate-100 text-slate-600",
|
|
46
|
+
"gray-soft": "bg-gray-100 text-gray-600",
|
|
47
|
+
"emerald-soft": "bg-emerald-100 text-emerald-700",
|
|
48
|
+
"orange-soft": "bg-orange-100 text-orange-700",
|
|
49
|
+
"lime-soft": "bg-lime-100 text-lime-700",
|
|
50
|
+
"teal-soft": "bg-teal-100 text-teal-700",
|
|
51
|
+
"cyan-soft": "bg-cyan-100 text-cyan-700",
|
|
52
|
+
"rose-soft": "bg-rose-100 text-rose-700",
|
|
53
|
+
"indigo-soft": "bg-indigo-100 text-indigo-700",
|
|
54
|
+
"violet-soft": "bg-violet-100 text-violet-700",
|
|
55
|
+
"fuchsia-soft": "bg-fuchsia-100 text-fuchsia-700",
|
|
36
56
|
none: ""
|
|
37
57
|
};
|
|
38
58
|
|