@ai-matrx/rich-editor 0.1.3 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (75) hide show
  1. package/dist/core/text-insertion.d.ts +14 -0
  2. package/dist/core/text-insertion.js +55 -0
  3. package/dist/editor/RichEditor.d.ts +4 -0
  4. package/dist/editor/RichEditor.js +14 -0
  5. package/dist/editor/RichEditorContext.d.ts +16 -0
  6. package/dist/editor/RichEditorContext.js +17 -0
  7. package/dist/editor/RichEditorImpl.d.ts +137 -0
  8. package/dist/editor/RichEditorImpl.js +714 -0
  9. package/dist/editor/caretInsert.d.ts +11 -0
  10. package/dist/editor/caretInsert.js +9 -0
  11. package/dist/editor/rich-editor.css +181 -0
  12. package/dist/format/format-actions.d.ts +10 -0
  13. package/dist/format/format-actions.js +78 -0
  14. package/dist/format/useTextareaFormatting.d.ts +4 -0
  15. package/dist/format/useTextareaFormatting.js +37 -0
  16. package/dist/in-place/EditInPlace.d.ts +38 -0
  17. package/dist/in-place/EditInPlace.js +72 -0
  18. package/dist/in-place/EditInPlaceText.d.ts +18 -0
  19. package/dist/in-place/EditInPlaceText.js +159 -0
  20. package/dist/in-place/InPlaceEditor.d.ts +41 -0
  21. package/dist/in-place/InPlaceEditor.js +169 -0
  22. package/dist/in-place/caret-handoff.js +2 -1
  23. package/dist/islands/IslandCodeEditor.d.ts +12 -0
  24. package/dist/islands/IslandCodeEditor.js +82 -0
  25. package/dist/islands/IslandPreview.d.ts +6 -0
  26. package/dist/islands/IslandPreview.js +31 -0
  27. package/dist/islands/island-meta.d.ts +28 -0
  28. package/dist/islands/island-meta.js +176 -0
  29. package/dist/panels/FindReplacePanel.d.ts +10 -0
  30. package/dist/panels/FindReplacePanel.js +118 -0
  31. package/dist/panels/KindPicker.d.ts +4 -0
  32. package/dist/panels/KindPicker.js +95 -0
  33. package/dist/panels/OutlinePanel.d.ts +6 -0
  34. package/dist/panels/OutlinePanel.js +60 -0
  35. package/dist/panels/ShortcutsDialog.d.ts +4 -0
  36. package/dist/panels/ShortcutsDialog.js +24 -0
  37. package/dist/source/SourceEditor.d.ts +28 -0
  38. package/dist/source/SourceEditor.js +429 -0
  39. package/dist/source/cm-theme.d.ts +4 -0
  40. package/dist/source/cm-theme.js +71 -0
  41. package/dist/source/live-preview.d.ts +30 -0
  42. package/dist/source/live-preview.js +304 -0
  43. package/dist/source/markdown-language.d.ts +2 -0
  44. package/dist/source/markdown-language.js +7 -0
  45. package/dist/visual/BlockHandle.d.ts +6 -0
  46. package/dist/visual/BlockHandle.js +173 -0
  47. package/dist/visual/VisualEditor.d.ts +56 -0
  48. package/dist/visual/VisualEditor.js +312 -0
  49. package/dist/visual/auto-edit.d.ts +3 -0
  50. package/dist/visual/auto-edit.js +17 -0
  51. package/dist/visual/context-menu-caret.d.ts +10 -0
  52. package/dist/visual/context-menu-caret.js +42 -0
  53. package/dist/visual/decorations.d.ts +9 -0
  54. package/dist/visual/decorations.js +131 -0
  55. package/dist/visual/format-actions.d.ts +20 -0
  56. package/dist/visual/format-actions.js +128 -0
  57. package/dist/visual/menus/SuggestionMenu.d.ts +37 -0
  58. package/dist/visual/menus/SuggestionMenu.js +121 -0
  59. package/dist/visual/nodes/CalloutView.d.ts +2 -0
  60. package/dist/visual/nodes/CalloutView.js +45 -0
  61. package/dist/visual/nodes/InlineIslandView.d.ts +2 -0
  62. package/dist/visual/nodes/InlineIslandView.js +174 -0
  63. package/dist/visual/nodes/IslandBlockView.d.ts +2 -0
  64. package/dist/visual/nodes/IslandBlockView.js +206 -0
  65. package/dist/visual/nodes/SourceLockedView.d.ts +2 -0
  66. package/dist/visual/nodes/SourceLockedView.js +62 -0
  67. package/dist/visual/shortcut-handlers.d.ts +27 -0
  68. package/dist/visual/shortcut-handlers.js +43 -0
  69. package/dist/visual/slash-items.d.ts +18 -0
  70. package/dist/visual/slash-items.js +152 -0
  71. package/dist/visual/visual-extensions.d.ts +10 -0
  72. package/dist/visual/visual-extensions.js +246 -0
  73. package/dist/visual/visual-format-target.d.ts +9 -0
  74. package/dist/visual/visual-format-target.js +43 -0
  75. package/package.json +43 -5
@@ -0,0 +1,169 @@
1
+ "use client";
2
+ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
3
+ import { useEffect, useRef, useState } from "react";
4
+ import { Maximize2, Minimize2 } from "lucide-react";
5
+ import { useIsMobile } from "@ai-matrx/design-system";
6
+ import { Button } from "@ai-matrx/design-system/controls";
7
+ import { cn } from "@ai-matrx/design-system";
8
+ import { ConfirmDialog } from "@ai-matrx/rich-content/host/app-bindings";
9
+ import RichEditor from "../editor/RichEditor.js";
10
+ import { createInPlaceSession } from "./in-place-session.js";
11
+ function InPlaceEditor({
12
+ value,
13
+ mode = "explicit",
14
+ write,
15
+ close,
16
+ caret,
17
+ minHeight,
18
+ expandable = false,
19
+ startExpanded = false,
20
+ discardDescription = "What you typed here is dropped; the saved text stays as it was.",
21
+ editor,
22
+ className,
23
+ id,
24
+ toolbarExtras,
25
+ actions
26
+ }) {
27
+ const isMobile = useIsMobile();
28
+ const [openedOn] = useState(value);
29
+ const writeRef = useRef(write);
30
+ useEffect(() => {
31
+ writeRef.current = write;
32
+ });
33
+ const [session] = useState(() => createInPlaceSession({ openedOn: value, mode, write: (text) => writeRef.current(text) }));
34
+ const [draft, setDraft] = useState(openedOn);
35
+ const [expandedChoice, setExpandedChoice] = useState(startExpanded ? true : null);
36
+ const expanded = expandable && (expandedChoice ?? isMobile);
37
+ const [confirmDiscard, setConfirmDiscard] = useState(false);
38
+ const shellRef = useRef(null);
39
+ const controller = useRef(null);
40
+ const dirty = draft !== openedOn;
41
+ useEffect(() => {
42
+ let tries = 0;
43
+ let frame = 0;
44
+ const place = () => {
45
+ const c = controller.current;
46
+ if (c && (caret ? c.placeCaret(caret) : (c.focus(), true))) return;
47
+ if (++tries > 90) {
48
+ controller.current?.focus();
49
+ return;
50
+ }
51
+ frame = requestAnimationFrame(place);
52
+ };
53
+ frame = requestAnimationFrame(place);
54
+ return () => cancelAnimationFrame(frame);
55
+ }, [caret]);
56
+ const inputSinceReport = useRef(false);
57
+ const onDraft = (text) => {
58
+ inputSinceReport.current = false;
59
+ setDraft(text);
60
+ void session.draft(text);
61
+ };
62
+ const markDocumentInput = (event) => {
63
+ if (event.target instanceof Element && event.target.closest(".ProseMirror, .cm-content")) {
64
+ inputSinceReport.current = true;
65
+ }
66
+ };
67
+ const cancel = () => {
68
+ if (dirty || inputSinceReport.current) setConfirmDiscard(true);
69
+ else close();
70
+ };
71
+ const discard = async () => {
72
+ await session.cancel();
73
+ close();
74
+ };
75
+ const onSave = async (text) => {
76
+ const result = await session.save(text);
77
+ window.setTimeout(close, 0);
78
+ return result.wrote ? result.stored : void 0;
79
+ };
80
+ const [running, setRunning] = useState(null);
81
+ const runAction = async (action) => {
82
+ const text = controller.current?.flush() ?? draft;
83
+ setRunning(action.id);
84
+ try {
85
+ await action.run(text);
86
+ close();
87
+ } catch {
88
+ } finally {
89
+ setRunning(null);
90
+ }
91
+ };
92
+ const escapeOwnedByChild = useRef(false);
93
+ const onKeyDownCapture = (event) => {
94
+ if (event.key !== "Escape") return;
95
+ const target = event.target instanceof Element ? event.target : null;
96
+ escapeOwnedByChild.current = !!document.querySelector('.react-renderer > [role="listbox"]') || !!target?.closest(".ProseMirror .cm-editor") || !shellRef.current?.querySelector('[role="tablist"][aria-label="View"]') || target instanceof HTMLInputElement || target instanceof HTMLTextAreaElement;
97
+ };
98
+ const onKeyDown = (event) => {
99
+ if (event.key !== "Escape" || escapeOwnedByChild.current) return;
100
+ if (!(event.target instanceof Node) || !shellRef.current?.contains(event.target)) return;
101
+ event.preventDefault();
102
+ cancel();
103
+ };
104
+ return /* @__PURE__ */ jsxs(
105
+ "div",
106
+ {
107
+ ref: shellRef,
108
+ onKeyDownCapture,
109
+ onInputCapture: markDocumentInput,
110
+ onBeforeInputCapture: markDocumentInput,
111
+ onKeyDown,
112
+ "data-in-place-editor": id ?? "",
113
+ style: expanded || !minHeight ? void 0 : { minHeight },
114
+ className: cn(
115
+ "flex w-full flex-col overflow-hidden",
116
+ expanded ? "fixed inset-0 z-50 h-dvh bg-background pt-safe" : "max-h-[min(80dvh,56rem)] min-h-40 rounded-lg border border-border",
117
+ !expanded && className
118
+ ),
119
+ children: [
120
+ /* @__PURE__ */ jsx(
121
+ RichEditor,
122
+ {
123
+ defaultView: "visual",
124
+ defaultOutlineOpen: false,
125
+ ...editor,
126
+ controllerRef: controller,
127
+ value: openedOn,
128
+ onChange: onDraft,
129
+ onSave,
130
+ className: cn(expanded ? "h-full" : "h-auto min-h-0", editor?.className),
131
+ onCancel: cancel,
132
+ cancelLabel: dirty ? "Cancel" : "Close",
133
+ onNothingToSave: close,
134
+ toolbarExtras: /* @__PURE__ */ jsxs(Fragment, { children: [
135
+ editor?.toolbarExtras,
136
+ toolbarExtras,
137
+ actions?.map((action) => /* @__PURE__ */ jsx(
138
+ Button,
139
+ {
140
+ variant: "outline",
141
+ disabled: running !== null,
142
+ onClick: () => void runAction(action),
143
+ children: action.label
144
+ },
145
+ action.id
146
+ )),
147
+ expandable && /* @__PURE__ */ jsx(Button, { variant: "quiet", icon: expanded ? /* @__PURE__ */ jsx(Minimize2, {}) : /* @__PURE__ */ jsx(Maximize2, {}), onClick: () => setExpandedChoice(!expanded), title: expanded ? "Back into place" : "Expand to full screen", "aria-label": expanded ? "Back into place" : "Expand to full screen" })
148
+ ] })
149
+ }
150
+ ),
151
+ /* @__PURE__ */ jsx(
152
+ ConfirmDialog,
153
+ {
154
+ open: confirmDiscard,
155
+ onOpenChange: setConfirmDiscard,
156
+ title: "Discard your edit?",
157
+ description: discardDescription,
158
+ confirmLabel: "Discard edit",
159
+ cancelLabel: "Keep editing",
160
+ onConfirm: () => void discard()
161
+ }
162
+ )
163
+ ]
164
+ }
165
+ );
166
+ }
167
+ export {
168
+ InPlaceEditor
169
+ };
@@ -1,4 +1,5 @@
1
- const pending = /* @__PURE__ */ new Map();
1
+ const PENDING_KEY = /* @__PURE__ */ Symbol.for("ai-matrx.rich-editor.in-place-caret-handoff");
2
+ const pending = globalThis[PENDING_KEY] ??= /* @__PURE__ */ new Map();
2
3
  function handInPlaceCaret(key, caret) {
3
4
  if (caret) pending.set(key, caret);
4
5
  else pending.delete(key);
@@ -0,0 +1,12 @@
1
+ import type { IslandEditorLanguage } from "./island-meta.js";
2
+ export interface IslandCodeEditorProps {
3
+ value: string;
4
+ onChange: (value: string) => void;
5
+ language: IslandEditorLanguage;
6
+ /** Leave the island editor (Escape, ⌘/Ctrl+Enter). */
7
+ onExit?: () => void;
8
+ autoFocus?: boolean;
9
+ ariaLabel: string;
10
+ className?: string;
11
+ }
12
+ export declare function IslandCodeEditor({ value, onChange, language, onExit, autoFocus, ariaLabel, className, }: IslandCodeEditorProps): import("react").JSX.Element;
@@ -0,0 +1,82 @@
1
+ "use client";
2
+ import { jsx } from "react/jsx-runtime";
3
+ import { useEffect, useEffectEvent, useRef } from "react";
4
+ import { EditorState } from "@codemirror/state";
5
+ import { EditorView, keymap } from "@codemirror/view";
6
+ import { defaultKeymap, history, historyKeymap, indentWithTab } from "@codemirror/commands";
7
+ import { json } from "@codemirror/lang-json";
8
+ import { cn } from "@ai-matrx/design-system";
9
+ import { monoTheme, richEditorTheme, richHighlight } from "../source/cm-theme.js";
10
+ import { markdownSourceLanguage } from "../source/markdown-language.js";
11
+ function IslandCodeEditor({
12
+ value,
13
+ onChange,
14
+ language,
15
+ onExit,
16
+ autoFocus,
17
+ ariaLabel,
18
+ className
19
+ }) {
20
+ const host = useRef(null);
21
+ const view = useRef(null);
22
+ const emitChange = useEffectEvent((next) => onChange(next));
23
+ const emitExit = useEffectEvent(() => {
24
+ onExit?.();
25
+ return Boolean(onExit);
26
+ });
27
+ const initialValue = useEffectEvent(() => value);
28
+ useEffect(() => {
29
+ if (!host.current) return;
30
+ const exit = () => emitExit();
31
+ const instance = new EditorView({
32
+ parent: host.current,
33
+ state: EditorState.create({
34
+ doc: initialValue(),
35
+ extensions: [
36
+ history(),
37
+ keymap.of([
38
+ { key: "Escape", run: exit },
39
+ { key: "Mod-Enter", run: exit },
40
+ indentWithTab,
41
+ ...defaultKeymap,
42
+ ...historyKeymap
43
+ ]),
44
+ language === "json" ? json() : markdownSourceLanguage,
45
+ richHighlight,
46
+ richEditorTheme,
47
+ monoTheme,
48
+ EditorView.lineWrapping,
49
+ EditorView.contentAttributes.of({ "aria-label": ariaLabel }),
50
+ EditorView.updateListener.of((update) => {
51
+ if (update.docChanged) emitChange(update.state.doc.toString());
52
+ })
53
+ ]
54
+ })
55
+ });
56
+ view.current = instance;
57
+ if (autoFocus) instance.focus();
58
+ return () => {
59
+ instance.destroy();
60
+ view.current = null;
61
+ };
62
+ }, [language, autoFocus, ariaLabel]);
63
+ useEffect(() => {
64
+ const instance = view.current;
65
+ if (!instance) return;
66
+ const current = instance.state.doc.toString();
67
+ if (current !== value) {
68
+ instance.dispatch({ changes: { from: 0, to: current.length, insert: value } });
69
+ }
70
+ }, [value]);
71
+ return /* @__PURE__ */ jsx(
72
+ "div",
73
+ {
74
+ ref: host,
75
+ className: cn("rounded-md border border-border bg-background/60 text-sm", className),
76
+ onKeyDown: (event) => event.stopPropagation()
77
+ }
78
+ );
79
+ }
80
+ export {
81
+ IslandCodeEditor
82
+ };
@@ -0,0 +1,6 @@
1
+ export interface IslandPreviewProps {
2
+ raw: string;
3
+ islandType: string;
4
+ className?: string;
5
+ }
6
+ export declare function IslandPreview({ raw, islandType, className }: IslandPreviewProps): import("react").JSX.Element;
@@ -0,0 +1,31 @@
1
+ "use client";
2
+ import { jsx, jsxs } from "react/jsx-runtime";
3
+ import { RichContent } from "@ai-matrx/rich-content/levels/RichContent";
4
+ import { PAGE_BREAK_CLASS, PAGE_BREAK_LABEL, isPageBreakLine } from "@ai-matrx/print/directives";
5
+ import { cn } from "@ai-matrx/design-system";
6
+ import { islandMeta } from "./island-meta.js";
7
+ function IslandPreview({ raw, islandType, className }) {
8
+ if (isPageBreakLine(raw.trim())) {
9
+ return /* @__PURE__ */ jsxs(
10
+ "div",
11
+ {
12
+ className: cn(PAGE_BREAK_CLASS, "my-1 flex items-center gap-3 text-xs text-muted-foreground", className),
13
+ role: "separator",
14
+ "aria-label": PAGE_BREAK_LABEL,
15
+ children: [
16
+ /* @__PURE__ */ jsx("span", { className: "h-px flex-1 border-t border-dashed border-border" }),
17
+ PAGE_BREAK_LABEL,
18
+ /* @__PURE__ */ jsx("span", { className: "h-px flex-1 border-t border-dashed border-border" })
19
+ ]
20
+ }
21
+ );
22
+ }
23
+ const meta = islandMeta(islandType, raw);
24
+ if (!meta.renders || !raw.trim()) {
25
+ return /* @__PURE__ */ jsx("pre", { className: cn("whitespace-pre-wrap break-words font-mono text-xs text-muted-foreground", className), children: raw || "(empty)" });
26
+ }
27
+ return /* @__PURE__ */ jsx("div", { className: cn("rich-editor-island-render min-w-0", className), children: /* @__PURE__ */ jsx(RichContent, { level: "full", imagePolicy: "inherit", source: raw, hideCopyButton: true, allowFullScreenEditor: false }) });
28
+ }
29
+ export {
30
+ IslandPreview
31
+ };
@@ -0,0 +1,28 @@
1
+ import { type LucideIcon } from "lucide-react";
2
+ export type IslandEditorLanguage = "markdown" | "json" | "xml" | "code" | "math";
3
+ export interface IslandMeta {
4
+ label: string;
5
+ icon: LucideIcon;
6
+ language: IslandEditorLanguage;
7
+ /** Rendered through the shared renderer (false: shown as its source). */
8
+ renders: boolean;
9
+ /** True when the type has no name of its own ("Protected block"). */
10
+ generic?: boolean;
11
+ }
12
+ /** The kind an island's source names, in any realistic spelling of the key (L-3, round 9). */
13
+ export declare function kindOf(raw: string): string | null;
14
+ /** The info string of a fence that carries one Matrx Envelope (a directive). */
15
+ export declare const MATRX_FENCE_LANGUAGE = "matrx";
16
+ /** A ```matrx fence: a reference chip or an action card, never a code block. */
17
+ export declare function isMatrxFence(islandType: string, raw: string): boolean;
18
+ /** A fence island's language — THE one code-range rule's opener parse. */
19
+ export declare function fenceLanguage(raw: string): string;
20
+ export declare function islandMeta(islandType: string, raw: string): IslandMeta;
21
+ /** Inline islands: short label for a chip. */
22
+ export declare function inlineIslandLabel(islandType: string): string;
23
+ /** Common fence languages for the picker, most used first. */
24
+ export declare const CODE_LANGUAGES: readonly ["", "python", "typescript", "javascript", "tsx", "json", "sql", "bash", "html", "css", "markdown", "yaml", "mermaid", "go", "rust", "java", "csharp", "php", "ruby", "swift", "kotlin", "xml", "text"];
25
+ /** Rewrite a fence's info string, keeping its fence characters and body bytes. */
26
+ export declare function withFenceLanguage(raw: string, language: string): string;
27
+ /** The TeX inside `$$…$$` / `\[…\]` / `$…$`. */
28
+ export declare function texOf(raw: string): string;
@@ -0,0 +1,176 @@
1
+ import { isPageBreakLine } from "@ai-matrx/print/directives";
2
+ import {
3
+ Braces,
4
+ Code,
5
+ Hash,
6
+ Info,
7
+ LayoutPanelTop,
8
+ Link2,
9
+ MousePointerClick,
10
+ CodeXml,
11
+ EyeOff,
12
+ FileCog,
13
+ FileText,
14
+ FolderTree,
15
+ Lock,
16
+ Pin,
17
+ SeparatorHorizontal,
18
+ Shapes,
19
+ Sigma,
20
+ Tags,
21
+ Workflow
22
+ } from "lucide-react";
23
+ import { fenceOpenerOf } from "@ai-matrx/content-ir/source";
24
+ import { ALL_KIND_SPELLINGS, firstKindSlug } from "@ai-matrx/content-ir/surfaces";
25
+ function kindOf(raw) {
26
+ return firstKindSlug(raw, ALL_KIND_SPELLINGS);
27
+ }
28
+ const MATRX_FENCE_LANGUAGE = "matrx";
29
+ function isMatrxFence(islandType, raw) {
30
+ return islandType === "fence" && fenceLanguage(raw) === MATRX_FENCE_LANGUAGE;
31
+ }
32
+ function fenceLanguage(raw) {
33
+ return fenceOpenerOf(raw.split("\n", 1)[0] ?? "")?.lang ?? "";
34
+ }
35
+ function xmlTag(raw) {
36
+ return /^\s*<([A-Za-z][\w:-]*)/.exec(raw)?.[1] ?? "section";
37
+ }
38
+ function islandMeta(islandType, raw) {
39
+ if (isPageBreakLine(raw.trim())) {
40
+ return { label: "Page break", icon: SeparatorHorizontal, language: "markdown", renders: false };
41
+ }
42
+ switch (islandType) {
43
+ case "fence": {
44
+ const language = fenceLanguage(raw);
45
+ const kind = kindOf(raw);
46
+ if (language === MATRX_FENCE_LANGUAGE) {
47
+ const action = !/^directive_v1_(reference|secret)_/.test(kind ?? "");
48
+ return { label: action ? "Action" : "Reference", icon: action ? MousePointerClick : Link2, language: "json", renders: true };
49
+ }
50
+ if (kind) return { label: `Kind \xB7 ${kind}`, icon: Shapes, language: "json", renders: true };
51
+ if (language === "mermaid") return { label: "Diagram", icon: Workflow, language: "code", renders: true };
52
+ if (language === "markdown" || language === "md") {
53
+ return { label: "Markdown block", icon: FileText, language: "markdown", renders: true };
54
+ }
55
+ return { label: language ? `Code \xB7 ${language}` : "Code", icon: Code, language: "code", renders: true };
56
+ }
57
+ case "json": {
58
+ const kind = kindOf(raw);
59
+ return {
60
+ label: kind ? `Kind \xB7 ${kind}` : "Structured data",
61
+ icon: kind ? Shapes : Braces,
62
+ language: "json",
63
+ renders: true
64
+ };
65
+ }
66
+ case "xml_region":
67
+ case "xml_attr":
68
+ case "xml_container":
69
+ return { label: `Section \xB7 ${xmlTag(raw)}`, icon: Tags, language: "xml", renders: true };
70
+ case "html_block":
71
+ return { label: "HTML", icon: CodeXml, language: "xml", renders: true };
72
+ case "html_comment":
73
+ return { label: "Hidden comment", icon: EyeOff, language: "xml", renders: false };
74
+ case "anchor":
75
+ return { label: "Pinned anchor", icon: Pin, language: "xml", renders: false };
76
+ case "math_block":
77
+ return { label: "Equation", icon: Sigma, language: "math", renders: true };
78
+ case "front_matter":
79
+ return { label: "Front matter", icon: FileCog, language: "code", renders: false };
80
+ case "tree":
81
+ return { label: "File tree", icon: FolderTree, language: "code", renders: true };
82
+ case "callout": {
83
+ const name = /^\s*>\s*\[!([\w-]+)\]/.exec(raw)?.[1];
84
+ return { label: name ? `Callout \xB7 ${name.toLowerCase()}` : "Callout", icon: Info, language: "markdown", renders: true };
85
+ }
86
+ case "directive": {
87
+ const name = /^\s*(?::{2,}|!!!|\?{3}\+?)\s*([\w-]+)/.exec(raw)?.[1];
88
+ return { label: name ? `Block \xB7 ${name}` : "Block", icon: LayoutPanelTop, language: "markdown", renders: true };
89
+ }
90
+ case "footnote_def": {
91
+ const id = /^\s*\[\^([^\]]+)\]:/.exec(raw)?.[1];
92
+ return { label: id ? `Footnote ${id}` : "Footnote", icon: Hash, language: "markdown", renders: true };
93
+ }
94
+ default:
95
+ return { label: "Protected block", icon: Lock, language: "markdown", renders: true, generic: true };
96
+ }
97
+ }
98
+ function inlineIslandLabel(islandType) {
99
+ switch (islandType) {
100
+ case "variable":
101
+ return "Variable";
102
+ case "math_inline":
103
+ return "Equation";
104
+ case "cite":
105
+ return "Citation";
106
+ case "md_image":
107
+ return "Image";
108
+ case "anchor":
109
+ return "Pinned anchor";
110
+ case "kind_json":
111
+ return "Kind";
112
+ case "media_ref":
113
+ return "Media";
114
+ case "wikilink":
115
+ return "Page link";
116
+ case "html_tag":
117
+ return "HTML tag";
118
+ case "xml_inline":
119
+ case "xml_tag":
120
+ return "Inline tag";
121
+ default:
122
+ return "Protected text";
123
+ }
124
+ }
125
+ const CODE_LANGUAGES = [
126
+ "",
127
+ "python",
128
+ "typescript",
129
+ "javascript",
130
+ "tsx",
131
+ "json",
132
+ "sql",
133
+ "bash",
134
+ "html",
135
+ "css",
136
+ "markdown",
137
+ "yaml",
138
+ "mermaid",
139
+ "go",
140
+ "rust",
141
+ "java",
142
+ "csharp",
143
+ "php",
144
+ "ruby",
145
+ "swift",
146
+ "kotlin",
147
+ "xml",
148
+ "text"
149
+ ];
150
+ function withFenceLanguage(raw, language) {
151
+ const newline = raw.indexOf("\n");
152
+ const first = newline === -1 ? raw : raw.slice(0, newline);
153
+ const opener = fenceOpenerOf(first);
154
+ if (!opener) return raw;
155
+ const indent = first.slice(0, first.length - first.trimStart().length);
156
+ return `${indent}${opener.char.repeat(opener.ticks)}${language}${newline === -1 ? "" : raw.slice(newline)}`;
157
+ }
158
+ function texOf(raw) {
159
+ const block = /^\$\$\s*\n?([\s\S]*?)\n?\s*\$\$$/.exec(raw.trim());
160
+ if (block) return block[1] ?? "";
161
+ const bracket = /^\\\[([\s\S]*?)\\\]$/.exec(raw.trim());
162
+ if (bracket) return (bracket[1] ?? "").trim();
163
+ const inline = /^\$([\s\S]*?)\$$/.exec(raw.trim()) ?? /^\\\(([\s\S]*?)\\\)$/.exec(raw.trim());
164
+ return inline?.[1] ?? raw;
165
+ }
166
+ export {
167
+ CODE_LANGUAGES,
168
+ MATRX_FENCE_LANGUAGE,
169
+ fenceLanguage,
170
+ inlineIslandLabel,
171
+ isMatrxFence,
172
+ islandMeta,
173
+ kindOf,
174
+ texOf,
175
+ withFenceLanguage
176
+ };
@@ -0,0 +1,10 @@
1
+ import type { FindOptions } from "../core/find-replace.js";
2
+ import type { ViewFindState } from "../visual/VisualEditor.js";
3
+ export interface FindReplacePanelProps {
4
+ showReplace: boolean;
5
+ onFind: (query: string, options: FindOptions, step?: 1 | -1) => ViewFindState;
6
+ onReplace: (query: string, replacement: string, options: FindOptions) => void;
7
+ onReplaceAll: (query: string, replacement: string, options: FindOptions) => number;
8
+ onClose: () => void;
9
+ }
10
+ export declare function FindReplacePanel({ showReplace, onFind, onReplace, onReplaceAll, onClose }: FindReplacePanelProps): import("react").JSX.Element;
@@ -0,0 +1,118 @@
1
+ "use client";
2
+ import { jsx, jsxs } from "react/jsx-runtime";
3
+ import { useEffect, useRef, useState } from "react";
4
+ import { CaseSensitive, ChevronDown, ChevronUp, Regex, ShieldAlert, WholeWord, X } from "lucide-react";
5
+ import { cn } from "@ai-matrx/design-system";
6
+ import { Toggle } from "@ai-matrx/design-system";
7
+ const OPTION_TOGGLE = "h-7 w-7 min-w-7 px-0 text-muted-foreground data-[state=on]:bg-primary/15 data-[state=on]:text-primary-ink";
8
+ function FindReplacePanel({ showReplace, onFind, onReplace, onReplaceAll, onClose }) {
9
+ const [query, setQuery] = useState("");
10
+ const [replacement, setReplacement] = useState("");
11
+ const [options, setOptions] = useState({});
12
+ const [state, setState] = useState({ count: 0, current: -1, skippedProtected: 0, error: null });
13
+ const [replacing, setReplacing] = useState(showReplace);
14
+ const [notice, setNotice] = useState(null);
15
+ const input = useRef(null);
16
+ useEffect(() => setReplacing((value) => value || showReplace), [showReplace]);
17
+ useEffect(() => input.current?.focus(), []);
18
+ useEffect(() => {
19
+ setState(onFind(query, options));
20
+ setNotice(null);
21
+ }, [query, options, onFind]);
22
+ const step = (direction) => setState(onFind(query, options, direction));
23
+ const toggle = (key) => setOptions((current) => ({ ...current, [key]: !current[key] }));
24
+ const summary = state.error ? state.error : !query ? "Type to find" : state.count === 0 ? "No matches" : `${state.current + 1} of ${state.count}`;
25
+ return /* @__PURE__ */ jsxs(
26
+ "div",
27
+ {
28
+ className: "matrx-touch-targets absolute inset-x-2 top-full z-30 mt-1 flex flex-col gap-1.5 rounded-lg border border-border bg-popover/95 px-2 py-1.5 text-sm shadow-lg backdrop-blur sm:left-auto sm:w-[34rem] sm:max-w-[calc(100%-1rem)]",
29
+ role: "search",
30
+ onKeyDown: (event) => {
31
+ if (event.key === "Escape") onClose();
32
+ },
33
+ children: [
34
+ /* @__PURE__ */ jsxs("div", { className: "flex flex-wrap items-center gap-1.5", children: [
35
+ /* @__PURE__ */ jsx(
36
+ "input",
37
+ {
38
+ ref: input,
39
+ value: query,
40
+ onChange: (event) => setQuery(event.target.value),
41
+ onKeyDown: (event) => {
42
+ if (event.key === "Enter") step(event.shiftKey ? -1 : 1);
43
+ },
44
+ placeholder: "Find",
45
+ "aria-label": "Find",
46
+ className: "h-8 w-full min-w-0 rounded-md border border-border bg-background px-2 text-base sm:w-auto sm:flex-1 sm:text-sm"
47
+ }
48
+ ),
49
+ /* @__PURE__ */ jsx("span", { className: cn("shrink-0 text-xs text-muted-foreground sm:w-24", state.error && "text-destructive"), "aria-live": "polite", children: summary }),
50
+ /* @__PURE__ */ jsx(Toggle, { size: "sm", className: OPTION_TOGGLE, pressed: Boolean(options.caseSensitive), onPressedChange: () => toggle("caseSensitive"), title: "Match case", "aria-label": "Match case", children: /* @__PURE__ */ jsx(CaseSensitive, { className: "h-4 w-4" }) }),
51
+ /* @__PURE__ */ jsx(Toggle, { size: "sm", className: OPTION_TOGGLE, pressed: Boolean(options.wholeWord), onPressedChange: () => toggle("wholeWord"), title: "Whole words", "aria-label": "Whole words", children: /* @__PURE__ */ jsx(WholeWord, { className: "h-4 w-4" }) }),
52
+ /* @__PURE__ */ jsx(Toggle, { size: "sm", className: OPTION_TOGGLE, pressed: Boolean(options.regex), onPressedChange: () => toggle("regex"), title: "Regular expression", "aria-label": "Regular expression", children: /* @__PURE__ */ jsx(Regex, { className: "h-4 w-4" }) }),
53
+ /* @__PURE__ */ jsx(Toggle, { size: "sm", className: OPTION_TOGGLE, pressed: Boolean(options.includeProtected), onPressedChange: () => toggle("includeProtected"), title: "Include protected content (kinds, code, variables\u2026)", "aria-label": "Include protected content (kinds, code, variables\u2026)", children: /* @__PURE__ */ jsx(ShieldAlert, { className: "h-4 w-4" }) }),
54
+ /* @__PURE__ */ jsx("button", { type: "button", className: "flex h-7 w-7 items-center justify-center rounded hover:bg-muted", onClick: () => step(-1), "aria-label": "Previous match", children: /* @__PURE__ */ jsx(ChevronUp, { className: "h-4 w-4" }) }),
55
+ /* @__PURE__ */ jsx("button", { type: "button", className: "flex h-7 w-7 items-center justify-center rounded hover:bg-muted", onClick: () => step(1), "aria-label": "Next match", children: /* @__PURE__ */ jsx(ChevronDown, { className: "h-4 w-4" }) }),
56
+ /* @__PURE__ */ jsx(
57
+ "button",
58
+ {
59
+ type: "button",
60
+ className: "h-7 rounded px-2 text-xs text-muted-foreground hover:bg-muted",
61
+ onClick: () => setReplacing((value) => !value),
62
+ "aria-expanded": replacing,
63
+ children: "Replace"
64
+ }
65
+ ),
66
+ /* @__PURE__ */ jsx("button", { type: "button", className: "flex h-7 w-7 items-center justify-center rounded hover:bg-muted", onClick: onClose, "aria-label": "Close find", children: /* @__PURE__ */ jsx(X, { className: "h-4 w-4" }) })
67
+ ] }),
68
+ replacing && /* @__PURE__ */ jsxs("div", { className: "flex flex-wrap items-center gap-1.5", children: [
69
+ /* @__PURE__ */ jsx(
70
+ "input",
71
+ {
72
+ value: replacement,
73
+ onChange: (event) => setReplacement(event.target.value),
74
+ placeholder: options.regex ? "Replace with ($1 for groups)" : "Replace with",
75
+ "aria-label": "Replace with",
76
+ className: "h-8 min-w-0 flex-1 rounded-md border border-border bg-background px-2 text-base sm:text-sm"
77
+ }
78
+ ),
79
+ /* @__PURE__ */ jsx(
80
+ "button",
81
+ {
82
+ type: "button",
83
+ disabled: state.count === 0,
84
+ className: "h-8 rounded-md border border-border px-3 text-xs hover:bg-muted disabled:opacity-50",
85
+ onClick: () => {
86
+ onReplace(query, replacement, options);
87
+ setState(onFind(query, options));
88
+ },
89
+ children: "Replace"
90
+ }
91
+ ),
92
+ /* @__PURE__ */ jsx(
93
+ "button",
94
+ {
95
+ type: "button",
96
+ disabled: state.count === 0,
97
+ className: "h-8 rounded-md border border-border px-3 text-xs hover:bg-muted disabled:opacity-50",
98
+ onClick: () => {
99
+ const replaced = onReplaceAll(query, replacement, options);
100
+ setNotice(`Replaced ${replaced} ${replaced === 1 ? "match" : "matches"}.`);
101
+ setState(onFind(query, options));
102
+ },
103
+ children: "Replace all"
104
+ }
105
+ )
106
+ ] }),
107
+ (state.skippedProtected > 0 || notice) && /* @__PURE__ */ jsxs("p", { className: "text-xs text-muted-foreground", children: [
108
+ notice,
109
+ notice && state.skippedProtected > 0 ? " " : "",
110
+ state.skippedProtected > 0 && `${state.skippedProtected} more ${state.skippedProtected === 1 ? "match is" : "matches are"} inside protected content and left alone \u2014 turn on the shield to include them.`
111
+ ] })
112
+ ]
113
+ }
114
+ );
115
+ }
116
+ export {
117
+ FindReplacePanel
118
+ };
@@ -0,0 +1,4 @@
1
+ export declare function KindPicker({ open, onResolve, }: {
2
+ open: boolean;
3
+ onResolve: (markdown: string | null) => void;
4
+ }): import("react").JSX.Element;