@timmbr/ui 1.14.0 โ 1.15.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/components/RichTextEditor/RichTextContent.d.ts +8 -0
- package/dist/components/RichTextEditor/RichTextContent.d.ts.map +1 -0
- package/dist/components/RichTextEditor/RichTextContent.js +18 -0
- package/dist/components/RichTextEditor/RichTextEditor.context.d.ts +6 -0
- package/dist/components/RichTextEditor/RichTextEditor.context.d.ts.map +1 -0
- package/dist/components/RichTextEditor/RichTextEditor.context.js +14 -0
- package/dist/components/RichTextEditor/RichTextEditor.d.ts +4 -0
- package/dist/components/RichTextEditor/RichTextEditor.d.ts.map +1 -0
- package/dist/components/RichTextEditor/RichTextEditor.helpers.d.ts +28 -0
- package/dist/components/RichTextEditor/RichTextEditor.helpers.d.ts.map +1 -0
- package/dist/components/RichTextEditor/RichTextEditor.helpers.js +52 -0
- package/dist/components/RichTextEditor/RichTextEditor.js +156 -0
- package/dist/components/RichTextEditor/RichTextEditor.styles.d.ts +10 -0
- package/dist/components/RichTextEditor/RichTextEditor.styles.d.ts.map +1 -0
- package/dist/components/RichTextEditor/RichTextEditor.styles.js +95 -0
- package/dist/components/RichTextEditor/RichTextEditor.types.d.ts +73 -0
- package/dist/components/RichTextEditor/RichTextEditor.types.d.ts.map +1 -0
- package/dist/components/RichTextEditor/RichTextJsonContent.d.ts +9 -0
- package/dist/components/RichTextEditor/RichTextJsonContent.d.ts.map +1 -0
- package/dist/components/RichTextEditor/RichTextJsonContent.js +27 -0
- package/dist/components/RichTextEditor/index.d.ts +14 -0
- package/dist/components/RichTextEditor/index.d.ts.map +1 -0
- package/dist/components/RichTextEditor/subcomponents/RteCharCount.d.ts +4 -0
- package/dist/components/RichTextEditor/subcomponents/RteCharCount.d.ts.map +1 -0
- package/dist/components/RichTextEditor/subcomponents/RteCharCount.js +37 -0
- package/dist/components/RichTextEditor/subcomponents/RteEmojiPicker.d.ts +3 -0
- package/dist/components/RichTextEditor/subcomponents/RteEmojiPicker.d.ts.map +1 -0
- package/dist/components/RichTextEditor/subcomponents/RteEmojiPicker.js +274 -0
- package/dist/components/RichTextEditor/subcomponents/RteLinkPopover.d.ts +3 -0
- package/dist/components/RichTextEditor/subcomponents/RteLinkPopover.d.ts.map +1 -0
- package/dist/components/RichTextEditor/subcomponents/RteLinkPopover.js +93 -0
- package/dist/components/RichTextEditor/subcomponents/RteToolbar.d.ts +7 -0
- package/dist/components/RichTextEditor/subcomponents/RteToolbar.d.ts.map +1 -0
- package/dist/components/RichTextEditor/subcomponents/RteToolbar.js +234 -0
- package/dist/components/RichTextEditor/subcomponents/RteToolbarButton.d.ts +4 -0
- package/dist/components/RichTextEditor/subcomponents/RteToolbarButton.d.ts.map +1 -0
- package/dist/components/RichTextEditor/subcomponents/RteToolbarButton.js +37 -0
- package/dist/components/RichTextEditor/subcomponents/RteToolbarGroup.d.ts +4 -0
- package/dist/components/RichTextEditor/subcomponents/RteToolbarGroup.d.ts.map +1 -0
- package/dist/components/RichTextEditor/subcomponents/RteToolbarGroup.js +13 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +12 -0
- package/package.json +12 -4
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsxs, jsx } from 'react/jsx-runtime';
|
|
3
|
+
import * as React from 'react';
|
|
4
|
+
import { SmilePlus } from '@timmbr/icons';
|
|
5
|
+
import { cn } from '@timmbr/utils';
|
|
6
|
+
import { useRteContext } from '../RichTextEditor.context.js';
|
|
7
|
+
import { Popover, PopoverTrigger, PopoverContent } from '../../Popover/Popover.js';
|
|
8
|
+
import { Tooltip } from '../../Tooltip/Tooltip.js';
|
|
9
|
+
import { Button } from '../../Button/Button.js';
|
|
10
|
+
import { Input } from '../../Input/Input.js';
|
|
11
|
+
|
|
12
|
+
const CATEGORIES = [
|
|
13
|
+
{
|
|
14
|
+
id: "smileys",
|
|
15
|
+
label: "Smileys",
|
|
16
|
+
entries: [
|
|
17
|
+
{ e: "๐", k: "smile happy" },
|
|
18
|
+
{ e: "๐", k: "laugh funny joy" },
|
|
19
|
+
{ e: "๐ฅฐ", k: "love adore hearts" },
|
|
20
|
+
{ e: "๐", k: "love eyes star" },
|
|
21
|
+
{ e: "๐คฉ", k: "star struck amazing" },
|
|
22
|
+
{ e: "๐", k: "cool sunglasses" },
|
|
23
|
+
{ e: "๐ฅณ", k: "party celebrate" },
|
|
24
|
+
{ e: "๐ค", k: "thinking hmm" },
|
|
25
|
+
{ e: "๐
", k: "sweat nervous" },
|
|
26
|
+
{ e: "๐", k: "grin laugh" },
|
|
27
|
+
{ e: "๐", k: "slight smile" },
|
|
28
|
+
{ e: "๐", k: "wink" },
|
|
29
|
+
{ e: "๐คฃ", k: "rolling laugh floor" },
|
|
30
|
+
{ e: "๐", k: "angel innocent" },
|
|
31
|
+
{ e: "๐ฅน", k: "tearful touched" },
|
|
32
|
+
{ e: "๐ค", k: "hug hugging" },
|
|
33
|
+
{ e: "๐", k: "yum delicious" },
|
|
34
|
+
{ e: "๐", k: "upside down silly" },
|
|
35
|
+
{ e: "๐ค", k: "triumph steam" },
|
|
36
|
+
{ e: "๐ฅบ", k: "pleading eyes cute" }
|
|
37
|
+
]
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
id: "hands",
|
|
41
|
+
label: "Hands",
|
|
42
|
+
entries: [
|
|
43
|
+
{ e: "๐", k: "thumbs up good like" },
|
|
44
|
+
{ e: "๐", k: "ok perfect" },
|
|
45
|
+
{ e: "๐", k: "raising hands celebrate" },
|
|
46
|
+
{ e: "๐ช", k: "muscle strong flex" },
|
|
47
|
+
{ e: "๐ค", k: "handshake deal" },
|
|
48
|
+
{ e: "๐", k: "clap applause" },
|
|
49
|
+
{ e: "๐", k: "pray thanks please" },
|
|
50
|
+
{ e: "โ๏ธ", k: "peace victory two" },
|
|
51
|
+
{ e: "๐ค", k: "fingers crossed luck" },
|
|
52
|
+
{ e: "๐ซถ", k: "heart hands love" },
|
|
53
|
+
{ e: "๐", k: "wave hello hi" },
|
|
54
|
+
{ e: "โ๏ธ", k: "point one up" },
|
|
55
|
+
{ e: "๐ค", k: "call shaka hang loose" },
|
|
56
|
+
{ e: "๐", k: "point up" },
|
|
57
|
+
{ e: "โ", k: "raised hand stop" },
|
|
58
|
+
{ e: "๐๏ธ", k: "five fingers" }
|
|
59
|
+
]
|
|
60
|
+
},
|
|
61
|
+
{
|
|
62
|
+
id: "hearts",
|
|
63
|
+
label: "Hearts",
|
|
64
|
+
entries: [
|
|
65
|
+
{ e: "โค๏ธ", k: "red heart love" },
|
|
66
|
+
{ e: "๐งก", k: "orange heart" },
|
|
67
|
+
{ e: "๐", k: "yellow heart" },
|
|
68
|
+
{ e: "๐", k: "green heart" },
|
|
69
|
+
{ e: "๐", k: "blue heart" },
|
|
70
|
+
{ e: "๐", k: "purple heart" },
|
|
71
|
+
{ e: "๐ค", k: "black heart" },
|
|
72
|
+
{ e: "๐ค", k: "white heart" },
|
|
73
|
+
{ e: "๐", k: "two hearts" },
|
|
74
|
+
{ e: "๐", k: "sparkling heart" },
|
|
75
|
+
{ e: "๐", k: "growing heart" },
|
|
76
|
+
{ e: "๐", k: "beating heart" },
|
|
77
|
+
{ e: "๐", k: "heart ribbon" },
|
|
78
|
+
{ e: "โค๏ธโ๐ฅ", k: "heart fire passion" },
|
|
79
|
+
{ e: "๐", k: "revolving hearts" },
|
|
80
|
+
{ e: "๐", k: "heart arrow cupid" }
|
|
81
|
+
]
|
|
82
|
+
},
|
|
83
|
+
{
|
|
84
|
+
id: "stars",
|
|
85
|
+
label: "Stars",
|
|
86
|
+
entries: [
|
|
87
|
+
{ e: "โญ", k: "star gold" },
|
|
88
|
+
{ e: "๐", k: "glowing star" },
|
|
89
|
+
{ e: "โจ", k: "sparkles magic" },
|
|
90
|
+
{ e: "๐ซ", k: "dizzy star spin" },
|
|
91
|
+
{ e: "๐", k: "trophy winner award" },
|
|
92
|
+
{ e: "๐ฅ", k: "gold medal first" },
|
|
93
|
+
{ e: "๐ฏ", k: "bullseye target" },
|
|
94
|
+
{ e: "๐ฅ", k: "fire hot trending" },
|
|
95
|
+
{ e: "๐ฅ", k: "boom explosion" },
|
|
96
|
+
{ e: "๐", k: "rainbow colors" },
|
|
97
|
+
{ e: "๐", k: "party popper celebration" },
|
|
98
|
+
{ e: "๐", k: "confetti ball party" },
|
|
99
|
+
{ e: "๐", k: "rocket launch" },
|
|
100
|
+
{ e: "๐ก", k: "lightbulb idea" }
|
|
101
|
+
]
|
|
102
|
+
},
|
|
103
|
+
{
|
|
104
|
+
id: "nature",
|
|
105
|
+
label: "Nature",
|
|
106
|
+
entries: [
|
|
107
|
+
{ e: "๐ธ", k: "cherry blossom flower pink" },
|
|
108
|
+
{ e: "๐บ", k: "hibiscus flower" },
|
|
109
|
+
{ e: "๐ป", k: "sunflower yellow" },
|
|
110
|
+
{ e: "๐น", k: "rose red flower" },
|
|
111
|
+
{ e: "๐ท", k: "tulip flower" },
|
|
112
|
+
{ e: "๐ฟ", k: "herb leaf green" },
|
|
113
|
+
{ e: "๐ฑ", k: "seedling sprout grow" },
|
|
114
|
+
{ e: "๐ฒ", k: "tree pine evergreen" },
|
|
115
|
+
{ e: "๐", k: "leaves wind" },
|
|
116
|
+
{ e: "๐", k: "wave ocean sea" },
|
|
117
|
+
{ e: "๐ง", k: "droplet water" },
|
|
118
|
+
{ e: "๐", k: "four leaf clover luck" },
|
|
119
|
+
{ e: "๐ชด", k: "potted plant" },
|
|
120
|
+
{ e: "๐", k: "moon night" },
|
|
121
|
+
{ e: "โ๏ธ", k: "sun sunny bright" },
|
|
122
|
+
{ e: "๐ค๏ธ", k: "sun behind cloud" }
|
|
123
|
+
]
|
|
124
|
+
},
|
|
125
|
+
{
|
|
126
|
+
id: "objects",
|
|
127
|
+
label: "Objects",
|
|
128
|
+
entries: [
|
|
129
|
+
{ e: "๐", k: "gift present wrapped" },
|
|
130
|
+
{ e: "๐๏ธ", k: "shopping bag store" },
|
|
131
|
+
{ e: "๐", k: "diamond gem jewel" },
|
|
132
|
+
{ e: "๐ท๏ธ", k: "label tag price" },
|
|
133
|
+
{ e: "๐", k: "key unlock" },
|
|
134
|
+
{ e: "๐ช", k: "magic wand" },
|
|
135
|
+
{ e: "๐จ", k: "art paint palette" },
|
|
136
|
+
{ e: "๐ฆ", k: "box package delivery" },
|
|
137
|
+
{ e: "๐งฟ", k: "eye protection amulet" },
|
|
138
|
+
{ e: "๐", k: "dress fashion clothing" },
|
|
139
|
+
{ e: "๐", k: "sneaker shoe running" },
|
|
140
|
+
{ e: "๐", k: "hat fashion" },
|
|
141
|
+
{ e: "๐", k: "ring engagement jewel" },
|
|
142
|
+
{ e: "๐", k: "handbag purse" },
|
|
143
|
+
{ e: "๐ถ๏ธ", k: "sunglasses cool" },
|
|
144
|
+
{ e: "๐งด", k: "lotion cream bottle" }
|
|
145
|
+
]
|
|
146
|
+
},
|
|
147
|
+
{
|
|
148
|
+
id: "symbols",
|
|
149
|
+
label: "Symbols",
|
|
150
|
+
entries: [
|
|
151
|
+
{ e: "โ
", k: "check done complete" },
|
|
152
|
+
{ e: "โ", k: "cross no wrong" },
|
|
153
|
+
{ e: "๐ฏ", k: "hundred percent perfect" },
|
|
154
|
+
{ e: "โป๏ธ", k: "recycle eco sustainable" },
|
|
155
|
+
{ e: "โก๏ธ", k: "right arrow" },
|
|
156
|
+
{ e: "โฌ๏ธ", k: "up arrow" },
|
|
157
|
+
{ e: "โฌ๏ธ", k: "down arrow" },
|
|
158
|
+
{ e: "โฌ
๏ธ", k: "left arrow" },
|
|
159
|
+
{ e: "๐", k: "top up above" },
|
|
160
|
+
{ e: "๐", k: "new" },
|
|
161
|
+
{ e: "๐", k: "free" },
|
|
162
|
+
{ e: "๐ด", k: "red circle dot" },
|
|
163
|
+
{ e: "๐ก", k: "yellow circle dot" },
|
|
164
|
+
{ e: "๐ข", k: "green circle dot" },
|
|
165
|
+
{ e: "๐ต", k: "blue circle dot" },
|
|
166
|
+
{ e: "โก", k: "lightning bolt fast energy" }
|
|
167
|
+
]
|
|
168
|
+
}
|
|
169
|
+
];
|
|
170
|
+
const ALL_ENTRIES = CATEGORIES.flatMap((c) => c.entries);
|
|
171
|
+
function RteEmojiPicker() {
|
|
172
|
+
const { editor, disabled, readOnly } = useRteContext();
|
|
173
|
+
const [open, setOpen] = React.useState(false);
|
|
174
|
+
const [search, setSearch] = React.useState("");
|
|
175
|
+
const [activeCategory, setActiveCategory] = React.useState("all");
|
|
176
|
+
const searchRef = React.useRef(null);
|
|
177
|
+
const isDisabled = disabled || readOnly || !editor;
|
|
178
|
+
const displayEmojis = React.useMemo(() => {
|
|
179
|
+
const pool = activeCategory === "all" ? ALL_ENTRIES : CATEGORIES.find((c) => c.id === activeCategory)?.entries ?? [];
|
|
180
|
+
if (!search.trim()) return pool.map((e) => e.e);
|
|
181
|
+
const q = search.toLowerCase();
|
|
182
|
+
return pool.filter((e) => e.k.includes(q) || e.e.includes(q)).map((e) => e.e);
|
|
183
|
+
}, [activeCategory, search]);
|
|
184
|
+
function handleSelect(emoji) {
|
|
185
|
+
editor?.chain().focus().insertContent(emoji).run();
|
|
186
|
+
setOpen(false);
|
|
187
|
+
}
|
|
188
|
+
function handleOpenChange(next) {
|
|
189
|
+
setOpen(next);
|
|
190
|
+
if (!next) {
|
|
191
|
+
setSearch("");
|
|
192
|
+
setActiveCategory("all");
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
return /* @__PURE__ */ jsxs(Popover, { open, onOpenChange: handleOpenChange, children: [
|
|
196
|
+
/* @__PURE__ */ jsx(Tooltip, { content: "Insert emoji", side: "bottom", delayDuration: 400, children: /* @__PURE__ */ jsx(PopoverTrigger, { asChild: true, children: /* @__PURE__ */ jsx(
|
|
197
|
+
Button,
|
|
198
|
+
{
|
|
199
|
+
variant: "ghost",
|
|
200
|
+
size: "icon-sm",
|
|
201
|
+
"aria-label": "Insert emoji",
|
|
202
|
+
disabled: isDisabled,
|
|
203
|
+
className: "h-7 w-7 rounded-[2px] data-[state=open]:bg-accent data-[state=open]:text-accent-foreground",
|
|
204
|
+
children: /* @__PURE__ */ jsx(SmilePlus, { size: 14 })
|
|
205
|
+
}
|
|
206
|
+
) }) }),
|
|
207
|
+
/* @__PURE__ */ jsx(
|
|
208
|
+
PopoverContent,
|
|
209
|
+
{
|
|
210
|
+
className: "w-72 max-w-[calc(100vw-1.5rem)] p-3",
|
|
211
|
+
side: "bottom",
|
|
212
|
+
align: "start",
|
|
213
|
+
onOpenAutoFocus: (e) => {
|
|
214
|
+
e.preventDefault();
|
|
215
|
+
searchRef.current?.focus();
|
|
216
|
+
},
|
|
217
|
+
children: /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-2", children: [
|
|
218
|
+
/* @__PURE__ */ jsx(
|
|
219
|
+
Input,
|
|
220
|
+
{
|
|
221
|
+
ref: searchRef,
|
|
222
|
+
placeholder: "Search emojiโฆ",
|
|
223
|
+
value: search,
|
|
224
|
+
onChange: (e) => setSearch(e.target.value),
|
|
225
|
+
size: "sm"
|
|
226
|
+
}
|
|
227
|
+
),
|
|
228
|
+
/* @__PURE__ */ jsxs("div", { className: "flex flex-wrap gap-1", children: [
|
|
229
|
+
/* @__PURE__ */ jsx(
|
|
230
|
+
"button",
|
|
231
|
+
{
|
|
232
|
+
type: "button",
|
|
233
|
+
onClick: () => setActiveCategory("all"),
|
|
234
|
+
className: cn(
|
|
235
|
+
"rounded px-2 py-0.5 text-caption transition-colors outline-none",
|
|
236
|
+
"focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-1",
|
|
237
|
+
activeCategory === "all" ? "bg-primary text-primary-foreground" : "text-muted-foreground hover:bg-accent hover:text-accent-foreground"
|
|
238
|
+
),
|
|
239
|
+
children: "All"
|
|
240
|
+
}
|
|
241
|
+
),
|
|
242
|
+
CATEGORIES.map((cat) => /* @__PURE__ */ jsx(
|
|
243
|
+
"button",
|
|
244
|
+
{
|
|
245
|
+
type: "button",
|
|
246
|
+
onClick: () => setActiveCategory(cat.id),
|
|
247
|
+
className: cn(
|
|
248
|
+
"rounded px-2 py-0.5 text-caption transition-colors outline-none",
|
|
249
|
+
"focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-1",
|
|
250
|
+
activeCategory === cat.id ? "bg-primary text-primary-foreground" : "text-muted-foreground hover:bg-accent hover:text-accent-foreground"
|
|
251
|
+
),
|
|
252
|
+
children: cat.label
|
|
253
|
+
},
|
|
254
|
+
cat.id
|
|
255
|
+
))
|
|
256
|
+
] }),
|
|
257
|
+
/* @__PURE__ */ jsx("div", { className: "grid grid-cols-[repeat(auto-fill,minmax(2rem,1fr))] gap-0.5 max-h-44 overflow-y-auto", children: displayEmojis.length > 0 ? displayEmojis.map((emoji) => /* @__PURE__ */ jsx(
|
|
258
|
+
"button",
|
|
259
|
+
{
|
|
260
|
+
type: "button",
|
|
261
|
+
onClick: () => handleSelect(emoji),
|
|
262
|
+
"aria-label": emoji,
|
|
263
|
+
className: "flex h-8 w-full items-center justify-center rounded hover:bg-accent transition-colors select-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-1 outline-none",
|
|
264
|
+
children: emoji
|
|
265
|
+
},
|
|
266
|
+
emoji
|
|
267
|
+
)) : /* @__PURE__ */ jsx("p", { className: "col-span-full text-center text-caption text-muted-foreground py-4", children: "No results" }) })
|
|
268
|
+
] })
|
|
269
|
+
}
|
|
270
|
+
)
|
|
271
|
+
] });
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
export { RteEmojiPicker };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"RteLinkPopover.d.ts","sourceRoot":"","sources":["../../../../src/components/RichTextEditor/subcomponents/RteLinkPopover.tsx"],"names":[],"mappings":"AAEA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAC;AAS/B,wBAAgB,cAAc,sBAwF7B"}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsxs, jsx } from 'react/jsx-runtime';
|
|
3
|
+
import * as React from 'react';
|
|
4
|
+
import { Link2, Unlink2 } from '@timmbr/icons';
|
|
5
|
+
import { useRteContext } from '../RichTextEditor.context.js';
|
|
6
|
+
import { normalizeUrl } from '../RichTextEditor.helpers.js';
|
|
7
|
+
import { Popover, PopoverTrigger, PopoverContent } from '../../Popover/Popover.js';
|
|
8
|
+
import { Tooltip } from '../../Tooltip/Tooltip.js';
|
|
9
|
+
import { Button } from '../../Button/Button.js';
|
|
10
|
+
import { Input } from '../../Input/Input.js';
|
|
11
|
+
|
|
12
|
+
function RteLinkPopover() {
|
|
13
|
+
const { editor, disabled, readOnly } = useRteContext();
|
|
14
|
+
const [open, setOpen] = React.useState(false);
|
|
15
|
+
const [urlInput, setUrlInput] = React.useState("");
|
|
16
|
+
const isActive = editor?.isActive("link") ?? false;
|
|
17
|
+
const isDisabled = disabled || readOnly || !editor;
|
|
18
|
+
function handleOpenChange(nextOpen) {
|
|
19
|
+
if (nextOpen && editor) {
|
|
20
|
+
const existing = editor.getAttributes("link").href ?? "";
|
|
21
|
+
setUrlInput(existing);
|
|
22
|
+
} else {
|
|
23
|
+
setUrlInput("");
|
|
24
|
+
}
|
|
25
|
+
setOpen(nextOpen);
|
|
26
|
+
}
|
|
27
|
+
function handleSubmit(e) {
|
|
28
|
+
e.preventDefault();
|
|
29
|
+
if (!editor || !urlInput.trim()) return;
|
|
30
|
+
const href = normalizeUrl(urlInput);
|
|
31
|
+
editor.chain().focus().extendMarkRange("link").setLink({ href }).run();
|
|
32
|
+
setOpen(false);
|
|
33
|
+
}
|
|
34
|
+
function handleUnlink() {
|
|
35
|
+
editor?.chain().focus().extendMarkRange("link").unsetLink().run();
|
|
36
|
+
}
|
|
37
|
+
return /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-0.5", children: [
|
|
38
|
+
/* @__PURE__ */ jsxs(Popover, { open, onOpenChange: handleOpenChange, children: [
|
|
39
|
+
/* @__PURE__ */ jsx(Tooltip, { content: "Insert link", side: "bottom", delayDuration: 400, children: /* @__PURE__ */ jsx(PopoverTrigger, { asChild: true, children: /* @__PURE__ */ jsx(
|
|
40
|
+
Button,
|
|
41
|
+
{
|
|
42
|
+
variant: "ghost",
|
|
43
|
+
size: "icon-sm",
|
|
44
|
+
"aria-label": "Insert link",
|
|
45
|
+
"aria-pressed": isActive,
|
|
46
|
+
disabled: isDisabled,
|
|
47
|
+
className: "h-7 w-7 rounded-[2px] data-[state=open]:bg-accent data-[state=open]:text-accent-foreground",
|
|
48
|
+
children: /* @__PURE__ */ jsx(Link2, { size: 14 })
|
|
49
|
+
}
|
|
50
|
+
) }) }),
|
|
51
|
+
/* @__PURE__ */ jsx(PopoverContent, { className: "w-80 max-w-[calc(100vw-1.5rem)] p-3", side: "bottom", align: "start", children: /* @__PURE__ */ jsxs("form", { onSubmit: handleSubmit, className: "flex flex-col gap-2", children: [
|
|
52
|
+
/* @__PURE__ */ jsx("p", { className: "text-body-2-semibold text-foreground mb-1", children: "Insert link" }),
|
|
53
|
+
/* @__PURE__ */ jsx(
|
|
54
|
+
Input,
|
|
55
|
+
{
|
|
56
|
+
placeholder: "https://example.com",
|
|
57
|
+
value: urlInput,
|
|
58
|
+
onChange: (e) => setUrlInput(e.target.value),
|
|
59
|
+
autoFocus: true,
|
|
60
|
+
size: "sm"
|
|
61
|
+
}
|
|
62
|
+
),
|
|
63
|
+
/* @__PURE__ */ jsxs("div", { className: "flex gap-2 justify-end", children: [
|
|
64
|
+
/* @__PURE__ */ jsx(
|
|
65
|
+
Button,
|
|
66
|
+
{
|
|
67
|
+
type: "button",
|
|
68
|
+
variant: "ghost",
|
|
69
|
+
size: "sm",
|
|
70
|
+
onClick: () => setOpen(false),
|
|
71
|
+
children: "Cancel"
|
|
72
|
+
}
|
|
73
|
+
),
|
|
74
|
+
/* @__PURE__ */ jsx(Button, { type: "submit", size: "sm", disabled: !urlInput.trim(), children: "Apply" })
|
|
75
|
+
] })
|
|
76
|
+
] }) })
|
|
77
|
+
] }),
|
|
78
|
+
/* @__PURE__ */ jsx(Tooltip, { content: "Remove link", side: "bottom", delayDuration: 400, children: /* @__PURE__ */ jsx(
|
|
79
|
+
Button,
|
|
80
|
+
{
|
|
81
|
+
variant: "ghost",
|
|
82
|
+
size: "icon-sm",
|
|
83
|
+
"aria-label": "Remove link",
|
|
84
|
+
disabled: isDisabled || !isActive,
|
|
85
|
+
onClick: handleUnlink,
|
|
86
|
+
className: "h-7 w-7 rounded-[2px]",
|
|
87
|
+
children: /* @__PURE__ */ jsx(Unlink2, { size: 14 })
|
|
88
|
+
}
|
|
89
|
+
) })
|
|
90
|
+
] });
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export { RteLinkPopover };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"RteToolbar.d.ts","sourceRoot":"","sources":["../../../../src/components/RichTextEditor/subcomponents/RteToolbar.tsx"],"names":[],"mappings":"AAEA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAC;AA+B/B,UAAU,eAAe;IACvB,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAoBD,wBAAgB,UAAU,CAAC,EAAE,SAAS,EAAE,EAAE,eAAe,4BAwKxD"}
|
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsxs, jsx } from 'react/jsx-runtime';
|
|
3
|
+
import { cn } from '@timmbr/utils';
|
|
4
|
+
import { Undo2, Redo2, Pilcrow, Heading2, Heading3, Bold, Italic, Underline, Strikethrough, List, ListOrdered, Quote, AlignLeft, AlignCenter, AlignRight, AlignJustify, Minus, RemoveFormatting } from '@timmbr/icons';
|
|
5
|
+
import { useRteContext } from '../RichTextEditor.context.js';
|
|
6
|
+
import { toolbarVariants } from '../RichTextEditor.styles.js';
|
|
7
|
+
import { RteToolbarButton } from './RteToolbarButton.js';
|
|
8
|
+
import { RteToolbarGroup } from './RteToolbarGroup.js';
|
|
9
|
+
import { RteLinkPopover } from './RteLinkPopover.js';
|
|
10
|
+
import { RteEmojiPicker } from './RteEmojiPicker.js';
|
|
11
|
+
|
|
12
|
+
const ICON_SIZE = 14;
|
|
13
|
+
function handleToolbarKeyDown(e) {
|
|
14
|
+
if (!["ArrowRight", "ArrowLeft", "Home", "End"].includes(e.key)) return;
|
|
15
|
+
const toolbar = e.currentTarget;
|
|
16
|
+
const focusable = Array.from(
|
|
17
|
+
toolbar.querySelectorAll("button:not([disabled])")
|
|
18
|
+
);
|
|
19
|
+
const idx = focusable.indexOf(document.activeElement);
|
|
20
|
+
if (idx === -1) return;
|
|
21
|
+
e.preventDefault();
|
|
22
|
+
const len = focusable.length;
|
|
23
|
+
const next = e.key === "ArrowRight" ? focusable[(idx + 1) % len] : e.key === "ArrowLeft" ? focusable[(idx - 1 + len) % len] : e.key === "Home" ? focusable[0] : focusable[len - 1];
|
|
24
|
+
next?.focus();
|
|
25
|
+
}
|
|
26
|
+
function RteToolbar({ className }) {
|
|
27
|
+
const { editor, disabled, readOnly } = useRteContext();
|
|
28
|
+
if (!editor) return null;
|
|
29
|
+
const isDisabled = disabled || readOnly;
|
|
30
|
+
return /* @__PURE__ */ jsxs(
|
|
31
|
+
"div",
|
|
32
|
+
{
|
|
33
|
+
"data-slot": "rte-toolbar",
|
|
34
|
+
role: "toolbar",
|
|
35
|
+
"aria-label": "Text formatting",
|
|
36
|
+
onKeyDown: handleToolbarKeyDown,
|
|
37
|
+
className: cn(toolbarVariants(), className),
|
|
38
|
+
children: [
|
|
39
|
+
/* @__PURE__ */ jsxs("div", { className: "flex items-center gap-0.5", children: [
|
|
40
|
+
/* @__PURE__ */ jsx(
|
|
41
|
+
RteToolbarButton,
|
|
42
|
+
{
|
|
43
|
+
label: "Undo",
|
|
44
|
+
icon: /* @__PURE__ */ jsx(Undo2, { size: ICON_SIZE }),
|
|
45
|
+
onClick: () => editor.chain().focus().undo().run(),
|
|
46
|
+
disabled: isDisabled || !editor.can().undo()
|
|
47
|
+
}
|
|
48
|
+
),
|
|
49
|
+
/* @__PURE__ */ jsx(
|
|
50
|
+
RteToolbarButton,
|
|
51
|
+
{
|
|
52
|
+
label: "Redo",
|
|
53
|
+
icon: /* @__PURE__ */ jsx(Redo2, { size: ICON_SIZE }),
|
|
54
|
+
onClick: () => editor.chain().focus().redo().run(),
|
|
55
|
+
disabled: isDisabled || !editor.can().redo()
|
|
56
|
+
}
|
|
57
|
+
)
|
|
58
|
+
] }),
|
|
59
|
+
/* @__PURE__ */ jsxs(RteToolbarGroup, { children: [
|
|
60
|
+
/* @__PURE__ */ jsx(
|
|
61
|
+
RteToolbarButton,
|
|
62
|
+
{
|
|
63
|
+
label: "Paragraph",
|
|
64
|
+
icon: /* @__PURE__ */ jsx(Pilcrow, { size: ICON_SIZE }),
|
|
65
|
+
onClick: () => editor.chain().focus().setParagraph().run(),
|
|
66
|
+
isActive: editor.isActive("paragraph") && !editor.isActive("heading"),
|
|
67
|
+
disabled: isDisabled
|
|
68
|
+
}
|
|
69
|
+
),
|
|
70
|
+
/* @__PURE__ */ jsx(
|
|
71
|
+
RteToolbarButton,
|
|
72
|
+
{
|
|
73
|
+
label: "Heading 2",
|
|
74
|
+
icon: /* @__PURE__ */ jsx(Heading2, { size: ICON_SIZE }),
|
|
75
|
+
onClick: () => editor.chain().focus().toggleHeading({ level: 2 }).run(),
|
|
76
|
+
isActive: editor.isActive("heading", { level: 2 }),
|
|
77
|
+
disabled: isDisabled
|
|
78
|
+
}
|
|
79
|
+
),
|
|
80
|
+
/* @__PURE__ */ jsx(
|
|
81
|
+
RteToolbarButton,
|
|
82
|
+
{
|
|
83
|
+
label: "Heading 3",
|
|
84
|
+
icon: /* @__PURE__ */ jsx(Heading3, { size: ICON_SIZE }),
|
|
85
|
+
onClick: () => editor.chain().focus().toggleHeading({ level: 3 }).run(),
|
|
86
|
+
isActive: editor.isActive("heading", { level: 3 }),
|
|
87
|
+
disabled: isDisabled
|
|
88
|
+
}
|
|
89
|
+
)
|
|
90
|
+
] }),
|
|
91
|
+
/* @__PURE__ */ jsxs(RteToolbarGroup, { children: [
|
|
92
|
+
/* @__PURE__ */ jsx(
|
|
93
|
+
RteToolbarButton,
|
|
94
|
+
{
|
|
95
|
+
label: "Bold",
|
|
96
|
+
icon: /* @__PURE__ */ jsx(Bold, { size: ICON_SIZE }),
|
|
97
|
+
onClick: () => editor.chain().focus().toggleBold().run(),
|
|
98
|
+
isActive: editor.isActive("bold"),
|
|
99
|
+
disabled: isDisabled
|
|
100
|
+
}
|
|
101
|
+
),
|
|
102
|
+
/* @__PURE__ */ jsx(
|
|
103
|
+
RteToolbarButton,
|
|
104
|
+
{
|
|
105
|
+
label: "Italic",
|
|
106
|
+
icon: /* @__PURE__ */ jsx(Italic, { size: ICON_SIZE }),
|
|
107
|
+
onClick: () => editor.chain().focus().toggleItalic().run(),
|
|
108
|
+
isActive: editor.isActive("italic"),
|
|
109
|
+
disabled: isDisabled
|
|
110
|
+
}
|
|
111
|
+
),
|
|
112
|
+
/* @__PURE__ */ jsx(
|
|
113
|
+
RteToolbarButton,
|
|
114
|
+
{
|
|
115
|
+
label: "Underline",
|
|
116
|
+
icon: /* @__PURE__ */ jsx(Underline, { size: ICON_SIZE }),
|
|
117
|
+
onClick: () => editor.chain().focus().toggleUnderline().run(),
|
|
118
|
+
isActive: editor.isActive("underline"),
|
|
119
|
+
disabled: isDisabled
|
|
120
|
+
}
|
|
121
|
+
),
|
|
122
|
+
/* @__PURE__ */ jsx(
|
|
123
|
+
RteToolbarButton,
|
|
124
|
+
{
|
|
125
|
+
label: "Strikethrough",
|
|
126
|
+
icon: /* @__PURE__ */ jsx(Strikethrough, { size: ICON_SIZE }),
|
|
127
|
+
onClick: () => editor.chain().focus().toggleStrike().run(),
|
|
128
|
+
isActive: editor.isActive("strike"),
|
|
129
|
+
disabled: isDisabled
|
|
130
|
+
}
|
|
131
|
+
)
|
|
132
|
+
] }),
|
|
133
|
+
/* @__PURE__ */ jsxs(RteToolbarGroup, { children: [
|
|
134
|
+
/* @__PURE__ */ jsx(
|
|
135
|
+
RteToolbarButton,
|
|
136
|
+
{
|
|
137
|
+
label: "Bullet list",
|
|
138
|
+
icon: /* @__PURE__ */ jsx(List, { size: ICON_SIZE }),
|
|
139
|
+
onClick: () => editor.chain().focus().toggleBulletList().run(),
|
|
140
|
+
isActive: editor.isActive("bulletList"),
|
|
141
|
+
disabled: isDisabled
|
|
142
|
+
}
|
|
143
|
+
),
|
|
144
|
+
/* @__PURE__ */ jsx(
|
|
145
|
+
RteToolbarButton,
|
|
146
|
+
{
|
|
147
|
+
label: "Ordered list",
|
|
148
|
+
icon: /* @__PURE__ */ jsx(ListOrdered, { size: ICON_SIZE }),
|
|
149
|
+
onClick: () => editor.chain().focus().toggleOrderedList().run(),
|
|
150
|
+
isActive: editor.isActive("orderedList"),
|
|
151
|
+
disabled: isDisabled
|
|
152
|
+
}
|
|
153
|
+
),
|
|
154
|
+
/* @__PURE__ */ jsx(
|
|
155
|
+
RteToolbarButton,
|
|
156
|
+
{
|
|
157
|
+
label: "Blockquote",
|
|
158
|
+
icon: /* @__PURE__ */ jsx(Quote, { size: ICON_SIZE }),
|
|
159
|
+
onClick: () => editor.chain().focus().toggleBlockquote().run(),
|
|
160
|
+
isActive: editor.isActive("blockquote"),
|
|
161
|
+
disabled: isDisabled
|
|
162
|
+
}
|
|
163
|
+
)
|
|
164
|
+
] }),
|
|
165
|
+
/* @__PURE__ */ jsxs(RteToolbarGroup, { children: [
|
|
166
|
+
/* @__PURE__ */ jsx(
|
|
167
|
+
RteToolbarButton,
|
|
168
|
+
{
|
|
169
|
+
label: "Align left",
|
|
170
|
+
icon: /* @__PURE__ */ jsx(AlignLeft, { size: ICON_SIZE }),
|
|
171
|
+
onClick: () => editor.chain().focus().setTextAlign("left").run(),
|
|
172
|
+
isActive: editor.isActive({ textAlign: "left" }),
|
|
173
|
+
disabled: isDisabled
|
|
174
|
+
}
|
|
175
|
+
),
|
|
176
|
+
/* @__PURE__ */ jsx(
|
|
177
|
+
RteToolbarButton,
|
|
178
|
+
{
|
|
179
|
+
label: "Align center",
|
|
180
|
+
icon: /* @__PURE__ */ jsx(AlignCenter, { size: ICON_SIZE }),
|
|
181
|
+
onClick: () => editor.chain().focus().setTextAlign("center").run(),
|
|
182
|
+
isActive: editor.isActive({ textAlign: "center" }),
|
|
183
|
+
disabled: isDisabled
|
|
184
|
+
}
|
|
185
|
+
),
|
|
186
|
+
/* @__PURE__ */ jsx(
|
|
187
|
+
RteToolbarButton,
|
|
188
|
+
{
|
|
189
|
+
label: "Align right",
|
|
190
|
+
icon: /* @__PURE__ */ jsx(AlignRight, { size: ICON_SIZE }),
|
|
191
|
+
onClick: () => editor.chain().focus().setTextAlign("right").run(),
|
|
192
|
+
isActive: editor.isActive({ textAlign: "right" }),
|
|
193
|
+
disabled: isDisabled
|
|
194
|
+
}
|
|
195
|
+
),
|
|
196
|
+
/* @__PURE__ */ jsx(
|
|
197
|
+
RteToolbarButton,
|
|
198
|
+
{
|
|
199
|
+
label: "Justify",
|
|
200
|
+
icon: /* @__PURE__ */ jsx(AlignJustify, { size: ICON_SIZE }),
|
|
201
|
+
onClick: () => editor.chain().focus().setTextAlign("justify").run(),
|
|
202
|
+
isActive: editor.isActive({ textAlign: "justify" }),
|
|
203
|
+
disabled: isDisabled
|
|
204
|
+
}
|
|
205
|
+
)
|
|
206
|
+
] }),
|
|
207
|
+
/* @__PURE__ */ jsxs(RteToolbarGroup, { children: [
|
|
208
|
+
/* @__PURE__ */ jsx(RteLinkPopover, {}),
|
|
209
|
+
/* @__PURE__ */ jsx(RteEmojiPicker, {}),
|
|
210
|
+
/* @__PURE__ */ jsx(
|
|
211
|
+
RteToolbarButton,
|
|
212
|
+
{
|
|
213
|
+
label: "Horizontal rule",
|
|
214
|
+
icon: /* @__PURE__ */ jsx(Minus, { size: ICON_SIZE }),
|
|
215
|
+
onClick: () => editor.chain().focus().setHorizontalRule().run(),
|
|
216
|
+
disabled: isDisabled
|
|
217
|
+
}
|
|
218
|
+
)
|
|
219
|
+
] }),
|
|
220
|
+
/* @__PURE__ */ jsx(RteToolbarGroup, { children: /* @__PURE__ */ jsx(
|
|
221
|
+
RteToolbarButton,
|
|
222
|
+
{
|
|
223
|
+
label: "Clear formatting",
|
|
224
|
+
icon: /* @__PURE__ */ jsx(RemoveFormatting, { size: ICON_SIZE }),
|
|
225
|
+
onClick: () => editor.chain().focus().clearNodes().unsetAllMarks().run(),
|
|
226
|
+
disabled: isDisabled
|
|
227
|
+
}
|
|
228
|
+
) })
|
|
229
|
+
]
|
|
230
|
+
}
|
|
231
|
+
);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
export { RteToolbar };
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
import type { RteToolbarButtonProps } from '../RichTextEditor.types';
|
|
3
|
+
export declare function RteToolbarButton({ label, icon, onClick, isActive, disabled, className, }: RteToolbarButtonProps): React.JSX.Element;
|
|
4
|
+
//# sourceMappingURL=RteToolbarButton.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"RteToolbarButton.d.ts","sourceRoot":"","sources":["../../../../src/components/RichTextEditor/subcomponents/RteToolbarButton.tsx"],"names":[],"mappings":"AAEA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAC;AAK/B,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,yBAAyB,CAAC;AAErE,wBAAgB,gBAAgB,CAAC,EAC/B,KAAK,EACL,IAAI,EACJ,OAAO,EACP,QAAQ,EACR,QAAgB,EAChB,SAAS,GACV,EAAE,qBAAqB,qBAuBvB"}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx } from 'react/jsx-runtime';
|
|
3
|
+
import { cn } from '@timmbr/utils';
|
|
4
|
+
import { useRteContext } from '../RichTextEditor.context.js';
|
|
5
|
+
import { Tooltip } from '../../Tooltip/Tooltip.js';
|
|
6
|
+
import { Button } from '../../Button/Button.js';
|
|
7
|
+
|
|
8
|
+
function RteToolbarButton({
|
|
9
|
+
label,
|
|
10
|
+
icon,
|
|
11
|
+
onClick,
|
|
12
|
+
isActive,
|
|
13
|
+
disabled = false,
|
|
14
|
+
className
|
|
15
|
+
}) {
|
|
16
|
+
const { shouldAnimate } = useRteContext();
|
|
17
|
+
return /* @__PURE__ */ jsx(Tooltip, { content: label, side: "bottom", delayDuration: 400, children: /* @__PURE__ */ jsx(
|
|
18
|
+
Button,
|
|
19
|
+
{
|
|
20
|
+
variant: "ghost",
|
|
21
|
+
size: "icon-sm",
|
|
22
|
+
"aria-label": label,
|
|
23
|
+
"aria-pressed": isActive !== void 0 ? isActive : void 0,
|
|
24
|
+
disabled,
|
|
25
|
+
onClick,
|
|
26
|
+
className: cn(
|
|
27
|
+
"h-7 w-7 rounded-[2px]",
|
|
28
|
+
isActive === true && "bg-accent text-accent-foreground",
|
|
29
|
+
shouldAnimate ? "active:scale-[0.92]" : "active:scale-100",
|
|
30
|
+
className
|
|
31
|
+
),
|
|
32
|
+
children: icon
|
|
33
|
+
}
|
|
34
|
+
) });
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export { RteToolbarButton };
|