Skip to content

Commit dc0cdcf

Browse files
committed
Revamp editor toolbar and extend BBCode support
1 parent 4fa158b commit dc0cdcf

File tree

8 files changed

+81
-35
lines changed

8 files changed

+81
-35
lines changed

src/App.test.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ describe('App', () => {
1717
const textarea = screen.getByRole('textbox') as HTMLTextAreaElement
1818
fireEvent.change(textarea, { target: { value: 'texto' } })
1919
textarea.setSelectionRange(0, 5)
20-
await userEvent.click(screen.getByText('[b]'))
20+
await userEvent.click(screen.getByRole('button', { name: 'B' }))
2121
expect(textarea.value.startsWith('[b]')).toBe(true)
2222
await userEvent.click(screen.getByRole('button', { name: /Limpiar/i }))
2323
expect(textarea.value).toBe('')

src/App.tsx

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { useEffect, useMemo, useRef, useState } from 'react'
22
import { useTranslation } from 'react-i18next'
3-
import { chips, languages, sample, type LanguageId } from './constants'
3+
import { languages, sample, toolbarButtons, type LanguageId } from './constants'
44
import EditorPanel from './components/EditorPanel'
55
import LanguageSwitch from './components/LanguageSwitch'
66
import PreviewPanel from './components/PreviewPanel'
@@ -33,11 +33,18 @@ function App() {
3333
const templates: Record<TagKey, string> = {
3434
bold: `[b]${selected || 'texto'}[/b]`,
3535
italic: `[i]${selected || 'texto'}[/i]`,
36+
underline: `[u]${selected || 'texto'}[/u]`,
37+
strike: `[s]${selected || 'texto'}[/s]`,
3638
link: `[url=${selected ? 'https://example.com' : 'https://example.com'}]${selected || 'enlace'}[/url]`,
3739
image: `[img]https://placekitten.com/400/240[/img]`,
3840
quote: `[quote=${selected || 'Autor'}]${selected || 'Texto citado'}[/quote]`,
3941
code: `[code]const hello = "world";[/code]`,
4042
list: `[list][*]Item 1[*]Item 2[/list]`,
43+
color: `[color=#00bcd4]${selected || 'texto'}[/color]`,
44+
size: `[size=18]${selected || 'texto grande'}[/size]`,
45+
center: `[center]${selected || 'texto centrado'}[/center]`,
46+
left: `[left]${selected || 'texto alineado a la izquierda'}[/left]`,
47+
right: `[right]${selected || 'texto alineado a la derecha'}[/right]`,
4148
}
4249

4350
const snippet = templates[tag] || ''
@@ -104,7 +111,7 @@ function App() {
104111
<EditorPanel
105112
title={t('editor')}
106113
subtitle={t('write_bbcode')}
107-
chips={chips}
114+
buttons={toolbarButtons}
108115
textareaRef={textareaRef}
109116
value={input}
110117
placeholder={t('placeholder')}

src/components/EditorPanel.tsx

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,29 +1,29 @@
11
import type { ChangeEvent, RefObject } from 'react'
2-
import type { ChipConfig } from '../constants'
2+
import type { ButtonConfig } from '../constants'
33
import type { TagKey } from '../utils/bbcode'
4-
import TagChip from './TagChip'
4+
import ToolbarButton from './ToolbarButton'
55

66
type Props = {
77
title: string
88
subtitle: string
9-
chips: ChipConfig[]
9+
buttons: ButtonConfig[]
1010
textareaRef: RefObject<HTMLTextAreaElement | null>
1111
value: string
1212
placeholder: string
1313
onChange: (value: string) => void
1414
onInsert: (tag: TagKey) => void
1515
}
1616

17-
const EditorPanel = ({ title, subtitle, chips, textareaRef, value, placeholder, onChange, onInsert }: Props) => (
17+
const EditorPanel = ({ title, subtitle, buttons, textareaRef, value, placeholder, onChange, onInsert }: Props) => (
1818
<section className="panel bg-slate-900/60 border border-white/5 rounded-2xl shadow-xl p-4 space-y-3">
1919
<div className="flex items-center justify-between flex-wrap gap-3">
2020
<div>
2121
<p className="text-xs uppercase tracking-[0.2em] text-cyan-200">{title}</p>
2222
<h2 className="text-xl font-semibold">{subtitle}</h2>
2323
</div>
24-
<div className="flex gap-2 flex-wrap">
25-
{chips.map(({ key, label }) => (
26-
<TagChip key={key} label={label} onClick={() => onInsert(key)} />
24+
<div className="flex gap-2 flex-wrap border border-white/10 bg-white/5 rounded-lg px-2 py-1">
25+
{buttons.map(({ key, label, title: hint }) => (
26+
<ToolbarButton key={key} label={label} title={hint} onClick={() => onInsert(key)} />
2727
))}
2828
</div>
2929
</div>

src/components/TagChip.tsx

Lines changed: 0 additions & 15 deletions
This file was deleted.

src/components/ToolbarButton.tsx

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
type Props = {
2+
label: string
3+
title?: string
4+
onClick: () => void
5+
}
6+
7+
const ToolbarButton = ({ label, title, onClick }: Props) => (
8+
<button
9+
type="button"
10+
title={title}
11+
onClick={onClick}
12+
className="toolbar-btn px-2.5 py-1.5 rounded-md border border-white/10 bg-white/5 text-sm font-semibold transition hover:-translate-y-0.5 hover:border-white/20 hover:bg-white/10"
13+
>
14+
{label}
15+
</button>
16+
)
17+
18+
export default ToolbarButton

src/constants.ts

Lines changed: 16 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import type { TagKey } from './utils/bbcode'
22

33
export type LanguageId = 'es' | 'en'
4-
export type ChipConfig = { key: TagKey; label: string }
4+
export type ButtonConfig = { key: TagKey; label: string; title?: string }
55

66
export const sample = `[b]BBCode Preview[/b]
77
Escribe BBCode y mira el HTML renderizado al instante.
@@ -13,14 +13,21 @@ Escribe BBCode y mira el HTML renderizado al instante.
1313
1414
[code]const greet = () => console.log("hola");[/code]`
1515

16-
export const chips: ChipConfig[] = [
17-
{ key: 'bold', label: '[b]' },
18-
{ key: 'italic', label: '[i]' },
19-
{ key: 'link', label: '[url]' },
20-
{ key: 'image', label: '[img]' },
21-
{ key: 'quote', label: '[quote]' },
22-
{ key: 'code', label: '[code]' },
23-
{ key: 'list', label: '[list]' },
16+
export const toolbarButtons: ButtonConfig[] = [
17+
{ key: 'bold', label: 'B', title: 'Negrita' },
18+
{ key: 'italic', label: 'I', title: 'Cursiva' },
19+
{ key: 'underline', label: 'U', title: 'Subrayado' },
20+
{ key: 'strike', label: 'S', title: 'Tachado' },
21+
{ key: 'list', label: '• List', title: 'Lista' },
22+
{ key: 'left', label: 'L', title: 'Alinear izquierda' },
23+
{ key: 'center', label: 'C', title: 'Centrar' },
24+
{ key: 'right', label: 'R', title: 'Alinear derecha' },
25+
{ key: 'quote', label: '❝', title: 'Cita' },
26+
{ key: 'code', label: '</>', title: 'Código' },
27+
{ key: 'link', label: 'Link', title: 'Enlace' },
28+
{ key: 'image', label: 'Img', title: 'Imagen' },
29+
{ key: 'color', label: 'Color', title: 'Color' },
30+
{ key: 'size', label: 'Size', title: 'Tamaño' },
2431
]
2532

2633
export const languages: Array<{ id: LanguageId; label: string }> = [

src/index.css

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,21 @@ body {
4444
border-color: rgba(0, 0, 0, 0.06);
4545
}
4646

47+
.toolbar-btn {
48+
transition: background-color 0.15s ease, border-color 0.15s ease, transform 0.1s ease, color 0.15s ease;
49+
}
50+
51+
.theme-light .toolbar-btn {
52+
border-color: rgba(0, 0, 0, 0.08);
53+
background: #f7f9fc;
54+
color: #0b1223;
55+
}
56+
57+
.theme-light .toolbar-btn:hover {
58+
border-color: rgba(0, 0, 0, 0.16);
59+
background: #eef2f7;
60+
}
61+
4762
.theme-light .chip {
4863
border-color: rgba(0, 0, 0, 0.08);
4964
background: #f7f9fc;

src/utils/bbcode.ts

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,18 @@
1-
export type TagKey = 'bold' | 'italic' | 'link' | 'image' | 'quote' | 'code' | 'list'
1+
export type TagKey =
2+
| 'bold'
3+
| 'italic'
4+
| 'underline'
5+
| 'strike'
6+
| 'link'
7+
| 'image'
8+
| 'quote'
9+
| 'code'
10+
| 'list'
11+
| 'color'
12+
| 'size'
13+
| 'center'
14+
| 'left'
15+
| 'right'
216

317
const escapeHtml = (str: string) =>
418
str

0 commit comments

Comments
 (0)