feat: implement FieldRichTextEditor component using TipTap and Mantine integration

This commit is contained in:
Firman Ramdhani
2026-06-22 11:24:44 +07:00
parent eaec6ec38f
commit edd71243a4
10 changed files with 737 additions and 38 deletions
@@ -6,7 +6,8 @@ import {
FieldMultiSelect, FieldNativeSelect, FieldTagsInput, FieldCheckbox,
FieldRadioGroup, FieldSwitch, FieldChipGroup, FieldSegmentedControl,
FieldSlider, FieldRangeSlider, FieldRating, FieldColorInput,
FieldColorPicker, FieldFileInput, FieldLocalSelect, FieldAsyncSelect
FieldColorPicker, FieldFileInput, FieldLocalSelect, FieldAsyncSelect,
FieldRichTextEditor
} from '@repo/ui/form';
import type { LoadOptionsFn } from '@repo/ui/form';
import { useFormDemoTranslation } from '../i18n/useFormDemoTranslation';
@@ -91,6 +92,8 @@ export default function AllFieldsDemo() {
{ id: 888, code: 'ASYNC-88', name: 'Ghost Async Vendor 1' },
{ id: 999, code: 'ASYNC-99', name: 'Ghost Async Vendor 2' }
],
richTextEmpty: "",
richTextPrefilled: "<h2 style=\"text-align: center\">ERP Release Notes</h2><p>This is a <b>highly important</b> update. Please observe the following:</p><ul><li>System maintenance at <i>midnight</i>.</li><li><u style=\"text-align: justify\">All users must log out.</u></li></ul><p style=\"text-align: justify\">Thank you for your cooperation.</p>",
realPokeSelect: null,
multiRealPokeSelect: []
}
@@ -305,6 +308,23 @@ export default function AllFieldsDemo() {
clearable
/>
</Group>
<Title order={5} mb="sm" mt="lg" c="brand">Rich Text Editor (TipTap)</Title>
<Divider mb="md" />
<FieldRichTextEditor
name="richTextEmpty"
control={control}
label="Rich Text (Empty)"
description="A fresh TipTap editor instance"
/>
<div style={{ marginTop: '16px' }}>
<FieldRichTextEditor
name="richTextPrefilled"
control={control}
label="Rich Text (Prefilled / Edit Mode)"
description="HTML string successfully loaded from default values"
/>
</div>
</div>
{/* --- Toggles & Choices --- */}
@@ -1,8 +1,8 @@
import { useForm, useWatch } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import { z } from 'zod';
import { Button, Paper, Title, Divider, Stack, Code, Alert } from '@repo/ui/components';
import { FieldTextInput, FieldSelect, FieldSwitch, FieldLocalSelect, FieldAsyncSelect } from '@repo/ui/form';
import { Button, Paper, Title, Divider, Stack, Code, Alert, TypographyStylesProvider } from '@repo/ui/components';
import { FieldTextInput, FieldSelect, FieldSwitch, FieldLocalSelect, FieldAsyncSelect, FieldRichTextEditor } from '@repo/ui/form';
import { useConditionalField } from '@repo/ui/hooks';
import { compose, required, emailValidator } from '@repo/ui/validators';
import { useFormDemoTranslation } from '../i18n/useFormDemoTranslation';
@@ -67,6 +67,7 @@ export default function ReactiveWatchDemo() {
role: z.string().optional(),
regions: z.array(z.object({ id: z.string(), code: z.string(), taxRate: z.number() })).optional(),
warehouses: z.array(z.object({ id: z.string(), regionId: z.string(), name: z.string() })).optional(),
richTextLive: z.string().optional(),
})
.and(
z.discriminatedUnion('userType', [
@@ -109,6 +110,7 @@ export default function ReactiveWatchDemo() {
{ id: 'W-99', regionId: 'R1', name: 'APAC Central Hub' },
{ id: 'W-98', regionId: 'R1', name: 'APAC Backup Hub' }
],
richTextLive: "<p>Start typing to see the live preview...</p>",
},
});
@@ -119,6 +121,7 @@ export default function ReactiveWatchDemo() {
const department = useWatch({ control, name: 'department' });
const role = useWatch({ control, name: 'role' });
const regions = useWatch({ control, name: 'regions' });
const watchedRichTextLive = useWatch({ control, name: 'richTextLive' });
// Use the custom hook to cleanly unregister and reset fields when hidden
useConditionalField({
@@ -328,6 +331,25 @@ export default function ReactiveWatchDemo() {
</Alert>
)}
<Title order={5} mb="sm" c="brand" mt="lg">
Reactive Rich Text Preview
</Title>
<Divider mb="sm" />
<FieldRichTextEditor
name="richTextLive"
control={control as any}
label="Live Editor"
description="Type to see instantaneous reactive rendering below"
/>
<Paper p="md" withBorder radius="md" mt="sm">
<Title order={6} mb="xs">Live HTML Preview Render</Title>
<TypographyStylesProvider>
<div dangerouslySetInnerHTML={{ __html: watchedRichTextLive }} />
</TypographyStylesProvider>
</Paper>
<Button type="submit" mt="md">
{t.common?.submit || 'Submit Reactive Form'}
</Button>
@@ -4,7 +4,7 @@ import { z } from 'zod';
import { Button, Paper, Title, Group, Stack, Code, Divider } from '@repo/ui/components';
import {
FieldTextInput, FieldPasswordInput, FieldNumberInput,
FieldLocalSelect, FieldAsyncSelect
FieldLocalSelect, FieldAsyncSelect, FieldRichTextEditor
} from '@repo/ui/form';
import type { LoadOptionsFn } from '@repo/ui/form';
@@ -75,6 +75,7 @@ export default function ValidationBankDemo() {
prefilledVendor: z.object({ id: z.union([z.string(), z.number()]), code: z.string(), name: z.string() }, { required_error: 'Vendor is required' }),
emptyVendor: z.object({ id: z.union([z.string(), z.number()]), code: z.string(), name: z.string() }, { required_error: 'Vendor is required' }),
prefilledAsyncMulti: z.array(z.object({ id: z.union([z.string(), z.number()]), code: z.string(), name: z.string() })).min(1, "Select at least 1 vendor"),
richTextNotes: z.string().min(15, "Notes must be at least 15 characters long (including HTML tags)"),
});
type ValidationFormValues = z.infer<typeof validationSchema>;
@@ -96,6 +97,7 @@ export default function ValidationBankDemo() {
{ id: 'V1', code: 'VN-01', name: 'Vendor One' },
{ id: 'V2', code: 'VN-02', name: 'Vendor Two' }
] as any,
richTextNotes: '',
}
});
@@ -226,6 +228,17 @@ export default function ValidationBankDemo() {
withAsterisk
/>
<Title order={5} c="brand" mt="lg">Rich Text Editor Validations</Title>
<Divider mb="sm" />
<FieldRichTextEditor
name="richTextNotes"
control={control as any}
label="Important Notes"
description="This uses Zod minimum length string validation"
withAsterisk
/>
<Button type="submit" mt="md">{t.common.submit}</Button>
</Stack>
</form>