Merge branch 'feat/monaco-editor-schema-draft' into 'main'

feat(workflow): Monaco editor для proposed schema (C)

See merge request 2-6/2-6-4/terravault/ordinis!132
This commit is contained in:
Александр Зимин
2026-05-12 11:41:12 +00:00
5 changed files with 149 additions and 8 deletions
+1
View File
@@ -12,6 +12,7 @@
},
"dependencies": {
"@hookform/resolvers": "^3.9.1",
"@monaco-editor/react": "^4.7.0",
"@phosphor-icons/react": "^2.1.10",
"@radix-ui/react-checkbox": "^1.3.3",
"@radix-ui/react-dialog": "^1.1.15",
+57
View File
@@ -11,6 +11,9 @@ importers:
'@hookform/resolvers':
specifier: ^3.9.1
version: 3.10.0(react-hook-form@7.75.0(react@19.2.5))
'@monaco-editor/react':
specifier: ^4.7.0
version: 4.7.0(monaco-editor@0.55.1)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
'@phosphor-icons/react':
specifier: ^2.1.10
version: 2.1.10(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
@@ -720,6 +723,16 @@ packages:
'@keyv/serialize@1.1.1':
resolution: {integrity: sha512-dXn3FZhPv0US+7dtJsIi2R+c7qWYiReoEh5zUntWCf4oSpMNib8FDhSoed6m3QyZdx5hK7iLFkYk3rNxwt8vTA==}
'@monaco-editor/loader@1.7.0':
resolution: {integrity: sha512-gIwR1HrJrrx+vfyOhYmCZ0/JcWqG5kbfG7+d3f/C1LXk2EvzAbHSg3MQ5lO2sMlo9izoAZ04shohfKLVT6crVA==}
'@monaco-editor/react@4.7.0':
resolution: {integrity: sha512-cyzXQCtO47ydzxpQtCGSQGOC8Gk3ZUeBXFAxD+CWXYFo5OqZyZUonFl0DwUlTyAfRHntBfw2p3w4s9R6oe1eCA==}
peerDependencies:
monaco-editor: '>= 0.25.0 < 1'
react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
'@nodelib/fs.scandir@2.1.5':
resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==}
engines: {node: '>= 8'}
@@ -1697,6 +1710,9 @@ packages:
'@types/topojson-specification@1.0.5':
resolution: {integrity: sha512-C7KvcQh+C2nr6Y2Ub4YfgvWvWCgP2nOQMtfhlnwsRL4pYmmwzBS7HclGiS87eQfDOU/DLQpX6GEscviaz4yLIQ==}
'@types/trusted-types@2.0.7':
resolution: {integrity: sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==}
'@types/webxr@0.5.24':
resolution: {integrity: sha512-h8fgEd/DpoS9CBrjEQXR+dIDraopAEfu4wYVNY2tEPwk60stPWhvZMf4Foo5FakuQ7HFZoa8WceaWFervK2Ovg==}
@@ -2158,6 +2174,9 @@ packages:
dom-accessibility-api@0.6.3:
resolution: {integrity: sha512-7ZgogeTnjuHbo+ct10G9Ffp0mif17idi0IyWNVA/wcwcm7NPOD/WEHVP3n7n3MhXqxoIYm8d6MuZohYWIZ4T3w==}
dompurify@3.2.7:
resolution: {integrity: sha512-WhL/YuveyGXJaerVlMYGWhvQswa7myDG17P7Vu65EWC05o8vfeNbvNf4d/BOvH99+ZW+LlQsc1GDKMa1vNK6dw==}
dot-prop@5.3.0:
resolution: {integrity: sha512-QM8q3zDe58hqUqjraQOmzZ1LIH9SWQJTlEKCH4kJ2oQvLZk7RbQXvtDM2XEq3fwkV9CCvvH4LA0AV+ogFsBM2Q==}
engines: {node: '>=8'}
@@ -2817,6 +2836,11 @@ packages:
peerDependencies:
marked: '>=1 <16'
marked@14.0.0:
resolution: {integrity: sha512-uIj4+faQ+MgHgwUW1l2PsPglZLOLOT1uErt06dAPtx2kjteLAkbsd/0FiYg/MGS+i7ZKLb7w2WClxHkzOOuryQ==}
engines: {node: '>= 18'}
hasBin: true
marked@15.0.12:
resolution: {integrity: sha512-8dD6FusOQSrpv9Z1rdNMdlSgQOIP880DHqnohobOmYLElGEqAL/JvxvuxZO16r4HtjTlfPRDC1hbvxC9dPN2nA==}
engines: {node: '>= 18'}
@@ -2879,6 +2903,9 @@ packages:
minimist@1.2.8:
resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==}
monaco-editor@0.55.1:
resolution: {integrity: sha512-jz4x+TJNFHwHtwuV9vA9rMujcZRb0CEilTEwG2rRSpe/A7Jdkuj8xPKttCgOh+v/lkHy7HsZ64oj+q3xoAFl9A==}
ms@2.1.3:
resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==}
@@ -3482,6 +3509,9 @@ packages:
stackback@0.0.2:
resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==}
state-local@1.0.7:
resolution: {integrity: sha512-HTEHMNieakEnoe33shBYcZ7NX83ACUjCu8c40iOGEZsngj9zRnkqS9j1pqQPXwobB0ZcVTk27REb7COQ0UR59w==}
std-env@3.10.0:
resolution: {integrity: sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==}
@@ -4312,6 +4342,17 @@ snapshots:
'@keyv/serialize@1.1.1': {}
'@monaco-editor/loader@1.7.0':
dependencies:
state-local: 1.0.7
'@monaco-editor/react@4.7.0(monaco-editor@0.55.1)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)':
dependencies:
'@monaco-editor/loader': 1.7.0
monaco-editor: 0.55.1
react: 19.2.5
react-dom: 19.2.5(react@19.2.5)
'@nodelib/fs.scandir@2.1.5':
dependencies:
'@nodelib/fs.stat': 2.0.5
@@ -5285,6 +5326,9 @@ snapshots:
dependencies:
'@types/geojson': 7946.0.16
'@types/trusted-types@2.0.7':
optional: true
'@types/webxr@0.5.24': {}
'@vitejs/plugin-react@4.7.0(vite@6.4.2(jiti@2.6.1)(lightningcss@1.32.0))':
@@ -5746,6 +5790,10 @@ snapshots:
dom-accessibility-api@0.6.3: {}
dompurify@3.2.7:
optionalDependencies:
'@types/trusted-types': 2.0.7
dot-prop@5.3.0:
dependencies:
is-obj: 2.0.0
@@ -6407,6 +6455,8 @@ snapshots:
node-emoji: 2.2.0
supports-hyperlinks: 3.2.0
marked@14.0.0: {}
marked@15.0.12: {}
math-intrinsics@1.1.0: {}
@@ -6444,6 +6494,11 @@ snapshots:
minimist@1.2.8: {}
monaco-editor@0.55.1:
dependencies:
dompurify: 3.2.7
marked: 14.0.0
ms@2.1.3: {}
mz@2.7.0:
@@ -6957,6 +7012,8 @@ snapshots:
stackback@0.0.2: {}
state-local@1.0.7: {}
std-env@3.10.0: {}
stream-combiner2@1.1.1:
@@ -11,6 +11,7 @@ import {
import { ArrowsClockwiseIcon, CheckSquareIcon } from '@phosphor-icons/react'
import type { DictionaryDetail, SchemaDraft } from '@/api/client'
import { useCreateSchemaDraft, useUpdateSchemaDraft } from '@/api/mutations'
import { JsonEditor } from '@/components/workflow/JsonEditor'
type Props = {
open: boolean
@@ -247,18 +248,18 @@ export function CreateSchemaDraftModal({ open, onClose, detail, editDraft, onCre
</Button>
</div>
</div>
{/* Custom monospace textarea — для full Monaco editor'а Phase 1d.
Высота min-h-[400px] чтобы средняя schema ~30-50 полей помещалась
без скролла. */}
<textarea
{/* Monaco editor — lazy-loaded (~1.2MB gzip). Syntax highlight,
bracket matching, fold/unfold по структуре JSON. Высота 420px —
средняя schema (~30-50 полей) помещается без вертикального
скролла. */}
<JsonEditor
value={json}
onChange={(e) => {
setJson(e.target.value)
onChange={(v) => {
setJson(v)
if (parseError) setParseError(null)
}}
spellCheck={false}
disabled={pending}
className="w-full min-h-[400px] max-h-[60vh] text-mono text-cell rounded-md border border-line bg-[#0c0a23] text-[#f0eaff] p-3 focus:outline-none focus:ring-1 focus:ring-accent"
height={420}
/>
{parseError && (
<Alert variant="error" className="mt-2">
@@ -0,0 +1,73 @@
import { lazy, Suspense } from 'react'
import { useTranslation } from 'react-i18next'
import { LoadingBlock } from '@/ui'
const MonacoEditor = lazy(() => import('@monaco-editor/react').then((m) => ({ default: m.Editor })))
type Props = {
value: string
onChange: (v: string) => void
disabled?: boolean
/** Pixel height; default 400. Используется minHeight CSS editor сам
* растягивается до этого значения. */
height?: number | string
}
/**
* Lazy-loaded Monaco editor для JSON. Bundle ~1.2MB gzip lazy через
* dynamic import. Suspense fallback показывает loading block.
*
* <p>Theme подставляется fixed 'vs-dark' чтобы match'илось с нашим
* space-theme background (#0c0a23). Monaco поддерживает custom themes
* через monaco.editor.defineTheme это можно добавить отдельно если
* захочется precise color match.
*
* <p>Options:
* <ul>
* <li>JSON language с syntax highlighting + bracket matching.</li>
* <li>Auto-format on paste/type, format on save (через
* monaco.languages.json.jsonDefaults).</li>
* <li>Minimap скрыт narrow modal viewport, minimap занимает место.</li>
* <li>Read-only режим через disabled prop.</li>
* </ul>
*/
export function JsonEditor({ value, onChange, disabled = false, height = 400 }: Props) {
const { t } = useTranslation()
return (
<Suspense fallback={<LoadingBlock size="md" label={t('loading')} />}>
<div
className="rounded-md border border-line overflow-hidden bg-[#0c0a23]"
style={{ height: typeof height === 'number' ? `${height}px` : height }}
>
<MonacoEditor
height="100%"
defaultLanguage="json"
language="json"
value={value}
onChange={(v) => onChange(v ?? '')}
theme="vs-dark"
options={{
readOnly: disabled,
minimap: { enabled: false },
fontSize: 13,
fontFamily:
'ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, monospace',
lineNumbers: 'on',
renderLineHighlight: 'line',
scrollBeyondLastLine: false,
wordWrap: 'on',
tabSize: 2,
insertSpaces: true,
formatOnPaste: true,
formatOnType: true,
bracketPairColorization: { enabled: true },
// Monaco autoclosing для JSON: brackets/quotes на open auto-close,
// помогает писать большие schema'ы без потери indentation.
autoClosingBrackets: 'always',
autoClosingQuotes: 'always',
}}
/>
</div>
</Suspense>
)
}
+9
View File
@@ -129,6 +129,15 @@ export default defineConfig({
// Axios + парсеры
if (id.match(/node_modules\/(axios|jose|crypto-js)\//)) return 'vendor-net'
// Monaco editor — ~1.2MB gzip. Lazy через React.lazy в JsonEditor.tsx;
// separate chunk даёт on-demand загрузку только когда CreateModal
// открыт. Без isolated chunk Monaco попадал бы в vendor-misc
// (главная страница). Аналог graph3d HOTFIX'а ниже — НО Monaco
// не зависит от d3 utils, поэтому circular chunk риска нет.
if (id.match(/node_modules\/(@monaco-editor|monaco-editor)\//)) {
return 'vendor-monaco'
}
// HOTFIX: graph3d isolated chunk был причиной runtime "Super
// expression must either be null or a function" (cyclic chunk
// dependency vendor-misc ↔ vendor-graph3d ломает порядок