@glw907/cairn-cms 0.18.0 → 0.21.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/dist/components/DeleteDialog.svelte +81 -0
- package/dist/components/DeleteDialog.svelte.d.ts +21 -0
- package/dist/components/DeleteDialog.svelte.d.ts.map +1 -0
- package/dist/components/EditPage.svelte +127 -8
- package/dist/components/EditPage.svelte.d.ts +8 -0
- package/dist/components/EditPage.svelte.d.ts.map +1 -1
- package/dist/components/LinkPicker.svelte +109 -0
- package/dist/components/LinkPicker.svelte.d.ts +18 -0
- package/dist/components/LinkPicker.svelte.d.ts.map +1 -0
- package/dist/components/MarkdownEditor.svelte +33 -3
- package/dist/components/MarkdownEditor.svelte.d.ts +5 -0
- package/dist/components/MarkdownEditor.svelte.d.ts.map +1 -1
- package/dist/components/RenameDialog.svelte +72 -0
- package/dist/components/RenameDialog.svelte.d.ts +20 -0
- package/dist/components/RenameDialog.svelte.d.ts.map +1 -0
- package/dist/components/index.d.ts +3 -0
- package/dist/components/index.d.ts.map +1 -1
- package/dist/components/index.js +3 -0
- package/dist/components/link-completion.d.ts +16 -0
- package/dist/components/link-completion.d.ts.map +1 -0
- package/dist/components/link-completion.js +48 -0
- package/dist/components/markdown-format.d.ts +25 -5
- package/dist/components/markdown-format.d.ts.map +1 -1
- package/dist/components/markdown-format.js +85 -0
- package/dist/content/ids.d.ts +7 -0
- package/dist/content/ids.d.ts.map +1 -1
- package/dist/content/ids.js +11 -0
- package/dist/content/links.d.ts +7 -0
- package/dist/content/links.d.ts.map +1 -1
- package/dist/content/links.js +11 -0
- package/dist/content/manifest.d.ts +15 -1
- package/dist/content/manifest.d.ts.map +1 -1
- package/dist/content/manifest.js +45 -3
- package/dist/delivery/manifest.d.ts.map +1 -1
- package/dist/delivery/manifest.js +7 -0
- package/dist/github/repo.d.ts.map +1 -1
- package/dist/github/repo.js +8 -1
- package/dist/index.d.ts +3 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -2
- package/dist/sveltekit/content-routes.d.ts +11 -2
- package/dist/sveltekit/content-routes.d.ts.map +1 -1
- package/dist/sveltekit/content-routes.js +157 -9
- package/package.json +2 -1
- package/src/lib/components/DeleteDialog.svelte +81 -0
- package/src/lib/components/EditPage.svelte +127 -8
- package/src/lib/components/LinkPicker.svelte +109 -0
- package/src/lib/components/MarkdownEditor.svelte +33 -3
- package/src/lib/components/RenameDialog.svelte +72 -0
- package/src/lib/components/index.ts +3 -0
- package/src/lib/components/link-completion.ts +57 -0
- package/src/lib/components/markdown-format.ts +82 -0
- package/src/lib/content/ids.ts +12 -0
- package/src/lib/content/links.ts +13 -0
- package/src/lib/content/manifest.ts +55 -3
- package/src/lib/delivery/manifest.ts +6 -0
- package/src/lib/github/repo.ts +8 -1
- package/src/lib/index.ts +3 -2
- package/src/lib/sveltekit/content-routes.ts +178 -11
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
@component
|
|
3
|
+
The Delete control and its modal. With no inbound links it is a plain confirm that posts to the
|
|
4
|
+
?/delete action. With inbound links it blocks: it names how many entries link here and lists them,
|
|
5
|
+
each linking to its edit page, so the author repoints or removes those links first. Built on a native
|
|
6
|
+
<dialog>, following the LinkPicker a11y conventions.
|
|
7
|
+
-->
|
|
8
|
+
<script lang="ts">
|
|
9
|
+
import type { InboundLink } from '../content/manifest.js';
|
|
10
|
+
|
|
11
|
+
interface Props {
|
|
12
|
+
/** The concept this entry belongs to, e.g. "posts". Posted with the confirm. */
|
|
13
|
+
conceptId: string;
|
|
14
|
+
/** The entry id within its concept. Posted with the confirm. */
|
|
15
|
+
id: string;
|
|
16
|
+
/** A human label for the concept, e.g. "Post", used in the prompts. */
|
|
17
|
+
label: string;
|
|
18
|
+
/** The entries that link to this one; non-empty blocks the delete. */
|
|
19
|
+
inboundLinks: InboundLink[];
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
let { conceptId, id, label, inboundLinks }: Props = $props();
|
|
23
|
+
|
|
24
|
+
let dialog = $state<HTMLDialogElement | null>(null);
|
|
25
|
+
const blocked = $derived(inboundLinks.length > 0);
|
|
26
|
+
const noun = $derived(label.toLowerCase());
|
|
27
|
+
// One inbound link reads "1 post links here ... repoint it"; many reads "2 posts link here ...
|
|
28
|
+
// repoint them". The subject-verb agreement inverts the usual plural-s, so derive each form once.
|
|
29
|
+
const single = $derived(inboundLinks.length === 1);
|
|
30
|
+
const nouns = $derived(single ? noun : `${noun}s`);
|
|
31
|
+
const verb = $derived(single ? 'links' : 'link');
|
|
32
|
+
const pronoun = $derived(single ? 'it' : 'them');
|
|
33
|
+
|
|
34
|
+
function open() {
|
|
35
|
+
dialog?.showModal();
|
|
36
|
+
}
|
|
37
|
+
function close() {
|
|
38
|
+
dialog?.close();
|
|
39
|
+
}
|
|
40
|
+
</script>
|
|
41
|
+
|
|
42
|
+
<button type="button" class="btn btn-sm btn-ghost text-error" aria-haspopup="dialog" onclick={open}>
|
|
43
|
+
Delete
|
|
44
|
+
</button>
|
|
45
|
+
|
|
46
|
+
<dialog class="modal" aria-labelledby="cairn-delete-dialog-title" bind:this={dialog}>
|
|
47
|
+
<div class="modal-box">
|
|
48
|
+
<div class="mb-3 flex items-center justify-between">
|
|
49
|
+
<h2 id="cairn-delete-dialog-title" class="text-base font-semibold">Delete this {label.toLowerCase()}?</h2>
|
|
50
|
+
<button type="button" class="btn btn-ghost btn-sm" aria-label="Close" onclick={close}>✕</button>
|
|
51
|
+
</div>
|
|
52
|
+
|
|
53
|
+
{#if blocked}
|
|
54
|
+
<p class="mb-2 text-sm">
|
|
55
|
+
{inboundLinks.length} {nouns} {verb} here. Remove or repoint {pronoun} before deleting, so no link is left
|
|
56
|
+
broken.
|
|
57
|
+
</p>
|
|
58
|
+
<ul class="menu w-full">
|
|
59
|
+
{#each inboundLinks as link (link.concept + '/' + link.id)}
|
|
60
|
+
<li>
|
|
61
|
+
<a href={`/admin/${link.concept}/${link.id}`}>{link.title}</a>
|
|
62
|
+
</li>
|
|
63
|
+
{/each}
|
|
64
|
+
</ul>
|
|
65
|
+
<div class="mt-3 flex justify-end">
|
|
66
|
+
<button type="button" class="btn btn-sm" onclick={close}>Close</button>
|
|
67
|
+
</div>
|
|
68
|
+
{:else}
|
|
69
|
+
<p class="mb-3 text-sm">This cannot be undone.</p>
|
|
70
|
+
<form method="POST" action="?/delete" class="flex justify-end gap-2">
|
|
71
|
+
<input type="hidden" name="concept" value={conceptId} />
|
|
72
|
+
<input type="hidden" name="id" value={id} />
|
|
73
|
+
<button type="button" class="btn btn-sm" onclick={close}>Cancel</button>
|
|
74
|
+
<button type="submit" class="btn btn-sm btn-error">Delete this {label.toLowerCase()}</button>
|
|
75
|
+
</form>
|
|
76
|
+
{/if}
|
|
77
|
+
</div>
|
|
78
|
+
<form method="dialog" class="modal-backdrop">
|
|
79
|
+
<button tabindex="-1" aria-label="Close">close</button>
|
|
80
|
+
</form>
|
|
81
|
+
</dialog>
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { InboundLink } from '../content/manifest.js';
|
|
2
|
+
interface Props {
|
|
3
|
+
/** The concept this entry belongs to, e.g. "posts". Posted with the confirm. */
|
|
4
|
+
conceptId: string;
|
|
5
|
+
/** The entry id within its concept. Posted with the confirm. */
|
|
6
|
+
id: string;
|
|
7
|
+
/** A human label for the concept, e.g. "Post", used in the prompts. */
|
|
8
|
+
label: string;
|
|
9
|
+
/** The entries that link to this one; non-empty blocks the delete. */
|
|
10
|
+
inboundLinks: InboundLink[];
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* The Delete control and its modal. With no inbound links it is a plain confirm that posts to the
|
|
14
|
+
* ?/delete action. With inbound links it blocks: it names how many entries link here and lists them,
|
|
15
|
+
* each linking to its edit page, so the author repoints or removes those links first. Built on a native
|
|
16
|
+
* <dialog>, following the LinkPicker a11y conventions.
|
|
17
|
+
*/
|
|
18
|
+
declare const DeleteDialog: import("svelte").Component<Props, {}, "">;
|
|
19
|
+
type DeleteDialog = ReturnType<typeof DeleteDialog>;
|
|
20
|
+
export default DeleteDialog;
|
|
21
|
+
//# sourceMappingURL=DeleteDialog.svelte.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"DeleteDialog.svelte.d.ts","sourceRoot":"","sources":["../../src/lib/components/DeleteDialog.svelte.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,wBAAwB,CAAC;AAGxD,UAAU,KAAK;IACb,gFAAgF;IAChF,SAAS,EAAE,MAAM,CAAC;IAClB,gEAAgE;IAChE,EAAE,EAAE,MAAM,CAAC;IACX,uEAAuE;IACvE,KAAK,EAAE,MAAM,CAAC;IACd,sEAAsE;IACtE,YAAY,EAAE,WAAW,EAAE,CAAC;CAC7B;AAqEH;;;;;GAKG;AACH,QAAA,MAAM,YAAY,2CAAwC,CAAC;AAC3D,KAAK,YAAY,GAAG,UAAU,CAAC,OAAO,YAAY,CAAC,CAAC;AACpD,eAAe,YAAY,CAAC"}
|
|
@@ -8,6 +8,11 @@ markdown editor and a live, design-accurate preview. The whole surface is one fo
|
|
|
8
8
|
import { untrack } from 'svelte';
|
|
9
9
|
import MarkdownEditor from './MarkdownEditor.svelte';
|
|
10
10
|
import ComponentInsertDialog from './ComponentInsertDialog.svelte';
|
|
11
|
+
import LinkPicker from './LinkPicker.svelte';
|
|
12
|
+
import DeleteDialog from './DeleteDialog.svelte';
|
|
13
|
+
import RenameDialog from './RenameDialog.svelte';
|
|
14
|
+
import { cairnLinkCompletionSource } from './link-completion.js';
|
|
15
|
+
import { unwrapCairnLink } from './markdown-format.js';
|
|
11
16
|
import type { ComponentRegistry } from '../render/registry.js';
|
|
12
17
|
import type { IconSet } from '../render/glyph.js';
|
|
13
18
|
import type { EditData } from '../sveltekit/content-routes.js';
|
|
@@ -24,21 +29,86 @@ markdown editor and a live, design-accurate preview. The whole surface is one fo
|
|
|
24
29
|
render?: (md: string, opts?: { stagger?: boolean; resolve?: LinkResolve }) => string | Promise<string>;
|
|
25
30
|
/** The site's icon set, for the guided form's icon fields. */
|
|
26
31
|
icons?: IconSet;
|
|
32
|
+
/** The `?/save` or `?/delete` action result. Carries the save guard's broken links when a save was
|
|
33
|
+
* blocked, or the delete guard's inbound linkers when a delete was refused. */
|
|
34
|
+
form?: { brokenLinks?: string[]; body?: string; inboundLinks?: import('../content/manifest.js').InboundLink[]; renameError?: string } | null;
|
|
27
35
|
}
|
|
28
36
|
|
|
29
|
-
let { data, registry, render, icons }: Props = $props();
|
|
37
|
+
let { data, registry, render, icons, form }: Props = $props();
|
|
30
38
|
|
|
31
|
-
// `body` is local editor state seeded once
|
|
32
|
-
//
|
|
33
|
-
|
|
39
|
+
// `body` is local editor state seeded once; it diverges as the user types. A blocked save returns
|
|
40
|
+
// the author's edited markdown as form.body, so seed from that when present to keep the edits and
|
|
41
|
+
// the broken link they were told to fix. On the success and delete-refused paths form carries no
|
|
42
|
+
// body, so it falls back to the committed data.body. untrack() captures the initial value without
|
|
43
|
+
// subscribing to future prop changes.
|
|
44
|
+
let body = $state(untrack(() => form?.body ?? data.body));
|
|
34
45
|
let showPreview = $state(false);
|
|
35
46
|
let previewHtml = $state('');
|
|
36
47
|
let insert = $state.raw<(text: string) => void>(() => {});
|
|
48
|
+
let insertLink = $state.raw<(href: string, title: string) => void>(() => {});
|
|
49
|
+
|
|
50
|
+
// The save guard's broken links, from the blocked action result. The fix unwraps a link in the
|
|
51
|
+
// local body, which the bound editor reconciles, so the author re-saves clean.
|
|
52
|
+
const brokenLinks = $derived(form?.brokenLinks ?? []);
|
|
53
|
+
// Track the hrefs the author has already fixed this session. The banner reads the immutable action
|
|
54
|
+
// result, so without this a fixed row would linger and "Remove link" would read as a no-op.
|
|
55
|
+
let removedLinks = $state<string[]>([]);
|
|
56
|
+
const visibleBrokenLinks = $derived(brokenLinks.filter((h) => !removedLinks.includes(h)));
|
|
57
|
+
function removeBrokenLink(href: string) {
|
|
58
|
+
// Hide the row only when the unwrap changed the body. A genuine no-op keeps the row honest.
|
|
59
|
+
const next = unwrapCairnLink(body, href);
|
|
60
|
+
if (next !== body) {
|
|
61
|
+
body = next;
|
|
62
|
+
removedLinks = [...removedLinks, href];
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// The delete guard's inbound linkers, from a refused delete (fail 409). Empty when the delete was
|
|
67
|
+
// not refused. When set, a delete was blocked by a link that appeared since the page loaded.
|
|
68
|
+
const deleteRefusedLinks = $derived(form?.inboundLinks ?? []);
|
|
69
|
+
|
|
70
|
+
// A rename that hit a collision or an invalid slug returns form.renameError.
|
|
71
|
+
const renameError = $derived(form?.renameError ?? '');
|
|
72
|
+
|
|
73
|
+
// After a save that links to a draft target, the redirect carries ?drafts=<tokens>.
|
|
74
|
+
let draftWarning = $state('');
|
|
75
|
+
$effect(() => {
|
|
76
|
+
const search = typeof location === 'undefined' ? '' : location.search;
|
|
77
|
+
const drafts = new URLSearchParams(search).get('drafts');
|
|
78
|
+
draftWarning = drafts ? drafts.split(',').filter(Boolean).join(', ') : '';
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
// One persistent live region announces the current message, since a {#if}-gated role element
|
|
82
|
+
// inserted fresh is announced inconsistently. A polite region carries the success and draft
|
|
83
|
+
// notices; an assertive region carries the errors. The visible banners below keep their styling
|
|
84
|
+
// but drop their roles, so a message is announced once.
|
|
85
|
+
const politeMessage = $derived.by(() => {
|
|
86
|
+
if (draftWarning) return `Saved. This page links to unpublished pages: ${draftWarning}.`;
|
|
87
|
+
if (data.saved) return 'Saved.';
|
|
88
|
+
if (data.renamed) return `The URL is now ${data.slug}.`;
|
|
89
|
+
return '';
|
|
90
|
+
});
|
|
91
|
+
const assertiveMessage = $derived.by(() => {
|
|
92
|
+
if (data.error) return data.error;
|
|
93
|
+
if (renameError) return renameError;
|
|
94
|
+
if (deleteRefusedLinks.length) {
|
|
95
|
+
const count = deleteRefusedLinks.length;
|
|
96
|
+
return `This ${data.label.toLowerCase()} could not be deleted. ${count} ${count === 1 ? 'page links' : 'pages link'} to it.`;
|
|
97
|
+
}
|
|
98
|
+
if (visibleBrokenLinks.length) {
|
|
99
|
+
const count = visibleBrokenLinks.length;
|
|
100
|
+
return `This page links to ${count} missing ${count === 1 ? 'page' : 'pages'}.`;
|
|
101
|
+
}
|
|
102
|
+
return '';
|
|
103
|
+
});
|
|
37
104
|
|
|
38
105
|
// The manifest-backed resolver turns a cairn: link into its live permalink in the preview, and
|
|
39
106
|
// returns undefined for a missing target so the render step marks it cairn-broken-link.
|
|
40
107
|
const resolveLink = $derived(manifestLinkResolver(data.linkTargets));
|
|
41
108
|
|
|
109
|
+
// The [[ autocomplete source over the same link targets, handed to the editor's generic seam.
|
|
110
|
+
const completionSources = $derived([cairnLinkCompletionSource(data.linkTargets)]);
|
|
111
|
+
|
|
42
112
|
const PREVIEW_KEY = 'cairn-admin:preview';
|
|
43
113
|
|
|
44
114
|
$effect(() => {
|
|
@@ -85,6 +155,9 @@ markdown editor and a live, design-accurate preview. The whole surface is one fo
|
|
|
85
155
|
</div>
|
|
86
156
|
<div class="flex items-center gap-2">
|
|
87
157
|
<ComponentInsertDialog {registry} {insert} {icons} />
|
|
158
|
+
<LinkPicker linkTargets={data.linkTargets} insert={insertLink} />
|
|
159
|
+
<RenameDialog conceptId={data.conceptId} id={data.id} label={data.label} slug={data.slug} />
|
|
160
|
+
<DeleteDialog conceptId={data.conceptId} id={data.id} label={data.label} inboundLinks={data.inboundLinks} />
|
|
88
161
|
<button
|
|
89
162
|
type="button"
|
|
90
163
|
class="btn btn-sm btn-ghost"
|
|
@@ -97,11 +170,51 @@ markdown editor and a live, design-accurate preview. The whole surface is one fo
|
|
|
97
170
|
</div>
|
|
98
171
|
</header>
|
|
99
172
|
|
|
100
|
-
{
|
|
101
|
-
|
|
173
|
+
<div class="sr-only" aria-live="polite">{politeMessage}</div>
|
|
174
|
+
<div class="sr-only" aria-live="assertive">{assertiveMessage}</div>
|
|
175
|
+
|
|
176
|
+
{#if data.saved && !draftWarning}
|
|
177
|
+
<div class="alert alert-success mb-4 text-sm">Saved.</div>
|
|
178
|
+
{/if}
|
|
179
|
+
{#if data.renamed}
|
|
180
|
+
<div class="alert alert-success mb-4 text-sm">The URL is now {data.slug}.</div>
|
|
102
181
|
{/if}
|
|
103
182
|
{#if data.error}
|
|
104
|
-
<div
|
|
183
|
+
<div class="alert alert-error mb-4 text-sm">{data.error}</div>
|
|
184
|
+
{/if}
|
|
185
|
+
{#if renameError}
|
|
186
|
+
<div class="alert alert-error mb-4 text-sm">{renameError}</div>
|
|
187
|
+
{/if}
|
|
188
|
+
{#if deleteRefusedLinks.length}
|
|
189
|
+
<div class="alert alert-error mb-4 flex-col items-start text-sm">
|
|
190
|
+
<p class="font-medium">This {data.label.toLowerCase()} could not be deleted.</p>
|
|
191
|
+
<p>{deleteRefusedLinks.length} {deleteRefusedLinks.length === 1 ? 'page' : 'pages'} now link to it. Remove or repoint the {deleteRefusedLinks.length === 1 ? 'link' : 'links'} listed below, then delete again.</p>
|
|
192
|
+
<ul class="mt-1 w-full">
|
|
193
|
+
{#each deleteRefusedLinks as link (link.concept + '/' + link.id)}
|
|
194
|
+
<li>
|
|
195
|
+
<a class="link" href={`/admin/${link.concept}/${link.id}`}>{link.title}</a>
|
|
196
|
+
</li>
|
|
197
|
+
{/each}
|
|
198
|
+
</ul>
|
|
199
|
+
</div>
|
|
200
|
+
{/if}
|
|
201
|
+
{#if visibleBrokenLinks.length}
|
|
202
|
+
<div class="alert alert-error mb-4 flex-col items-start text-sm">
|
|
203
|
+
<p>This page links to {visibleBrokenLinks.length === 1 ? 'a page' : 'pages'} that no longer {visibleBrokenLinks.length === 1 ? 'exists' : 'exist'}. Remove the broken {visibleBrokenLinks.length === 1 ? 'link' : 'links'} and save again.</p>
|
|
204
|
+
<ul class="mt-1 w-full">
|
|
205
|
+
{#each visibleBrokenLinks as href (href)}
|
|
206
|
+
<li class="flex items-center justify-between gap-2">
|
|
207
|
+
<code class="text-xs">{href}</code>
|
|
208
|
+
<button type="button" class="btn btn-xs" onclick={() => removeBrokenLink(href)}>Remove link</button>
|
|
209
|
+
</li>
|
|
210
|
+
{/each}
|
|
211
|
+
</ul>
|
|
212
|
+
</div>
|
|
213
|
+
{/if}
|
|
214
|
+
{#if draftWarning}
|
|
215
|
+
<div class="alert alert-warning mb-4 text-sm">
|
|
216
|
+
Saved. Note: this page links to unpublished {draftWarning.includes(',') ? 'pages' : 'a page'} ({draftWarning}), which will 404 until published.
|
|
217
|
+
</div>
|
|
105
218
|
{/if}
|
|
106
219
|
|
|
107
220
|
<form method="POST" action="?/save" class="lg:grid lg:grid-cols-[1fr_20rem] lg:gap-6">
|
|
@@ -109,7 +222,13 @@ markdown editor and a live, design-accurate preview. The whole surface is one fo
|
|
|
109
222
|
|
|
110
223
|
<div class="lg:order-1">
|
|
111
224
|
<div class="rounded-box border border-base-300 bg-base-100 overflow-hidden">
|
|
112
|
-
<MarkdownEditor
|
|
225
|
+
<MarkdownEditor
|
|
226
|
+
bind:value={body}
|
|
227
|
+
name="body"
|
|
228
|
+
registerInsert={(fn) => (insert = fn)}
|
|
229
|
+
registerInsertLink={(fn) => (insertLink = fn)}
|
|
230
|
+
{completionSources}
|
|
231
|
+
/>
|
|
113
232
|
</div>
|
|
114
233
|
{#if showPreview}
|
|
115
234
|
<section
|
|
@@ -16,6 +16,14 @@ interface Props {
|
|
|
16
16
|
}) => string | Promise<string>;
|
|
17
17
|
/** The site's icon set, for the guided form's icon fields. */
|
|
18
18
|
icons?: IconSet;
|
|
19
|
+
/** The `?/save` or `?/delete` action result. Carries the save guard's broken links when a save was
|
|
20
|
+
* blocked, or the delete guard's inbound linkers when a delete was refused. */
|
|
21
|
+
form?: {
|
|
22
|
+
brokenLinks?: string[];
|
|
23
|
+
body?: string;
|
|
24
|
+
inboundLinks?: import('../content/manifest.js').InboundLink[];
|
|
25
|
+
renameError?: string;
|
|
26
|
+
} | null;
|
|
19
27
|
}
|
|
20
28
|
/**
|
|
21
29
|
* The differentiated editor: the per-concept frontmatter form (from `data.fields`) beside the
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"EditPage.svelte.d.ts","sourceRoot":"","sources":["../../src/lib/components/EditPage.svelte.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"EditPage.svelte.d.ts","sourceRoot":"","sources":["../../src/lib/components/EditPage.svelte.ts"],"names":[],"mappings":"AAWA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,uBAAuB,CAAC;AAC/D,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,oBAAoB,CAAC;AAClD,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,gCAAgC,CAAC;AAE/D,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,qBAAqB,CAAC;AAIrD,UAAU,KAAK;IACb,gEAAgE;IAChE,IAAI,EAAE,QAAQ,GAAG;QAAE,QAAQ,EAAE,MAAM,CAAA;KAAE,CAAC;IACtC,6DAA6D;IAC7D,QAAQ,CAAC,EAAE,iBAAiB,CAAC;IAC7B,qIAAqI;IACrI,MAAM,CAAC,EAAE,CAAC,EAAE,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE;QAAE,OAAO,CAAC,EAAE,OAAO,CAAC;QAAC,OAAO,CAAC,EAAE,WAAW,CAAA;KAAE,KAAK,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;IACvG,8DAA8D;IAC9D,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB;oFACgF;IAChF,IAAI,CAAC,EAAE;QAAE,WAAW,CAAC,EAAE,MAAM,EAAE,CAAC;QAAC,IAAI,CAAC,EAAE,MAAM,CAAC;QAAC,YAAY,CAAC,EAAE,OAAO,wBAAwB,EAAE,WAAW,EAAE,CAAC;QAAC,WAAW,CAAC,EAAE,MAAM,CAAA;KAAE,GAAG,IAAI,CAAC;CAC9I;AAyQH;;;;GAIG;AACH,QAAA,MAAM,QAAQ,2CAAwC,CAAC;AACvD,KAAK,QAAQ,GAAG,UAAU,CAAC,OAAO,QAAQ,CAAC,CAAC;AAC5C,eAAe,QAAQ,CAAC"}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
@component
|
|
3
|
+
The "Link to page" control and its modal. It lists the site's posts and pages from the committed
|
|
4
|
+
manifest (the linkTargets the editor receives), grouped by concept with Pages first, each post
|
|
5
|
+
showing its date and each draft marked. Picking a target inserts a cairn: internal link through the
|
|
6
|
+
editor's registerInsertLink seam. Built on a native <dialog>, following the component dialog's a11y
|
|
7
|
+
conventions. The plain-URL link stays the toolbar's link button; this is for an internal target.
|
|
8
|
+
-->
|
|
9
|
+
<script lang="ts">
|
|
10
|
+
import type { LinkTarget } from '../content/manifest.js';
|
|
11
|
+
import { formatCairnToken } from '../content/links.js';
|
|
12
|
+
|
|
13
|
+
interface Props {
|
|
14
|
+
/** The site's link targets, from the committed manifest (editLoad ships them). */
|
|
15
|
+
linkTargets: LinkTarget[];
|
|
16
|
+
/** Insert an inline cairn link at the editor cursor. */
|
|
17
|
+
insert: (href: string, title: string) => void;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
let { linkTargets, insert }: Props = $props();
|
|
21
|
+
|
|
22
|
+
let dialog = $state<HTMLDialogElement | null>(null);
|
|
23
|
+
let query = $state('');
|
|
24
|
+
|
|
25
|
+
// Group filtered targets by concept, Pages first then Posts then any other concept, so the list
|
|
26
|
+
// reads in a stable order. The filter is a case-insensitive title substring.
|
|
27
|
+
const ORDER: Record<string, number> = { pages: 0, posts: 1 };
|
|
28
|
+
function rank(concept: string): number {
|
|
29
|
+
return ORDER[concept] ?? 2;
|
|
30
|
+
}
|
|
31
|
+
function heading(concept: string): string {
|
|
32
|
+
if (concept === 'pages') return 'Pages';
|
|
33
|
+
if (concept === 'posts') return 'Posts';
|
|
34
|
+
return concept.charAt(0).toUpperCase() + concept.slice(1);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const groups = $derived.by(() => {
|
|
38
|
+
const q = query.trim().toLowerCase();
|
|
39
|
+
const matched = q ? linkTargets.filter((t) => t.title.toLowerCase().includes(q)) : linkTargets;
|
|
40
|
+
const byConcept = new Map<string, LinkTarget[]>();
|
|
41
|
+
for (const t of matched) {
|
|
42
|
+
const list = byConcept.get(t.concept) ?? [];
|
|
43
|
+
list.push(t);
|
|
44
|
+
byConcept.set(t.concept, list);
|
|
45
|
+
}
|
|
46
|
+
return [...byConcept.entries()]
|
|
47
|
+
.map(([concept, items]) => ({ concept, heading: heading(concept), items }))
|
|
48
|
+
.sort((a, b) => rank(a.concept) - rank(b.concept) || a.heading.localeCompare(b.heading));
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
function open() {
|
|
52
|
+
query = '';
|
|
53
|
+
dialog?.showModal();
|
|
54
|
+
}
|
|
55
|
+
function close() {
|
|
56
|
+
dialog?.close();
|
|
57
|
+
}
|
|
58
|
+
function choose(target: LinkTarget) {
|
|
59
|
+
insert(formatCairnToken(target), target.title);
|
|
60
|
+
close();
|
|
61
|
+
}
|
|
62
|
+
</script>
|
|
63
|
+
|
|
64
|
+
<button type="button" class="btn btn-sm btn-ghost" aria-haspopup="dialog" aria-label="Link to page" onclick={open}>
|
|
65
|
+
Link to page
|
|
66
|
+
</button>
|
|
67
|
+
|
|
68
|
+
<dialog class="modal" aria-labelledby="cairn-link-dialog-title" bind:this={dialog}>
|
|
69
|
+
<div class="modal-box">
|
|
70
|
+
<div class="mb-3 flex items-center justify-between">
|
|
71
|
+
<h2 id="cairn-link-dialog-title" class="text-base font-semibold">Link to a page</h2>
|
|
72
|
+
<button type="button" class="btn btn-ghost btn-sm" aria-label="Close" onclick={close}>✕</button>
|
|
73
|
+
</div>
|
|
74
|
+
|
|
75
|
+
<input
|
|
76
|
+
type="search"
|
|
77
|
+
class="input input-bordered mb-3 w-full"
|
|
78
|
+
placeholder="Search by title"
|
|
79
|
+
aria-label="Search pages and posts"
|
|
80
|
+
bind:value={query}
|
|
81
|
+
/>
|
|
82
|
+
|
|
83
|
+
{#if groups.length === 0}
|
|
84
|
+
<p class="text-sm text-[var(--color-muted)]">No pages or posts to link to.</p>
|
|
85
|
+
{:else}
|
|
86
|
+
{#each groups as group (group.concept)}
|
|
87
|
+
<h3 class="mt-2 mb-1 text-xs font-semibold tracking-wide text-[var(--color-muted)] uppercase">{group.heading}</h3>
|
|
88
|
+
<ul class="menu w-full">
|
|
89
|
+
{#each group.items as target (`${target.concept}/${target.id}`)}
|
|
90
|
+
<li>
|
|
91
|
+
<button type="button" onclick={() => choose(target)}>
|
|
92
|
+
<span class="flex flex-col items-start">
|
|
93
|
+
<span class="font-medium">{target.title}</span>
|
|
94
|
+
<span class="text-xs text-[var(--color-muted)]">
|
|
95
|
+
{#if target.draft}<span class="badge badge-ghost badge-sm mr-1">Draft</span>{/if}
|
|
96
|
+
{#if target.date}{target.date}{/if}
|
|
97
|
+
</span>
|
|
98
|
+
</span>
|
|
99
|
+
</button>
|
|
100
|
+
</li>
|
|
101
|
+
{/each}
|
|
102
|
+
</ul>
|
|
103
|
+
{/each}
|
|
104
|
+
{/if}
|
|
105
|
+
</div>
|
|
106
|
+
<form method="dialog" class="modal-backdrop">
|
|
107
|
+
<button tabindex="-1" aria-label="Close">close</button>
|
|
108
|
+
</form>
|
|
109
|
+
</dialog>
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { LinkTarget } from '../content/manifest.js';
|
|
2
|
+
interface Props {
|
|
3
|
+
/** The site's link targets, from the committed manifest (editLoad ships them). */
|
|
4
|
+
linkTargets: LinkTarget[];
|
|
5
|
+
/** Insert an inline cairn link at the editor cursor. */
|
|
6
|
+
insert: (href: string, title: string) => void;
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* The "Link to page" control and its modal. It lists the site's posts and pages from the committed
|
|
10
|
+
* manifest (the linkTargets the editor receives), grouped by concept with Pages first, each post
|
|
11
|
+
* showing its date and each draft marked. Picking a target inserts a cairn: internal link through the
|
|
12
|
+
* editor's registerInsertLink seam. Built on a native <dialog>, following the component dialog's a11y
|
|
13
|
+
* conventions. The plain-URL link stays the toolbar's link button; this is for an internal target.
|
|
14
|
+
*/
|
|
15
|
+
declare const LinkPicker: import("svelte").Component<Props, {}, "">;
|
|
16
|
+
type LinkPicker = ReturnType<typeof LinkPicker>;
|
|
17
|
+
export default LinkPicker;
|
|
18
|
+
//# sourceMappingURL=LinkPicker.svelte.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"LinkPicker.svelte.d.ts","sourceRoot":"","sources":["../../src/lib/components/LinkPicker.svelte.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,wBAAwB,CAAC;AAIvD,UAAU,KAAK;IACb,kFAAkF;IAClF,WAAW,EAAE,UAAU,EAAE,CAAC;IAC1B,wDAAwD;IACxD,MAAM,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;CAC/C;AA8FH;;;;;;GAMG;AACH,QAAA,MAAM,UAAU,2CAAwC,CAAC;AACzD,KAAK,UAAU,GAAG,UAAU,CAAC,OAAO,UAAU,CAAC,CAAC;AAChD,eAAe,UAAU,CAAC"}
|
|
@@ -9,7 +9,7 @@ preview lives in EditPage through the adapter's render. Swapping the editor stay
|
|
|
9
9
|
<script lang="ts">
|
|
10
10
|
import { onMount, onDestroy } from 'svelte';
|
|
11
11
|
import EditorToolbar from './EditorToolbar.svelte';
|
|
12
|
-
import { applyMarkdownFormat, type FormatKind } from './markdown-format.js';
|
|
12
|
+
import { applyMarkdownFormat, insertInlineLink, type FormatKind } from './markdown-format.js';
|
|
13
13
|
|
|
14
14
|
interface Props {
|
|
15
15
|
/** The markdown source; bindable so the parent reads edits back. */
|
|
@@ -18,9 +18,14 @@ preview lives in EditPage through the adapter's render. Swapping the editor stay
|
|
|
18
18
|
name: string;
|
|
19
19
|
/** Receives a `(text) => void` that inserts at the cursor; the palette calls it. */
|
|
20
20
|
registerInsert?: (insert: (text: string) => void) => void;
|
|
21
|
+
/** Receives a `(href, title) => void` that inserts an inline link; the link picker calls it. */
|
|
22
|
+
registerInsertLink?: (insert: (href: string, title: string) => void) => void;
|
|
23
|
+
/** Generic CodeMirror completion sources wired into the editor; the link autocomplete is one. The
|
|
24
|
+
* type is referenced inline so no static `@codemirror/*` import sits in this client-only file. */
|
|
25
|
+
completionSources?: import('@codemirror/autocomplete').CompletionSource[];
|
|
21
26
|
}
|
|
22
27
|
|
|
23
|
-
let { value = $bindable(), name, registerInsert }: Props = $props();
|
|
28
|
+
let { value = $bindable(), name, registerInsert, registerInsertLink, completionSources = [] }: Props = $props();
|
|
24
29
|
|
|
25
30
|
let host = $state<HTMLDivElement | null>(null);
|
|
26
31
|
let mounted = $state(false);
|
|
@@ -35,6 +40,7 @@ preview lives in EditPage through the adapter's render. Swapping the editor stay
|
|
|
35
40
|
const markdownMod = await import('@codemirror/lang-markdown');
|
|
36
41
|
const commandsMod = await import('@codemirror/commands');
|
|
37
42
|
const languageMod = await import('@codemirror/language');
|
|
43
|
+
const autocompleteMod = await import('@codemirror/autocomplete');
|
|
38
44
|
|
|
39
45
|
if (!host) return;
|
|
40
46
|
|
|
@@ -56,8 +62,13 @@ preview lives in EditPage through the adapter's render. Swapping the editor stay
|
|
|
56
62
|
doc: value,
|
|
57
63
|
extensions: [
|
|
58
64
|
commandsMod.history(),
|
|
59
|
-
keymap.of([...commandsMod.defaultKeymap, ...commandsMod.historyKeymap]),
|
|
65
|
+
keymap.of([...autocompleteMod.completionKeymap, ...commandsMod.defaultKeymap, ...commandsMod.historyKeymap]),
|
|
60
66
|
markdownMod.markdown(),
|
|
67
|
+
...(completionSources.length
|
|
68
|
+
? // interactionDelay 0: the popup opens only on an explicit `[[` trigger, so the default
|
|
69
|
+
// accidental-accept guard adds no value and would swallow an immediate Enter into a newline.
|
|
70
|
+
[autocompleteMod.autocompletion({ override: completionSources, interactionDelay: 0 })]
|
|
71
|
+
: []),
|
|
61
72
|
EditorView.lineWrapping,
|
|
62
73
|
languageMod.syntaxHighlighting(languageMod.defaultHighlightStyle, { fallback: true }),
|
|
63
74
|
theme,
|
|
@@ -69,6 +80,7 @@ preview lives in EditPage through the adapter's render. Swapping the editor stay
|
|
|
69
80
|
});
|
|
70
81
|
|
|
71
82
|
registerInsert?.(insertAtCursor);
|
|
83
|
+
registerInsertLink?.(insertLink);
|
|
72
84
|
mounted = true;
|
|
73
85
|
});
|
|
74
86
|
|
|
@@ -96,6 +108,24 @@ preview lives in EditPage through the adapter's render. Swapping the editor stay
|
|
|
96
108
|
view.focus();
|
|
97
109
|
}
|
|
98
110
|
|
|
111
|
+
function insertLink(href: string, title: string) {
|
|
112
|
+
if (!view) {
|
|
113
|
+
// The editor has not mounted yet; append the link to the raw value so a pick is never lost,
|
|
114
|
+
// mirroring insertAtCursor's pre-mount fallback.
|
|
115
|
+
const link = insertInlineLink('', 0, 0, href, title).doc;
|
|
116
|
+
value = value ? `${value} ${link}` : link;
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
const { from, to } = view.state.selection.main;
|
|
120
|
+
const doc = view.state.doc.toString();
|
|
121
|
+
const next = insertInlineLink(doc, from, to, href, title);
|
|
122
|
+
view.dispatch({
|
|
123
|
+
changes: { from: 0, to: doc.length, insert: next.doc },
|
|
124
|
+
selection: { anchor: next.from, head: next.to },
|
|
125
|
+
});
|
|
126
|
+
view.focus();
|
|
127
|
+
}
|
|
128
|
+
|
|
99
129
|
function applyFormat(kind: FormatKind) {
|
|
100
130
|
if (!view) return;
|
|
101
131
|
const { from, to } = view.state.selection.main;
|
|
@@ -5,6 +5,11 @@ interface Props {
|
|
|
5
5
|
name: string;
|
|
6
6
|
/** Receives a `(text) => void` that inserts at the cursor; the palette calls it. */
|
|
7
7
|
registerInsert?: (insert: (text: string) => void) => void;
|
|
8
|
+
/** Receives a `(href, title) => void` that inserts an inline link; the link picker calls it. */
|
|
9
|
+
registerInsertLink?: (insert: (href: string, title: string) => void) => void;
|
|
10
|
+
/** Generic CodeMirror completion sources wired into the editor; the link autocomplete is one. The
|
|
11
|
+
* type is referenced inline so no static `@codemirror/*` import sits in this client-only file. */
|
|
12
|
+
completionSources?: import('@codemirror/autocomplete').CompletionSource[];
|
|
8
13
|
}
|
|
9
14
|
/**
|
|
10
15
|
* The `MarkdownEditor` seam (spec §6, seam 5): a thin wrapper over CodeMirror 6 exposing a bindable
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"MarkdownEditor.svelte.d.ts","sourceRoot":"","sources":["../../src/lib/components/MarkdownEditor.svelte.ts"],"names":[],"mappings":"AAQE,UAAU,KAAK;IACb,oEAAoE;IACpE,KAAK,EAAE,MAAM,CAAC;IACd,sEAAsE;IACtE,IAAI,EAAE,MAAM,CAAC;IACb,oFAAoF;IACpF,cAAc,CAAC,EAAE,CAAC,MAAM,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,KAAK,IAAI,CAAC;
|
|
1
|
+
{"version":3,"file":"MarkdownEditor.svelte.d.ts","sourceRoot":"","sources":["../../src/lib/components/MarkdownEditor.svelte.ts"],"names":[],"mappings":"AAQE,UAAU,KAAK;IACb,oEAAoE;IACpE,KAAK,EAAE,MAAM,CAAC;IACd,sEAAsE;IACtE,IAAI,EAAE,MAAM,CAAC;IACb,oFAAoF;IACpF,cAAc,CAAC,EAAE,CAAC,MAAM,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,KAAK,IAAI,CAAC;IAC1D,gGAAgG;IAChG,kBAAkB,CAAC,EAAE,CAAC,MAAM,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,KAAK,IAAI,KAAK,IAAI,CAAC;IAC7E;uGACmG;IACnG,iBAAiB,CAAC,EAAE,OAAO,0BAA0B,EAAE,gBAAgB,EAAE,CAAC;CAC3E;AAsIH;;;;;;GAMG;AACH,QAAA,MAAM,cAAc,gDAAwC,CAAC;AAC7D,KAAK,cAAc,GAAG,UAAU,CAAC,OAAO,cAAc,CAAC,CAAC;AACxD,eAAe,cAAc,CAAC"}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
@component
|
|
3
|
+
The Change URL control and its modal. The author edits the URL slug; on submit the ?/rename action
|
|
4
|
+
moves the entry and rewrites every inbound cairn link in one commit, so no internal link breaks. A
|
|
5
|
+
dated post keeps its date; only the slug changes. Built on a native <dialog>, following the
|
|
6
|
+
DeleteDialog a11y conventions.
|
|
7
|
+
-->
|
|
8
|
+
<script lang="ts">
|
|
9
|
+
interface Props {
|
|
10
|
+
/** The concept this entry belongs to, e.g. "posts". Posted with the confirm. */
|
|
11
|
+
conceptId: string;
|
|
12
|
+
/** The entry id within its concept. Posted with the confirm. */
|
|
13
|
+
id: string;
|
|
14
|
+
/** A human label for the concept, e.g. "Post", used in the prompts. */
|
|
15
|
+
label: string;
|
|
16
|
+
/** The current slug, prefilled into the input. */
|
|
17
|
+
slug: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
let { conceptId, id, label, slug }: Props = $props();
|
|
21
|
+
|
|
22
|
+
let dialog = $state<HTMLDialogElement | null>(null);
|
|
23
|
+
let slugInput = $state<HTMLInputElement | null>(null);
|
|
24
|
+
// Seeded on open() rather than from the prop at declaration, so the input prefills with the
|
|
25
|
+
// current slug each time the dialog opens without capturing only the initial prop value.
|
|
26
|
+
let nextSlug = $state('');
|
|
27
|
+
|
|
28
|
+
function open() {
|
|
29
|
+
nextSlug = slug;
|
|
30
|
+
dialog?.showModal();
|
|
31
|
+
// showModal() lands focus on the first focusable element (the header Close button), so move
|
|
32
|
+
// it to the slug input the dialog exists for, and select the prefill so the author can replace
|
|
33
|
+
// it in one keystroke (WCAG 2.4.3). A microtask defers past the dialog's own focus handling.
|
|
34
|
+
queueMicrotask(() => {
|
|
35
|
+
slugInput?.focus();
|
|
36
|
+
slugInput?.select();
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
function close() {
|
|
40
|
+
dialog?.close();
|
|
41
|
+
}
|
|
42
|
+
</script>
|
|
43
|
+
|
|
44
|
+
<button type="button" class="btn btn-sm btn-ghost" aria-haspopup="dialog" onclick={open}>Change URL</button>
|
|
45
|
+
|
|
46
|
+
<dialog class="modal" aria-labelledby="cairn-rename-dialog-title" bind:this={dialog}>
|
|
47
|
+
<div class="modal-box">
|
|
48
|
+
<div class="mb-3 flex items-center justify-between">
|
|
49
|
+
<h2 id="cairn-rename-dialog-title" class="text-base font-semibold">Change this {label.toLowerCase()} URL</h2>
|
|
50
|
+
<button type="button" class="btn btn-ghost btn-sm" aria-label="Close" onclick={close}>✕</button>
|
|
51
|
+
</div>
|
|
52
|
+
<form method="POST" action="?/rename" class="flex flex-col gap-3">
|
|
53
|
+
<input type="hidden" name="concept" value={conceptId} />
|
|
54
|
+
<input type="hidden" name="id" value={id} />
|
|
55
|
+
<label class="flex flex-col gap-1">
|
|
56
|
+
<span class="text-sm font-medium">URL slug</span>
|
|
57
|
+
<input class="input" name="slug" bind:value={nextSlug} bind:this={slugInput} autocomplete="off" />
|
|
58
|
+
</label>
|
|
59
|
+
<p class="text-xs text-[var(--color-muted)]">
|
|
60
|
+
Links from other pages update automatically, so nothing breaks. The new URL slug will be
|
|
61
|
+
<code class="text-xs">{nextSlug}</code>.
|
|
62
|
+
</p>
|
|
63
|
+
<div class="flex justify-end gap-2">
|
|
64
|
+
<button type="button" class="btn btn-sm" onclick={close}>Cancel</button>
|
|
65
|
+
<button type="submit" class="btn btn-sm btn-primary">Change URL</button>
|
|
66
|
+
</div>
|
|
67
|
+
</form>
|
|
68
|
+
</div>
|
|
69
|
+
<form method="dialog" class="modal-backdrop">
|
|
70
|
+
<button tabindex="-1" aria-label="Close">close</button>
|
|
71
|
+
</form>
|
|
72
|
+
</dialog>
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
interface Props {
|
|
2
|
+
/** The concept this entry belongs to, e.g. "posts". Posted with the confirm. */
|
|
3
|
+
conceptId: string;
|
|
4
|
+
/** The entry id within its concept. Posted with the confirm. */
|
|
5
|
+
id: string;
|
|
6
|
+
/** A human label for the concept, e.g. "Post", used in the prompts. */
|
|
7
|
+
label: string;
|
|
8
|
+
/** The current slug, prefilled into the input. */
|
|
9
|
+
slug: string;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* The Change URL control and its modal. The author edits the URL slug; on submit the ?/rename action
|
|
13
|
+
* moves the entry and rewrites every inbound cairn link in one commit, so no internal link breaks. A
|
|
14
|
+
* dated post keeps its date; only the slug changes. Built on a native <dialog>, following the
|
|
15
|
+
* DeleteDialog a11y conventions.
|
|
16
|
+
*/
|
|
17
|
+
declare const RenameDialog: import("svelte").Component<Props, {}, "">;
|
|
18
|
+
type RenameDialog = ReturnType<typeof RenameDialog>;
|
|
19
|
+
export default RenameDialog;
|
|
20
|
+
//# sourceMappingURL=RenameDialog.svelte.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"RenameDialog.svelte.d.ts","sourceRoot":"","sources":["../../src/lib/components/RenameDialog.svelte.ts"],"names":[],"mappings":"AAGE,UAAU,KAAK;IACb,gFAAgF;IAChF,SAAS,EAAE,MAAM,CAAC;IAClB,gEAAgE;IAChE,EAAE,EAAE,MAAM,CAAC;IACX,uEAAuE;IACvE,KAAK,EAAE,MAAM,CAAC;IACd,kDAAkD;IAClD,IAAI,EAAE,MAAM,CAAC;CACd;AA6DH;;;;;GAKG;AACH,QAAA,MAAM,YAAY,2CAAwC,CAAC;AAC3D,KAAK,YAAY,GAAG,UAAU,CAAC,OAAO,YAAY,CAAC,CAAC;AACpD,eAAe,YAAY,CAAC"}
|
|
@@ -9,4 +9,7 @@ export { default as ComponentInsertDialog } from './ComponentInsertDialog.svelte
|
|
|
9
9
|
export { default as ComponentForm } from './ComponentForm.svelte';
|
|
10
10
|
export { default as IconPicker } from './IconPicker.svelte';
|
|
11
11
|
export { default as NavTree } from './NavTree.svelte';
|
|
12
|
+
export { default as LinkPicker } from './LinkPicker.svelte';
|
|
13
|
+
export { default as DeleteDialog } from './DeleteDialog.svelte';
|
|
14
|
+
export { default as RenameDialog } from './RenameDialog.svelte';
|
|
12
15
|
//# sourceMappingURL=index.d.ts.map
|