@timmbr/ui 1.14.0 → 1.15.1

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.
Files changed (45) hide show
  1. package/dist/components/RichTextEditor/RichTextContent.d.ts +8 -0
  2. package/dist/components/RichTextEditor/RichTextContent.d.ts.map +1 -0
  3. package/dist/components/RichTextEditor/RichTextContent.js +18 -0
  4. package/dist/components/RichTextEditor/RichTextEditor.context.d.ts +6 -0
  5. package/dist/components/RichTextEditor/RichTextEditor.context.d.ts.map +1 -0
  6. package/dist/components/RichTextEditor/RichTextEditor.context.js +14 -0
  7. package/dist/components/RichTextEditor/RichTextEditor.d.ts +4 -0
  8. package/dist/components/RichTextEditor/RichTextEditor.d.ts.map +1 -0
  9. package/dist/components/RichTextEditor/RichTextEditor.helpers.d.ts +28 -0
  10. package/dist/components/RichTextEditor/RichTextEditor.helpers.d.ts.map +1 -0
  11. package/dist/components/RichTextEditor/RichTextEditor.helpers.js +52 -0
  12. package/dist/components/RichTextEditor/RichTextEditor.js +156 -0
  13. package/dist/components/RichTextEditor/RichTextEditor.styles.d.ts +10 -0
  14. package/dist/components/RichTextEditor/RichTextEditor.styles.d.ts.map +1 -0
  15. package/dist/components/RichTextEditor/RichTextEditor.styles.js +95 -0
  16. package/dist/components/RichTextEditor/RichTextEditor.types.d.ts +73 -0
  17. package/dist/components/RichTextEditor/RichTextEditor.types.d.ts.map +1 -0
  18. package/dist/components/RichTextEditor/RichTextJsonContent.d.ts +9 -0
  19. package/dist/components/RichTextEditor/RichTextJsonContent.d.ts.map +1 -0
  20. package/dist/components/RichTextEditor/RichTextJsonContent.js +27 -0
  21. package/dist/components/RichTextEditor/index.d.ts +14 -0
  22. package/dist/components/RichTextEditor/index.d.ts.map +1 -0
  23. package/dist/components/RichTextEditor/subcomponents/RteCharCount.d.ts +4 -0
  24. package/dist/components/RichTextEditor/subcomponents/RteCharCount.d.ts.map +1 -0
  25. package/dist/components/RichTextEditor/subcomponents/RteCharCount.js +37 -0
  26. package/dist/components/RichTextEditor/subcomponents/RteEmojiPicker.d.ts +3 -0
  27. package/dist/components/RichTextEditor/subcomponents/RteEmojiPicker.d.ts.map +1 -0
  28. package/dist/components/RichTextEditor/subcomponents/RteEmojiPicker.js +274 -0
  29. package/dist/components/RichTextEditor/subcomponents/RteLinkPopover.d.ts +3 -0
  30. package/dist/components/RichTextEditor/subcomponents/RteLinkPopover.d.ts.map +1 -0
  31. package/dist/components/RichTextEditor/subcomponents/RteLinkPopover.js +93 -0
  32. package/dist/components/RichTextEditor/subcomponents/RteToolbar.d.ts +7 -0
  33. package/dist/components/RichTextEditor/subcomponents/RteToolbar.d.ts.map +1 -0
  34. package/dist/components/RichTextEditor/subcomponents/RteToolbar.js +234 -0
  35. package/dist/components/RichTextEditor/subcomponents/RteToolbarButton.d.ts +4 -0
  36. package/dist/components/RichTextEditor/subcomponents/RteToolbarButton.d.ts.map +1 -0
  37. package/dist/components/RichTextEditor/subcomponents/RteToolbarButton.js +37 -0
  38. package/dist/components/RichTextEditor/subcomponents/RteToolbarGroup.d.ts +4 -0
  39. package/dist/components/RichTextEditor/subcomponents/RteToolbarGroup.d.ts.map +1 -0
  40. package/dist/components/RichTextEditor/subcomponents/RteToolbarGroup.js +13 -0
  41. package/dist/components/SideBarNavigation/SideBarNavigation.js +1 -4
  42. package/dist/index.d.ts +1 -0
  43. package/dist/index.d.ts.map +1 -1
  44. package/dist/index.js +12 -0
  45. package/package.json +10 -2
@@ -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,3 @@
1
+ import * as React from 'react';
2
+ export declare function RteLinkPopover(): React.JSX.Element;
3
+ //# sourceMappingURL=RteLinkPopover.d.ts.map
@@ -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,7 @@
1
+ import * as React from 'react';
2
+ interface RteToolbarProps {
3
+ className?: string;
4
+ }
5
+ export declare function RteToolbar({ className }: RteToolbarProps): React.JSX.Element | null;
6
+ export {};
7
+ //# sourceMappingURL=RteToolbar.d.ts.map
@@ -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 };
@@ -0,0 +1,4 @@
1
+ import * as React from 'react';
2
+ import type { RteToolbarGroupProps } from '../RichTextEditor.types';
3
+ export declare function RteToolbarGroup({ children, showLeadingDivider }: RteToolbarGroupProps): React.JSX.Element;
4
+ //# sourceMappingURL=RteToolbarGroup.d.ts.map