includio-cms 0.0.66 → 0.0.68
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/admin/client/entry/entry-form.svelte +1 -1
- package/dist/admin/client/entry/entry-form.svelte.d.ts +1 -1
- package/dist/admin/client/entry/entry-header.svelte +6 -4
- package/dist/admin/client/entry/entry.svelte +43 -63
- package/dist/admin/client/entry/hybrid/hybrid-context.svelte.d.ts +1 -0
- package/dist/admin/client/entry/hybrid/hybrid-preview.svelte +69 -0
- package/dist/admin/client/entry/hybrid/hybrid-preview.svelte.d.ts +16 -0
- package/dist/admin/components/fields/field-renderer.svelte +8 -14
- package/dist/admin/components/fields/image-field.svelte +6 -5
- package/dist/admin/components/fields/lazy-field.svelte +24 -0
- package/dist/admin/components/fields/lazy-field.svelte.d.ts +11 -0
- package/dist/admin/components/fields/text-field-wrapper.svelte +10 -7
- package/dist/admin/components/media/file/file-details.svelte +3 -1
- package/dist/admin/components/media/focal-point-input.svelte +106 -0
- package/dist/admin/components/media/focal-point-input.svelte.d.ts +7 -0
- package/dist/admin/remote/media.remote.d.ts +5 -0
- package/dist/admin/remote/media.remote.js +23 -0
- package/dist/cms/runtime/types.d.ts +16 -1
- package/dist/cms/runtime/types.js +3 -0
- package/dist/core/cms.d.ts +2 -1
- package/dist/core/cms.js +2 -0
- package/dist/core/server/fields/resolveImageFields.js +10 -5
- package/dist/core/server/fields/utils/imageStyles.d.ts +9 -2
- package/dist/core/server/fields/utils/imageStyles.js +87 -20
- package/dist/core/server/generator/fields.js +9 -3
- package/dist/core/server/generator/generator.js +4 -7
- package/dist/core/server/generator/utils.d.ts +1 -0
- package/dist/core/server/generator/utils.js +6 -0
- package/dist/core/server/media/operations/replaceFile.js +18 -2
- package/dist/core/server/media/operations/uploadFile.js +50 -1
- package/dist/core/server/media/styles/operations/generateDefaultStyles.d.ts +3 -0
- package/dist/core/server/media/styles/operations/generateDefaultStyles.js +26 -0
- package/dist/core/server/media/styles/sharp/generateImageStyle.js +36 -7
- package/dist/core/server/media/utils/calculateFocalCropRegion.d.ts +11 -0
- package/dist/core/server/media/utils/calculateFocalCropRegion.js +26 -0
- package/dist/core/server/media/utils/generateBlurDataUrl.d.ts +1 -0
- package/dist/core/server/media/utils/generateBlurDataUrl.js +5 -0
- package/dist/db-postgres/index.js +1 -1
- package/dist/db-postgres/schema/imageStyle.d.ts +17 -0
- package/dist/db-postgres/schema/imageStyle.js +1 -0
- package/dist/db-postgres/schema/mediaFile.d.ts +51 -0
- package/dist/db-postgres/schema/mediaFile.js +4 -1
- package/dist/sveltekit/components/image.svelte +26 -2
- package/dist/sveltekit/components/image.svelte.d.ts +1 -0
- package/dist/sveltekit/components/preview.svelte +22 -14
- package/dist/sveltekit/components/preview.svelte.d.ts +38 -8
- package/dist/sveltekit/index.d.ts +1 -0
- package/dist/sveltekit/index.js +1 -0
- package/dist/sveltekit/utils/getLink.d.ts +7 -0
- package/dist/sveltekit/utils/getLink.js +32 -0
- package/dist/sveltekit/utils/index.d.ts +2 -0
- package/dist/sveltekit/utils/index.js +2 -0
- package/dist/sveltekit/utils/media.d.ts +3 -0
- package/dist/sveltekit/utils/media.js +6 -0
- package/dist/types/cms.d.ts +6 -0
- package/dist/types/fields.d.ts +4 -0
- package/dist/types/index.d.ts +8 -1
- package/dist/types/index.js +7 -0
- package/dist/types/media.d.ts +8 -0
- package/dist/updates/0.0.67/index.d.ts +2 -0
- package/dist/updates/0.0.67/index.js +40 -0
- package/dist/updates/0.0.67/migration.sql +9 -0
- package/dist/updates/0.0.68/index.d.ts +2 -0
- package/dist/updates/0.0.68/index.js +21 -0
- package/dist/updates/index.js +3 -1
- package/package.json +1 -1
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { RawEntry } from '../../../types/entries.js';
|
|
2
2
|
import type { SuperForm } from 'sveltekit-superforms';
|
|
3
3
|
type Props = {
|
|
4
|
-
form: SuperForm<
|
|
4
|
+
form: SuperForm<Record<string, unknown>>;
|
|
5
5
|
entry: RawEntry;
|
|
6
6
|
focusedPath?: string | null;
|
|
7
7
|
onPathSelect?: (path: string) => void;
|
|
@@ -7,8 +7,6 @@
|
|
|
7
7
|
import type { InterfaceLanguage } from '../../../types/languages.js';
|
|
8
8
|
import StatusBadge from './header/status-badge.svelte';
|
|
9
9
|
import SaveIndicator from './header/save-indicator.svelte';
|
|
10
|
-
import PublishPanel from './header/publish-panel.svelte';
|
|
11
|
-
import VersionHistorySheet from './header/version-history-sheet.svelte';
|
|
12
10
|
import type { UpdateEntryVersionCommandType } from '../../../core/server/entries/operations/update.js';
|
|
13
11
|
import LayoutSidebar from '@tabler/icons-svelte/icons/layout-sidebar';
|
|
14
12
|
import { hasHybridContext, getHybridContext } from './hybrid/hybrid-context.svelte.js';
|
|
@@ -108,9 +106,13 @@
|
|
|
108
106
|
{primaryButtonLabel}
|
|
109
107
|
</Button>
|
|
110
108
|
|
|
111
|
-
|
|
109
|
+
{#await import('./header/publish-panel.svelte') then { default: PublishPanel }}
|
|
110
|
+
<PublishPanel {entry} {version} {onSave} {onArchive} />
|
|
111
|
+
{/await}
|
|
112
112
|
|
|
113
|
-
|
|
113
|
+
{#await import('./header/version-history-sheet.svelte') then { default: VersionHistorySheet }}
|
|
114
|
+
<VersionHistorySheet {entry} currentVersionId={version.id} />
|
|
115
|
+
{/await}
|
|
114
116
|
|
|
115
117
|
{#if hybridContext?.enabled}
|
|
116
118
|
<Button
|
|
@@ -14,20 +14,12 @@
|
|
|
14
14
|
import { zod4, zod4Client } from 'sveltekit-superforms/adapters';
|
|
15
15
|
import { generateZodSchemaFromFields } from '../../../core/fields/fieldSchemaToTs.js';
|
|
16
16
|
import type { DbEntryVersion, RawEntry } from '../../../types/entries.js';
|
|
17
|
-
import * as ToggleGroup from '../../../components/ui/toggle-group/index.js';
|
|
18
17
|
import EntryForm from './entry-form.svelte';
|
|
19
|
-
import DeviceTablet from '@tabler/icons-svelte/icons/device-tablet';
|
|
20
|
-
import DeviceMobile from '@tabler/icons-svelte/icons/device-mobile';
|
|
21
|
-
import Button from '../../../components/ui/button/button.svelte';
|
|
22
|
-
import Link from '@tabler/icons-svelte/icons/link';
|
|
23
|
-
import DeviceDesktop from '@tabler/icons-svelte/icons/device-desktop';
|
|
24
|
-
import ArrowsMaximize from '@tabler/icons-svelte/icons/arrows-maximize';
|
|
25
18
|
import type { UpdateEntryVersionCommandType } from '../../../core/server/entries/operations/update.js';
|
|
26
19
|
import { getRawCollectionEntryLabel } from '../../utils/entryLabel.js';
|
|
27
20
|
import type { Field } from '../../../types/fields.js';
|
|
28
21
|
import type { ValidationErrors } from 'sveltekit-superforms';
|
|
29
22
|
import { createHybridContext } from './hybrid/hybrid-context.svelte.js';
|
|
30
|
-
import HybridLayout from './hybrid/hybrid-layout.svelte';
|
|
31
23
|
import { onMount } from 'svelte';
|
|
32
24
|
import { get } from 'svelte/store';
|
|
33
25
|
|
|
@@ -298,8 +290,18 @@
|
|
|
298
290
|
|
|
299
291
|
let sizePreset: SizePreset = $state('responsive');
|
|
300
292
|
|
|
293
|
+
function getOriginFromUrl(url: string): string {
|
|
294
|
+
try {
|
|
295
|
+
return new URL(url, window.location.origin).origin;
|
|
296
|
+
} catch {
|
|
297
|
+
return window.location.origin;
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
const previewOrigin = collection.previewUrl ? getOriginFromUrl(collection.previewUrl) : window.location.origin;
|
|
302
|
+
|
|
301
303
|
const updatePreview = useDebounce(
|
|
302
|
-
async (window: Window, form: SuperForm<
|
|
304
|
+
async (window: Window, form: SuperForm<Record<string, unknown>>) => {
|
|
303
305
|
const data = await form.validateForm();
|
|
304
306
|
|
|
305
307
|
if (data.valid) {
|
|
@@ -314,7 +316,7 @@
|
|
|
314
316
|
type: 'preview-update',
|
|
315
317
|
data: updatedData
|
|
316
318
|
},
|
|
317
|
-
|
|
319
|
+
previewOrigin
|
|
318
320
|
);
|
|
319
321
|
}
|
|
320
322
|
},
|
|
@@ -329,6 +331,7 @@
|
|
|
329
331
|
|
|
330
332
|
// Subscribe to form changes for preview updates
|
|
331
333
|
onMount(() => {
|
|
334
|
+
if (!collection.previewUrl) return;
|
|
332
335
|
const unsub = form.form.subscribe(() => {
|
|
333
336
|
onUpdate();
|
|
334
337
|
});
|
|
@@ -347,7 +350,7 @@
|
|
|
347
350
|
// Send hybrid mode state first
|
|
348
351
|
previewIframe.contentWindow.postMessage(
|
|
349
352
|
{ type: 'hybrid-mode-enable', enabled: hybridContext.mode === 'hybrid' },
|
|
350
|
-
|
|
353
|
+
previewOrigin
|
|
351
354
|
);
|
|
352
355
|
|
|
353
356
|
// Then send form data
|
|
@@ -360,7 +363,7 @@
|
|
|
360
363
|
});
|
|
361
364
|
previewIframe.contentWindow.postMessage(
|
|
362
365
|
{ type: 'preview-update', data: updatedData },
|
|
363
|
-
|
|
366
|
+
previewOrigin
|
|
364
367
|
);
|
|
365
368
|
}
|
|
366
369
|
}
|
|
@@ -376,6 +379,7 @@
|
|
|
376
379
|
// Listen for messages from iframe
|
|
377
380
|
onMount(() => {
|
|
378
381
|
function handleMessage(e: MessageEvent) {
|
|
382
|
+
if (e.origin !== previewOrigin) return;
|
|
379
383
|
// Click on preview element - debounced to prevent scroll jumping
|
|
380
384
|
if (e.data.type === 'hybrid-focus' && e.data.path) {
|
|
381
385
|
debouncedSetFocusPath(e.data.path);
|
|
@@ -394,7 +398,7 @@
|
|
|
394
398
|
if (previewIframe?.contentWindow) {
|
|
395
399
|
previewIframe.contentWindow.postMessage(
|
|
396
400
|
{ type: 'hybrid-highlight', path: hybridContext.focusedPath },
|
|
397
|
-
|
|
401
|
+
previewOrigin
|
|
398
402
|
);
|
|
399
403
|
}
|
|
400
404
|
});
|
|
@@ -407,7 +411,7 @@
|
|
|
407
411
|
if (previewIframe?.contentWindow) {
|
|
408
412
|
previewIframe.contentWindow.postMessage(
|
|
409
413
|
{ type: 'hybrid-mode-enable', enabled: hybridContext.mode === 'hybrid' },
|
|
410
|
-
|
|
414
|
+
previewOrigin
|
|
411
415
|
);
|
|
412
416
|
}
|
|
413
417
|
}
|
|
@@ -418,55 +422,31 @@
|
|
|
418
422
|
|
|
419
423
|
{#if hybridContext.mode === 'hybrid' && collection.previewUrl}
|
|
420
424
|
<div class="flex min-h-0 flex-1 overflow-hidden">
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
</Button>
|
|
447
|
-
</div>
|
|
448
|
-
<div class="relative flex-1 overflow-hidden" bind:this={el}>
|
|
449
|
-
<iframe
|
|
450
|
-
bind:this={previewIframe}
|
|
451
|
-
style={sizePreset !== 'responsive' ? `transform: scale(${size.width / sizePresets[sizePreset][0]});` : ''}
|
|
452
|
-
class="{sizePreset === 'responsive' ? 'h-full w-full' : 'absolute top-0 left-0 origin-top-left'} border-0"
|
|
453
|
-
width={sizePreset !== 'responsive' ? sizePresets[sizePreset][0] : undefined}
|
|
454
|
-
height={sizePreset !== 'responsive' ? sizePresets[sizePreset][1] : undefined}
|
|
455
|
-
src="{collection.previewUrl}?preview={editingEntry.id}"
|
|
456
|
-
title="preview"
|
|
457
|
-
></iframe>
|
|
458
|
-
</div>
|
|
459
|
-
</div>
|
|
460
|
-
{/snippet}
|
|
461
|
-
{#snippet formPanel()}
|
|
462
|
-
<EntryForm
|
|
463
|
-
{form}
|
|
464
|
-
{entry}
|
|
465
|
-
focusedPath={hybridContext.focusedPath}
|
|
466
|
-
onPathSelect={(path) => hybridContext.focusedPath = path}
|
|
467
|
-
/>
|
|
468
|
-
{/snippet}
|
|
469
|
-
</HybridLayout>
|
|
425
|
+
{#await import('./hybrid/hybrid-layout.svelte')}
|
|
426
|
+
<div class="h-full animate-pulse rounded-md bg-accent"></div>
|
|
427
|
+
{:then { default: HybridLayout }}
|
|
428
|
+
<HybridLayout>
|
|
429
|
+
{#snippet preview()}
|
|
430
|
+
{#await import('./hybrid/hybrid-preview.svelte')}
|
|
431
|
+
<div class="h-full animate-pulse rounded-md bg-accent"></div>
|
|
432
|
+
{:then { default: HybridPreview }}
|
|
433
|
+
<HybridPreview {collection} {editingEntry} bind:previewIframe bind:sizePreset {size} bind:el />
|
|
434
|
+
{:catch}
|
|
435
|
+
<p class="p-4 text-sm text-destructive">Failed to load preview</p>
|
|
436
|
+
{/await}
|
|
437
|
+
{/snippet}
|
|
438
|
+
{#snippet formPanel()}
|
|
439
|
+
<EntryForm
|
|
440
|
+
{form}
|
|
441
|
+
{entry}
|
|
442
|
+
focusedPath={hybridContext.focusedPath}
|
|
443
|
+
onPathSelect={(path) => hybridContext.focusedPath = path}
|
|
444
|
+
/>
|
|
445
|
+
{/snippet}
|
|
446
|
+
</HybridLayout>
|
|
447
|
+
{:catch}
|
|
448
|
+
<p class="p-4 text-sm text-destructive">Failed to load layout</p>
|
|
449
|
+
{/await}
|
|
470
450
|
</div>
|
|
471
451
|
{:else}
|
|
472
452
|
<div class="flex items-stretch justify-center">
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import * as ToggleGroup from '../../../../components/ui/toggle-group/index.js';
|
|
3
|
+
import Button from '../../../../components/ui/button/button.svelte';
|
|
4
|
+
import DeviceTablet from '@tabler/icons-svelte/icons/device-tablet';
|
|
5
|
+
import DeviceMobile from '@tabler/icons-svelte/icons/device-mobile';
|
|
6
|
+
import DeviceDesktop from '@tabler/icons-svelte/icons/device-desktop';
|
|
7
|
+
import ArrowsMaximize from '@tabler/icons-svelte/icons/arrows-maximize';
|
|
8
|
+
import Link from '@tabler/icons-svelte/icons/link';
|
|
9
|
+
import type { DbEntryVersion } from '../../../../types/entries.js';
|
|
10
|
+
import type { ElementSize } from 'runed';
|
|
11
|
+
|
|
12
|
+
type SizePreset = 'responsive' | 'desktop' | 'tablet' | 'mobile';
|
|
13
|
+
|
|
14
|
+
type Props = {
|
|
15
|
+
collection: { previewUrl?: string };
|
|
16
|
+
editingEntry: DbEntryVersion;
|
|
17
|
+
previewIframe?: HTMLIFrameElement;
|
|
18
|
+
sizePreset: SizePreset;
|
|
19
|
+
size: ElementSize;
|
|
20
|
+
el?: HTMLElement;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
let { collection, editingEntry, previewIframe = $bindable(), sizePreset = $bindable('responsive'), size, el = $bindable() }: Props = $props();
|
|
24
|
+
|
|
25
|
+
const sizePresets = {
|
|
26
|
+
responsive: null,
|
|
27
|
+
desktop: [1440, 925],
|
|
28
|
+
tablet: [768, 1024],
|
|
29
|
+
mobile: [375, 667]
|
|
30
|
+
} as const;
|
|
31
|
+
</script>
|
|
32
|
+
|
|
33
|
+
<div class="flex h-full flex-col overflow-hidden">
|
|
34
|
+
<div class="flex items-center justify-center gap-2 border-b px-2 py-1.5">
|
|
35
|
+
<ToggleGroup.Root type="single" bind:value={sizePreset}>
|
|
36
|
+
<ToggleGroup.Item value="responsive" aria-label="Responsive view">
|
|
37
|
+
<ArrowsMaximize class="size-4" />
|
|
38
|
+
</ToggleGroup.Item>
|
|
39
|
+
<ToggleGroup.Item value="desktop" aria-label="Desktop view">
|
|
40
|
+
<DeviceDesktop class="size-4" />
|
|
41
|
+
</ToggleGroup.Item>
|
|
42
|
+
<ToggleGroup.Item value="tablet" aria-label="Tablet view">
|
|
43
|
+
<DeviceTablet class="size-4" />
|
|
44
|
+
</ToggleGroup.Item>
|
|
45
|
+
<ToggleGroup.Item value="mobile" aria-label="Mobile view">
|
|
46
|
+
<DeviceMobile class="size-4" />
|
|
47
|
+
</ToggleGroup.Item>
|
|
48
|
+
</ToggleGroup.Root>
|
|
49
|
+
<Button
|
|
50
|
+
variant="ghost"
|
|
51
|
+
size="icon"
|
|
52
|
+
href="{collection.previewUrl}?preview={editingEntry.id}"
|
|
53
|
+
target="_blank"
|
|
54
|
+
>
|
|
55
|
+
<Link class="size-4" />
|
|
56
|
+
</Button>
|
|
57
|
+
</div>
|
|
58
|
+
<div class="relative flex-1 overflow-hidden" bind:this={el}>
|
|
59
|
+
<iframe
|
|
60
|
+
bind:this={previewIframe}
|
|
61
|
+
style={sizePreset !== 'responsive' ? `transform: scale(${size.width / sizePresets[sizePreset][0]});` : ''}
|
|
62
|
+
class="{sizePreset === 'responsive' ? 'h-full w-full' : 'absolute top-0 left-0 origin-top-left'} border-0"
|
|
63
|
+
width={sizePreset !== 'responsive' ? sizePresets[sizePreset][0] : undefined}
|
|
64
|
+
height={sizePreset !== 'responsive' ? sizePresets[sizePreset][1] : undefined}
|
|
65
|
+
src="{collection.previewUrl}?preview={editingEntry.id}"
|
|
66
|
+
title="preview"
|
|
67
|
+
></iframe>
|
|
68
|
+
</div>
|
|
69
|
+
</div>
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { DbEntryVersion } from '../../../../types/entries.js';
|
|
2
|
+
import type { ElementSize } from 'runed';
|
|
3
|
+
type SizePreset = 'responsive' | 'desktop' | 'tablet' | 'mobile';
|
|
4
|
+
type Props = {
|
|
5
|
+
collection: {
|
|
6
|
+
previewUrl?: string;
|
|
7
|
+
};
|
|
8
|
+
editingEntry: DbEntryVersion;
|
|
9
|
+
previewIframe?: HTMLIFrameElement;
|
|
10
|
+
sizePreset: SizePreset;
|
|
11
|
+
size: ElementSize;
|
|
12
|
+
el?: HTMLElement;
|
|
13
|
+
};
|
|
14
|
+
declare const HybridPreview: import("svelte").Component<Props, {}, "previewIframe" | "sizePreset" | "el">;
|
|
15
|
+
type HybridPreview = ReturnType<typeof HybridPreview>;
|
|
16
|
+
export default HybridPreview;
|
|
@@ -2,10 +2,6 @@
|
|
|
2
2
|
import * as Form from '../../../components/ui/form/index.js';
|
|
3
3
|
|
|
4
4
|
import type { FormPathLeaves, SuperForm } from 'sveltekit-superforms';
|
|
5
|
-
import ImageField from './image-field.svelte';
|
|
6
|
-
import MediaFieldComponent from './media-field.svelte';
|
|
7
|
-
import ArrayField from './array-field.svelte';
|
|
8
|
-
import ObjectField from './object-field.svelte';
|
|
9
5
|
import SlugField from './slug-field.svelte';
|
|
10
6
|
import type { Field, FieldType } from '../../../types/fields.js';
|
|
11
7
|
import TextFieldWrapper from './text-field-wrapper.svelte';
|
|
@@ -13,14 +9,12 @@
|
|
|
13
9
|
import BooleanField from './boolean-field.svelte';
|
|
14
10
|
import RadioField from './radio-field.svelte';
|
|
15
11
|
import CheckboxesField from './checkboxes-field.svelte';
|
|
16
|
-
import FileField from './file-field.svelte';
|
|
17
12
|
import NumberField from './number-field.svelte';
|
|
18
13
|
import DateField from './date-field.svelte';
|
|
19
14
|
import DateTimeField from './datetime-field.svelte';
|
|
20
15
|
import SelectField from './select-field.svelte';
|
|
21
|
-
import SeoField from './seo-field.svelte';
|
|
22
|
-
import RelationField from './relation-field.svelte';
|
|
23
16
|
import UrlFieldWrapper from './url-field-wrapper.svelte';
|
|
17
|
+
import LazyField from './lazy-field.svelte';
|
|
24
18
|
import { useInterfaceLanguage } from '../../state/interface-language.svelte.js';
|
|
25
19
|
import { getLocalizedLabel } from '../../utils/collectionLabel.js';
|
|
26
20
|
|
|
@@ -105,15 +99,15 @@
|
|
|
105
99
|
{/if}
|
|
106
100
|
|
|
107
101
|
{#if field.type === 'image'}
|
|
108
|
-
<
|
|
102
|
+
<LazyField loader={() => import('./image-field.svelte')} props={{ field, form, path, ...props }} skeletonClass="h-24" />
|
|
109
103
|
{:else if field.type === 'media'}
|
|
110
|
-
<
|
|
104
|
+
<LazyField loader={() => import('./media-field.svelte')} props={{ field, form, path, ...props }} skeletonClass="h-24" />
|
|
111
105
|
{:else if field.type === 'file'}
|
|
112
|
-
<
|
|
106
|
+
<LazyField loader={() => import('./file-field.svelte')} props={{ field, form, path, ...props }} skeletonClass="h-12" />
|
|
113
107
|
{:else if field.type === 'array'}
|
|
114
|
-
<
|
|
108
|
+
<LazyField loader={() => import('./array-field.svelte')} props={{ field, form, path, focusedPath, flashingPath, depth, ...props }} skeletonClass="h-20" />
|
|
115
109
|
{:else if field.type === 'object'}
|
|
116
|
-
<
|
|
110
|
+
<LazyField loader={() => import('./object-field.svelte')} props={{ field, form, path, objectFieldType, focusedPath, flashingPath, depth, ...props }} skeletonClass="h-20" />
|
|
117
111
|
{:else if field.type === 'slug'}
|
|
118
112
|
<SlugField {field} {form} {path} {...props} />
|
|
119
113
|
{:else if field.type === 'boolean'}
|
|
@@ -121,11 +115,11 @@
|
|
|
121
115
|
{:else if field.type === 'number'}
|
|
122
116
|
<NumberField {field} {form} {path} {...props} />
|
|
123
117
|
{:else if field.type === 'seo'}
|
|
124
|
-
<
|
|
118
|
+
<LazyField loader={() => import('./seo-field.svelte')} props={{ field, form, path, ...props }} skeletonClass="h-16" />
|
|
125
119
|
{:else if field.type === 'url'}
|
|
126
120
|
<UrlFieldWrapper {field} {form} {path} {...props} />
|
|
127
121
|
{:else if field.type === 'relation'}
|
|
128
|
-
<
|
|
122
|
+
<LazyField loader={() => import('./relation-field.svelte')} props={{ field, form, path, ...props }} skeletonClass="h-16" />
|
|
129
123
|
{:else if field.type === 'date'}
|
|
130
124
|
<DateField {field} {form} {path} {...props} />
|
|
131
125
|
{:else if field.type === 'datetime'}
|
|
@@ -35,7 +35,6 @@
|
|
|
35
35
|
let currentIndex = $state(0);
|
|
36
36
|
let lightboxOpen = $state(false);
|
|
37
37
|
let lightboxFile = $state<MediaFile | null>(null);
|
|
38
|
-
|
|
39
38
|
function openLightbox(file: MediaFile) {
|
|
40
39
|
lightboxFile = file;
|
|
41
40
|
lightboxOpen = true;
|
|
@@ -139,10 +138,12 @@
|
|
|
139
138
|
<div
|
|
140
139
|
class="absolute inset-x-0 bottom-0 flex items-center justify-between gap-2 bg-gradient-to-t from-black/60 to-transparent p-2 pt-8"
|
|
141
140
|
>
|
|
142
|
-
<
|
|
143
|
-
<
|
|
144
|
-
|
|
145
|
-
|
|
141
|
+
<div class="flex gap-1">
|
|
142
|
+
<Button {...props} size="sm" variant="secondary" class="h-8">
|
|
143
|
+
<Plus class="h-4 w-4" />
|
|
144
|
+
{lang[interfaceLanguage.current].change}
|
|
145
|
+
</Button>
|
|
146
|
+
</div>
|
|
146
147
|
{#if !field.required}
|
|
147
148
|
<Button
|
|
148
149
|
variant="destructive"
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { Component } from 'svelte';
|
|
3
|
+
|
|
4
|
+
type Props = {
|
|
5
|
+
loader: () => Promise<{ default: Component<any> }>;
|
|
6
|
+
props: Record<string, any>;
|
|
7
|
+
skeletonClass?: string;
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
let { loader, props: fieldProps, skeletonClass = 'h-10' }: Props = $props();
|
|
11
|
+
let Comp = $state<Component<any> | null>(null);
|
|
12
|
+
|
|
13
|
+
$effect(() => {
|
|
14
|
+
loader().then((m) => {
|
|
15
|
+
Comp = m.default;
|
|
16
|
+
});
|
|
17
|
+
});
|
|
18
|
+
</script>
|
|
19
|
+
|
|
20
|
+
{#if Comp}
|
|
21
|
+
<Comp {...fieldProps} />
|
|
22
|
+
{:else}
|
|
23
|
+
<div class="{skeletonClass} animate-pulse rounded-md bg-accent"></div>
|
|
24
|
+
{/if}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { Component } from 'svelte';
|
|
2
|
+
type Props = {
|
|
3
|
+
loader: () => Promise<{
|
|
4
|
+
default: Component<any>;
|
|
5
|
+
}>;
|
|
6
|
+
props: Record<string, any>;
|
|
7
|
+
skeletonClass?: string;
|
|
8
|
+
};
|
|
9
|
+
declare const LazyField: Component<Props, {}, "">;
|
|
10
|
+
type LazyField = ReturnType<typeof LazyField>;
|
|
11
|
+
export default LazyField;
|
|
@@ -12,7 +12,6 @@
|
|
|
12
12
|
import { type FormPathLeaves, type SuperForm } from 'sveltekit-superforms';
|
|
13
13
|
import TextField from './text-field.svelte';
|
|
14
14
|
import { joinPath } from '../../utils/objectPath.js';
|
|
15
|
-
import RichtextField from './richtext-field.svelte';
|
|
16
15
|
import { getContentLanguage } from '../../state/content-language.svelte.js';
|
|
17
16
|
import { useInterfaceLanguage } from '../../state/interface-language.svelte.js';
|
|
18
17
|
import { getLocalizedLabel } from '../../utils/collectionLabel.js';
|
|
@@ -53,12 +52,16 @@
|
|
|
53
52
|
path={joinPath(path, lang) as FormPathLeaves<T, string | undefined>}
|
|
54
53
|
/>
|
|
55
54
|
{:else if field.type === 'richtext'}
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
55
|
+
{#await import('./richtext-field.svelte') then { default: RichtextField }}
|
|
56
|
+
<RichtextField
|
|
57
|
+
{...props}
|
|
58
|
+
{field}
|
|
59
|
+
{form}
|
|
60
|
+
path={joinPath(path, lang) as FormPathLeaves<T, string | undefined>}
|
|
61
|
+
/>
|
|
62
|
+
{:catch}
|
|
63
|
+
<div class="h-32 animate-pulse rounded-md bg-accent"></div>
|
|
64
|
+
{/await}
|
|
62
65
|
{/if}
|
|
63
66
|
{/snippet}
|
|
64
67
|
</Form.Control>
|
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
import type { InterfaceLanguage } from '../../../../types/languages.js';
|
|
7
7
|
import type { MediaFile, MediaTag } from '../../../../types/media.js';
|
|
8
8
|
import AltInput from '../alt-input.svelte';
|
|
9
|
+
import FocalPointInput from '../focal-point-input.svelte';
|
|
9
10
|
import TagCombobox from '../tag-combobox.svelte';
|
|
10
11
|
import FileMiniature from './file-miniature.svelte';
|
|
11
12
|
import MediaSelector from '../media-selector.svelte';
|
|
@@ -276,12 +277,13 @@
|
|
|
276
277
|
</InputGroup.Root>
|
|
277
278
|
</div>
|
|
278
279
|
|
|
279
|
-
<!-- Alt text
|
|
280
|
+
<!-- Alt text + focal point for images -->
|
|
280
281
|
{#if file.type === 'image'}
|
|
281
282
|
<div class="space-y-1.5">
|
|
282
283
|
<Label class="text-xs">{lang[interfaceLanguage.current].fileAltLabel}</Label>
|
|
283
284
|
<AltInput alt={file.alt} fileId={file.id} />
|
|
284
285
|
</div>
|
|
286
|
+
<FocalPointInput {file} />
|
|
285
287
|
{/if}
|
|
286
288
|
|
|
287
289
|
<!-- Accessibility section for video -->
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import Button from '../../../components/ui/button/button.svelte';
|
|
3
|
+
import { getRemotes } from '../../context/remotes.js';
|
|
4
|
+
import { toast } from 'svelte-sonner';
|
|
5
|
+
import { useInterfaceLanguage } from '../../state/interface-language.svelte.js';
|
|
6
|
+
import type { InterfaceLanguage } from '../../../types/languages.js';
|
|
7
|
+
import type { MediaFile } from '../../../types/media.js';
|
|
8
|
+
import Focus from '@tabler/icons-svelte/icons/focus-2';
|
|
9
|
+
|
|
10
|
+
const interfaceLanguage = useInterfaceLanguage();
|
|
11
|
+
const lang: Record<InterfaceLanguage, { label: string; save: string; success: string; error: string }> = {
|
|
12
|
+
pl: {
|
|
13
|
+
label: 'Punkt ogniskowy',
|
|
14
|
+
save: 'Zapisz punkt ogniskowy',
|
|
15
|
+
success: 'Punkt ogniskowy zapisany.',
|
|
16
|
+
error: 'Nie udało się zapisać punktu ogniskowego.'
|
|
17
|
+
},
|
|
18
|
+
en: {
|
|
19
|
+
label: 'Focal point',
|
|
20
|
+
save: 'Save focal point',
|
|
21
|
+
success: 'Focal point saved.',
|
|
22
|
+
error: 'Failed to save focal point.'
|
|
23
|
+
}
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
const remotes = getRemotes();
|
|
27
|
+
|
|
28
|
+
type Props = {
|
|
29
|
+
file: MediaFile;
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
let { file }: Props = $props();
|
|
33
|
+
|
|
34
|
+
let focalX = $state(file.focalX ?? 0.5);
|
|
35
|
+
let focalY = $state(file.focalY ?? 0.5);
|
|
36
|
+
let saving = $state(false);
|
|
37
|
+
let container: HTMLDivElement;
|
|
38
|
+
|
|
39
|
+
function handlePointer(e: PointerEvent) {
|
|
40
|
+
if (!container) return;
|
|
41
|
+
const rect = container.getBoundingClientRect();
|
|
42
|
+
focalX = Math.max(0, Math.min(1, (e.clientX - rect.left) / rect.width));
|
|
43
|
+
focalY = Math.max(0, Math.min(1, (e.clientY - rect.top) / rect.height));
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function onPointerDown(e: PointerEvent) {
|
|
47
|
+
container.setPointerCapture(e.pointerId);
|
|
48
|
+
handlePointer(e);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function onPointerMove(e: PointerEvent) {
|
|
52
|
+
if (container.hasPointerCapture(e.pointerId)) {
|
|
53
|
+
handlePointer(e);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
async function save() {
|
|
58
|
+
saving = true;
|
|
59
|
+
try {
|
|
60
|
+
await remotes.setFocalPoint({ fileId: file.id, focalX, focalY });
|
|
61
|
+
file.focalX = focalX;
|
|
62
|
+
file.focalY = focalY;
|
|
63
|
+
toast.success(lang[interfaceLanguage.current].success);
|
|
64
|
+
} catch {
|
|
65
|
+
toast.error(lang[interfaceLanguage.current].error);
|
|
66
|
+
} finally {
|
|
67
|
+
saving = false;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Reset when file changes
|
|
72
|
+
$effect(() => {
|
|
73
|
+
focalX = file.focalX ?? 0.5;
|
|
74
|
+
focalY = file.focalY ?? 0.5;
|
|
75
|
+
});
|
|
76
|
+
</script>
|
|
77
|
+
|
|
78
|
+
<div class="space-y-2">
|
|
79
|
+
<div class="flex items-center gap-1.5 text-xs font-medium">
|
|
80
|
+
<Focus class="h-3.5 w-3.5 text-muted-foreground" />
|
|
81
|
+
{lang[interfaceLanguage.current].label}
|
|
82
|
+
</div>
|
|
83
|
+
|
|
84
|
+
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
|
85
|
+
<div
|
|
86
|
+
bind:this={container}
|
|
87
|
+
class="relative cursor-crosshair overflow-hidden rounded-lg border select-none"
|
|
88
|
+
onpointerdown={onPointerDown}
|
|
89
|
+
onpointermove={onPointerMove}
|
|
90
|
+
>
|
|
91
|
+
<img src={file.url} alt={file.alt || file.name} class="w-full pointer-events-none" draggable="false" />
|
|
92
|
+
<!-- Crosshair -->
|
|
93
|
+
<div
|
|
94
|
+
class="absolute w-6 h-6 -translate-x-1/2 -translate-y-1/2 pointer-events-none"
|
|
95
|
+
style="left: {focalX * 100}%; top: {focalY * 100}%;"
|
|
96
|
+
>
|
|
97
|
+
<div class="absolute inset-0 rounded-full border-2 border-white shadow-[0_0_0_1px_rgba(0,0,0,0.3)]"></div>
|
|
98
|
+
<div class="absolute left-1/2 top-0 h-full w-px bg-white/80 -translate-x-px"></div>
|
|
99
|
+
<div class="absolute top-1/2 left-0 w-full h-px bg-white/80 -translate-y-px"></div>
|
|
100
|
+
</div>
|
|
101
|
+
</div>
|
|
102
|
+
|
|
103
|
+
<Button variant="outline" class="w-full" size="sm" onclick={save} disabled={saving}>
|
|
104
|
+
{lang[interfaceLanguage.current].save}
|
|
105
|
+
</Button>
|
|
106
|
+
</div>
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { MediaFile } from '../../../types/media.js';
|
|
2
|
+
type Props = {
|
|
3
|
+
file: MediaFile;
|
|
4
|
+
};
|
|
5
|
+
declare const FocalPointInput: import("svelte").Component<Props, {}, "">;
|
|
6
|
+
type FocalPointInput = ReturnType<typeof FocalPointInput>;
|
|
7
|
+
export default FocalPointInput;
|
|
@@ -46,6 +46,11 @@ export declare const updateMediaAccessibility: import("@sveltejs/kit").RemoteCom
|
|
|
46
46
|
audioDescriptionFileId?: string | null | undefined;
|
|
47
47
|
posterUrl?: string | null | undefined;
|
|
48
48
|
}, Promise<import("../../types/media.js").MediaFile>>;
|
|
49
|
+
export declare const setFocalPoint: import("@sveltejs/kit").RemoteCommand<{
|
|
50
|
+
fileId: string;
|
|
51
|
+
focalX: number;
|
|
52
|
+
focalY: number;
|
|
53
|
+
}, Promise<void>>;
|
|
49
54
|
export declare const renameMediaFile: import("@sveltejs/kit").RemoteCommand<{
|
|
50
55
|
fileId: string;
|
|
51
56
|
newName: string;
|