@aswin.dev/core 0.7.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.
package/dist/index.js ADDED
@@ -0,0 +1,645 @@
1
+ // src/editor.ts
2
+ import {
3
+ createDefaultTemplateContent,
4
+ normalizeTemplateContentPages
5
+ } from "@aswin.dev/types";
6
+ import {
7
+ computed,
8
+ reactive,
9
+ readonly
10
+ } from "@vue/reactivity";
11
+ function useEditor(options) {
12
+ const initialRaw = options.content ?? createDefaultTemplateContent(
13
+ options.defaultFontFamily,
14
+ options.templateDefaults
15
+ );
16
+ const state = reactive({
17
+ content: normalizeTemplateContentPages(initialRaw),
18
+ selectedBlockId: null,
19
+ viewport: "desktop",
20
+ darkMode: false,
21
+ previewMode: false,
22
+ isDirty: false,
23
+ uiTheme: "auto"
24
+ });
25
+ const content = computed({
26
+ get: () => state.content,
27
+ set: (value) => {
28
+ state.content = normalizeTemplateContentPages(value);
29
+ state.isDirty = true;
30
+ }
31
+ });
32
+ const selectedBlock = computed(() => {
33
+ if (!state.selectedBlockId) return null;
34
+ return findBlockById(state.content.blocks, state.selectedBlockId);
35
+ });
36
+ function findBlockById(blocks, id) {
37
+ for (const block of blocks) {
38
+ if (block.id === id) return block;
39
+ if (block.type === "section") {
40
+ for (const column of block.children) {
41
+ const found = findBlockById(column, id);
42
+ if (found) return found;
43
+ }
44
+ }
45
+ }
46
+ return null;
47
+ }
48
+ function findBlockParent(blocks, id, parent = { blocks }) {
49
+ for (let i = 0; i < blocks.length; i++) {
50
+ const block = blocks[i];
51
+ if (block.id === id) return parent;
52
+ if (block.type === "section") {
53
+ for (let colIdx = 0; colIdx < block.children.length; colIdx++) {
54
+ const result = findBlockParent(block.children[colIdx], id, {
55
+ blocks: block.children[colIdx],
56
+ sectionId: block.id,
57
+ columnIndex: colIdx
58
+ });
59
+ if (result) return result;
60
+ }
61
+ }
62
+ }
63
+ return null;
64
+ }
65
+ function isBlockLocked(blockId) {
66
+ return options.lockedBlocks?.value.has(blockId) ?? false;
67
+ }
68
+ function setContent(newContent, markDirty2 = true) {
69
+ state.content = normalizeTemplateContentPages(newContent);
70
+ if (markDirty2) {
71
+ state.isDirty = true;
72
+ }
73
+ }
74
+ function selectBlock(blockId) {
75
+ if (blockId && isBlockLocked(blockId)) {
76
+ return;
77
+ }
78
+ state.selectedBlockId = blockId;
79
+ }
80
+ function setViewport(viewport) {
81
+ state.viewport = viewport;
82
+ }
83
+ function setDarkMode(darkMode) {
84
+ state.darkMode = darkMode;
85
+ }
86
+ function setUiTheme(theme) {
87
+ state.uiTheme = theme;
88
+ }
89
+ function setPreviewMode(previewMode) {
90
+ state.previewMode = previewMode;
91
+ if (previewMode) {
92
+ state.selectedBlockId = null;
93
+ }
94
+ }
95
+ function updateBlock(blockId, updates) {
96
+ if (isBlockLocked(blockId)) {
97
+ return;
98
+ }
99
+ const block = findBlockById(state.content.blocks, blockId);
100
+ if (block) {
101
+ Object.assign(block, updates);
102
+ state.isDirty = true;
103
+ }
104
+ }
105
+ function updateSettings(updates) {
106
+ state.content.settings = { ...state.content.settings, ...updates };
107
+ state.isDirty = true;
108
+ }
109
+ function addBlock(block, targetSectionId, columnIndex = 0, index) {
110
+ if (targetSectionId) {
111
+ if (isBlockLocked(targetSectionId)) {
112
+ return;
113
+ }
114
+ const section = findBlockById(state.content.blocks, targetSectionId);
115
+ if (section && section.type === "section") {
116
+ section.children[columnIndex] = section.children[columnIndex] || [];
117
+ const targetArray = section.children[columnIndex];
118
+ if (index !== void 0 && index < targetArray.length) {
119
+ targetArray.splice(index, 0, block);
120
+ } else {
121
+ targetArray.push(block);
122
+ }
123
+ }
124
+ } else {
125
+ if (index !== void 0 && index < state.content.blocks.length) {
126
+ state.content.blocks.splice(index, 0, block);
127
+ } else {
128
+ state.content.blocks.push(block);
129
+ }
130
+ }
131
+ state.isDirty = true;
132
+ }
133
+ function removeBlock(blockId) {
134
+ if (isBlockLocked(blockId)) {
135
+ return;
136
+ }
137
+ const parent = findBlockParent(state.content.blocks, blockId);
138
+ if (parent) {
139
+ const index = parent.blocks.findIndex((b) => b.id === blockId);
140
+ if (index !== -1) {
141
+ parent.blocks.splice(index, 1);
142
+ if (state.selectedBlockId === blockId) {
143
+ state.selectedBlockId = null;
144
+ }
145
+ state.isDirty = true;
146
+ }
147
+ }
148
+ }
149
+ function moveBlock(blockId, newIndex, targetSectionId, columnIndex = 0) {
150
+ if (isBlockLocked(blockId)) {
151
+ return;
152
+ }
153
+ if (targetSectionId && isBlockLocked(targetSectionId)) {
154
+ return;
155
+ }
156
+ const parent = findBlockParent(state.content.blocks, blockId);
157
+ if (!parent) return;
158
+ const oldIndex = parent.blocks.findIndex((b) => b.id === blockId);
159
+ if (oldIndex === -1) return;
160
+ let targetArray;
161
+ if (targetSectionId) {
162
+ const section = findBlockById(state.content.blocks, targetSectionId);
163
+ if (!section || section.type !== "section") return;
164
+ section.children[columnIndex] = section.children[columnIndex] || [];
165
+ targetArray = section.children[columnIndex];
166
+ } else {
167
+ targetArray = state.content.blocks;
168
+ }
169
+ const [block] = parent.blocks.splice(oldIndex, 1);
170
+ targetArray.splice(newIndex, 0, block);
171
+ state.isDirty = true;
172
+ }
173
+ function markDirty() {
174
+ state.isDirty = true;
175
+ }
176
+ function ensureCanvasPages() {
177
+ if (state.content.canvasPages?.length) {
178
+ return;
179
+ }
180
+ const id = crypto.randomUUID();
181
+ const blocks = state.content.blocks;
182
+ state.content.canvasPages = [{ id, title: "Step 1", blocks }];
183
+ state.content.activeCanvasPageId = id;
184
+ state.content.blocks = blocks;
185
+ state.isDirty = true;
186
+ }
187
+ function switchCanvasPage(pageId) {
188
+ const pages = state.content.canvasPages;
189
+ if (!pages?.length) {
190
+ return;
191
+ }
192
+ const page = pages.find((p) => p.id === pageId);
193
+ if (!page) {
194
+ return;
195
+ }
196
+ state.selectedBlockId = null;
197
+ state.content.blocks = page.blocks;
198
+ state.content.activeCanvasPageId = pageId;
199
+ state.isDirty = true;
200
+ }
201
+ function addCanvasPage() {
202
+ ensureCanvasPages();
203
+ const pages = state.content.canvasPages;
204
+ const id = crypto.randomUUID();
205
+ const title = `Step ${pages.length + 1}`;
206
+ const newBlocks = [];
207
+ pages.push({ id, title, blocks: newBlocks });
208
+ switchCanvasPage(id);
209
+ }
210
+ function removeCanvasPage(pageId) {
211
+ const pages = state.content.canvasPages;
212
+ if (!pages || pages.length <= 1) {
213
+ return;
214
+ }
215
+ const idx = pages.findIndex((p) => p.id === pageId);
216
+ if (idx === -1) {
217
+ return;
218
+ }
219
+ pages.splice(idx, 1);
220
+ if (state.content.activeCanvasPageId === pageId) {
221
+ const next = pages[Math.min(idx, pages.length - 1)];
222
+ switchCanvasPage(next.id);
223
+ } else {
224
+ state.content.canvasPages = [...pages];
225
+ state.isDirty = true;
226
+ }
227
+ }
228
+ function renameCanvasPage(pageId, title) {
229
+ const pages = state.content.canvasPages;
230
+ if (!pages) {
231
+ return;
232
+ }
233
+ const page = pages.find((p) => p.id === pageId);
234
+ if (!page) {
235
+ return;
236
+ }
237
+ page.title = title.trim() || page.title;
238
+ state.isDirty = true;
239
+ }
240
+ function switchToNextCanvasPage() {
241
+ const pages = state.content.canvasPages;
242
+ if (!pages?.length) {
243
+ return false;
244
+ }
245
+ const activeId = state.content.activeCanvasPageId;
246
+ const idx = pages.findIndex((p) => p.id === activeId);
247
+ if (idx === -1 || idx >= pages.length - 1) {
248
+ return false;
249
+ }
250
+ switchCanvasPage(pages[idx + 1].id);
251
+ return true;
252
+ }
253
+ function switchToPreviousCanvasPage() {
254
+ const pages = state.content.canvasPages;
255
+ if (!pages?.length) {
256
+ return false;
257
+ }
258
+ const activeId = state.content.activeCanvasPageId;
259
+ const idx = pages.findIndex((p) => p.id === activeId);
260
+ if (idx <= 0) {
261
+ return false;
262
+ }
263
+ switchCanvasPage(pages[idx - 1].id);
264
+ return true;
265
+ }
266
+ return {
267
+ state: readonly(state),
268
+ content,
269
+ selectedBlock,
270
+ isBlockLocked,
271
+ setContent,
272
+ selectBlock,
273
+ setViewport,
274
+ setDarkMode,
275
+ setUiTheme,
276
+ setPreviewMode,
277
+ updateBlock,
278
+ updateSettings,
279
+ addBlock,
280
+ removeBlock,
281
+ moveBlock,
282
+ markDirty,
283
+ ensureCanvasPages,
284
+ switchCanvasPage,
285
+ addCanvasPage,
286
+ removeCanvasPage,
287
+ renameCanvasPage,
288
+ switchToNextCanvasPage,
289
+ switchToPreviousCanvasPage
290
+ };
291
+ }
292
+
293
+ // src/history.ts
294
+ import { computed as computed2, ref } from "@vue/reactivity";
295
+ var MAX_STACK_SIZE = 50;
296
+ var DEBOUNCE_MS = 300;
297
+ var NAVIGATE_IDLE_MS = 1500;
298
+ function useHistory(options) {
299
+ const {
300
+ content,
301
+ setContent,
302
+ isRemoteOperation,
303
+ maxSize = MAX_STACK_SIZE
304
+ } = options;
305
+ const undoStack = ref([]);
306
+ const redoStack = ref([]);
307
+ const isNavigating = ref(false);
308
+ let navigatingTimeoutId = null;
309
+ let pendingDebounce = null;
310
+ const canUndo = computed2(() => undoStack.value.length > 0);
311
+ const canRedo = computed2(() => redoStack.value.length > 0);
312
+ function cloneContent() {
313
+ return JSON.parse(JSON.stringify(content.value));
314
+ }
315
+ function pushToUndoStack(snapshot) {
316
+ undoStack.value.push(snapshot);
317
+ if (undoStack.value.length > maxSize) {
318
+ undoStack.value.splice(0, undoStack.value.length - maxSize);
319
+ }
320
+ }
321
+ function flushPendingDebounce() {
322
+ if (pendingDebounce) {
323
+ clearTimeout(pendingDebounce.timeoutId);
324
+ pendingDebounce = null;
325
+ }
326
+ }
327
+ function record() {
328
+ if (isRemoteOperation?.()) {
329
+ return;
330
+ }
331
+ flushPendingDebounce();
332
+ pushToUndoStack(cloneContent());
333
+ redoStack.value = [];
334
+ }
335
+ function recordDebounced(blockId) {
336
+ if (isRemoteOperation?.()) {
337
+ return;
338
+ }
339
+ if (pendingDebounce && pendingDebounce.blockId === blockId) {
340
+ clearTimeout(pendingDebounce.timeoutId);
341
+ pendingDebounce.timeoutId = setTimeout(() => {
342
+ pendingDebounce = null;
343
+ }, DEBOUNCE_MS);
344
+ return;
345
+ }
346
+ flushPendingDebounce();
347
+ pushToUndoStack(cloneContent());
348
+ redoStack.value = [];
349
+ pendingDebounce = {
350
+ blockId,
351
+ timeoutId: setTimeout(() => {
352
+ pendingDebounce = null;
353
+ }, DEBOUNCE_MS)
354
+ };
355
+ }
356
+ function setNavigating() {
357
+ isNavigating.value = true;
358
+ if (navigatingTimeoutId) {
359
+ clearTimeout(navigatingTimeoutId);
360
+ }
361
+ navigatingTimeoutId = setTimeout(() => {
362
+ isNavigating.value = false;
363
+ navigatingTimeoutId = null;
364
+ }, NAVIGATE_IDLE_MS);
365
+ }
366
+ function undo() {
367
+ if (undoStack.value.length === 0) {
368
+ return;
369
+ }
370
+ flushPendingDebounce();
371
+ const snapshot = undoStack.value.pop();
372
+ redoStack.value.push(cloneContent());
373
+ setContent(snapshot, true);
374
+ setNavigating();
375
+ }
376
+ function redo() {
377
+ if (redoStack.value.length === 0) {
378
+ return;
379
+ }
380
+ flushPendingDebounce();
381
+ const snapshot = redoStack.value.pop();
382
+ undoStack.value.push(cloneContent());
383
+ setContent(snapshot, true);
384
+ setNavigating();
385
+ }
386
+ function clear() {
387
+ undoStack.value = [];
388
+ redoStack.value = [];
389
+ flushPendingDebounce();
390
+ }
391
+ function destroy() {
392
+ clear();
393
+ if (navigatingTimeoutId) {
394
+ clearTimeout(navigatingTimeoutId);
395
+ navigatingTimeoutId = null;
396
+ }
397
+ }
398
+ return {
399
+ canUndo,
400
+ canRedo,
401
+ isNavigating,
402
+ undo,
403
+ redo,
404
+ record,
405
+ recordDebounced,
406
+ clear,
407
+ destroy
408
+ };
409
+ }
410
+
411
+ // src/block-actions.ts
412
+ import { createBlock, generateId } from "@aswin.dev/types";
413
+ function useBlockActions(options) {
414
+ const { addBlock, removeBlock, updateBlock, selectBlock } = options;
415
+ function createAndAddBlock(type, targetSectionId, columnIndex) {
416
+ const block = createBlock(type, options.blockDefaults);
417
+ addBlock(block, targetSectionId, columnIndex);
418
+ selectBlock(block.id);
419
+ return block;
420
+ }
421
+ function duplicateBlock(block, targetSectionId, columnIndex) {
422
+ const cloned = JSON.parse(JSON.stringify(block));
423
+ cloned.id = generateId();
424
+ if (cloned.type === "section") {
425
+ cloned.children = cloned.children.map(
426
+ (column) => column.map((child) => {
427
+ const clonedChild = JSON.parse(JSON.stringify(child));
428
+ clonedChild.id = generateId();
429
+ return clonedChild;
430
+ })
431
+ );
432
+ }
433
+ addBlock(cloned, targetSectionId, columnIndex);
434
+ selectBlock(cloned.id);
435
+ return cloned;
436
+ }
437
+ function deleteBlock(blockId) {
438
+ removeBlock(blockId);
439
+ }
440
+ function updateBlockProperty(blockId, key, value) {
441
+ updateBlock(blockId, { [key]: value });
442
+ }
443
+ return {
444
+ createAndAddBlock,
445
+ duplicateBlock,
446
+ deleteBlock,
447
+ updateBlockProperty
448
+ };
449
+ }
450
+
451
+ // src/auto-save.ts
452
+ import { watch } from "@vue/reactivity";
453
+ function useAutoSave(options) {
454
+ const {
455
+ content,
456
+ isDirty,
457
+ onChange,
458
+ debounce = 1e3,
459
+ enabled = true
460
+ } = options;
461
+ let timeoutId = null;
462
+ let paused = false;
463
+ function isEnabled() {
464
+ return typeof enabled === "function" ? enabled() : enabled;
465
+ }
466
+ function pause() {
467
+ paused = true;
468
+ cancel();
469
+ }
470
+ function resume() {
471
+ paused = false;
472
+ }
473
+ function cancel() {
474
+ if (timeoutId) {
475
+ clearTimeout(timeoutId);
476
+ timeoutId = null;
477
+ }
478
+ }
479
+ function flush() {
480
+ cancel();
481
+ if (isDirty()) {
482
+ onChange(JSON.parse(JSON.stringify(content.value)));
483
+ }
484
+ }
485
+ function scheduleOnChange() {
486
+ if (!isEnabled() || paused) return;
487
+ cancel();
488
+ timeoutId = setTimeout(() => {
489
+ timeoutId = null;
490
+ if (isEnabled() && !paused && isDirty()) {
491
+ onChange(JSON.parse(JSON.stringify(content.value)));
492
+ }
493
+ }, debounce);
494
+ }
495
+ const stopWatch = watch(
496
+ content,
497
+ () => {
498
+ if (isEnabled() && !paused && isDirty()) {
499
+ scheduleOnChange();
500
+ }
501
+ },
502
+ { deep: true }
503
+ );
504
+ function destroy() {
505
+ stopWatch();
506
+ cancel();
507
+ }
508
+ return {
509
+ flush,
510
+ cancel,
511
+ pause,
512
+ resume,
513
+ destroy
514
+ };
515
+ }
516
+
517
+ // src/condition-preview.ts
518
+ import { computed as computed3, reactive as reactive2 } from "@vue/reactivity";
519
+ function useConditionPreview(editor) {
520
+ const hiddenBlockIds = reactive2(/* @__PURE__ */ new Set());
521
+ const hasHiddenBlocks = computed3(() => hiddenBlockIds.size > 0);
522
+ function isHidden(blockId) {
523
+ return hiddenBlockIds.has(blockId);
524
+ }
525
+ function toggleBlock(blockId) {
526
+ if (hiddenBlockIds.has(blockId)) {
527
+ hiddenBlockIds.delete(blockId);
528
+ } else {
529
+ hiddenBlockIds.add(blockId);
530
+ if (editor.state.selectedBlockId === blockId) {
531
+ editor.selectBlock(null);
532
+ }
533
+ }
534
+ }
535
+ function reset() {
536
+ hiddenBlockIds.clear();
537
+ }
538
+ return {
539
+ isHidden,
540
+ toggleBlock,
541
+ reset,
542
+ hasHiddenBlocks
543
+ };
544
+ }
545
+
546
+ // src/data-source-fetch.ts
547
+ import { computed as computed4, ref as ref2 } from "@vue/reactivity";
548
+ function useDataSourceFetch(options) {
549
+ const isFetching = ref2(false);
550
+ const fetchError = ref2(false);
551
+ const hasDataSource = computed4(() => !!options.definition.value?.dataSource);
552
+ const needsFetch = computed4(
553
+ () => hasDataSource.value && !options.block.value.dataSourceFetched
554
+ );
555
+ async function fetch() {
556
+ const def = options.definition.value;
557
+ if (!def?.dataSource) {
558
+ return;
559
+ }
560
+ isFetching.value = true;
561
+ fetchError.value = false;
562
+ try {
563
+ const result = await def.dataSource.onFetch({
564
+ fieldValues: { ...options.block.value.fieldValues },
565
+ blockId: options.block.value.id
566
+ });
567
+ if (result == null) {
568
+ return;
569
+ }
570
+ const merged = { ...options.block.value.fieldValues };
571
+ for (const key of Object.keys(merged)) {
572
+ if (key in result) {
573
+ merged[key] = result[key];
574
+ }
575
+ }
576
+ options.onUpdate(merged, true);
577
+ } catch (error) {
578
+ console.warn("[Templatical] Data source fetch error:", error);
579
+ fetchError.value = true;
580
+ } finally {
581
+ isFetching.value = false;
582
+ }
583
+ }
584
+ return {
585
+ isFetching,
586
+ fetchError,
587
+ fetch,
588
+ hasDataSource,
589
+ needsFetch
590
+ };
591
+ }
592
+
593
+ // src/history-interceptor.ts
594
+ function useHistoryInterceptor(editor, history) {
595
+ const originalAddBlock = editor.addBlock;
596
+ const originalRemoveBlock = editor.removeBlock;
597
+ const originalMoveBlock = editor.moveBlock;
598
+ const originalUpdateBlock = editor.updateBlock;
599
+ const originalUpdateSettings = editor.updateSettings;
600
+ editor.addBlock = (block, targetSectionId, columnIndex, index) => {
601
+ if (targetSectionId && editor.isBlockLocked(targetSectionId)) {
602
+ return;
603
+ }
604
+ history.record();
605
+ originalAddBlock(block, targetSectionId, columnIndex, index);
606
+ };
607
+ editor.removeBlock = (blockId) => {
608
+ if (editor.isBlockLocked(blockId)) {
609
+ return;
610
+ }
611
+ history.record();
612
+ originalRemoveBlock(blockId);
613
+ };
614
+ editor.moveBlock = (blockId, newIndex, targetSectionId, columnIndex) => {
615
+ if (editor.isBlockLocked(blockId)) {
616
+ return;
617
+ }
618
+ if (targetSectionId && editor.isBlockLocked(targetSectionId)) {
619
+ return;
620
+ }
621
+ history.record();
622
+ originalMoveBlock(blockId, newIndex, targetSectionId, columnIndex);
623
+ };
624
+ editor.updateBlock = (blockId, updates) => {
625
+ if (editor.isBlockLocked(blockId)) {
626
+ return;
627
+ }
628
+ history.recordDebounced(blockId);
629
+ originalUpdateBlock(blockId, updates);
630
+ };
631
+ editor.updateSettings = (updates) => {
632
+ history.record();
633
+ originalUpdateSettings(updates);
634
+ };
635
+ }
636
+ export {
637
+ useAutoSave,
638
+ useBlockActions,
639
+ useConditionPreview,
640
+ useDataSourceFetch,
641
+ useEditor,
642
+ useHistory,
643
+ useHistoryInterceptor
644
+ };
645
+ //# sourceMappingURL=index.js.map