@x33025/sveltely 0.1.10 → 0.1.12

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 (56) hide show
  1. package/dist/components/Library/ArticleEditor/ArticleBlockCode.svelte +21 -0
  2. package/dist/components/Library/ArticleEditor/ArticleBlockCode.svelte.d.ts +8 -0
  3. package/dist/components/Library/ArticleEditor/ArticleBlockDragControl.svelte +144 -0
  4. package/dist/components/Library/ArticleEditor/ArticleBlockDragControl.svelte.d.ts +14 -0
  5. package/dist/components/Library/ArticleEditor/ArticleBlockFAQ.svelte +47 -0
  6. package/dist/components/Library/ArticleEditor/ArticleBlockFAQ.svelte.d.ts +8 -0
  7. package/dist/components/Library/ArticleEditor/ArticleBlockFallback.svelte +79 -0
  8. package/dist/components/Library/ArticleEditor/ArticleBlockFallback.svelte.d.ts +15 -0
  9. package/dist/components/Library/ArticleEditor/ArticleBlockHeading.svelte +73 -0
  10. package/dist/components/Library/ArticleEditor/ArticleBlockHeading.svelte.d.ts +14 -0
  11. package/dist/components/Library/ArticleEditor/ArticleBlockImage.svelte +48 -0
  12. package/dist/components/Library/ArticleEditor/ArticleBlockImage.svelte.d.ts +9 -0
  13. package/dist/components/Library/ArticleEditor/ArticleBlockInsertControl.svelte +120 -0
  14. package/dist/components/Library/ArticleEditor/ArticleBlockInsertControl.svelte.d.ts +9 -0
  15. package/dist/components/Library/ArticleEditor/ArticleBlockList.svelte +114 -0
  16. package/dist/components/Library/ArticleEditor/ArticleBlockList.svelte.d.ts +15 -0
  17. package/dist/components/Library/ArticleEditor/ArticleBlockParagraph.svelte +79 -0
  18. package/dist/components/Library/ArticleEditor/ArticleBlockParagraph.svelte.d.ts +15 -0
  19. package/dist/components/Library/ArticleEditor/ArticleBlockShell.svelte +127 -0
  20. package/dist/components/Library/ArticleEditor/ArticleBlockShell.svelte.d.ts +22 -0
  21. package/dist/components/Library/ArticleEditor/ArticleBlockTable.svelte +274 -0
  22. package/dist/components/Library/ArticleEditor/ArticleBlockTable.svelte.d.ts +13 -0
  23. package/dist/components/Library/ArticleEditor/ArticleEditor.svelte +192 -0
  24. package/dist/components/Library/ArticleEditor/ArticleEditor.svelte.d.ts +39 -0
  25. package/dist/components/Library/ArticleEditor/ArticleEditorBody.svelte +328 -0
  26. package/dist/components/Library/ArticleEditor/ArticleEditorBody.svelte.d.ts +31 -0
  27. package/dist/components/Library/ArticleEditor/ArticleEditorHeader.svelte +57 -0
  28. package/dist/components/Library/ArticleEditor/ArticleEditorHeader.svelte.d.ts +11 -0
  29. package/dist/components/Library/ArticleEditor/ArticleImagePreview.svelte +71 -0
  30. package/dist/components/Library/ArticleEditor/ArticleImagePreview.svelte.d.ts +8 -0
  31. package/dist/components/Library/ArticleEditor/articleEditor.svelte.js +532 -0
  32. package/dist/components/Library/ArticleEditor/index.d.ts +18 -0
  33. package/dist/components/Library/ArticleEditor/index.js +16 -0
  34. package/dist/components/Library/ArticleEditor/types.d.ts +37 -0
  35. package/dist/components/Library/ArticleEditor/types.js +1 -0
  36. package/dist/components/Library/Floating/Floating.svelte +2 -1
  37. package/dist/components/Library/Grid/index.d.ts +1 -0
  38. package/dist/components/Library/Grid/index.js +1 -0
  39. package/dist/components/Library/HStack/HStack.svelte +12 -7
  40. package/dist/components/Library/Sheet/Sheet.svelte +1 -0
  41. package/dist/components/Library/TextEditor/TextEditor.svelte +94 -0
  42. package/dist/components/Library/TextEditor/TextEditor.svelte.d.ts +16 -0
  43. package/dist/components/Library/TextEditor/index.d.ts +1 -0
  44. package/dist/components/Library/TextEditor/index.js +1 -0
  45. package/dist/components/Library/TokenSearchField/TokenSearchField.svelte +2 -1
  46. package/dist/components/Library/VStack/VStack.svelte +12 -7
  47. package/dist/components/Local/ComponentGrid.svelte +1 -1
  48. package/dist/index.d.ts +3 -1
  49. package/dist/index.js +3 -1
  50. package/dist/style/index.css +1 -0
  51. package/dist/style.css +233 -0
  52. package/package.json +1 -1
  53. package/dist/components/Library/GridItem/index.d.ts +0 -1
  54. package/dist/components/Library/GridItem/index.js +0 -1
  55. /package/dist/components/Library/{GridItem → Grid}/GridItem.svelte +0 -0
  56. /package/dist/components/Library/{GridItem → Grid}/GridItem.svelte.d.ts +0 -0
@@ -0,0 +1,192 @@
1
+ <script lang="ts">
2
+ import VStack from '../VStack';
3
+ import TextEditor from '../TextEditor';
4
+ import ArticleBlockFAQ from './ArticleBlockFAQ.svelte';
5
+ import ArticleEditorBody from './ArticleEditorBody.svelte';
6
+ import ArticleEditorHeader from './ArticleEditorHeader.svelte';
7
+ import type { BlockInsertKind, BlockTextFormat } from './articleEditor.svelte.js';
8
+ import type { ArticleEditorArticle, BlockDraft } from './types.js';
9
+
10
+ let {
11
+ article,
12
+ draftTitle,
13
+ draftImageAltText,
14
+ blocks,
15
+ selectedBlockID,
16
+ onTitleChange,
17
+ onImageAltTextChange,
18
+ onSelectBlock,
19
+ onUpdateBlock,
20
+ onChangeBlockTextFormat,
21
+ onInsertBlockAfter,
22
+ onUpdateListItem,
23
+ onInsertListItemAfter,
24
+ onMergeListItemWithPrevious,
25
+ onRemoveListItem,
26
+ onCreateParagraphAfter,
27
+ onMergeBlockWithPrevious,
28
+ onRemoveBlock,
29
+ onConvertToList,
30
+ onUpdateFAQItem,
31
+ onUpdateTableCell,
32
+ onUpdateTableHeader,
33
+ onAddTableColumn,
34
+ onAddTableRow,
35
+ onRemoveTableColumn,
36
+ onRemoveTableRow,
37
+ onReorderBlock,
38
+ focusTarget,
39
+ focusPosition,
40
+ onFocusHandled,
41
+ onUndo,
42
+ onRedo
43
+ } = $props<{
44
+ article: ArticleEditorArticle;
45
+ draftTitle: string;
46
+ draftImageAltText: string;
47
+ blocks: BlockDraft[];
48
+ selectedBlockID: string | null;
49
+ onTitleChange: (value: string) => void;
50
+ onImageAltTextChange: (value: string) => void;
51
+ onSelectBlock: (id: string) => void;
52
+ onUpdateBlock: (id: string, patch: Partial<BlockDraft>) => void;
53
+ onChangeBlockTextFormat: (id: string, format: BlockTextFormat) => void;
54
+ onInsertBlockAfter: (id: string, kind: BlockInsertKind) => void;
55
+ onUpdateListItem: (blockID: string, index: number, value: string) => void;
56
+ onInsertListItemAfter: (
57
+ blockID: string,
58
+ index: number,
59
+ text?: string,
60
+ currentText?: string | null
61
+ ) => void;
62
+ onMergeListItemWithPrevious: (blockID: string, index: number) => void;
63
+ onRemoveListItem: (blockID: string, index: number) => void;
64
+ onCreateParagraphAfter: (id: string, text?: string, currentText?: string | null) => void;
65
+ onMergeBlockWithPrevious: (id: string) => void;
66
+ onRemoveBlock: (id: string) => void;
67
+ onConvertToList: (id: string, kind: 'bullet_list' | 'numbered_list') => void;
68
+ onUpdateFAQItem: (
69
+ blockID: string,
70
+ index: number,
71
+ field: 'question' | 'answer',
72
+ value: string
73
+ ) => void;
74
+ onUpdateTableCell: (
75
+ blockID: string,
76
+ rowIndex: number,
77
+ cellIndex: number,
78
+ value: string
79
+ ) => void;
80
+ onUpdateTableHeader: (blockID: string, cellIndex: number, value: string) => void;
81
+ onAddTableColumn: (blockID: string) => void;
82
+ onAddTableRow: (blockID: string) => void;
83
+ onRemoveTableColumn: (blockID: string, columnIndex: number) => void;
84
+ onRemoveTableRow: (blockID: string, rowIndex: number) => void;
85
+ onReorderBlock: (draggedID: string, targetID: string, placement: 'before' | 'after') => void;
86
+ focusTarget: string | null;
87
+ focusPosition: number | null;
88
+ onFocusHandled: () => void;
89
+ onUndo: () => void;
90
+ onRedo: () => void;
91
+ }>();
92
+
93
+ const faqBlocks = $derived(blocks.filter((block: BlockDraft) => block.type === 'faq'));
94
+
95
+ const handleGlobalKeyDown = (event: KeyboardEvent) => {
96
+ const isUndoKey = event.key.toLowerCase() === 'z';
97
+ const isRedoKey = event.key.toLowerCase() === 'y';
98
+ const hasShortcutModifier = event.metaKey || event.ctrlKey;
99
+
100
+ if (!hasShortcutModifier || event.altKey) return;
101
+
102
+ if (isUndoKey && !event.shiftKey) {
103
+ event.preventDefault();
104
+ onUndo();
105
+ return;
106
+ }
107
+
108
+ if ((isUndoKey && event.shiftKey) || isRedoKey) {
109
+ event.preventDefault();
110
+ onRedo();
111
+ }
112
+ };
113
+ </script>
114
+
115
+ <svelte:window onkeydown={handleGlobalKeyDown} />
116
+
117
+ <VStack
118
+ width="100%"
119
+ maxWidth="860px"
120
+ gap="2.5rem"
121
+ paddingX="3.5rem"
122
+ paddingY="2.5rem"
123
+ background="var(--sveltely-background-color)"
124
+ >
125
+ <ArticleEditorHeader
126
+ {article}
127
+ {draftTitle}
128
+ {draftImageAltText}
129
+ {onTitleChange}
130
+ {onImageAltTextChange}
131
+ />
132
+
133
+ <ArticleEditorBody
134
+ {draftTitle}
135
+ {blocks}
136
+ {onSelectBlock}
137
+ {onUpdateBlock}
138
+ {onChangeBlockTextFormat}
139
+ {onInsertBlockAfter}
140
+ {onUpdateListItem}
141
+ {onInsertListItemAfter}
142
+ {onMergeListItemWithPrevious}
143
+ {onRemoveListItem}
144
+ {onCreateParagraphAfter}
145
+ {onMergeBlockWithPrevious}
146
+ {onRemoveBlock}
147
+ {onConvertToList}
148
+ {onUpdateTableCell}
149
+ {onUpdateTableHeader}
150
+ {onAddTableColumn}
151
+ {onAddTableRow}
152
+ {onRemoveTableColumn}
153
+ {onRemoveTableRow}
154
+ {onReorderBlock}
155
+ {focusTarget}
156
+ {focusPosition}
157
+ {onFocusHandled}
158
+ />
159
+
160
+ {#if faqBlocks.length > 0}
161
+ <section class="article-editor-faq-section">
162
+ <VStack gap="1.25rem">
163
+ {#each faqBlocks as block (block.id)}
164
+ <TextEditor
165
+ autosize
166
+ value={block.text ?? 'Frequently asked questions'}
167
+ rows={1}
168
+ onInput={(event) =>
169
+ onUpdateBlock(block.id, {
170
+ text: (event.currentTarget as HTMLTextAreaElement).value
171
+ })}
172
+ className="text-2xl leading-tight font-semibold tracking-[-0.025em] text-zinc-950 placeholder:text-zinc-300"
173
+ placeholder="FAQ title"
174
+ />
175
+ <ArticleBlockFAQ {block} onUpdateItem={onUpdateFAQItem} />
176
+ {/each}
177
+ </VStack>
178
+ </section>
179
+ {/if}
180
+ </VStack>
181
+
182
+ <style>
183
+ .article-editor-faq-section {
184
+ display: flex;
185
+ min-width: 0;
186
+ min-height: 0;
187
+ flex-direction: column;
188
+ gap: 1.25rem;
189
+ border-top: 1px solid color-mix(in oklab, var(--sveltely-border-color) 70%, transparent);
190
+ padding-block: 2.5rem;
191
+ }
192
+ </style>
@@ -0,0 +1,39 @@
1
+ import type { BlockInsertKind, BlockTextFormat } from './articleEditor.svelte.js';
2
+ import type { ArticleEditorArticle, BlockDraft } from './types.js';
3
+ type $$ComponentProps = {
4
+ article: ArticleEditorArticle;
5
+ draftTitle: string;
6
+ draftImageAltText: string;
7
+ blocks: BlockDraft[];
8
+ selectedBlockID: string | null;
9
+ onTitleChange: (value: string) => void;
10
+ onImageAltTextChange: (value: string) => void;
11
+ onSelectBlock: (id: string) => void;
12
+ onUpdateBlock: (id: string, patch: Partial<BlockDraft>) => void;
13
+ onChangeBlockTextFormat: (id: string, format: BlockTextFormat) => void;
14
+ onInsertBlockAfter: (id: string, kind: BlockInsertKind) => void;
15
+ onUpdateListItem: (blockID: string, index: number, value: string) => void;
16
+ onInsertListItemAfter: (blockID: string, index: number, text?: string, currentText?: string | null) => void;
17
+ onMergeListItemWithPrevious: (blockID: string, index: number) => void;
18
+ onRemoveListItem: (blockID: string, index: number) => void;
19
+ onCreateParagraphAfter: (id: string, text?: string, currentText?: string | null) => void;
20
+ onMergeBlockWithPrevious: (id: string) => void;
21
+ onRemoveBlock: (id: string) => void;
22
+ onConvertToList: (id: string, kind: 'bullet_list' | 'numbered_list') => void;
23
+ onUpdateFAQItem: (blockID: string, index: number, field: 'question' | 'answer', value: string) => void;
24
+ onUpdateTableCell: (blockID: string, rowIndex: number, cellIndex: number, value: string) => void;
25
+ onUpdateTableHeader: (blockID: string, cellIndex: number, value: string) => void;
26
+ onAddTableColumn: (blockID: string) => void;
27
+ onAddTableRow: (blockID: string) => void;
28
+ onRemoveTableColumn: (blockID: string, columnIndex: number) => void;
29
+ onRemoveTableRow: (blockID: string, rowIndex: number) => void;
30
+ onReorderBlock: (draggedID: string, targetID: string, placement: 'before' | 'after') => void;
31
+ focusTarget: string | null;
32
+ focusPosition: number | null;
33
+ onFocusHandled: () => void;
34
+ onUndo: () => void;
35
+ onRedo: () => void;
36
+ };
37
+ declare const ArticleEditor: import("svelte").Component<$$ComponentProps, {}, "">;
38
+ type ArticleEditor = ReturnType<typeof ArticleEditor>;
39
+ export default ArticleEditor;
@@ -0,0 +1,328 @@
1
+ <script lang="ts">
2
+ import VStack from '../VStack';
3
+ import type { BlockInsertKind, BlockTextFormat } from './articleEditor.svelte.js';
4
+ import type { BlockDraft } from './types.js';
5
+ import ArticleBlockCode from './ArticleBlockCode.svelte';
6
+ import ArticleBlockFallback from './ArticleBlockFallback.svelte';
7
+ import ArticleBlockHeading from './ArticleBlockHeading.svelte';
8
+ import ArticleBlockImage from './ArticleBlockImage.svelte';
9
+ import ArticleBlockList from './ArticleBlockList.svelte';
10
+ import ArticleBlockParagraph from './ArticleBlockParagraph.svelte';
11
+ import ArticleBlockShell from './ArticleBlockShell.svelte';
12
+ import ArticleBlockTable from './ArticleBlockTable.svelte';
13
+
14
+ let {
15
+ draftTitle,
16
+ blocks,
17
+ onSelectBlock,
18
+ onUpdateBlock,
19
+ onChangeBlockTextFormat,
20
+ onInsertBlockAfter,
21
+ onUpdateListItem,
22
+ onInsertListItemAfter,
23
+ onMergeListItemWithPrevious,
24
+ onRemoveListItem,
25
+ onCreateParagraphAfter,
26
+ onMergeBlockWithPrevious,
27
+ onRemoveBlock,
28
+ onConvertToList,
29
+ onUpdateTableCell,
30
+ onUpdateTableHeader,
31
+ onAddTableColumn,
32
+ onAddTableRow,
33
+ onRemoveTableColumn,
34
+ onRemoveTableRow,
35
+ onReorderBlock,
36
+ focusTarget,
37
+ focusPosition,
38
+ onFocusHandled
39
+ } = $props<{
40
+ draftTitle: string;
41
+ blocks: BlockDraft[];
42
+ onSelectBlock: (id: string) => void;
43
+ onUpdateBlock: (id: string, patch: Partial<BlockDraft>) => void;
44
+ onChangeBlockTextFormat: (id: string, format: BlockTextFormat) => void;
45
+ onInsertBlockAfter: (id: string, kind: BlockInsertKind) => void;
46
+ onUpdateListItem: (blockID: string, index: number, value: string) => void;
47
+ onInsertListItemAfter: (
48
+ blockID: string,
49
+ index: number,
50
+ text?: string,
51
+ currentText?: string | null
52
+ ) => void;
53
+ onMergeListItemWithPrevious: (blockID: string, index: number) => void;
54
+ onRemoveListItem: (blockID: string, index: number) => void;
55
+ onCreateParagraphAfter: (id: string, text?: string, currentText?: string | null) => void;
56
+ onMergeBlockWithPrevious: (id: string) => void;
57
+ onRemoveBlock: (id: string) => void;
58
+ onConvertToList: (id: string, kind: 'bullet_list' | 'numbered_list') => void;
59
+ onUpdateTableCell: (
60
+ blockID: string,
61
+ rowIndex: number,
62
+ cellIndex: number,
63
+ value: string
64
+ ) => void;
65
+ onUpdateTableHeader: (blockID: string, cellIndex: number, value: string) => void;
66
+ onAddTableColumn: (blockID: string) => void;
67
+ onAddTableRow: (blockID: string) => void;
68
+ onRemoveTableColumn: (blockID: string, columnIndex: number) => void;
69
+ onRemoveTableRow: (blockID: string, rowIndex: number) => void;
70
+ onReorderBlock: (draggedID: string, targetID: string, placement: 'before' | 'after') => void;
71
+ focusTarget: string | null;
72
+ focusPosition: number | null;
73
+ onFocusHandled: () => void;
74
+ }>();
75
+
76
+ let draggedBlockID = $state<string | null>(null);
77
+ let dropTarget = $state<{ id: string; placement: 'before' | 'after' } | null>(null);
78
+ let autoScrollFrame: number | null = null;
79
+ let autoScrollVelocity = 0;
80
+ let autoScrollTarget: HTMLElement | Window | null = null;
81
+
82
+ const articleBlocks = $derived(blocks.filter((block: BlockDraft) => block.type !== 'faq'));
83
+
84
+ const blockTextFormat = (block: BlockDraft): BlockTextFormat | null => {
85
+ if (block.type === 'heading') {
86
+ const level = Math.min(3, Math.max(1, Number(block.level ?? 2)));
87
+ if (level === 1) return null;
88
+ return `heading-${level}` as BlockTextFormat;
89
+ }
90
+ if (block.type === 'paragraph' || block.type === 'code') return 'paragraph';
91
+ if (block.type === 'numbered_list') return 'numbered_list';
92
+ if (block.type === 'bullet_list' || block.type === 'list') return 'bullet_list';
93
+ return null;
94
+ };
95
+
96
+ const canChangeBlockTextFormat = (block: BlockDraft) => blockTextFormat(block) !== null;
97
+
98
+ const updateDropTarget = (event: DragEvent, id: string) => {
99
+ if (!draggedBlockID || draggedBlockID === id) {
100
+ dropTarget = null;
101
+ return;
102
+ }
103
+
104
+ const rect = (event.currentTarget as HTMLElement).getBoundingClientRect();
105
+ dropTarget = {
106
+ id,
107
+ placement: event.clientY < rect.top + rect.height / 2 ? 'before' : 'after'
108
+ };
109
+ };
110
+
111
+ const stopAutoScroll = () => {
112
+ autoScrollVelocity = 0;
113
+ if (autoScrollFrame === null) return;
114
+ cancelAnimationFrame(autoScrollFrame);
115
+ autoScrollFrame = null;
116
+ };
117
+
118
+ const runAutoScroll = () => {
119
+ if (!autoScrollVelocity) {
120
+ autoScrollFrame = null;
121
+ return;
122
+ }
123
+
124
+ const target = autoScrollTarget ?? window;
125
+ if (target instanceof Window) {
126
+ target.scrollBy(0, autoScrollVelocity);
127
+ } else {
128
+ target.scrollTop += autoScrollVelocity;
129
+ }
130
+ autoScrollFrame = requestAnimationFrame(runAutoScroll);
131
+ };
132
+
133
+ const scrollTargetFor = (event: DragEvent): HTMLElement | Window =>
134
+ ((event.target as HTMLElement | null)?.closest?.(
135
+ '.scroll-view, .navigation-stack-content'
136
+ ) as HTMLElement | null) ?? window;
137
+
138
+ const updateAutoScroll = (event: DragEvent) => {
139
+ if (!draggedBlockID) {
140
+ stopAutoScroll();
141
+ return;
142
+ }
143
+
144
+ autoScrollTarget = scrollTargetFor(event);
145
+ const scrollRect =
146
+ autoScrollTarget instanceof Window
147
+ ? { top: 0, bottom: window.innerHeight, height: window.innerHeight }
148
+ : autoScrollTarget.getBoundingClientRect();
149
+ const edgeSize = Math.min(160, scrollRect.height / 3);
150
+ const maxSpeed = 22;
151
+ const topDistance = event.clientY - scrollRect.top;
152
+ const bottomDistance = scrollRect.bottom - event.clientY;
153
+
154
+ if (topDistance < edgeSize) {
155
+ autoScrollVelocity = -Math.ceil(((edgeSize - topDistance) / edgeSize) * maxSpeed);
156
+ } else if (bottomDistance < edgeSize) {
157
+ autoScrollVelocity = Math.ceil(((edgeSize - bottomDistance) / edgeSize) * maxSpeed);
158
+ } else {
159
+ stopAutoScroll();
160
+ return;
161
+ }
162
+
163
+ if (autoScrollFrame === null) {
164
+ autoScrollFrame = requestAnimationFrame(runAutoScroll);
165
+ }
166
+ };
167
+
168
+ const dropPlacementFor = (id: string) => {
169
+ const currentDropTarget = dropTarget;
170
+ return currentDropTarget?.id === id ? currentDropTarget.placement : null;
171
+ };
172
+
173
+ const handleBlockDragOver = (event: DragEvent, id: string) => {
174
+ if (!draggedBlockID) return;
175
+ event.preventDefault();
176
+ event.dataTransfer!.dropEffect = 'move';
177
+ updateAutoScroll(event);
178
+ updateDropTarget(event, id);
179
+ };
180
+
181
+ const handleBlockDragLeave = (event: DragEvent) => {
182
+ if (!(event.currentTarget as HTMLElement).contains(event.relatedTarget as Node | null)) {
183
+ dropTarget = null;
184
+ }
185
+ };
186
+
187
+ const handleBlockDrop = (event: DragEvent, id: string) => {
188
+ event.preventDefault();
189
+ const draggedID = event.dataTransfer?.getData('text/plain') || draggedBlockID;
190
+ const currentDropTarget = dropTarget;
191
+ if (draggedID && currentDropTarget && currentDropTarget.id === id) {
192
+ onReorderBlock(draggedID, id, currentDropTarget.placement);
193
+ }
194
+ draggedBlockID = null;
195
+ dropTarget = null;
196
+ stopAutoScroll();
197
+ };
198
+
199
+ const handleBlockDragStart = (event: DragEvent, id: string) => {
200
+ draggedBlockID = id;
201
+ dropTarget = null;
202
+ event.dataTransfer!.effectAllowed = 'move';
203
+ event.dataTransfer!.setData('text/plain', id);
204
+ const row = (event.currentTarget as HTMLElement).closest<HTMLElement>('[data-block-row]');
205
+ if (row) {
206
+ const rect = row.getBoundingClientRect();
207
+ event.dataTransfer!.setDragImage(row, event.clientX - rect.left, event.clientY - rect.top);
208
+ }
209
+ };
210
+
211
+ const handleBlockDragEnd = () => {
212
+ draggedBlockID = null;
213
+ dropTarget = null;
214
+ stopAutoScroll();
215
+ };
216
+
217
+ $effect(() => {
218
+ const handleWindowDragOver = (event: DragEvent) => {
219
+ if (!draggedBlockID) return;
220
+ updateAutoScroll(event);
221
+ };
222
+
223
+ const handleWindowDrop = () => {
224
+ draggedBlockID = null;
225
+ dropTarget = null;
226
+ stopAutoScroll();
227
+ };
228
+
229
+ window.addEventListener('dragover', handleWindowDragOver);
230
+ window.addEventListener('drop', handleWindowDrop);
231
+
232
+ return () => {
233
+ window.removeEventListener('dragover', handleWindowDragOver);
234
+ window.removeEventListener('drop', handleWindowDrop);
235
+ stopAutoScroll();
236
+ };
237
+ });
238
+ </script>
239
+
240
+ <VStack gap="0.5rem" paddingX="0" paddingY="0">
241
+ {#each articleBlocks as block (block.id)}
242
+ {@const dropPlacement = dropPlacementFor(block.id)}
243
+ {@const textFormat = blockTextFormat(block)}
244
+ {@const canChangeTextFormat = canChangeBlockTextFormat(block)}
245
+ <ArticleBlockShell
246
+ {block}
247
+ {draggedBlockID}
248
+ {dropPlacement}
249
+ {textFormat}
250
+ {canChangeTextFormat}
251
+ onSelect={onSelectBlock}
252
+ onChangeTextFormat={onChangeBlockTextFormat}
253
+ {onInsertBlockAfter}
254
+ onDragOver={handleBlockDragOver}
255
+ onDragLeave={handleBlockDragLeave}
256
+ onDrop={handleBlockDrop}
257
+ onDragStart={handleBlockDragStart}
258
+ onDragEnd={handleBlockDragEnd}
259
+ >
260
+ {#snippet children()}
261
+ {#if block.type === 'heading'}
262
+ <ArticleBlockHeading
263
+ {block}
264
+ onUpdate={onUpdateBlock}
265
+ {onCreateParagraphAfter}
266
+ {onMergeBlockWithPrevious}
267
+ {onRemoveBlock}
268
+ shouldFocus={focusTarget === block.id}
269
+ focusPosition={focusTarget === block.id ? focusPosition : null}
270
+ onFocused={onFocusHandled}
271
+ />
272
+ {:else if block.type === 'paragraph'}
273
+ <ArticleBlockParagraph
274
+ {block}
275
+ onUpdate={onUpdateBlock}
276
+ {onCreateParagraphAfter}
277
+ {onMergeBlockWithPrevious}
278
+ {onRemoveBlock}
279
+ {onConvertToList}
280
+ shouldFocus={focusTarget === block.id}
281
+ focusPosition={focusTarget === block.id ? focusPosition : null}
282
+ onFocused={onFocusHandled}
283
+ />
284
+ {:else if block.type === 'bullet_list' || block.type === 'list' || block.type === 'numbered_list'}
285
+ <ArticleBlockList
286
+ {block}
287
+ onUpdateItem={onUpdateListItem}
288
+ onInsertItemAfter={onInsertListItemAfter}
289
+ {onMergeListItemWithPrevious}
290
+ {onCreateParagraphAfter}
291
+ {onRemoveListItem}
292
+ focusItemIndex={focusTarget?.startsWith(`${block.id}:`)
293
+ ? Number(focusTarget.split(':')[1])
294
+ : null}
295
+ focusPosition={focusTarget?.startsWith(`${block.id}:`) ? focusPosition : null}
296
+ onFocused={onFocusHandled}
297
+ />
298
+ {:else if block.type === 'image'}
299
+ <ArticleBlockImage {block} titleFallback={draftTitle} onUpdate={onUpdateBlock} />
300
+ {:else if block.type === 'table'}
301
+ <ArticleBlockTable
302
+ {block}
303
+ onUpdateHeader={onUpdateTableHeader}
304
+ onUpdateCell={onUpdateTableCell}
305
+ onAddColumn={onAddTableColumn}
306
+ onAddRow={onAddTableRow}
307
+ onRemoveColumn={onRemoveTableColumn}
308
+ onRemoveRow={onRemoveTableRow}
309
+ />
310
+ {:else if block.type === 'code'}
311
+ <ArticleBlockCode {block} onUpdate={onUpdateBlock} />
312
+ {:else}
313
+ <ArticleBlockFallback
314
+ {block}
315
+ onUpdate={onUpdateBlock}
316
+ {onCreateParagraphAfter}
317
+ {onMergeBlockWithPrevious}
318
+ {onRemoveBlock}
319
+ {onConvertToList}
320
+ shouldFocus={focusTarget === block.id}
321
+ focusPosition={focusTarget === block.id ? focusPosition : null}
322
+ onFocused={onFocusHandled}
323
+ />
324
+ {/if}
325
+ {/snippet}
326
+ </ArticleBlockShell>
327
+ {/each}
328
+ </VStack>
@@ -0,0 +1,31 @@
1
+ import type { BlockInsertKind, BlockTextFormat } from './articleEditor.svelte.js';
2
+ import type { BlockDraft } from './types.js';
3
+ type $$ComponentProps = {
4
+ draftTitle: string;
5
+ blocks: BlockDraft[];
6
+ onSelectBlock: (id: string) => void;
7
+ onUpdateBlock: (id: string, patch: Partial<BlockDraft>) => void;
8
+ onChangeBlockTextFormat: (id: string, format: BlockTextFormat) => void;
9
+ onInsertBlockAfter: (id: string, kind: BlockInsertKind) => void;
10
+ onUpdateListItem: (blockID: string, index: number, value: string) => void;
11
+ onInsertListItemAfter: (blockID: string, index: number, text?: string, currentText?: string | null) => void;
12
+ onMergeListItemWithPrevious: (blockID: string, index: number) => void;
13
+ onRemoveListItem: (blockID: string, index: number) => void;
14
+ onCreateParagraphAfter: (id: string, text?: string, currentText?: string | null) => void;
15
+ onMergeBlockWithPrevious: (id: string) => void;
16
+ onRemoveBlock: (id: string) => void;
17
+ onConvertToList: (id: string, kind: 'bullet_list' | 'numbered_list') => void;
18
+ onUpdateTableCell: (blockID: string, rowIndex: number, cellIndex: number, value: string) => void;
19
+ onUpdateTableHeader: (blockID: string, cellIndex: number, value: string) => void;
20
+ onAddTableColumn: (blockID: string) => void;
21
+ onAddTableRow: (blockID: string) => void;
22
+ onRemoveTableColumn: (blockID: string, columnIndex: number) => void;
23
+ onRemoveTableRow: (blockID: string, rowIndex: number) => void;
24
+ onReorderBlock: (draggedID: string, targetID: string, placement: 'before' | 'after') => void;
25
+ focusTarget: string | null;
26
+ focusPosition: number | null;
27
+ onFocusHandled: () => void;
28
+ };
29
+ declare const ArticleEditorBody: import("svelte").Component<$$ComponentProps, {}, "">;
30
+ type ArticleEditorBody = ReturnType<typeof ArticleEditorBody>;
31
+ export default ArticleEditorBody;
@@ -0,0 +1,57 @@
1
+ <script lang="ts">
2
+ import VStack from '../VStack';
3
+ import ArticleImagePreview from './ArticleImagePreview.svelte';
4
+ import TextEditor from '../TextEditor';
5
+ import type { ArticleEditorArticle } from './types.js';
6
+
7
+ let { article, draftTitle, draftImageAltText, onTitleChange, onImageAltTextChange } = $props<{
8
+ article: ArticleEditorArticle;
9
+ draftTitle: string;
10
+ draftImageAltText: string;
11
+ onTitleChange: (value: string) => void;
12
+ onImageAltTextChange: (value: string) => void;
13
+ }>();
14
+ </script>
15
+
16
+ <VStack gap="2rem" paddingX="0" paddingY="2rem">
17
+ <TextEditor
18
+ autosize
19
+ value={draftTitle}
20
+ rows={1}
21
+ spellcheck={true}
22
+ onInput={(event) => onTitleChange((event.currentTarget as HTMLTextAreaElement).value)}
23
+ className="text-4xl leading-tight font-semibold tracking-[-0.03em] text-zinc-950 placeholder:text-zinc-300"
24
+ placeholder="Untitled article"
25
+ />
26
+
27
+ {#if article.imageURL}
28
+ <ArticleImagePreview
29
+ src={article.imageURL}
30
+ alt={draftImageAltText || article.title}
31
+ aspectRatio="4 / 3"
32
+ />
33
+ <TextEditor
34
+ autosize
35
+ value={draftImageAltText}
36
+ rows={1}
37
+ onInput={(event) => onImageAltTextChange((event.currentTarget as HTMLTextAreaElement).value)}
38
+ className="article-editor-hero-description"
39
+ placeholder="Image description"
40
+ />
41
+ {/if}
42
+ </VStack>
43
+
44
+ <style>
45
+ :global(.article-editor-hero-description) {
46
+ border: 0;
47
+ background: transparent;
48
+ padding: 0;
49
+ color: #3f3f46;
50
+ font-size: 0.875rem;
51
+ line-height: 1.5rem;
52
+ }
53
+
54
+ :global(.article-editor-hero-description::placeholder) {
55
+ color: #d4d4d8;
56
+ }
57
+ </style>
@@ -0,0 +1,11 @@
1
+ import type { ArticleEditorArticle } from './types.js';
2
+ type $$ComponentProps = {
3
+ article: ArticleEditorArticle;
4
+ draftTitle: string;
5
+ draftImageAltText: string;
6
+ onTitleChange: (value: string) => void;
7
+ onImageAltTextChange: (value: string) => void;
8
+ };
9
+ declare const ArticleEditorHeader: import("svelte").Component<$$ComponentProps, {}, "">;
10
+ type ArticleEditorHeader = ReturnType<typeof ArticleEditorHeader>;
11
+ export default ArticleEditorHeader;