feat(ui): add CodeBlock and CodeEditor components with Monaco and hljs

- add CodeBlock for read-only syntax-highlighted display (hljs, js/json)
- add CodeEditor wrapping Monaco editor with theme sync, focus state, and error state
- replace code textareas in snippet, credential, and step pages with CodeEditor
- replace code <pre> blocks in snippet and credential detail pages with CodeBlock
- make form cards full-width on pages with code editors
- add resize:vertical support to CodeEditor wrapper with automaticLayout
This commit is contained in:
2026-04-10 15:53:26 +03:00
parent 2046e64428
commit 1c13e068ad
21 changed files with 391 additions and 76 deletions
+2
View File
@@ -14,6 +14,8 @@
"format": "prettier --write \"src/**/*.{ts,tsx}\" \".storybook/**/*.{ts,tsx}\""
},
"dependencies": {
"@monaco-editor/react": "^4.7.0",
"highlight.js": "^11.11.1",
"i18next": "^26.0.4",
"lucide-react": "^1.7.0",
"moment": "^2.30.1",
+12
View File
@@ -0,0 +1,12 @@
import hljs from 'highlight.js/lib/core';
import javascript from 'highlight.js/lib/languages/javascript';
import json from 'highlight.js/lib/languages/json';
hljs.registerLanguage('javascript', javascript);
hljs.registerLanguage('json', json);
export default hljs;
export function escapeHtml(str: string): string {
return str.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
}
+4
View File
@@ -196,6 +196,10 @@
max-width: 540px;
}
.formCardFull {
max-width: none;
}
.formFields {
display: flex;
flex-direction: column;
@@ -2,7 +2,7 @@ import { useState } from 'react';
import { useNavigate } from 'react-router-dom';
import { useTranslation } from 'react-i18next';
import { credentials } from '../../api';
import { Breadcrumbs, Button, Card, Input, Textarea } from '../../ui';
import { Breadcrumbs, Button, Card, Input, CodeEditor } from '../../ui';
import styles from '../Page.module.css';
export function CreateCredentialPage() {
@@ -58,7 +58,7 @@ export function CreateCredentialPage() {
{error && <p className={styles.error}>{error}</p>}
<form onSubmit={handleSubmit} noValidate>
<Card className={styles.formCard}>
<Card className={styles.formCardFull}>
<div className={styles.formFields}>
<Input
label={t('credentials.form_name')}
@@ -72,17 +72,23 @@ export function CreateCredentialPage() {
required
autoFocus
/>
<Textarea
label={t('credentials.form_data')}
placeholder={t('credentials.form_data_placeholder')}
value={data}
onChange={(e) => {
setData(e.target.value);
setDataError('');
}}
error={dataError || undefined}
rows={6}
/>
<div className={styles.formField}>
<label className={styles.fieldLabel}>
{t('credentials.form_data')}
<span className={styles.requiredMark}> *</span>
</label>
<CodeEditor
language="json"
value={data}
onChange={(v) => {
setData(v);
setDataError('');
}}
rows={6}
error={!!dataError}
/>
{dataError && <span className={styles.fieldError}>{dataError}</span>}
</div>
</div>
<div className={styles.formActions}>
@@ -1,7 +1,8 @@
import { useEffect, useState } from 'react';
import { useNavigate, useParams } from 'react-router-dom';
import { useTranslation } from 'react-i18next';
import { Download, Pencil, Trash2 } from 'lucide-react';
import { Upload, Pencil, Trash2 } from 'lucide-react';
import { CodeBlock } from '../../ui';
import { stringify as yamlStringify } from 'yaml';
import { credentials } from '../../api';
import type { Credential } from '../../api';
@@ -55,7 +56,7 @@ export function CredentialDetailPage() {
{credential && (
<div className={styles.toolbarActions}>
<Button variant="secondary" size="sm" onClick={handleExport}>
<Download size={14} />
<Upload size={14} />
{t('credentials.action_export')}
</Button>
<Button
@@ -116,15 +117,16 @@ export function CredentialDetailPage() {
<>
<h2 className={styles.sectionHeading}>{t('credentials.section_data')}</h2>
<Card>
<pre className={styles.codeBlock}>
{(() => {
<CodeBlock
language="json"
code={(() => {
try {
return JSON.stringify(JSON.parse(credential.data), null, 2);
} catch {
return credential.data;
}
})()}
</pre>
/>
</Card>
</>
)}
@@ -1,7 +1,7 @@
import { useEffect, useRef, useState } from 'react';
import { useNavigate } from 'react-router-dom';
import { useTranslation } from 'react-i18next';
import { Settings, Pencil, Trash2, Plus, Upload } from 'lucide-react';
import { Settings, Pencil, Trash2, Plus, Download } from 'lucide-react';
import { parse as yamlParse } from 'yaml';
import { credentials } from '../../api';
import type { Credential } from '../../api';
@@ -141,7 +141,7 @@ export function CredentialsPage() {
onChange={handleImport}
/>
<Button variant="secondary" size="sm" onClick={() => fileInputRef.current?.click()}>
<Upload size={14} />
<Download size={14} />
{t('credentials.action_import')}
</Button>
</div>
@@ -3,7 +3,7 @@ import { useNavigate, useParams } from 'react-router-dom';
import { useTranslation } from 'react-i18next';
import { credentials } from '../../api';
import type { Credential } from '../../api';
import { Breadcrumbs, Button, Card, Input, Textarea } from '../../ui';
import { Breadcrumbs, Button, Card, Input, CodeEditor } from '../../ui';
import styles from '../Page.module.css';
export function EditCredentialPage() {
@@ -84,7 +84,7 @@ export function EditCredentialPage() {
{!loading && credential && (
<form onSubmit={handleSubmit} noValidate>
<Card className={styles.formCard}>
<Card className={styles.formCardFull}>
<div className={styles.formFields}>
<Input
label={t('credentials.form_name')}
@@ -98,17 +98,23 @@ export function EditCredentialPage() {
required
autoFocus
/>
<Textarea
label={t('credentials.form_data')}
placeholder={t('credentials.form_data_placeholder')}
value={data}
onChange={(e) => {
setData(e.target.value);
setDataError('');
}}
error={dataError || undefined}
rows={6}
/>
<div className={styles.formField}>
<label className={styles.fieldLabel}>
{t('credentials.form_data')}
<span className={styles.requiredMark}> *</span>
</label>
<CodeEditor
language="json"
value={data}
onChange={(v) => {
setData(v);
setDataError('');
}}
rows={6}
error={!!dataError}
/>
{dataError && <span className={styles.fieldError}>{dataError}</span>}
</div>
</div>
<div className={styles.formActions}>
+2 -2
View File
@@ -4,7 +4,7 @@ import { useTranslation } from 'react-i18next';
import { scenarios, steps } from '../../api';
import type { Scenario, StepType } from '../../api';
import type { CreateStepPayload } from '../../api/client';
import { Breadcrumbs, Button, Card, Input, Select } from '../../ui';
import { Breadcrumbs, Button, Card, Input, Select, CodeEditor } from '../../ui';
import styles from '../Page.module.css';
const STEP_TYPES: StepType[] = ['login', 'exec', 'sign'];
@@ -75,7 +75,7 @@ export function CreateStepPage() {
{error && <p className={styles.error}>{error}</p>}
<form onSubmit={handleSubmit} noValidate>
<Card className={styles.formCard}>
<Card className={styles.formCardFull}>
<div className={styles.formFields}>
<Input
label={t('steps.form_order')}
+8 -14
View File
@@ -3,7 +3,7 @@ import { useNavigate, useParams } from 'react-router-dom';
import { useTranslation } from 'react-i18next';
import { scenarios, steps } from '../../api';
import type { Scenario, ScenarioStep, StepType } from '../../api';
import { Breadcrumbs, Button, Card, Input, Select } from '../../ui';
import { Breadcrumbs, Button, Card, Input, Select, CodeEditor } from '../../ui';
import styles from '../Page.module.css';
const STEP_TYPES: StepType[] = ['login', 'exec', 'sign'];
@@ -91,7 +91,7 @@ export function EditStepPage() {
{!loading && step && (
<form onSubmit={handleSubmit} noValidate>
<Card className={styles.formCard}>
<Card className={styles.formCardFull}>
<div className={styles.formFields}>
<Input
label={t('steps.form_order')}
@@ -118,24 +118,18 @@ export function EditStepPage() {
/>
<div className={styles.formField}>
<label className={styles.fieldLabel}>{t('steps.form_exec_code')}</label>
<textarea
className={styles.textarea}
rows={6}
<CodeEditor
value={execCode}
onChange={(e) => setExecCode(e.target.value)}
placeholder={t('steps.form_exec_code_placeholder')}
spellCheck={false}
onChange={setExecCode}
rows={6}
/>
</div>
<div className={styles.formField}>
<label className={styles.fieldLabel}>{t('steps.form_validate_code')}</label>
<textarea
className={styles.textarea}
rows={6}
<CodeEditor
value={validateCode}
onChange={(e) => setValidateCode(e.target.value)}
placeholder={t('steps.form_validate_code_placeholder')}
spellCheck={false}
onChange={setValidateCode}
rows={6}
/>
</div>
</div>
@@ -1,7 +1,7 @@
import { useEffect, useState } from 'react';
import { useNavigate, useParams } from 'react-router-dom';
import { useTranslation } from 'react-i18next';
import { Play, Pencil, Plus, History, Trash2, Download } from 'lucide-react';
import { Play, Pencil, Plus, History, Trash2, Upload } from 'lucide-react';
import { stringify as yamlStringify } from 'yaml';
import { scenarios, steps, scenarioCredentials, credentials as credentialsApi } from '../../api';
import type { Scenario, ScenarioStep, ScenarioCredential, Credential } from '../../api';
@@ -227,7 +227,7 @@ export function ScenarioDetailPage() {
{t('scenarios.action_runs')}
</Button>
<Button variant="secondary" size="sm" onClick={handleExport}>
<Download size={14} />
<Upload size={14} />
{t('scenarios.action_export')}
</Button>
<Button variant="secondary" size="sm" onClick={() => navigate(`/scenarios/${id}/edit`)}>
+2 -2
View File
@@ -1,7 +1,7 @@
import { useCallback, useEffect, useRef, useState } from 'react';
import { useNavigate } from 'react-router-dom';
import { useTranslation } from 'react-i18next';
import { Play, Plus, Trash2, Upload } from 'lucide-react';
import { Play, Plus, Trash2, Download } from 'lucide-react';
import { parse as yamlParse } from 'yaml';
import { scenarios } from '../../api';
import type { Scenario } from '../../api';
@@ -99,7 +99,7 @@ export function ScenariosPage() {
onChange={handleImport}
/>
<Button variant="secondary" size="sm" onClick={() => fileInputRef.current?.click()}>
<Upload size={14} />
<Download size={14} />
{t('scenarios.action_import')}
</Button>
<Button size="sm" onClick={() => navigate('/scenarios/new')}>
@@ -2,7 +2,7 @@ import { useState } from 'react';
import { useNavigate } from 'react-router-dom';
import { useTranslation } from 'react-i18next';
import { snippets } from '../../api';
import { Breadcrumbs, Button, Card, Input, Textarea } from '../../ui';
import { Breadcrumbs, Button, Card, Input, CodeEditor } from '../../ui';
import styles from '../Page.module.css';
export function CreateSnippetPage() {
@@ -59,7 +59,7 @@ export function CreateSnippetPage() {
{error && <p className={styles.error}>{error}</p>}
<form onSubmit={handleSubmit} noValidate>
<Card className={styles.formCard}>
<Card className={styles.formCardFull}>
<div className={styles.formFields}>
<Input
label={t('snippets.form_name')}
@@ -84,16 +84,14 @@ export function CreateSnippetPage() {
{t('snippets.form_code')}
<span className={styles.requiredMark}> *</span>
</label>
<textarea
className={[styles.textarea, codeError ? styles.textareaError : ''].filter(Boolean).join(' ')}
rows={14}
<CodeEditor
value={code}
onChange={(e) => {
setCode(e.target.value);
onChange={(v) => {
setCode(v);
setCodeError('');
}}
placeholder={t('snippets.form_code_placeholder')}
spellCheck={false}
rows={14}
error={!!codeError}
/>
{codeError && <span className={styles.fieldError}>{codeError}</span>}
</div>
+7 -9
View File
@@ -3,7 +3,7 @@ import { useNavigate, useParams } from 'react-router-dom';
import { useTranslation } from 'react-i18next';
import { snippets } from '../../api';
import type { Snippet } from '../../api';
import { Breadcrumbs, Button, Card, Input } from '../../ui';
import { Breadcrumbs, Button, Card, Input, CodeEditor } from '../../ui';
import styles from '../Page.module.css';
export function EditSnippetPage() {
@@ -83,7 +83,7 @@ export function EditSnippetPage() {
{!loading && snippet && (
<form onSubmit={handleSubmit} noValidate>
<Card className={styles.formCard}>
<Card className={styles.formCardFull}>
<div className={styles.formFields}>
<Input
label={t('snippets.form_name')}
@@ -108,16 +108,14 @@ export function EditSnippetPage() {
{t('snippets.form_code')}
<span className={styles.requiredMark}> *</span>
</label>
<textarea
className={[styles.textarea, codeError ? styles.textareaError : ''].filter(Boolean).join(' ')}
rows={14}
<CodeEditor
value={code}
onChange={(e) => {
setCode(e.target.value);
onChange={(v) => {
setCode(v);
setCodeError('');
}}
placeholder={t('snippets.form_code_placeholder')}
spellCheck={false}
rows={14}
error={!!codeError}
/>
{codeError && <span className={styles.fieldError}>{codeError}</span>}
</div>
@@ -1,7 +1,8 @@
import { useEffect, useState } from 'react';
import { useNavigate, useParams } from 'react-router-dom';
import { useTranslation } from 'react-i18next';
import { Download, Pencil, Trash2 } from 'lucide-react';
import { Upload, Pencil, Trash2 } from 'lucide-react';
import { CodeBlock } from '../../ui';
import { stringify as yamlStringify } from 'yaml';
import { snippets } from '../../api';
import type { Snippet } from '../../api';
@@ -55,7 +56,7 @@ export function SnippetDetailPage() {
{snippet && (
<div className={styles.toolbarActions}>
<Button variant="secondary" size="sm" onClick={handleExport}>
<Download size={14} />
<Upload size={14} />
{t('snippets.action_export')}
</Button>
<Button
@@ -114,7 +115,7 @@ export function SnippetDetailPage() {
<h2 className={styles.sectionHeading}>{t('snippets.section_code')}</h2>
</div>
<Card>
<pre className={styles.codeBlock}>{snippet.code}</pre>
<CodeBlock code={snippet.code} />
</Card>
</div>
</>
+2 -2
View File
@@ -1,7 +1,7 @@
import { useEffect, useRef, useState } from 'react';
import { useNavigate } from 'react-router-dom';
import { useTranslation } from 'react-i18next';
import { Code, Pencil, Trash2, Plus, Upload } from 'lucide-react';
import { Code, Pencil, Trash2, Plus, Download } from 'lucide-react';
import { parse as yamlParse } from 'yaml';
import { snippets } from '../../api';
import type { Snippet } from '../../api';
@@ -134,7 +134,7 @@ export function SnippetsPage() {
onChange={handleImport}
/>
<Button variant="secondary" size="sm" onClick={() => fileInputRef.current?.click()}>
<Upload size={14} />
<Download size={14} />
{t('snippets.action_import')}
</Button>
</div>
@@ -0,0 +1,76 @@
.pre {
margin: 0;
overflow-x: auto;
white-space: pre-wrap;
word-break: break-all;
}
.code {
font-family: var(--font-mono, ui-monospace, 'Cascadia Code', 'Fira Code', monospace);
font-size: var(--font-size-sm);
line-height: 1.6;
display: block;
background: transparent !important;
color: var(--color-text);
}
/* ── highlight.js token overrides (theme-aware) ─────────────────────── */
.code :global(.hljs-comment),
.code :global(.hljs-quote) {
color: var(--hl-comment, #6A8072);
font-style: italic;
}
.code :global(.hljs-keyword),
.code :global(.hljs-selector-tag),
.code :global(.hljs-built_in),
.code :global(.hljs-name),
.code :global(.hljs-tag) {
color: var(--hl-keyword, #A67DCC);
}
.code :global(.hljs-string),
.code :global(.hljs-title),
.code :global(.hljs-section),
.code :global(.hljs-attribute),
.code :global(.hljs-literal),
.code :global(.hljs-template-tag),
.code :global(.hljs-template-variable),
.code :global(.hljs-type),
.code :global(.hljs-addition) {
color: var(--hl-string, #9BB870);
}
.code :global(.hljs-number),
.code :global(.hljs-regexp),
.code :global(.hljs-link) {
color: var(--hl-number, #E8A85F);
}
.code :global(.hljs-function),
.code :global(.hljs-title.hljs-function_) {
color: var(--hl-function, #5BB8D4);
}
.code :global(.hljs-variable),
.code :global(.hljs-params) {
color: var(--hl-variable, #D4A0A0);
}
.code :global(.hljs-property) {
color: var(--hl-property, #79C0C8);
}
.code :global(.hljs-deletion),
.code :global(.hljs-meta) {
color: var(--hl-meta, #A9A3C9);
}
.code :global(.hljs-emphasis) {
font-style: italic;
}
.code :global(.hljs-strong) {
font-weight: 700;
}
+27
View File
@@ -0,0 +1,27 @@
import { useMemo } from 'react';
import hljs, { escapeHtml } from '../../lib/hljs';
import styles from './CodeBlock.module.css';
export interface CodeBlockProps {
code: string;
language?: string;
}
export function CodeBlock({ code, language = 'javascript' }: CodeBlockProps) {
const highlighted = useMemo(() => {
try {
return hljs.highlight(code, { language }).value;
} catch {
return code.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
}
}, [code, language]);
return (
<pre className={styles.pre}>
<code
className={`${styles.code} hljs language-${language}`}
dangerouslySetInnerHTML={{ __html: highlighted }}
/>
</pre>
);
}
@@ -0,0 +1,18 @@
.wrapper {
width: 100%;
border: var(--border-width) solid var(--color-border);
border-radius: var(--radius-md);
overflow: hidden;
resize: vertical;
min-height: 80px;
transition: border-color var(--transition), box-shadow var(--transition);
}
.focused {
border-color: var(--color-primary);
box-shadow: 0 0 0 3px color-mix(in srgb, var(--color-primary) 20%, transparent);
}
.hasError {
border-color: var(--color-danger);
}
+83
View File
@@ -0,0 +1,83 @@
import { useState, useEffect } from 'react';
import Editor, { OnMount } from '@monaco-editor/react';
import styles from './CodeEditor.module.css';
export interface CodeEditorProps {
value: string;
onChange: (value: string) => void;
language?: string;
rows?: number;
error?: boolean;
}
function useMonacoTheme() {
const [theme, setTheme] = useState(() =>
document.documentElement.getAttribute('data-theme') === 'dark' ? 'vs-dark' : 'vs-light'
);
useEffect(() => {
const observer = new MutationObserver(() => {
setTheme(
document.documentElement.getAttribute('data-theme') === 'dark' ? 'vs-dark' : 'vs-light'
);
});
observer.observe(document.documentElement, {
attributes: true,
attributeFilter: ['data-theme'],
});
return () => observer.disconnect();
}, []);
return theme;
}
export function CodeEditor({
value,
onChange,
language = 'javascript',
rows = 6,
error = false,
}: CodeEditorProps) {
const theme = useMonacoTheme();
const [focused, setFocused] = useState(false);
const height = rows * 20 + 16;
const handleMount: OnMount = (editor) => {
editor.onDidFocusEditorWidget(() => setFocused(true));
editor.onDidBlurEditorWidget(() => setFocused(false));
};
return (
<div
className={[
styles.wrapper,
focused ? styles.focused : '',
error ? styles.hasError : '',
]
.filter(Boolean)
.join(' ')}
style={{ height }}
>
<Editor
height="100%"
theme={theme}
language={language}
value={value}
onChange={(v) => onChange(v ?? '')}
onMount={handleMount}
options={{
minimap: { enabled: false },
scrollBeyondLastLine: false,
fontSize: 14,
lineHeight: 20,
wordWrap: 'on',
padding: { top: 8, bottom: 8 },
folding: false,
renderLineHighlight: 'line',
scrollbar: { vertical: 'auto', horizontal: 'hidden' },
automaticLayout: true,
}}
/>
</div>
);
}
+6
View File
@@ -36,6 +36,12 @@ export type { PaginationProps } from './Pagination/Pagination';
export { Search } from './Search/Search';
export type { SearchProps } from './Search/Search';
export { CodeBlock } from './CodeBlock/CodeBlock';
export type { CodeBlockProps } from './CodeBlock/CodeBlock';
export { CodeEditor } from './CodeEditor/CodeEditor';
export type { CodeEditorProps } from './CodeEditor/CodeEditor';
export { ContextMenu } from './ContextMenu/ContextMenu';
export type { ContextMenuProps, ContextMenuItem } from './ContextMenu/ContextMenu';
+82
View File
@@ -15,6 +15,8 @@
"client": {
"name": "liqa-client",
"dependencies": {
"@monaco-editor/react": "^4.7.0",
"highlight.js": "^11.11.1",
"i18next": "^26.0.4",
"lucide-react": "^1.7.0",
"moment": "^2.30.1",
@@ -3063,6 +3065,29 @@
}
}
},
"node_modules/@monaco-editor/loader": {
"version": "1.7.0",
"resolved": "https://registry.npmjs.org/@monaco-editor/loader/-/loader-1.7.0.tgz",
"integrity": "sha512-gIwR1HrJrrx+vfyOhYmCZ0/JcWqG5kbfG7+d3f/C1LXk2EvzAbHSg3MQ5lO2sMlo9izoAZ04shohfKLVT6crVA==",
"license": "MIT",
"dependencies": {
"state-local": "^1.0.6"
}
},
"node_modules/@monaco-editor/react": {
"version": "4.7.0",
"resolved": "https://registry.npmjs.org/@monaco-editor/react/-/react-4.7.0.tgz",
"integrity": "sha512-cyzXQCtO47ydzxpQtCGSQGOC8Gk3ZUeBXFAxD+CWXYFo5OqZyZUonFl0DwUlTyAfRHntBfw2p3w4s9R6oe1eCA==",
"license": "MIT",
"dependencies": {
"@monaco-editor/loader": "^1.5.0"
},
"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"
}
},
"node_modules/@mozilla/readability": {
"version": "0.6.0",
"resolved": "https://registry.npmjs.org/@mozilla/readability/-/readability-0.6.0.tgz",
@@ -4642,6 +4667,14 @@
"dev": true,
"license": "MIT"
},
"node_modules/@types/trusted-types": {
"version": "2.0.7",
"resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz",
"integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==",
"license": "MIT",
"optional": true,
"peer": true
},
"node_modules/@types/validator": {
"version": "13.15.10",
"resolved": "https://registry.npmjs.org/@types/validator/-/validator-13.15.10.tgz",
@@ -6947,6 +6980,16 @@
"node": ">=6.0.0"
}
},
"node_modules/dompurify": {
"version": "3.2.7",
"resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.2.7.tgz",
"integrity": "sha512-WhL/YuveyGXJaerVlMYGWhvQswa7myDG17P7Vu65EWC05o8vfeNbvNf4d/BOvH99+ZW+LlQsc1GDKMa1vNK6dw==",
"license": "(MPL-2.0 OR Apache-2.0)",
"peer": true,
"optionalDependencies": {
"@types/trusted-types": "^2.0.7"
}
},
"node_modules/dotenv": {
"version": "17.2.3",
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.2.3.tgz",
@@ -8564,6 +8607,15 @@
"hermes-estree": "0.25.1"
}
},
"node_modules/highlight.js": {
"version": "11.11.1",
"resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-11.11.1.tgz",
"integrity": "sha512-Xwwo44whKBVCYoliBQwaPvtd/2tYFkRQtXDWj1nackaV2JPXx3L0+Jvd8/qCJ2p+ML0/XVkJ2q+Mr+UVdpJK5w==",
"license": "BSD-3-Clause",
"engines": {
"node": ">=12.0.0"
}
},
"node_modules/hono": {
"version": "4.12.12",
"resolved": "https://registry.npmjs.org/hono/-/hono-4.12.12.tgz",
@@ -10350,6 +10402,19 @@
"tmpl": "1.0.5"
}
},
"node_modules/marked": {
"version": "14.0.0",
"resolved": "https://registry.npmjs.org/marked/-/marked-14.0.0.tgz",
"integrity": "sha512-uIj4+faQ+MgHgwUW1l2PsPglZLOLOT1uErt06dAPtx2kjteLAkbsd/0FiYg/MGS+i7ZKLb7w2WClxHkzOOuryQ==",
"license": "MIT",
"peer": true,
"bin": {
"marked": "bin/marked.js"
},
"engines": {
"node": ">= 18"
}
},
"node_modules/math-intrinsics": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
@@ -10532,6 +10597,17 @@
"node": "*"
}
},
"node_modules/monaco-editor": {
"version": "0.55.1",
"resolved": "https://registry.npmjs.org/monaco-editor/-/monaco-editor-0.55.1.tgz",
"integrity": "sha512-jz4x+TJNFHwHtwuV9vA9rMujcZRb0CEilTEwG2rRSpe/A7Jdkuj8xPKttCgOh+v/lkHy7HsZ64oj+q3xoAFl9A==",
"license": "MIT",
"peer": true,
"dependencies": {
"dompurify": "3.2.7",
"marked": "14.0.0"
}
},
"node_modules/mrmime": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/mrmime/-/mrmime-2.0.1.tgz",
@@ -12315,6 +12391,12 @@
"dev": true,
"license": "MIT"
},
"node_modules/state-local": {
"version": "1.0.7",
"resolved": "https://registry.npmjs.org/state-local/-/state-local-1.0.7.tgz",
"integrity": "sha512-HTEHMNieakEnoe33shBYcZ7NX83ACUjCu8c40iOGEZsngj9zRnkqS9j1pqQPXwobB0ZcVTk27REb7COQ0UR59w==",
"license": "MIT"
},
"node_modules/statuses": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz",