includio-cms 0.15.4 → 0.16.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.
Files changed (80) hide show
  1. package/CHANGELOG.md +27 -0
  2. package/DOCS.md +1 -1
  3. package/ROADMAP.md +22 -8
  4. package/dist/admin/components/fields/media-field.svelte +54 -2
  5. package/dist/admin/components/media/file/file-details.svelte +65 -0
  6. package/dist/admin/remote/media.remote.d.ts +1 -0
  7. package/dist/admin/remote/media.remote.js +5 -0
  8. package/dist/cmp/types.d.ts +25 -0
  9. package/dist/cmp/types.js +1 -0
  10. package/dist/core/cms.d.ts +2 -0
  11. package/dist/core/cms.js +2 -0
  12. package/dist/core/server/cmp/getCountryFromHeaders.d.ts +10 -0
  13. package/dist/core/server/cmp/getCountryFromHeaders.js +30 -0
  14. package/dist/core/server/cmp/operations/create.d.ts +17 -0
  15. package/dist/core/server/cmp/operations/create.js +38 -0
  16. package/dist/core/server/cmp/operations/get.d.ts +2 -0
  17. package/dist/core/server/cmp/operations/get.js +8 -0
  18. package/dist/core/server/cmp/operations/list.d.ts +3 -0
  19. package/dist/core/server/cmp/operations/list.js +15 -0
  20. package/dist/core/server/cmp/truncateIpAddress.d.ts +7 -0
  21. package/dist/core/server/cmp/truncateIpAddress.js +57 -0
  22. package/dist/core/server/fields/resolveImageFields.d.ts +5 -0
  23. package/dist/core/server/fields/resolveImageFields.js +9 -1
  24. package/dist/core/server/generator/generator.js +22 -6
  25. package/dist/core/server/media/operations/backgroundMaintenance.js +51 -20
  26. package/dist/core/server/media/operations/findMediaReferences.d.ts +16 -0
  27. package/dist/core/server/media/operations/findMediaReferences.js +60 -0
  28. package/dist/db-postgres/index.js +46 -1
  29. package/dist/db-postgres/schema/consentLog.d.ts +17 -0
  30. package/dist/db-postgres/schema/consentLog.js +4 -1
  31. package/dist/paraglide/messages/_index.d.ts +36 -3
  32. package/dist/paraglide/messages/_index.js +71 -3
  33. package/dist/paraglide/messages/en.d.ts +5 -0
  34. package/dist/paraglide/messages/en.js +14 -0
  35. package/dist/paraglide/messages/pl.d.ts +5 -0
  36. package/dist/paraglide/messages/pl.js +14 -0
  37. package/dist/sveltekit/server/handle.js +1 -1
  38. package/dist/types/adapters/db.d.ts +7 -1
  39. package/dist/types/cms.d.ts +3 -0
  40. package/dist/types/consent.d.ts +11 -0
  41. package/dist/updates/0.15.5/index.d.ts +2 -0
  42. package/dist/updates/0.15.5/index.js +15 -0
  43. package/dist/updates/0.16.0/index.d.ts +2 -0
  44. package/dist/updates/0.16.0/index.js +14 -0
  45. package/dist/updates/index.js +3 -1
  46. package/package.json +1 -1
  47. package/dist/demo/seed.d.ts +0 -1
  48. package/dist/demo/seed.js +0 -117
  49. package/dist/inline-edit-proto/ModeToggle.svelte +0 -36
  50. package/dist/inline-edit-proto/ModeToggle.svelte.d.ts +0 -18
  51. package/dist/inline-edit-proto/blocks/AddBlockButton.svelte +0 -47
  52. package/dist/inline-edit-proto/blocks/AddBlockButton.svelte.d.ts +0 -8
  53. package/dist/inline-edit-proto/blocks/BlockToolbar.svelte +0 -80
  54. package/dist/inline-edit-proto/blocks/BlockToolbar.svelte.d.ts +0 -13
  55. package/dist/inline-edit-proto/blocks/BlockWrapper.svelte +0 -83
  56. package/dist/inline-edit-proto/blocks/BlockWrapper.svelte.d.ts +0 -11
  57. package/dist/inline-edit-proto/context.svelte.d.ts +0 -65
  58. package/dist/inline-edit-proto/context.svelte.js +0 -194
  59. package/dist/inline-edit-proto/hybrid/EditableHybrid.svelte +0 -70
  60. package/dist/inline-edit-proto/hybrid/EditableHybrid.svelte.d.ts +0 -11
  61. package/dist/inline-edit-proto/hybrid/FieldRenderer.svelte +0 -94
  62. package/dist/inline-edit-proto/hybrid/FieldRenderer.svelte.d.ts +0 -11
  63. package/dist/inline-edit-proto/hybrid/HybridEditor.svelte +0 -107
  64. package/dist/inline-edit-proto/hybrid/HybridEditor.svelte.d.ts +0 -7
  65. package/dist/inline-edit-proto/hybrid/SyncPanel.svelte +0 -181
  66. package/dist/inline-edit-proto/hybrid/SyncPanel.svelte.d.ts +0 -3
  67. package/dist/inline-edit-proto/inline/EditableInline.svelte +0 -120
  68. package/dist/inline-edit-proto/inline/EditableInline.svelte.d.ts +0 -11
  69. package/dist/inline-edit-proto/inline/InlineToolbar.svelte +0 -71
  70. package/dist/inline-edit-proto/inline/InlineToolbar.svelte.d.ts +0 -6
  71. package/dist/inline-edit-proto/panel/EditSheet.svelte +0 -130
  72. package/dist/inline-edit-proto/panel/EditSheet.svelte.d.ts +0 -3
  73. package/dist/inline-edit-proto/panel/EditablePanel.svelte +0 -44
  74. package/dist/inline-edit-proto/panel/EditablePanel.svelte.d.ts +0 -9
  75. package/dist/paraglide/messages/hello_world.d.ts +0 -5
  76. package/dist/paraglide/messages/hello_world.js +0 -33
  77. package/dist/paraglide/messages/login_hello.d.ts +0 -16
  78. package/dist/paraglide/messages/login_hello.js +0 -34
  79. package/dist/paraglide/messages/login_please_login.d.ts +0 -16
  80. package/dist/paraglide/messages/login_please_login.js +0 -34
@@ -1,70 +0,0 @@
1
- <script lang="ts">
2
- import type { Snippet } from 'svelte';
3
- import { getEditContext } from '../context.svelte.js';
4
-
5
- type Props = {
6
- path: string;
7
- tag?: 'h1' | 'h2' | 'h3' | 'p' | 'span' | 'div' | 'figcaption';
8
- label?: string;
9
- class?: string;
10
- children?: Snippet;
11
- };
12
-
13
- let { path, tag = 'div', label, class: className = '', children }: Props = $props();
14
-
15
- const ctx = getEditContext();
16
-
17
- let element = $state<HTMLElement | null>(null);
18
-
19
- let isFocused = $derived(ctx.focusedPath === path);
20
- let value = $derived(ctx.getData(path) as string);
21
-
22
- function handleClick() {
23
- if (ctx.mode !== 'hybrid') return;
24
- ctx.focusedPath = path;
25
- }
26
-
27
- function handleKeydown(e: KeyboardEvent) {
28
- if (ctx.mode !== 'hybrid') return;
29
-
30
- if (e.key === 'Enter' || e.key === ' ') {
31
- e.preventDefault();
32
- ctx.focusedPath = path;
33
- }
34
- }
35
-
36
- // Scroll into view when focused from panel
37
- $effect(() => {
38
- if (isFocused && element) {
39
- element.scrollIntoView({ behavior: 'smooth', block: 'center' });
40
- }
41
- });
42
- </script>
43
-
44
- {#if ctx.mode === 'hybrid'}
45
- <svelte:element
46
- this={tag}
47
- bind:this={element}
48
- class="editable-hybrid cursor-pointer rounded transition-all {className} {isFocused
49
- ? 'ring-2 ring-[#1e3a5f] ring-offset-2 bg-blue-50/50'
50
- : 'hover:ring-2 hover:ring-[#1e3a5f]/30 hover:ring-offset-2'}"
51
- onclick={handleClick}
52
- onkeydown={handleKeydown}
53
- role="button"
54
- tabindex="0"
55
- aria-label={label || `Edit ${path}`}
56
- data-path={path}
57
- >
58
- {#if children}
59
- {@render children()}
60
- {:else}
61
- {value}
62
- {/if}
63
- </svelte:element>
64
- {:else if children}
65
- {@render children()}
66
- {:else}
67
- <svelte:element this={tag} class={className}>
68
- {value}
69
- </svelte:element>
70
- {/if}
@@ -1,11 +0,0 @@
1
- import type { Snippet } from 'svelte';
2
- type Props = {
3
- path: string;
4
- tag?: 'h1' | 'h2' | 'h3' | 'p' | 'span' | 'div' | 'figcaption';
5
- label?: string;
6
- class?: string;
7
- children?: Snippet;
8
- };
9
- declare const EditableHybrid: import("svelte").Component<Props, {}, "">;
10
- type EditableHybrid = ReturnType<typeof EditableHybrid>;
11
- export default EditableHybrid;
@@ -1,94 +0,0 @@
1
- <script lang="ts">
2
- import Input from '../../components/ui/input/input.svelte';
3
- import Textarea from '../../components/ui/textarea/textarea.svelte';
4
- import Label from '../../components/ui/label/label.svelte';
5
- import { getEditContext } from '../context.svelte.js';
6
- import AlertTriangle from '@tabler/icons-svelte/icons/alert-triangle';
7
-
8
- type FieldType = 'text' | 'textarea' | 'url' | 'alt';
9
-
10
- type Props = {
11
- path: string;
12
- label: string;
13
- type?: FieldType;
14
- required?: boolean;
15
- isFocused?: boolean;
16
- };
17
-
18
- let { path, label, type = 'text', required = false, isFocused = false }: Props = $props();
19
-
20
- const ctx = getEditContext();
21
-
22
- let inputRef = $state<HTMLInputElement | HTMLTextAreaElement | null>(null);
23
- let value = $derived(ctx.getData(path) as string);
24
-
25
- // A11y validation for alt text
26
- let altWarning = $derived.by(() => {
27
- if (type !== 'alt') return null;
28
- if (!value || value.trim() === '') return 'Alt text is required for accessibility';
29
- const genericAlts = ['image', 'picture', 'photo', 'img', 'graphic'];
30
- if (genericAlts.includes(value.toLowerCase().trim())) {
31
- return 'Alt text should be descriptive, not generic';
32
- }
33
- return null;
34
- });
35
-
36
- function handleInput(e: Event) {
37
- const target = e.currentTarget as HTMLInputElement | HTMLTextAreaElement;
38
- ctx.updateData(path, target.value);
39
- }
40
-
41
- // Auto-focus when this field becomes focused
42
- $effect(() => {
43
- if (isFocused && inputRef) {
44
- inputRef.focus();
45
- inputRef.scrollIntoView({ behavior: 'smooth', block: 'center' });
46
- }
47
- });
48
- </script>
49
-
50
- <div
51
- class="field-renderer flex flex-col gap-1.5 rounded-lg p-3 transition-colors {isFocused
52
- ? 'bg-blue-50 ring-2 ring-[#1e3a5f]/20'
53
- : ''}"
54
- data-path={path}
55
- >
56
- <Label for={path} class="text-sm font-medium text-slate-700">
57
- {label}
58
- {#if required}
59
- <span class="text-red-500">*</span>
60
- {/if}
61
- </Label>
62
-
63
- {#if type === 'textarea'}
64
- <Textarea
65
- id={path}
66
- bind:ref={inputRef}
67
- {value}
68
- oninput={handleInput}
69
- rows={4}
70
- class="resize-none"
71
- aria-describedby={altWarning ? `${path}-warning` : undefined}
72
- />
73
- {:else}
74
- <Input
75
- id={path}
76
- bind:ref={inputRef}
77
- type={type === 'url' ? 'url' : 'text'}
78
- {value}
79
- oninput={handleInput}
80
- aria-describedby={altWarning ? `${path}-warning` : undefined}
81
- />
82
- {/if}
83
-
84
- {#if altWarning}
85
- <div
86
- id="{path}-warning"
87
- class="flex items-center gap-1.5 text-xs text-amber-600"
88
- role="alert"
89
- >
90
- <AlertTriangle class="h-3.5 w-3.5" />
91
- {altWarning}
92
- </div>
93
- {/if}
94
- </div>
@@ -1,11 +0,0 @@
1
- type FieldType = 'text' | 'textarea' | 'url' | 'alt';
2
- type Props = {
3
- path: string;
4
- label: string;
5
- type?: FieldType;
6
- required?: boolean;
7
- isFocused?: boolean;
8
- };
9
- declare const FieldRenderer: import("svelte").Component<Props, {}, "">;
10
- type FieldRenderer = ReturnType<typeof FieldRenderer>;
11
- export default FieldRenderer;
@@ -1,107 +0,0 @@
1
- <script lang="ts">
2
- import type { Snippet } from 'svelte';
3
- import { getEditContext } from '../context.svelte.js';
4
- import SyncPanel from './SyncPanel.svelte';
5
-
6
- type Props = {
7
- children: Snippet;
8
- };
9
-
10
- let { children }: Props = $props();
11
-
12
- const ctx = getEditContext();
13
-
14
- let panelWidth = $state(380);
15
- let isResizing = $state(false);
16
- let containerRef = $state<HTMLElement | null>(null);
17
-
18
- function startResize(e: MouseEvent) {
19
- isResizing = true;
20
- e.preventDefault();
21
- }
22
-
23
- function handleResize(e: MouseEvent) {
24
- if (!isResizing || !containerRef) return;
25
- const rect = containerRef.getBoundingClientRect();
26
- const newWidth = rect.right - e.clientX;
27
- panelWidth = Math.max(280, Math.min(600, newWidth));
28
- }
29
-
30
- function stopResize() {
31
- isResizing = false;
32
- }
33
-
34
- // Keyboard shortcuts
35
- function handleKeydown(e: KeyboardEvent) {
36
- if (ctx.mode !== 'hybrid') return;
37
-
38
- // Ctrl+Z = Undo
39
- if ((e.ctrlKey || e.metaKey) && e.key === 'z' && !e.shiftKey) {
40
- e.preventDefault();
41
- ctx.undo();
42
- }
43
- // Ctrl+Shift+Z or Ctrl+Y = Redo
44
- if ((e.ctrlKey || e.metaKey) && (e.key === 'y' || (e.key === 'z' && e.shiftKey))) {
45
- e.preventDefault();
46
- ctx.redo();
47
- }
48
- // Escape = Clear focus
49
- if (e.key === 'Escape') {
50
- ctx.focusedPath = null;
51
- }
52
- }
53
- </script>
54
-
55
- <svelte:window
56
- onmousemove={handleResize}
57
- onmouseup={stopResize}
58
- onkeydown={handleKeydown}
59
- />
60
-
61
- {#if ctx.mode === 'hybrid'}
62
- <div
63
- bind:this={containerRef}
64
- class="hybrid-editor flex h-full min-h-screen"
65
- class:cursor-col-resize={isResizing}
66
- >
67
- <!-- Preview area -->
68
- <div
69
- class="flex-1 overflow-auto"
70
- style="margin-right: {panelWidth}px"
71
- >
72
- {@render children()}
73
- </div>
74
-
75
- <!-- Resizer -->
76
- <!-- svelte-ignore a11y_no_noninteractive_tabindex -->
77
- <!-- svelte-ignore a11y_no_noninteractive_element_interactions -->
78
- <div
79
- class="fixed top-0 z-40 h-full w-1 cursor-col-resize bg-slate-200 transition-colors hover:bg-[#1e3a5f]/50"
80
- style="right: {panelWidth}px"
81
- onmousedown={startResize}
82
- role="separator"
83
- aria-orientation="vertical"
84
- aria-valuenow={panelWidth}
85
- aria-valuemin={280}
86
- aria-valuemax={600}
87
- tabindex="0"
88
- ></div>
89
-
90
- <!-- Sync Panel -->
91
- <div
92
- class="fixed right-0 top-0 z-30 h-full overflow-hidden border-l border-slate-200 bg-white shadow-lg"
93
- style="width: {panelWidth}px"
94
- >
95
- <SyncPanel />
96
- </div>
97
- </div>
98
- {:else}
99
- {@render children()}
100
- {/if}
101
-
102
- <style>
103
- .hybrid-editor :global(.editable-hybrid:focus-within) {
104
- outline: 2px solid #1e3a5f;
105
- outline-offset: 2px;
106
- }
107
- </style>
@@ -1,7 +0,0 @@
1
- import type { Snippet } from 'svelte';
2
- type Props = {
3
- children: Snippet;
4
- };
5
- declare const HybridEditor: import("svelte").Component<Props, {}, "">;
6
- type HybridEditor = ReturnType<typeof HybridEditor>;
7
- export default HybridEditor;
@@ -1,181 +0,0 @@
1
- <script lang="ts">
2
- import { getEditContext } from '../context.svelte.js';
3
- import FieldRenderer from './FieldRenderer.svelte';
4
- import ArrowBackUp from '@tabler/icons-svelte/icons/arrow-back-up';
5
- import ArrowForwardUp from '@tabler/icons-svelte/icons/arrow-forward-up';
6
- import DeviceFloppy from '@tabler/icons-svelte/icons/device-floppy';
7
- import Check from '@tabler/icons-svelte/icons/check';
8
- import Loader2 from '@tabler/icons-svelte/icons/loader-2';
9
-
10
- const ctx = getEditContext();
11
-
12
- // Field configs for each section
13
- type FieldConfig = {
14
- key: string;
15
- label: string;
16
- type: 'text' | 'textarea' | 'url' | 'alt';
17
- required?: boolean;
18
- };
19
-
20
- type SectionConfig = {
21
- label: string;
22
- fields: FieldConfig[];
23
- };
24
-
25
- const sectionConfigs: Record<string, SectionConfig> = {
26
- hero: {
27
- label: 'Hero Section',
28
- fields: [
29
- { key: 'title', label: 'Title', type: 'text', required: true },
30
- { key: 'subtitle', label: 'Subtitle', type: 'textarea' },
31
- { key: 'cta', label: 'CTA Button', type: 'text' }
32
- ]
33
- },
34
- content: {
35
- label: 'Content Section',
36
- fields: [
37
- { key: 'title', label: 'Title', type: 'text', required: true },
38
- { key: 'body', label: 'Body', type: 'textarea' }
39
- ]
40
- },
41
- image: {
42
- label: 'Image',
43
- fields: [
44
- { key: 'src', label: 'Image URL', type: 'url', required: true },
45
- { key: 'alt', label: 'Alt Text', type: 'alt', required: true },
46
- { key: 'caption', label: 'Caption', type: 'text' }
47
- ]
48
- }
49
- };
50
-
51
- function getFeatureConfig(index: number): SectionConfig {
52
- return {
53
- label: `Feature ${index + 1}`,
54
- fields: [
55
- { key: 'icon', label: 'Icon', type: 'text' },
56
- { key: 'title', label: 'Title', type: 'text', required: true },
57
- { key: 'description', label: 'Description', type: 'textarea' }
58
- ]
59
- };
60
- }
61
-
62
- // Derive current section and focused field
63
- let currentSection = $derived.by(() => {
64
- const path = ctx.focusedPath;
65
- if (!path) return null;
66
-
67
- // Get root section (hero, content, image, or features.N)
68
- const parts = path.split('.');
69
- const root = parts[0];
70
-
71
- // Check for feature item
72
- if (root === 'features' && parts[1]) {
73
- const featureMatch = path.match(/^features\.(\d+)/);
74
- if (featureMatch) {
75
- return {
76
- path: `features.${featureMatch[1]}`,
77
- config: getFeatureConfig(parseInt(featureMatch[1])),
78
- focusedField: parts.length > 2 ? parts.slice(2).join('.') : null
79
- };
80
- }
81
- }
82
-
83
- const config = sectionConfigs[root];
84
- if (!config) return null;
85
-
86
- return {
87
- path: root,
88
- config,
89
- focusedField: parts.length > 1 ? parts.slice(1).join('.') : null
90
- };
91
- });
92
-
93
- function handleFieldClick(fieldPath: string) {
94
- ctx.focusedPath = fieldPath;
95
- }
96
- </script>
97
-
98
- <div class="flex h-full flex-col">
99
- <!-- Header -->
100
- <div class="flex items-center justify-between border-b border-slate-200 px-4 py-3">
101
- <h2 class="text-sm font-semibold text-slate-900">
102
- {currentSection?.config.label || 'Properties'}
103
- </h2>
104
-
105
- <div class="flex items-center gap-1">
106
- <!-- Undo/Redo -->
107
- <button
108
- onclick={() => ctx.undo()}
109
- disabled={!ctx.canUndo}
110
- class="rounded p-1.5 text-slate-500 transition-colors hover:bg-slate-100 hover:text-slate-700 disabled:opacity-30 disabled:cursor-not-allowed"
111
- title="Undo (Ctrl+Z)"
112
- aria-label="Undo"
113
- >
114
- <ArrowBackUp class="h-4 w-4" />
115
- </button>
116
- <button
117
- onclick={() => ctx.redo()}
118
- disabled={!ctx.canRedo}
119
- class="rounded p-1.5 text-slate-500 transition-colors hover:bg-slate-100 hover:text-slate-700 disabled:opacity-30 disabled:cursor-not-allowed"
120
- title="Redo (Ctrl+Y)"
121
- aria-label="Redo"
122
- >
123
- <ArrowForwardUp class="h-4 w-4" />
124
- </button>
125
-
126
- <!-- Save status -->
127
- <div class="ml-2 flex items-center gap-1.5 text-xs">
128
- {#if ctx.saveStatus === 'saving'}
129
- <Loader2 class="h-3.5 w-3.5 animate-spin text-slate-400" />
130
- <span class="text-slate-500">Saving...</span>
131
- {:else if ctx.saveStatus === 'saved'}
132
- <Check class="h-3.5 w-3.5 text-green-500" />
133
- <span class="text-green-600">Saved</span>
134
- {:else if ctx.isDirty}
135
- <DeviceFloppy class="h-3.5 w-3.5 text-amber-500" />
136
- <span class="text-amber-600">Unsaved</span>
137
- {/if}
138
- </div>
139
- </div>
140
- </div>
141
-
142
- <!-- Content -->
143
- <div class="flex-1 overflow-auto p-4">
144
- {#if currentSection}
145
- <div class="flex flex-col gap-2">
146
- {#each currentSection.config.fields as field}
147
- {@const fieldPath = `${currentSection.path}.${field.key}`}
148
- <button
149
- class="text-left w-full"
150
- onclick={() => handleFieldClick(fieldPath)}
151
- >
152
- <FieldRenderer
153
- path={fieldPath}
154
- label={field.label}
155
- type={field.type}
156
- required={field.required}
157
- isFocused={ctx.focusedPath === fieldPath}
158
- />
159
- </button>
160
- {/each}
161
- </div>
162
- {:else}
163
- <div class="flex h-full items-center justify-center text-center">
164
- <div class="text-slate-400">
165
- <p class="text-sm">Click an element in the preview</p>
166
- <p class="mt-1 text-xs">or use Tab to navigate</p>
167
- </div>
168
- </div>
169
- {/if}
170
- </div>
171
-
172
- <!-- Keyboard shortcuts hint -->
173
- <div class="border-t border-slate-200 px-4 py-2">
174
- <div class="flex flex-wrap gap-x-4 gap-y-1 text-[10px] text-slate-400">
175
- <span><kbd class="rounded bg-slate-100 px-1">Tab</kbd> Navigate</span>
176
- <span><kbd class="rounded bg-slate-100 px-1">Enter</kbd> Select</span>
177
- <span><kbd class="rounded bg-slate-100 px-1">Esc</kbd> Deselect</span>
178
- <span><kbd class="rounded bg-slate-100 px-1">Ctrl+Z</kbd> Undo</span>
179
- </div>
180
- </div>
181
- </div>
@@ -1,3 +0,0 @@
1
- declare const SyncPanel: import("svelte").Component<Record<string, never>, {}, "">;
2
- type SyncPanel = ReturnType<typeof SyncPanel>;
3
- export default SyncPanel;
@@ -1,120 +0,0 @@
1
- <script lang="ts">
2
- import type { Snippet } from 'svelte';
3
- import { getEditContext } from '../context.svelte.js';
4
- import InlineToolbar from './InlineToolbar.svelte';
5
-
6
- type Props = {
7
- path: string;
8
- tag?: 'h1' | 'h2' | 'h3' | 'p' | 'span' | 'div';
9
- richtext?: boolean;
10
- class?: string;
11
- children?: Snippet;
12
- };
13
-
14
- let { path, tag = 'div', richtext = false, class: className = '', children }: Props = $props();
15
-
16
- const ctx = getEditContext();
17
-
18
- let element = $state<HTMLElement | null>(null);
19
- let isEditing = $state(false);
20
- let isHovered = $state(false);
21
- let showToolbar = $state(false);
22
-
23
- let value = $derived(ctx.getData(path) as string);
24
-
25
- function startEdit() {
26
- if (ctx.mode !== 'inline') return;
27
- isEditing = true;
28
- showToolbar = richtext;
29
- // Focus at end
30
- setTimeout(() => {
31
- if (element) {
32
- element.focus();
33
- // Move cursor to end
34
- const range = document.createRange();
35
- range.selectNodeContents(element);
36
- range.collapse(false);
37
- const sel = window.getSelection();
38
- sel?.removeAllRanges();
39
- sel?.addRange(range);
40
- }
41
- }, 0);
42
- }
43
-
44
- function stopEdit() {
45
- if (!isEditing) return;
46
- isEditing = false;
47
- showToolbar = false;
48
- if (element) {
49
- const newValue = richtext ? element.innerHTML : element.textContent || '';
50
- if (newValue !== value) {
51
- ctx.updateData(path, newValue);
52
- }
53
- }
54
- }
55
-
56
- function handleKeydown(e: KeyboardEvent) {
57
- if (e.key === 'Enter' && !richtext) {
58
- e.preventDefault();
59
- stopEdit();
60
- }
61
- if (e.key === 'Escape') {
62
- // Revert
63
- if (element) {
64
- if (richtext) {
65
- element.innerHTML = value;
66
- } else {
67
- element.textContent = value;
68
- }
69
- }
70
- isEditing = false;
71
- showToolbar = false;
72
- }
73
- }
74
- </script>
75
-
76
- {#if ctx.mode === 'inline'}
77
- <!-- svelte-ignore a11y_no_static_element_interactions -->
78
- <div
79
- class="editable-inline relative"
80
- onmouseenter={() => (isHovered = true)}
81
- onmouseleave={() => (isHovered = false)}
82
- >
83
- {#if showToolbar}
84
- <InlineToolbar {element} />
85
- {/if}
86
-
87
- <!-- svelte-ignore a11y_click_events_have_key_events -->
88
- <svelte:element
89
- this={tag}
90
- bind:this={element}
91
- class="{className} {isHovered || isEditing
92
- ? 'outline outline-2 outline-[#1e3a5f]/30 outline-offset-2'
93
- : ''} {isEditing
94
- ? 'outline-[#1e3a5f] bg-white/50'
95
- : ''} rounded transition-all cursor-text"
96
- contenteditable={isEditing}
97
- onclick={startEdit}
98
- onblur={stopEdit}
99
- onkeydown={handleKeydown}
100
- role="textbox"
101
- tabindex="0"
102
- >
103
- {#if richtext}
104
- {@html value}
105
- {:else}
106
- {value}
107
- {/if}
108
- </svelte:element>
109
- </div>
110
- {:else if children}
111
- {@render children()}
112
- {:else}
113
- <svelte:element this={tag} class={className}>
114
- {#if richtext}
115
- {@html value}
116
- {:else}
117
- {value}
118
- {/if}
119
- </svelte:element>
120
- {/if}
@@ -1,11 +0,0 @@
1
- import type { Snippet } from 'svelte';
2
- type Props = {
3
- path: string;
4
- tag?: 'h1' | 'h2' | 'h3' | 'p' | 'span' | 'div';
5
- richtext?: boolean;
6
- class?: string;
7
- children?: Snippet;
8
- };
9
- declare const EditableInline: import("svelte").Component<Props, {}, "">;
10
- type EditableInline = ReturnType<typeof EditableInline>;
11
- export default EditableInline;
@@ -1,71 +0,0 @@
1
- <script lang="ts">
2
- import Bold from '@tabler/icons-svelte/icons/bold';
3
- import Italic from '@tabler/icons-svelte/icons/italic';
4
- import Strikethrough from '@tabler/icons-svelte/icons/strikethrough';
5
- import List from '@tabler/icons-svelte/icons/list';
6
-
7
- type Props = {
8
- element: HTMLElement | null;
9
- };
10
-
11
- let { element }: Props = $props();
12
-
13
- function execCommand(cmd: string, value?: string) {
14
- document.execCommand(cmd, false, value);
15
- element?.focus();
16
- }
17
-
18
- function toggleBold() {
19
- execCommand('bold');
20
- }
21
-
22
- function toggleItalic() {
23
- execCommand('italic');
24
- }
25
-
26
- function toggleStrike() {
27
- execCommand('strikeThrough');
28
- }
29
-
30
- function toggleList() {
31
- execCommand('insertUnorderedList');
32
- }
33
- </script>
34
-
35
- <div
36
- class="absolute -top-10 left-0 z-20 flex items-center gap-1 rounded-lg bg-slate-900 p-1 shadow-xl"
37
- >
38
- <button
39
- type="button"
40
- onclick={toggleBold}
41
- class="rounded p-1.5 text-slate-300 transition-colors hover:bg-slate-700 hover:text-white"
42
- title="Bold"
43
- >
44
- <Bold class="h-4 w-4" />
45
- </button>
46
- <button
47
- type="button"
48
- onclick={toggleItalic}
49
- class="rounded p-1.5 text-slate-300 transition-colors hover:bg-slate-700 hover:text-white"
50
- title="Italic"
51
- >
52
- <Italic class="h-4 w-4" />
53
- </button>
54
- <button
55
- type="button"
56
- onclick={toggleStrike}
57
- class="rounded p-1.5 text-slate-300 transition-colors hover:bg-slate-700 hover:text-white"
58
- title="Strikethrough"
59
- >
60
- <Strikethrough class="h-4 w-4" />
61
- </button>
62
- <div class="mx-1 h-4 w-px bg-slate-600"></div>
63
- <button
64
- type="button"
65
- onclick={toggleList}
66
- class="rounded p-1.5 text-slate-300 transition-colors hover:bg-slate-700 hover:text-white"
67
- title="Bullet List"
68
- >
69
- <List class="h-4 w-4" />
70
- </button>
71
- </div>