@lovett/ui 0.0.11 → 0.2.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.
Files changed (142) hide show
  1. package/dist/chunk-RBYWGBQ2.js +2752 -0
  2. package/dist/chunk-RBYWGBQ2.js.map +1 -0
  3. package/dist/index.d.ts +5574 -57
  4. package/dist/index.js +21650 -11206
  5. package/dist/index.js.map +1 -1
  6. package/dist/rich-composer-impl-5NO443A6.js +1859 -0
  7. package/dist/rich-composer-impl-5NO443A6.js.map +1 -0
  8. package/dist/styles.css +1570 -0
  9. package/dist/tokens.css +112 -0
  10. package/package.json +8 -1
  11. package/src/__tests__/avatar.test.tsx +272 -0
  12. package/src/__tests__/bar-chart.test.tsx +809 -0
  13. package/src/__tests__/board.test.tsx +420 -0
  14. package/src/__tests__/chart-math.test.ts +922 -0
  15. package/src/__tests__/chart-series.test.ts +339 -0
  16. package/src/__tests__/code-block.test.tsx +134 -0
  17. package/src/__tests__/display-popover.test.tsx +195 -0
  18. package/src/__tests__/display-store.test.tsx +307 -0
  19. package/src/__tests__/donut-chart.test.tsx +397 -0
  20. package/src/__tests__/dropdown-menu.test.tsx +156 -2
  21. package/src/__tests__/filter-menu.test.tsx +175 -0
  22. package/src/__tests__/gauge-ring.test.tsx +233 -0
  23. package/src/__tests__/line-chart.test.tsx +612 -0
  24. package/src/__tests__/ranked-bars.test.tsx +343 -0
  25. package/src/__tests__/remark-underline.test.ts +194 -0
  26. package/src/__tests__/sortable.test.tsx +394 -0
  27. package/src/__tests__/sparkline.test.tsx +368 -0
  28. package/src/__tests__/stat-layer.test.tsx +271 -0
  29. package/src/__tests__/stat-strip.test.tsx +175 -0
  30. package/src/__tests__/status.test.tsx +293 -0
  31. package/src/__tests__/tabs.test.tsx +303 -0
  32. package/src/__tests__/token-shape.test.ts +132 -2
  33. package/src/avatar.tsx +352 -0
  34. package/src/bar-chart.tsx +1214 -0
  35. package/src/board.tsx +658 -0
  36. package/src/chart-frame.tsx +960 -0
  37. package/src/chart-legend.tsx +304 -0
  38. package/src/chart-tooltip.tsx +267 -0
  39. package/src/code-block.tsx +62 -8
  40. package/src/delta-chip.tsx +263 -0
  41. package/src/detail/__tests__/activity-pane.test.tsx +369 -0
  42. package/src/detail/__tests__/detail-chrome.test.tsx +134 -0
  43. package/src/detail/__tests__/detail-surface.test.tsx +529 -0
  44. package/src/detail/__tests__/field-row.test.tsx +357 -0
  45. package/src/detail/activity-pane.tsx +611 -0
  46. package/src/detail/calendar.tsx +355 -0
  47. package/src/detail/detail-divider.tsx +261 -0
  48. package/src/detail/detail-header.tsx +287 -0
  49. package/src/detail/detail-menu.tsx +254 -0
  50. package/src/detail/detail-surface.tsx +1110 -0
  51. package/src/detail/field-list.tsx +196 -0
  52. package/src/detail/field-row.tsx +1131 -0
  53. package/src/detail/index.ts +58 -0
  54. package/src/detail/segmented-choice.tsx +94 -0
  55. package/src/detail/types.ts +129 -0
  56. package/src/display-popover.tsx +487 -0
  57. package/src/display-store.tsx +301 -0
  58. package/src/donut-chart.tsx +988 -0
  59. package/src/dropdown-menu.tsx +290 -19
  60. package/src/filter-core/EXPORTS.md +85 -0
  61. package/src/filter-core/__tests__/columns.test.ts +159 -0
  62. package/src/filter-core/__tests__/faceting.test.ts +193 -0
  63. package/src/filter-core/__tests__/filter-fns.test.ts +519 -0
  64. package/src/filter-core/__tests__/operators.test.ts +235 -0
  65. package/src/filter-core/__tests__/state.test.ts +268 -0
  66. package/src/filter-core/__tests__/url.test.ts +350 -0
  67. package/src/filter-core/columns.ts +134 -0
  68. package/src/filter-core/date-utils.ts +38 -0
  69. package/src/filter-core/examples/task-filter-columns.ts +121 -0
  70. package/src/filter-core/faceting.ts +120 -0
  71. package/src/filter-core/filter-fns.ts +335 -0
  72. package/src/filter-core/index.ts +105 -0
  73. package/src/filter-core/operators.ts +433 -0
  74. package/src/filter-core/state.ts +280 -0
  75. package/src/filter-core/types.ts +247 -0
  76. package/src/filter-core/url.ts +261 -0
  77. package/src/filter-dropdown.tsx +12 -0
  78. package/src/filter-menu.tsx +649 -0
  79. package/src/floating-drawer.tsx +19 -1
  80. package/src/gauge-ring.tsx +435 -0
  81. package/src/hue.ts +52 -0
  82. package/src/index.ts +303 -0
  83. package/src/kbd.tsx +27 -4
  84. package/src/lib/chart.ts +866 -0
  85. package/src/lib/focus.ts +43 -1
  86. package/src/lib/layer-stack.ts +30 -3
  87. package/src/lib/remark-underline.ts +443 -0
  88. package/src/lib/series.ts +169 -0
  89. package/src/line-chart.tsx +1176 -0
  90. package/src/markdown.tsx +26 -7
  91. package/src/modal.tsx +42 -18
  92. package/src/progress-ledger.tsx +304 -0
  93. package/src/ranked-bars.tsx +386 -0
  94. package/src/segmented-pill.tsx +32 -9
  95. package/src/sortable.tsx +520 -1
  96. package/src/sparkline.tsx +416 -0
  97. package/src/stat-card.tsx +376 -0
  98. package/src/stat-strip.tsx +327 -0
  99. package/src/status.tsx +215 -0
  100. package/src/styles.css +1570 -0
  101. package/src/tabs.tsx +206 -25
  102. package/src/task-card.tsx +610 -0
  103. package/src/thread/__tests__/comment-body-hostile.test.tsx +331 -0
  104. package/src/thread/__tests__/comment-tree.test.ts +151 -0
  105. package/src/thread/__tests__/emoji.test.ts +187 -0
  106. package/src/thread/__tests__/fixtures/thread-fixture.ts +252 -0
  107. package/src/thread/__tests__/link-preview-source.test.ts +120 -0
  108. package/src/thread/__tests__/link-preview.test.tsx +600 -0
  109. package/src/thread/__tests__/markdown-format.test.ts +82 -0
  110. package/src/thread/__tests__/markdown-spec.test.ts +469 -0
  111. package/src/thread/__tests__/relative-time.test.ts +71 -0
  112. package/src/thread/__tests__/rich-composer.test.tsx +799 -0
  113. package/src/thread/__tests__/scroll-caret.test.ts +58 -0
  114. package/src/thread/__tests__/suggestion-list.test.tsx +648 -0
  115. package/src/thread/__tests__/thread-scroll-ownership.test.tsx +88 -0
  116. package/src/thread/__tests__/thread.test.tsx +742 -0
  117. package/src/thread/__tests__/use-attachments.test.tsx +679 -0
  118. package/src/thread/actions.tsx +196 -0
  119. package/src/thread/attachments.tsx +1071 -0
  120. package/src/thread/comment-body.tsx +148 -0
  121. package/src/thread/comment-tree.ts +182 -0
  122. package/src/thread/comment.tsx +967 -0
  123. package/src/thread/composer-footer.tsx +125 -0
  124. package/src/thread/composer.tsx +319 -0
  125. package/src/thread/emoji.ts +283 -0
  126. package/src/thread/index.ts +153 -0
  127. package/src/thread/link-preview.tsx +341 -0
  128. package/src/thread/markdown-format.ts +155 -0
  129. package/src/thread/markdown-spec.ts +754 -0
  130. package/src/thread/rail.tsx +372 -0
  131. package/src/thread/reactions.tsx +415 -0
  132. package/src/thread/relative-time.tsx +94 -0
  133. package/src/thread/rich-composer-impl.tsx +1601 -0
  134. package/src/thread/rich-composer.tsx +195 -0
  135. package/src/thread/scroll-caret.ts +37 -0
  136. package/src/thread/suggestion-list.tsx +182 -0
  137. package/src/thread/thread.tsx +718 -0
  138. package/src/thread/types.ts +232 -0
  139. package/src/thread/use-attachments.ts +598 -0
  140. package/src/thread/use-now.ts +73 -0
  141. package/src/thread/use-thread.ts +316 -0
  142. package/src/tokens.css +112 -0
@@ -0,0 +1,1859 @@
1
+ import { parseMentionHref, ComposerNotice, ThreadComposer, useAttachments, AttachmentDropZone, Button, AttachmentTray, ComposerFooter, Kbd, AttachmentButton, ATTACHMENT_ACCEPT, Popover, GifPicker, cn } from './chunk-RBYWGBQ2.js';
2
+ import { useRef, useState, useCallback, useEffect, useId, useLayoutEffect } from 'react';
3
+ import { createPortal } from 'react-dom';
4
+ import { Bold, Italic, Underline, Strikethrough, Code, Link, CornerDownRight, Check, Unlink, X, Images, Heading1, Heading2, Heading3, List, ListOrdered, TextQuote, SquareCode, Minus } from 'lucide-react';
5
+ import { Mark, markInputRule, mergeAttributes, Extension, InputRule, isTextSelection, Editor } from '@tiptap/core';
6
+ import { Placeholder } from '@tiptap/extension-placeholder';
7
+ import { Slice, Fragment, Node as Node$1 } from '@tiptap/pm/model';
8
+ import { PluginKey } from '@tiptap/pm/state';
9
+ import { useEditor, useEditorState, EditorContent } from '@tiptap/react';
10
+ import { BubbleMenu } from '@tiptap/react/menus';
11
+ import { Mention } from '@tiptap/extension-mention';
12
+ import { Markdown } from '@tiptap/markdown';
13
+ import { StarterKit } from '@tiptap/starter-kit';
14
+ import { jsxs, jsx, Fragment as Fragment$1 } from 'react/jsx-runtime';
15
+
16
+ // src/thread/scroll-caret.ts
17
+ function caretScrollDelta(box, caret, pad) {
18
+ if (caret.top < box.top + pad) return caret.top - (box.top + pad);
19
+ if (caret.bottom > box.bottom - pad) return caret.bottom - (box.bottom - pad);
20
+ return 0;
21
+ }
22
+
23
+ // src/thread/emoji.ts
24
+ var EMOJI_ROWS = [
25
+ /* ── Faces ───────────────────────────────────────────────────────────── */
26
+ ["grinning", "\u{1F600}", "", "smile|happy|grin"],
27
+ ["smiley", "\u{1F603}", "", "happy|joy|cheerful"],
28
+ ["smile", "\u{1F604}", "", "happy|joy|laugh"],
29
+ ["grin", "\u{1F601}", "", "happy|beam|pleased"],
30
+ ["laughing", "\u{1F606}", "satisfied", "haha|lol|funny"],
31
+ ["sweat_smile", "\u{1F605}", "", "phew|relief|nervous"],
32
+ ["joy", "\u{1F602}", "lol", "laugh|tears|haha|funny"],
33
+ ["rofl", "\u{1F923}", "rolling_on_the_floor_laughing", "lol|haha|hilarious"],
34
+ ["slightly_smiling_face", "\u{1F642}", "slight_smile", "smile|fine|ok"],
35
+ ["upside_down_face", "\u{1F643}", "upside_down", "irony|sarcasm|silly"],
36
+ ["wink", "\u{1F609}", "", "flirt|joke|kidding"],
37
+ ["blush", "\u{1F60A}", "", "happy|shy|smile"],
38
+ ["innocent", "\u{1F607}", "", "angel|halo|good"],
39
+ ["heart_eyes", "\u{1F60D}", "", "love|adore|crush"],
40
+ ["star_struck", "\u{1F929}", "", "amazed|wow|excited"],
41
+ ["yum", "\u{1F60B}", "", "delicious|tasty|tongue"],
42
+ ["raised_eyebrow", "\u{1F928}", "face_with_raised_eyebrow", "skeptical|doubt|suspicious"],
43
+ ["neutral_face", "\u{1F610}", "", "meh|blank|straight"],
44
+ ["expressionless", "\u{1F611}", "", "blank|meh|deadpan"],
45
+ ["smirk", "\u{1F60F}", "", "smug|sly|knowing"],
46
+ ["unamused", "\u{1F612}", "", "meh|annoyed|unimpressed"],
47
+ ["roll_eyes", "\u{1F644}", "face_with_rolling_eyes|side_eye|sideeye", "whatever|annoyed|sarcasm|really"],
48
+ ["grimacing", "\u{1F62C}", "", "awkward|eek|yikes"],
49
+ ["relieved", "\u{1F60C}", "", "phew|calm|content"],
50
+ ["pensive", "\u{1F614}", "", "sad|thoughtful|wistful"],
51
+ ["sleeping", "\u{1F634}", "", "zzz|asleep|bored"],
52
+ ["exploding_head", "\u{1F92F}", "mind_blown", "mindblown|whoa|shocked"],
53
+ ["partying_face", "\u{1F973}", "", "birthday|hooray|horn"],
54
+ ["sunglasses", "\u{1F60E}", "", "cool|deal|smooth"],
55
+ ["nerd_face", "\u{1F913}", "nerd", "geek|glasses|smart"],
56
+ ["monocle_face", "\u{1F9D0}", "monocle", "inspect|scrutinize|examine"],
57
+ ["confused", "\u{1F615}", "", "unsure|puzzled"],
58
+ ["slightly_frowning_face", "\u{1F641}", "slight_frown", "sad|frown"],
59
+ ["open_mouth", "\u{1F62E}", "", "wow|surprise|shock"],
60
+ ["flushed", "\u{1F633}", "", "blush|embarrassed|shy"],
61
+ ["pleading_face", "\u{1F97A}", "pleading", "puppy|beg|please"],
62
+ ["fearful", "\u{1F628}", "", "scared|afraid|yikes"],
63
+ ["cry", "\u{1F622}", "", "sad|tear|upset"],
64
+ ["sob", "\u{1F62D}", "crying", "bawl|sad|tears"],
65
+ ["scream", "\u{1F631}", "", "horror|shock|panic"],
66
+ ["disappointed", "\u{1F61E}", "", "sad|letdown"],
67
+ ["weary", "\u{1F629}", "", "exhausted|tired|done"],
68
+ ["tired_face", "\u{1F62B}", "", "exhausted|fed_up"],
69
+ ["yawning_face", "\u{1F971}", "yawn", "bored|sleepy|tired"],
70
+ ["rage", "\u{1F621}", "enraged", "angry|mad|furious"],
71
+ ["angry", "\u{1F620}", "", "mad|annoyed|cross"],
72
+ ["thinking", "\u{1F914}", "thinking_face|hmm", "ponder|consider|unsure"],
73
+ ["shushing_face", "\u{1F92B}", "shush", "quiet|secret|hush"],
74
+ ["zipper_mouth_face", "\u{1F910}", "zipper_mouth", "secret|quiet|sealed"],
75
+ ["saluting_face", "\u{1FAE1}", "salute", "respect|acknowledge|yessir"],
76
+ ["face_holding_back_tears", "\u{1F979}", "holding_back_tears", "emotional|touched|proud"],
77
+ ["clown_face", "\u{1F921}", "clown", "joke|silly|circus"],
78
+ ["ghost", "\u{1F47B}", "", "boo|spooky|halloween"],
79
+ ["skull", "\u{1F480}", "", "dead|rip|died"],
80
+ ["robot", "\u{1F916}", "robot_face", "bot|automation|machine"],
81
+ ["see_no_evil", "\u{1F648}", "", "monkey|hide|oops"],
82
+ ["poop", "\u{1F4A9}", "hankey", "bad|garbage|rubbish"],
83
+ /* ── Hands and body ──────────────────────────────────────────────────── */
84
+ ["+1", "\u{1F44D}", "thumbsup|thumbs_up|like", "yes|agree|approve|ok"],
85
+ ["-1", "\u{1F44E}", "thumbsdown|thumbs_down|dislike", "no|disagree|reject"],
86
+ ["ok_hand", "\u{1F44C}", "", "perfect|fine|good"],
87
+ ["clap", "\u{1F44F}", "", "applause|bravo|well_done"],
88
+ ["raised_hands", "\u{1F64C}", "", "praise|hooray|celebrate"],
89
+ ["pray", "\u{1F64F}", "folded_hands", "please|thanks|thank_you|hope"],
90
+ ["muscle", "\u{1F4AA}", "flex", "strong|strength|power"],
91
+ ["wave", "\u{1F44B}", "", "hi|hello|bye|greeting"],
92
+ ["point_right", "\u{1F449}", "", "this|direction|there"],
93
+ ["point_left", "\u{1F448}", "", "that|direction|back"],
94
+ ["crossed_fingers", "\u{1F91E}", "fingers_crossed", "luck|hope|wish"],
95
+ ["handshake", "\u{1F91D}", "", "deal|agree|partner"],
96
+ ["writing_hand", "\u270D\uFE0F", "writing", "note|sign|draft"],
97
+ ["eyes", "\u{1F440}", "", "look|watch|reviewing|see"],
98
+ ["brain", "\u{1F9E0}", "", "smart|idea|think"],
99
+ ["facepalm", "\u{1F926}", "", "oops|ugh|smh"],
100
+ ["shrug", "\u{1F937}", "", "dunno|idk|whatever"],
101
+ ["raising_hand", "\u{1F64B}", "raise_hand", "question|volunteer|me"],
102
+ /* ── Celebration and energy ──────────────────────────────────────────── */
103
+ ["tada", "\u{1F389}", "party|hooray|celebrate", "confetti|launch|ship|shipped|done"],
104
+ ["sparkles", "\u2728", "", "shiny|magic|new|clean"],
105
+ ["fire", "\u{1F525}", "flame|lit", "hot|burning|great"],
106
+ ["rocket", "\u{1F680}", "", "launch|ship|deploy|fast"],
107
+ ["boom", "\u{1F4A5}", "collision", "explode|bang|impact"],
108
+ ["zap", "\u26A1", "lightning", "fast|power|energy"],
109
+ ["star", "\u2B50", "", "favourite|rating|good"],
110
+ ["100", "\u{1F4AF}", "hundred|perfect", "score|agree|exactly"],
111
+ ["trophy", "\u{1F3C6}", "", "win|award|champion"],
112
+ ["dart", "\u{1F3AF}", "bullseye|target", "goal|aim|precise"],
113
+ ["crown", "\u{1F451}", "", "king|queen|best"],
114
+ ["gem", "\u{1F48E}", "diamond", "jewel|valuable|premium"],
115
+ ["bulb", "\u{1F4A1}", "idea|light_bulb", "insight|bright|suggestion"],
116
+ /* ── Work, status and objects ────────────────────────────────────────── */
117
+ ["moneybag", "\u{1F4B0}", "money", "budget|cash|spend"],
118
+ ["chart_with_upwards_trend", "\u{1F4C8}", "chart_up|trending_up", "growth|increase|up"],
119
+ ["chart_with_downwards_trend", "\u{1F4C9}", "chart_down|trending_down", "decline|decrease|down"],
120
+ ["bar_chart", "\u{1F4CA}", "chart", "data|report|stats"],
121
+ ["clipboard", "\u{1F4CB}", "", "list|notes|tasks"],
122
+ ["calendar", "\u{1F4C5}", "date", "schedule|day|meeting"],
123
+ ["pushpin", "\u{1F4CC}", "pin", "pinned|note|important"],
124
+ ["paperclip", "\u{1F4CE}", "attachment", "attach|file|clip"],
125
+ ["mag", "\u{1F50D}", "search|magnifying_glass", "find|look|zoom"],
126
+ ["lock", "\u{1F512}", "", "secure|private|closed"],
127
+ ["unlock", "\u{1F513}", "", "open|access"],
128
+ ["key", "\u{1F511}", "", "access|password|secret"],
129
+ ["bell", "\u{1F514}", "notification", "alert|ring|remind"],
130
+ ["no_bell", "\u{1F515}", "mute", "silence|quiet|off"],
131
+ ["hourglass", "\u231B", "waiting", "time|wait|pending"],
132
+ ["alarm_clock", "\u23F0", "alarm", "time|remind|deadline"],
133
+ ["stopwatch", "\u23F1\uFE0F", "timer", "time|measure|speed"],
134
+ ["warning", "\u26A0\uFE0F", "caution", "careful|alert|risk"],
135
+ ["no_entry", "\u26D4", "blocked", "stop|forbidden|blocker"],
136
+ ["white_check_mark", "\u2705", "check|done", "complete|yes|approved|shipped"],
137
+ ["heavy_check_mark", "\u2714\uFE0F", "checkmark|tick", "done|yes|verified"],
138
+ ["x", "\u274C", "cross_mark", "no|wrong|cancel|failed"],
139
+ ["question", "\u2753", "", "help|ask|unknown"],
140
+ ["exclamation", "\u2757", "heavy_exclamation_mark", "important|alert"],
141
+ ["construction", "\u{1F6A7}", "wip", "work_in_progress|building|roadworks"],
142
+ ["hammer", "\u{1F528}", "", "build|fix|tool"],
143
+ ["wrench", "\u{1F527}", "", "fix|tool|maintenance"],
144
+ ["gear", "\u2699\uFE0F", "settings", "config|cog|options"],
145
+ ["bug", "\u{1F41B}", "", "defect|issue|error"],
146
+ ["computer", "\u{1F4BB}", "laptop", "code|work|dev"],
147
+ ["email", "\u2709\uFE0F", "envelope", "mail|message|send"],
148
+ ["package", "\u{1F4E6}", "", "box|release|ship|delivery"],
149
+ ["memo", "\u{1F4DD}", "note|pencil", "write|notes|edit"],
150
+ ["art", "\u{1F3A8}", "palette", "creative|paint|design"],
151
+ ["speech_balloon", "\u{1F4AC}", "comment", "talk|message|chat"],
152
+ ["thought_balloon", "\u{1F4AD}", "thought", "idea|think|dream"],
153
+ ["mega", "\u{1F4E3}", "megaphone|announce", "announcement|shout|promote"],
154
+ ["link", "\u{1F517}", "", "url|chain|connect"],
155
+ ["globe_with_meridians", "\u{1F310}", "globe", "internet|web|world"],
156
+ /* ── Hearts ──────────────────────────────────────────────────────────── */
157
+ ["heart", "\u2764\uFE0F", "red_heart|love", "like|adore"],
158
+ ["broken_heart", "\u{1F494}", "", "sad|heartbreak"],
159
+ ["sparkling_heart", "\u{1F496}", "", "love|sparkle"],
160
+ ["two_hearts", "\u{1F495}", "", "love|cute"],
161
+ /* ── Nature ──────────────────────────────────────────────────────────── */
162
+ ["sunny", "\u2600\uFE0F", "sun", "weather|clear|bright"],
163
+ ["rainbow", "\u{1F308}", "", "colour|pride|hope"],
164
+ ["seedling", "\u{1F331}", "sprout", "grow|new|plant"],
165
+ ["four_leaf_clover", "\u{1F340}", "clover", "luck|lucky"],
166
+ ["unicorn", "\u{1F984}", "", "magic|rare|special"],
167
+ /* ── Food and drink ──────────────────────────────────────────────────── */
168
+ ["coffee", "\u2615", "cafe", "caffeine|morning|espresso"],
169
+ ["beer", "\u{1F37A}", "", "drink|cheers|friday"],
170
+ ["champagne", "\u{1F37E}", "", "celebrate|launch|cheers"],
171
+ ["clinking_glasses", "\u{1F942}", "cheers", "toast|celebrate"],
172
+ ["pizza", "\u{1F355}", "", "food|slice"],
173
+ ["birthday", "\u{1F382}", "birthday_cake", "cake|celebrate"],
174
+ /* ── Play ────────────────────────────────────────────────────────────── */
175
+ ["video_game", "\u{1F3AE}", "gamepad", "play|gaming"],
176
+ ["gift", "\u{1F381}", "present", "birthday|surprise"]
177
+ ];
178
+ function splitList(value) {
179
+ return value === "" ? [] : value.split("|");
180
+ }
181
+ var THREAD_EMOJI = EMOJI_ROWS.map(
182
+ ([name, glyph, aliases, keywords]) => ({
183
+ name,
184
+ glyph,
185
+ aliases: splitList(aliases),
186
+ keywords: splitList(keywords)
187
+ })
188
+ );
189
+ function normalizeShortcode(value) {
190
+ return value.trim().toLowerCase().replace(/^:+|:+$/g, "").replaceAll("-", "_");
191
+ }
192
+ var BY_SHORTCODE = (() => {
193
+ const map = /* @__PURE__ */ new Map();
194
+ for (const entry of THREAD_EMOJI) {
195
+ for (const code of [entry.name, ...entry.aliases]) {
196
+ const key = normalizeShortcode(code);
197
+ if (!map.has(key)) map.set(key, entry);
198
+ }
199
+ }
200
+ return map;
201
+ })();
202
+ function emojiByShortcode(code) {
203
+ return BY_SHORTCODE.get(normalizeShortcode(code));
204
+ }
205
+ function score(entry, query) {
206
+ if (entry.name === query) return 0;
207
+ if (entry.aliases.includes(query)) return 1;
208
+ if (entry.keywords.includes(query)) return 2;
209
+ if (entry.name.startsWith(query)) return 3;
210
+ if (entry.aliases.some((alias) => alias.startsWith(query))) return 4;
211
+ if (entry.keywords.some((keyword) => keyword.startsWith(query))) return 5;
212
+ return null;
213
+ }
214
+ var EMOJI_RESULT_LIMIT = 8;
215
+ function searchEmoji(query, limit = EMOJI_RESULT_LIMIT) {
216
+ const normalized = normalizeShortcode(query);
217
+ if (normalized === "") return [];
218
+ const ranked = [];
219
+ for (const [order, entry] of THREAD_EMOJI.entries()) {
220
+ const rank = score(entry, normalized);
221
+ if (rank !== null) ranked.push({ entry, rank, order });
222
+ }
223
+ ranked.sort((a, b) => a.rank === b.rank ? a.order - b.order : a.rank - b.rank);
224
+ return ranked.slice(0, limit).map((match) => match.entry);
225
+ }
226
+ var UNDERLINE_MARKER = "++";
227
+ var UNDERLINE_PATTERN = /^\+\+(?=\S)((?:\\[^\n]|(?!\+\+)[^\n\\])+?)(?<=\S)\+\+/;
228
+ var UNDERLINE_INPUT_RULE = /(?:^|\s)(\+\+(?!\s)([^+]+?)(?<!\s)\+\+)$/;
229
+ var ThreadUnderline = Mark.create({
230
+ name: "underline",
231
+ addOptions() {
232
+ return { HTMLAttributes: {} };
233
+ },
234
+ parseHTML() {
235
+ return [
236
+ { tag: "u" },
237
+ {
238
+ style: "text-decoration",
239
+ consuming: false,
240
+ getAttrs: (style) => typeof style === "string" && style.includes("underline") ? {} : false
241
+ }
242
+ ];
243
+ },
244
+ renderHTML({ HTMLAttributes }) {
245
+ return ["u", mergeAttributes(this.options.HTMLAttributes, HTMLAttributes), 0];
246
+ },
247
+ addCommands() {
248
+ return {
249
+ setUnderline: () => ({ commands }) => commands.setMark(this.name),
250
+ toggleUnderline: () => ({ commands }) => commands.toggleMark(this.name),
251
+ unsetUnderline: () => ({ commands }) => commands.unsetMark(this.name)
252
+ };
253
+ },
254
+ addKeyboardShortcuts() {
255
+ return {
256
+ "Mod-u": () => this.editor.commands.toggleUnderline(),
257
+ "Mod-U": () => this.editor.commands.toggleUnderline()
258
+ };
259
+ },
260
+ /**
261
+ * Type `++text++` and it becomes underlined, the moment the closing `++`
262
+ * lands — the same as `**bold**` and `~~strike~~`, which StarterKit gives us
263
+ * for free. Without this the mark had commands, a shortcut and full markdown
264
+ * serialization but no way to reach it by TYPING, which is the way people
265
+ * who know markdown actually reach it. (Owner, 2026-09-07: "is it not like
266
+ * obsidian where you type them in and it autoformats as you type?")
267
+ *
268
+ * The pattern mirrors TipTap's own bold rule, with `+` swapped for `*`:
269
+ * - `(?:^|\s)` — the run starts at the line start or after whitespace, so
270
+ * `i++;` and `C++` are never rules, matching the renderer's flanking rule
271
+ * (D13) rather than fighting it.
272
+ * - `(?!\s+\+\+)` — no whitespace immediately inside the opener.
273
+ * - `[^+]+` — the content cannot itself contain `+`, so `+++` runs and
274
+ * `a++b++c++d` degrade to text instead of matching greedily.
275
+ */
276
+ addInputRules() {
277
+ return [markInputRule({ find: UNDERLINE_INPUT_RULE, type: this.type })];
278
+ },
279
+ markdownTokenizer: {
280
+ name: "underline",
281
+ level: "inline",
282
+ start: (src) => src.indexOf(UNDERLINE_MARKER),
283
+ tokenize: (src, _tokens, lexer) => {
284
+ const match = UNDERLINE_PATTERN.exec(src);
285
+ const inner = match?.[1];
286
+ if (match === null || inner === void 0) return void 0;
287
+ return { type: "underline", raw: match[0], text: inner, tokens: lexer.inlineTokens(inner) };
288
+ }
289
+ },
290
+ parseMarkdown: (token, helpers) => helpers.applyMark("underline", helpers.parseInline(token.tokens ?? [])),
291
+ renderMarkdown: (node, helpers) => `++${helpers.renderChildren(node)}++`
292
+ });
293
+ var THREAD_MENTION_SUGGESTION_KEY = new PluginKey("threadMentionSuggestion");
294
+ var MENTION_PATTERN = /^@\[((?:\\.|[^\]\\\n])+)\]\(([^)\s]+)\)/;
295
+ function escapeMentionLabel(label) {
296
+ return label.replace(/[\\[\]]/g, (char) => `\\${char}`);
297
+ }
298
+ function unescapeMentionLabel(raw) {
299
+ return raw.replace(/\\(.)/g, "$1");
300
+ }
301
+ function readMentionAttrs(attrs) {
302
+ if (attrs === void 0) return null;
303
+ const id = attrs["id"];
304
+ const label = attrs["label"];
305
+ const type = attrs["type"];
306
+ if (typeof id !== "string" || id === "") return null;
307
+ if (typeof type !== "string") return null;
308
+ const target = parseMentionHref(`${type}:${id}`);
309
+ if (target === null) return null;
310
+ return { id: target.id, label: typeof label === "string" && label !== "" ? label : id, type: target.type };
311
+ }
312
+ var ThreadMention = Mention.extend({
313
+ addAttributes() {
314
+ return {
315
+ ...this.parent?.(),
316
+ type: {
317
+ default: "user",
318
+ parseHTML: (element) => element.getAttribute("data-mention-type"),
319
+ renderHTML: (attributes) => ({ "data-mention-type": attributes["type"] })
320
+ }
321
+ };
322
+ },
323
+ markdownTokenizer: {
324
+ name: "mention",
325
+ level: "inline",
326
+ start: (src) => src.indexOf("@["),
327
+ tokenize: (src) => {
328
+ const match = MENTION_PATTERN.exec(src);
329
+ const rawLabel = match?.[1];
330
+ const href = match?.[2];
331
+ if (match === null || rawLabel === void 0 || href === void 0) return void 0;
332
+ const target = parseMentionHref(href);
333
+ if (target === null) return void 0;
334
+ return {
335
+ type: "mention",
336
+ raw: match[0],
337
+ label: unescapeMentionLabel(rawLabel),
338
+ id: target.id,
339
+ mentionType: target.type
340
+ };
341
+ }
342
+ },
343
+ parseMarkdown: (token) => ({
344
+ type: "mention",
345
+ attrs: {
346
+ id: token["id"],
347
+ label: token["label"],
348
+ type: token["mentionType"],
349
+ mentionSuggestionChar: "@"
350
+ }
351
+ }),
352
+ renderMarkdown: (node) => {
353
+ const mention = readMentionAttrs(node.attrs);
354
+ if (mention === null) return typeof node.attrs?.["label"] === "string" ? `@${node.attrs["label"]}` : "";
355
+ return `@[${escapeMentionLabel(mention.label)}](${mention.type}:${mention.id})`;
356
+ }
357
+ });
358
+ var RICH_HEADING_LEVELS = [1, 2, 3];
359
+ var RICH_HEADING_LEVEL_SET = new Set(RICH_HEADING_LEVELS);
360
+ var MENTION_SUGGESTION_MIN_QUERY = 1;
361
+ var EMOJI_SUGGESTION_MIN_QUERY = 2;
362
+ var THREAD_EMOJI_SUGGESTION_KEY = new PluginKey("threadEmojiSuggestion");
363
+ var EMOJI_SHORTCODE_AT_END = /(?:^|\s)(:([^:\s]+):)$/;
364
+ var EMOJI_INPUT_RULE = (text) => {
365
+ const match = EMOJI_SHORTCODE_AT_END.exec(text);
366
+ const shortcode = match?.[1];
367
+ if (match === null || shortcode === void 0 || emojiByShortcode(shortcode) === void 0) {
368
+ return null;
369
+ }
370
+ return { index: match.index, text: match[0], data: { shortcode } };
371
+ };
372
+ var ThreadEmojiShortcode = Extension.create({
373
+ name: "threadEmojiShortcode",
374
+ addInputRules() {
375
+ return [
376
+ new InputRule({
377
+ find: EMOJI_INPUT_RULE,
378
+ handler: ({ state, range, match }) => {
379
+ const shortcode = match.data?.["shortcode"];
380
+ const entry = typeof shortcode === "string" ? emojiByShortcode(shortcode) : void 0;
381
+ if (entry === void 0 || typeof shortcode !== "string") return null;
382
+ state.tr.insertText(entry.glyph, range.to - shortcode.length, range.to);
383
+ return void 0;
384
+ }
385
+ })
386
+ ];
387
+ }
388
+ });
389
+ function insertEmoji({
390
+ editor,
391
+ range,
392
+ props
393
+ }) {
394
+ const entry = emojiByShortcode(props.id ?? "");
395
+ if (entry === void 0) return;
396
+ const nodeAfter = editor.view.state.selection.$to.nodeAfter;
397
+ const to = nodeAfter?.text?.startsWith(" ") === true ? range.to + 1 : range.to;
398
+ editor.chain().focus().insertContentAt({ from: range.from, to }, [{ type: "text", text: `${entry.glyph} ` }]).run();
399
+ }
400
+ function emojiSuggestionItems(query) {
401
+ return searchEmoji(query).map((entry) => ({ id: entry.name, label: entry.glyph }));
402
+ }
403
+ function richSubsetExtensions(renderers = {}) {
404
+ return [
405
+ StarterKit.configure({
406
+ heading: { levels: [...RICH_HEADING_LEVELS] },
407
+ // horizontalRule: default, IN — owner, 2026-09-07.
408
+ underline: false,
409
+ link: {
410
+ openOnClick: false,
411
+ autolink: true,
412
+ linkOnPaste: true,
413
+ enableClickSelection: false
414
+ }
415
+ }),
416
+ ThreadUnderline,
417
+ ThreadEmojiShortcode,
418
+ ThreadMention.configure({
419
+ HTMLAttributes: { class: "ds-thread-mention" },
420
+ suggestions: [
421
+ {
422
+ char: "@",
423
+ pluginKey: THREAD_MENTION_SUGGESTION_KEY,
424
+ minQueryLength: MENTION_SUGGESTION_MIN_QUERY,
425
+ ...renderers.mention
426
+ },
427
+ {
428
+ char: ":",
429
+ pluginKey: THREAD_EMOJI_SUGGESTION_KEY,
430
+ minQueryLength: EMOJI_SUGGESTION_MIN_QUERY,
431
+ // The plugin's own view of what it is offering. The POPUP does not
432
+ // read this — it recomputes from the query, because
433
+ // `@tiptap/suggestion` types its items as `any` and the composer
434
+ // does not take `any` — but leaving it unset would make the plugin's
435
+ // state say "no items" while eight are on screen, which is a lie any
436
+ // future reader of that state would trip over.
437
+ items: ({ query }) => emojiSuggestionItems(query),
438
+ command: insertEmoji,
439
+ // A shortcode typed inside code stays literal: `:tada:` in a snippet
440
+ // is text about an emoji, not an emoji. The mention trigger keeps
441
+ // the extension's own default, which asks the schema whether a
442
+ // mention node may sit here.
443
+ allow: ({ editor }) => !editor.isActive("codeBlock") && !editor.isActive("code"),
444
+ ...renderers.emoji
445
+ }
446
+ ]
447
+ }),
448
+ Markdown
449
+ ];
450
+ }
451
+ var probe = null;
452
+ function getProbe() {
453
+ if (probe === null) {
454
+ probe = new Editor({ element: null, extensions: richSubsetExtensions(), content: "" });
455
+ }
456
+ return probe;
457
+ }
458
+ function getManager() {
459
+ const manager = getProbe().markdown;
460
+ if (manager === void 0) {
461
+ throw new Error("rich composer: the Markdown extension did not attach a manager");
462
+ }
463
+ return manager;
464
+ }
465
+ function getSchema() {
466
+ return getProbe().schema;
467
+ }
468
+ function parseRichMarkdown(markdown) {
469
+ return canonicalizeInlineMarks(getManager().parse(markdown));
470
+ }
471
+ var BOUNDARY_MARKS = ["bold", "italic", "underline", "strike", "code"];
472
+ function hasMark(node, type) {
473
+ return (node.marks ?? []).some((mark) => mark.type === type);
474
+ }
475
+ function withoutMark(node, type) {
476
+ const marks = (node.marks ?? []).filter((mark) => mark.type !== type);
477
+ const { marks: _marks, ...rest } = node;
478
+ return marks.length === 0 ? rest : { ...rest, marks };
479
+ }
480
+ function trimMarkRuns(content, type) {
481
+ const out = [];
482
+ let run = [];
483
+ const flush = () => {
484
+ if (run.length === 0) return;
485
+ const first = run[0];
486
+ const last = run[run.length - 1];
487
+ if (first === void 0 || last === void 0) return;
488
+ const leading = /^\s+/.exec(first.text ?? "")?.[0] ?? "";
489
+ if (leading !== "" && leading.length < (first.text ?? "").length) {
490
+ run[0] = { ...first, text: (first.text ?? "").slice(leading.length) };
491
+ out.push(withoutMark({ ...first, text: leading }, type));
492
+ } else if (leading !== "") {
493
+ run[0] = withoutMark(first, type);
494
+ }
495
+ const tail = run[run.length - 1];
496
+ if (tail !== void 0 && run.length >= 1 && !(run.length === 1 && leading !== "" && leading.length >= (first.text ?? "").length)) {
497
+ const trailing = /\s+$/.exec(tail.text ?? "")?.[0] ?? "";
498
+ if (trailing !== "" && trailing.length < (tail.text ?? "").length) {
499
+ run[run.length - 1] = { ...tail, text: (tail.text ?? "").slice(0, -trailing.length) };
500
+ out.push(...run);
501
+ out.push(withoutMark({ ...tail, text: trailing }, type));
502
+ run = [];
503
+ return;
504
+ }
505
+ if (trailing !== "" && run.length > 1) {
506
+ run[run.length - 1] = withoutMark(tail, type);
507
+ }
508
+ }
509
+ out.push(...run);
510
+ run = [];
511
+ };
512
+ for (const node of content) {
513
+ if (node.type === "text" && hasMark(node, type)) {
514
+ run.push(node);
515
+ continue;
516
+ }
517
+ flush();
518
+ out.push(node);
519
+ }
520
+ flush();
521
+ return out;
522
+ }
523
+ function canonicalizeInlineMarks(node) {
524
+ const content = node.content;
525
+ if (content === void 0) return node;
526
+ if (node.type === "paragraph" || node.type === "heading") {
527
+ let inline = [...content];
528
+ for (const type of BOUNDARY_MARKS) inline = trimMarkRuns(inline, type);
529
+ return { ...node, content: inline };
530
+ }
531
+ return { ...node, content: content.map(canonicalizeInlineMarks) };
532
+ }
533
+ function normalizeRichDocument(doc) {
534
+ const node = Node$1.fromJSON(getSchema(), doc);
535
+ node.check();
536
+ return node.toJSON();
537
+ }
538
+ var ESCAPE_SENTINEL = "\uE000";
539
+ function hasCodeMark(node) {
540
+ return (node.marks ?? []).some((mark) => mark.type === "code");
541
+ }
542
+ function escapeBlockStart(text) {
543
+ return text.replace(/^(#{1,6})(?=[ \t]|$)/, (hashes) => hashes.replace(/#/g, `${ESCAPE_SENTINEL}#`)).replace(/^([-+])(?=[ \t]|$)/, `${ESCAPE_SENTINEL}$1`).replace(/^(\d{1,9})([.)])(?=[ \t]|$)/, `$1${ESCAPE_SENTINEL}$2`).replace(/^-{3,}[ \t]*$/, (rule) => `${ESCAPE_SENTINEL}${rule}`);
544
+ }
545
+ function escapePlusRuns(text) {
546
+ return text.replace(/\+{2,}/g, (run) => run.replace(/\+/g, `${ESCAPE_SENTINEL}+`));
547
+ }
548
+ function markLiteralSyntax(node) {
549
+ if (node.type === "codeBlock") return node;
550
+ const content = node.content;
551
+ if (content === void 0) return node;
552
+ let lineStart = node.type === "paragraph" || node.type === "heading";
553
+ const next = content.map((child) => {
554
+ if (child.type === "text") {
555
+ const startsLine = lineStart;
556
+ lineStart = false;
557
+ if (hasCodeMark(child)) return child;
558
+ const source = (child.text ?? "").replaceAll(ESCAPE_SENTINEL, "");
559
+ let text = escapePlusRuns(source);
560
+ if (startsLine) text = escapeBlockStart(text);
561
+ return text === child.text ? child : { ...child, text };
562
+ }
563
+ if (child.type === "hardBreak") {
564
+ lineStart = true;
565
+ return child;
566
+ }
567
+ lineStart = false;
568
+ return markLiteralSyntax(child);
569
+ });
570
+ return { ...node, content: next };
571
+ }
572
+ function serializeRichMarkdown(doc) {
573
+ return getManager().serialize(markLiteralSyntax(canonicalizeInlineMarks(doc))).replaceAll(ESCAPE_SENTINEL, "\\").trimEnd();
574
+ }
575
+ var SUPPORTED_TOKENS = /* @__PURE__ */ new Set([
576
+ "space",
577
+ "paragraph",
578
+ "heading",
579
+ "hr",
580
+ "blockquote",
581
+ "list",
582
+ "list_item",
583
+ "code",
584
+ "def",
585
+ "text",
586
+ "strong",
587
+ "em",
588
+ "del",
589
+ "codespan",
590
+ "link",
591
+ "br",
592
+ "escape",
593
+ "underline",
594
+ "mention"
595
+ ]);
596
+ function tokensSupported(tokens) {
597
+ return tokens.every((token) => {
598
+ const type = token.type;
599
+ if (type === void 0 || !SUPPORTED_TOKENS.has(type)) return false;
600
+ if (type === "heading" && !RICH_HEADING_LEVEL_SET.has(token.depth ?? 1)) return false;
601
+ if (type === "list_item" && token["task"] === true) return false;
602
+ const children = [...token.tokens ?? [], ...token.items ?? []];
603
+ return tokensSupported(children);
604
+ });
605
+ }
606
+ function documentSupported(node) {
607
+ if (node.type === "heading") {
608
+ const level = node.attrs?.["level"];
609
+ if (typeof level !== "number" || !RICH_HEADING_LEVEL_SET.has(level)) return false;
610
+ }
611
+ if (node.type === "mention" && readMentionAttrs(node.attrs) === null) return false;
612
+ return (node.content ?? []).every(documentSupported);
613
+ }
614
+ function isRichEditable(markdown) {
615
+ if (markdown.trim() === "") return true;
616
+ const manager = getManager();
617
+ let tokens;
618
+ try {
619
+ tokens = manager.instance.lexer(markdown);
620
+ } catch {
621
+ return false;
622
+ }
623
+ if (!tokensSupported(tokens)) return false;
624
+ try {
625
+ const doc = manager.parse(markdown);
626
+ if (!documentSupported(doc)) return false;
627
+ normalizeRichDocument(doc);
628
+ return true;
629
+ } catch {
630
+ return false;
631
+ }
632
+ }
633
+ function wrapIndex(current, length, delta) {
634
+ if (length <= 0) return 0;
635
+ return ((current + delta) % length + length) % length;
636
+ }
637
+ function listScrollTop(view, target) {
638
+ if (target.offsetTop < view.scrollTop) return target.offsetTop;
639
+ const bottom = target.offsetTop + target.offsetHeight;
640
+ if (bottom > view.scrollTop + view.clientHeight) return bottom - view.clientHeight;
641
+ return view.scrollTop;
642
+ }
643
+ function SuggestionList({
644
+ id,
645
+ label,
646
+ options,
647
+ activeIndex,
648
+ optionId,
649
+ onSelect,
650
+ onActivate
651
+ }) {
652
+ const listRef = useRef(null);
653
+ useLayoutEffect(() => {
654
+ const list = listRef.current;
655
+ if (list === null) return;
656
+ const option = list.children.item(activeIndex);
657
+ if (!(option instanceof HTMLElement)) return;
658
+ list.scrollTop = listScrollTop(list, option);
659
+ }, [activeIndex, options]);
660
+ return /* @__PURE__ */ jsx(
661
+ "div",
662
+ {
663
+ ref: listRef,
664
+ id,
665
+ role: "listbox",
666
+ "aria-label": label,
667
+ className: "ds-thread-suggestions",
668
+ onMouseDown: (event) => {
669
+ event.preventDefault();
670
+ },
671
+ children: options.map((option, index) => {
672
+ const active = index === activeIndex;
673
+ return /* @__PURE__ */ jsxs(
674
+ "button",
675
+ {
676
+ type: "button",
677
+ id: optionId(index),
678
+ role: "option",
679
+ "aria-selected": active,
680
+ tabIndex: -1,
681
+ "data-active": active,
682
+ className: "ds-thread-suggestions__option",
683
+ onMouseDown: (event) => {
684
+ event.preventDefault();
685
+ },
686
+ onMouseEnter: () => {
687
+ onActivate(index);
688
+ },
689
+ onClick: () => {
690
+ onSelect(index);
691
+ },
692
+ children: [
693
+ /* @__PURE__ */ jsx(
694
+ "span",
695
+ {
696
+ ...option.glyphDecorative === true || option.glyph === void 0 ? { "aria-hidden": true } : {},
697
+ className: "ds-thread-suggestions__glyph",
698
+ children: option.glyph
699
+ }
700
+ ),
701
+ /* @__PURE__ */ jsx("span", { className: "ds-thread-suggestions__label", children: option.label }),
702
+ option.hint === void 0 ? null : /* @__PURE__ */ jsx("span", { className: "ds-thread-suggestions__hint", children: option.hint })
703
+ ]
704
+ },
705
+ option.key
706
+ );
707
+ })
708
+ }
709
+ );
710
+ }
711
+ var DRAFT_DEBOUNCE_MS = 300;
712
+ var FAIL_CLOSED_NOTICE = "Opened in the plain editor: this comment uses formatting the rich editor cannot hold. Nothing was changed.";
713
+ var IS_APPLE = typeof navigator !== "undefined" && /Mac|iPhone|iPad/.test(navigator.userAgent);
714
+ var SEND_KEYCAP = IS_APPLE ? "\u2318\u21B5" : "Ctrl \u21B5";
715
+ var MOD_NAME = IS_APPLE ? "Command" : "Control";
716
+ var BUBBLE_MENU_KEY = new PluginKey("threadBubbleMenu");
717
+ function RichThreadComposerImpl(props) {
718
+ const { value, onChange, announce } = props;
719
+ const lastEmitted = useRef(null);
720
+ const [gate, setGate] = useState(() => ({ value, rich: isRichEditable(value) }));
721
+ if (value !== gate.value) {
722
+ setGate({ value, rich: value === lastEmitted.current ? gate.rich : isRichEditable(value) });
723
+ }
724
+ const emit = useCallback(
725
+ (next) => {
726
+ lastEmitted.current = next;
727
+ onChange(next);
728
+ },
729
+ [onChange]
730
+ );
731
+ useEffect(() => {
732
+ if (!gate.rich) announce?.(FAIL_CLOSED_NOTICE);
733
+ }, [announce, gate.rich]);
734
+ if (!gate.rich) {
735
+ const {
736
+ onEditor: _onEditor,
737
+ mentionItems: _items,
738
+ announce: _announce,
739
+ uploadAttachment: _uploadAttachment,
740
+ attachGif: _attachGif,
741
+ searchGifs: _searchGifs,
742
+ attachmentAccept: _attachmentAccept,
743
+ attachmentMaxPerComment: _attachmentMaxPerComment,
744
+ attachmentMaxFileBytes: _attachmentMaxFileBytes,
745
+ attachmentMaxTotalBytes: _attachmentMaxTotalBytes,
746
+ ...plain
747
+ } = props;
748
+ return /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-[var(--space-1)]", children: [
749
+ /* @__PURE__ */ jsx(ComposerNotice, { children: FAIL_CLOSED_NOTICE }),
750
+ /* @__PURE__ */ jsx(ThreadComposer, { ...plain, onChange: emit })
751
+ ] });
752
+ }
753
+ return /* @__PURE__ */ jsx(RichSurface, { ...props, onChange: emit });
754
+ }
755
+ var EMPTY_ROWS = { options: [], attrs: [] };
756
+ var SUGGESTION_KEYS = {
757
+ mention: THREAD_MENTION_SUGGESTION_KEY,
758
+ emoji: THREAD_EMOJI_SUGGESTION_KEY
759
+ };
760
+ var SUGGESTION_LABELS = {
761
+ mention: "Mention someone",
762
+ emoji: "Emoji"
763
+ };
764
+ var SUGGESTION_NOUNS = {
765
+ mention: "mention",
766
+ emoji: "emoji"
767
+ };
768
+ var SUGGESTION_MIN_QUERY = {
769
+ mention: MENTION_SUGGESTION_MIN_QUERY,
770
+ emoji: EMOJI_SUGGESTION_MIN_QUERY
771
+ };
772
+ var MENTION_KIND_LABELS = {
773
+ user: "Person",
774
+ task_item: "Task",
775
+ task_board: "Board"
776
+ };
777
+ function exitSuggestionPlugin(view, kind) {
778
+ view.dispatch(view.state.tr.setMeta(SUGGESTION_KEYS[kind], { exit: true }));
779
+ }
780
+ function emojiRows(query) {
781
+ const entries = searchEmoji(query);
782
+ return {
783
+ options: entries.map((entry) => ({
784
+ key: entry.name,
785
+ label: `:${entry.name}:`,
786
+ glyph: entry.glyph
787
+ })),
788
+ attrs: entries.map((entry) => ({ id: entry.name, label: entry.glyph }))
789
+ };
790
+ }
791
+ function leadingInitial(label) {
792
+ return ([...label][0] ?? "").toUpperCase();
793
+ }
794
+ function mentionRows(items) {
795
+ return {
796
+ options: items.map((item) => ({
797
+ key: `${item.type}:${item.id}`,
798
+ label: item.label,
799
+ hint: MENTION_KIND_LABELS[item.type],
800
+ glyph: leadingInitial(item.label),
801
+ // The initial is the label's own first letter — decoration, not
802
+ // information. The emoji rows above deliberately do NOT set this.
803
+ glyphDecorative: true
804
+ })),
805
+ attrs: [...items]
806
+ };
807
+ }
808
+ function isPlainTab(event) {
809
+ return event.key === "Tab" && !event.altKey && !event.ctrlKey && !event.metaKey;
810
+ }
811
+ var BARE_URL = /^(?:https?:\/\/|www\.)\S+$/i;
812
+ function normalizeHref(input) {
813
+ const trimmed = input.trim();
814
+ if (trimmed === "") return null;
815
+ if (/^[a-z][a-z0-9+.-]*:/i.test(trimmed)) {
816
+ return /^(?:https?:|mailto:|tel:)/i.test(trimmed) ? trimmed : null;
817
+ }
818
+ if (/^(?:www\.)?[\w-]+(?:\.[\w-]+)+(?:[/?#]\S*)?$/i.test(trimmed)) return `https://${trimmed}`;
819
+ return null;
820
+ }
821
+ function readSpaceToken(element, token, fallback) {
822
+ if (element === null || typeof getComputedStyle !== "function") return fallback;
823
+ const raw = parseFloat(getComputedStyle(element).getPropertyValue(token));
824
+ return Number.isFinite(raw) && raw > 0 ? raw : fallback;
825
+ }
826
+ function focusableButtons(root) {
827
+ if (root === null) return [];
828
+ return Array.from(root.querySelectorAll("button:not([disabled])"));
829
+ }
830
+ function RichSurface({
831
+ value,
832
+ onChange,
833
+ onSubmit,
834
+ onCancel,
835
+ placeholder = "Add a comment\u2026",
836
+ submitLabel = "Comment",
837
+ cancelLabel = "Cancel",
838
+ sending = false,
839
+ disabled = false,
840
+ replyingTo,
841
+ avatar,
842
+ rows = 3,
843
+ mode,
844
+ onModeChange,
845
+ autoFocus = false,
846
+ toolbar = true,
847
+ maxLength,
848
+ label,
849
+ className,
850
+ onEditor,
851
+ mentionItems,
852
+ announce,
853
+ uploadAttachment,
854
+ attachGif,
855
+ searchGifs,
856
+ attachmentAccept,
857
+ attachmentMaxPerComment,
858
+ attachmentMaxFileBytes,
859
+ attachmentMaxTotalBytes
860
+ }) {
861
+ const labelId = useId();
862
+ const hintId = useId();
863
+ const linkErrorId = useId();
864
+ const listboxId = useId();
865
+ const shellRef = useRef(null);
866
+ const bubbleRef = useRef(null);
867
+ const linkInputRef = useRef(null);
868
+ const [gifPickerOpen, setGifPickerOpen] = useState(false);
869
+ const attachments = useAttachments({
870
+ ...uploadAttachment === void 0 ? {} : { upload: uploadAttachment },
871
+ ...attachGif === void 0 ? {} : { attachGif },
872
+ ...announce === void 0 ? {} : { onError: announce },
873
+ ...attachmentMaxPerComment === void 0 ? {} : { maxPerComment: attachmentMaxPerComment },
874
+ ...attachmentMaxFileBytes === void 0 ? {} : { maxFileBytes: attachmentMaxFileBytes },
875
+ ...attachmentMaxTotalBytes === void 0 ? {} : { maxTotalBytes: attachmentMaxTotalBytes }
876
+ });
877
+ const latest = useRef({ value, onChange, onSubmit, onCancel, mentionItems, announce });
878
+ useLayoutEffect(() => {
879
+ latest.current = { value, onChange, onSubmit, onCancel, mentionItems, announce };
880
+ });
881
+ const editorRef = useRef(null);
882
+ const lastEmitted = useRef(value);
883
+ const emitTimer = useRef(null);
884
+ const [pendingSubmit, setPendingSubmit] = useState(null);
885
+ const composing = useRef(false);
886
+ const bubbleOpenRef = useRef(false);
887
+ const suggestionRef = useRef(null);
888
+ const activeIndexRef = useRef(0);
889
+ const [suggestion, setSuggestion] = useState(null);
890
+ const [activeIndex, setActiveIndex] = useState(0);
891
+ const setActiveOption = useCallback((index) => {
892
+ activeIndexRef.current = index;
893
+ setActiveIndex(index);
894
+ }, []);
895
+ const optionDomId = useCallback(
896
+ (index) => `${listboxId}-option-${String(index)}`,
897
+ [listboxId]
898
+ );
899
+ const commitSuggestion = useCallback((index) => {
900
+ const active2 = suggestionRef.current;
901
+ const attrs = active2?.attrs[index];
902
+ if (active2 === void 0 || active2 === null || attrs === void 0) return;
903
+ active2.command(attrs);
904
+ }, []);
905
+ const dismissedSelection = useRef(null);
906
+ const [bubbleOpen, setBubbleOpen] = useState(false);
907
+ const [focusRequest, setFocusRequest] = useState(null);
908
+ const [linkMode, setLinkMode] = useState(false);
909
+ const [linkDraft, setLinkDraft] = useState("");
910
+ const [linkError, setLinkError] = useState(false);
911
+ const [rovingIndex, setRovingIndex] = useState(0);
912
+ const setBubble = useCallback((open) => {
913
+ bubbleOpenRef.current = open;
914
+ setBubbleOpen((prev) => prev === open ? prev : open);
915
+ if (!open) {
916
+ setLinkMode(false);
917
+ setLinkError(false);
918
+ setFocusRequest(null);
919
+ }
920
+ }, []);
921
+ const shouldShowBubble = useCallback((editor2) => {
922
+ if (!editor2.isEditable) return false;
923
+ const { selection, doc } = editor2.state;
924
+ if (selection.empty || !isTextSelection(selection)) return false;
925
+ if (doc.textBetween(selection.from, selection.to, " ").trim() === "") return false;
926
+ if (editor2.isActive("codeBlock")) return false;
927
+ if (editor2.view.composing || composing.current) return false;
928
+ if (suggestionRef.current !== null) return false;
929
+ const dismissed = dismissedSelection.current;
930
+ if (dismissed !== null) {
931
+ if (dismissed.from === selection.from && dismissed.to === selection.to) return false;
932
+ dismissedSelection.current = null;
933
+ }
934
+ return true;
935
+ }, []);
936
+ const syncBubble = useCallback(
937
+ (editor2) => {
938
+ setBubble(shouldShowBubble(editor2));
939
+ },
940
+ [setBubble, shouldShowBubble]
941
+ );
942
+ const dismissBubble = useCallback(
943
+ (editor2) => {
944
+ const { from, to } = editor2.state.selection;
945
+ dismissedSelection.current = { from, to };
946
+ setBubble(false);
947
+ editor2.view.dispatch(editor2.state.tr.setMeta(BUBBLE_MENU_KEY, "hide"));
948
+ },
949
+ [setBubble]
950
+ );
951
+ const emitNow = useCallback((editor2) => {
952
+ const markdown = serializeRichMarkdown(editor2.getJSON());
953
+ if (markdown !== lastEmitted.current) {
954
+ lastEmitted.current = markdown;
955
+ latest.current.onChange(markdown);
956
+ }
957
+ return markdown;
958
+ }, []);
959
+ const clearEmitTimer = useCallback(() => {
960
+ if (emitTimer.current !== null) {
961
+ window.clearTimeout(emitTimer.current);
962
+ emitTimer.current = null;
963
+ }
964
+ }, []);
965
+ const scheduleEmit = useCallback(
966
+ (editor2) => {
967
+ clearEmitTimer();
968
+ emitTimer.current = window.setTimeout(() => {
969
+ emitTimer.current = null;
970
+ if (!editor2.isDestroyed) emitNow(editor2);
971
+ }, DRAFT_DEBOUNCE_MS);
972
+ },
973
+ [clearEmitTimer, emitNow]
974
+ );
975
+ const flushEmit = useCallback(
976
+ (editor2) => {
977
+ clearEmitTimer();
978
+ return emitNow(editor2);
979
+ },
980
+ [clearEmitTimer, emitNow]
981
+ );
982
+ const requestSubmit = useCallback(
983
+ (editor2) => {
984
+ if (editor2.isEmpty) return;
985
+ const markdown = flushEmit(editor2);
986
+ setPendingSubmit(markdown);
987
+ },
988
+ [flushEmit]
989
+ );
990
+ const parsePastedText = useCallback(
991
+ (text, plain, view) => {
992
+ const { schema } = view.state;
993
+ const trimmed = text.trim();
994
+ const linkType = schema.marks["link"];
995
+ const paragraphType = schema.nodes["paragraph"];
996
+ if (linkType !== void 0 && BARE_URL.test(trimmed)) {
997
+ const href = /^www\./i.test(trimmed) ? `https://${trimmed}` : trimmed;
998
+ return new Slice(Fragment.from(schema.text(trimmed, [linkType.create({ href })])), 0, 0);
999
+ }
1000
+ if (!plain && isRichEditable(text)) {
1001
+ try {
1002
+ const doc = Node$1.fromJSON(schema, parseRichMarkdown(text));
1003
+ doc.check();
1004
+ return Slice.maxOpen(doc.content);
1005
+ } catch {
1006
+ }
1007
+ }
1008
+ if (paragraphType === void 0) return Slice.empty;
1009
+ const paragraphs = text.split(/\r\n?|\n/).map((line) => paragraphType.create(null, line === "" ? null : schema.text(line)));
1010
+ return Slice.maxOpen(Fragment.from(paragraphs));
1011
+ },
1012
+ []
1013
+ );
1014
+ const makeSuggestionRenderer = useCallback(
1015
+ (kind) => {
1016
+ let container = null;
1017
+ let unmount = null;
1018
+ let lastQuery = null;
1019
+ const rowsFor = (query) => {
1020
+ if (query.length < SUGGESTION_MIN_QUERY[kind]) return EMPTY_ROWS;
1021
+ if (kind === "emoji") return emojiRows(query);
1022
+ const items = latest.current.mentionItems?.(query);
1023
+ return items === void 0 ? EMPTY_ROWS : mentionRows(items);
1024
+ };
1025
+ const close = () => {
1026
+ if (unmount !== null) {
1027
+ unmount();
1028
+ unmount = null;
1029
+ }
1030
+ container = null;
1031
+ lastQuery = null;
1032
+ if (suggestionRef.current?.kind === kind) suggestionRef.current = null;
1033
+ setSuggestion((prev) => prev !== null && prev.kind === kind ? null : prev);
1034
+ };
1035
+ const sync = (props) => {
1036
+ const rows2 = rowsFor(props.query);
1037
+ if (rows2.options.length === 0) {
1038
+ close();
1039
+ return;
1040
+ }
1041
+ const opening = container === null;
1042
+ let element = container;
1043
+ if (element === null) {
1044
+ element = document.createElement("div");
1045
+ element.className = "ds-thread-suggestions-anchor";
1046
+ container = element;
1047
+ const anchored = element;
1048
+ unmount = props.mount(element, {
1049
+ onPosition: ({ x, y, strategy }) => {
1050
+ anchored.style.position = strategy;
1051
+ anchored.style.left = `${String(x)}px`;
1052
+ anchored.style.top = `${String(y)}px`;
1053
+ }
1054
+ });
1055
+ }
1056
+ if (!opening && lastQuery === props.query) return;
1057
+ suggestionRef.current = { kind, attrs: rows2.attrs, command: props.command };
1058
+ lastQuery = props.query;
1059
+ setActiveOption(0);
1060
+ setBubble(false);
1061
+ setSuggestion({ kind, container: element, options: rows2.options });
1062
+ if (opening) {
1063
+ latest.current.announce?.(
1064
+ `${String(rows2.options.length)} ${SUGGESTION_NOUNS[kind]} suggestions. Use the arrow keys to choose, Enter to insert, Escape to close.`
1065
+ );
1066
+ }
1067
+ };
1068
+ return {
1069
+ onStart: sync,
1070
+ onUpdate: sync,
1071
+ onExit: close,
1072
+ // Escape, the arrows, Enter and Tab are all answered in
1073
+ // `handleKeyDown` below — `editorProps` handlers run before any
1074
+ // plugin's, which is what makes ADR-148 D9's Escape order a fact.
1075
+ onKeyDown: () => false
1076
+ };
1077
+ },
1078
+ [setActiveOption, setBubble]
1079
+ );
1080
+ const editor = useEditor(
1081
+ {
1082
+ extensions: [
1083
+ ...richSubsetExtensions({
1084
+ mention: { render: () => makeSuggestionRenderer("mention") },
1085
+ emoji: { render: () => makeSuggestionRenderer("emoji") }
1086
+ }),
1087
+ Placeholder.configure({ placeholder })
1088
+ ],
1089
+ content: value,
1090
+ contentType: "markdown",
1091
+ editable: !disabled,
1092
+ autofocus: autoFocus ? "end" : false,
1093
+ // D8 is implemented once, in `clipboardTextParser` below. TipTap's
1094
+ // regex paste rules (bold, italic, strike, code, …) would run a second,
1095
+ // looser pass over the pasted text — and re-format a Shift-paste that
1096
+ // was meant to stay literal. Input rules (typing `**x**`, `- `, `---`)
1097
+ // are unaffected.
1098
+ enablePasteRules: false,
1099
+ editorProps: {
1100
+ /**
1101
+ * THE fix for "the entire viewport is scrolling down" on Enter.
1102
+ *
1103
+ * ProseMirror scrolls the selection into view after any transaction
1104
+ * that asks for it — every Enter, every arrow key, most typing. Its
1105
+ * `scrollRectIntoView` walks UP from the editor and scrolls *every*
1106
+ * scrollable ancestor it finds until the caret is visible: the
1107
+ * thread's own body, then `PageShell`'s content region, then whatever
1108
+ * else. Capping the editor's height does not stop that; it only
1109
+ * changes which ancestor moves. The ancestors have to be taken out of
1110
+ * the walk entirely, and this hook is the only supported way to do it.
1111
+ *
1112
+ * Returning `true` means "handled — do not run the default". We then
1113
+ * do the one scroll that is legitimate: bring the caret into view
1114
+ * inside the EDITOR, and touch nothing else. `scrollTop` on a single
1115
+ * element cannot chain.
1116
+ */
1117
+ handleScrollToSelection: (view) => {
1118
+ const dom = view.dom;
1119
+ const head = view.state.selection.head;
1120
+ let caret;
1121
+ try {
1122
+ caret = view.coordsAtPos(head);
1123
+ } catch {
1124
+ return true;
1125
+ }
1126
+ const box = dom.getBoundingClientRect();
1127
+ const pad = parseFloat(getComputedStyle(dom).lineHeight) || 0;
1128
+ dom.scrollTop += caretScrollDelta(box, caret, pad);
1129
+ return true;
1130
+ },
1131
+ attributes: {
1132
+ role: "textbox",
1133
+ "aria-multiline": "true",
1134
+ "aria-labelledby": labelId,
1135
+ "aria-describedby": hintId
1136
+ },
1137
+ handleDOMEvents: {
1138
+ // D9: Tab is never captured. Returning true here skips ProseMirror's
1139
+ // own keydown handling (and every keymap) without preventDefault, so
1140
+ // the browser moves focus. ADR-149 D5 is the one exception: while a
1141
+ // suggestion popup is open Tab INSERTS, so this stands down and
1142
+ // `handleKeyDown` answers it (and does preventDefault, so focus
1143
+ // stays where it already is — on the editor).
1144
+ keydown: (_view, event) => isPlainTab(event) && suggestionRef.current === null,
1145
+ compositionstart: () => {
1146
+ composing.current = true;
1147
+ setBubble(false);
1148
+ return false;
1149
+ },
1150
+ compositionend: () => {
1151
+ composing.current = false;
1152
+ return false;
1153
+ }
1154
+ },
1155
+ handleKeyDown: (view, event) => {
1156
+ const editorFromView = editorRef.current;
1157
+ if (editorFromView === null) return false;
1158
+ const mod = event.metaKey || event.ctrlKey;
1159
+ if (event.key === "Escape") {
1160
+ if (bubbleOpenRef.current) {
1161
+ dismissBubble(editorFromView);
1162
+ return true;
1163
+ }
1164
+ const openSuggestion = suggestionRef.current;
1165
+ if (openSuggestion !== null) {
1166
+ exitSuggestionPlugin(view, openSuggestion.kind);
1167
+ return true;
1168
+ }
1169
+ const cancel = latest.current.onCancel;
1170
+ if (cancel !== void 0) {
1171
+ cancel();
1172
+ return true;
1173
+ }
1174
+ return false;
1175
+ }
1176
+ if (mod && event.key === "Enter") {
1177
+ requestSubmit(editorFromView);
1178
+ return true;
1179
+ }
1180
+ const navigating = suggestionRef.current;
1181
+ if (navigating !== null && !mod && !event.altKey && !event.shiftKey) {
1182
+ const count = navigating.attrs.length;
1183
+ if (event.key === "ArrowDown") {
1184
+ setActiveOption(wrapIndex(activeIndexRef.current, count, 1));
1185
+ return true;
1186
+ }
1187
+ if (event.key === "ArrowUp") {
1188
+ setActiveOption(wrapIndex(activeIndexRef.current, count, -1));
1189
+ return true;
1190
+ }
1191
+ if (event.key === "Enter" || event.key === "Tab") {
1192
+ commitSuggestion(activeIndexRef.current);
1193
+ return true;
1194
+ }
1195
+ }
1196
+ if (mod && event.shiftKey && !event.altKey && event.key.toLowerCase() === "x") {
1197
+ editorFromView.chain().focus().toggleStrike().run();
1198
+ return true;
1199
+ }
1200
+ if (mod && event.key === ".") {
1201
+ if (!shouldShowBubble(editorFromView)) return false;
1202
+ dismissedSelection.current = null;
1203
+ setBubble(true);
1204
+ setLinkMode(false);
1205
+ setRovingIndex(0);
1206
+ setFocusRequest("toolbar");
1207
+ return true;
1208
+ }
1209
+ if (mod && !event.shiftKey && !event.altKey && event.key.toLowerCase() === "k") {
1210
+ if (editorFromView.isActive("link") && view.state.selection.empty) {
1211
+ editorFromView.commands.extendMarkRange("link");
1212
+ }
1213
+ if (!shouldShowBubble(editorFromView)) return true;
1214
+ dismissedSelection.current = null;
1215
+ setBubble(true);
1216
+ setLinkDraft(readHref(editorFromView));
1217
+ setLinkError(false);
1218
+ setLinkMode(true);
1219
+ setFocusRequest("link");
1220
+ return true;
1221
+ }
1222
+ return false;
1223
+ },
1224
+ handlePaste: (_view, event) => {
1225
+ const dt = event.clipboardData;
1226
+ if (!dt || dt.files.length > 0 || dt.types.includes("text/html")) return false;
1227
+ return attachments.addPastedText(dt.getData("text/plain"));
1228
+ },
1229
+ clipboardTextParser: (text, _context, plain, view) => parsePastedText(text, plain, view)
1230
+ },
1231
+ onCreate: ({ editor: created }) => {
1232
+ editorRef.current = created;
1233
+ },
1234
+ onUpdate: ({ editor: updated }) => {
1235
+ setPendingSubmit(null);
1236
+ scheduleEmit(updated);
1237
+ syncBubble(updated);
1238
+ },
1239
+ onSelectionUpdate: ({ editor: updated }) => {
1240
+ syncBubble(updated);
1241
+ },
1242
+ onFocus: ({ editor: focused }) => {
1243
+ syncBubble(focused);
1244
+ },
1245
+ onBlur: ({ editor: blurred, event }) => {
1246
+ flushEmit(blurred);
1247
+ const next = event.relatedTarget;
1248
+ const insideMenu = next instanceof Node && bubbleRef.current?.contains(next) === true;
1249
+ if (!insideMenu) setBubble(false);
1250
+ const openSuggestion = suggestionRef.current;
1251
+ if (openSuggestion !== null) exitSuggestionPlugin(blurred.view, openSuggestion.kind);
1252
+ }
1253
+ },
1254
+ []
1255
+ );
1256
+ useLayoutEffect(() => {
1257
+ editorRef.current = editor;
1258
+ }, [editor]);
1259
+ useEffect(() => {
1260
+ onEditor?.(editor);
1261
+ return () => {
1262
+ onEditor?.(null);
1263
+ };
1264
+ }, [editor, onEditor]);
1265
+ useEffect(() => {
1266
+ editor.setEditable(!disabled);
1267
+ }, [disabled, editor]);
1268
+ const insertPastedText = useCallback(
1269
+ (localId) => {
1270
+ const text = attachments.insertAsText(localId);
1271
+ if (text === null) return;
1272
+ const slice = parsePastedText(text, true, editor.view);
1273
+ editor.view.dispatch(editor.state.tr.replaceSelection(slice).scrollIntoView());
1274
+ editor.commands.focus();
1275
+ },
1276
+ [attachments, editor, parsePastedText]
1277
+ );
1278
+ useEffect(() => {
1279
+ if (value === lastEmitted.current) return;
1280
+ clearEmitTimer();
1281
+ lastEmitted.current = value;
1282
+ editor.commands.setContent(value, { contentType: "markdown", emitUpdate: false });
1283
+ }, [clearEmitTimer, editor, value]);
1284
+ useEffect(() => {
1285
+ if (pendingSubmit === null || value !== pendingSubmit || attachments.isUploading) return;
1286
+ if (attachments.attachments.some((attachment) => attachment.state !== "ready")) return;
1287
+ setPendingSubmit(null);
1288
+ onSubmit(attachments.ticketIds.length === 0 ? void 0 : attachments.ticketIds);
1289
+ attachments.clear();
1290
+ }, [attachments, onSubmit, pendingSubmit, value]);
1291
+ useEffect(
1292
+ () => () => {
1293
+ if (emitTimer.current === null) return;
1294
+ window.clearTimeout(emitTimer.current);
1295
+ emitTimer.current = null;
1296
+ if (latest.current.value === lastEmitted.current && !editor.isDestroyed) emitNow(editor);
1297
+ },
1298
+ [editor, emitNow]
1299
+ );
1300
+ const announced = useRef(false);
1301
+ useEffect(() => {
1302
+ if (maxLength === void 0) return;
1303
+ const over = value.length > maxLength * 0.9;
1304
+ if (over && !announced.current) {
1305
+ announced.current = true;
1306
+ announce?.(
1307
+ `Approaching the comment length limit: ${String(value.length)} of ${String(maxLength)} characters`
1308
+ );
1309
+ } else if (!over) {
1310
+ announced.current = false;
1311
+ }
1312
+ }, [announce, maxLength, value.length]);
1313
+ useLayoutEffect(() => {
1314
+ if (editor.isDestroyed) return;
1315
+ const dom = editor.view.dom;
1316
+ if (suggestion === null) {
1317
+ dom.removeAttribute("aria-controls");
1318
+ dom.removeAttribute("aria-activedescendant");
1319
+ return;
1320
+ }
1321
+ dom.setAttribute("aria-controls", listboxId);
1322
+ dom.setAttribute("aria-activedescendant", optionDomId(activeIndex));
1323
+ }, [activeIndex, editor, listboxId, optionDomId, suggestion]);
1324
+ useEffect(() => {
1325
+ if (focusRequest === null || !bubbleOpen) return;
1326
+ const target = focusRequest === "link" ? linkInputRef.current : focusableButtons(bubbleRef.current)[0];
1327
+ if (target instanceof HTMLElement) {
1328
+ target.focus();
1329
+ setFocusRequest(null);
1330
+ }
1331
+ }, [bubbleOpen, focusRequest, linkMode]);
1332
+ const active = useEditorState({
1333
+ editor,
1334
+ selector: ({ editor: current }) => ({
1335
+ isEmpty: current.isEmpty,
1336
+ bold: current.isActive("bold"),
1337
+ italic: current.isActive("italic"),
1338
+ underline: current.isActive("underline"),
1339
+ strike: current.isActive("strike"),
1340
+ code: current.isActive("code"),
1341
+ link: current.isActive("link"),
1342
+ h1: current.isActive("heading", { level: 1 }),
1343
+ h2: current.isActive("heading", { level: 2 }),
1344
+ h3: current.isActive("heading", { level: 3 }),
1345
+ bulletList: current.isActive("bulletList"),
1346
+ orderedList: current.isActive("orderedList"),
1347
+ blockquote: current.isActive("blockquote"),
1348
+ codeBlock: current.isActive("codeBlock")
1349
+ })
1350
+ });
1351
+ const overLimit = maxLength !== void 0 && value.length > maxLength;
1352
+ const canSend = !active.isEmpty && !sending && !disabled && !overLimit && pendingSubmit === null && !attachments.attachments.some((attachment) => attachment.state === "failed");
1353
+ const openLinkField = useCallback(() => {
1354
+ setLinkDraft(readHref(editor));
1355
+ setLinkError(false);
1356
+ setLinkMode(true);
1357
+ setFocusRequest("link");
1358
+ }, [editor]);
1359
+ const applyLink = useCallback(() => {
1360
+ const href = normalizeHref(linkDraft);
1361
+ if (href === null) {
1362
+ setLinkError(true);
1363
+ return;
1364
+ }
1365
+ const applied = editor.chain().focus().extendMarkRange("link").setLink({ href }).run();
1366
+ if (!applied) {
1367
+ setLinkError(true);
1368
+ return;
1369
+ }
1370
+ setLinkMode(false);
1371
+ setLinkError(false);
1372
+ }, [editor, linkDraft]);
1373
+ const removeLink = useCallback(() => {
1374
+ editor.chain().focus().extendMarkRange("link").unsetLink().run();
1375
+ setLinkMode(false);
1376
+ setLinkError(false);
1377
+ }, [editor]);
1378
+ const closeLinkField = useCallback(() => {
1379
+ setLinkMode(false);
1380
+ setLinkError(false);
1381
+ setRovingIndex(BUBBLE_ACTIONS.length - 1);
1382
+ setFocusRequest("toolbar");
1383
+ }, []);
1384
+ const onToolbarKeyDown = useCallback(
1385
+ (event) => {
1386
+ const buttons = focusableButtons(event.currentTarget);
1387
+ const current = buttons.findIndex((button) => button === document.activeElement);
1388
+ const move = (index) => {
1389
+ const next = buttons[(index + buttons.length) % buttons.length];
1390
+ if (next === void 0) return;
1391
+ setRovingIndex(buttons.indexOf(next));
1392
+ next.focus();
1393
+ };
1394
+ switch (event.key) {
1395
+ case "ArrowRight":
1396
+ case "ArrowDown":
1397
+ event.preventDefault();
1398
+ move(current + 1);
1399
+ return;
1400
+ case "ArrowLeft":
1401
+ case "ArrowUp":
1402
+ event.preventDefault();
1403
+ move(current - 1);
1404
+ return;
1405
+ case "Home":
1406
+ event.preventDefault();
1407
+ move(0);
1408
+ return;
1409
+ case "End":
1410
+ event.preventDefault();
1411
+ move(buttons.length - 1);
1412
+ return;
1413
+ case "Escape":
1414
+ event.preventDefault();
1415
+ event.stopPropagation();
1416
+ dismissBubble(editor);
1417
+ editor.commands.focus();
1418
+ return;
1419
+ default:
1420
+ return;
1421
+ }
1422
+ },
1423
+ [dismissBubble, editor]
1424
+ );
1425
+ const onLinkFieldKeyDown = useCallback(
1426
+ (event) => {
1427
+ if (event.key === "Enter") {
1428
+ event.preventDefault();
1429
+ applyLink();
1430
+ } else if (event.key === "Escape") {
1431
+ event.preventDefault();
1432
+ event.stopPropagation();
1433
+ closeLinkField();
1434
+ }
1435
+ },
1436
+ [applyLink, closeLinkField]
1437
+ );
1438
+ const bubbleActive = {
1439
+ bold: active.bold,
1440
+ italic: active.italic,
1441
+ underline: active.underline,
1442
+ strike: active.strike,
1443
+ code: active.code,
1444
+ link: active.link
1445
+ };
1446
+ const runBubbleAction = useCallback(
1447
+ (action) => {
1448
+ switch (action) {
1449
+ case "bold":
1450
+ editor.chain().focus().toggleBold().run();
1451
+ return;
1452
+ case "italic":
1453
+ editor.chain().focus().toggleItalic().run();
1454
+ return;
1455
+ case "underline":
1456
+ editor.chain().focus().toggleUnderline().run();
1457
+ return;
1458
+ case "strike":
1459
+ editor.chain().focus().toggleStrike().run();
1460
+ return;
1461
+ case "code":
1462
+ editor.chain().focus().toggleCode().run();
1463
+ return;
1464
+ case "link":
1465
+ openLinkField();
1466
+ return;
1467
+ }
1468
+ },
1469
+ [editor, openLinkField]
1470
+ );
1471
+ const runBlockAction = useCallback(
1472
+ (action) => {
1473
+ const chain = editor.chain().focus();
1474
+ switch (action) {
1475
+ case "h1":
1476
+ chain.toggleHeading({ level: 1 }).run();
1477
+ return;
1478
+ case "h2":
1479
+ chain.toggleHeading({ level: 2 }).run();
1480
+ return;
1481
+ case "h3":
1482
+ chain.toggleHeading({ level: 3 }).run();
1483
+ return;
1484
+ case "bulletList":
1485
+ chain.toggleBulletList().run();
1486
+ return;
1487
+ case "orderedList":
1488
+ chain.toggleOrderedList().run();
1489
+ return;
1490
+ case "blockquote":
1491
+ chain.toggleBlockquote().run();
1492
+ return;
1493
+ case "codeBlock":
1494
+ chain.toggleCodeBlock().run();
1495
+ return;
1496
+ case "divider":
1497
+ chain.setHorizontalRule().run();
1498
+ return;
1499
+ }
1500
+ },
1501
+ [editor]
1502
+ );
1503
+ const blockActive = {
1504
+ h1: active.h1,
1505
+ h2: active.h2,
1506
+ h3: active.h3,
1507
+ bulletList: active.bulletList,
1508
+ orderedList: active.orderedList,
1509
+ blockquote: active.blockquote,
1510
+ codeBlock: active.codeBlock,
1511
+ divider: false
1512
+ };
1513
+ const shellStyle = {
1514
+ // Concentric: the host panel declares --thread-inner-radius as its own
1515
+ // radius minus its padding. Nothing here eyeballs a corner.
1516
+ borderRadius: "var(--thread-inner-radius, var(--radius-sm))",
1517
+ borderColor: "rgb(var(--border))",
1518
+ background: "rgb(var(--surface-card))",
1519
+ "--composer-rows": String(rows)
1520
+ };
1521
+ const controlsDisabled = disabled || sending || pendingSubmit !== null;
1522
+ return /* @__PURE__ */ jsxs("div", { className: cn("flex flex-col gap-[var(--space-1)]", className), children: [
1523
+ replyingTo === void 0 ? null : /* @__PURE__ */ jsxs(
1524
+ "p",
1525
+ {
1526
+ className: "flex items-center gap-[var(--space-1)] text-[12px]",
1527
+ style: { color: "rgb(var(--text-tertiary))" },
1528
+ children: [
1529
+ /* @__PURE__ */ jsx(CornerDownRight, { size: 10, strokeWidth: 1.5, "aria-hidden": "true" }),
1530
+ "Replying to",
1531
+ " ",
1532
+ /* @__PURE__ */ jsx("span", { className: "font-semibold", style: { color: "rgb(var(--text-secondary))" }, children: replyingTo })
1533
+ ]
1534
+ }
1535
+ ),
1536
+ /* @__PURE__ */ jsxs("div", { className: "flex items-start gap-[var(--space-2)]", children: [
1537
+ avatar,
1538
+ /* @__PURE__ */ jsx(
1539
+ AttachmentDropZone,
1540
+ {
1541
+ onFiles: attachments.addFiles,
1542
+ disabled: controlsDisabled || uploadAttachment === void 0,
1543
+ className: "min-w-0 flex-1",
1544
+ children: /* @__PURE__ */ jsxs(
1545
+ "div",
1546
+ {
1547
+ ref: shellRef,
1548
+ className: cn(
1549
+ "ds-thread-rich-composer relative min-w-0 w-full border",
1550
+ "transition-[border-color,box-shadow] duration-[var(--dur-fast)] ease-[var(--ease-out)]",
1551
+ "motion-reduce:transition-none",
1552
+ "focus-within:border-[rgb(var(--accent))] focus-within:[box-shadow:var(--ring-focus)]"
1553
+ ),
1554
+ style: shellStyle,
1555
+ children: [
1556
+ /* @__PURE__ */ jsx("span", { id: labelId, className: "sr-only", children: label }),
1557
+ /* @__PURE__ */ jsx("span", { id: hintId, className: "sr-only", children: `${MOD_NAME}+Enter sends. ${MOD_NAME}+Period opens the formatting menu on a selection. Type @ to mention someone, or a colon and two letters for emoji, then use the arrow keys and Enter. Escape closes the menu${onCancel === void 0 ? "" : ", then cancels the reply"}.` }),
1558
+ /* @__PURE__ */ jsx(EditorContent, { editor }),
1559
+ suggestion === null ? null : createPortal(
1560
+ /* @__PURE__ */ jsx(
1561
+ SuggestionList,
1562
+ {
1563
+ id: listboxId,
1564
+ label: SUGGESTION_LABELS[suggestion.kind],
1565
+ options: suggestion.options,
1566
+ activeIndex,
1567
+ optionId: optionDomId,
1568
+ onSelect: commitSuggestion,
1569
+ onActivate: setActiveOption
1570
+ }
1571
+ ),
1572
+ suggestion.container
1573
+ ),
1574
+ /* @__PURE__ */ jsx(
1575
+ BubbleMenu,
1576
+ {
1577
+ ref: bubbleRef,
1578
+ editor,
1579
+ pluginKey: BUBBLE_MENU_KEY,
1580
+ updateDelay: 0,
1581
+ shouldShow: ({ editor: current }) => shouldShowBubble(current),
1582
+ appendTo: () => shellRef.current ?? document.body,
1583
+ options: {
1584
+ placement: "top",
1585
+ offset: readSpaceToken(shellRef.current, "--space-2", 8),
1586
+ flip: true,
1587
+ shift: true
1588
+ },
1589
+ className: "ds-thread-bubble-menu",
1590
+ "data-open": bubbleOpen,
1591
+ children: !bubbleOpen ? null : linkMode ? /* @__PURE__ */ jsxs(
1592
+ "div",
1593
+ {
1594
+ role: "group",
1595
+ "aria-label": "Link",
1596
+ className: "flex items-center gap-[var(--space-1)]",
1597
+ style: {
1598
+ padding: "var(--space-1)",
1599
+ background: "rgb(var(--surface-card))",
1600
+ border: "1px solid rgb(var(--border))",
1601
+ borderRadius: "var(--radius-sm)",
1602
+ boxShadow: "var(--shadow-lg)"
1603
+ },
1604
+ children: [
1605
+ /* @__PURE__ */ jsx(
1606
+ "input",
1607
+ {
1608
+ ref: linkInputRef,
1609
+ type: "url",
1610
+ "aria-label": "Link URL",
1611
+ "aria-invalid": linkError,
1612
+ ...linkError ? { "aria-describedby": linkErrorId } : {},
1613
+ placeholder: "https://",
1614
+ value: linkDraft,
1615
+ onChange: (event) => {
1616
+ setLinkDraft(event.target.value);
1617
+ setLinkError(false);
1618
+ },
1619
+ onKeyDown: onLinkFieldKeyDown,
1620
+ className: "min-w-0 bg-transparent text-[12.5px] outline-none",
1621
+ style: {
1622
+ inlineSize: "22ch",
1623
+ blockSize: "var(--ctrl-sm)",
1624
+ paddingInline: "var(--space-2)",
1625
+ color: "rgb(var(--foreground))",
1626
+ borderRadius: "var(--radius-xs)",
1627
+ border: linkError ? "1px solid rgb(var(--warning))" : "1px solid rgb(var(--border))"
1628
+ }
1629
+ }
1630
+ ),
1631
+ /* @__PURE__ */ jsx(
1632
+ Button,
1633
+ {
1634
+ variant: "icon",
1635
+ size: "sm",
1636
+ "aria-label": "Apply link",
1637
+ title: "Apply link",
1638
+ onMouseDown: (event) => {
1639
+ event.preventDefault();
1640
+ },
1641
+ onClick: applyLink,
1642
+ children: /* @__PURE__ */ jsx(Check, { size: 12, strokeWidth: 2, "aria-hidden": "true" })
1643
+ }
1644
+ ),
1645
+ active.link ? /* @__PURE__ */ jsx(
1646
+ Button,
1647
+ {
1648
+ variant: "icon",
1649
+ size: "sm",
1650
+ "aria-label": "Remove link",
1651
+ title: "Remove link",
1652
+ onMouseDown: (event) => {
1653
+ event.preventDefault();
1654
+ },
1655
+ onClick: removeLink,
1656
+ children: /* @__PURE__ */ jsx(Unlink, { size: 12, strokeWidth: 1.75, "aria-hidden": "true" })
1657
+ }
1658
+ ) : null,
1659
+ /* @__PURE__ */ jsx(
1660
+ Button,
1661
+ {
1662
+ variant: "icon",
1663
+ size: "sm",
1664
+ "aria-label": "Close link field",
1665
+ title: "Close",
1666
+ onMouseDown: (event) => {
1667
+ event.preventDefault();
1668
+ },
1669
+ onClick: closeLinkField,
1670
+ children: /* @__PURE__ */ jsx(X, { size: 12, strokeWidth: 1.75, "aria-hidden": "true" })
1671
+ }
1672
+ ),
1673
+ linkError ? /* @__PURE__ */ jsx("span", { id: linkErrorId, role: "alert", className: "sr-only", children: "Enter a web, email or phone link." }) : null
1674
+ ]
1675
+ }
1676
+ ) : /* @__PURE__ */ jsx(
1677
+ "div",
1678
+ {
1679
+ role: "toolbar",
1680
+ "aria-label": "Format selection",
1681
+ "aria-orientation": "horizontal",
1682
+ onKeyDown: onToolbarKeyDown,
1683
+ children: BUBBLE_ACTIONS.map((action, index) => {
1684
+ const Icon = action.icon;
1685
+ return /* @__PURE__ */ jsx(
1686
+ Button,
1687
+ {
1688
+ variant: "icon",
1689
+ size: "sm",
1690
+ "aria-label": action.label,
1691
+ "aria-pressed": bubbleActive[action.id],
1692
+ title: `${action.label} (${MOD_NAME}+${action.keycap})`,
1693
+ tabIndex: index === rovingIndex ? 0 : -1,
1694
+ onMouseDown: (event) => {
1695
+ event.preventDefault();
1696
+ },
1697
+ onClick: () => {
1698
+ runBubbleAction(action.id);
1699
+ },
1700
+ children: /* @__PURE__ */ jsx(Icon, { size: 12, strokeWidth: 1.75, "aria-hidden": "true" })
1701
+ },
1702
+ action.id
1703
+ );
1704
+ })
1705
+ }
1706
+ )
1707
+ }
1708
+ ),
1709
+ /* @__PURE__ */ jsx(
1710
+ AttachmentTray,
1711
+ {
1712
+ attachments: attachments.attachments,
1713
+ onRemove: attachments.remove,
1714
+ onInsertAsText: insertPastedText,
1715
+ className: "px-[var(--space-2)]"
1716
+ }
1717
+ ),
1718
+ /* @__PURE__ */ jsx(
1719
+ ComposerFooter,
1720
+ {
1721
+ ...mode === void 0 ? {} : { mode },
1722
+ ...onModeChange === void 0 ? {} : { onModeChange },
1723
+ modeDisabled: controlsDisabled || attachments.attachments.length > 0,
1724
+ toolbar: /* @__PURE__ */ jsxs(Fragment$1, { children: [
1725
+ uploadAttachment === void 0 ? null : /* @__PURE__ */ jsx(
1726
+ AttachmentButton,
1727
+ {
1728
+ onFiles: attachments.addFiles,
1729
+ accept: attachmentAccept ?? ATTACHMENT_ACCEPT,
1730
+ disabled: controlsDisabled
1731
+ }
1732
+ ),
1733
+ attachGif === void 0 || searchGifs === void 0 ? null : /* @__PURE__ */ jsxs(Popover, { open: gifPickerOpen, onOpenChange: setGifPickerOpen, children: [
1734
+ /* @__PURE__ */ jsx(Popover.Trigger, { asChild: true, children: /* @__PURE__ */ jsx(
1735
+ Button,
1736
+ {
1737
+ variant: "icon",
1738
+ size: "sm",
1739
+ "aria-label": "Choose a GIF",
1740
+ title: "Choose a GIF",
1741
+ disabled: controlsDisabled,
1742
+ children: /* @__PURE__ */ jsx(Images, { size: 12, strokeWidth: 1.75, "aria-hidden": "true" })
1743
+ }
1744
+ ) }),
1745
+ /* @__PURE__ */ jsx(
1746
+ Popover.Content,
1747
+ {
1748
+ "aria-label": "Choose a GIF",
1749
+ align: "start",
1750
+ className: "p-[var(--space-3)]",
1751
+ style: {
1752
+ inlineSize: "calc(var(--space-12) * 8)",
1753
+ maxInlineSize: "calc(100vw - var(--space-6))"
1754
+ },
1755
+ children: /* @__PURE__ */ jsx(
1756
+ GifPicker,
1757
+ {
1758
+ search: searchGifs,
1759
+ onPick: (gif) => {
1760
+ attachments.addGif(gif);
1761
+ setGifPickerOpen(false);
1762
+ }
1763
+ }
1764
+ )
1765
+ }
1766
+ )
1767
+ ] }),
1768
+ toolbar ? BLOCK_ACTIONS.map((action) => {
1769
+ const Icon = action.icon;
1770
+ return /* @__PURE__ */ jsx(
1771
+ Button,
1772
+ {
1773
+ variant: "icon",
1774
+ size: "sm",
1775
+ "aria-label": action.label,
1776
+ title: action.label,
1777
+ ...action.id === "divider" ? {} : { "aria-pressed": blockActive[action.id] },
1778
+ disabled: controlsDisabled,
1779
+ onMouseDown: (event) => {
1780
+ event.preventDefault();
1781
+ },
1782
+ onClick: () => {
1783
+ runBlockAction(action.id);
1784
+ },
1785
+ children: /* @__PURE__ */ jsx(Icon, { size: 12, strokeWidth: 1.75, "aria-hidden": "true" })
1786
+ },
1787
+ action.id
1788
+ );
1789
+ }) : null
1790
+ ] }),
1791
+ meta: /* @__PURE__ */ jsx(Fragment$1, { children: maxLength === void 0 ? null : /* @__PURE__ */ jsxs(
1792
+ "span",
1793
+ {
1794
+ className: "tabular-nums text-right",
1795
+ style: {
1796
+ minInlineSize: "7ch",
1797
+ fontSize: "11.5px",
1798
+ color: value.length > maxLength * 0.9 ? "rgb(var(--warning-ink))" : "rgb(var(--text-tertiary))"
1799
+ },
1800
+ children: [
1801
+ value.length,
1802
+ "/",
1803
+ maxLength
1804
+ ]
1805
+ }
1806
+ ) }),
1807
+ actions: /* @__PURE__ */ jsxs(Fragment$1, { children: [
1808
+ onCancel === void 0 ? null : /* @__PURE__ */ jsx(Button, { variant: "secondary", size: "sm", onClick: onCancel, disabled: controlsDisabled, children: cancelLabel }),
1809
+ /* @__PURE__ */ jsx(
1810
+ Button,
1811
+ {
1812
+ variant: "primary",
1813
+ size: "sm",
1814
+ onClick: () => {
1815
+ requestSubmit(editor);
1816
+ },
1817
+ disabled: !canSend,
1818
+ "aria-label": sending ? "Sending\u2026" : submitLabel,
1819
+ trailingIcon: /* @__PURE__ */ jsx("span", { "aria-hidden": "true", className: "ds-composer-keycap", children: /* @__PURE__ */ jsx(Kbd, { size: "sm", tone: "on-accent", children: SEND_KEYCAP }) }),
1820
+ children: sending ? "Sending\u2026" : submitLabel
1821
+ }
1822
+ )
1823
+ ] })
1824
+ }
1825
+ )
1826
+ ]
1827
+ }
1828
+ )
1829
+ }
1830
+ )
1831
+ ] })
1832
+ ] });
1833
+ }
1834
+ function readHref(editor) {
1835
+ const href = editor.getAttributes("link")["href"];
1836
+ return typeof href === "string" ? href : "";
1837
+ }
1838
+ var BUBBLE_ACTIONS = [
1839
+ { id: "bold", label: "Bold", icon: Bold, keycap: "B" },
1840
+ { id: "italic", label: "Italic", icon: Italic, keycap: "I" },
1841
+ { id: "underline", label: "Underline", icon: Underline, keycap: "U" },
1842
+ { id: "strike", label: "Strikethrough", icon: Strikethrough, keycap: "Shift+X" },
1843
+ { id: "code", label: "Code", icon: Code, keycap: "E" },
1844
+ { id: "link", label: "Link", icon: Link, keycap: "K" }
1845
+ ];
1846
+ var BLOCK_ACTIONS = [
1847
+ { id: "h1", label: "Heading 1", icon: Heading1 },
1848
+ { id: "h2", label: "Heading 2", icon: Heading2 },
1849
+ { id: "h3", label: "Heading 3", icon: Heading3 },
1850
+ { id: "bulletList", label: "Bulleted list", icon: List },
1851
+ { id: "orderedList", label: "Numbered list", icon: ListOrdered },
1852
+ { id: "blockquote", label: "Quote", icon: TextQuote },
1853
+ { id: "codeBlock", label: "Code block", icon: SquareCode },
1854
+ { id: "divider", label: "Divider", icon: Minus }
1855
+ ];
1856
+
1857
+ export { DRAFT_DEBOUNCE_MS, FAIL_CLOSED_NOTICE, RichThreadComposerImpl as default };
1858
+ //# sourceMappingURL=rich-composer-impl-5NO443A6.js.map
1859
+ //# sourceMappingURL=rich-composer-impl-5NO443A6.js.map