@bendyline/squisq-editor-react 1.0.0 → 1.1.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 (70) hide show
  1. package/dist/DropZoneOverlay.d.ts +24 -0
  2. package/dist/DropZoneOverlay.d.ts.map +1 -0
  3. package/dist/DropZoneOverlay.js +53 -0
  4. package/dist/DropZoneOverlay.js.map +1 -0
  5. package/dist/EditorContext.d.ts +79 -0
  6. package/dist/EditorContext.d.ts.map +1 -0
  7. package/dist/EditorContext.js +204 -0
  8. package/dist/EditorContext.js.map +1 -0
  9. package/dist/EditorShell.d.ts +39 -0
  10. package/dist/EditorShell.d.ts.map +1 -0
  11. package/dist/EditorShell.js +104 -0
  12. package/dist/EditorShell.js.map +1 -0
  13. package/dist/MediaBin.d.ts +18 -0
  14. package/dist/MediaBin.d.ts.map +1 -0
  15. package/dist/MediaBin.js +141 -0
  16. package/dist/MediaBin.js.map +1 -0
  17. package/dist/PreviewPanel.d.ts +33 -0
  18. package/dist/PreviewPanel.d.ts.map +1 -0
  19. package/dist/PreviewPanel.js +385 -0
  20. package/dist/PreviewPanel.js.map +1 -0
  21. package/dist/RawEditor.d.ts +25 -0
  22. package/dist/RawEditor.d.ts.map +1 -0
  23. package/dist/RawEditor.js +100 -0
  24. package/dist/RawEditor.js.map +1 -0
  25. package/dist/StatusBar.d.ts +15 -0
  26. package/dist/StatusBar.d.ts.map +1 -0
  27. package/dist/StatusBar.js +24 -0
  28. package/dist/StatusBar.js.map +1 -0
  29. package/dist/TemplateAnnotation.d.ts +20 -0
  30. package/dist/TemplateAnnotation.d.ts.map +1 -0
  31. package/dist/TemplateAnnotation.js +69 -0
  32. package/dist/TemplateAnnotation.js.map +1 -0
  33. package/dist/Toolbar.d.ts +23 -0
  34. package/dist/Toolbar.d.ts.map +1 -0
  35. package/dist/Toolbar.js +350 -0
  36. package/dist/Toolbar.js.map +1 -0
  37. package/dist/ViewSwitcher.d.ts +14 -0
  38. package/dist/ViewSwitcher.d.ts.map +1 -0
  39. package/dist/ViewSwitcher.js +20 -0
  40. package/dist/ViewSwitcher.js.map +1 -0
  41. package/dist/WysiwygEditor.d.ts +28 -0
  42. package/dist/WysiwygEditor.d.ts.map +1 -0
  43. package/dist/WysiwygEditor.js +111 -0
  44. package/dist/WysiwygEditor.js.map +1 -0
  45. package/dist/hooks/useFileDrop.d.ts +41 -0
  46. package/dist/hooks/useFileDrop.d.ts.map +1 -0
  47. package/dist/hooks/useFileDrop.js +167 -0
  48. package/dist/hooks/useFileDrop.js.map +1 -0
  49. package/dist/index.d.ts +43 -268
  50. package/dist/index.d.ts.map +1 -0
  51. package/dist/index.js +39 -3832
  52. package/dist/index.js.map +1 -1
  53. package/dist/tiptapBridge.d.ts +24 -0
  54. package/dist/tiptapBridge.d.ts.map +1 -0
  55. package/dist/tiptapBridge.js +358 -0
  56. package/dist/tiptapBridge.js.map +1 -0
  57. package/dist/utils/dropUtils.d.ts +36 -0
  58. package/dist/utils/dropUtils.d.ts.map +1 -0
  59. package/dist/utils/dropUtils.js +71 -0
  60. package/dist/utils/dropUtils.js.map +1 -0
  61. package/package.json +5 -5
  62. package/src/DropZoneOverlay.tsx +137 -0
  63. package/src/EditorContext.tsx +56 -0
  64. package/src/EditorShell.tsx +102 -8
  65. package/src/MediaBin.tsx +223 -0
  66. package/src/Toolbar.tsx +21 -1
  67. package/src/hooks/useFileDrop.ts +226 -0
  68. package/src/index.ts +23 -0
  69. package/src/styles/editor.css +318 -0
  70. package/src/utils/dropUtils.ts +88 -0
@@ -0,0 +1,100 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ /**
3
+ * RawEditor
4
+ *
5
+ * Monaco-based raw markdown editor. Provides full VS Code-like editing
6
+ * experience with syntax highlighting, minimap, and bracket matching.
7
+ * Syncs changes back to EditorContext on every keystroke (debounced).
8
+ */
9
+ import { useRef, useCallback, useEffect } from 'react';
10
+ import Editor, { loader } from '@monaco-editor/react';
11
+ import * as monaco from 'monaco-editor';
12
+ import { useEditorContext } from './EditorContext';
13
+ import { getAvailableTemplates } from '@bendyline/squisq/doc';
14
+ // Use locally installed monaco-editor instead of CDN
15
+ loader.config({ monaco });
16
+ /**
17
+ * Raw markdown editor using Monaco Editor.
18
+ * Binds to the shared EditorContext for source synchronization.
19
+ */
20
+ export function RawEditor({ theme = 'vs', minimap = false, fontSize = 14, wordWrap = 'on', className, }) {
21
+ const { markdownSource, setMarkdownSource, setMonacoEditor } = useEditorContext();
22
+ const editorRef = useRef(null);
23
+ const isExternalUpdate = useRef(false);
24
+ const completionDisposable = useRef(null);
25
+ const handleMount = useCallback((editor, monaco) => {
26
+ editorRef.current = editor;
27
+ setMonacoEditor(editor);
28
+ editor.focus();
29
+ // Dispose any previous completion provider (from a prior mount)
30
+ completionDisposable.current?.dispose();
31
+ // Register template annotation completion provider for {[ trigger
32
+ const templates = getAvailableTemplates();
33
+ completionDisposable.current = monaco.languages.registerCompletionItemProvider('markdown', {
34
+ triggerCharacters: ['['],
35
+ provideCompletionItems(model, position) {
36
+ const lineContent = model.getLineContent(position.lineNumber);
37
+ // Only trigger inside a heading line that has {[ before the cursor
38
+ if (!/^#{1,6}\s/.test(lineContent))
39
+ return { suggestions: [] };
40
+ const textBeforeCursor = lineContent.substring(0, position.column - 1);
41
+ const bracketIdx = textBeforeCursor.lastIndexOf('{[');
42
+ if (bracketIdx === -1)
43
+ return { suggestions: [] };
44
+ // The range to replace: from after {[ to the cursor
45
+ const startCol = bracketIdx + 3; // after {[
46
+ const range = new monaco.Range(position.lineNumber, startCol, position.lineNumber, position.column);
47
+ const suggestions = templates.map((name) => ({
48
+ label: name,
49
+ kind: monaco.languages.CompletionItemKind.Value,
50
+ insertText: name + ']}',
51
+ range,
52
+ detail: 'Block template',
53
+ sortText: name,
54
+ }));
55
+ return { suggestions };
56
+ },
57
+ });
58
+ }, [setMonacoEditor]);
59
+ // Unregister on unmount
60
+ useEffect(() => {
61
+ return () => {
62
+ setMonacoEditor(null);
63
+ completionDisposable.current?.dispose();
64
+ completionDisposable.current = null;
65
+ };
66
+ }, [setMonacoEditor]);
67
+ const handleChange = useCallback((value) => {
68
+ if (isExternalUpdate.current)
69
+ return;
70
+ if (value !== undefined) {
71
+ setMarkdownSource(value);
72
+ }
73
+ }, [setMarkdownSource]);
74
+ // When external changes happen (e.g. from WYSIWYG), update Monaco
75
+ useEffect(() => {
76
+ const editor = editorRef.current;
77
+ if (editor) {
78
+ const currentValue = editor.getValue();
79
+ if (currentValue !== markdownSource) {
80
+ isExternalUpdate.current = true;
81
+ editor.setValue(markdownSource);
82
+ isExternalUpdate.current = false;
83
+ }
84
+ }
85
+ }, [markdownSource]);
86
+ return (_jsx("div", { className: className, style: { width: '100%', height: '100%' }, "data-testid": "raw-editor", children: _jsx(Editor, { defaultLanguage: "markdown", value: markdownSource, theme: theme, onMount: handleMount, onChange: handleChange, options: {
87
+ fontSize,
88
+ wordWrap,
89
+ minimap: { enabled: minimap },
90
+ lineNumbers: 'on',
91
+ scrollBeyondLastLine: false,
92
+ automaticLayout: true,
93
+ tabSize: 2,
94
+ renderWhitespace: 'selection',
95
+ bracketPairColorization: { enabled: true },
96
+ guides: { indentation: true },
97
+ padding: { top: 12, bottom: 12 },
98
+ } }) }));
99
+ }
100
+ //# sourceMappingURL=RawEditor.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"RawEditor.js","sourceRoot":"","sources":["../src/RawEditor.tsx"],"names":[],"mappings":"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"}
@@ -0,0 +1,15 @@
1
+ /**
2
+ * StatusBar
3
+ *
4
+ * Bottom status bar showing document statistics and parse status.
5
+ */
6
+ export interface StatusBarProps {
7
+ /** Additional class name */
8
+ className?: string;
9
+ }
10
+ /**
11
+ * Status bar displaying document statistics: character count, word count,
12
+ * block count, and parse/error status.
13
+ */
14
+ export declare function StatusBar({ className }: StatusBarProps): import("react/jsx-runtime").JSX.Element;
15
+ //# sourceMappingURL=StatusBar.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"StatusBar.d.ts","sourceRoot":"","sources":["../src/StatusBar.tsx"],"names":[],"mappings":"AAAA;;;;GAIG;AAKH,MAAM,WAAW,cAAc;IAC7B,4BAA4B;IAC5B,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED;;;GAGG;AACH,wBAAgB,SAAS,CAAC,EAAE,SAAS,EAAE,EAAE,cAAc,2CA6BtD"}
@@ -0,0 +1,24 @@
1
+ import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
2
+ /**
3
+ * StatusBar
4
+ *
5
+ * Bottom status bar showing document statistics and parse status.
6
+ */
7
+ import { useMemo } from 'react';
8
+ import { useEditorContext } from './EditorContext';
9
+ /**
10
+ * Status bar displaying document statistics: character count, word count,
11
+ * block count, and parse/error status.
12
+ */
13
+ export function StatusBar({ className }) {
14
+ const { markdownSource, doc, parseError, isParsing } = useEditorContext();
15
+ const stats = useMemo(() => {
16
+ const chars = markdownSource.length;
17
+ const words = markdownSource.trim() ? markdownSource.trim().split(/\s+/).length : 0;
18
+ const lines = markdownSource.split('\n').length;
19
+ const blocks = doc?.blocks.length ?? 0;
20
+ return { chars, words, lines, blocks };
21
+ }, [markdownSource, doc]);
22
+ return (_jsxs("div", { className: `squisq-status-bar ${className || ''}`, children: [_jsxs("span", { className: "squisq-status-item", children: [stats.words, " words"] }), _jsxs("span", { className: "squisq-status-item", children: [stats.chars, " chars"] }), _jsxs("span", { className: "squisq-status-item", children: [stats.lines, " lines"] }), _jsxs("span", { className: "squisq-status-item", children: [stats.blocks, " blocks"] }), _jsx("span", { className: "squisq-status-spacer" }), isParsing && _jsx("span", { className: "squisq-status-item squisq-status-parsing", children: "Parsing\u2026" }), parseError && (_jsx("span", { className: "squisq-status-item squisq-status-error", title: parseError, children: "\u26A0 Error" })), !isParsing && !parseError && (_jsx("span", { className: "squisq-status-item squisq-status-ok", children: "\u2713 OK" }))] }));
23
+ }
24
+ //# sourceMappingURL=StatusBar.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"StatusBar.js","sourceRoot":"","sources":["../src/StatusBar.tsx"],"names":[],"mappings":";AAAA;;;;GAIG;AAEH,OAAO,EAAE,OAAO,EAAE,MAAM,OAAO,CAAC;AAChC,OAAO,EAAE,gBAAgB,EAAE,MAAM,iBAAiB,CAAC;AAOnD;;;GAGG;AACH,MAAM,UAAU,SAAS,CAAC,EAAE,SAAS,EAAkB;IACrD,MAAM,EAAE,cAAc,EAAE,GAAG,EAAE,UAAU,EAAE,SAAS,EAAE,GAAG,gBAAgB,EAAE,CAAC;IAE1E,MAAM,KAAK,GAAG,OAAO,CAAC,GAAG,EAAE;QACzB,MAAM,KAAK,GAAG,cAAc,CAAC,MAAM,CAAC;QACpC,MAAM,KAAK,GAAG,cAAc,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC,cAAc,CAAC,IAAI,EAAE,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC;QACpF,MAAM,KAAK,GAAG,cAAc,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,MAAM,CAAC;QAChD,MAAM,MAAM,GAAG,GAAG,EAAE,MAAM,CAAC,MAAM,IAAI,CAAC,CAAC;QACvC,OAAO,EAAE,KAAK,EAAE,KAAK,EAAE,KAAK,EAAE,MAAM,EAAE,CAAC;IACzC,CAAC,EAAE,CAAC,cAAc,EAAE,GAAG,CAAC,CAAC,CAAC;IAE1B,OAAO,CACL,eAAK,SAAS,EAAE,qBAAqB,SAAS,IAAI,EAAE,EAAE,aACpD,gBAAM,SAAS,EAAC,oBAAoB,aAAE,KAAK,CAAC,KAAK,cAAc,EAC/D,gBAAM,SAAS,EAAC,oBAAoB,aAAE,KAAK,CAAC,KAAK,cAAc,EAC/D,gBAAM,SAAS,EAAC,oBAAoB,aAAE,KAAK,CAAC,KAAK,cAAc,EAC/D,gBAAM,SAAS,EAAC,oBAAoB,aAAE,KAAK,CAAC,MAAM,eAAe,EACjE,eAAM,SAAS,EAAC,sBAAsB,GAAG,EACxC,SAAS,IAAI,eAAM,SAAS,EAAC,0CAA0C,8BAAgB,EACvF,UAAU,IAAI,CACb,eAAM,SAAS,EAAC,wCAAwC,EAAC,KAAK,EAAE,UAAU,6BAEnE,CACR,EACA,CAAC,SAAS,IAAI,CAAC,UAAU,IAAI,CAC5B,eAAM,SAAS,EAAC,qCAAqC,0BAAY,CAClE,IACG,CACP,CAAC;AACJ,CAAC"}
@@ -0,0 +1,20 @@
1
+ /**
2
+ * TemplateAnnotation — Tiptap Heading Extension
3
+ *
4
+ * Extends Tiptap's built-in Heading node to support `data-template` and
5
+ * `data-template-params` HTML attributes. These attributes store which block
6
+ * template should be used for a heading section.
7
+ *
8
+ * When present, the heading renders a visible badge (styled CSS chip)
9
+ * showing the template name, e.g. `[chart]`.
10
+ *
11
+ * The tiptapBridge converts `### Title {[chart]}` markdown into
12
+ * `<h3 data-template="chart">Title</h3>` and back, so this extension
13
+ * ensures Tiptap's schema preserves those attributes through edits.
14
+ */
15
+ /**
16
+ * HeadingWithTemplate — drop-in replacement for Tiptap's Heading that
17
+ * persists template annotation attributes.
18
+ */
19
+ export declare const HeadingWithTemplate: import("@tiptap/core").Node<import("@tiptap/extension-heading").HeadingOptions, any>;
20
+ //# sourceMappingURL=TemplateAnnotation.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"TemplateAnnotation.d.ts","sourceRoot":"","sources":["../src/TemplateAnnotation.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAIH;;;GAGG;AACH,eAAO,MAAM,mBAAmB,sFAiD9B,CAAC"}
@@ -0,0 +1,69 @@
1
+ /**
2
+ * TemplateAnnotation — Tiptap Heading Extension
3
+ *
4
+ * Extends Tiptap's built-in Heading node to support `data-template` and
5
+ * `data-template-params` HTML attributes. These attributes store which block
6
+ * template should be used for a heading section.
7
+ *
8
+ * When present, the heading renders a visible badge (styled CSS chip)
9
+ * showing the template name, e.g. `[chart]`.
10
+ *
11
+ * The tiptapBridge converts `### Title {[chart]}` markdown into
12
+ * `<h3 data-template="chart">Title</h3>` and back, so this extension
13
+ * ensures Tiptap's schema preserves those attributes through edits.
14
+ */
15
+ import Heading from '@tiptap/extension-heading';
16
+ /**
17
+ * HeadingWithTemplate — drop-in replacement for Tiptap's Heading that
18
+ * persists template annotation attributes.
19
+ */
20
+ export const HeadingWithTemplate = Heading.extend({
21
+ addAttributes() {
22
+ return {
23
+ ...this.parent?.(),
24
+ dataTemplate: {
25
+ default: null,
26
+ parseHTML: (element) => element.getAttribute('data-template') || null,
27
+ renderHTML: (attributes) => {
28
+ if (!attributes.dataTemplate)
29
+ return {};
30
+ return { 'data-template': attributes.dataTemplate };
31
+ },
32
+ },
33
+ dataTemplateParams: {
34
+ default: null,
35
+ parseHTML: (element) => element.getAttribute('data-template-params') || null,
36
+ renderHTML: (attributes) => {
37
+ if (!attributes.dataTemplateParams)
38
+ return {};
39
+ return { 'data-template-params': attributes.dataTemplateParams };
40
+ },
41
+ },
42
+ };
43
+ },
44
+ renderHTML({ node, HTMLAttributes }) {
45
+ const level = node.attrs.level;
46
+ const tag = `h${level}`;
47
+ const templateName = HTMLAttributes['data-template'];
48
+ if (templateName) {
49
+ // Render heading with a trailing badge span
50
+ return [
51
+ tag,
52
+ HTMLAttributes,
53
+ ['span', { class: 'squisq-heading-content' }, 0],
54
+ [
55
+ 'span',
56
+ {
57
+ class: 'squisq-template-badge',
58
+ contenteditable: 'false',
59
+ 'data-template': templateName,
60
+ },
61
+ templateName,
62
+ ],
63
+ ];
64
+ }
65
+ // No template — render as normal heading
66
+ return [tag, HTMLAttributes, 0];
67
+ },
68
+ });
69
+ //# sourceMappingURL=TemplateAnnotation.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"TemplateAnnotation.js","sourceRoot":"","sources":["../src/TemplateAnnotation.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAEH,OAAO,OAAO,MAAM,2BAA2B,CAAC;AAEhD;;;GAGG;AACH,MAAM,CAAC,MAAM,mBAAmB,GAAG,OAAO,CAAC,MAAM,CAAC;IAChD,aAAa;QACX,OAAO;YACL,GAAG,IAAI,CAAC,MAAM,EAAE,EAAE;YAClB,YAAY,EAAE;gBACZ,OAAO,EAAE,IAAI;gBACb,SAAS,EAAE,CAAC,OAAoB,EAAE,EAAE,CAAC,OAAO,CAAC,YAAY,CAAC,eAAe,CAAC,IAAI,IAAI;gBAClF,UAAU,EAAE,CAAC,UAAmC,EAAE,EAAE;oBAClD,IAAI,CAAC,UAAU,CAAC,YAAY;wBAAE,OAAO,EAAE,CAAC;oBACxC,OAAO,EAAE,eAAe,EAAE,UAAU,CAAC,YAAY,EAAE,CAAC;gBACtD,CAAC;aACF;YACD,kBAAkB,EAAE;gBAClB,OAAO,EAAE,IAAI;gBACb,SAAS,EAAE,CAAC,OAAoB,EAAE,EAAE,CAAC,OAAO,CAAC,YAAY,CAAC,sBAAsB,CAAC,IAAI,IAAI;gBACzF,UAAU,EAAE,CAAC,UAAmC,EAAE,EAAE;oBAClD,IAAI,CAAC,UAAU,CAAC,kBAAkB;wBAAE,OAAO,EAAE,CAAC;oBAC9C,OAAO,EAAE,sBAAsB,EAAE,UAAU,CAAC,kBAAkB,EAAE,CAAC;gBACnE,CAAC;aACF;SACF,CAAC;IACJ,CAAC;IAED,UAAU,CAAC,EAAE,IAAI,EAAE,cAAc,EAAE;QACjC,MAAM,KAAK,GAAG,IAAI,CAAC,KAAK,CAAC,KAAK,CAAC;QAC/B,MAAM,GAAG,GAAG,IAAI,KAAK,EAAE,CAAC;QACxB,MAAM,YAAY,GAAG,cAAc,CAAC,eAAe,CAAC,CAAC;QAErD,IAAI,YAAY,EAAE,CAAC;YACjB,4CAA4C;YAC5C,OAAO;gBACL,GAAG;gBACH,cAAc;gBACd,CAAC,MAAM,EAAE,EAAE,KAAK,EAAE,wBAAwB,EAAE,EAAE,CAAC,CAAC;gBAChD;oBACE,MAAM;oBACN;wBACE,KAAK,EAAE,uBAAuB;wBAC9B,eAAe,EAAE,OAAO;wBACxB,eAAe,EAAE,YAAY;qBAC9B;oBACD,YAAY;iBACb;aACF,CAAC;QACJ,CAAC;QAED,yCAAyC;QACzC,OAAO,CAAC,GAAG,EAAE,cAAc,EAAE,CAAC,CAAC,CAAC;IAClC,CAAC;CACF,CAAC,CAAC"}
@@ -0,0 +1,23 @@
1
+ /**
2
+ * Toolbar
3
+ *
4
+ * Formatting toolbar that provides common markdown editing actions.
5
+ * In WYSIWYG mode, uses Tiptap's chain commands to toggle marks / set nodes.
6
+ * In Raw mode, appends markdown syntax at the cursor (or end of source).
7
+ * Hidden in Preview mode.
8
+ */
9
+ export interface ToolbarProps {
10
+ /** Additional class name */
11
+ className?: string;
12
+ /** Whether the Files panel is currently shown */
13
+ showFiles?: boolean;
14
+ /** Toggle the Files panel. When provided, a "Files" button appears in the toolbar. */
15
+ onToggleFiles?: () => void;
16
+ }
17
+ /**
18
+ * Formatting toolbar.
19
+ * - WYSIWYG: calls Tiptap chain commands (toggleBold, etc.)
20
+ * - Raw: appends markdown syntax to the source
21
+ */
22
+ export declare function Toolbar({ className, showFiles, onToggleFiles }: ToolbarProps): import("react/jsx-runtime").JSX.Element;
23
+ //# sourceMappingURL=Toolbar.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"Toolbar.d.ts","sourceRoot":"","sources":["../src/Toolbar.tsx"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAaH,MAAM,WAAW,YAAY;IAC3B,4BAA4B;IAC5B,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,iDAAiD;IACjD,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,sFAAsF;IACtF,aAAa,CAAC,EAAE,MAAM,IAAI,CAAC;CAC5B;AAuFD;;;;GAIG;AACH,wBAAgB,OAAO,CAAC,EAAE,SAAS,EAAE,SAAS,EAAE,aAAa,EAAE,EAAE,YAAY,2CAkX5E"}
@@ -0,0 +1,350 @@
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ /**
3
+ * Toolbar
4
+ *
5
+ * Formatting toolbar that provides common markdown editing actions.
6
+ * In WYSIWYG mode, uses Tiptap's chain commands to toggle marks / set nodes.
7
+ * In Raw mode, appends markdown syntax at the cursor (or end of source).
8
+ * Hidden in Preview mode.
9
+ */
10
+ import { useCallback, useEffect, useReducer } from 'react';
11
+ import { useEditorContext } from './EditorContext';
12
+ import { getAvailableTemplates } from '@bendyline/squisq/doc';
13
+ const VIEWS = [
14
+ { id: 'wysiwyg', label: 'Editor', shortcut: '⌘1' },
15
+ { id: 'raw', label: 'Raw', shortcut: '⌘2' },
16
+ { id: 'preview', label: 'Preview', shortcut: '⌘3' },
17
+ ];
18
+ const BUTTONS = [
19
+ // Format group
20
+ {
21
+ id: 'bold',
22
+ label: 'B',
23
+ icon: 'B',
24
+ title: 'Bold (Ctrl+B)',
25
+ group: 'format',
26
+ iconStyle: { fontWeight: 700 },
27
+ },
28
+ {
29
+ id: 'italic',
30
+ label: 'I',
31
+ icon: 'I',
32
+ title: 'Italic (Ctrl+I)',
33
+ group: 'format',
34
+ iconStyle: { fontStyle: 'italic' },
35
+ },
36
+ {
37
+ id: 'strikethrough',
38
+ label: 'S',
39
+ icon: 'S',
40
+ title: 'Strikethrough',
41
+ group: 'format',
42
+ iconStyle: { textDecoration: 'line-through' },
43
+ },
44
+ { id: 'code', label: '<>', icon: '`', title: 'Inline code', group: 'format' },
45
+ // Structure group
46
+ { id: 'h1', label: 'H1', icon: 'H1', title: 'Heading 1', group: 'structure' },
47
+ { id: 'h2', label: 'H2', icon: 'H2', title: 'Heading 2', group: 'structure' },
48
+ { id: 'h3', label: 'H3', icon: 'H3', title: 'Heading 3', group: 'structure' },
49
+ { id: 'quote', label: '❝', icon: '❝', title: 'Blockquote', group: 'structure' },
50
+ // Insert group
51
+ { id: 'ul', label: '•', icon: '•', title: 'Bullet list', group: 'insert' },
52
+ { id: 'ol', label: '1.', icon: '1.', title: 'Numbered list', group: 'insert' },
53
+ { id: 'codeblock', label: '{ }', icon: '{ }', title: 'Code block', group: 'insert' },
54
+ { id: 'hr', label: '—', icon: '—', title: 'Horizontal rule', group: 'insert' },
55
+ { id: 'link', label: '🔗', icon: '🔗', title: 'Insert link', group: 'insert' },
56
+ ];
57
+ // ─── Tiptap active-state map ────────────────────────────
58
+ /** Returns true if the given button id is currently active in Tiptap */
59
+ function isTiptapActive(editor, id) {
60
+ if (!editor)
61
+ return false;
62
+ switch (id) {
63
+ case 'bold':
64
+ return editor.isActive('bold');
65
+ case 'italic':
66
+ return editor.isActive('italic');
67
+ case 'strikethrough':
68
+ return editor.isActive('strike');
69
+ case 'code':
70
+ return editor.isActive('code');
71
+ case 'h1':
72
+ return editor.isActive('heading', { level: 1 });
73
+ case 'h2':
74
+ return editor.isActive('heading', { level: 2 });
75
+ case 'h3':
76
+ return editor.isActive('heading', { level: 3 });
77
+ case 'quote':
78
+ return editor.isActive('blockquote');
79
+ case 'ul':
80
+ return editor.isActive('bulletList');
81
+ case 'ol':
82
+ return editor.isActive('orderedList');
83
+ case 'codeblock':
84
+ return editor.isActive('codeBlock');
85
+ default:
86
+ return false;
87
+ }
88
+ }
89
+ /**
90
+ * Formatting toolbar.
91
+ * - WYSIWYG: calls Tiptap chain commands (toggleBold, etc.)
92
+ * - Raw: appends markdown syntax to the source
93
+ */
94
+ export function Toolbar({ className, showFiles, onToggleFiles }) {
95
+ const { activeView, setActiveView, markdownSource, setMarkdownSource, tiptapEditor, monacoEditor, } = useEditorContext();
96
+ // Force re-render when Tiptap selection or formatting state changes
97
+ const [, forceUpdate] = useReducer((c) => c + 1, 0);
98
+ useEffect(() => {
99
+ if (!tiptapEditor)
100
+ return;
101
+ tiptapEditor.on('transaction', forceUpdate);
102
+ return () => {
103
+ tiptapEditor.off('transaction', forceUpdate);
104
+ };
105
+ }, [tiptapEditor]);
106
+ // ── Tiptap handler ─────────────────────────────────────
107
+ const handleTiptap = useCallback((id) => {
108
+ if (!tiptapEditor)
109
+ return;
110
+ const chain = tiptapEditor.chain().focus();
111
+ switch (id) {
112
+ case 'bold':
113
+ chain.toggleBold().run();
114
+ break;
115
+ case 'italic':
116
+ chain.toggleItalic().run();
117
+ break;
118
+ case 'strikethrough':
119
+ chain.toggleStrike().run();
120
+ break;
121
+ case 'code':
122
+ chain.toggleCode().run();
123
+ break;
124
+ case 'h1':
125
+ chain.toggleHeading({ level: 1 }).run();
126
+ break;
127
+ case 'h2':
128
+ chain.toggleHeading({ level: 2 }).run();
129
+ break;
130
+ case 'h3':
131
+ chain.toggleHeading({ level: 3 }).run();
132
+ break;
133
+ case 'quote':
134
+ chain.toggleBlockquote().run();
135
+ break;
136
+ case 'ul':
137
+ chain.toggleBulletList().run();
138
+ break;
139
+ case 'ol':
140
+ chain.toggleOrderedList().run();
141
+ break;
142
+ case 'codeblock':
143
+ chain.toggleCodeBlock().run();
144
+ break;
145
+ case 'hr':
146
+ chain.setHorizontalRule().run();
147
+ break;
148
+ case 'link': {
149
+ const url = window.prompt('URL:');
150
+ if (url) {
151
+ chain
152
+ .setLink?.({ href: url })
153
+ .run();
154
+ }
155
+ break;
156
+ }
157
+ }
158
+ }, [tiptapEditor]);
159
+ // ── Raw markdown handler ───────────────────────────────
160
+ const handleRaw = useCallback((id) => {
161
+ if (monacoEditor) {
162
+ // Use Monaco's selection API for proper wrap/insert behavior
163
+ const selection = monacoEditor.getSelection();
164
+ const model = monacoEditor.getModel();
165
+ if (!selection || !model)
166
+ return;
167
+ const selectedText = model.getValueInRange(selection);
168
+ const hasSelection = selectedText.length > 0;
169
+ let replacement = '';
170
+ let newCursorOffset = 0; // offset from start of replacement to place cursor
171
+ // Inline wrapping: wrap selection or insert placeholder
172
+ const wrapInline = (before, after, placeholder) => {
173
+ if (hasSelection) {
174
+ replacement = before + selectedText + after;
175
+ }
176
+ else {
177
+ replacement = before + placeholder + after;
178
+ // Select the placeholder text after insertion
179
+ newCursorOffset = before.length;
180
+ }
181
+ };
182
+ // Block-level: prefix each selected line, or insert a new block
183
+ const prefixLines = (prefix, placeholder) => {
184
+ if (hasSelection) {
185
+ replacement = selectedText
186
+ .split('\n')
187
+ .map((line) => prefix + line)
188
+ .join('\n');
189
+ }
190
+ else {
191
+ replacement = prefix + placeholder;
192
+ newCursorOffset = prefix.length;
193
+ }
194
+ };
195
+ switch (id) {
196
+ case 'bold':
197
+ wrapInline('**', '**', 'bold text');
198
+ break;
199
+ case 'italic':
200
+ wrapInline('*', '*', 'italic text');
201
+ break;
202
+ case 'strikethrough':
203
+ wrapInline('~~', '~~', 'strikethrough');
204
+ break;
205
+ case 'code':
206
+ wrapInline('`', '`', 'code');
207
+ break;
208
+ case 'h1':
209
+ prefixLines('# ', 'Heading 1');
210
+ break;
211
+ case 'h2':
212
+ prefixLines('## ', 'Heading 2');
213
+ break;
214
+ case 'h3':
215
+ prefixLines('### ', 'Heading 3');
216
+ break;
217
+ case 'quote':
218
+ prefixLines('> ', 'Quote');
219
+ break;
220
+ case 'ul':
221
+ prefixLines('- ', 'Item');
222
+ break;
223
+ case 'ol':
224
+ prefixLines('1. ', 'Item');
225
+ break;
226
+ case 'codeblock': {
227
+ const inner = hasSelection ? selectedText : 'code';
228
+ replacement = '```\n' + inner + '\n```';
229
+ if (!hasSelection)
230
+ newCursorOffset = 4; // after ```\n
231
+ break;
232
+ }
233
+ case 'hr': {
234
+ replacement = '\n---\n';
235
+ break;
236
+ }
237
+ case 'link': {
238
+ if (hasSelection) {
239
+ replacement = '[' + selectedText + '](url)';
240
+ }
241
+ else {
242
+ replacement = '[link text](url)';
243
+ newCursorOffset = 1; // inside the []
244
+ }
245
+ break;
246
+ }
247
+ }
248
+ // Apply the edit via Monaco's executeEdits for proper undo support
249
+ const range = selection;
250
+ monacoEditor.executeEdits('toolbar', [{ range, text: replacement }]);
251
+ // If no selection, select the placeholder text so user can type over it
252
+ if (!hasSelection && newCursorOffset > 0) {
253
+ const startPos = model.getPositionAt(model.getOffsetAt(range.getStartPosition()) + newCursorOffset);
254
+ const _placeholderLen = replacement.length -
255
+ newCursorOffset -
256
+ (replacement.length -
257
+ replacement.lastIndexOf(replacement.charAt(replacement.length - 1)));
258
+ // Just place cursor after the prefix
259
+ monacoEditor.setPosition(startPos);
260
+ }
261
+ monacoEditor.focus();
262
+ }
263
+ else {
264
+ // Fallback: no Monaco instance, just append
265
+ let insertion = '';
266
+ switch (id) {
267
+ case 'bold':
268
+ insertion = '**bold text**';
269
+ break;
270
+ case 'italic':
271
+ insertion = '*italic text*';
272
+ break;
273
+ case 'strikethrough':
274
+ insertion = '~~strikethrough~~';
275
+ break;
276
+ case 'code':
277
+ insertion = '`code`';
278
+ break;
279
+ case 'h1':
280
+ insertion = '\n# Heading 1\n';
281
+ break;
282
+ case 'h2':
283
+ insertion = '\n## Heading 2\n';
284
+ break;
285
+ case 'h3':
286
+ insertion = '\n### Heading 3\n';
287
+ break;
288
+ case 'quote':
289
+ insertion = '\n> Quote\n';
290
+ break;
291
+ case 'ul':
292
+ insertion = '\n- Item\n';
293
+ break;
294
+ case 'ol':
295
+ insertion = '\n1. Item\n';
296
+ break;
297
+ case 'codeblock':
298
+ insertion = '\n```\ncode\n```\n';
299
+ break;
300
+ case 'hr':
301
+ insertion = '\n---\n';
302
+ break;
303
+ case 'link':
304
+ insertion = '[link text](url)';
305
+ break;
306
+ }
307
+ if (insertion) {
308
+ setMarkdownSource(markdownSource + insertion);
309
+ }
310
+ }
311
+ }, [monacoEditor, markdownSource, setMarkdownSource]);
312
+ const handleAction = useCallback((id) => {
313
+ if (activeView === 'wysiwyg' && tiptapEditor) {
314
+ handleTiptap(id);
315
+ }
316
+ else {
317
+ handleRaw(id);
318
+ }
319
+ }, [activeView, tiptapEditor, handleTiptap, handleRaw]);
320
+ const groups = ['format', 'structure', 'insert'];
321
+ const isWysiwyg = activeView === 'wysiwyg' && tiptapEditor;
322
+ const isPreview = activeView === 'preview';
323
+ // Detect current heading template (WYSIWYG mode only)
324
+ const currentTemplate = isWysiwyg
325
+ ? tiptapEditor.isActive('heading')
326
+ ? (tiptapEditor.getAttributes('heading')?.dataTemplate ?? '')
327
+ : null
328
+ : null;
329
+ const handleTemplatePick = (value) => {
330
+ if (!tiptapEditor)
331
+ return;
332
+ if (value === '') {
333
+ // Clear template
334
+ tiptapEditor
335
+ .chain()
336
+ .focus()
337
+ .updateAttributes('heading', { dataTemplate: null, dataTemplateParams: null })
338
+ .run();
339
+ }
340
+ else {
341
+ tiptapEditor.chain().focus().updateAttributes('heading', { dataTemplate: value }).run();
342
+ }
343
+ };
344
+ const templateNames = getAvailableTemplates();
345
+ return (_jsxs("div", { className: `squisq-toolbar ${className || ''}`, role: "toolbar", "aria-label": "Formatting toolbar", children: [_jsx("div", { className: "squisq-toolbar-view-tabs", role: "tablist", "aria-label": "Editor view", children: VIEWS.map((view) => (_jsx("button", { role: "tab", "data-view": view.id, "aria-selected": activeView === view.id, className: `squisq-toolbar-view-tab${activeView === view.id ? ' squisq-toolbar-view-tab--active' : ''}`, onClick: () => setActiveView(view.id), title: `${view.label} (${view.shortcut})`, children: view.label }, view.id))) }), !isPreview && (_jsxs("div", { className: "squisq-toolbar-actions", children: [groups.map((group, gi) => (_jsxs("div", { className: "squisq-toolbar-group", children: [gi > 0 && _jsx("div", { className: "squisq-toolbar-separator" }), BUTTONS.filter((b) => b.group === group).map((btn) => {
346
+ const active = isWysiwyg ? isTiptapActive(tiptapEditor, btn.id) : false;
347
+ return (_jsx("button", { className: `squisq-toolbar-button${active ? ' squisq-toolbar-button--active' : ''}`, title: btn.title, onClick: () => handleAction(btn.id), "aria-label": btn.title, "aria-pressed": active, style: btn.iconStyle, children: btn.icon }, btn.id));
348
+ })] }, group))), currentTemplate !== null && (_jsxs(_Fragment, { children: [_jsx("div", { className: "squisq-toolbar-separator" }), _jsx("div", { className: "squisq-toolbar-group squisq-template-picker", children: _jsxs("label", { className: "squisq-template-picker-label", title: "Block template for this heading", children: ["Template:", _jsxs("select", { className: "squisq-template-picker-select", value: currentTemplate, onChange: (e) => handleTemplatePick(e.target.value), children: [_jsx("option", { value: "", children: "\u2014 none \u2014" }), templateNames.map((name) => (_jsx("option", { value: name, children: name }, name)))] })] }) })] }))] })), onToggleFiles && _jsx("div", { style: { flex: 1 } }), onToggleFiles && (_jsx("button", { className: `squisq-toolbar-button squisq-toolbar-files-toggle${showFiles ? ' squisq-toolbar-button--active' : ''}`, onClick: onToggleFiles, title: showFiles ? 'Hide Files panel' : 'Show Files panel', "aria-pressed": showFiles, "aria-label": "Toggle Files panel", children: '\u{1F4CE}' }))] }));
349
+ }
350
+ //# sourceMappingURL=Toolbar.js.map