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:
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
</>
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user