@multiplatform.one/frappe-ui 6.3.0 → 6.4.1
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/dist/cjs/FrappeDashboard.cjs +12 -2
- package/dist/cjs/FrappeDashboard.native.js +12 -2
- package/dist/cjs/FrappeDashboard.native.js.map +1 -1
- package/dist/cjs/FrappeLikedBy.cjs +2 -1
- package/dist/cjs/FrappeLikedBy.native.js +3 -1
- package/dist/cjs/FrappeLikedBy.native.js.map +1 -1
- package/dist/cjs/FrappeListView.cjs +10 -1
- package/dist/cjs/FrappeListView.native.js +10 -1
- package/dist/cjs/FrappeListView.native.js.map +1 -1
- package/dist/cjs/fieldRegistry.cjs +1 -1
- package/dist/cjs/fieldRegistry.native.js +1 -1
- package/dist/cjs/fieldRegistry.native.js.map +1 -1
- package/dist/cjs/fields/MarkdownEditor/index.cjs +33 -108
- package/dist/cjs/fields/MarkdownEditor/index.native.js +3 -3
- package/dist/cjs/fields/MarkdownEditor/index.native.js.map +1 -1
- package/dist/esm/FrappeDashboard.mjs +13 -3
- package/dist/esm/FrappeDashboard.mjs.map +1 -1
- package/dist/esm/FrappeDashboard.native.js +13 -3
- package/dist/esm/FrappeDashboard.native.js.map +1 -1
- package/dist/esm/FrappeLikedBy.mjs +3 -2
- package/dist/esm/FrappeLikedBy.mjs.map +1 -1
- package/dist/esm/FrappeLikedBy.native.js +4 -2
- package/dist/esm/FrappeLikedBy.native.js.map +1 -1
- package/dist/esm/FrappeListView.mjs +11 -2
- package/dist/esm/FrappeListView.mjs.map +1 -1
- package/dist/esm/FrappeListView.native.js +11 -2
- package/dist/esm/FrappeListView.native.js.map +1 -1
- package/dist/esm/fieldRegistry.mjs +1 -1
- package/dist/esm/fieldRegistry.mjs.map +1 -1
- package/dist/esm/fieldRegistry.native.js +1 -1
- package/dist/esm/fieldRegistry.native.js.map +1 -1
- package/dist/esm/fields/MarkdownEditor/index.mjs +36 -100
- package/dist/esm/fields/MarkdownEditor/index.mjs.map +1 -1
- package/dist/esm/fields/MarkdownEditor/index.native.js.map +1 -1
- package/dist/jsx/FrappeDashboard.mjs +13 -3
- package/dist/jsx/FrappeDashboard.mjs.map +1 -1
- package/dist/jsx/FrappeDashboard.native.js +12 -2
- package/dist/jsx/FrappeDashboard.native.js.map +1 -1
- package/dist/jsx/FrappeLikedBy.mjs +3 -2
- package/dist/jsx/FrappeLikedBy.mjs.map +1 -1
- package/dist/jsx/FrappeLikedBy.native.js +3 -1
- package/dist/jsx/FrappeLikedBy.native.js.map +1 -1
- package/dist/jsx/FrappeListView.mjs +11 -2
- package/dist/jsx/FrappeListView.mjs.map +1 -1
- package/dist/jsx/FrappeListView.native.js +10 -1
- package/dist/jsx/FrappeListView.native.js.map +1 -1
- package/dist/jsx/fieldRegistry.mjs +1 -1
- package/dist/jsx/fieldRegistry.mjs.map +1 -1
- package/dist/jsx/fieldRegistry.native.js +1 -1
- package/dist/jsx/fieldRegistry.native.js.map +1 -1
- package/dist/jsx/fields/MarkdownEditor/index.mjs +36 -100
- package/dist/jsx/fields/MarkdownEditor/index.mjs.map +1 -1
- package/dist/jsx/fields/MarkdownEditor/index.native.js +3 -3
- package/dist/jsx/fields/MarkdownEditor/index.native.js.map +1 -1
- package/package.json +22 -15
- package/src/FrappeDashboard.stories.tsx +3 -1
- package/src/FrappeDashboard.tsx +21 -1
- package/src/FrappeLikedBy.tsx +13 -2
- package/src/FrappeListView.tsx +21 -1
- package/src/FrappeTable.stories.tsx +91 -12
- package/src/fieldRegistry.ts +1 -1
- package/src/fields/Date/index.tsx +47 -3
- package/src/fields/MarkdownEditor/MarkdownEditor.spec.tsx +56 -32
- package/src/fields/MarkdownEditor/MarkdownEditor.stories.tsx +5 -5
- package/src/fields/MarkdownEditor/index.native.tsx +292 -0
- package/src/fields/MarkdownEditor/index.tsx +64 -139
- package/types/FrappeDashboard.d.ts.map +1 -1
- package/types/FrappeLikedBy.d.ts.map +1 -1
- package/types/FrappeListView.d.ts.map +1 -1
- package/types/fieldRegistry.d.ts.map +1 -1
- package/types/fields/MarkdownEditor/index.d.ts +11 -5
- package/types/fields/MarkdownEditor/index.d.ts.map +1 -1
- package/types/fields/MarkdownEditor/index.native.d.ts +47 -0
- package/types/fields/MarkdownEditor/index.native.d.ts.map +1 -0
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* MarkdownEditorField contract specs:
|
|
3
|
-
*
|
|
4
|
-
*
|
|
2
|
+
* MarkdownEditorField contract specs: rich editor in markdown capabilities
|
|
3
|
+
* (LC-52 MARKDOWN-EXPRESSIBLE-EDITING — no highlight/underline/color
|
|
4
|
+
* controls), canonical onChange emitting markdown (LC-14), readOnly/disabled
|
|
5
|
+
* gating, table-cell chromeless branch, skeleton branch, and registry
|
|
6
|
+
* resolution for the "Markdown Editor" fieldtype.
|
|
5
7
|
*/
|
|
6
8
|
import { TableCellContext } from "@multiplatform.one/forms";
|
|
7
9
|
import { renderWithProviders } from "@multiplatform.one/test-utils";
|
|
8
|
-
import { act, cleanup, fireEvent } from "@testing-library/react";
|
|
10
|
+
import { act, cleanup, fireEvent, waitFor } from "@testing-library/react";
|
|
9
11
|
import type React from "react";
|
|
10
12
|
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
11
13
|
import { getFieldComponent, type FieldRenderProps } from "../../fieldRegistry";
|
|
@@ -13,63 +15,85 @@ import { MarkdownEditorField } from ".";
|
|
|
13
15
|
|
|
14
16
|
afterEach(cleanup);
|
|
15
17
|
|
|
18
|
+
const getProseMirror = (container: HTMLElement) =>
|
|
19
|
+
container.querySelector<HTMLElement>('[contenteditable="true"]');
|
|
20
|
+
|
|
16
21
|
describe("MarkdownEditorField (standalone)", () => {
|
|
17
|
-
it("
|
|
18
|
-
const onChange = vi.fn();
|
|
22
|
+
it("renders the rich text editor (WYSIWYG), not a source textarea", async () => {
|
|
19
23
|
const result = renderWithProviders(
|
|
20
|
-
<MarkdownEditorField label="Notes" value="" onChange={
|
|
24
|
+
<MarkdownEditorField label="Notes" value="# Hello" onChange={() => {}} />,
|
|
21
25
|
);
|
|
22
|
-
|
|
23
|
-
expect(
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
});
|
|
27
|
-
expect(onChange).toHaveBeenCalledWith("# Hello");
|
|
26
|
+
await waitFor(() => expect(getProseMirror(result.container)).toBeTruthy());
|
|
27
|
+
expect(result.getTextArea()).toBeNull();
|
|
28
|
+
// markdown value parsed into rich nodes
|
|
29
|
+
expect(result.container.querySelector("h1")?.textContent).toBe("Hello");
|
|
28
30
|
});
|
|
29
31
|
|
|
30
|
-
it("
|
|
32
|
+
it("markdown capabilities: no underline/highlight/color toolbar controls (LC-52)", async () => {
|
|
33
|
+
const result = renderWithProviders(
|
|
34
|
+
<MarkdownEditorField label="Notes" value="hello" onChange={() => {}} />,
|
|
35
|
+
);
|
|
36
|
+
await waitFor(() => expect(result.queryByLabelText("Bold")).toBeTruthy());
|
|
37
|
+
expect(result.queryByLabelText("Underline")).toBeNull();
|
|
38
|
+
expect(result.queryByLabelText("Text Color")).toBeNull();
|
|
39
|
+
expect(result.queryByLabelText("Highlight Color")).toBeNull();
|
|
40
|
+
// markdown-expressible controls stay
|
|
41
|
+
expect(result.queryByLabelText("Italic")).toBeTruthy();
|
|
42
|
+
expect(result.queryByLabelText("Blockquote")).toBeTruthy();
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it("emits markdown (not HTML) through canonical onChange", async () => {
|
|
31
46
|
const onChange = vi.fn();
|
|
32
47
|
const result = renderWithProviders(
|
|
33
|
-
<
|
|
34
|
-
<MarkdownEditorField label="Notes" value="original" readOnly onChange={onChange} />
|
|
35
|
-
</TableCellContext.Provider>,
|
|
48
|
+
<MarkdownEditorField label="Notes" value="hello" onChange={onChange} />,
|
|
36
49
|
);
|
|
37
|
-
|
|
38
|
-
expect(textarea).toBeTruthy();
|
|
50
|
+
await waitFor(() => expect(result.queryByLabelText("Blockquote")).toBeTruthy());
|
|
39
51
|
await act(async () => {
|
|
40
|
-
|
|
52
|
+
fireEvent.click(result.getByLabelText("Blockquote"));
|
|
41
53
|
});
|
|
42
|
-
expect(onChange).
|
|
54
|
+
await waitFor(() => expect(onChange).toHaveBeenCalled());
|
|
55
|
+
const emitted = onChange.mock.calls.at(-1)?.[0] as string;
|
|
56
|
+
expect(emitted).toContain("> hello");
|
|
57
|
+
expect(emitted).not.toContain("<blockquote>");
|
|
43
58
|
});
|
|
44
59
|
|
|
45
|
-
it("
|
|
60
|
+
it("readOnly renders a non-editable surface without emitting changes", async () => {
|
|
46
61
|
const onChange = vi.fn();
|
|
47
62
|
const result = renderWithProviders(
|
|
48
|
-
<
|
|
49
|
-
<MarkdownEditorField label="Notes" value="original" disabled onChange={onChange} />
|
|
50
|
-
</TableCellContext.Provider>,
|
|
63
|
+
<MarkdownEditorField label="Notes" value="original" readOnly onChange={onChange} />,
|
|
51
64
|
);
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
65
|
+
await waitFor(() =>
|
|
66
|
+
expect(result.container.querySelector('[contenteditable="false"]')).toBeTruthy(),
|
|
67
|
+
);
|
|
68
|
+
expect(getProseMirror(result.container)).toBeNull();
|
|
56
69
|
expect(onChange).not.toHaveBeenCalled();
|
|
57
70
|
});
|
|
58
71
|
|
|
59
|
-
it("renders
|
|
72
|
+
it("disabled renders a non-editable surface", async () => {
|
|
73
|
+
const result = renderWithProviders(
|
|
74
|
+
<MarkdownEditorField label="Notes" value="original" disabled onChange={() => {}} />,
|
|
75
|
+
);
|
|
76
|
+
await waitFor(() =>
|
|
77
|
+
expect(result.container.querySelector('[contenteditable="false"]')).toBeTruthy(),
|
|
78
|
+
);
|
|
79
|
+
expect(getProseMirror(result.container)).toBeNull();
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it("renders chromeless (no fixed formatting toolbar) in table cells", async () => {
|
|
60
83
|
const result = renderWithProviders(
|
|
61
84
|
<TableCellContext.Provider value={{ inTableCell: true, editable: true }}>
|
|
62
85
|
<MarkdownEditorField label="Notes" value="# cell" onChange={() => {}} />
|
|
63
86
|
</TableCellContext.Provider>,
|
|
64
87
|
);
|
|
65
|
-
expect(result.
|
|
66
|
-
expect(result.
|
|
88
|
+
await waitFor(() => expect(getProseMirror(result.container)).toBeTruthy());
|
|
89
|
+
expect(result.queryByLabelText("Formatting toolbar")).toBeNull();
|
|
67
90
|
});
|
|
68
91
|
|
|
69
92
|
it("skeleton branch renders no editable surface", () => {
|
|
70
93
|
const result = renderWithProviders(
|
|
71
94
|
<MarkdownEditorField label="Notes" skeleton value="# hidden" onChange={() => {}} />,
|
|
72
95
|
);
|
|
96
|
+
expect(getProseMirror(result.container)).toBeNull();
|
|
73
97
|
expect(result.getTextArea()).toBeNull();
|
|
74
98
|
});
|
|
75
99
|
});
|
|
@@ -9,14 +9,14 @@ const FIXTURE = `# Release notes
|
|
|
9
9
|
|
|
10
10
|
Dedicated **Markdown Editor** field for the Frappe \`Markdown Editor\` fieldtype.
|
|
11
11
|
|
|
12
|
-
-
|
|
13
|
-
-
|
|
12
|
+
- WYSIWYG editing via the canonical rich text editor
|
|
13
|
+
- Constrained to what ~~HTML~~ *markdown* can express (LC-52)
|
|
14
14
|
- Full field contract: label / hint / error / readOnly / skeleton
|
|
15
15
|
|
|
16
|
-
>
|
|
16
|
+
> Serializes to and from a GFM markdown string.
|
|
17
17
|
|
|
18
18
|
\`\`\`python
|
|
19
|
-
print("fenced code
|
|
19
|
+
print("fenced code works too")
|
|
20
20
|
\`\`\`
|
|
21
21
|
`;
|
|
22
22
|
|
|
@@ -28,7 +28,7 @@ export default {
|
|
|
28
28
|
docs: {
|
|
29
29
|
description: {
|
|
30
30
|
component:
|
|
31
|
-
"Frappe `Markdown Editor` fieldtype: markdown
|
|
31
|
+
"Frappe `Markdown Editor` fieldtype: the canonical rich text editor in markdown capabilities (LC-52) — WYSIWYG constrained to GFM-expressible features (no highlight/underline/color/alignment), serializing to and from a markdown string. Native falls back to markdown source editing.",
|
|
32
32
|
},
|
|
33
33
|
},
|
|
34
34
|
},
|
|
@@ -0,0 +1,292 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MarkdownEditorField (NATIVE fallback) — markdown source pane with a live
|
|
3
|
+
* rendered preview (house `@multiplatform.one/markdown` renderer).
|
|
4
|
+
*
|
|
5
|
+
* The web tier renders the canonical rich text editor constrained to
|
|
6
|
+
* markdown capabilities (LC-52 MARKDOWN-EXPRESSIBLE-EDITING). On native the
|
|
7
|
+
* 10Tap WebView cannot express that constraint honestly tonight (extension
|
|
8
|
+
* gating + markdown serialization live in the web bundle), so this tier
|
|
9
|
+
* keeps plain markdown source editing — documented fallback, not a parallel
|
|
10
|
+
* WYSIWYG implementation.
|
|
11
|
+
*
|
|
12
|
+
* Full field contract: three-branch render (skeleton → standalone →
|
|
13
|
+
* form-integrated), canonical `onChange(value)` (LC-14), FieldLayout slot
|
|
14
|
+
* order, chromeless inside table cells (useIsInTableCell).
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import {
|
|
18
|
+
Field,
|
|
19
|
+
FieldLayout,
|
|
20
|
+
Skeleton,
|
|
21
|
+
TextArea,
|
|
22
|
+
formCommonColors,
|
|
23
|
+
getFieldError,
|
|
24
|
+
mergeFieldHandler,
|
|
25
|
+
useFormField,
|
|
26
|
+
useIsInTableCell,
|
|
27
|
+
useResolvedValidators,
|
|
28
|
+
type AnyFormApi,
|
|
29
|
+
} from "@multiplatform.one/forms";
|
|
30
|
+
import { useEffect, useState, type ComponentType, type ReactNode } from "react";
|
|
31
|
+
import { useTranslation } from "react-i18next";
|
|
32
|
+
import type { FontSizeTokens, LabelProps, SizeTokens } from "tamagui";
|
|
33
|
+
import { Paragraph, YStack } from "tamagui";
|
|
34
|
+
import { useResolvedKnobs } from "@multiplatform.one/theme";
|
|
35
|
+
import { EditorPreviewPanes, PreviewSurface } from "../shared/EditorPreviewPanes";
|
|
36
|
+
|
|
37
|
+
// ---------------------------------------------------------------------------
|
|
38
|
+
// Preview (lazy house Markdown renderer)
|
|
39
|
+
// ---------------------------------------------------------------------------
|
|
40
|
+
|
|
41
|
+
type MarkdownComponent = ComponentType<{ children?: string }>;
|
|
42
|
+
|
|
43
|
+
// Module-level cache so the renderer loads once per session, not per field.
|
|
44
|
+
let markdownRendererCache: MarkdownComponent | null = null;
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* The house Markdown renderer loads on demand the first time a preview pane
|
|
48
|
+
* renders (same optional-dependency pattern as the components shiki
|
|
49
|
+
* Highlighter): `@multiplatform.one/markdown` reaches consumers transitively
|
|
50
|
+
* via `@multiplatform.one/rich-text`, and a failed import degrades to a
|
|
51
|
+
* plain-text preview instead of crashing the field.
|
|
52
|
+
*/
|
|
53
|
+
function useMarkdownRenderer(enabled: boolean): MarkdownComponent | null {
|
|
54
|
+
const [renderer, setRenderer] = useState<MarkdownComponent | null>(markdownRendererCache);
|
|
55
|
+
|
|
56
|
+
useEffect(() => {
|
|
57
|
+
if (!enabled || renderer) return;
|
|
58
|
+
let cancelled = false;
|
|
59
|
+
import("@multiplatform.one/markdown")
|
|
60
|
+
.then((mod) => {
|
|
61
|
+
markdownRendererCache = mod.Markdown as MarkdownComponent;
|
|
62
|
+
if (!cancelled) setRenderer(() => markdownRendererCache);
|
|
63
|
+
})
|
|
64
|
+
.catch(() => {
|
|
65
|
+
// Renderer unavailable — the plain-text fallback below stays up.
|
|
66
|
+
});
|
|
67
|
+
return () => {
|
|
68
|
+
cancelled = true;
|
|
69
|
+
};
|
|
70
|
+
}, [enabled, renderer]);
|
|
71
|
+
|
|
72
|
+
return renderer;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
interface MarkdownPreviewProps {
|
|
76
|
+
value: string;
|
|
77
|
+
compact?: boolean;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function MarkdownPreview({ value, compact }: MarkdownPreviewProps) {
|
|
81
|
+
const { t } = useTranslation();
|
|
82
|
+
const { knobProps } = useResolvedKnobs({ compact });
|
|
83
|
+
const Renderer = useMarkdownRenderer(!!value);
|
|
84
|
+
// Content type scale follows the size knob (same idiom as CodeBlock).
|
|
85
|
+
const textSize = knobProps.sizeToken as FontSizeTokens;
|
|
86
|
+
|
|
87
|
+
return (
|
|
88
|
+
<PreviewSurface compact={compact}>
|
|
89
|
+
{!value ? (
|
|
90
|
+
<Paragraph color={formCommonColors.muted} size={textSize}>
|
|
91
|
+
{t("Nothing to preview")}
|
|
92
|
+
</Paragraph>
|
|
93
|
+
) : Renderer ? (
|
|
94
|
+
<Renderer>{value}</Renderer>
|
|
95
|
+
) : (
|
|
96
|
+
// Loading / renderer unavailable: honest plain-text fallback.
|
|
97
|
+
<Paragraph size={textSize} whiteSpace="pre-wrap">
|
|
98
|
+
{value}
|
|
99
|
+
</Paragraph>
|
|
100
|
+
)}
|
|
101
|
+
</PreviewSurface>
|
|
102
|
+
);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// ---------------------------------------------------------------------------
|
|
106
|
+
// MarkdownEditorField
|
|
107
|
+
// ---------------------------------------------------------------------------
|
|
108
|
+
|
|
109
|
+
export interface MarkdownEditorFieldProps {
|
|
110
|
+
name?: string;
|
|
111
|
+
form?: AnyFormApi;
|
|
112
|
+
validators?: Record<string, unknown>;
|
|
113
|
+
label?: ReactNode;
|
|
114
|
+
labelProps?: Omit<LabelProps, "htmlFor" | "ref">;
|
|
115
|
+
helperText?: string;
|
|
116
|
+
error?: string | boolean;
|
|
117
|
+
required?: boolean;
|
|
118
|
+
disabled?: boolean;
|
|
119
|
+
readOnly?: boolean;
|
|
120
|
+
skeleton?: boolean;
|
|
121
|
+
compact?: boolean;
|
|
122
|
+
size?: SizeTokens;
|
|
123
|
+
id?: string;
|
|
124
|
+
value?: string;
|
|
125
|
+
defaultValue?: string;
|
|
126
|
+
/** Canonical change contract (LC-14): emits the markdown source string. */
|
|
127
|
+
onChange?: (value: string) => void;
|
|
128
|
+
onBlur?: (...args: any[]) => void;
|
|
129
|
+
placeholder?: string;
|
|
130
|
+
/** Minimum visible source rows (default 6). */
|
|
131
|
+
minRows?: number;
|
|
132
|
+
/** Maximum source rows before the textarea scrolls (default 16). */
|
|
133
|
+
maxRows?: number;
|
|
134
|
+
"aria-label"?: string;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
export function MarkdownEditorField({
|
|
138
|
+
name,
|
|
139
|
+
form: formProp,
|
|
140
|
+
validators,
|
|
141
|
+
label,
|
|
142
|
+
labelProps,
|
|
143
|
+
helperText,
|
|
144
|
+
error,
|
|
145
|
+
required,
|
|
146
|
+
disabled,
|
|
147
|
+
readOnly,
|
|
148
|
+
skeleton,
|
|
149
|
+
compact,
|
|
150
|
+
size,
|
|
151
|
+
id: idProp,
|
|
152
|
+
value,
|
|
153
|
+
defaultValue,
|
|
154
|
+
onChange,
|
|
155
|
+
onBlur,
|
|
156
|
+
placeholder,
|
|
157
|
+
minRows = 6,
|
|
158
|
+
maxRows = 16,
|
|
159
|
+
"aria-label": ariaLabel,
|
|
160
|
+
}: MarkdownEditorFieldProps) {
|
|
161
|
+
const { t } = useTranslation();
|
|
162
|
+
const { resolvedForm, knobProps, id } = useFormField({ form: formProp, id: idProp, compact });
|
|
163
|
+
const resolvedValidators = useResolvedValidators(required, validators, label, name);
|
|
164
|
+
const inTableCell = useIsInTableCell();
|
|
165
|
+
|
|
166
|
+
// Standalone/uncontrolled mirror so the live preview always has the current
|
|
167
|
+
// source (the form branch reads field state instead).
|
|
168
|
+
const [internalValue, setInternalValue] = useState(value ?? defaultValue ?? "");
|
|
169
|
+
useEffect(() => {
|
|
170
|
+
if (value !== undefined) setInternalValue(value);
|
|
171
|
+
}, [value]);
|
|
172
|
+
|
|
173
|
+
const accessibleName =
|
|
174
|
+
ariaLabel ?? (typeof label === "string" ? label : undefined) ?? placeholder;
|
|
175
|
+
|
|
176
|
+
if (skeleton) {
|
|
177
|
+
return (
|
|
178
|
+
<YStack {...knobProps.gap} width="100%">
|
|
179
|
+
{label && <Skeleton variant="text" width="30%" height={14} />}
|
|
180
|
+
<Skeleton variant="rounded" width="100%" height={compact ? 120 : 200} />
|
|
181
|
+
</YStack>
|
|
182
|
+
);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
const renderSource = (
|
|
186
|
+
textValue: string,
|
|
187
|
+
handleChange?: (text: string) => void,
|
|
188
|
+
hasError?: boolean,
|
|
189
|
+
) => (
|
|
190
|
+
<TextArea
|
|
191
|
+
id={id}
|
|
192
|
+
value={textValue}
|
|
193
|
+
onChangeText={(text) => {
|
|
194
|
+
if (disabled || readOnly) return;
|
|
195
|
+
handleChange?.(text);
|
|
196
|
+
onChange?.(text);
|
|
197
|
+
}}
|
|
198
|
+
placeholder={placeholder}
|
|
199
|
+
disabled={disabled}
|
|
200
|
+
readOnly={readOnly}
|
|
201
|
+
required={required}
|
|
202
|
+
error={hasError ? true : undefined}
|
|
203
|
+
compact={compact}
|
|
204
|
+
size={size}
|
|
205
|
+
minRows={minRows}
|
|
206
|
+
maxRows={maxRows}
|
|
207
|
+
textAreaProps={{
|
|
208
|
+
fontFamily: "$mono",
|
|
209
|
+
autoCapitalize: "none",
|
|
210
|
+
autoCorrect: false,
|
|
211
|
+
spellCheck: false,
|
|
212
|
+
...(accessibleName ? { "aria-label": accessibleName } : {}),
|
|
213
|
+
}}
|
|
214
|
+
/>
|
|
215
|
+
);
|
|
216
|
+
|
|
217
|
+
// Table cells get the chromeless source pane only (no tabs/preview chrome);
|
|
218
|
+
// InputParts inside the forms TextArea detects the cell context itself.
|
|
219
|
+
const renderControl = (
|
|
220
|
+
textValue: string,
|
|
221
|
+
handleChange?: (text: string) => void,
|
|
222
|
+
hasError?: boolean,
|
|
223
|
+
) => {
|
|
224
|
+
if (inTableCell) return renderSource(textValue, handleChange, hasError);
|
|
225
|
+
return (
|
|
226
|
+
<EditorPreviewPanes
|
|
227
|
+
editor={renderSource(textValue, handleChange, hasError)}
|
|
228
|
+
preview={<MarkdownPreview value={textValue} compact={compact} />}
|
|
229
|
+
editTabLabel={t("Write")}
|
|
230
|
+
previewTabLabel={t("Preview")}
|
|
231
|
+
compact={compact}
|
|
232
|
+
defaultPreview={!!readOnly}
|
|
233
|
+
/>
|
|
234
|
+
);
|
|
235
|
+
};
|
|
236
|
+
|
|
237
|
+
if (!resolvedForm || !name) {
|
|
238
|
+
if (inTableCell) return renderControl(internalValue, setInternalValue, !!error);
|
|
239
|
+
return (
|
|
240
|
+
<FieldLayout
|
|
241
|
+
id={id}
|
|
242
|
+
label={label}
|
|
243
|
+
labelProps={labelProps}
|
|
244
|
+
error={error}
|
|
245
|
+
helperText={helperText}
|
|
246
|
+
required={required}
|
|
247
|
+
disabled={disabled}
|
|
248
|
+
size={size}
|
|
249
|
+
knobProps={knobProps}
|
|
250
|
+
onBlur={onBlur}
|
|
251
|
+
aria-label={ariaLabel}
|
|
252
|
+
placeholder={placeholder}
|
|
253
|
+
>
|
|
254
|
+
{renderControl(internalValue, setInternalValue, !!error)}
|
|
255
|
+
</FieldLayout>
|
|
256
|
+
);
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
return (
|
|
260
|
+
<Field
|
|
261
|
+
form={resolvedForm}
|
|
262
|
+
name={name}
|
|
263
|
+
defaultValue={defaultValue}
|
|
264
|
+
validators={resolvedValidators}
|
|
265
|
+
>
|
|
266
|
+
{(field) => {
|
|
267
|
+
const resolvedError = getFieldError(field, error);
|
|
268
|
+
const fieldValue = typeof field.state.value === "string" ? field.state.value : "";
|
|
269
|
+
const handleChange = (text: string) => field.handleChange(text as any);
|
|
270
|
+
if (inTableCell) return renderControl(fieldValue, handleChange, !!resolvedError);
|
|
271
|
+
return (
|
|
272
|
+
<FieldLayout
|
|
273
|
+
id={id}
|
|
274
|
+
label={label}
|
|
275
|
+
labelProps={labelProps}
|
|
276
|
+
error={resolvedError}
|
|
277
|
+
helperText={helperText}
|
|
278
|
+
required={required}
|
|
279
|
+
disabled={disabled}
|
|
280
|
+
size={size}
|
|
281
|
+
knobProps={knobProps}
|
|
282
|
+
onBlur={mergeFieldHandler(field, "handleBlur", onBlur)}
|
|
283
|
+
aria-label={ariaLabel}
|
|
284
|
+
placeholder={placeholder}
|
|
285
|
+
>
|
|
286
|
+
{renderControl(fieldValue, handleChange, !!resolvedError)}
|
|
287
|
+
</FieldLayout>
|
|
288
|
+
);
|
|
289
|
+
}}
|
|
290
|
+
</Field>
|
|
291
|
+
);
|
|
292
|
+
}
|