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.
- package/CHANGELOG.md +27 -0
- package/DOCS.md +1 -1
- package/ROADMAP.md +22 -8
- package/dist/admin/components/fields/media-field.svelte +54 -2
- package/dist/admin/components/media/file/file-details.svelte +65 -0
- package/dist/admin/remote/media.remote.d.ts +1 -0
- package/dist/admin/remote/media.remote.js +5 -0
- package/dist/cmp/types.d.ts +25 -0
- package/dist/cmp/types.js +1 -0
- package/dist/core/cms.d.ts +2 -0
- package/dist/core/cms.js +2 -0
- package/dist/core/server/cmp/getCountryFromHeaders.d.ts +10 -0
- package/dist/core/server/cmp/getCountryFromHeaders.js +30 -0
- package/dist/core/server/cmp/operations/create.d.ts +17 -0
- package/dist/core/server/cmp/operations/create.js +38 -0
- package/dist/core/server/cmp/operations/get.d.ts +2 -0
- package/dist/core/server/cmp/operations/get.js +8 -0
- package/dist/core/server/cmp/operations/list.d.ts +3 -0
- package/dist/core/server/cmp/operations/list.js +15 -0
- package/dist/core/server/cmp/truncateIpAddress.d.ts +7 -0
- package/dist/core/server/cmp/truncateIpAddress.js +57 -0
- package/dist/core/server/fields/resolveImageFields.d.ts +5 -0
- package/dist/core/server/fields/resolveImageFields.js +9 -1
- package/dist/core/server/generator/generator.js +22 -6
- package/dist/core/server/media/operations/backgroundMaintenance.js +51 -20
- package/dist/core/server/media/operations/findMediaReferences.d.ts +16 -0
- package/dist/core/server/media/operations/findMediaReferences.js +60 -0
- package/dist/db-postgres/index.js +46 -1
- package/dist/db-postgres/schema/consentLog.d.ts +17 -0
- package/dist/db-postgres/schema/consentLog.js +4 -1
- package/dist/paraglide/messages/_index.d.ts +36 -3
- package/dist/paraglide/messages/_index.js +71 -3
- package/dist/paraglide/messages/en.d.ts +5 -0
- package/dist/paraglide/messages/en.js +14 -0
- package/dist/paraglide/messages/pl.d.ts +5 -0
- package/dist/paraglide/messages/pl.js +14 -0
- package/dist/sveltekit/server/handle.js +1 -1
- package/dist/types/adapters/db.d.ts +7 -1
- package/dist/types/cms.d.ts +3 -0
- package/dist/types/consent.d.ts +11 -0
- package/dist/updates/0.15.5/index.d.ts +2 -0
- package/dist/updates/0.15.5/index.js +15 -0
- package/dist/updates/0.16.0/index.d.ts +2 -0
- package/dist/updates/0.16.0/index.js +14 -0
- package/dist/updates/index.js +3 -1
- package/package.json +1 -1
- package/dist/demo/seed.d.ts +0 -1
- package/dist/demo/seed.js +0 -117
- package/dist/inline-edit-proto/ModeToggle.svelte +0 -36
- package/dist/inline-edit-proto/ModeToggle.svelte.d.ts +0 -18
- package/dist/inline-edit-proto/blocks/AddBlockButton.svelte +0 -47
- package/dist/inline-edit-proto/blocks/AddBlockButton.svelte.d.ts +0 -8
- package/dist/inline-edit-proto/blocks/BlockToolbar.svelte +0 -80
- package/dist/inline-edit-proto/blocks/BlockToolbar.svelte.d.ts +0 -13
- package/dist/inline-edit-proto/blocks/BlockWrapper.svelte +0 -83
- package/dist/inline-edit-proto/blocks/BlockWrapper.svelte.d.ts +0 -11
- package/dist/inline-edit-proto/context.svelte.d.ts +0 -65
- package/dist/inline-edit-proto/context.svelte.js +0 -194
- package/dist/inline-edit-proto/hybrid/EditableHybrid.svelte +0 -70
- package/dist/inline-edit-proto/hybrid/EditableHybrid.svelte.d.ts +0 -11
- package/dist/inline-edit-proto/hybrid/FieldRenderer.svelte +0 -94
- package/dist/inline-edit-proto/hybrid/FieldRenderer.svelte.d.ts +0 -11
- package/dist/inline-edit-proto/hybrid/HybridEditor.svelte +0 -107
- package/dist/inline-edit-proto/hybrid/HybridEditor.svelte.d.ts +0 -7
- package/dist/inline-edit-proto/hybrid/SyncPanel.svelte +0 -181
- package/dist/inline-edit-proto/hybrid/SyncPanel.svelte.d.ts +0 -3
- package/dist/inline-edit-proto/inline/EditableInline.svelte +0 -120
- package/dist/inline-edit-proto/inline/EditableInline.svelte.d.ts +0 -11
- package/dist/inline-edit-proto/inline/InlineToolbar.svelte +0 -71
- package/dist/inline-edit-proto/inline/InlineToolbar.svelte.d.ts +0 -6
- package/dist/inline-edit-proto/panel/EditSheet.svelte +0 -130
- package/dist/inline-edit-proto/panel/EditSheet.svelte.d.ts +0 -3
- package/dist/inline-edit-proto/panel/EditablePanel.svelte +0 -44
- package/dist/inline-edit-proto/panel/EditablePanel.svelte.d.ts +0 -9
- package/dist/paraglide/messages/hello_world.d.ts +0 -5
- package/dist/paraglide/messages/hello_world.js +0 -33
- package/dist/paraglide/messages/login_hello.d.ts +0 -16
- package/dist/paraglide/messages/login_hello.js +0 -34
- package/dist/paraglide/messages/login_please_login.d.ts +0 -16
- 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,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,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>
|