Skip to content

Commit 3171d4f

Browse files
committed
Add markdown widget
1 parent be22ec1 commit 3171d4f

File tree

4 files changed

+285
-7
lines changed

4 files changed

+285
-7
lines changed

src/assets/css/style.css

Lines changed: 182 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,188 @@ body {
134134
border: thin solid;
135135
}
136136

137+
/* Shared markdown content styling for consistent rendering across components */
138+
.comfy-markdown-content {
139+
/* Typography */
140+
font-size: 0.875rem; /* text-sm */
141+
line-height: 1.6;
142+
word-wrap: break-word;
143+
}
144+
145+
/* Headings */
146+
.comfy-markdown-content h1 {
147+
font-size: 22px; /* text-[22px] */
148+
font-weight: 700; /* font-bold */
149+
margin-top: 2rem; /* mt-8 */
150+
margin-bottom: 1rem; /* mb-4 */
151+
}
152+
153+
.comfy-markdown-content h1:first-child {
154+
margin-top: 0; /* first:mt-0 */
155+
}
156+
157+
.comfy-markdown-content h2 {
158+
font-size: 18px; /* text-[18px] */
159+
font-weight: 700; /* font-bold */
160+
margin-top: 2rem; /* mt-8 */
161+
margin-bottom: 1rem; /* mb-4 */
162+
}
163+
164+
.comfy-markdown-content h2:first-child {
165+
margin-top: 0; /* first:mt-0 */
166+
}
167+
168+
.comfy-markdown-content h3 {
169+
font-size: 16px; /* text-[16px] */
170+
font-weight: 700; /* font-bold */
171+
margin-top: 2rem; /* mt-8 */
172+
margin-bottom: 1rem; /* mb-4 */
173+
}
174+
175+
.comfy-markdown-content h3:first-child {
176+
margin-top: 0; /* first:mt-0 */
177+
}
178+
179+
.comfy-markdown-content h4,
180+
.comfy-markdown-content h5,
181+
.comfy-markdown-content h6 {
182+
margin-top: 2rem; /* mt-8 */
183+
margin-bottom: 1rem; /* mb-4 */
184+
}
185+
186+
.comfy-markdown-content h4:first-child,
187+
.comfy-markdown-content h5:first-child,
188+
.comfy-markdown-content h6:first-child {
189+
margin-top: 0; /* first:mt-0 */
190+
}
191+
192+
/* Paragraphs */
193+
.comfy-markdown-content p {
194+
margin: 0 0 0.5em;
195+
}
196+
197+
.comfy-markdown-content p:last-child {
198+
margin-bottom: 0;
199+
}
200+
201+
/* First child reset */
202+
.comfy-markdown-content *:first-child {
203+
margin-top: 0; /* mt-0 */
204+
}
205+
206+
/* Lists */
207+
.comfy-markdown-content ul,
208+
.comfy-markdown-content ol {
209+
padding-left: 2rem; /* pl-8 */
210+
margin: 0.5rem 0; /* my-2 */
211+
}
212+
213+
/* Nested lists */
214+
.comfy-markdown-content ul ul,
215+
.comfy-markdown-content ol ol,
216+
.comfy-markdown-content ul ol,
217+
.comfy-markdown-content ol ul {
218+
padding-left: 1.5rem; /* pl-6 */
219+
margin: 0.5rem 0; /* my-2 */
220+
}
221+
222+
.comfy-markdown-content li {
223+
margin: 0.5rem 0; /* my-2 */
224+
}
225+
226+
/* Code */
227+
.comfy-markdown-content code {
228+
color: var(--code-text-color);
229+
background-color: var(--code-bg-color);
230+
border-radius: 0.25rem; /* rounded */
231+
padding: 0.125rem 0.375rem; /* px-1.5 py-0.5 */
232+
font-family: monospace;
233+
}
234+
235+
.comfy-markdown-content pre {
236+
background-color: var(--code-block-bg-color);
237+
border-radius: 0.25rem; /* rounded */
238+
padding: 1rem; /* p-4 */
239+
margin: 1rem 0; /* my-4 */
240+
overflow-x: auto; /* overflow-x-auto */
241+
}
242+
243+
.comfy-markdown-content pre code {
244+
background-color: transparent; /* bg-transparent */
245+
padding: 0; /* p-0 */
246+
color: var(--p-text-color);
247+
}
248+
249+
/* Tables */
250+
.comfy-markdown-content table {
251+
width: 100%; /* w-full */
252+
border-collapse: collapse; /* border-collapse */
253+
}
254+
255+
.comfy-markdown-content th,
256+
.comfy-markdown-content td {
257+
padding: 0.5rem; /* px-2 py-2 */
258+
}
259+
260+
.comfy-markdown-content th {
261+
color: var(--fg-color);
262+
}
263+
264+
.comfy-markdown-content td {
265+
color: var(--drag-text);
266+
}
267+
268+
.comfy-markdown-content tr {
269+
border-bottom: 1px solid var(--content-bg);
270+
}
271+
272+
.comfy-markdown-content tr:last-child {
273+
border-bottom: none;
274+
}
275+
276+
.comfy-markdown-content thead {
277+
border-bottom: 1px solid var(--p-text-color);
278+
}
279+
280+
/* Links */
281+
.comfy-markdown-content a {
282+
color: var(--drag-text);
283+
text-decoration: underline;
284+
}
285+
286+
/* Media */
287+
.comfy-markdown-content img,
288+
.comfy-markdown-content video {
289+
max-width: 100%; /* max-w-full */
290+
height: auto; /* h-auto */
291+
display: block; /* block */
292+
margin-bottom: 1rem; /* mb-4 */
293+
}
294+
295+
/* Blockquotes */
296+
.comfy-markdown-content blockquote {
297+
border-left: 3px solid var(--p-primary-color, var(--primary-bg));
298+
padding-left: 0.75em;
299+
margin: 0.5em 0;
300+
opacity: 0.8;
301+
}
302+
303+
/* Horizontal rule */
304+
.comfy-markdown-content hr {
305+
border: none;
306+
border-top: 1px solid var(--p-border-color, var(--border-color));
307+
margin: 1em 0;
308+
}
309+
310+
/* Strong and emphasis */
311+
.comfy-markdown-content strong {
312+
font-weight: bold;
313+
}
314+
315+
.comfy-markdown-content em {
316+
font-style: italic;
317+
}
318+
137319
.comfy-modal {
138320
display: none; /* Hidden by default */
139321
position: fixed; /* Stay in place */
Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
<template>
2+
<div
3+
class="widget-markdown relative w-full cursor-text"
4+
@click="startEditing"
5+
>
6+
<!-- Display mode: Rendered markdown -->
7+
<div
8+
v-if="!isEditing"
9+
class="comfy-markdown-content text-xs min-h-[60px] rounded-lg px-4 py-2 overflow-y-auto"
10+
v-html="renderedHtml"
11+
/>
12+
13+
<!-- Edit mode: Textarea -->
14+
<Textarea
15+
v-else
16+
ref="textareaRef"
17+
v-model="localValue"
18+
:disabled="readonly"
19+
class="w-full text-xs"
20+
size="small"
21+
rows="6"
22+
:pt="{
23+
root: {
24+
onBlur: handleBlur
25+
}
26+
}"
27+
@update:model-value="onChange"
28+
@click.stop
29+
@keydown.stop
30+
/>
31+
</div>
32+
</template>
33+
34+
<script setup lang="ts">
35+
import Textarea from 'primevue/textarea'
36+
import { computed, nextTick, ref } from 'vue'
37+
38+
import { useStringWidgetValue } from '@/composables/graph/useWidgetValue'
39+
import type { SimplifiedWidget } from '@/types/simplifiedWidget'
40+
import { renderMarkdownToHtml } from '@/utils/markdownRendererUtil'
41+
42+
const props = defineProps<{
43+
widget: SimplifiedWidget<string>
44+
modelValue: string
45+
readonly?: boolean
46+
}>()
47+
48+
const emit = defineEmits<{
49+
'update:modelValue': [value: string]
50+
}>()
51+
52+
// State
53+
const isEditing = ref(false)
54+
const textareaRef = ref<InstanceType<typeof Textarea> | undefined>()
55+
56+
// Use the composable for consistent widget value handling
57+
const { localValue, onChange } = useStringWidgetValue(
58+
props.widget,
59+
props.modelValue,
60+
emit
61+
)
62+
63+
// Computed
64+
const renderedHtml = computed(() => {
65+
return renderMarkdownToHtml(localValue.value || '')
66+
})
67+
68+
// Methods
69+
const startEditing = async () => {
70+
if (props.readonly || isEditing.value) return
71+
72+
isEditing.value = true
73+
await nextTick()
74+
75+
// Focus the textarea
76+
// @ts-expect-error - $el is an internal property of the Textarea component
77+
textareaRef.value?.$el?.focus()
78+
}
79+
80+
const handleBlur = () => {
81+
isEditing.value = false
82+
}
83+
</script>
84+
85+
<style scoped>
86+
.widget-markdown {
87+
background-color: var(--p-muted-color);
88+
border: 1px solid var(--p-border-color);
89+
border-radius: var(--p-border-radius);
90+
}
91+
92+
.widget-markdown:hover:not(:has(textarea)) {
93+
background-color: var(--p-content-hover-background);
94+
}
95+
</style>

src/components/graph/vueWidgets/widgetRegistry.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import WidgetGalleria from './WidgetGalleria.vue'
1212
import WidgetImage from './WidgetImage.vue'
1313
import WidgetImageCompare from './WidgetImageCompare.vue'
1414
import WidgetInputText from './WidgetInputText.vue'
15+
import WidgetMarkdown from './WidgetMarkdown.vue'
1516
import WidgetMultiSelect from './WidgetMultiSelect.vue'
1617
import WidgetSelect from './WidgetSelect.vue'
1718
import WidgetSelectButton from './WidgetSelectButton.vue'
@@ -42,7 +43,8 @@ export enum WidgetType {
4243
IMAGECOMPARE = 'IMAGECOMPARE',
4344
GALLERIA = 'GALLERIA',
4445
FILEUPLOAD = 'FILEUPLOAD',
45-
TREESELECT = 'TREESELECT'
46+
TREESELECT = 'TREESELECT',
47+
MARKDOWN = 'MARKDOWN'
4648
}
4749

4850
/**
@@ -69,7 +71,8 @@ export const widgetTypeToComponent: Record<string, Component> = {
6971
[WidgetType.IMAGECOMPARE]: WidgetImageCompare,
7072
[WidgetType.GALLERIA]: WidgetGalleria,
7173
[WidgetType.FILEUPLOAD]: WidgetFileUpload,
72-
[WidgetType.TREESELECT]: WidgetTreeSelect
74+
[WidgetType.TREESELECT]: WidgetTreeSelect,
75+
[WidgetType.MARKDOWN]: WidgetMarkdown
7376
}
7477

7578
/**

src/composables/graph/useWidgetRenderer.ts

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,8 @@ export const useWidgetRenderer = () => {
3737
// Multiline text
3838
multiline: WidgetType.TEXTAREA,
3939
textarea: WidgetType.TEXTAREA,
40+
customtext: WidgetType.TEXTAREA,
41+
MARKDOWN: WidgetType.MARKDOWN,
4042

4143
// Advanced widgets
4244
color: WidgetType.COLOR,
@@ -48,11 +50,7 @@ export const useWidgetRenderer = () => {
4850

4951
// Button widget
5052
button: WidgetType.BUTTON,
51-
BUTTON: WidgetType.BUTTON,
52-
53-
// Text-based widgets that don't have dedicated components yet
54-
MARKDOWN: WidgetType.TEXTAREA, // Markdown should use textarea for now
55-
customtext: WidgetType.TEXTAREA // Custom text widgets use textarea for multiline
53+
BUTTON: WidgetType.BUTTON
5654
}
5755

5856
// Get mapped enum key

0 commit comments

Comments
 (0)