@commonpub/layer 0.7.2 → 0.7.3

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 (38) hide show
  1. package/components/editors/ArticleEditor.vue +11 -12
  2. package/components/editors/BlogEditor.vue +17 -18
  3. package/components/editors/ExplainerEditor.vue +13 -14
  4. package/components/editors/ProjectEditor.vue +17 -18
  5. package/composables/useMarkdownImport.ts +1 -1
  6. package/package.json +5 -5
  7. package/pages/docs/[siteSlug]/edit.vue +4 -4
  8. package/pages/u/[username]/[type]/[slug]/edit.vue +2 -1
  9. package/components/editors/BlockCanvas.vue +0 -487
  10. package/components/editors/BlockInsertZone.vue +0 -84
  11. package/components/editors/BlockPicker.vue +0 -285
  12. package/components/editors/BlockWrapper.vue +0 -192
  13. package/components/editors/EditorBlocks.vue +0 -248
  14. package/components/editors/EditorSection.vue +0 -81
  15. package/components/editors/EditorShell.vue +0 -196
  16. package/components/editors/EditorTagInput.vue +0 -114
  17. package/components/editors/EditorVisibility.vue +0 -110
  18. package/components/editors/blocks/BuildStepBlock.vue +0 -102
  19. package/components/editors/blocks/CalloutBlock.vue +0 -122
  20. package/components/editors/blocks/CheckpointBlock.vue +0 -27
  21. package/components/editors/blocks/CodeBlock.vue +0 -177
  22. package/components/editors/blocks/DividerBlock.vue +0 -22
  23. package/components/editors/blocks/DownloadsBlock.vue +0 -41
  24. package/components/editors/blocks/EmbedBlock.vue +0 -20
  25. package/components/editors/blocks/GalleryBlock.vue +0 -236
  26. package/components/editors/blocks/HeadingBlock.vue +0 -96
  27. package/components/editors/blocks/ImageBlock.vue +0 -271
  28. package/components/editors/blocks/MarkdownBlock.vue +0 -258
  29. package/components/editors/blocks/MathBlock.vue +0 -37
  30. package/components/editors/blocks/PartsListBlock.vue +0 -358
  31. package/components/editors/blocks/QuizBlock.vue +0 -47
  32. package/components/editors/blocks/QuoteBlock.vue +0 -101
  33. package/components/editors/blocks/SectionHeaderBlock.vue +0 -130
  34. package/components/editors/blocks/SliderBlock.vue +0 -318
  35. package/components/editors/blocks/TextBlock.vue +0 -201
  36. package/components/editors/blocks/ToolListBlock.vue +0 -70
  37. package/components/editors/blocks/VideoBlock.vue +0 -22
  38. package/composables/useBlockEditor.ts +0 -187
@@ -1,201 +0,0 @@
1
- <script setup lang="ts">
2
- /**
3
- * Text/paragraph block — inline TipTap editor for rich text.
4
- * Supports bold, italic, code, strike, link, lists.
5
- * Detects "/" at start of empty block to trigger slash command menu.
6
- */
7
- import { Editor } from '@tiptap/core';
8
- import { Document } from '@tiptap/extension-document';
9
- import { Text } from '@tiptap/extension-text';
10
- import { Paragraph } from '@tiptap/extension-paragraph';
11
- import { Bold } from '@tiptap/extension-bold';
12
- import { Italic } from '@tiptap/extension-italic';
13
- import { Code } from '@tiptap/extension-code';
14
- import { Strike } from '@tiptap/extension-strike';
15
- import { Link } from '@tiptap/extension-link';
16
- import { History } from '@tiptap/extension-history';
17
- import { Placeholder } from '@tiptap/extension-placeholder';
18
- import { BulletList } from '@tiptap/extension-bullet-list';
19
- import { OrderedList } from '@tiptap/extension-ordered-list';
20
- import { ListItem } from '@tiptap/extension-list-item';
21
- import { Extension } from '@tiptap/core';
22
-
23
- const props = defineProps<{
24
- content: Record<string, unknown>;
25
- }>();
26
-
27
- const emit = defineEmits<{
28
- update: [content: Record<string, unknown>];
29
- 'slash-command': [];
30
- 'selection-change': [hasSelection: boolean, rect: DOMRect | null];
31
- 'enter-at-end': [];
32
- 'backspace-empty': [];
33
- }>();
34
-
35
- const editorEl = ref<HTMLElement | null>(null);
36
- let editor: Editor | null = null;
37
-
38
- onMounted(() => {
39
- if (!editorEl.value) return;
40
-
41
- editor = new Editor({
42
- element: editorEl.value,
43
- extensions: [
44
- Document,
45
- Text,
46
- Paragraph,
47
- Bold,
48
- Italic,
49
- Code,
50
- Strike,
51
- Link.configure({ openOnClick: false }),
52
- History,
53
- BulletList,
54
- OrderedList,
55
- ListItem,
56
- Placeholder.configure({ placeholder: 'Type / for commands...' }),
57
- Extension.create({
58
- name: 'blockKeyboard',
59
- addKeyboardShortcuts() {
60
- return {
61
- 'Enter': ({ editor: e }) => {
62
- // If cursor is at the very end of the document and last node is an empty paragraph, create new block
63
- const { state } = e;
64
- const { $from } = state.selection;
65
- const atEnd = $from.pos === state.doc.content.size - 1;
66
- const isEmpty = state.doc.textContent.length === 0;
67
- const lastNode = state.doc.lastChild;
68
- const lastIsEmptyP = lastNode?.type.name === 'paragraph' && lastNode.textContent === '';
69
-
70
- // If we're at end and the text has content, emit to create a new block below
71
- if (atEnd && !isEmpty) {
72
- emit('enter-at-end');
73
- return true;
74
- }
75
- return false;
76
- },
77
- 'Backspace': ({ editor: e }) => {
78
- // If block is completely empty, emit to delete this block
79
- if (e.isEmpty) {
80
- emit('backspace-empty');
81
- return true;
82
- }
83
- return false;
84
- },
85
- };
86
- },
87
- }),
88
- ],
89
- content: (props.content.html as string) || '',
90
- onUpdate: ({ editor: e }) => {
91
- const html = e.getHTML();
92
- const text = e.getText();
93
-
94
- // Detect slash command: "/" typed at start of otherwise-empty block
95
- if (text === '/') {
96
- // Clear the slash character and trigger command menu
97
- e.commands.clearContent(true);
98
- emit('slash-command');
99
- return;
100
- }
101
-
102
- emit('update', { html });
103
- },
104
- onSelectionUpdate: ({ editor: e }) => {
105
- const { from, to } = e.state.selection;
106
- const hasSelection = from !== to;
107
-
108
- if (hasSelection) {
109
- // Get the bounding rect of the selection for floating toolbar positioning
110
- const view = e.view;
111
- const start = view.coordsAtPos(from);
112
- const end = view.coordsAtPos(to);
113
- const rect = new DOMRect(
114
- Math.min(start.left, end.left),
115
- start.top,
116
- Math.abs(end.right - start.left),
117
- end.bottom - start.top,
118
- );
119
- emit('selection-change', true, rect);
120
- } else {
121
- emit('selection-change', false, null);
122
- }
123
- },
124
- });
125
- });
126
-
127
- onUnmounted(() => {
128
- editor?.destroy();
129
- });
130
-
131
- // Sync external content changes (e.g. undo at the block level)
132
- watch(() => props.content.html, (newHtml) => {
133
- if (editor && newHtml !== editor.getHTML()) {
134
- editor.commands.setContent((newHtml as string) || '', false);
135
- }
136
- });
137
-
138
- /** Expose the TipTap editor instance for the floating toolbar */
139
- function getEditor(): Editor | null {
140
- return editor;
141
- }
142
-
143
- defineExpose({ getEditor });
144
- </script>
145
-
146
- <template>
147
- <div ref="editorEl" class="cpub-text-block" />
148
- </template>
149
-
150
- <style scoped>
151
- .cpub-text-block {
152
- font-size: 15px;
153
- line-height: 1.75;
154
- color: var(--text);
155
- min-height: 1.75em;
156
- }
157
-
158
- .cpub-text-block :deep(.tiptap) {
159
- outline: none;
160
- min-height: 1.75em;
161
- }
162
-
163
- .cpub-text-block :deep(.tiptap p) {
164
- margin-bottom: 0.5em;
165
- }
166
-
167
- .cpub-text-block :deep(.tiptap p:last-child) {
168
- margin-bottom: 0;
169
- }
170
-
171
- .cpub-text-block :deep(.tiptap p.is-editor-empty:first-child::before) {
172
- content: attr(data-placeholder);
173
- color: var(--text-faint);
174
- pointer-events: none;
175
- float: left;
176
- height: 0;
177
- }
178
-
179
- .cpub-text-block :deep(.tiptap strong) {
180
- font-weight: 600;
181
- }
182
-
183
- .cpub-text-block :deep(.tiptap code) {
184
- font-family: var(--font-mono);
185
- font-size: 0.9em;
186
- padding: 1px 4px;
187
- background: var(--surface3);
188
- border: var(--border-width-default) solid var(--border2);
189
- }
190
-
191
- .cpub-text-block :deep(.tiptap a) {
192
- color: var(--accent);
193
- text-decoration: underline;
194
- }
195
-
196
- .cpub-text-block :deep(.tiptap ul),
197
- .cpub-text-block :deep(.tiptap ol) {
198
- padding-left: 1.5em;
199
- margin-bottom: 0.5em;
200
- }
201
- </style>
@@ -1,70 +0,0 @@
1
- <script setup lang="ts">
2
- /**
3
- * Tool list block — list of required tools with name and notes.
4
- */
5
- interface Tool { name: string; url?: string; required?: boolean; notes?: string; }
6
-
7
- const props = defineProps<{
8
- content: Record<string, unknown>;
9
- }>();
10
-
11
- const emit = defineEmits<{
12
- update: [content: Record<string, unknown>];
13
- }>();
14
-
15
- const tools = computed(() => (props.content.tools as Tool[]) ?? []);
16
-
17
- function updateTool(index: number, field: string, value: unknown): void {
18
- const updated = [...tools.value];
19
- updated[index] = { ...updated[index]!, [field]: value };
20
- emit('update', { tools: updated });
21
- }
22
-
23
- function addTool(): void {
24
- emit('update', { tools: [...tools.value, { name: '', required: true }] });
25
- }
26
-
27
- function removeTool(index: number): void {
28
- emit('update', { tools: tools.value.filter((_: Tool, i: number) => i !== index) });
29
- }
30
- </script>
31
-
32
- <template>
33
- <div class="cpub-tools-block">
34
- <div class="cpub-tools-header">
35
- <i class="fa-solid fa-wrench cpub-tools-icon"></i>
36
- <span class="cpub-tools-title">Tools Required</span>
37
- <button class="cpub-tools-add" @click="addTool"><i class="fa-solid fa-plus"></i> Add tool</button>
38
- </div>
39
- <div class="cpub-tools-list">
40
- <div v-for="(tool, i) in tools" :key="i" class="cpub-tool-item">
41
- <input class="cpub-tool-name" type="text" :value="tool.name" placeholder="Tool name..." @input="updateTool(i, 'name', ($event.target as HTMLInputElement).value)" />
42
- <input class="cpub-tool-note" type="text" :value="tool.notes ?? ''" placeholder="Notes..." @input="updateTool(i, 'notes', ($event.target as HTMLInputElement).value)" />
43
- <button class="cpub-tool-remove" @click="removeTool(i)"><i class="fa-solid fa-xmark"></i></button>
44
- </div>
45
- <div v-if="tools.length === 0" class="cpub-tools-empty" @click="addTool">
46
- <i class="fa-solid fa-plus"></i> Add your first tool
47
- </div>
48
- </div>
49
- </div>
50
- </template>
51
-
52
- <style scoped>
53
- .cpub-tools-block { border: var(--border-width-default) solid var(--border2); background: var(--surface); }
54
- .cpub-tools-header { display: flex; align-items: center; gap: 8px; padding: 10px 14px; border-bottom: var(--border-width-default) solid var(--border2); background: var(--surface2); }
55
- .cpub-tools-icon { font-size: 12px; color: var(--accent); }
56
- .cpub-tools-title { font-size: 12px; font-weight: 600; flex: 1; }
57
- .cpub-tools-add { font-family: var(--font-mono); font-size: 10px; padding: 3px 8px; background: transparent; border: var(--border-width-default) solid var(--border2); color: var(--text-dim); cursor: pointer; display: flex; align-items: center; gap: 4px; }
58
- .cpub-tools-add:hover { border-color: var(--accent); color: var(--accent); }
59
- .cpub-tools-list { padding: 8px; }
60
- .cpub-tool-item { display: flex; align-items: center; gap: 8px; padding: 6px 8px; border-bottom: var(--border-width-default) solid var(--border2); }
61
- .cpub-tool-item:last-child { border-bottom: none; }
62
- .cpub-tool-name { flex: 1; font-size: 12px; font-weight: 500; background: transparent; border: none; outline: none; color: var(--text); }
63
- .cpub-tool-name::placeholder { color: var(--text-faint); }
64
- .cpub-tool-note { flex: 1; font-size: 11px; background: transparent; border: none; outline: none; color: var(--text-dim); }
65
- .cpub-tool-note::placeholder { color: var(--text-faint); }
66
- .cpub-tool-remove { background: none; border: none; color: var(--text-faint); cursor: pointer; font-size: 10px; }
67
- .cpub-tool-remove:hover { color: var(--red); }
68
- .cpub-tools-empty { padding: 20px; text-align: center; font-size: 12px; color: var(--text-faint); cursor: pointer; }
69
- .cpub-tools-empty:hover { color: var(--accent); background: var(--accent-bg); }
70
- </style>
@@ -1,22 +0,0 @@
1
- <script setup lang="ts">
2
- const props = defineProps<{ content: Record<string, unknown> }>();
3
- const emit = defineEmits<{ update: [content: Record<string, unknown>] }>();
4
- const url = computed(() => (props.content.url as string) ?? '');
5
- function updateField(field: string, value: string): void { emit('update', { ...props.content, [field]: value }); }
6
- </script>
7
- <template>
8
- <div class="cpub-video-block">
9
- <div class="cpub-video-header"><i class="fa-solid fa-film"></i> Video Embed</div>
10
- <input class="cpub-video-url" type="url" :value="url" placeholder="Paste YouTube or Vimeo URL..." @input="updateField('url', ($event.target as HTMLInputElement).value)" />
11
- <div v-if="url" class="cpub-video-preview"><i class="fa-solid fa-play"></i> {{ url }}</div>
12
- </div>
13
- </template>
14
- <style scoped>
15
- .cpub-video-block { border: var(--border-width-default) solid var(--border2); background: var(--surface); }
16
- .cpub-video-header { padding: 8px 12px; font-size: 12px; font-weight: 600; background: var(--surface2); border-bottom: var(--border-width-default) solid var(--border2); display: flex; align-items: center; gap: 8px; }
17
- .cpub-video-header i { color: var(--accent); }
18
- .cpub-video-url { width: 100%; padding: 8px 12px; font-size: 12px; background: transparent; border: none; border-bottom: var(--border-width-default) solid var(--border2); color: var(--text); outline: none; }
19
- .cpub-video-url:focus { border-bottom-color: var(--accent); }
20
- .cpub-video-url::placeholder { color: var(--text-faint); }
21
- .cpub-video-preview { padding: 32px; text-align: center; font-size: 12px; color: var(--text-dim); background: var(--text); color: var(--surface); display: flex; align-items: center; justify-content: center; gap: 8px; }
22
- </style>
@@ -1,187 +0,0 @@
1
- /**
2
- * Block editor composable — manages an array of content blocks with full CRUD operations.
3
- * Blocks are stored as { id, type, content } and serialized to/from BlockTuple format for persistence.
4
- */
5
- import type { BlockTuple } from '@commonpub/editor';
6
-
7
- export interface EditorBlock {
8
- id: string;
9
- type: string;
10
- content: Record<string, unknown>;
11
- }
12
-
13
- /** Default content values when creating a new block of each type */
14
- const BLOCK_DEFAULTS: Record<string, () => Record<string, unknown>> = {
15
- paragraph: () => ({ html: '' }),
16
- heading: () => ({ text: '', level: 2 }),
17
- code_block: () => ({ code: '', language: '', filename: '' }),
18
- image: () => ({ src: '', alt: '', caption: '' }),
19
- blockquote: () => ({ html: '', attribution: '' }),
20
- callout: () => ({ html: '', variant: 'info' }),
21
- gallery: () => ({ images: [] }),
22
- video: () => ({ url: '', platform: 'youtube', caption: '' }),
23
- embed: () => ({ url: '', type: 'generic', html: '' }),
24
- horizontal_rule: () => ({}),
25
- partsList: () => ({ parts: [] }),
26
- buildStep: () => ({ stepNumber: 1, instructions: '', image: '', time: '' }),
27
- toolList: () => ({ tools: [] }),
28
- downloads: () => ({ files: [] }),
29
- quiz: () => ({ question: '', options: [], feedback: '' }),
30
- interactiveSlider: () => ({ label: '', min: 0, max: 100, step: 1, defaultValue: 50, states: [] }),
31
- checkpoint: () => ({ message: '' }),
32
- mathNotation: () => ({ expression: '', display: false }),
33
- bulletList: () => ({ html: '' }),
34
- orderedList: () => ({ html: '' }),
35
- sectionHeader: () => ({ tag: '', title: '', body: '' }),
36
- };
37
-
38
- function generateId(): string {
39
- return `blk-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`;
40
- }
41
-
42
- export function useBlockEditor(initialBlocks?: BlockTuple[]) {
43
- const blocks = ref<EditorBlock[]>([]);
44
- const selectedBlockId = ref<string | null>(null);
45
-
46
- // --- Init from BlockTuples ---
47
- function fromBlockTuples(tuples: BlockTuple[]): void {
48
- blocks.value = tuples.map(([type, content]) => ({
49
- id: generateId(),
50
- type,
51
- content: { ...content },
52
- }));
53
- }
54
-
55
- if (initialBlocks && initialBlocks.length > 0) {
56
- fromBlockTuples(initialBlocks);
57
- }
58
-
59
- // --- Serialize back to BlockTuples ---
60
- function toBlockTuples(): BlockTuple[] {
61
- return blocks.value.map((b) => [b.type, { ...b.content }]);
62
- }
63
-
64
- // --- Mutations ---
65
-
66
- function addBlock(type: string, attrs?: Record<string, unknown>, atIndex?: number): string {
67
- const defaults = BLOCK_DEFAULTS[type]?.() ?? {};
68
- const block: EditorBlock = {
69
- id: generateId(),
70
- type,
71
- content: { ...defaults, ...attrs },
72
- };
73
-
74
- if (atIndex !== undefined && atIndex >= 0 && atIndex <= blocks.value.length) {
75
- blocks.value.splice(atIndex, 0, block);
76
- } else {
77
- blocks.value.push(block);
78
- }
79
-
80
- selectedBlockId.value = block.id;
81
- return block.id;
82
- }
83
-
84
- /** Replace a block with a new block type (used by slash command) */
85
- function replaceBlock(id: string, newType: string, attrs?: Record<string, unknown>): string {
86
- const idx = blocks.value.findIndex((b) => b.id === id);
87
- if (idx === -1) return addBlock(newType, attrs);
88
-
89
- const defaults = BLOCK_DEFAULTS[newType]?.() ?? {};
90
- const newBlock: EditorBlock = {
91
- id: generateId(),
92
- type: newType,
93
- content: { ...defaults, ...attrs },
94
- };
95
-
96
- blocks.value.splice(idx, 1, newBlock);
97
- selectedBlockId.value = newBlock.id;
98
- return newBlock.id;
99
- }
100
-
101
- function removeBlock(id: string): void {
102
- const idx = blocks.value.findIndex((b) => b.id === id);
103
- if (idx === -1) return;
104
- blocks.value.splice(idx, 1);
105
- if (selectedBlockId.value === id) {
106
- selectedBlockId.value = null;
107
- }
108
- }
109
-
110
- function clearBlocks(): void {
111
- blocks.value.splice(0, blocks.value.length);
112
- selectedBlockId.value = null;
113
- }
114
-
115
- function updateBlock(id: string, content: Record<string, unknown>): void {
116
- const block = blocks.value.find((b) => b.id === id);
117
- if (block) {
118
- block.content = { ...block.content, ...content };
119
- }
120
- }
121
-
122
- function moveBlock(fromIndex: number, toIndex: number): void {
123
- if (fromIndex < 0 || fromIndex >= blocks.value.length) return;
124
- if (toIndex < 0 || toIndex >= blocks.value.length) return;
125
- const [moved] = blocks.value.splice(fromIndex, 1);
126
- blocks.value.splice(toIndex, 0, moved!);
127
- }
128
-
129
- function moveBlockUp(id: string): void {
130
- const idx = blocks.value.findIndex((b) => b.id === id);
131
- if (idx > 0) moveBlock(idx, idx - 1);
132
- }
133
-
134
- function moveBlockDown(id: string): void {
135
- const idx = blocks.value.findIndex((b) => b.id === id);
136
- if (idx < blocks.value.length - 1) moveBlock(idx, idx + 1);
137
- }
138
-
139
- function duplicateBlock(id: string): void {
140
- const idx = blocks.value.findIndex((b) => b.id === id);
141
- if (idx === -1) return;
142
- const original = blocks.value[idx]!;
143
- const clone: EditorBlock = {
144
- id: generateId(),
145
- type: original.type,
146
- content: JSON.parse(JSON.stringify(original.content)),
147
- };
148
- blocks.value.splice(idx + 1, 0, clone);
149
- selectedBlockId.value = clone.id;
150
- }
151
-
152
- function selectBlock(id: string | null): void {
153
- selectedBlockId.value = id;
154
- }
155
-
156
- function getBlockIndex(id: string): number {
157
- return blocks.value.findIndex((b) => b.id === id);
158
- }
159
-
160
- const isEmpty = computed(() => blocks.value.length === 0);
161
-
162
- const selectedBlock = computed(() =>
163
- blocks.value.find((b) => b.id === selectedBlockId.value) ?? null,
164
- );
165
-
166
- return {
167
- blocks: readonly(blocks) as Readonly<Ref<EditorBlock[]>>,
168
- selectedBlockId: readonly(selectedBlockId),
169
- selectedBlock,
170
- isEmpty,
171
- addBlock,
172
- removeBlock,
173
- clearBlocks,
174
- updateBlock,
175
- moveBlock,
176
- moveBlockUp,
177
- moveBlockDown,
178
- duplicateBlock,
179
- replaceBlock,
180
- selectBlock,
181
- getBlockIndex,
182
- toBlockTuples,
183
- fromBlockTuples,
184
- };
185
- }
186
-
187
- export type BlockEditor = ReturnType<typeof useBlockEditor>;