Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 47 additions & 0 deletions docs/source/tags/textarea.md
Original file line number Diff line number Diff line change
Expand Up @@ -87,3 +87,50 @@ For that you should note the following:
<TextArea name="text-area-2" toName="text2" rows="3" />
</View>
```

### Markdown Editor

You can enable a markdown editor for rich text formatting with syntax highlighting and live preview. When `markdown="true"`, the editor uses CodeMirror with Markdown syntax highlighting and includes a split-view mode for real-time preview.

```html
<View>
<Text name="text" value="$text"/>
<TextArea
name="edited_content"
toName="text"
markdown="true"
editable="true"
placeholder="Edit markdown content..."
/>
</View>
```

#### Markdown Editor Features

- **Syntax Highlighting**: Markdown syntax is highlighted in the editor
- **Live Preview**: Toggle between "Edit" and "Split" modes for side-by-side preview
- **Keyboard Shortcuts**:
- `Ctrl/Cmd+B`: Bold - Wrap selection with `**text**`
- `Ctrl/Cmd+I`: Italic - Wrap selection with `*text*`
- `Ctrl/Cmd+K`: Link - Insert `[text](url)` format
- `Ctrl/Cmd+\``: Inline code - Wrap selection with `` `code` ``
- `Ctrl/Cmd+Z`: Undo
- `Ctrl/Cmd+Shift+Z`: Redo
- `Ctrl/Cmd+F`: Find
- `Ctrl/Cmd+/`: Toggle comment
- **Stats Display**: Character and word counts update in real-time

#### Parameters

The `TextArea` tag accepts the following parameters related to markdown editing:

| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| `markdown` | `boolean` | `false` | Enable markdown editor mode with syntax highlighting and preview |
| `rows` | `number` | `10` | Number of rows for the editor (minimum 3) |
| `placeholder` | `string` | `"Enter markdown text..."` | Placeholder text shown when editor is empty |

#### Related

- [Markdown View tag](../tags/markdown): For displaying rendered markdown content
- [Text tag](../tags/text): For displaying text data
30 changes: 19 additions & 11 deletions web/libs/editor/src/components/HtxTextBox/HtxTextBox.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { IconPencil, IconTrashAlt, IconCheck } from "@humansignal/icons";
import { Button, Tooltip, Typography } from "@humansignal/ui";
import { throttle } from "@humansignal/core/lib/utils/lodash-replacements";
import { cn } from "../../utils/bem";
import { Markdown } from "../Markdown/Markdown";

// used for correct auto-height calculation
const BORDER_WIDTH = 1;
Expand Down Expand Up @@ -177,6 +178,7 @@ export class HtxTextBox extends React.Component {
isEditable,
isDeleteable,
text,
markdown,

// don't pass non-DOM props to Paragraph
ignoreShortcuts: _,
Expand All @@ -188,17 +190,23 @@ export class HtxTextBox extends React.Component {
return (
<div className={cn("textarea").elem("region").toClassName()} data-testid="htx-textbox-view">
<div className={this.inputClassName} id={props.id} name={props.name} data-testid="htx-textbox-content">
<Typography ref={this.textRef} size="small">
{text.split("\n").map((line, index, array) => {
const isLastLine = index === array.length - 1;
return (
<React.Fragment key={index}>
{line}
{!isLastLine && <br />}
</React.Fragment>
);
})}
</Typography>
{markdown ? (
<div ref={this.textRef}>
<Markdown text={text} allowHtml={false} />
</div>
) : (
<Typography ref={this.textRef} size="small">
{text.split("\n").map((line, index, array) => {
const isLastLine = index === array.length - 1;
return (
<React.Fragment key={index}>
{line}
{!isLastLine && <br />}
</React.Fragment>
);
})}
</Typography>
)}
</div>
<div className={cn("textarea").elem("actions").toClassName()} data-testid="htx-textbox-actions">
{isEditable && onChange && (
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,259 @@
.markdownEditor {
display: flex;
flex-direction: column;
border: 1px solid var(--color-neutral-border);
border-radius: 6px;
overflow: hidden;
background: var(--color-neutral-background);

&__tabs {
display: flex;
justify-content: space-between;
align-items: center;
padding: 8px 12px;
border-bottom: 1px solid var(--color-neutral-border);
background: var(--color-neutral-surface);
}

&__tabsButtons {
display: flex;
gap: 8px;
align-items: center;
}

&__tab {
transition: all 0.2s ease;

&_active {
font-weight: 600;
}
}

&__viewToggle {
// Button styling is handled by the Button component itself
}

&__stats {
display: flex;
align-items: center;
gap: 8px;
font-size: 12px;
color: var(--color-neutral-content-subtle);
}

&__stat {
white-space: nowrap;
}

&__statSeparator {
color: var(--color-neutral-content-subtler);
}

&__content {
flex: 1;
min-height: var(--markdown-editor-min-height, 200px);
overflow: hidden;
display: flex;
flex-direction: column;

&_split {
flex-direction: row;
gap: 1px;
background: var(--color-neutral-border);

.markdownEditor__editor,
.markdownEditor__preview {
flex: 1;
width: 50%;
border: none;
}
}
}

&__editor {
height: 100%;

:global(.react-codemirror2) {
height: 100%;
width: 100%;

:global(.CodeMirror) {
height: 100%;
width: 100%;
border: none;
background: var(--color-neutral-background);
color: var(--color-neutral-content-subtle);
font-family: var(--font-mono);
font-size: 14px;
line-height: 1.6;
}

:global(.CodeMirror-lines) {
padding: var(--spacing-tight) 0;
}

:global(.CodeMirror-line) {
padding: 0 var(--spacing-tight);
}

:global(.CodeMirror-scroll) {
min-height: var(--markdown-editor-min-height, 200px);
}

:global(.CodeMirror-cursor) {
border-color: var(--color-neutral-content);
}

// Syntax highlighting
:global(.cm-attribute),
:global(.cm-keyword) {
color: var(--color-accent-blueberry-bold);
}

:global(.cm-def) {
color: var(--color-accent-grape-bold);
}

:global(.cm-builtin) {
color: var(--color-accent-canteloupe-bold);
}

:global(.cm-number) {
color: var(--color-accent-kiwi-bold);
}

:global(.cm-tag),
:global(.cm-bracket) {
color: var(--color-accent-kale-bold);
}

:global(.cm-string) {
color: var(--color-accent-persimmon-bold);
}

:global(.cm-comment) {
color: var(--color-accent-sand-bold);
}

:global(.cm-header) {
color: var(--color-accent-blueberry-bold);
}

:global(.cm-link) {
color: var(--color-accent-persimmon-bold);
}

:global(.cm-quote) {
color: var(--color-accent-sand-bold);
}

:global(.cm-strong) {
color: var(--color-accent-canteloupe-bold);
}

:global(.cm-string-2) {
color: var(--color-accent-persimmon-bold);
}

:global(.cm-variable) {
color: var(--color-neutral-content);
}

:global(.CodeMirror-gutters) {
background-color: var(--color-neutral-surface-inset);
color: var(--color-neutral-content-subtlest);
border-right: 1px solid var(--color-neutral-border);
}

:global(.CodeMirror-linenumber) {
color: var(--color-neutral-content-subtlest);
}

:global(.CodeMirror-placeholder) {
color: var(--color-neutral-content-subtler);
font-style: italic;
}
}
}

&__preview {
height: 100%;
padding: 16px;
overflow-y: auto;
background: var(--color-neutral-background);
min-height: var(--markdown-editor-min-height, 200px);

> * {
max-width: 100%;
}

pre {
overflow-x: auto;
}

img {
max-width: 100%;
height: auto;
}
}

&__preview-empty {
display: flex;
align-items: center;
justify-content: center;
height: 100%;
min-height: var(--markdown-editor-min-height, 200px);
color: var(--color-neutral-content-subtler);
font-style: italic;
text-align: center;
padding: 24px;
}

@media (max-width: 768px) {
&__tabs {
flex-direction: column;
gap: 8px;
align-items: stretch;
}

&__tabsButtons {
width: 100%;
flex-wrap: wrap;

button {
flex: 1;
}
}

&__stats {
justify-content: center;
font-size: 11px;
}

&__content_split {
flex-direction: column;

.markdownEditor__editor,
.markdownEditor__preview {
width: 100%;
min-height: 200px;
}
}
}
}

// Dark mode: CSS variables handle editor styling automatically
[data-color-scheme="dark"] {
.markdownEditor {
background: var(--color-neutral-on-dark-background);

&__tabs {
background: var(--color-neutral-on-dark-surface);
}

&__preview {
background: var(--color-neutral-on-dark-background);
color: var(--color-neutral-on-dark-content);
}
}
}
Loading
Loading