Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 22 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,28 @@ versioning follows [SemVer](https://semver.org/).

<!-- Generated by `scripts/build-changelog.mjs`; edit `changelog/X.Y.Z.md`. -->

## [2.1.0](https://github.com/FicSysFR/jsonforms_builder/compare/2.0.2...2.1.0) - 2026-10-19

### ✨ Features

- WYSIWYG renderer: tables, task lists, alignment, colors, highlights, link panel, find & replace,
source mode, preview, an opt-in text bubble and an opt-in slash palette (`0de8d92`).
- Optional AI writing assistant over a host AI SDK endpoint (UI message stream, text stream or
JSON); results stay in a detached panel until accepted, requests are abortable and the feature
is inert unless `ai.enabled` is `true` (`0de8d92`).
- `options.contentType: 'markdown'` stores the WYSIWYG value as a Markdown string through TipTap's
Markdown extension; source mode edits the raw Markdown and the builder inspector offers the
Markdown format (`string` schema).

### 🐛 Fixes

- The `@nuxt/ui` peer dependency now requires `^4.10`: the rating control imports `UInputRating`,
which first shipped in Nuxt UI 4.10.0, so hosts on 4.8 or 4.9 failed to build.

### 📝 Documentation

- WYSIWYG & AI guide, Markdown storage options and playground example (FR/EN).

## [2.0.2](https://github.com/FicSysFR/jsonforms_builder/compare/2.0.1...2.0.2) - 2026-09-08

### ⚠️ Breaking Changes
Expand Down
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -74,14 +74,14 @@ Palette, drag-and-drop reorderable tree, property inspector, live preview, and J
| `string` | `UInput` |
| `string` + `options.multi` | `UTextarea` |
| `string` + `format: password` | `UInput` + visibility toggle |
| `object` / `string` + `options.wysiwyg` | `UEditor` (JSON or HTML via `contentType`) |
| `object` / `string` + `options.wysiwyg` | `UEditor` (JSON, HTML or Markdown via `contentType`) |
| WYSIWYG images | upload dropzone, drag-resize, bubble replace/delete |

WYSIWYG options (`options.wysiwyg: true`):

| Option | Role |
|---|---|
| `contentType` | `'json'` \| `'html'` (else inferred from schema) |
| `contentType` | `'json'` \| `'html'` \| `'markdown'` (else inferred from schema) |
| `toolbar` | Nuxt UI toolbar groups, or `false` to hide |
| `density` | `'compact'` \| `'comfortable'` \| `'prose'` |
| `minHeight` / `padding` / `blockSpacing` / `editorClass` | spacing overrides |
Expand Down
27 changes: 27 additions & 0 deletions changelog/2.1.0.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
---
version: 2.1.0
date: 2026-10-19
title: Rich WYSIWYG, AI assistant and Markdown content
previous: 2.0.2
prerelease: false
---

### ✨ Features

- WYSIWYG renderer: tables, task lists, alignment, colors, highlights, link panel, find & replace,
source mode, preview, an opt-in text bubble and an opt-in slash palette (`0de8d92`).
- Optional AI writing assistant over a host AI SDK endpoint (UI message stream, text stream or
JSON); results stay in a detached panel until accepted, requests are abortable and the feature
is inert unless `ai.enabled` is `true` (`0de8d92`).
- `options.contentType: 'markdown'` stores the WYSIWYG value as a Markdown string through TipTap's
Markdown extension; source mode edits the raw Markdown and the builder inspector offers the
Markdown format (`string` schema).

### 🐛 Fixes

- The `@nuxt/ui` peer dependency now requires `^4.10`: the rating control imports `UInputRating`,
which first shipped in Nuxt UI 4.10.0, so hosts on 4.8 or 4.9 failed to build.

### 📝 Documentation

- WYSIWYG & AI guide, Markdown storage options and playground example (FR/EN).
18 changes: 17 additions & 1 deletion docs/en/guide/options/string.md
Original file line number Diff line number Diff line change
Expand Up @@ -286,7 +286,7 @@ Requires `allRenderers` (not just `nuxtUiRenderers`).
| Name | Type | Default | Description |
|---|---|---|---|
| `wysiwyg` | `Boolean` | — | **Required** (`true`) to enable the editor. |
| `contentType` | `"json"` \| `"html"` | inferred | Stored format: ProseMirror object (`json`) or HTML string (`html`). |
| `contentType` | `"json"` \| `"html"` \| `"markdown"` | inferred | Stored format: ProseMirror object (`json`), HTML string (`html`) or Markdown string (`markdown`, `string` schema). |
| `toolbar` | `Array` \| `false` | built-in toolbar | Replaces the toolbar (`EditorToolbarItem[][]`) or hides it. |
| `placeholder` | `String` | — | Editor placeholder. |
| `features` | `false` \| `Object` | all on | Tables, task lists, alignment, colors, highlight, link, find & replace, source, preview. |
Expand Down Expand Up @@ -320,6 +320,22 @@ Editing features, the AI assistant and an AI SDK backend are detailed in
}
```

```json
{
"type": "Control",
"scope": "#/properties/article",
"options": {
"wysiwyg": true,
"contentType": "markdown",
"density": "prose"
}
}
```

With `markdown`, the value is serialized by TipTap's Markdown extension (loaded by `UEditor`):
headings, lists, links, code, tables and images (`![alt](src)`) round-trip; the width of a resized
image has no Markdown equivalent and is not kept. Source mode then edits the raw Markdown.

## Playground

- [String & Textarea](/en/playground#/?section=docs&example=nuxt-string)
Expand Down
19 changes: 18 additions & 1 deletion docs/guide/options/string.md
Original file line number Diff line number Diff line change
Expand Up @@ -286,7 +286,7 @@ Nécessite `allRenderers` (pas seulement `nuxtUiRenderers`).
| Name | Type | Default | Description |
|---|---|---|---|
| `wysiwyg` | `Boolean` | — | **Requis** (`true`) pour activer l’éditeur. |
| `contentType` | `"json"` \| `"html"` | inféré | Format stocké : objet ProseMirror (`json`) ou chaîne HTML (`html`). |
| `contentType` | `"json"` \| `"html"` \| `"markdown"` | inféré | Format stocké : objet ProseMirror (`json`), chaîne HTML (`html`) ou chaîne Markdown (`markdown`, schema `string`). |
| `toolbar` | `Array` \| `false` | barre intégrée | Remplace la barre d’outils (`EditorToolbarItem[][]`) ou la masque. |
| `placeholder` | `String` | — | Placeholder de l’éditeur. |
| `features` | `false` \| `Object` | tout activé | Tableaux, listes de tâches, alignement, couleurs, surlignage, lien, rechercher/remplacer, source, aperçu. |
Expand Down Expand Up @@ -320,6 +320,23 @@ Détail des fonctions d’édition, de l’assistant IA et d’un backend AI SDK
}
```

```json
{
"type": "Control",
"scope": "#/properties/article",
"options": {
"wysiwyg": true,
"contentType": "markdown",
"density": "prose"
}
}
```

En `markdown`, la valeur est sérialisée par l'extension Markdown de TipTap (chargée par `UEditor`) :
titres, listes, liens, code, tableaux et images (`![alt](src)`) font l'aller-retour ; la largeur
d'une image redimensionnée n'a pas d'équivalent Markdown et n'est pas conservée. Le mode source
édite alors le Markdown brut.

## Playground

- [String & Textarea](/playground#/?section=docs&example=nuxt-string)
Expand Down
6 changes: 4 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,9 @@
"@iconify-json/lucide": "^1.2.111",
"@jsonforms/core": "^3.6.0",
"@jsonforms/vue": "^3.6.0",
"@nuxt/ui": "^4.8.1",
"@nuxt/ui": "^4.10.0",
"@tiptap/extension-image": "^3.29.2",
"@tiptap/markdown": "^3.29.2",
"@types/express": "^5.0.6",
"@types/multer": "^2.0.0",
"@types/node": "^22.10.0",
Expand Down Expand Up @@ -118,7 +120,7 @@
"peerDependencies": {
"@jsonforms/core": "^3.6",
"@jsonforms/vue": "^3.6",
"@nuxt/ui": "^4.8",
"@nuxt/ui": "^4.10",
"typescript": "^5",
"vue": "^3.5"
},
Expand Down
23 changes: 23 additions & 0 deletions playground/examples/items/nuxt-wysiwyg.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,14 @@ export const data = {
],
},
notesHtml: '<p>Notes HTML libres…</p>',
article: [
'## Procédure',
'',
'1. Ouvrir le portail',
'2. Cliquer sur **Nouvelle demande**',
'',
'> Astuce : joignez une capture.',
].join('\n'),
}

export const schema = {
Expand All @@ -26,6 +34,11 @@ export const schema = {
title: 'Notes (HTML)',
description: 'contentType: "html" stocke une chaîne HTML.',
},
article: {
type: 'string',
title: 'Article (Markdown)',
description: 'contentType: "markdown" stocke une chaîne Markdown.',
},
},
}

Expand All @@ -52,6 +65,16 @@ export const uischema = {
showUnfocusedDescription: true,
},
},
{
type: 'Control',
scope: '#/properties/article',
options: {
wysiwyg: true,
contentType: 'markdown',
density: 'prose',
showUnfocusedDescription: true,
},
},
],
}

Expand Down
2 changes: 1 addition & 1 deletion project-context.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ Termes canoniques du métier, un par entrée, affûtés par le skill

## Développement

- Runtime et versions : Node.js 22 ou supérieur ; TypeScript 5 ; Vue 3.5 ; JSON Forms 3.6 ; Nuxt UI 4.8.
- Runtime et versions : Node.js 22 ou supérieur ; TypeScript 5 ; Vue 3.5 ; JSON Forms 3.6 ; Nuxt UI 4.10.
- Package manager : Yarn Classic 1.22.22, avec un lockfile à la racine et un lockfile séparé dans `mcp/`.
- Commande d'installation : `yarn install --frozen-lockfile` ; pour le paquet MCP, `yarn --cwd mcp install --frozen-lockfile`.
- Commande de développement : `yarn start:dev` pour le playground sur le port 5174 ; `yarn docs:dev` pour VitePress sur le port 5173.
Expand Down
2 changes: 1 addition & 1 deletion scripts/package.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ export const PACKAGE_SPECS = [
/(?:^|\/)\.env(?:\.|$)/,
],
maximumPackedSize: 750_000,
maximumUnpackedSize: 3_000_000,
maximumUnpackedSize: 3_500_000,
},
{
directory: join(root, 'mcp'),
Expand Down
17 changes: 12 additions & 5 deletions src/advanced/wysiwyg/WysiwygSourceEditor.vue
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
autoresize
:maxrows="30"
:readonly="!editable"
:aria-label="contentType === 'json' ? 'Source JSON' : 'Source HTML'"
:aria-label="sourceLabel"
:ui="{ base: 'font-mono text-xs' }"
)
u-alert(v-if="error" color="error" variant="subtle" icon="i-lucide-circle-alert" :description="error")
Expand All @@ -16,29 +16,34 @@
</template>

<script lang="ts">
import { defineComponent, ref, type PropType } from 'vue'
import { computed, defineComponent, ref, type PropType } from 'vue'
import type { Editor } from '@tiptap/vue-3'
import UAlert from '@nuxt/ui/components/Alert.vue'
import UButton from '@nuxt/ui/components/Button.vue'
import UTextarea from '@nuxt/ui/components/Textarea.vue'
import type { WysiwygContentType } from '../wysiwygOptions'
import { parseWysiwygSource, serializeWysiwygSource } from './source'

/**
* Raw HTML / JSON editing. Applied content goes through the Tiptap schema, which
* Raw HTML / JSON / Markdown editing. Applied content goes through the Tiptap schema, which
* drops any element or attribute the editor does not model (scripts included).
*/
export default defineComponent({
name: 'WysiwygSourceEditor',
components: { UAlert, UButton, UTextarea },
props: {
editor: { type: Object as PropType<Editor>, required: true },
contentType: { type: String as PropType<'html' | 'json'>, default: 'html' },
contentType: { type: String as PropType<WysiwygContentType>, default: 'html' },
editable: { type: Boolean, default: true },
},
emits: ['close'],
setup(props, { emit }) {
const draft = ref(serializeWysiwygSource(props.editor, props.contentType))
const error = ref<string | null>(null)
const sourceLabel = computed(
() =>
`Source ${props.contentType === 'json' ? 'JSON' : props.contentType === 'markdown' ? 'Markdown' : 'HTML'}`,
)

const apply = () => {
const parsed = parseWysiwygSource(draft.value, props.contentType)
Expand All @@ -49,6 +54,8 @@ export default defineComponent({
try {
props.editor.commands.setContent(parsed.content, {
emitUpdate: true,
// Markdown is parsed by the editor's Markdown extension (loaded by UEditor).
...(props.contentType === 'markdown' ? { contentType: 'markdown' as const } : {}),
// HTML is filtered by the schema like pasted content; unknown JSON nodes
// would otherwise silently wipe the document, so they are rejected.
errorOnInvalidContent: props.contentType === 'json',
Expand All @@ -61,7 +68,7 @@ export default defineComponent({
emit('close')
}

return { draft, error, apply }
return { draft, error, sourceLabel, apply }
},
})
</script>
14 changes: 9 additions & 5 deletions src/advanced/wysiwyg/source.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,22 @@
import type { Editor, JSONContent } from '@tiptap/core'
import type { WysiwygContentType } from '../wysiwygOptions'

export type WysiwygSourceParseResult =
| { ok: true; content: string | JSONContent }
| { ok: false; error: string }

export const serializeWysiwygSource = (editor: Editor, contentType: 'html' | 'json'): string =>
contentType === 'json' ? JSON.stringify(editor.getJSON(), null, 2) : editor.getHTML()
export const serializeWysiwygSource = (editor: Editor, contentType: WysiwygContentType): string => {
if (contentType === 'json') return JSON.stringify(editor.getJSON(), null, 2)
if (contentType === 'markdown') return editor.getMarkdown()
return editor.getHTML()
}

/** Validates source-mode input; JSON must be a Tiptap `doc` node. */
/** Validates source-mode input; JSON must be a Tiptap `doc` node, HTML and Markdown pass through. */
export const parseWysiwygSource = (
source: string,
contentType: 'html' | 'json',
contentType: WysiwygContentType,
): WysiwygSourceParseResult => {
if (contentType === 'html') return { ok: true, content: source }
if (contentType !== 'json') return { ok: true, content: source }
let parsed: unknown
try {
parsed = JSON.parse(source)
Expand Down
7 changes: 5 additions & 2 deletions src/advanced/wysiwygOptions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,10 @@ import {
} from './wysiwyg/features'
import { buildWysiwygToolbar } from './wysiwyg/toolbar'

export type WysiwygContentType = 'html' | 'json'
/** Stored value format: TipTap JSON (object), HTML or Markdown (strings). */
export type WysiwygContentType = 'html' | 'json' | 'markdown'

const CONTENT_TYPES: readonly WysiwygContentType[] = ['html', 'json', 'markdown']

export type WysiwygDensity = 'compact' | 'comfortable' | 'prose'

Expand Down Expand Up @@ -163,7 +166,7 @@ export const resolveWysiwygOptions = (

const explicitContent = opts.contentType
const contentType: WysiwygContentType =
explicitContent === 'html' || explicitContent === 'json'
explicitContent && CONTENT_TYPES.includes(explicitContent)
? explicitContent
: schemaType === 'string'
? 'html'
Expand Down
7 changes: 4 additions & 3 deletions src/builder/BuilderInspector.vue
Original file line number Diff line number Diff line change
Expand Up @@ -476,7 +476,7 @@
p.text-xs.font-semibold.uppercase.tracking-wide.text-dimmed Options — texte riche
u-form-field(label="Format stocké" help="options.contentType — adapte aussi le type du schema")
u-select(
:model-value="options.contentType === 'html' ? 'html' : 'json'"
:model-value="options.contentType === 'html' || options.contentType === 'markdown' ? options.contentType : 'json'"
:items="wysiwygContentItems"
value-key="value"
class="w-full"
Expand Down Expand Up @@ -699,6 +699,7 @@ export default defineComponent({
const wysiwygContentItems = [
{ label: 'JSON (ProseMirror)', value: 'json' },
{ label: 'HTML', value: 'html' },
{ label: 'Markdown', value: 'markdown' },
]

const asNumber = (value: unknown): number | undefined =>
Expand Down Expand Up @@ -799,10 +800,10 @@ export default defineComponent({

/** contentType drives both the option and the schema `type` (object vs string). */
const patchWysiwygContentType = (value: unknown) => {
const contentType = value === 'html' ? 'html' : 'json'
const contentType = value === 'html' || value === 'markdown' ? value : 'json'
emit('update:control', {
element: { options: { ...options.value, contentType } },
property: { type: contentType === 'html' ? 'string' : 'object' },
property: { type: contentType === 'json' ? 'object' : 'string' },
})
}

Expand Down
Loading
Loading