@aphexcms/cms-core 9.1.0 → 9.2.1
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/cli/generate-types.d.ts.map +1 -1
- package/dist/cli/generate-types.js +8 -3
- package/dist/cli/generate-types.js.map +1 -1
- package/dist/client/index.d.ts +1 -0
- package/dist/client/index.d.ts.map +1 -1
- package/dist/client/index.js +4 -0
- package/dist/components/AdminApp.svelte +27 -11
- package/dist/components/AdminApp.svelte.d.ts.map +1 -1
- package/dist/components/admin/DocumentEditor.svelte +548 -21
- package/dist/components/admin/DocumentEditor.svelte.d.ts +6 -0
- package/dist/components/admin/DocumentEditor.svelte.d.ts.map +1 -1
- package/dist/components/admin/SchemaField.svelte +1 -1
- package/dist/components/admin/fields/ArrayField.svelte +2 -2
- package/dist/components/admin/fields/ImageField.svelte +85 -27
- package/dist/components/admin/fields/ImageField.svelte.d.ts.map +1 -1
- package/dist/components/admin/fields/richtext/ImageBlockModal.svelte +133 -0
- package/dist/components/admin/fields/richtext/ImageBlockModal.svelte.d.ts +14 -0
- package/dist/components/admin/fields/richtext/ImageBlockModal.svelte.d.ts.map +1 -0
- package/dist/components/admin/fields/richtext/PortableTextImageView.svelte +2 -2
- package/dist/components/admin/fields/richtext/RichtextField.svelte +182 -19
- package/dist/components/admin/fields/richtext/RichtextField.svelte.d.ts.map +1 -1
- package/dist/components/admin/fields/richtext/portable-text-serializer.d.ts.map +1 -1
- package/dist/components/admin/fields/richtext/portable-text-serializer.js +22 -1
- package/dist/components/admin/fields/richtext/svelte-node-view.d.ts.map +1 -1
- package/dist/components/admin/fields/richtext/svelte-node-view.js +11 -0
- package/dist/lib/client/index.d.ts +1 -0
- package/dist/lib/client/index.d.ts.map +1 -1
- package/dist/lib/client/index.js +4 -0
- package/dist/lib/client/index.js.map +1 -1
- package/dist/lib/components/admin/fields/richtext/portable-text-serializer.d.ts.map +1 -1
- package/dist/lib/components/admin/fields/richtext/portable-text-serializer.js +22 -1
- package/dist/lib/components/admin/fields/richtext/portable-text-serializer.js.map +1 -1
- package/dist/lib/components/admin/fields/richtext/svelte-node-view.d.ts.map +1 -1
- package/dist/lib/components/admin/fields/richtext/svelte-node-view.js +11 -0
- package/dist/lib/components/admin/fields/richtext/svelte-node-view.js.map +1 -1
- package/dist/lib/preview/assets.d.ts +19 -0
- package/dist/lib/preview/assets.d.ts.map +1 -0
- package/dist/lib/preview/assets.js +60 -0
- package/dist/lib/preview/assets.js.map +1 -0
- package/dist/lib/preview/perspective.d.ts +20 -0
- package/dist/lib/preview/perspective.d.ts.map +1 -0
- package/dist/lib/preview/perspective.js +21 -0
- package/dist/lib/preview/perspective.js.map +1 -0
- package/dist/lib/preview/stega.d.ts +25 -0
- package/dist/lib/preview/stega.d.ts.map +1 -0
- package/dist/lib/preview/stega.js +168 -0
- package/dist/lib/preview/stega.js.map +1 -0
- package/dist/lib/richtext-context.svelte.d.ts +18 -0
- package/dist/lib/richtext-context.svelte.d.ts.map +1 -0
- package/dist/lib/richtext-context.svelte.js +13 -0
- package/dist/lib/richtext-context.svelte.js.map +1 -0
- package/dist/lib/server/index.d.ts +1 -0
- package/dist/lib/server/index.d.ts.map +1 -1
- package/dist/lib/server/index.js +2 -0
- package/dist/lib/server/index.js.map +1 -1
- package/dist/lib/services/asset-service.d.ts +12 -0
- package/dist/lib/services/asset-service.d.ts.map +1 -1
- package/dist/lib/services/asset-service.js +32 -0
- package/dist/lib/services/asset-service.js.map +1 -1
- package/dist/lib/types/asset.d.ts +15 -0
- package/dist/lib/types/asset.d.ts.map +1 -1
- package/dist/lib/types/config.d.ts +7 -0
- package/dist/lib/types/config.d.ts.map +1 -1
- package/dist/lib/types/schemas.d.ts +17 -0
- package/dist/lib/types/schemas.d.ts.map +1 -1
- package/dist/lib/utils/preview.d.ts +4 -0
- package/dist/lib/utils/preview.d.ts.map +1 -1
- package/dist/lib/utils/preview.js +7 -0
- package/dist/lib/utils/preview.js.map +1 -1
- package/dist/preview/assets.d.ts +19 -0
- package/dist/preview/assets.d.ts.map +1 -0
- package/dist/preview/assets.js +59 -0
- package/dist/preview/perspective.d.ts +20 -0
- package/dist/preview/perspective.d.ts.map +1 -0
- package/dist/preview/perspective.js +20 -0
- package/dist/preview/stega.d.ts +25 -0
- package/dist/preview/stega.d.ts.map +1 -0
- package/dist/preview/stega.js +167 -0
- package/dist/richtext-context.svelte.d.ts +18 -0
- package/dist/richtext-context.svelte.d.ts.map +1 -0
- package/dist/richtext-context.svelte.js +12 -0
- package/dist/server/index.d.ts +1 -0
- package/dist/server/index.d.ts.map +1 -1
- package/dist/server/index.js +2 -0
- package/dist/services/asset-service.d.ts +12 -0
- package/dist/services/asset-service.d.ts.map +1 -1
- package/dist/services/asset-service.js +32 -0
- package/dist/types/asset.d.ts +15 -0
- package/dist/types/asset.d.ts.map +1 -1
- package/dist/types/config.d.ts +7 -0
- package/dist/types/config.d.ts.map +1 -1
- package/dist/types/schemas.d.ts +17 -0
- package/dist/types/schemas.d.ts.map +1 -1
- package/dist/utils/preview.d.ts +4 -0
- package/dist/utils/preview.d.ts.map +1 -1
- package/dist/utils/preview.js +7 -0
- package/package.json +3 -2
|
@@ -211,21 +211,25 @@
|
|
|
211
211
|
assetData?.originalFilename || assetData?.filename || value?.asset?._ref || 'Image'
|
|
212
212
|
);
|
|
213
213
|
|
|
214
|
-
//
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
//
|
|
214
|
+
// Two layers of alt text:
|
|
215
|
+
// • Default — stored on the ASSET, shared across every placement. Set it once and
|
|
216
|
+
// it applies everywhere the image is used.
|
|
217
|
+
// • Override — stored on THIS image value, for when one spot needs a different
|
|
218
|
+
// description. Render precedence is `value.alt || asset.alt`.
|
|
219
|
+
// The override is also what carries visual-editing stega.
|
|
220
|
+
let assetAltText = $state('');
|
|
221
|
+
let assetAltTimer: ReturnType<typeof setTimeout> | null = null;
|
|
222
|
+
|
|
223
|
+
// Sync the default from the loaded asset record.
|
|
219
224
|
$effect(() => {
|
|
220
|
-
|
|
225
|
+
assetAltText = assetData?.alt || '';
|
|
221
226
|
});
|
|
222
227
|
|
|
223
|
-
function
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
altSaveTimer = setTimeout(async () => {
|
|
228
|
+
function updateAssetAlt(newAlt: string) {
|
|
229
|
+
assetAltText = newAlt;
|
|
230
|
+
// Debounce the asset-update API call.
|
|
231
|
+
if (assetAltTimer) clearTimeout(assetAltTimer);
|
|
232
|
+
assetAltTimer = setTimeout(async () => {
|
|
229
233
|
const assetId = value?.asset?._ref;
|
|
230
234
|
if (!assetId) return;
|
|
231
235
|
try {
|
|
@@ -236,6 +240,17 @@
|
|
|
236
240
|
}, 800);
|
|
237
241
|
}
|
|
238
242
|
|
|
243
|
+
function updateOverrideAlt(newAlt: string) {
|
|
244
|
+
if (!value) return;
|
|
245
|
+
onUpdate({ ...value, alt: newAlt || undefined });
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// Whether to reveal the per-placement override input.
|
|
249
|
+
let showOverride = $state(false);
|
|
250
|
+
$effect(() => {
|
|
251
|
+
if (value?.alt) showOverride = true;
|
|
252
|
+
});
|
|
253
|
+
|
|
239
254
|
// Download image
|
|
240
255
|
function downloadImage() {
|
|
241
256
|
if (previewUrl) {
|
|
@@ -272,7 +287,7 @@
|
|
|
272
287
|
{:else if previewUrl}
|
|
273
288
|
<img
|
|
274
289
|
src={previewUrl}
|
|
275
|
-
alt={assetData?.alt || displayName}
|
|
290
|
+
alt={value?.alt || assetData?.alt || displayName}
|
|
276
291
|
class="h-full w-full object-cover"
|
|
277
292
|
loading="lazy"
|
|
278
293
|
/>
|
|
@@ -299,7 +314,7 @@
|
|
|
299
314
|
{:else if previewUrl}
|
|
300
315
|
<img
|
|
301
316
|
src={previewUrl}
|
|
302
|
-
alt={assetData?.alt || displayName}
|
|
317
|
+
alt={value?.alt || assetData?.alt || displayName}
|
|
303
318
|
class="h-full w-full object-cover"
|
|
304
319
|
loading="lazy"
|
|
305
320
|
/>
|
|
@@ -389,7 +404,7 @@
|
|
|
389
404
|
{:else if previewUrl}
|
|
390
405
|
<img
|
|
391
406
|
src={previewUrl}
|
|
392
|
-
alt={assetData?.alt || 'Uploaded image'}
|
|
407
|
+
alt={value?.alt || assetData?.alt || 'Uploaded image'}
|
|
393
408
|
class="h-full w-full object-contain"
|
|
394
409
|
loading="lazy"
|
|
395
410
|
/>
|
|
@@ -453,18 +468,61 @@
|
|
|
453
468
|
{/if}
|
|
454
469
|
</div>
|
|
455
470
|
|
|
456
|
-
<!-- Alt text
|
|
457
|
-
<div class="border-border space-y-
|
|
458
|
-
<
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
471
|
+
<!-- Alt text — asset-level default + optional per-placement override -->
|
|
472
|
+
<div class="border-border space-y-2 border-t p-3">
|
|
473
|
+
<div class="space-y-1.5">
|
|
474
|
+
<label class="text-sm font-medium" for="alt-{field.name}">Alt text</label>
|
|
475
|
+
<p class="text-muted-foreground text-xs">
|
|
476
|
+
Describes the image everywhere it's used (accessibility & SEO).
|
|
477
|
+
</p>
|
|
478
|
+
<Input
|
|
479
|
+
id="alt-{field.name}"
|
|
480
|
+
type="text"
|
|
481
|
+
placeholder="Describe this image..."
|
|
482
|
+
value={assetAltText}
|
|
483
|
+
oninput={(e) => updateAssetAlt(e.currentTarget.value)}
|
|
484
|
+
disabled={isReadOnly}
|
|
485
|
+
/>
|
|
486
|
+
</div>
|
|
487
|
+
|
|
488
|
+
{#if showOverride}
|
|
489
|
+
<div class="space-y-1.5">
|
|
490
|
+
<div class="flex items-center justify-between">
|
|
491
|
+
<label
|
|
492
|
+
class="text-muted-foreground text-xs font-medium"
|
|
493
|
+
for="alt-override-{field.name}">Override for this placement</label
|
|
494
|
+
>
|
|
495
|
+
{#if !isReadOnly}
|
|
496
|
+
<button
|
|
497
|
+
type="button"
|
|
498
|
+
class="text-muted-foreground hover:text-foreground text-xs underline-offset-2 hover:underline"
|
|
499
|
+
onclick={() => {
|
|
500
|
+
updateOverrideAlt('');
|
|
501
|
+
showOverride = false;
|
|
502
|
+
}}
|
|
503
|
+
>
|
|
504
|
+
Remove
|
|
505
|
+
</button>
|
|
506
|
+
{/if}
|
|
507
|
+
</div>
|
|
508
|
+
<Input
|
|
509
|
+
id="alt-override-{field.name}"
|
|
510
|
+
type="text"
|
|
511
|
+
placeholder={assetAltText || 'Describe this image...'}
|
|
512
|
+
value={value?.alt ?? ''}
|
|
513
|
+
oninput={(e) => updateOverrideAlt(e.currentTarget.value)}
|
|
514
|
+
disabled={isReadOnly}
|
|
515
|
+
/>
|
|
516
|
+
</div>
|
|
517
|
+
{:else if !isReadOnly}
|
|
518
|
+
<button
|
|
519
|
+
type="button"
|
|
520
|
+
class="text-muted-foreground hover:text-foreground text-xs underline-offset-2 hover:underline"
|
|
521
|
+
onclick={() => (showOverride = true)}
|
|
522
|
+
>
|
|
523
|
+
+ Override for this placement
|
|
524
|
+
</button>
|
|
525
|
+
{/if}
|
|
468
526
|
</div>
|
|
469
527
|
</div>
|
|
470
528
|
{:else}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ImageField.svelte.d.ts","sourceRoot":"","sources":["../../../../src/lib/components/admin/fields/ImageField.svelte.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,sBAAsB,CAAC;AACvD,OAAO,KAAK,EAAE,UAAU,IAAI,cAAc,EAAE,MAAM,wBAAwB,CAAC;AAmB1E,UAAU,KAAK;IACd,KAAK,EAAE,cAAc,CAAC;IACtB,KAAK,EAAE,UAAU,GAAG,IAAI,CAAC;IACzB,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B,QAAQ,EAAE,CAAC,KAAK,EAAE,UAAU,GAAG,IAAI,KAAK,IAAI,CAAC;IAC7C,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,cAAc,CAAC,EAAE,MAAM,CAAC;CACxB;
|
|
1
|
+
{"version":3,"file":"ImageField.svelte.d.ts","sourceRoot":"","sources":["../../../../src/lib/components/admin/fields/ImageField.svelte.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,sBAAsB,CAAC;AACvD,OAAO,KAAK,EAAE,UAAU,IAAI,cAAc,EAAE,MAAM,wBAAwB,CAAC;AAmB1E,UAAU,KAAK;IACd,KAAK,EAAE,cAAc,CAAC;IACtB,KAAK,EAAE,UAAU,GAAG,IAAI,CAAC;IACzB,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B,QAAQ,EAAE,CAAC,KAAK,EAAE,UAAU,GAAG,IAAI,KAAK,IAAI,CAAC;IAC7C,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,cAAc,CAAC,EAAE,MAAM,CAAC;CACxB;AAofF,QAAA,MAAM,UAAU,2CAAwC,CAAC;AACzD,KAAK,UAAU,GAAG,UAAU,CAAC,OAAO,UAAU,CAAC,CAAC;AAChD,eAAe,UAAU,CAAC"}
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { Button } from '@aphexcms/ui/shadcn/button';
|
|
3
|
+
import { Input } from '@aphexcms/ui/shadcn/input';
|
|
4
|
+
import { Image as ImageIcon, Upload, Trash2, X } from '@lucide/svelte';
|
|
5
|
+
import { assets } from '../../../../api/assets';
|
|
6
|
+
|
|
7
|
+
interface Props {
|
|
8
|
+
open: boolean;
|
|
9
|
+
assetRef?: string;
|
|
10
|
+
alt: string;
|
|
11
|
+
readonly?: boolean;
|
|
12
|
+
onAltChange: (alt: string) => void;
|
|
13
|
+
onReplace: () => void;
|
|
14
|
+
onRemove: () => void;
|
|
15
|
+
onClose: () => void;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
let {
|
|
19
|
+
open,
|
|
20
|
+
assetRef,
|
|
21
|
+
alt,
|
|
22
|
+
readonly = false,
|
|
23
|
+
onAltChange,
|
|
24
|
+
onReplace,
|
|
25
|
+
onRemove,
|
|
26
|
+
onClose
|
|
27
|
+
}: Props = $props();
|
|
28
|
+
|
|
29
|
+
const assetPromise = $derived(assetRef ? assets.getById(assetRef) : null);
|
|
30
|
+
|
|
31
|
+
function handleBackdropClick(e: MouseEvent) {
|
|
32
|
+
if (e.target === e.currentTarget) onClose();
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function handleKeydown(e: KeyboardEvent) {
|
|
36
|
+
if (e.key === 'Escape') onClose();
|
|
37
|
+
}
|
|
38
|
+
</script>
|
|
39
|
+
|
|
40
|
+
{#if open}
|
|
41
|
+
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
|
42
|
+
<div
|
|
43
|
+
class="bg-background/80 absolute inset-0 z-50 flex items-center justify-center p-4 lg:p-6"
|
|
44
|
+
onclick={handleBackdropClick}
|
|
45
|
+
onkeydown={handleKeydown}
|
|
46
|
+
role="button"
|
|
47
|
+
tabindex="-1"
|
|
48
|
+
>
|
|
49
|
+
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
|
50
|
+
<div
|
|
51
|
+
class="border-border bg-background w-full max-w-lg overflow-hidden rounded-lg border shadow-lg"
|
|
52
|
+
onclick={(e) => e.stopPropagation()}
|
|
53
|
+
onkeydown={(e) => e.stopPropagation()}
|
|
54
|
+
>
|
|
55
|
+
<div class="border-border flex items-center justify-between border-b px-4 py-2">
|
|
56
|
+
<span class="text-sm font-medium">Edit image</span>
|
|
57
|
+
<Button variant="ghost" size="icon" class="h-7 w-7" onclick={onClose}>
|
|
58
|
+
<X class="h-3.5 w-3.5" />
|
|
59
|
+
</Button>
|
|
60
|
+
</div>
|
|
61
|
+
|
|
62
|
+
<div class="space-y-3 p-4">
|
|
63
|
+
<!-- Preview -->
|
|
64
|
+
<div
|
|
65
|
+
class="bg-muted flex items-center justify-center overflow-hidden rounded-md"
|
|
66
|
+
style="aspect-ratio: 16 / 9;"
|
|
67
|
+
>
|
|
68
|
+
{#if assetPromise}
|
|
69
|
+
{#await assetPromise}
|
|
70
|
+
<div
|
|
71
|
+
class="border-primary h-5 w-5 animate-spin rounded-full border-2 border-t-transparent"
|
|
72
|
+
></div>
|
|
73
|
+
{:then result}
|
|
74
|
+
{#if result.success && result.data?.url}
|
|
75
|
+
<img src={result.data.url} {alt} class="h-full w-full object-contain" />
|
|
76
|
+
{:else}
|
|
77
|
+
<div class="text-muted-foreground flex flex-col items-center gap-1">
|
|
78
|
+
<ImageIcon class="h-8 w-8" />
|
|
79
|
+
<span class="text-xs">Image not found</span>
|
|
80
|
+
</div>
|
|
81
|
+
{/if}
|
|
82
|
+
{:catch}
|
|
83
|
+
<div class="text-muted-foreground flex flex-col items-center gap-1">
|
|
84
|
+
<ImageIcon class="h-8 w-8" />
|
|
85
|
+
<span class="text-xs">Failed to load</span>
|
|
86
|
+
</div>
|
|
87
|
+
{/await}
|
|
88
|
+
{:else}
|
|
89
|
+
<div class="text-muted-foreground flex flex-col items-center gap-1">
|
|
90
|
+
<ImageIcon class="h-8 w-8" />
|
|
91
|
+
<span class="text-xs">No image</span>
|
|
92
|
+
</div>
|
|
93
|
+
{/if}
|
|
94
|
+
</div>
|
|
95
|
+
|
|
96
|
+
<!-- Alt text (per-placement override of the asset's default alt) -->
|
|
97
|
+
<div class="space-y-1.5">
|
|
98
|
+
<label class="text-sm font-medium" for="image-block-alt">Alt text</label>
|
|
99
|
+
<p class="text-muted-foreground text-xs">
|
|
100
|
+
Overrides the image's default alt for this spot. Leave blank to use the default.
|
|
101
|
+
</p>
|
|
102
|
+
<Input
|
|
103
|
+
id="image-block-alt"
|
|
104
|
+
type="text"
|
|
105
|
+
placeholder="Describe this image..."
|
|
106
|
+
value={alt}
|
|
107
|
+
oninput={(e) => onAltChange(e.currentTarget.value)}
|
|
108
|
+
disabled={readonly}
|
|
109
|
+
/>
|
|
110
|
+
</div>
|
|
111
|
+
|
|
112
|
+
<!-- Actions -->
|
|
113
|
+
{#if !readonly}
|
|
114
|
+
<div class="flex items-center justify-between gap-2 pt-1">
|
|
115
|
+
<Button variant="outline" size="sm" onclick={onReplace}>
|
|
116
|
+
<Upload class="mr-1.5 h-4 w-4" />
|
|
117
|
+
Replace
|
|
118
|
+
</Button>
|
|
119
|
+
<Button
|
|
120
|
+
variant="ghost"
|
|
121
|
+
size="sm"
|
|
122
|
+
class="text-destructive hover:text-destructive"
|
|
123
|
+
onclick={onRemove}
|
|
124
|
+
>
|
|
125
|
+
<Trash2 class="mr-1.5 h-4 w-4" />
|
|
126
|
+
Remove
|
|
127
|
+
</Button>
|
|
128
|
+
</div>
|
|
129
|
+
{/if}
|
|
130
|
+
</div>
|
|
131
|
+
</div>
|
|
132
|
+
</div>
|
|
133
|
+
{/if}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
interface Props {
|
|
2
|
+
open: boolean;
|
|
3
|
+
assetRef?: string;
|
|
4
|
+
alt: string;
|
|
5
|
+
readonly?: boolean;
|
|
6
|
+
onAltChange: (alt: string) => void;
|
|
7
|
+
onReplace: () => void;
|
|
8
|
+
onRemove: () => void;
|
|
9
|
+
onClose: () => void;
|
|
10
|
+
}
|
|
11
|
+
declare const ImageBlockModal: import("svelte").Component<Props, {}, "">;
|
|
12
|
+
type ImageBlockModal = ReturnType<typeof ImageBlockModal>;
|
|
13
|
+
export default ImageBlockModal;
|
|
14
|
+
//# sourceMappingURL=ImageBlockModal.svelte.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ImageBlockModal.svelte.d.ts","sourceRoot":"","sources":["../../../../../src/lib/components/admin/fields/richtext/ImageBlockModal.svelte.ts"],"names":[],"mappings":"AASC,UAAU,KAAK;IACd,IAAI,EAAE,OAAO,CAAC;IACd,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,GAAG,EAAE,MAAM,CAAC;IACZ,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,WAAW,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,IAAI,CAAC;IACnC,SAAS,EAAE,MAAM,IAAI,CAAC;IACtB,QAAQ,EAAE,MAAM,IAAI,CAAC;IACrB,OAAO,EAAE,MAAM,IAAI,CAAC;CACpB;AAoGF,QAAA,MAAM,eAAe,2CAAwC,CAAC;AAC9D,KAAK,eAAe,GAAG,UAAU,CAAC,OAAO,eAAe,CAAC,CAAC;AAC1D,eAAe,eAAe,CAAC"}
|
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
|
|
20
20
|
<!-- svelte-ignore a11y_click_events_have_key_events -->
|
|
21
21
|
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
|
22
|
-
<div class="image-block-view group" class:selected onclick={onEdit}>
|
|
22
|
+
<div class="image-block-view group" class:selected data-pt-image-edit onclick={onEdit}>
|
|
23
23
|
{#if assetPromise}
|
|
24
24
|
{#await assetPromise}
|
|
25
25
|
<div class="image-block-placeholder">
|
|
@@ -29,7 +29,7 @@
|
|
|
29
29
|
</div>
|
|
30
30
|
{:then result}
|
|
31
31
|
{#if result.success && result.data?.url}
|
|
32
|
-
<img src={result.data.url} alt={
|
|
32
|
+
<img src={result.data.url} alt={(data?.alt as string) || ''} class="image-block-img" />
|
|
33
33
|
{:else}
|
|
34
34
|
<div class="image-block-placeholder">
|
|
35
35
|
<ImageIcon class="text-muted-foreground h-8 w-8" />
|
|
@@ -35,7 +35,9 @@
|
|
|
35
35
|
import type { ArrayField as ArrayFieldType, SchemaType } from '../../../../types/schemas';
|
|
36
36
|
import { getSchemaContext } from '../../../../schema-context.svelte';
|
|
37
37
|
import { getSchemaByName } from '../../../../schema-utils/utils';
|
|
38
|
+
import { getRichtextEditorRegistry } from '../../../../richtext-context.svelte.js';
|
|
38
39
|
import ObjectModal from '../../ObjectModal.svelte';
|
|
40
|
+
import ImageBlockModal from './ImageBlockModal.svelte';
|
|
39
41
|
import AssetBrowserModal from '../../AssetBrowserModal.svelte';
|
|
40
42
|
import * as Tooltip from '@aphexcms/ui/shadcn/tooltip';
|
|
41
43
|
import { Image as ImageIcon } from '@lucide/svelte';
|
|
@@ -66,6 +68,7 @@
|
|
|
66
68
|
let editor = $state<Editor | null>(null);
|
|
67
69
|
let editorState = $state(0);
|
|
68
70
|
let internalVersion = 0;
|
|
71
|
+
let isSyncingFromParent = false;
|
|
69
72
|
|
|
70
73
|
let expanded = $state(false);
|
|
71
74
|
let containerBounds = $state<{ top: number; left: number; width: number; height: number } | null>(
|
|
@@ -189,8 +192,9 @@
|
|
|
189
192
|
|
|
190
193
|
function handleEditBlock(attrs: { _type: string; _key: string; data: Record<string, unknown> }) {
|
|
191
194
|
if (attrs._type === 'image') {
|
|
192
|
-
|
|
193
|
-
|
|
195
|
+
editingImageKey = attrs._key;
|
|
196
|
+
editingImageData = { ...attrs.data };
|
|
197
|
+
imageModalOpen = true;
|
|
194
198
|
return;
|
|
195
199
|
}
|
|
196
200
|
const typeDef = customTypes.find((t) => t.type === attrs._type);
|
|
@@ -277,6 +281,50 @@
|
|
|
277
281
|
|
|
278
282
|
let showAssetBrowser = $state(false);
|
|
279
283
|
|
|
284
|
+
// Inline image block editing — preview + alt + replace/remove. `editingImageData`
|
|
285
|
+
// holds the block's data ({ asset, alt }); alt is persisted into the node so it
|
|
286
|
+
// serializes to the portable-text block and carries visual-editing stega.
|
|
287
|
+
let imageModalOpen = $state(false);
|
|
288
|
+
let editingImageKey = $state<string | null>(null);
|
|
289
|
+
let editingImageData = $state<Record<string, any>>({});
|
|
290
|
+
|
|
291
|
+
function applyImageData(key: string, data: Record<string, any>) {
|
|
292
|
+
if (!editor) return;
|
|
293
|
+
const existing = findNodeByKey(key);
|
|
294
|
+
if (!existing) return;
|
|
295
|
+
const { tr } = editor.state;
|
|
296
|
+
tr.setNodeMarkup(existing.pos, undefined, { _type: 'image', _key: key, data });
|
|
297
|
+
editor.view.dispatch(tr);
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
function handleImageAltChange(alt: string) {
|
|
301
|
+
if (!editingImageKey) return;
|
|
302
|
+
editingImageData = { ...editingImageData, alt: alt || undefined };
|
|
303
|
+
applyImageData(editingImageKey, editingImageData);
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
function handleImageReplace() {
|
|
307
|
+
// Hand off to the asset browser, keeping editingImageKey so the alt is preserved
|
|
308
|
+
// and the modal can re-open with the new asset.
|
|
309
|
+
editingNodeKey = editingImageKey;
|
|
310
|
+
imageModalOpen = false;
|
|
311
|
+
showAssetBrowser = true;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
function handleImageRemove() {
|
|
315
|
+
const key = editingImageKey;
|
|
316
|
+
imageModalOpen = false;
|
|
317
|
+
editingImageKey = null;
|
|
318
|
+
editingImageData = {};
|
|
319
|
+
if (key) handleDeleteBlock(key);
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
function handleImageModalClose() {
|
|
323
|
+
imageModalOpen = false;
|
|
324
|
+
editingImageKey = null;
|
|
325
|
+
editingImageData = {};
|
|
326
|
+
}
|
|
327
|
+
|
|
280
328
|
function handleInsertImageBlock() {
|
|
281
329
|
editingNodeKey = null;
|
|
282
330
|
insertMenuOpen = false;
|
|
@@ -286,13 +334,17 @@
|
|
|
286
334
|
function handleAssetSelected(asset: any) {
|
|
287
335
|
if (!editor || !asset) return;
|
|
288
336
|
|
|
337
|
+
const newAssetRef = { _type: 'reference', _ref: asset.id };
|
|
289
338
|
const existing = editingNodeKey ? findNodeByKey(editingNodeKey) : null;
|
|
290
339
|
if (existing) {
|
|
340
|
+
// Replace — merge into existing data so the block's alt survives the swap.
|
|
341
|
+
const prevData = (existing.node.attrs?.data as Record<string, any>) ?? {};
|
|
342
|
+
const merged = { ...prevData, asset: newAssetRef };
|
|
291
343
|
const { tr } = editor.state;
|
|
292
344
|
tr.setNodeMarkup(existing.pos, undefined, {
|
|
293
345
|
_type: 'image',
|
|
294
346
|
_key: editingNodeKey,
|
|
295
|
-
data:
|
|
347
|
+
data: merged
|
|
296
348
|
});
|
|
297
349
|
editor.view.dispatch(tr);
|
|
298
350
|
} else {
|
|
@@ -304,7 +356,7 @@
|
|
|
304
356
|
attrs: {
|
|
305
357
|
_type: 'image',
|
|
306
358
|
_key: genKey(),
|
|
307
|
-
data: { asset:
|
|
359
|
+
data: { asset: newAssetRef }
|
|
308
360
|
}
|
|
309
361
|
})
|
|
310
362
|
.run();
|
|
@@ -312,6 +364,14 @@
|
|
|
312
364
|
|
|
313
365
|
showAssetBrowser = false;
|
|
314
366
|
editingNodeKey = null;
|
|
367
|
+
|
|
368
|
+
// If the browser was opened from the image modal (a replace), re-open it on the
|
|
369
|
+
// updated block so the user can keep editing the alt.
|
|
370
|
+
if (editingImageKey) {
|
|
371
|
+
const updated = findNodeByKey(editingImageKey);
|
|
372
|
+
editingImageData = (updated?.node.attrs?.data as Record<string, any>) ?? {};
|
|
373
|
+
imageModalOpen = true;
|
|
374
|
+
}
|
|
315
375
|
}
|
|
316
376
|
|
|
317
377
|
function handleModalUpdate(updatedData: Record<string, any>) {
|
|
@@ -440,6 +500,7 @@
|
|
|
440
500
|
extensions,
|
|
441
501
|
content: tiptapDoc,
|
|
442
502
|
onUpdate: ({ editor: e }) => {
|
|
503
|
+
if (isSyncingFromParent) return;
|
|
443
504
|
internalVersion++;
|
|
444
505
|
const json = e.getJSON();
|
|
445
506
|
const pt = tiptapToPortableText(json);
|
|
@@ -495,14 +556,18 @@
|
|
|
495
556
|
annotationModalOpen = true;
|
|
496
557
|
}
|
|
497
558
|
|
|
559
|
+
const editorRegistry = getRichtextEditorRegistry();
|
|
560
|
+
|
|
498
561
|
onMount(() => {
|
|
499
562
|
createEditor();
|
|
563
|
+
if (editor) editorRegistry?.set(field.name, { editor, openLinkPopover: showLinkPreview });
|
|
500
564
|
document.addEventListener('click', handleClickOutside);
|
|
501
565
|
document.addEventListener('keydown', handleKeydown);
|
|
502
566
|
element?.addEventListener('click', handleAnnotationClick);
|
|
503
567
|
});
|
|
504
568
|
|
|
505
569
|
onDestroy(() => {
|
|
570
|
+
editorRegistry?.delete(field.name);
|
|
506
571
|
editor?.destroy();
|
|
507
572
|
document.removeEventListener('click', handleClickOutside);
|
|
508
573
|
document.removeEventListener('keydown', handleKeydown);
|
|
@@ -534,6 +599,33 @@
|
|
|
534
599
|
// own change back" vs "parent loaded a different document."
|
|
535
600
|
let lastSyncedVersion = 0;
|
|
536
601
|
let editorJustCreated = false;
|
|
602
|
+
|
|
603
|
+
// Link marks round-trip lossy: only `href` is persisted to Portable Text, so a
|
|
604
|
+
// rehydrated link drops tiptap's presentation attrs (e.g. `class: 'richtext-link'`
|
|
605
|
+
// → null). Strip those volatile attrs before the equality check below, otherwise an
|
|
606
|
+
// identical round-trip looks "changed", replaces the doc, and collapses the caret to
|
|
607
|
+
// the end — yanking it out of a link the user just clicked in presentation mode.
|
|
608
|
+
function stripVolatileMarks(node: unknown): unknown {
|
|
609
|
+
if (Array.isArray(node)) return node.map(stripVolatileMarks);
|
|
610
|
+
if (node && typeof node === 'object') {
|
|
611
|
+
const obj = node as Record<string, unknown>;
|
|
612
|
+
const out: Record<string, unknown> = {};
|
|
613
|
+
for (const key in obj) {
|
|
614
|
+
if (key === 'marks' && Array.isArray(obj.marks)) {
|
|
615
|
+
out.marks = (obj.marks as Array<Record<string, unknown>>).map((m) =>
|
|
616
|
+
m?.type === 'link'
|
|
617
|
+
? { type: 'link', attrs: { href: (m.attrs as { href?: unknown })?.href ?? '' } }
|
|
618
|
+
: m
|
|
619
|
+
);
|
|
620
|
+
} else {
|
|
621
|
+
out[key] = stripVolatileMarks(obj[key]);
|
|
622
|
+
}
|
|
623
|
+
}
|
|
624
|
+
return out;
|
|
625
|
+
}
|
|
626
|
+
return node;
|
|
627
|
+
}
|
|
628
|
+
|
|
537
629
|
$effect(() => {
|
|
538
630
|
const v = value;
|
|
539
631
|
if (!editor) return;
|
|
@@ -545,12 +637,24 @@
|
|
|
545
637
|
lastSyncedVersion = internalVersion;
|
|
546
638
|
return;
|
|
547
639
|
}
|
|
548
|
-
//
|
|
640
|
+
// Skip when the incoming value would produce identical content. The parent
|
|
641
|
+
// can hand us a fresh array reference with the same data (e.g. the live-
|
|
642
|
+
// preview round-trip in presentation mode reassigns documentData); replacing
|
|
643
|
+
// the doc in that case needlessly collapses the selection to the end — which
|
|
644
|
+
// reads as "the cursor jumps to the bottom" mid-edit.
|
|
549
645
|
const tiptapDoc = portableTextToTiptap(v || []);
|
|
646
|
+
if (
|
|
647
|
+
JSON.stringify(stripVolatileMarks(tiptapDoc)) ===
|
|
648
|
+
JSON.stringify(stripVolatileMarks(editor.getJSON()))
|
|
649
|
+
)
|
|
650
|
+
return;
|
|
651
|
+
// Replace content without polluting undo history
|
|
550
652
|
const newDoc = editor.state.schema.nodeFromJSON(tiptapDoc);
|
|
551
653
|
const tr = editor.state.tr.replaceWith(0, editor.state.doc.content.size, newDoc.content);
|
|
552
654
|
tr.setMeta('addToHistory', false);
|
|
655
|
+
isSyncingFromParent = true;
|
|
553
656
|
editor.view.dispatch(tr);
|
|
657
|
+
isSyncingFromParent = false;
|
|
554
658
|
});
|
|
555
659
|
|
|
556
660
|
$effect(() => {
|
|
@@ -585,15 +689,45 @@
|
|
|
585
689
|
let linkPopoverPos = $state({ top: 0, left: 0 });
|
|
586
690
|
let linkPreviewHref = $state('');
|
|
587
691
|
|
|
692
|
+
// Render the link popover at <body> level. The editor can sit inside an ancestor with a
|
|
693
|
+
// `transform` (e.g. the sliding responsive document panel), which would make the popover's
|
|
694
|
+
// `position: fixed` resolve against that ancestor instead of the viewport — placing it
|
|
695
|
+
// off-screen. Portaling escapes any such containing block / overflow clip.
|
|
696
|
+
function portal(node: HTMLElement) {
|
|
697
|
+
document.body.appendChild(node);
|
|
698
|
+
return {
|
|
699
|
+
destroy() {
|
|
700
|
+
node.remove();
|
|
701
|
+
}
|
|
702
|
+
};
|
|
703
|
+
}
|
|
704
|
+
|
|
588
705
|
function positionLinkPopover() {
|
|
589
|
-
if (!editor
|
|
706
|
+
if (!editor) return;
|
|
590
707
|
const { from, to } = editor.state.selection;
|
|
591
708
|
const start = editor.view.coordsAtPos(from);
|
|
592
709
|
const end = editor.view.coordsAtPos(to);
|
|
593
|
-
|
|
710
|
+
|
|
711
|
+
// The popover is `position: fixed`, so it works in viewport coordinates — `coordsAtPos`
|
|
712
|
+
// already returns those. This sidesteps the editor's scroll/overflow container (e.g. the
|
|
713
|
+
// split form panel in visual-editing mode), which would otherwise clip or shift an
|
|
714
|
+
// absolutely-positioned child.
|
|
715
|
+
const POPOVER_H = 44;
|
|
716
|
+
const POPOVER_W = 320; // approx; only used to keep the popover off the right edge
|
|
717
|
+
const GAP = 4;
|
|
718
|
+
const MARGIN = 8;
|
|
719
|
+
|
|
720
|
+
// Below the selection by default; flip above when it would spill past the viewport bottom.
|
|
721
|
+
const roomBelow = end.bottom + GAP + POPOVER_H <= window.innerHeight - MARGIN;
|
|
722
|
+
const roomAbove = start.top - GAP - POPOVER_H >= MARGIN;
|
|
723
|
+
const placeBelow = roomBelow || !roomAbove;
|
|
724
|
+
|
|
594
725
|
linkPopoverPos = {
|
|
595
|
-
top: end.bottom
|
|
596
|
-
left: Math.
|
|
726
|
+
top: placeBelow ? end.bottom + GAP : start.top - GAP - POPOVER_H,
|
|
727
|
+
left: Math.min(
|
|
728
|
+
Math.max(MARGIN, Math.min(start.left, end.left)),
|
|
729
|
+
window.innerWidth - POPOVER_W - MARGIN
|
|
730
|
+
)
|
|
597
731
|
};
|
|
598
732
|
}
|
|
599
733
|
|
|
@@ -850,14 +984,12 @@
|
|
|
850
984
|
Strikethrough
|
|
851
985
|
)}
|
|
852
986
|
{/if}
|
|
853
|
-
{
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
)}
|
|
860
|
-
{/if}
|
|
987
|
+
{@render toolbarBtn(
|
|
988
|
+
() => editor?.chain().focus().toggleCodeBlock().run(),
|
|
989
|
+
'Code block',
|
|
990
|
+
() => editor?.isActive('codeBlock') ?? false,
|
|
991
|
+
Code
|
|
992
|
+
)}
|
|
861
993
|
|
|
862
994
|
{#if (hasDecorator('strong') || hasDecorator('em')) && (hasList('bullet') || hasList('number') || hasLink)}
|
|
863
995
|
<div class="bg-border mx-1 h-4 w-px"></div>
|
|
@@ -1016,7 +1148,8 @@
|
|
|
1016
1148
|
|
|
1017
1149
|
{#if linkMode === 'preview'}
|
|
1018
1150
|
<div
|
|
1019
|
-
|
|
1151
|
+
use:portal
|
|
1152
|
+
class="border-rule bg-popover fixed z-[60] flex items-center gap-1.5 rounded-md border px-2 py-1.5 shadow-lg"
|
|
1020
1153
|
style="top: {linkPopoverPos.top}px; left: {linkPopoverPos.left}px;"
|
|
1021
1154
|
>
|
|
1022
1155
|
<a
|
|
@@ -1058,7 +1191,8 @@
|
|
|
1058
1191
|
</div>
|
|
1059
1192
|
{:else if linkMode === 'edit'}
|
|
1060
1193
|
<div
|
|
1061
|
-
|
|
1194
|
+
use:portal
|
|
1195
|
+
class="border-rule bg-popover fixed z-[60] flex items-center gap-2 rounded-md border px-3 py-2 shadow-lg"
|
|
1062
1196
|
style="top: {linkPopoverPos.top}px; left: {linkPopoverPos.left}px;"
|
|
1063
1197
|
>
|
|
1064
1198
|
<LinkIcon class="text-muted-foreground h-4 w-4 shrink-0" />
|
|
@@ -1137,6 +1271,19 @@
|
|
|
1137
1271
|
onSelect={handleAssetSelected}
|
|
1138
1272
|
/>
|
|
1139
1273
|
|
|
1274
|
+
{#if imageModalOpen}
|
|
1275
|
+
<ImageBlockModal
|
|
1276
|
+
open={imageModalOpen}
|
|
1277
|
+
assetRef={editingImageData?.asset?._ref}
|
|
1278
|
+
alt={(editingImageData?.alt as string) ?? ''}
|
|
1279
|
+
{readonly}
|
|
1280
|
+
onAltChange={handleImageAltChange}
|
|
1281
|
+
onReplace={handleImageReplace}
|
|
1282
|
+
onRemove={handleImageRemove}
|
|
1283
|
+
onClose={handleImageModalClose}
|
|
1284
|
+
/>
|
|
1285
|
+
{/if}
|
|
1286
|
+
|
|
1140
1287
|
<style>
|
|
1141
1288
|
.richtext-editor {
|
|
1142
1289
|
position: relative;
|
|
@@ -1251,6 +1398,22 @@
|
|
|
1251
1398
|
margin: 0.25em 0;
|
|
1252
1399
|
}
|
|
1253
1400
|
|
|
1401
|
+
.richtext-content :global(.tiptap pre) {
|
|
1402
|
+
background: var(--muted);
|
|
1403
|
+
border-radius: 6px;
|
|
1404
|
+
padding: 0.75rem 1rem;
|
|
1405
|
+
margin: 0.75em 0;
|
|
1406
|
+
overflow-x: auto;
|
|
1407
|
+
}
|
|
1408
|
+
|
|
1409
|
+
.richtext-content :global(.tiptap pre code) {
|
|
1410
|
+
background: none;
|
|
1411
|
+
padding: 0;
|
|
1412
|
+
border-radius: 0;
|
|
1413
|
+
font-size: 0.875em;
|
|
1414
|
+
font-family: var(--font-mono, monospace);
|
|
1415
|
+
}
|
|
1416
|
+
|
|
1254
1417
|
.richtext-content :global(.tiptap code) {
|
|
1255
1418
|
background: var(--muted);
|
|
1256
1419
|
border-radius: 3px;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"RichtextField.svelte.d.ts","sourceRoot":"","sources":["../../../../../src/lib/components/admin/fields/richtext/RichtextField.svelte.ts"],"names":[],"mappings":"AA4BA,OAAO,EAGL,KAAK,iBAAiB,EACtB,MAAM,4BAA4B,CAAC;AAIrC,OAAO,KAAK,EAAE,UAAU,IAAI,cAAc,EAAc,MAAM,2BAA2B,CAAC;
|
|
1
|
+
{"version":3,"file":"RichtextField.svelte.d.ts","sourceRoot":"","sources":["../../../../../src/lib/components/admin/fields/richtext/RichtextField.svelte.ts"],"names":[],"mappings":"AA4BA,OAAO,EAGL,KAAK,iBAAiB,EACtB,MAAM,4BAA4B,CAAC;AAIrC,OAAO,KAAK,EAAE,UAAU,IAAI,cAAc,EAAc,MAAM,2BAA2B,CAAC;AAWzF,UAAU,KAAK;IACd,KAAK,EAAE,cAAc,CAAC;IACtB,KAAK,EAAE,iBAAiB,GAAG,IAAI,GAAG,SAAS,CAAC;IAC5C,QAAQ,EAAE,CAAC,KAAK,EAAE,iBAAiB,KAAK,IAAI,CAAC;IAC7C,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,eAAe,CAAC,EAAE,CAAC,UAAU,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,KAAK,IAAI,CAAC;IACrE,cAAc,CAAC,EAAE,MAAM,CAAC;CACxB;AAymCF,QAAA,MAAM,aAAa,2CAAwC,CAAC;AAC5D,KAAK,aAAa,GAAG,UAAU,CAAC,OAAO,aAAa,CAAC,CAAC;AACtD,eAAe,aAAa,CAAC"}
|