@glw907/cairn-cms 0.59.0 → 0.60.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/CHANGELOG.md +60 -0
- package/dist/components/AdminLayout.svelte +130 -229
- package/dist/components/CairnAdmin.svelte +12 -41
- package/dist/components/CairnLogo.svelte +1 -6
- package/dist/components/CairnMediaLibrary.svelte +821 -1210
- package/dist/components/CairnTidySettings.svelte +486 -0
- package/dist/components/CairnTidySettings.svelte.d.ts +32 -0
- package/dist/components/ComponentForm.svelte +110 -185
- package/dist/components/ComponentInsertDialog.svelte +163 -283
- package/dist/components/ConceptList.svelte +111 -191
- package/dist/components/ConfirmPage.svelte +5 -12
- package/dist/components/CsrfField.svelte +5 -11
- package/dist/components/DeleteDialog.svelte +15 -42
- package/dist/components/EditPage.svelte +786 -918
- package/dist/components/EditorToolbar.svelte +108 -170
- package/dist/components/IconPicker.svelte +23 -53
- package/dist/components/LinkPicker.svelte +34 -58
- package/dist/components/LoginPage.svelte +14 -27
- package/dist/components/ManageEditors.svelte +3 -15
- package/dist/components/MarkdownEditor.svelte +688 -789
- package/dist/components/MarkdownEditor.svelte.d.ts +44 -0
- package/dist/components/MarkdownHelpDialog.svelte +8 -12
- package/dist/components/MediaCaptureCard.svelte +18 -57
- package/dist/components/MediaFigureControl.svelte +32 -71
- package/dist/components/MediaHeroField.svelte +210 -329
- package/dist/components/MediaInsertPopover.svelte +156 -283
- package/dist/components/MediaPicker.svelte +67 -131
- package/dist/components/NavTree.svelte +46 -78
- package/dist/components/RenameDialog.svelte +16 -43
- package/dist/components/ShortcutsDialog.svelte +9 -13
- package/dist/components/ShortcutsGrid.svelte +1 -2
- package/dist/components/TidyReview.svelte +355 -0
- package/dist/components/TidyReview.svelte.d.ts +47 -0
- package/dist/components/WebLinkDialog.svelte +19 -40
- package/dist/components/cairn-admin.css +768 -0
- package/dist/components/editor-tidy.d.ts +31 -0
- package/dist/components/editor-tidy.js +199 -0
- package/dist/components/index.d.ts +1 -0
- package/dist/components/index.js +1 -0
- package/dist/components/markdown-directives.d.ts +16 -0
- package/dist/components/markdown-directives.js +34 -0
- package/dist/components/objective-errors.d.ts +30 -0
- package/dist/components/objective-errors.js +113 -0
- package/dist/components/spellcheck-assets/dictionary-en-us.txt +104743 -0
- package/dist/components/spellcheck-assets/spellchecker-wasm-LICENSE.txt +21 -0
- package/dist/components/spellcheck-assets/spellchecker-wasm.wasm +0 -0
- package/dist/components/spellcheck-worker.d.ts +80 -0
- package/dist/components/spellcheck-worker.js +161 -0
- package/dist/components/spellcheck.d.ts +148 -0
- package/dist/components/spellcheck.js +553 -0
- package/dist/components/tidy-categorize.d.ts +67 -0
- package/dist/components/tidy-categorize.js +392 -0
- package/dist/components/tidy-diff.d.ts +60 -0
- package/dist/components/tidy-diff.js +147 -0
- package/dist/components/tidy-validate.d.ts +37 -0
- package/dist/components/tidy-validate.js +174 -0
- package/dist/content/compose.d.ts +1 -1
- package/dist/content/compose.js +11 -0
- package/dist/content/site-dictionary.d.ts +31 -0
- package/dist/content/site-dictionary.js +82 -0
- package/dist/content/types.d.ts +25 -0
- package/dist/delivery/CairnHead.svelte +8 -11
- package/dist/doctor/checks-local.d.ts +1 -0
- package/dist/doctor/checks-local.js +55 -6
- package/dist/doctor/index.js +2 -1
- package/dist/log/events.d.ts +1 -1
- package/dist/nav/site-config.d.ts +98 -0
- package/dist/nav/site-config.js +132 -0
- package/dist/sveltekit/admin-dispatch.d.ts +2 -0
- package/dist/sveltekit/admin-dispatch.js +6 -2
- package/dist/sveltekit/cairn-admin.d.ts +13 -1
- package/dist/sveltekit/cairn-admin.js +22 -3
- package/dist/sveltekit/content-routes.d.ts +135 -1
- package/dist/sveltekit/content-routes.js +351 -3
- package/dist/sveltekit/tidy-prompt.d.ts +11 -0
- package/dist/sveltekit/tidy-prompt.js +118 -0
- package/package.json +11 -2
- package/src/lib/components/CairnAdmin.svelte +3 -0
- package/src/lib/components/CairnTidySettings.svelte +553 -0
- package/src/lib/components/EditPage.svelte +371 -2
- package/src/lib/components/MarkdownEditor.svelte +168 -1
- package/src/lib/components/TidyReview.svelte +463 -0
- package/src/lib/components/cairn-admin.css +25 -0
- package/src/lib/components/editor-tidy.ts +241 -0
- package/src/lib/components/index.ts +1 -0
- package/src/lib/components/markdown-directives.ts +35 -0
- package/src/lib/components/objective-errors.ts +155 -0
- package/src/lib/components/spellcheck-assets/dictionary-en-us.txt +104743 -0
- package/src/lib/components/spellcheck-assets/spellchecker-wasm-LICENSE.txt +21 -0
- package/src/lib/components/spellcheck-assets/spellchecker-wasm.wasm +0 -0
- package/src/lib/components/spellcheck-worker.ts +279 -0
- package/src/lib/components/spellcheck.ts +693 -0
- package/src/lib/components/tidy-categorize.ts +460 -0
- package/src/lib/components/tidy-diff.ts +196 -0
- package/src/lib/components/tidy-validate.ts +202 -0
- package/src/lib/content/compose.ts +11 -1
- package/src/lib/content/site-dictionary.ts +84 -0
- package/src/lib/content/types.ts +25 -0
- package/src/lib/doctor/checks-local.ts +59 -5
- package/src/lib/doctor/index.ts +2 -0
- package/src/lib/log/events.ts +7 -1
- package/src/lib/nav/site-config.ts +197 -0
- package/src/lib/sveltekit/admin-dispatch.ts +7 -3
- package/src/lib/sveltekit/cairn-admin.ts +32 -4
- package/src/lib/sveltekit/content-routes.ts +504 -4
- package/src/lib/sveltekit/tidy-prompt.ts +153 -0
|
@@ -7,199 +7,119 @@ publish-state badge, and a delete action. A draft row de-emphasizes and carries
|
|
|
7
7
|
tag by the title. A trailing New row at the foot of the card opens the same create dialog as the
|
|
8
8
|
header button. Filtering, sorting, and paging run over the loaded entries in component state.
|
|
9
9
|
-->
|
|
10
|
-
<script lang="ts">
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
function formatDate(iso: string | null): string {
|
|
53
|
-
if (!iso) return '';
|
|
54
|
-
const parsed = new Date(`${iso}T00:00:00`);
|
|
55
|
-
return Number.isNaN(parsed.getTime()) ? iso : dateFmt.format(parsed);
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
// Triage counts over the full loaded set, each axis counted independently. Pending is new +
|
|
59
|
-
// edited (status !== 'published'); Published is live-as-is (status === 'published'); Hidden is
|
|
60
|
-
// the draft rows. The axes overlap: a published-but-hidden entry counts in BOTH Published and
|
|
61
|
-
// Hidden. All is the unconditional total.
|
|
62
|
-
const counts = $derived({
|
|
63
|
-
all: data.entries.length,
|
|
64
|
-
pending: data.entries.filter((e) => e.status !== 'published').length,
|
|
65
|
-
published: data.entries.filter((e) => e.status === 'published').length,
|
|
66
|
-
hidden: data.entries.filter((e) => e.draft).length,
|
|
67
|
-
});
|
|
68
|
-
|
|
69
|
-
// The three publish-state segments, in display order. Each names its partition value, its label,
|
|
70
|
-
// and the count axis it shows; the markup loops this so the segments share one block.
|
|
71
|
-
const segments: { value: Partition; label: string; count: () => number }[] = [
|
|
72
|
-
{ value: 'all', label: 'All', count: () => counts.all },
|
|
73
|
-
{ value: 'pending', label: 'Pending edits', count: () => counts.pending },
|
|
74
|
-
{ value: 'published', label: 'Published', count: () => counts.published },
|
|
75
|
-
];
|
|
76
|
-
|
|
77
|
-
function matchesPartition(entry: EntrySummary): boolean {
|
|
78
|
-
switch (partition) {
|
|
79
|
-
case 'pending':
|
|
80
|
-
return entry.status !== 'published';
|
|
81
|
-
case 'published':
|
|
82
|
-
return entry.status === 'published';
|
|
83
|
-
default:
|
|
84
|
-
return true;
|
|
85
|
-
}
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
// Compose the partition and the Hidden axis with the search query; sort and paging run
|
|
89
|
-
// downstream. Hidden is orthogonal: when on, it narrows the partition to its draft rows.
|
|
90
|
-
const filtered = $derived(
|
|
91
|
-
data.entries.filter(
|
|
92
|
-
(e) =>
|
|
93
|
-
matchesPartition(e) &&
|
|
94
|
-
(!hiddenOnly || e.draft === true) &&
|
|
95
|
-
e.title.toLowerCase().includes(query.trim().toLowerCase()),
|
|
96
|
-
),
|
|
97
|
-
);
|
|
98
|
-
|
|
99
|
-
function setPartition(next: Partition) {
|
|
100
|
-
partition = next;
|
|
101
|
-
page = 1;
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
function toggleHidden() {
|
|
105
|
-
hiddenOnly = !hiddenOnly;
|
|
106
|
-
page = 1;
|
|
10
|
+
<script lang="ts">import { slugify } from "../content/ids.js";
|
|
11
|
+
import CsrfField from "./CsrfField.svelte";
|
|
12
|
+
import DeleteDialog from "./DeleteDialog.svelte";
|
|
13
|
+
import CairnLogo from "./CairnLogo.svelte";
|
|
14
|
+
import { SearchIcon, ArrowUpIcon, ArrowDownIcon, ChevronsUpDownIcon, ChevronLeftIcon, ChevronRightIcon, PlusIcon, Trash2Icon } from "./admin-icons.js";
|
|
15
|
+
import EyeOffIcon from "@lucide/svelte/icons/eye-off";
|
|
16
|
+
let { data, form = null } = $props();
|
|
17
|
+
const deleteRefused = $derived(
|
|
18
|
+
form?.inboundLinks?.length ? { id: form.id, inboundLinks: form.inboundLinks } : null
|
|
19
|
+
);
|
|
20
|
+
let query = $state("");
|
|
21
|
+
let partition = $state("all");
|
|
22
|
+
let hiddenOnly = $state(false);
|
|
23
|
+
let sortKey = $state("date");
|
|
24
|
+
let sortAsc = $state(false);
|
|
25
|
+
let pageSize = $state(10);
|
|
26
|
+
let page = $state(1);
|
|
27
|
+
const dateFmt = new Intl.DateTimeFormat(void 0, { year: "numeric", month: "short", day: "numeric" });
|
|
28
|
+
function formatDate(iso) {
|
|
29
|
+
if (!iso) return "";
|
|
30
|
+
const parsed = /* @__PURE__ */ new Date(`${iso}T00:00:00`);
|
|
31
|
+
return Number.isNaN(parsed.getTime()) ? iso : dateFmt.format(parsed);
|
|
32
|
+
}
|
|
33
|
+
const counts = $derived({
|
|
34
|
+
all: data.entries.length,
|
|
35
|
+
pending: data.entries.filter((e) => e.status !== "published").length,
|
|
36
|
+
published: data.entries.filter((e) => e.status === "published").length,
|
|
37
|
+
hidden: data.entries.filter((e) => e.draft).length
|
|
38
|
+
});
|
|
39
|
+
const segments = [
|
|
40
|
+
{ value: "all", label: "All", count: () => counts.all },
|
|
41
|
+
{ value: "pending", label: "Pending edits", count: () => counts.pending },
|
|
42
|
+
{ value: "published", label: "Published", count: () => counts.published }
|
|
43
|
+
];
|
|
44
|
+
function matchesPartition(entry) {
|
|
45
|
+
switch (partition) {
|
|
46
|
+
case "pending":
|
|
47
|
+
return entry.status !== "published";
|
|
48
|
+
case "published":
|
|
49
|
+
return entry.status === "published";
|
|
50
|
+
default:
|
|
51
|
+
return true;
|
|
107
52
|
}
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
53
|
+
}
|
|
54
|
+
const filtered = $derived(
|
|
55
|
+
data.entries.filter(
|
|
56
|
+
(e) => matchesPartition(e) && (!hiddenOnly || e.draft === true) && e.title.toLowerCase().includes(query.trim().toLowerCase())
|
|
57
|
+
)
|
|
58
|
+
);
|
|
59
|
+
function setPartition(next) {
|
|
60
|
+
partition = next;
|
|
61
|
+
page = 1;
|
|
62
|
+
}
|
|
63
|
+
function toggleHidden() {
|
|
64
|
+
hiddenOnly = !hiddenOnly;
|
|
65
|
+
page = 1;
|
|
66
|
+
}
|
|
67
|
+
function clearSearch() {
|
|
68
|
+
query = "";
|
|
69
|
+
page = 1;
|
|
70
|
+
}
|
|
71
|
+
function segButtonClass(pressed) {
|
|
72
|
+
return `inline-flex items-center gap-1.5 px-3 py-1 text-[0.8125rem] font-normal ${pressed ? "bg-primary/10 text-primary font-medium" : "text-[var(--color-muted)]"}`;
|
|
73
|
+
}
|
|
74
|
+
function hiddenToggleClass(pressed) {
|
|
75
|
+
return `inline-flex items-center gap-1.5 rounded-lg px-3 py-1 text-[0.8125rem] font-normal hover:bg-base-content/[0.06] ${pressed ? "bg-primary/10 text-primary font-medium" : "text-[var(--color-muted)]"}`;
|
|
76
|
+
}
|
|
77
|
+
const draftDim = "opacity-[0.62]";
|
|
78
|
+
function sortValue(entry) {
|
|
79
|
+
if (sortKey === "title") return entry.title.toLowerCase();
|
|
80
|
+
return entry.date ?? "";
|
|
81
|
+
}
|
|
82
|
+
function compareStrings(a, b) {
|
|
83
|
+
if (a < b) return -1;
|
|
84
|
+
if (a > b) return 1;
|
|
85
|
+
return 0;
|
|
86
|
+
}
|
|
87
|
+
const sorted = $derived(
|
|
88
|
+
[...filtered].sort((a, b) => {
|
|
89
|
+
const cmp = compareStrings(sortValue(a), sortValue(b));
|
|
90
|
+
return sortAsc ? cmp : -cmp;
|
|
91
|
+
})
|
|
92
|
+
);
|
|
93
|
+
const pageCount = $derived(Math.max(1, Math.ceil(sorted.length / pageSize)));
|
|
94
|
+
$effect(() => {
|
|
95
|
+
if (page > pageCount) page = pageCount;
|
|
96
|
+
});
|
|
97
|
+
const pageRows = $derived(sorted.slice((page - 1) * pageSize, page * pageSize));
|
|
98
|
+
function toggleSort(key) {
|
|
99
|
+
if (sortKey === key) sortAsc = !sortAsc;
|
|
100
|
+
else {
|
|
101
|
+
sortKey = key;
|
|
102
|
+
sortAsc = true;
|
|
147
103
|
}
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
sortAsc = true;
|
|
168
|
-
}
|
|
169
|
-
page = 1;
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
// --- create form state, shown in a header-triggered dialog ---
|
|
173
|
-
let createDialog = $state<HTMLDialogElement>();
|
|
174
|
-
// Pending from submit until the create navigation lands, so the button shows a calm working state.
|
|
175
|
-
let creating = $state(false);
|
|
176
|
-
let title = $state('');
|
|
177
|
-
let slug = $state('');
|
|
178
|
-
let slugEdited = $state(false);
|
|
179
|
-
// Default the date client-side so the SSR pass and hydration agree across UTC midnight.
|
|
180
|
-
let dateDefault = $state('');
|
|
181
|
-
$effect(() => {
|
|
182
|
-
dateDefault = new Date().toISOString().slice(0, 10);
|
|
183
|
-
});
|
|
184
|
-
const derivedSlug = $derived(slugEdited ? slug : slugify(title));
|
|
185
|
-
const slugPlaceholder = $derived(data.dated ? 'my-entry' : 'about-us');
|
|
186
|
-
// The create affordances name one new item, so they read in the singular ("New post"). The
|
|
187
|
-
// descriptor resolves `singular` (defaulting it to the label), so the fallback here only guards an
|
|
188
|
-
// older caller that ships no `singular` on its ListData.
|
|
189
|
-
const createNoun = $derived(data.singular ?? data.label);
|
|
190
|
-
|
|
191
|
-
// Shared column-header typography: small uppercase muted labels. The sort buttons add their own
|
|
192
|
-
// flex layout and a hover affordance on top of this.
|
|
193
|
-
const headerLabel = 'text-[0.6875rem] font-semibold uppercase tracking-[0.08em] text-[var(--color-muted)]';
|
|
194
|
-
const sortButton = `inline-flex items-center gap-1 ${headerLabel} hover:text-base-content`;
|
|
195
|
-
|
|
196
|
-
// The publish-all flash. A racing second admin can publish first, leaving this redirect
|
|
197
|
-
// counting zero; say nothing then.
|
|
198
|
-
const publishedAllMessage = $derived(
|
|
199
|
-
data.publishedAll !== null && data.publishedAll > 0
|
|
200
|
-
? `Published ${data.publishedAll} ${data.publishedAll === 1 ? 'entry' : 'entries'}.`
|
|
201
|
-
: '',
|
|
202
|
-
);
|
|
104
|
+
page = 1;
|
|
105
|
+
}
|
|
106
|
+
let createDialog = $state();
|
|
107
|
+
let creating = $state(false);
|
|
108
|
+
let title = $state("");
|
|
109
|
+
let slug = $state("");
|
|
110
|
+
let slugEdited = $state(false);
|
|
111
|
+
let dateDefault = $state("");
|
|
112
|
+
$effect(() => {
|
|
113
|
+
dateDefault = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
114
|
+
});
|
|
115
|
+
const derivedSlug = $derived(slugEdited ? slug : slugify(title));
|
|
116
|
+
const slugPlaceholder = $derived(data.dated ? "my-entry" : "about-us");
|
|
117
|
+
const createNoun = $derived(data.singular ?? data.label);
|
|
118
|
+
const headerLabel = "text-[0.6875rem] font-semibold uppercase tracking-[0.08em] text-[var(--color-muted)]";
|
|
119
|
+
const sortButton = `inline-flex items-center gap-1 ${headerLabel} hover:text-base-content`;
|
|
120
|
+
const publishedAllMessage = $derived(
|
|
121
|
+
data.publishedAll !== null && data.publishedAll > 0 ? `Published ${data.publishedAll} ${data.publishedAll === 1 ? "entry" : "entries"}.` : ""
|
|
122
|
+
);
|
|
203
123
|
</script>
|
|
204
124
|
|
|
205
125
|
<!-- The non-color selected cue for the triage controls (WCAG 1.4.1): a small check glyph that
|
|
@@ -3,18 +3,11 @@
|
|
|
3
3
|
The scanner-safe confirm page. A GET renders this static "Confirm sign-in" button with the token
|
|
4
4
|
in a hidden field and consumes nothing; only the explicit POST verifies (spec §7.1). JS-free.
|
|
5
5
|
-->
|
|
6
|
-
<script lang="ts">
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
interface Props {
|
|
13
|
-
/** The confirm load's data: the token to submit, the site name, an optional error, the CSRF token. */
|
|
14
|
-
data: { token: string; siteName: string; error: string | null; csrf: string };
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
let { data }: Props = $props();
|
|
6
|
+
<script lang="ts">import "./cairn-admin.css";
|
|
7
|
+
import CairnLogo from "./CairnLogo.svelte";
|
|
8
|
+
import CsrfField from "./CsrfField.svelte";
|
|
9
|
+
import { cairnFaviconHref } from "./cairn-favicon.js";
|
|
10
|
+
let { data } = $props();
|
|
18
11
|
</script>
|
|
19
12
|
|
|
20
13
|
<svelte:head>
|
|
@@ -4,17 +4,11 @@ A hidden CSRF double-submit field for an admin form. Pass `token` directly (the
|
|
|
4
4
|
or omit it inside the authed shell, where AdminLayout provides the token through context. A form that
|
|
5
5
|
omits this field fails the guard's token check, which is the intended fail-closed signal.
|
|
6
6
|
-->
|
|
7
|
-
<script lang="ts">
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
/** The CSRF token. Falls back to the admin context when omitted. */
|
|
13
|
-
token?: string;
|
|
14
|
-
}
|
|
15
|
-
let { token }: Props = $props();
|
|
16
|
-
const fromContext = getContext<(() => string) | undefined>(CSRF_CONTEXT_KEY);
|
|
17
|
-
const value = $derived(token ?? fromContext?.() ?? '');
|
|
7
|
+
<script lang="ts">import { getContext } from "svelte";
|
|
8
|
+
import { CSRF_CONTEXT_KEY } from "./csrf-context.js";
|
|
9
|
+
let { token } = $props();
|
|
10
|
+
const fromContext = getContext(CSRF_CONTEXT_KEY);
|
|
11
|
+
const value = $derived(token ?? fromContext?.() ?? "");
|
|
18
12
|
</script>
|
|
19
13
|
|
|
20
14
|
<input type="hidden" name="csrf" value={value} />
|
|
@@ -5,48 +5,21 @@ The Delete control and its modal. With no inbound links it is a plain confirm th
|
|
|
5
5
|
each linking to its edit page, so the author repoints or removes those links first. Built on a native
|
|
6
6
|
<dialog>, following the LinkPicker a11y conventions.
|
|
7
7
|
-->
|
|
8
|
-
<script lang="ts">
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
/** Render the built-in Delete trigger. False mounts only the dialog, for a host that supplies
|
|
24
|
-
* its own trigger and opens the dialog through the exported open(). */
|
|
25
|
-
trigger?: boolean;
|
|
26
|
-
/** Called when the delete confirm submits, before the document navigates. The edit page uses
|
|
27
|
-
* it to stand down its leave guard while the POST is in flight. */
|
|
28
|
-
onsubmitting?: () => void;
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
let { conceptId, id, label, inboundLinks, pending = false, trigger = true, onsubmitting }: Props = $props();
|
|
32
|
-
|
|
33
|
-
let dialog = $state<HTMLDialogElement | null>(null);
|
|
34
|
-
const blocked = $derived(inboundLinks.length > 0);
|
|
35
|
-
const noun = $derived(label.toLowerCase());
|
|
36
|
-
// One inbound link reads "1 post links here ... repoint it"; many reads "2 posts link here ...
|
|
37
|
-
// repoint them". The subject-verb agreement inverts the usual plural-s, so derive each form once.
|
|
38
|
-
const single = $derived(inboundLinks.length === 1);
|
|
39
|
-
const nouns = $derived(single ? noun : `${noun}s`);
|
|
40
|
-
const verb = $derived(single ? 'links' : 'link');
|
|
41
|
-
const pronoun = $derived(single ? 'it' : 'them');
|
|
42
|
-
|
|
43
|
-
/** Open the confirm. Exported so a trigger={false} host can drive the dialog itself. */
|
|
44
|
-
export function open() {
|
|
45
|
-
dialog?.showModal();
|
|
46
|
-
}
|
|
47
|
-
function close() {
|
|
48
|
-
dialog?.close();
|
|
49
|
-
}
|
|
8
|
+
<script lang="ts">import CsrfField from "./CsrfField.svelte";
|
|
9
|
+
let { conceptId, id, label, inboundLinks, pending = false, trigger = true, onsubmitting } = $props();
|
|
10
|
+
let dialog = $state(null);
|
|
11
|
+
const blocked = $derived(inboundLinks.length > 0);
|
|
12
|
+
const noun = $derived(label.toLowerCase());
|
|
13
|
+
const single = $derived(inboundLinks.length === 1);
|
|
14
|
+
const nouns = $derived(single ? noun : `${noun}s`);
|
|
15
|
+
const verb = $derived(single ? "links" : "link");
|
|
16
|
+
const pronoun = $derived(single ? "it" : "them");
|
|
17
|
+
export function open() {
|
|
18
|
+
dialog?.showModal();
|
|
19
|
+
}
|
|
20
|
+
function close() {
|
|
21
|
+
dialog?.close();
|
|
22
|
+
}
|
|
50
23
|
</script>
|
|
51
24
|
|
|
52
25
|
{#if trigger}
|