@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.
- package/dist/App.d.ts +11 -0
- package/dist/App.d.ts.map +1 -0
- package/dist/MarkdownEditor.d.ts +18 -0
- package/dist/MarkdownEditor.d.ts.map +1 -0
- package/dist/MarkdownEditor2.js +111 -0
- package/dist/MarkdownEditor2.js.map +1 -0
- package/dist/MarkdownInput.d.ts +27 -0
- package/dist/MarkdownInput.d.ts.map +1 -0
- package/dist/MarkdownInput.js +57 -0
- package/dist/MarkdownInput.js.map +1 -0
- package/dist/callout.js +5 -0
- package/dist/context/SharedHistoryContext.d.ts +11 -0
- package/dist/context/SharedHistoryContext.d.ts.map +1 -0
- package/dist/context/SharedHistoryContext.js +12 -0
- package/dist/context/SharedHistoryContext.js.map +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/main.d.ts +5 -0
- package/dist/main.d.ts.map +1 -0
- package/dist/main.js +4 -0
- package/dist/nodes/EditorNodes.d.ts +4 -0
- package/dist/nodes/EditorNodes.d.ts.map +1 -0
- package/dist/nodes/EditorNodes.js +39 -0
- package/dist/nodes/EditorNodes.js.map +1 -0
- package/dist/nodes/callout/CalloutComponent.d.ts +6 -0
- package/dist/nodes/callout/CalloutComponent.d.ts.map +1 -0
- package/dist/nodes/callout/CalloutComponent.js +53 -0
- package/dist/nodes/callout/CalloutComponent.js.map +1 -0
- package/dist/nodes/callout/CalloutNode.d.ts +27 -0
- package/dist/nodes/callout/CalloutNode.d.ts.map +1 -0
- package/dist/nodes/callout/CalloutNode.js +153 -0
- package/dist/nodes/callout/CalloutNode.js.map +1 -0
- package/dist/nodes/callout/CalloutTransformer.d.ts +3 -0
- package/dist/nodes/callout/CalloutTransformer.d.ts.map +1 -0
- package/dist/nodes/callout/CalloutTransformer.js +48 -0
- package/dist/nodes/callout/CalloutTransformer.js.map +1 -0
- package/dist/nodes/callout/calloutPlugin.d.ts +3 -0
- package/dist/nodes/callout/calloutPlugin.d.ts.map +1 -0
- package/dist/nodes/callout/calloutPlugin.js +34 -0
- package/dist/nodes/callout/calloutPlugin.js.map +1 -0
- package/dist/nodes/callout/index.d.ts +6 -0
- package/dist/nodes/callout/index.d.ts.map +1 -0
- package/dist/nodes/schedule/ScheduleItemComponent.d.ts +9 -0
- package/dist/nodes/schedule/ScheduleItemComponent.d.ts.map +1 -0
- package/dist/nodes/schedule/ScheduleItemComponent.js +167 -0
- package/dist/nodes/schedule/ScheduleItemComponent.js.map +1 -0
- package/dist/nodes/schedule/ScheduleItemNode.d.ts +31 -0
- package/dist/nodes/schedule/ScheduleItemNode.d.ts.map +1 -0
- package/dist/nodes/schedule/ScheduleItemNode.js +94 -0
- package/dist/nodes/schedule/ScheduleItemNode.js.map +1 -0
- package/dist/nodes/schedule/ScheduleTransformer.d.ts +22 -0
- package/dist/nodes/schedule/ScheduleTransformer.d.ts.map +1 -0
- package/dist/nodes/schedule/ScheduleTransformer.js +60 -0
- package/dist/nodes/schedule/ScheduleTransformer.js.map +1 -0
- package/dist/nodes/schedule/index.d.ts +5 -0
- package/dist/nodes/schedule/index.d.ts.map +1 -0
- package/dist/nodes/schedule/schedulePlugin.d.ts +3 -0
- package/dist/nodes/schedule/schedulePlugin.d.ts.map +1 -0
- package/dist/nodes/schedule/schedulePlugin.js +33 -0
- package/dist/nodes/schedule/schedulePlugin.js.map +1 -0
- package/dist/plugins/AutoLinkPlugin.d.ts +3 -0
- package/dist/plugins/AutoLinkPlugin.d.ts.map +1 -0
- package/dist/plugins/AutoLinkPlugin.js +22 -0
- package/dist/plugins/AutoLinkPlugin.js.map +1 -0
- package/dist/plugins/FloatingLinkEditorPlugin.d.ts +7 -0
- package/dist/plugins/FloatingLinkEditorPlugin.d.ts.map +1 -0
- package/dist/plugins/FloatingLinkEditorPlugin2.js +302 -0
- package/dist/plugins/FloatingLinkEditorPlugin2.js.map +1 -0
- package/dist/plugins/HistoryPlugin.d.ts +26 -0
- package/dist/plugins/HistoryPlugin.d.ts.map +1 -0
- package/dist/plugins/LinkPlugin.d.ts +3 -0
- package/dist/plugins/LinkPlugin.d.ts.map +1 -0
- package/dist/plugins/LinkPlugin.js +18 -0
- package/dist/plugins/LinkPlugin.js.map +1 -0
- package/dist/plugins/ToolbarPlugin.d.ts +7 -0
- package/dist/plugins/ToolbarPlugin.d.ts.map +1 -0
- package/dist/plugins/ToolbarPlugin.js +372 -0
- package/dist/plugins/ToolbarPlugin.js.map +1 -0
- package/dist/schedule.js +4 -0
- package/dist/scribo.css +1977 -0
- package/dist/themes/EditorTheme.d.ts +4 -0
- package/dist/themes/EditorTheme.d.ts.map +1 -0
- package/dist/themes/EditorTheme2.js +107 -0
- package/dist/themes/EditorTheme2.js.map +1 -0
- package/dist/types.d.ts +26 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/ui/ContentEditable.d.ts +6 -0
- package/dist/ui/ContentEditable.d.ts.map +1 -0
- package/dist/ui/ContentEditable2.js +14 -0
- package/dist/ui/ContentEditable2.js.map +1 -0
- package/dist/ui/DropDown.d.ts +18 -0
- package/dist/ui/DropDown.d.ts.map +1 -0
- package/dist/ui/DropDown.js +162 -0
- package/dist/ui/DropDown.js.map +1 -0
- package/dist/ui/Placeholder.d.ts +6 -0
- package/dist/ui/Placeholder.d.ts.map +1 -0
- package/dist/ui/Placeholder2.js +13 -0
- package/dist/ui/Placeholder2.js.map +1 -0
- package/dist/utils/environment.d.ts +24 -0
- package/dist/utils/environment.d.ts.map +1 -0
- package/dist/utils/environment.js +20 -0
- package/dist/utils/environment.js.map +1 -0
- package/dist/utils/getSelectedNode.d.ts +3 -0
- package/dist/utils/getSelectedNode.d.ts.map +1 -0
- package/dist/utils/getSelectedNode.js +23 -0
- package/dist/utils/getSelectedNode.js.map +1 -0
- package/dist/utils/setFloatingElemPosition.d.ts +2 -0
- package/dist/utils/setFloatingElemPosition.d.ts.map +1 -0
- package/dist/utils/setFloatingElemPositionForLinkEditor.d.ts +2 -0
- package/dist/utils/setFloatingElemPositionForLinkEditor.d.ts.map +1 -0
- package/dist/utils/setFloatingElemPositionForLinkEditor.js +33 -0
- package/dist/utils/setFloatingElemPositionForLinkEditor.js.map +1 -0
- package/dist/utils/url.d.ts +11 -0
- package/dist/utils/url.d.ts.map +1 -0
- package/dist/utils/url.js +33 -0
- package/dist/utils/url.js.map +1 -0
- package/dist/vite.svg +1 -0
- package/package.json +55 -38
- package/src/MarkdownEditor.css +61 -47
- package/src/MarkdownEditor.tsx +76 -29
- package/src/MarkdownInput.tsx +9 -10
- package/src/context/SharedHistoryContext.tsx +8 -16
- package/src/main.tsx +3 -3
- package/src/nodes/EditorNodes.ts +10 -10
- package/src/nodes/callout/Callout.css +94 -0
- package/src/nodes/callout/CalloutComponent.tsx +61 -0
- package/src/nodes/callout/CalloutNode.ts +197 -0
- package/src/nodes/callout/CalloutTransformer.ts +87 -0
- package/src/nodes/callout/calloutPlugin.ts +37 -0
- package/src/nodes/callout/index.ts +5 -0
- package/src/nodes/schedule/ScheduleItem.css +140 -0
- package/src/nodes/schedule/ScheduleItemComponent.tsx +195 -0
- package/src/nodes/schedule/ScheduleItemNode.ts +135 -0
- package/src/nodes/schedule/ScheduleTransformer.ts +80 -0
- package/src/nodes/schedule/index.ts +4 -0
- package/src/nodes/schedule/schedulePlugin.ts +33 -0
- package/src/plugins/AutoLinkPlugin.tsx +5 -10
- package/src/plugins/FloatingLinkEditorPlugin.tsx +42 -66
- package/src/plugins/HistoryPlugin.tsx +20 -30
- package/src/plugins/LinkPlugin.tsx +2 -3
- package/src/plugins/ToolbarPlugin.tsx +129 -141
- package/src/themes/EditorTheme.css +9 -5
- package/src/themes/EditorTheme.ts +89 -90
- package/src/types.ts +27 -0
- package/src/ui/ContentEditable.tsx +5 -5
- package/src/ui/DropDown.tsx +23 -41
- package/src/ui/Placeholder.tsx +21 -21
- package/src/utils/environment.ts +6 -12
- package/src/utils/getSelectedNode.ts +3 -5
- package/src/utils/setFloatingElemPosition.ts +2 -5
- package/src/utils/setFloatingElemPositionForLinkEditor.ts +1 -1
- package/src/utils/url.ts +4 -10
- package/tsconfig.json +8 -0
- package/CHANGELOG.md +0 -65
- package/README.md +0 -126
- package/RELEASE.md +0 -102
- package/eslint.config.js +0 -4
- package/src/App.css +0 -35
- package/src/App.tsx +0 -42
- package/src/index.tsx +0 -13
- package/tsconfig.node.json +0 -11
- package/vite.config.site.ts +0 -19
- package/vite.config.ts +0 -33
- /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
|
|
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@"]+)*)|(".+"))@((\[
|
|
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
|
+
}
|