@eventuras/scribo 0.8.3 → 0.10.7

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 (164) hide show
  1. package/dist/App.d.ts +11 -0
  2. package/dist/App.d.ts.map +1 -0
  3. package/dist/MarkdownEditor.d.ts +18 -0
  4. package/dist/MarkdownEditor.d.ts.map +1 -0
  5. package/dist/MarkdownEditor2.js +111 -0
  6. package/dist/MarkdownEditor2.js.map +1 -0
  7. package/dist/MarkdownInput.d.ts +27 -0
  8. package/dist/MarkdownInput.d.ts.map +1 -0
  9. package/dist/MarkdownInput.js +57 -0
  10. package/dist/MarkdownInput.js.map +1 -0
  11. package/dist/callout.js +5 -0
  12. package/dist/context/SharedHistoryContext.d.ts +11 -0
  13. package/dist/context/SharedHistoryContext.d.ts.map +1 -0
  14. package/dist/context/SharedHistoryContext.js +12 -0
  15. package/dist/context/SharedHistoryContext.js.map +1 -0
  16. package/dist/index.d.ts +1 -0
  17. package/dist/index.d.ts.map +1 -0
  18. package/dist/main.d.ts +5 -0
  19. package/dist/main.d.ts.map +1 -0
  20. package/dist/main.js +4 -0
  21. package/dist/nodes/EditorNodes.d.ts +4 -0
  22. package/dist/nodes/EditorNodes.d.ts.map +1 -0
  23. package/dist/nodes/EditorNodes.js +39 -0
  24. package/dist/nodes/EditorNodes.js.map +1 -0
  25. package/dist/nodes/callout/CalloutComponent.d.ts +6 -0
  26. package/dist/nodes/callout/CalloutComponent.d.ts.map +1 -0
  27. package/dist/nodes/callout/CalloutComponent.js +53 -0
  28. package/dist/nodes/callout/CalloutComponent.js.map +1 -0
  29. package/dist/nodes/callout/CalloutNode.d.ts +27 -0
  30. package/dist/nodes/callout/CalloutNode.d.ts.map +1 -0
  31. package/dist/nodes/callout/CalloutNode.js +153 -0
  32. package/dist/nodes/callout/CalloutNode.js.map +1 -0
  33. package/dist/nodes/callout/CalloutTransformer.d.ts +3 -0
  34. package/dist/nodes/callout/CalloutTransformer.d.ts.map +1 -0
  35. package/dist/nodes/callout/CalloutTransformer.js +48 -0
  36. package/dist/nodes/callout/CalloutTransformer.js.map +1 -0
  37. package/dist/nodes/callout/calloutPlugin.d.ts +3 -0
  38. package/dist/nodes/callout/calloutPlugin.d.ts.map +1 -0
  39. package/dist/nodes/callout/calloutPlugin.js +34 -0
  40. package/dist/nodes/callout/calloutPlugin.js.map +1 -0
  41. package/dist/nodes/callout/index.d.ts +6 -0
  42. package/dist/nodes/callout/index.d.ts.map +1 -0
  43. package/dist/nodes/schedule/ScheduleItemComponent.d.ts +9 -0
  44. package/dist/nodes/schedule/ScheduleItemComponent.d.ts.map +1 -0
  45. package/dist/nodes/schedule/ScheduleItemComponent.js +167 -0
  46. package/dist/nodes/schedule/ScheduleItemComponent.js.map +1 -0
  47. package/dist/nodes/schedule/ScheduleItemNode.d.ts +31 -0
  48. package/dist/nodes/schedule/ScheduleItemNode.d.ts.map +1 -0
  49. package/dist/nodes/schedule/ScheduleItemNode.js +94 -0
  50. package/dist/nodes/schedule/ScheduleItemNode.js.map +1 -0
  51. package/dist/nodes/schedule/ScheduleTransformer.d.ts +22 -0
  52. package/dist/nodes/schedule/ScheduleTransformer.d.ts.map +1 -0
  53. package/dist/nodes/schedule/ScheduleTransformer.js +60 -0
  54. package/dist/nodes/schedule/ScheduleTransformer.js.map +1 -0
  55. package/dist/nodes/schedule/index.d.ts +5 -0
  56. package/dist/nodes/schedule/index.d.ts.map +1 -0
  57. package/dist/nodes/schedule/schedulePlugin.d.ts +3 -0
  58. package/dist/nodes/schedule/schedulePlugin.d.ts.map +1 -0
  59. package/dist/nodes/schedule/schedulePlugin.js +33 -0
  60. package/dist/nodes/schedule/schedulePlugin.js.map +1 -0
  61. package/dist/plugins/AutoLinkPlugin.d.ts +3 -0
  62. package/dist/plugins/AutoLinkPlugin.d.ts.map +1 -0
  63. package/dist/plugins/AutoLinkPlugin.js +22 -0
  64. package/dist/plugins/AutoLinkPlugin.js.map +1 -0
  65. package/dist/plugins/FloatingLinkEditorPlugin.d.ts +7 -0
  66. package/dist/plugins/FloatingLinkEditorPlugin.d.ts.map +1 -0
  67. package/dist/plugins/FloatingLinkEditorPlugin2.js +302 -0
  68. package/dist/plugins/FloatingLinkEditorPlugin2.js.map +1 -0
  69. package/dist/plugins/HistoryPlugin.d.ts +26 -0
  70. package/dist/plugins/HistoryPlugin.d.ts.map +1 -0
  71. package/dist/plugins/LinkPlugin.d.ts +3 -0
  72. package/dist/plugins/LinkPlugin.d.ts.map +1 -0
  73. package/dist/plugins/LinkPlugin.js +18 -0
  74. package/dist/plugins/LinkPlugin.js.map +1 -0
  75. package/dist/plugins/ToolbarPlugin.d.ts +7 -0
  76. package/dist/plugins/ToolbarPlugin.d.ts.map +1 -0
  77. package/dist/plugins/ToolbarPlugin.js +372 -0
  78. package/dist/plugins/ToolbarPlugin.js.map +1 -0
  79. package/dist/schedule.js +4 -0
  80. package/dist/scribo.css +1977 -0
  81. package/dist/themes/EditorTheme.d.ts +4 -0
  82. package/dist/themes/EditorTheme.d.ts.map +1 -0
  83. package/dist/themes/EditorTheme2.js +107 -0
  84. package/dist/themes/EditorTheme2.js.map +1 -0
  85. package/dist/types.d.ts +26 -0
  86. package/dist/types.d.ts.map +1 -0
  87. package/dist/ui/ContentEditable.d.ts +6 -0
  88. package/dist/ui/ContentEditable.d.ts.map +1 -0
  89. package/dist/ui/ContentEditable2.js +14 -0
  90. package/dist/ui/ContentEditable2.js.map +1 -0
  91. package/dist/ui/DropDown.d.ts +18 -0
  92. package/dist/ui/DropDown.d.ts.map +1 -0
  93. package/dist/ui/DropDown.js +162 -0
  94. package/dist/ui/DropDown.js.map +1 -0
  95. package/dist/ui/Placeholder.d.ts +6 -0
  96. package/dist/ui/Placeholder.d.ts.map +1 -0
  97. package/dist/ui/Placeholder2.js +13 -0
  98. package/dist/ui/Placeholder2.js.map +1 -0
  99. package/dist/utils/environment.d.ts +24 -0
  100. package/dist/utils/environment.d.ts.map +1 -0
  101. package/dist/utils/environment.js +20 -0
  102. package/dist/utils/environment.js.map +1 -0
  103. package/dist/utils/getSelectedNode.d.ts +3 -0
  104. package/dist/utils/getSelectedNode.d.ts.map +1 -0
  105. package/dist/utils/getSelectedNode.js +23 -0
  106. package/dist/utils/getSelectedNode.js.map +1 -0
  107. package/dist/utils/setFloatingElemPosition.d.ts +2 -0
  108. package/dist/utils/setFloatingElemPosition.d.ts.map +1 -0
  109. package/dist/utils/setFloatingElemPositionForLinkEditor.d.ts +2 -0
  110. package/dist/utils/setFloatingElemPositionForLinkEditor.d.ts.map +1 -0
  111. package/dist/utils/setFloatingElemPositionForLinkEditor.js +33 -0
  112. package/dist/utils/setFloatingElemPositionForLinkEditor.js.map +1 -0
  113. package/dist/utils/url.d.ts +11 -0
  114. package/dist/utils/url.d.ts.map +1 -0
  115. package/dist/utils/url.js +33 -0
  116. package/dist/utils/url.js.map +1 -0
  117. package/dist/vite.svg +1 -0
  118. package/package.json +55 -38
  119. package/src/MarkdownEditor.css +61 -47
  120. package/src/MarkdownEditor.tsx +76 -29
  121. package/src/MarkdownInput.tsx +9 -10
  122. package/src/context/SharedHistoryContext.tsx +8 -16
  123. package/src/main.tsx +3 -3
  124. package/src/nodes/EditorNodes.ts +10 -10
  125. package/src/nodes/callout/Callout.css +94 -0
  126. package/src/nodes/callout/CalloutComponent.tsx +61 -0
  127. package/src/nodes/callout/CalloutNode.ts +197 -0
  128. package/src/nodes/callout/CalloutTransformer.ts +87 -0
  129. package/src/nodes/callout/calloutPlugin.ts +37 -0
  130. package/src/nodes/callout/index.ts +5 -0
  131. package/src/nodes/schedule/ScheduleItem.css +140 -0
  132. package/src/nodes/schedule/ScheduleItemComponent.tsx +195 -0
  133. package/src/nodes/schedule/ScheduleItemNode.ts +135 -0
  134. package/src/nodes/schedule/ScheduleTransformer.ts +80 -0
  135. package/src/nodes/schedule/index.ts +4 -0
  136. package/src/nodes/schedule/schedulePlugin.ts +33 -0
  137. package/src/plugins/AutoLinkPlugin.tsx +5 -10
  138. package/src/plugins/FloatingLinkEditorPlugin.tsx +42 -66
  139. package/src/plugins/HistoryPlugin.tsx +20 -30
  140. package/src/plugins/LinkPlugin.tsx +2 -3
  141. package/src/plugins/ToolbarPlugin.tsx +129 -141
  142. package/src/themes/EditorTheme.css +9 -5
  143. package/src/themes/EditorTheme.ts +89 -90
  144. package/src/types.ts +27 -0
  145. package/src/ui/ContentEditable.tsx +5 -5
  146. package/src/ui/DropDown.tsx +23 -41
  147. package/src/ui/Placeholder.tsx +21 -21
  148. package/src/utils/environment.ts +6 -12
  149. package/src/utils/getSelectedNode.ts +3 -5
  150. package/src/utils/setFloatingElemPosition.ts +2 -5
  151. package/src/utils/setFloatingElemPositionForLinkEditor.ts +1 -1
  152. package/src/utils/url.ts +4 -10
  153. package/tsconfig.json +8 -0
  154. package/CHANGELOG.md +0 -65
  155. package/README.md +0 -126
  156. package/RELEASE.md +0 -102
  157. package/eslint.config.js +0 -4
  158. package/src/App.css +0 -35
  159. package/src/App.tsx +0 -42
  160. package/src/index.tsx +0 -13
  161. package/tsconfig.node.json +0 -11
  162. package/vite.config.site.ts +0 -19
  163. package/vite.config.ts +0 -33
  164. /package/{src → dist}/vite-env.d.ts +0 -0
@@ -0,0 +1,140 @@
1
+ /* Schedule item wrapper */
2
+ .schedule-item-wrapper + .schedule-item-wrapper {
3
+ margin-top: 0;
4
+ }
5
+
6
+ /* Main editor row */
7
+ .schedule-item-editor {
8
+ display: flex;
9
+ gap: 0.5rem;
10
+ align-items: center;
11
+ padding: 0.375rem 0.5rem;
12
+ border-radius: 0.375rem;
13
+ background: rgba(74, 142, 161, 0.06);
14
+ border: 1px solid rgba(74, 142, 161, 0.15);
15
+ transition:
16
+ border-color 0.15s,
17
+ opacity 0.15s;
18
+ }
19
+
20
+ .schedule-item-editor--selected {
21
+ border-color: rgba(74, 142, 161, 0.4);
22
+ }
23
+
24
+ .schedule-item-editor--dragging {
25
+ opacity: 0.4;
26
+ }
27
+
28
+ /* Drag handle */
29
+ .schedule-item-drag {
30
+ cursor: grab;
31
+ border: none;
32
+ background: transparent;
33
+ padding: 0.125rem;
34
+ color: rgba(0, 0, 0, 0.25);
35
+ font-size: 0.875rem;
36
+ line-height: 1;
37
+ letter-spacing: -0.05em;
38
+ flex-shrink: 0;
39
+ user-select: none;
40
+ }
41
+
42
+ .schedule-item-drag:hover {
43
+ color: rgba(0, 0, 0, 0.5);
44
+ }
45
+
46
+ .schedule-item-drag:active {
47
+ cursor: grabbing;
48
+ }
49
+
50
+ /* Input fields */
51
+ .schedule-item-editor input {
52
+ border: none;
53
+ background: transparent;
54
+ font: inherit;
55
+ padding: 0.125rem 0.25rem;
56
+ border-radius: 0.25rem;
57
+ outline: none;
58
+ min-width: 0;
59
+ }
60
+
61
+ .schedule-item-editor input:focus {
62
+ background: rgba(255, 255, 255, 0.8);
63
+ }
64
+
65
+ .schedule-item-time {
66
+ font-weight: 700;
67
+ font-variant-numeric: tabular-nums;
68
+ width: 8rem;
69
+ flex-shrink: 0;
70
+ }
71
+
72
+ .schedule-item-title {
73
+ font-weight: 600;
74
+ flex: 2;
75
+ }
76
+
77
+ .schedule-item-speaker {
78
+ flex: 1;
79
+ color: rgba(0, 0, 0, 0.6);
80
+ }
81
+
82
+ .schedule-item-description {
83
+ flex: 2;
84
+ color: rgba(0, 0, 0, 0.5);
85
+ font-size: 0.9em;
86
+ }
87
+
88
+ .schedule-item-editor input::placeholder {
89
+ color: rgba(0, 0, 0, 0.25);
90
+ }
91
+
92
+ /* Remove button */
93
+ .schedule-item-remove {
94
+ border: 1px solid rgba(0, 0, 0, 0.15);
95
+ background: #f5f5f5;
96
+ cursor: pointer;
97
+ padding: 0.25rem 0.5rem;
98
+ color: #666666;
99
+ font-size: 1rem;
100
+ line-height: 1;
101
+ flex-shrink: 0;
102
+ border-radius: 0.25rem;
103
+ transition:
104
+ color 0.15s,
105
+ background 0.15s,
106
+ border-color 0.15s;
107
+ }
108
+
109
+ .schedule-item-remove:hover {
110
+ color: #b91c1c;
111
+ background: #f8e8e8;
112
+ border-color: rgba(185, 28, 28, 0.3);
113
+ }
114
+
115
+ /* Add button — only on last item */
116
+ .schedule-item-add {
117
+ display: flex;
118
+ align-items: center;
119
+ justify-content: center;
120
+ width: 100%;
121
+ margin-top: 0.375rem;
122
+ padding: 0.375rem;
123
+ border: 1px dashed rgba(74, 142, 161, 0.35);
124
+ border-radius: 0.375rem;
125
+ background: #f4f8f9;
126
+ cursor: pointer;
127
+ color: #3a7287;
128
+ font-size: 1rem;
129
+ font-weight: 600;
130
+ transition:
131
+ border-color 0.15s,
132
+ color 0.15s,
133
+ background 0.15s;
134
+ }
135
+
136
+ .schedule-item-add:hover {
137
+ border-color: rgba(74, 142, 161, 0.6);
138
+ color: #2f5f6d;
139
+ background: #e9f1f4;
140
+ }
@@ -0,0 +1,195 @@
1
+ import './ScheduleItem.css';
2
+ import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext';
3
+ import { useLexicalNodeSelection } from '@lexical/react/useLexicalNodeSelection';
4
+ import { $createParagraphNode, $getNodeByKey, type NodeKey } from 'lexical';
5
+ import { type DragEvent, useCallback, useEffect, useRef, useState } from 'react';
6
+
7
+ import {
8
+ $createScheduleItemNode,
9
+ $isScheduleItemNode,
10
+ type ScheduleItemData,
11
+ } from './ScheduleItemNode';
12
+
13
+ interface ScheduleItemComponentProps {
14
+ nodeKey: NodeKey;
15
+ data: ScheduleItemData;
16
+ }
17
+
18
+ export const ScheduleItemComponent = ({ nodeKey, data }: ScheduleItemComponentProps) => {
19
+ const [editor] = useLexicalComposerContext();
20
+ const [isSelected, _setSelected, _clearSelection] = useLexicalNodeSelection(nodeKey);
21
+ const [localData, setLocalData] = useState(data);
22
+ const [isDragging, setIsDragging] = useState(false);
23
+ const [isLast, setIsLast] = useState(false);
24
+ const timeRef = useRef<HTMLInputElement>(null);
25
+
26
+ useEffect(() => {
27
+ setLocalData(data);
28
+ }, [data]);
29
+
30
+ // Check if this is the last schedule item (show + button only on last)
31
+ useEffect(() => {
32
+ return editor.registerUpdateListener(() => {
33
+ editor.getEditorState().read(() => {
34
+ const node = $getNodeByKey(nodeKey);
35
+ if (!node) return;
36
+ const next = node.getNextSibling();
37
+ setIsLast(!next || !$isScheduleItemNode(next));
38
+ });
39
+ });
40
+ }, [editor, nodeKey]);
41
+
42
+ const updateNode = useCallback(
43
+ (updated: ScheduleItemData) => {
44
+ editor.update(() => {
45
+ const node = $getNodeByKey(nodeKey);
46
+ if ($isScheduleItemNode(node)) {
47
+ node.setData(updated);
48
+ }
49
+ });
50
+ },
51
+ [editor, nodeKey]
52
+ );
53
+
54
+ const handleChange = (field: keyof ScheduleItemData, value: string) => {
55
+ const nextRaw: ScheduleItemData = {
56
+ ...localData,
57
+ time: field === 'time' ? value : localData.time,
58
+ title: field === 'title' ? value : localData.title,
59
+ speaker: field === 'speaker' ? value || undefined : localData.speaker,
60
+ description: field === 'description' ? value || undefined : localData.description,
61
+ };
62
+ const persisted: ScheduleItemData = {
63
+ ...nextRaw,
64
+ time: nextRaw.time.trim(),
65
+ };
66
+
67
+ setLocalData(nextRaw);
68
+ updateNode(persisted);
69
+ };
70
+
71
+ const insertAfter = () => {
72
+ editor.update(() => {
73
+ const node = $getNodeByKey(nodeKey);
74
+ if (!node) return;
75
+ const newNode = $createScheduleItemNode({ time: '', title: '' });
76
+ node.insertAfter(newNode);
77
+ newNode.selectEnd();
78
+ });
79
+ };
80
+
81
+ const removeNode = () => {
82
+ editor.update(() => {
83
+ const node = $getNodeByKey(nodeKey);
84
+ if (!node) return;
85
+ const next = node.getNextSibling();
86
+ const prev = node.getPreviousSibling();
87
+ if (!next && !prev) {
88
+ const p = $createParagraphNode();
89
+ node.replace(p);
90
+ p.selectEnd();
91
+ } else {
92
+ node.remove();
93
+ }
94
+ });
95
+ };
96
+
97
+ const handleDragStart = (e: DragEvent) => {
98
+ e.dataTransfer.setData('text/plain', nodeKey);
99
+ e.dataTransfer.effectAllowed = 'move';
100
+ setIsDragging(true);
101
+ };
102
+
103
+ const handleDragEnd = () => setIsDragging(false);
104
+
105
+ const handleDragOver = (e: DragEvent) => {
106
+ e.preventDefault();
107
+ e.dataTransfer.dropEffect = 'move';
108
+ };
109
+
110
+ const handleDrop = (e: DragEvent) => {
111
+ e.preventDefault();
112
+ const sourceKey = e.dataTransfer.getData('text/plain');
113
+ if (!sourceKey || sourceKey === nodeKey) return;
114
+ editor.update(() => {
115
+ const sourceNode = $getNodeByKey(sourceKey);
116
+ const targetNode = $getNodeByKey(nodeKey);
117
+ if (!sourceNode || !targetNode || !$isScheduleItemNode(sourceNode)) return;
118
+ targetNode.insertBefore(sourceNode);
119
+ });
120
+ };
121
+
122
+ return (
123
+ <div className="schedule-item-wrapper">
124
+ <div
125
+ role="group"
126
+ aria-label="Schedule item"
127
+ className={`schedule-item-editor ${isSelected ? 'schedule-item-editor--selected' : ''} ${isDragging ? 'schedule-item-editor--dragging' : ''}`}
128
+ onDragOver={handleDragOver}
129
+ onDrop={handleDrop}
130
+ >
131
+ <button
132
+ type="button"
133
+ className="schedule-item-drag"
134
+ draggable
135
+ onDragStart={handleDragStart}
136
+ onDragEnd={handleDragEnd}
137
+ title="Drag to reorder"
138
+ aria-label="Drag to reorder"
139
+ >
140
+
141
+ </button>
142
+ <input
143
+ ref={timeRef}
144
+ className="schedule-item-time"
145
+ type="text"
146
+ value={localData.time}
147
+ onChange={(e) => handleChange('time', e.target.value)}
148
+ placeholder="09:00–10:00"
149
+ size={13}
150
+ />
151
+ <input
152
+ className="schedule-item-title"
153
+ type="text"
154
+ value={localData.title}
155
+ onChange={(e) => handleChange('title', e.target.value)}
156
+ placeholder="Title"
157
+ />
158
+ <input
159
+ className="schedule-item-speaker"
160
+ type="text"
161
+ value={localData.speaker ?? ''}
162
+ onChange={(e) => handleChange('speaker', e.target.value)}
163
+ placeholder="Speaker"
164
+ />
165
+ <input
166
+ className="schedule-item-description"
167
+ type="text"
168
+ value={localData.description ?? ''}
169
+ onChange={(e) => handleChange('description', e.target.value)}
170
+ placeholder="Description"
171
+ />
172
+ <button
173
+ type="button"
174
+ className="schedule-item-remove"
175
+ onClick={removeNode}
176
+ title="Remove item"
177
+ aria-label="Remove schedule item"
178
+ >
179
+ ×
180
+ </button>
181
+ </div>
182
+ {isLast && (
183
+ <button
184
+ type="button"
185
+ className="schedule-item-add"
186
+ onClick={insertAfter}
187
+ title="Add item"
188
+ aria-label="Add schedule item"
189
+ >
190
+ +
191
+ </button>
192
+ )}
193
+ </div>
194
+ );
195
+ };
@@ -0,0 +1,135 @@
1
+ import {
2
+ $applyNodeReplacement,
3
+ DecoratorNode,
4
+ type DOMConversionMap,
5
+ type DOMExportOutput,
6
+ type LexicalNode,
7
+ type NodeKey,
8
+ type SerializedLexicalNode,
9
+ type Spread,
10
+ } from 'lexical';
11
+ import { createElement, type ReactElement } from 'react';
12
+
13
+ import { ScheduleItemComponent } from './ScheduleItemComponent';
14
+
15
+ export interface ScheduleItemData {
16
+ time: string;
17
+ title: string;
18
+ speaker?: string;
19
+ description?: string;
20
+ }
21
+
22
+ export type SerializedScheduleItemNode = Spread<{ data: ScheduleItemData }, SerializedLexicalNode>;
23
+
24
+ export class ScheduleItemNode extends DecoratorNode<ReactElement> {
25
+ __data: ScheduleItemData;
26
+
27
+ static getType(): string {
28
+ return 'schedule-item';
29
+ }
30
+
31
+ static clone(node: ScheduleItemNode): ScheduleItemNode {
32
+ return new ScheduleItemNode({ ...node.__data }, node.__key);
33
+ }
34
+
35
+ constructor(data: ScheduleItemData, key?: NodeKey) {
36
+ super(key);
37
+ this.__data = data;
38
+ }
39
+
40
+ getData(): ScheduleItemData {
41
+ return this.getLatest().__data;
42
+ }
43
+
44
+ setData(data: ScheduleItemData): void {
45
+ const self = this.getWritable();
46
+ self.__data = data;
47
+ }
48
+
49
+ // --- DOM ---
50
+
51
+ createDOM(): HTMLElement {
52
+ const div = document.createElement('div');
53
+ div.className = 'schedule-item';
54
+ return div;
55
+ }
56
+
57
+ updateDOM(): boolean {
58
+ return false;
59
+ }
60
+
61
+ static importDOM(): DOMConversionMap | null {
62
+ return {
63
+ div: (node: HTMLElement) => {
64
+ if (!node.classList.contains('schedule-item')) return null;
65
+ return {
66
+ conversion: () => {
67
+ const data = {
68
+ time: node.dataset.time ?? '',
69
+ title: node.dataset.title ?? '',
70
+ speaker: node.dataset.speaker || undefined,
71
+ description: node.dataset.description || undefined,
72
+ };
73
+ return { node: $createScheduleItemNode(data) };
74
+ },
75
+ priority: 1,
76
+ };
77
+ },
78
+ };
79
+ }
80
+
81
+ exportDOM(): DOMExportOutput {
82
+ const el = document.createElement('div');
83
+ el.className = 'schedule-item';
84
+ el.dataset.time = this.__data.time;
85
+ el.dataset.title = this.__data.title;
86
+ if (this.__data.speaker) el.dataset.speaker = this.__data.speaker;
87
+ if (this.__data.description) el.dataset.description = this.__data.description;
88
+ return { element: el };
89
+ }
90
+
91
+ // --- Serialization ---
92
+
93
+ static importJSON(json: SerializedScheduleItemNode): ScheduleItemNode {
94
+ return $createScheduleItemNode(json.data);
95
+ }
96
+
97
+ exportJSON(): SerializedScheduleItemNode {
98
+ return {
99
+ ...super.exportJSON(),
100
+ type: 'schedule-item',
101
+ data: this.__data,
102
+ };
103
+ }
104
+
105
+ // --- Text ---
106
+
107
+ getTextContent(): string {
108
+ const { time, title, speaker, description } = this.__data;
109
+ const parts = [title, speaker, description].filter(Boolean);
110
+ return `${time} ${parts.join(' | ')}`;
111
+ }
112
+
113
+ isInline(): boolean {
114
+ return false;
115
+ }
116
+
117
+ // --- Decorator (React rendering is handled by the component) ---
118
+
119
+ decorate(): ReactElement {
120
+ return createElement(ScheduleItemComponent, {
121
+ nodeKey: this.__key,
122
+ data: this.__data,
123
+ });
124
+ }
125
+ }
126
+
127
+ export function $createScheduleItemNode(data: ScheduleItemData): ScheduleItemNode {
128
+ return $applyNodeReplacement(new ScheduleItemNode(data));
129
+ }
130
+
131
+ export function $isScheduleItemNode(
132
+ node: LexicalNode | null | undefined
133
+ ): node is ScheduleItemNode {
134
+ return node instanceof ScheduleItemNode;
135
+ }
@@ -0,0 +1,80 @@
1
+ import type { ElementTransformer } from '@lexical/markdown';
2
+
3
+ import {
4
+ $createScheduleItemNode,
5
+ $isScheduleItemNode,
6
+ type ScheduleItemData,
7
+ ScheduleItemNode,
8
+ } from './ScheduleItemNode';
9
+
10
+ /**
11
+ * Markdown format: `- **09:00–10:00** Title | Speaker | Description`
12
+ *
13
+ * This matches the format expected by the remarkSchedule plugin in
14
+ * markdown-plugin-happening, so the same markdown renders correctly
15
+ * both in the editor and on the frontend.
16
+ */
17
+ const SCHEDULE_ITEM_REGEX =
18
+ /^-\s+\*\*(\d{1,2}[.:]\d{2}(?:\s*[–-]\s*\d{1,2}[.:]\d{2})?)\s*\*\*\s+(.+)$/;
19
+
20
+ export function normalizeScheduleItemData(data: {
21
+ time: string;
22
+ title: string;
23
+ speaker?: string;
24
+ description?: string;
25
+ }) {
26
+ return {
27
+ time: data.time.trim(),
28
+ title: data.title.trim(),
29
+ speaker: data.speaker?.trim() || undefined,
30
+ description: data.description?.trim() || undefined,
31
+ };
32
+ }
33
+
34
+ export function parseScheduleItemMarkdown(markdown: string) {
35
+ const match = markdown.match(SCHEDULE_ITEM_REGEX);
36
+ if (!match) return null;
37
+
38
+ const time = (match[1] ?? '').replaceAll('.', ':').trim();
39
+ const rest = (match[2] ?? '').trim();
40
+ const parts = rest.split('|').map((s) => s.trim());
41
+
42
+ return normalizeScheduleItemData({
43
+ time,
44
+ title: parts[0] ?? '',
45
+ speaker: parts[1] || undefined,
46
+ description: parts[2] || undefined,
47
+ });
48
+ }
49
+
50
+ export function formatScheduleItemMarkdown(data: ScheduleItemData): string {
51
+ const normalized = normalizeScheduleItemData(data);
52
+ const parts = [normalized.title, normalized.speaker, normalized.description].filter(Boolean);
53
+ return `- **${normalized.time}** ${parts.join(' | ')}`;
54
+ }
55
+
56
+ export const SCHEDULE_ITEM_TRANSFORMER: ElementTransformer = {
57
+ type: 'element',
58
+ dependencies: [ScheduleItemNode],
59
+
60
+ regExp: SCHEDULE_ITEM_REGEX,
61
+
62
+ export: (node) => {
63
+ if (!$isScheduleItemNode(node)) return null;
64
+
65
+ return formatScheduleItemMarkdown(node.getData());
66
+ },
67
+
68
+ replace: (parentNode, _children, match) => {
69
+ const parsed = parseScheduleItemMarkdown(match[0] ?? '');
70
+
71
+ const node = $createScheduleItemNode(
72
+ parsed ?? {
73
+ time: ((match[1] ?? '').replaceAll('.', ':').trim() || '').trim(),
74
+ title: '',
75
+ }
76
+ );
77
+
78
+ parentNode.replace(node);
79
+ },
80
+ };
@@ -0,0 +1,4 @@
1
+ export type { ScheduleItemData, SerializedScheduleItemNode } from './ScheduleItemNode';
2
+ export { $createScheduleItemNode, $isScheduleItemNode, ScheduleItemNode } from './ScheduleItemNode';
3
+ export { SCHEDULE_ITEM_TRANSFORMER } from './ScheduleTransformer';
4
+ export { schedulePlugin } from './schedulePlugin';
@@ -0,0 +1,33 @@
1
+ import { $findMatchingParent } from '@lexical/utils';
2
+ import { $getSelection, $isRangeSelection } from 'lexical';
3
+
4
+ import type { ScriboPlugin } from '../../types';
5
+ import { $createScheduleItemNode, ScheduleItemNode } from './ScheduleItemNode';
6
+ import { SCHEDULE_ITEM_TRANSFORMER } from './ScheduleTransformer';
7
+
8
+ export const schedulePlugin: ScriboPlugin = {
9
+ name: 'schedule',
10
+ nodes: [ScheduleItemNode],
11
+ transformers: [SCHEDULE_ITEM_TRANSFORMER],
12
+ toolbarButtons: [
13
+ {
14
+ key: 'schedule-item',
15
+ label: 'Schedule Item',
16
+ iconClass: 'schedule',
17
+ onSelect: (editor) => {
18
+ editor.update(() => {
19
+ const selection = $getSelection();
20
+ if (!$isRangeSelection(selection)) return;
21
+
22
+ const anchor = selection.anchor.getNode();
23
+ const topLevel =
24
+ $findMatchingParent(anchor, (n) => n.getParent()?.getType() === 'root') ?? anchor;
25
+
26
+ const node = $createScheduleItemNode({ time: '', title: '' });
27
+ topLevel.insertAfter(node);
28
+ node.selectEnd();
29
+ });
30
+ },
31
+ },
32
+ ],
33
+ };
@@ -6,19 +6,14 @@
6
6
  *
7
7
  */
8
8
 
9
- import type {JSX} from 'react';
10
-
11
- import {
12
- AutoLinkPlugin,
13
- createLinkMatcherWithRegExp,
14
- } from '@lexical/react/LexicalAutoLinkPlugin';
15
- import * as React from 'react';
9
+ import { AutoLinkPlugin, createLinkMatcherWithRegExp } from '@lexical/react/LexicalAutoLinkPlugin';
10
+ import type { JSX } from 'react';
16
11
 
17
12
  const URL_REGEX =
18
- /((https?:\/\/(www\.)?)|(www\.))[-a-zA-Z0-9@:%._+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_+.~#?&//=]*)(?<![-.+():%])/;
13
+ /((https?:\/\/(www\.)?)|(www\.))[-a-zA-Z0-9@:%._+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_+.~#?&/=]*)(?<![-.+():%])/;
19
14
 
20
15
  const EMAIL_REGEX =
21
- /(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))/;
16
+ /(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@((\[\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}\])|(([a-zA-Z\-\d]+\.)+[a-zA-Z]{2,}))/;
22
17
 
23
18
  const MATCHERS = [
24
19
  createLinkMatcherWithRegExp(URL_REGEX, (text) => {
@@ -31,4 +26,4 @@ const MATCHERS = [
31
26
 
32
27
  export default function LexicalAutoLinkPlugin(): JSX.Element {
33
28
  return <AutoLinkPlugin matchers={MATCHERS} />;
34
- }
29
+ }