@tangle-network/sandbox-ui 0.106.0 → 0.108.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tangle-network/sandbox-ui",
3
- "version": "0.106.0",
3
+ "version": "0.108.0",
4
4
  "description": "Unified UI component library for Tangle Sandbox — primitives, chat, dashboard, terminal, editor, and workspace components",
5
5
  "repository": {
6
6
  "type": "git",
@@ -174,10 +174,8 @@
174
174
  "@radix-ui/react-dropdown-menu": "^2.1.24",
175
175
  "@radix-ui/react-select": "^2.3.7",
176
176
  "@tiptap/core": "^3.28.0",
177
- "@tiptap/extension-mention": "^3.28.0",
178
177
  "@tiptap/react": "^3.28.0",
179
178
  "@tiptap/starter-kit": "^3.28.0",
180
- "@tiptap/suggestion": "^3.28.0",
181
179
  "diff": "^9.0.0",
182
180
  "eventsource-parser": "3.1.0",
183
181
  "lucide-react": "^1.25.0",
@@ -1,12 +0,0 @@
1
- import {
2
- cn
3
- } from "./chunk-EI44GEQ5.js";
4
-
5
- // src/chat/mention-pill.ts
6
- var MENTION_PILL_CLASS = cn(
7
- "rounded-md bg-primary/10 px-1 py-0.5 font-medium text-primary"
8
- );
9
-
10
- export {
11
- MENTION_PILL_CLASS
12
- };
@@ -1,490 +0,0 @@
1
- import {
2
- MENTION_PILL_CLASS
3
- } from "./chunk-WGOOQAW3.js";
4
- import {
5
- cn
6
- } from "./chunk-EI44GEQ5.js";
7
- import "./chunk-CZ7CSFO4.js";
8
-
9
- // src/chat/mention-editor.tsx
10
- import { mergeAttributes } from "@tiptap/core";
11
- import Mention from "@tiptap/extension-mention";
12
- import { EditorContent, useEditor } from "@tiptap/react";
13
- import StarterKit from "@tiptap/starter-kit";
14
- import { exitSuggestion } from "@tiptap/suggestion";
15
- import * as React2 from "react";
16
-
17
- // src/chat/mention-list.tsx
18
- import { Loader2 } from "lucide-react";
19
- import * as React from "react";
20
- import { Fragment, jsx, jsxs } from "react/jsx-runtime";
21
- var MentionList = React.forwardRef(
22
- function MentionList2({
23
- items,
24
- loading,
25
- error,
26
- emptyText = "No matches",
27
- renderItem,
28
- onSelect,
29
- className
30
- }, ref) {
31
- const [selected, setSelected] = React.useState(0);
32
- const selectedRef = React.useRef(0);
33
- const move = (next) => {
34
- selectedRef.current = next;
35
- setSelected(next);
36
- };
37
- React.useEffect(() => {
38
- move(0);
39
- }, [items]);
40
- React.useImperativeHandle(
41
- ref,
42
- () => ({
43
- onKeyDown(event) {
44
- const count = items.length;
45
- switch (event.key) {
46
- case "ArrowDown":
47
- if (count > 0) move((selectedRef.current + 1) % count);
48
- return true;
49
- case "ArrowUp":
50
- if (count > 0) move((selectedRef.current - 1 + count) % count);
51
- return true;
52
- case "Enter":
53
- case "Tab":
54
- if (count > 0) onSelect(items[selectedRef.current]);
55
- return true;
56
- default:
57
- return false;
58
- }
59
- }
60
- }),
61
- [items, onSelect]
62
- );
63
- return /* @__PURE__ */ jsxs(
64
- "div",
65
- {
66
- role: "listbox",
67
- "aria-label": "File mentions",
68
- className: cn(
69
- "max-h-64 min-w-[16rem] max-w-sm overflow-y-auto rounded-xl border border-border",
70
- "bg-popover text-popover-foreground p-1 shadow-lg",
71
- className
72
- ),
73
- children: [
74
- loading && /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2 px-2.5 py-2 text-muted-foreground text-sm", children: [
75
- /* @__PURE__ */ jsx(Loader2, { className: "size-3.5 animate-spin", "data-motion": "essential" }),
76
- "Searching\u2026"
77
- ] }),
78
- !loading && error && /* @__PURE__ */ jsx("div", { className: "px-2.5 py-2 text-[var(--status-error,#ff4d6d)] text-sm", children: "Couldn\u2019t load matches" }),
79
- !loading && !error && items.length === 0 && /* @__PURE__ */ jsx("div", { className: "px-2.5 py-2 text-muted-foreground text-sm", children: emptyText }),
80
- !loading && !error && items.map((item, index) => {
81
- const active = index === selected;
82
- return /* @__PURE__ */ jsx(
83
- "button",
84
- {
85
- type: "button",
86
- role: "option",
87
- "aria-selected": active,
88
- onMouseDown: (event) => {
89
- event.preventDefault();
90
- onSelect(item);
91
- },
92
- onMouseEnter: () => move(index),
93
- className: cn(
94
- "flex w-full items-center gap-2 rounded-lg px-2.5 py-1.5 text-left text-sm",
95
- active ? "bg-primary/10 text-foreground" : "text-muted-foreground"
96
- ),
97
- children: renderItem ? renderItem(item) : /* @__PURE__ */ jsxs(Fragment, { children: [
98
- /* @__PURE__ */ jsx("span", { className: "truncate text-foreground", children: item.label }),
99
- item.detail && /* @__PURE__ */ jsx("span", { className: "ml-auto truncate text-muted-foreground text-xs", children: item.detail })
100
- ] })
101
- },
102
- item.id
103
- );
104
- })
105
- ]
106
- }
107
- );
108
- }
109
- );
110
-
111
- // src/chat/mention-serialize.ts
112
- var MENTION_TYPE = "mention";
113
- function serializeMentionDoc(doc) {
114
- return (doc.content ?? []).map(serializeBlock).join("\n");
115
- }
116
- function serializeBlock(node) {
117
- if (node.type === "paragraph") {
118
- return (node.content ?? []).map(serializeInline).join("");
119
- }
120
- return "";
121
- }
122
- function serializeInline(node) {
123
- if (node.type === "text") return node.text ?? "";
124
- if (node.type === "hardBreak") return "\n";
125
- if (node.type === MENTION_TYPE) return `@${node.attrs?.id ?? ""}`;
126
- return "";
127
- }
128
- function collectMentions(doc) {
129
- const out = [];
130
- const walk = (node) => {
131
- if (node.type === MENTION_TYPE && node.attrs?.id) {
132
- out.push({
133
- id: node.attrs.id,
134
- label: node.attrs.label ?? node.attrs.id,
135
- kind: node.attrs.kind ?? void 0
136
- });
137
- }
138
- for (const child of node.content ?? []) walk(child);
139
- };
140
- walk(doc);
141
- return out;
142
- }
143
- function parseMentionValue(value, known) {
144
- const lines = value.split("\n");
145
- return {
146
- type: "doc",
147
- content: lines.map((line) => ({
148
- type: "paragraph",
149
- content: parseLine(line, known)
150
- }))
151
- };
152
- }
153
- function parseLine(line, known) {
154
- const content = [];
155
- let text = "";
156
- const flush = () => {
157
- if (text) {
158
- content.push({ type: "text", text });
159
- text = "";
160
- }
161
- };
162
- let i = 0;
163
- while (i < line.length) {
164
- if (line[i] === "@") {
165
- const id = matchKnownId(line, i + 1, known);
166
- if (id) {
167
- const item = known.get(id);
168
- flush();
169
- content.push({
170
- type: MENTION_TYPE,
171
- attrs: {
172
- id: item.id,
173
- label: item.label,
174
- kind: item.kind ?? null
175
- }
176
- });
177
- i += 1 + id.length;
178
- continue;
179
- }
180
- }
181
- text += line[i];
182
- i += 1;
183
- }
184
- flush();
185
- return content;
186
- }
187
- function matchKnownId(line, pos, known) {
188
- let best = null;
189
- for (const id of known.keys()) {
190
- if (id.length === 0) continue;
191
- if (best !== null && id.length <= best.length) continue;
192
- if (!line.startsWith(id, pos)) continue;
193
- const after = pos + id.length;
194
- if (after < line.length && !/\s/.test(line[after])) continue;
195
- best = id;
196
- }
197
- return best;
198
- }
199
-
200
- // src/chat/mention-editor.tsx
201
- import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
202
- function buildComposerStarterKit() {
203
- return StarterKit.configure({
204
- blockquote: false,
205
- bold: false,
206
- bulletList: false,
207
- code: false,
208
- codeBlock: false,
209
- dropcursor: false,
210
- heading: false,
211
- horizontalRule: false,
212
- italic: false,
213
- link: false,
214
- listItem: false,
215
- listKeymap: false,
216
- orderedList: false,
217
- strike: false,
218
- trailingNode: false,
219
- underline: false
220
- });
221
- }
222
- function buildMentionExtension(trigger, suggestion) {
223
- return Mention.extend({
224
- addAttributes() {
225
- return {
226
- id: {
227
- default: null,
228
- parseHTML: (element) => element.getAttribute("data-id"),
229
- renderHTML: (attrs) => attrs.id ? { "data-id": attrs.id } : {}
230
- },
231
- label: {
232
- default: null,
233
- parseHTML: (element) => element.getAttribute("data-label"),
234
- renderHTML: (attrs) => attrs.label ? { "data-label": attrs.label } : {}
235
- },
236
- kind: {
237
- default: null,
238
- parseHTML: (element) => element.getAttribute("data-kind"),
239
- renderHTML: (attrs) => attrs.kind ? { "data-kind": attrs.kind } : {}
240
- }
241
- };
242
- }
243
- }).configure({
244
- HTMLAttributes: { class: MENTION_PILL_CLASS },
245
- renderText: ({ node }) => `${trigger}${node.attrs.id}`,
246
- renderHTML: ({ options, node }) => [
247
- "span",
248
- mergeAttributes(options.HTMLAttributes, {
249
- title: node.attrs.id ?? void 0
250
- }),
251
- `${trigger}${node.attrs.label ?? node.attrs.id}`
252
- ],
253
- suggestion
254
- });
255
- }
256
- var EDITOR_CLASS = cn(
257
- "w-full whitespace-pre-wrap break-words bg-transparent px-2 py-1",
258
- "text-foreground text-sm leading-relaxed outline-none"
259
- );
260
- var FETCH_DEBOUNCE_MS = 100;
261
- function mentionsKey(mentions) {
262
- return mentions.map((item) => item.id).join("\0");
263
- }
264
- function MentionEditor({
265
- value,
266
- onChange,
267
- onSubmit,
268
- placeholder,
269
- disabled,
270
- autoFocus,
271
- minRows,
272
- maxHeight,
273
- mention,
274
- registerFocus,
275
- onPasteFiles
276
- }) {
277
- const trigger = mention.trigger ?? "@";
278
- const [anchor, setAnchor] = React2.useState(null);
279
- const [open, setOpen] = React2.useState(false);
280
- const [query, setQuery] = React2.useState("");
281
- const [items, setItems] = React2.useState([]);
282
- const [loading, setLoading] = React2.useState(false);
283
- const [errored, setErrored] = React2.useState(false);
284
- const openRef = React2.useRef(false);
285
- const commandRef = React2.useRef(null);
286
- const listRef = React2.useRef(null);
287
- const knownRef = React2.useRef(/* @__PURE__ */ new Map());
288
- const requestIdRef = React2.useRef(0);
289
- const clientRectRef = React2.useRef(null);
290
- const lastMentionsKeyRef = React2.useRef("");
291
- const onSubmitRef = React2.useRef(onSubmit);
292
- onSubmitRef.current = onSubmit;
293
- const onChangeRef = React2.useRef(onChange);
294
- onChangeRef.current = onChange;
295
- const onMentionsChangeRef = React2.useRef(mention.onMentionsChange);
296
- onMentionsChangeRef.current = mention.onMentionsChange;
297
- const onPasteFilesRef = React2.useRef(onPasteFiles);
298
- onPasteFilesRef.current = onPasteFiles;
299
- const fetchItemsRef = React2.useRef(mention.fetchItems);
300
- fetchItemsRef.current = mention.fetchItems;
301
- const editor = useEditor(
302
- {
303
- immediatelyRender: false,
304
- editable: !disabled,
305
- autofocus: autoFocus ? "end" : false,
306
- content: parseMentionValue(value, knownRef.current),
307
- editorProps: {
308
- attributes: {
309
- class: EDITOR_CLASS,
310
- role: "textbox",
311
- "aria-multiline": "true",
312
- "aria-label": "Message input"
313
- },
314
- handleKeyDown: (_view, event) => {
315
- if (event.key !== "Enter" || event.shiftKey) return false;
316
- if (event.isComposing || event.keyCode === 229) return false;
317
- if (openRef.current) return false;
318
- event.preventDefault();
319
- onSubmitRef.current();
320
- return true;
321
- },
322
- handlePaste: (_view, event) => {
323
- const files = event.clipboardData?.files;
324
- if (files && files.length > 0 && onPasteFilesRef.current) {
325
- return onPasteFilesRef.current(files);
326
- }
327
- return false;
328
- }
329
- },
330
- onUpdate: ({ editor: editor2 }) => {
331
- const json = editor2.getJSON();
332
- onChangeRef.current(serializeMentionDoc(json));
333
- const mentions = collectMentions(json);
334
- for (const item of mentions) knownRef.current.set(item.id, item);
335
- lastMentionsKeyRef.current = mentionsKey(mentions);
336
- onMentionsChangeRef.current?.(mentions);
337
- },
338
- extensions: [
339
- buildComposerStarterKit(),
340
- buildMentionExtension(trigger, {
341
- char: trigger,
342
- allowSpaces: false,
343
- // Items are fetched by this component (so it can model loading and
344
- // error states), not by the suggestion plugin.
345
- items: () => [],
346
- command: ({ editor: editor2, range, props }) => {
347
- const item = props;
348
- editor2.chain().focus().insertContentAt(range, [
349
- {
350
- type: "mention",
351
- attrs: {
352
- id: item.id,
353
- label: item.label,
354
- kind: item.kind ?? null
355
- }
356
- },
357
- { type: "text", text: " " }
358
- ]).run();
359
- knownRef.current.set(item.id, item);
360
- },
361
- render: () => ({
362
- onStart: (props) => {
363
- openRef.current = true;
364
- commandRef.current = props.command;
365
- clientRectRef.current = props.clientRect ?? null;
366
- setOpen(true);
367
- setQuery(props.query);
368
- setAnchor(props.clientRect?.() ?? null);
369
- },
370
- onUpdate: (props) => {
371
- commandRef.current = props.command;
372
- clientRectRef.current = props.clientRect ?? null;
373
- setQuery(props.query);
374
- setAnchor(props.clientRect?.() ?? null);
375
- },
376
- onKeyDown: (props) => {
377
- if (props.event.key === "Escape") {
378
- exitSuggestion(props.view);
379
- return true;
380
- }
381
- return listRef.current?.onKeyDown(props.event) ?? false;
382
- },
383
- onExit: () => {
384
- openRef.current = false;
385
- commandRef.current = null;
386
- setOpen(false);
387
- setItems([]);
388
- setLoading(false);
389
- setErrored(false);
390
- }
391
- })
392
- })
393
- ]
394
- },
395
- []
396
- );
397
- React2.useEffect(() => {
398
- if (!open) return;
399
- const requestId = requestIdRef.current += 1;
400
- setLoading(true);
401
- setErrored(false);
402
- const timer = setTimeout(() => {
403
- Promise.resolve(fetchItemsRef.current(query)).then((results) => {
404
- if (requestId !== requestIdRef.current) return;
405
- for (const item of results) knownRef.current.set(item.id, item);
406
- setItems(results);
407
- setLoading(false);
408
- }).catch(() => {
409
- if (requestId !== requestIdRef.current) return;
410
- setErrored(true);
411
- setLoading(false);
412
- });
413
- }, FETCH_DEBOUNCE_MS);
414
- return () => clearTimeout(timer);
415
- }, [open, query]);
416
- React2.useEffect(() => {
417
- if (!open) return;
418
- const reposition = () => setAnchor(clientRectRef.current?.() ?? null);
419
- window.addEventListener("scroll", reposition, true);
420
- window.addEventListener("resize", reposition);
421
- return () => {
422
- window.removeEventListener("scroll", reposition, true);
423
- window.removeEventListener("resize", reposition);
424
- };
425
- }, [open]);
426
- React2.useEffect(() => {
427
- if (!editor) return;
428
- if (serializeMentionDoc(editor.getJSON()) === value) return;
429
- const doc = parseMentionValue(value, knownRef.current);
430
- editor.commands.setContent(doc, { emitUpdate: false });
431
- const mentions = collectMentions(doc);
432
- const key = mentionsKey(mentions);
433
- if (key !== lastMentionsKeyRef.current) {
434
- lastMentionsKeyRef.current = key;
435
- onMentionsChangeRef.current?.(mentions);
436
- }
437
- }, [editor, value]);
438
- React2.useEffect(() => {
439
- editor?.setEditable(!disabled);
440
- }, [editor, disabled]);
441
- React2.useEffect(() => {
442
- if (!editor || !registerFocus) return;
443
- registerFocus(() => editor.commands.focus());
444
- }, [editor, registerFocus]);
445
- return /* @__PURE__ */ jsxs2("div", { className: "relative", children: [
446
- /* @__PURE__ */ jsxs2(
447
- "div",
448
- {
449
- className: "overflow-y-auto",
450
- style: { maxHeight, minHeight: `${minRows * 1.5}rem` },
451
- children: [
452
- /* @__PURE__ */ jsx2(EditorContent, { editor }),
453
- value.length === 0 && /* @__PURE__ */ jsx2("div", { className: "pointer-events-none absolute top-1 left-2 text-muted-foreground text-sm leading-relaxed", children: placeholder })
454
- ]
455
- }
456
- ),
457
- open && /* @__PURE__ */ jsx2(
458
- "div",
459
- {
460
- className: "absolute z-20",
461
- style: anchorStyle(anchor),
462
- "data-testid": "mention-popover",
463
- children: /* @__PURE__ */ jsx2(
464
- MentionList,
465
- {
466
- ref: listRef,
467
- items,
468
- loading,
469
- error: errored,
470
- emptyText: mention.emptyText,
471
- renderItem: mention.renderItem,
472
- onSelect: (item) => commandRef.current?.(item),
473
- className: mention.popoverClassName
474
- }
475
- )
476
- }
477
- )
478
- ] });
479
- }
480
- function anchorStyle(rect) {
481
- if (!rect || rect.left === 0 && rect.bottom === 0) {
482
- return { position: "absolute", top: "100%", left: 0 };
483
- }
484
- return { position: "fixed", top: rect.bottom + 4, left: rect.left };
485
- }
486
- export {
487
- buildComposerStarterKit,
488
- buildMentionExtension,
489
- MentionEditor as default
490
- };