@glw907/cairn-cms 0.68.0 → 0.76.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +82 -0
- package/dist/ambient.d.ts +2 -0
- package/dist/components/CairnAdmin.svelte.d.ts +2 -7
- package/dist/components/ComponentForm.svelte +44 -27
- package/dist/components/ComponentInsertDialog.svelte +5 -5
- package/dist/components/ComponentInsertDialog.svelte.d.ts +2 -6
- package/dist/components/EditPage.svelte +29 -107
- package/dist/components/EditPage.svelte.d.ts +2 -7
- package/dist/components/EntryPicker.svelte +117 -0
- package/dist/components/EntryPicker.svelte.d.ts +35 -0
- package/dist/components/FieldInput.svelte +218 -0
- package/dist/components/FieldInput.svelte.d.ts +51 -0
- package/dist/components/IconPicker.svelte +2 -2
- package/dist/components/IconPicker.svelte.d.ts +2 -0
- package/dist/components/LinkPicker.svelte +8 -75
- package/dist/components/LinkPicker.svelte.d.ts +4 -5
- package/dist/components/MediaHeroField.svelte +8 -5
- package/dist/components/MediaHeroField.svelte.d.ts +4 -0
- package/dist/components/ObjectGroupField.svelte +54 -0
- package/dist/components/ObjectGroupField.svelte.d.ts +47 -0
- package/dist/components/ReferenceField.svelte +94 -0
- package/dist/components/ReferenceField.svelte.d.ts +27 -0
- package/dist/components/RepeatableField.svelte +221 -0
- package/dist/components/RepeatableField.svelte.d.ts +53 -0
- package/dist/components/cairn-admin.css +4 -0
- package/dist/components/preview-doc.js +5 -1
- package/dist/components/tidy-validate.js +1 -1
- package/dist/content/adapter.js +18 -0
- package/dist/content/advisories.d.ts +2 -2
- package/dist/content/advisories.js +3 -5
- package/dist/content/compose.d.ts +7 -6
- package/dist/content/compose.js +26 -20
- package/dist/content/concepts.d.ts +21 -15
- package/dist/content/concepts.js +55 -32
- package/dist/content/field-rules.js +3 -4
- package/dist/content/fields.d.ts +49 -1
- package/dist/content/fields.js +11 -0
- package/dist/content/fieldset.d.ts +31 -10
- package/dist/content/fieldset.js +262 -109
- package/dist/content/frontmatter-region.d.ts +38 -0
- package/dist/content/frontmatter-region.js +75 -0
- package/dist/content/frontmatter.d.ts +35 -2
- package/dist/content/frontmatter.js +232 -11
- package/dist/content/manifest.d.ts +34 -0
- package/dist/content/manifest.js +80 -4
- package/dist/content/media-refs.d.ts +2 -2
- package/dist/content/media-rewrite.js +1 -69
- package/dist/content/reference-index.d.ts +56 -0
- package/dist/content/reference-index.js +95 -0
- package/dist/content/references.d.ts +40 -0
- package/dist/content/references.js +0 -0
- package/dist/content/standard-schema.d.ts +30 -0
- package/dist/content/standard-schema.js +4 -0
- package/dist/content/types.d.ts +127 -178
- package/dist/delivery/data.d.ts +2 -2
- package/dist/delivery/data.js +1 -1
- package/dist/delivery/public-routes.d.ts +2 -5
- package/dist/delivery/public-routes.js +15 -1
- package/dist/delivery/site-descriptors.d.ts +5 -1
- package/dist/delivery/site-descriptors.js +8 -3
- package/dist/delivery/site-indexes.d.ts +2 -2
- package/dist/delivery/site-resolver.d.ts +25 -0
- package/dist/delivery/site-resolver.js +49 -0
- package/dist/doctor/checks-local.js +6 -11
- package/dist/github/backend.d.ts +83 -0
- package/dist/github/backend.js +76 -0
- package/dist/github/credentials.d.ts +11 -5
- package/dist/github/credentials.js +3 -3
- package/dist/github/repo.d.ts +8 -19
- package/dist/github/repo.js +69 -80
- package/dist/github/types.d.ts +1 -1
- package/dist/github/types.js +4 -4
- package/dist/index.d.ts +16 -12
- package/dist/index.js +7 -8
- package/dist/islands/index.d.ts +12 -0
- package/dist/islands/index.js +83 -0
- package/dist/islands/types.d.ts +7 -0
- package/dist/islands/types.js +1 -0
- package/dist/media/rewrite-plan.d.ts +2 -3
- package/dist/media/rewrite-plan.js +2 -3
- package/dist/media/usage.d.ts +2 -2
- package/dist/media/usage.js +3 -5
- package/dist/nav/site-config.d.ts +0 -6
- package/dist/nav/site-config.js +6 -4
- package/dist/render/component-grammar.js +11 -11
- package/dist/render/component-reference.js +5 -3
- package/dist/render/component-validate.d.ts +4 -1
- package/dist/render/component-validate.js +10 -35
- package/dist/render/pipeline.d.ts +0 -6
- package/dist/render/pipeline.js +1 -1
- package/dist/render/registry.d.ts +34 -34
- package/dist/render/registry.js +26 -5
- package/dist/render/rehype-dispatch.d.ts +4 -4
- package/dist/render/rehype-dispatch.js +36 -11
- package/dist/render/remark-directives.js +4 -5
- package/dist/render/sanitize-schema.js +1 -1
- package/dist/sveltekit/cairn-admin.d.ts +5 -5
- package/dist/sveltekit/cairn-admin.js +3 -4
- package/dist/sveltekit/content-routes.d.ts +10 -8
- package/dist/sveltekit/content-routes.js +269 -181
- package/dist/sveltekit/health.d.ts +7 -3
- package/dist/sveltekit/health.js +9 -3
- package/dist/sveltekit/index.d.ts +1 -1
- package/dist/sveltekit/nav-routes.d.ts +6 -5
- package/dist/sveltekit/nav-routes.js +22 -20
- package/dist/sveltekit/types.d.ts +2 -0
- package/dist/vite/index.d.ts +3 -3
- package/dist/vite/index.js +17 -8
- package/package.json +5 -1
- package/src/lib/ambient.ts +7 -0
- package/src/lib/components/CairnAdmin.svelte +2 -6
- package/src/lib/components/ComponentForm.svelte +48 -27
- package/src/lib/components/ComponentInsertDialog.svelte +9 -8
- package/src/lib/components/EditPage.svelte +43 -119
- package/src/lib/components/EntryPicker.svelte +154 -0
- package/src/lib/components/FieldInput.svelte +262 -0
- package/src/lib/components/IconPicker.svelte +4 -2
- package/src/lib/components/LinkPicker.svelte +10 -81
- package/src/lib/components/MediaHeroField.svelte +12 -5
- package/src/lib/components/ObjectGroupField.svelte +97 -0
- package/src/lib/components/ReferenceField.svelte +126 -0
- package/src/lib/components/RepeatableField.svelte +310 -0
- package/src/lib/components/preview-doc.ts +5 -1
- package/src/lib/components/tidy-validate.ts +1 -1
- package/src/lib/content/adapter.ts +21 -0
- package/src/lib/content/advisories.ts +4 -7
- package/src/lib/content/compose.ts +30 -23
- package/src/lib/content/concepts.ts +68 -40
- package/src/lib/content/field-rules.ts +3 -4
- package/src/lib/content/fields.ts +52 -1
- package/src/lib/content/fieldset.ts +291 -128
- package/src/lib/content/frontmatter-region.ts +90 -0
- package/src/lib/content/frontmatter.ts +231 -15
- package/src/lib/content/manifest.ts +101 -4
- package/src/lib/content/media-refs.ts +2 -2
- package/src/lib/content/media-rewrite.ts +7 -80
- package/src/lib/content/reference-index.ts +159 -0
- package/src/lib/content/references.ts +0 -0
- package/src/lib/content/standard-schema.ts +25 -0
- package/src/lib/content/types.ts +128 -195
- package/src/lib/delivery/data.ts +2 -2
- package/src/lib/delivery/public-routes.ts +17 -3
- package/src/lib/delivery/site-descriptors.ts +8 -3
- package/src/lib/delivery/site-indexes.ts +2 -2
- package/src/lib/delivery/site-resolver.ts +64 -0
- package/src/lib/doctor/checks-local.ts +6 -14
- package/src/lib/github/backend.ts +161 -0
- package/src/lib/github/credentials.ts +10 -7
- package/src/lib/github/repo.ts +79 -83
- package/src/lib/github/types.ts +5 -5
- package/src/lib/index.ts +38 -23
- package/src/lib/islands/index.ts +84 -0
- package/src/lib/islands/types.ts +11 -0
- package/src/lib/media/rewrite-plan.ts +4 -6
- package/src/lib/media/usage.ts +4 -7
- package/src/lib/nav/site-config.ts +8 -9
- package/src/lib/render/component-grammar.ts +10 -10
- package/src/lib/render/component-reference.ts +4 -3
- package/src/lib/render/component-validate.ts +10 -35
- package/src/lib/render/pipeline.ts +1 -7
- package/src/lib/render/registry.ts +58 -39
- package/src/lib/render/rehype-dispatch.ts +45 -10
- package/src/lib/render/remark-directives.ts +4 -5
- package/src/lib/render/sanitize-schema.ts +1 -1
- package/src/lib/sveltekit/cairn-admin.ts +8 -9
- package/src/lib/sveltekit/content-routes.ts +330 -221
- package/src/lib/sveltekit/health.ts +13 -6
- package/src/lib/sveltekit/index.ts +2 -2
- package/src/lib/sveltekit/nav-routes.ts +33 -29
- package/src/lib/sveltekit/types.ts +5 -1
- package/src/lib/vite/index.ts +20 -11
- package/dist/content/schema.d.ts +0 -87
- package/dist/content/schema.js +0 -85
- package/dist/content/validate.d.ts +0 -17
- package/dist/content/validate.js +0 -93
- package/src/lib/content/schema.ts +0 -163
- package/src/lib/content/validate.ts +0 -90
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
@component
|
|
3
|
+
The reference field editor arm. A single `reference` renders a combobox-style button showing the
|
|
4
|
+
current target's resolved title (looked up in the site's link targets), opening EntryPicker scoped to
|
|
5
|
+
the field's concept; on pick it sets the value and emits one hidden input the decoder reads. A many
|
|
6
|
+
`array(reference)` renders a removable chip list, each chip showing the target's resolved title, plus
|
|
7
|
+
an EntryPicker that marks the already-held ids and adds another; it emits one hidden input per selected
|
|
8
|
+
id, so frontmatterFromForm's getAll reads them all. EntryPicker owns the search and grouped list; this
|
|
9
|
+
component owns the cardinality, the chips, and the hidden inputs the form submits.
|
|
10
|
+
-->
|
|
11
|
+
<script lang="ts">import { untrack } from "svelte";
|
|
12
|
+
import EntryPicker from "./EntryPicker.svelte";
|
|
13
|
+
let { field, value, targets, ondirty } = $props();
|
|
14
|
+
const concept = $derived.by(() => {
|
|
15
|
+
if (field.type === "array") return field.item.concept;
|
|
16
|
+
if (field.type === "reference") return field.concept;
|
|
17
|
+
return "";
|
|
18
|
+
});
|
|
19
|
+
let singleId = $state(untrack(() => typeof value === "string" ? value : ""));
|
|
20
|
+
let ids = $state(untrack(() => Array.isArray(value) ? [...value] : []));
|
|
21
|
+
let picker = $state(null);
|
|
22
|
+
function titleFor(id) {
|
|
23
|
+
return targets.find((t) => t.concept === concept && t.id === id)?.title ?? id;
|
|
24
|
+
}
|
|
25
|
+
function chooseSingle(target) {
|
|
26
|
+
singleId = target.id;
|
|
27
|
+
ondirty?.();
|
|
28
|
+
}
|
|
29
|
+
function chooseMany(target) {
|
|
30
|
+
if (!ids.includes(target.id)) {
|
|
31
|
+
ids = [...ids, target.id];
|
|
32
|
+
ondirty?.();
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
function remove(id) {
|
|
36
|
+
ids = ids.filter((x) => x !== id);
|
|
37
|
+
ondirty?.();
|
|
38
|
+
}
|
|
39
|
+
</script>
|
|
40
|
+
|
|
41
|
+
{#if field.type === 'array'}
|
|
42
|
+
<fieldset class="m-0 flex min-w-0 flex-col gap-2 border-0 p-0">
|
|
43
|
+
<legend class="text-sm font-medium">{field.label}</legend>
|
|
44
|
+
{#if ids.length}
|
|
45
|
+
<ul class="flex flex-wrap gap-2">
|
|
46
|
+
{#each ids as id (id)}
|
|
47
|
+
<li class="badge badge-ghost gap-1">
|
|
48
|
+
<span>{titleFor(id)}</span>
|
|
49
|
+
<button type="button" class="btn btn-ghost btn-xs btn-square" aria-label={`Remove ${titleFor(id)}`} onclick={() => remove(id)}>
|
|
50
|
+
<svg xmlns="http://www.w3.org/2000/svg" class="h-3 w-3" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M18 6 6 18M6 6l12 12" /></svg>
|
|
51
|
+
</button>
|
|
52
|
+
<input type="hidden" name={field.name} value={id} />
|
|
53
|
+
</li>
|
|
54
|
+
{/each}
|
|
55
|
+
</ul>
|
|
56
|
+
{/if}
|
|
57
|
+
<div>
|
|
58
|
+
<button type="button" class="btn btn-sm btn-ghost" aria-haspopup="dialog" aria-label={`Add ${field.label}`} onclick={() => picker?.open()}>
|
|
59
|
+
Add {field.label}
|
|
60
|
+
</button>
|
|
61
|
+
</div>
|
|
62
|
+
</fieldset>
|
|
63
|
+
<EntryPicker
|
|
64
|
+
bind:this={picker}
|
|
65
|
+
{targets}
|
|
66
|
+
choose={chooseMany}
|
|
67
|
+
conceptFilter={concept}
|
|
68
|
+
selectedIds={ids}
|
|
69
|
+
trigger={false}
|
|
70
|
+
heading={`Choose ${field.label}`}
|
|
71
|
+
searchLabel={`Search ${concept}`}
|
|
72
|
+
emptyText={`No ${concept} to choose.`}
|
|
73
|
+
/>
|
|
74
|
+
{:else}
|
|
75
|
+
<div class="flex flex-col gap-1">
|
|
76
|
+
<span class="text-sm font-medium">{field.label}</span>
|
|
77
|
+
<button type="button" class="btn btn-sm btn-ghost justify-start" aria-haspopup="dialog" aria-label={field.label} onclick={() => picker?.open()}>
|
|
78
|
+
{#if singleId}{titleFor(singleId)}{:else}<span class="text-[var(--color-muted)]">Choose {field.label}</span>{/if}
|
|
79
|
+
</button>
|
|
80
|
+
{#if singleId}
|
|
81
|
+
<input type="hidden" name={field.name} value={singleId} />
|
|
82
|
+
{/if}
|
|
83
|
+
</div>
|
|
84
|
+
<EntryPicker
|
|
85
|
+
bind:this={picker}
|
|
86
|
+
{targets}
|
|
87
|
+
choose={chooseSingle}
|
|
88
|
+
conceptFilter={concept}
|
|
89
|
+
trigger={false}
|
|
90
|
+
heading={`Choose ${field.label}`}
|
|
91
|
+
searchLabel={`Search ${concept}`}
|
|
92
|
+
emptyText={`No ${concept} to choose.`}
|
|
93
|
+
/>
|
|
94
|
+
{/if}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { LinkTarget } from '../content/manifest.js';
|
|
2
|
+
import type { ReferenceField } from '../content/fields.js';
|
|
3
|
+
import type { NamedField } from '../content/types.js';
|
|
4
|
+
interface Props {
|
|
5
|
+
/** The reference or array(reference) descriptor this arm renders. */
|
|
6
|
+
field: NamedField;
|
|
7
|
+
/** The current value: one id for a single reference, a list of ids for an array. */
|
|
8
|
+
value: string | string[];
|
|
9
|
+
/** The site's link targets, from the committed manifest (editLoad ships them). */
|
|
10
|
+
targets: LinkTarget[];
|
|
11
|
+
/** Called when the committed ids change (a pick, an add, or a remove), so the host sets
|
|
12
|
+
* fieldsDirty. The hidden-input writes do not fire the form's oninput, so the field signals
|
|
13
|
+
* dirty explicitly, the same way MediaHeroField does. */
|
|
14
|
+
ondirty?: () => void;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* The reference field editor arm. A single `reference` renders a combobox-style button showing the
|
|
18
|
+
* current target's resolved title (looked up in the site's link targets), opening EntryPicker scoped to
|
|
19
|
+
* the field's concept; on pick it sets the value and emits one hidden input the decoder reads. A many
|
|
20
|
+
* `array(reference)` renders a removable chip list, each chip showing the target's resolved title, plus
|
|
21
|
+
* an EntryPicker that marks the already-held ids and adds another; it emits one hidden input per selected
|
|
22
|
+
* id, so frontmatterFromForm's getAll reads them all. EntryPicker owns the search and grouped list; this
|
|
23
|
+
* component owns the cardinality, the chips, and the hidden inputs the form submits.
|
|
24
|
+
*/
|
|
25
|
+
declare const ReferenceField: import("svelte").Component<Props, {}, "">;
|
|
26
|
+
type ReferenceField = ReturnType<typeof ReferenceField>;
|
|
27
|
+
export default ReferenceField;
|
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
@component
|
|
3
|
+
The repeatable-row editor, the arm for a non-reference `array` container. It renders a list of rows,
|
|
4
|
+
each row either a single leaf (`array(text)`, `array(image)`) or a flat object group
|
|
5
|
+
(`array(object({...}))`), with keyboard-operable add, remove, and reorder. Each row collapses to its
|
|
6
|
+
`itemLabel` summary and expands to edit, the same buries-fewer-fields move the Details panel makes.
|
|
7
|
+
|
|
8
|
+
Rows are wrapped in a `{ id, value }` envelope so node identity follows a row through a reorder or a
|
|
9
|
+
remove and an in-progress edit (or the keyboard focus) never jumps to the wrong row. The id is a
|
|
10
|
+
seed-time counter, not a random uuid, so the server and client agree at hydration. The envelope is
|
|
11
|
+
UI-only; the form names derive from each row's CURRENT position (`${name}.${i}`), so the Task 3
|
|
12
|
+
decoder reads a compact, ordered set. The component seeds once from `rows`; the `{#key entryKey}`
|
|
13
|
+
wrapper in EditPage remounts it on an entry change, so it adds no re-seed effect.
|
|
14
|
+
|
|
15
|
+
A structural mutation (add, remove, reorder) marks the form dirty, because those do not fire the
|
|
16
|
+
form's `oninput`; a leaf edit inside a row does not, because the row inputs sit inside the edit form
|
|
17
|
+
whose `oninput` bubbles. An always-mounted polite live region announces add and remove.
|
|
18
|
+
-->
|
|
19
|
+
<script lang="ts">import { tick, untrack } from "svelte";
|
|
20
|
+
import { sortItems } from "@rodrigodagostino/svelte-sortable-list";
|
|
21
|
+
import FieldInput from "./FieldInput.svelte";
|
|
22
|
+
import ObjectGroupField from "./ObjectGroupField.svelte";
|
|
23
|
+
import ChevronRightIcon from "@lucide/svelte/icons/chevron-right";
|
|
24
|
+
import ChevronDownIcon from "@lucide/svelte/icons/chevron-down";
|
|
25
|
+
import ArrowUpIcon from "@lucide/svelte/icons/arrow-up";
|
|
26
|
+
import ArrowDownIcon from "@lucide/svelte/icons/arrow-down";
|
|
27
|
+
import Trash2Icon from "@lucide/svelte/icons/trash-2";
|
|
28
|
+
import PlusIcon from "@lucide/svelte/icons/plus";
|
|
29
|
+
let {
|
|
30
|
+
field,
|
|
31
|
+
name,
|
|
32
|
+
rows: seedRows,
|
|
33
|
+
targets,
|
|
34
|
+
markFieldsDirty,
|
|
35
|
+
mediaLibrary,
|
|
36
|
+
conceptId,
|
|
37
|
+
id,
|
|
38
|
+
heroFieldRefs,
|
|
39
|
+
onuploaded,
|
|
40
|
+
onheroneedsalt,
|
|
41
|
+
icons
|
|
42
|
+
} = $props();
|
|
43
|
+
let nextId = untrack(() => seedRows.length);
|
|
44
|
+
let rows = $state(untrack(() => seedRows.map((value, i) => ({ id: i, value }))));
|
|
45
|
+
let expanded = $state({});
|
|
46
|
+
let announcement = $state("");
|
|
47
|
+
let addButton = $state(null);
|
|
48
|
+
let root = $state(null);
|
|
49
|
+
const isObjectItem = $derived(field.item.type === "object");
|
|
50
|
+
const rowLabel = $derived(field.label ?? field.name);
|
|
51
|
+
let summaries = $state({});
|
|
52
|
+
function summaryNameFor(index) {
|
|
53
|
+
return isObjectItem && field.itemLabel != null ? `${name}.${index}.${field.itemLabel}` : `${name}.${index}`;
|
|
54
|
+
}
|
|
55
|
+
function onRowInput(row, index, event) {
|
|
56
|
+
const target = event.target;
|
|
57
|
+
if (target.name === summaryNameFor(index)) {
|
|
58
|
+
summaries = { ...summaries, [row.id]: target.value };
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
function summaryFor(value, index, rowId) {
|
|
62
|
+
if (rowId in summaries) {
|
|
63
|
+
const live = summaries[rowId].trim();
|
|
64
|
+
if (live !== "") return live;
|
|
65
|
+
}
|
|
66
|
+
let text = "";
|
|
67
|
+
if (isObjectItem && field.itemLabel != null && value !== null && typeof value === "object") {
|
|
68
|
+
text = String(value[field.itemLabel] ?? "").trim();
|
|
69
|
+
} else if (!isObjectItem && value != null && typeof value !== "object") {
|
|
70
|
+
text = String(value).trim();
|
|
71
|
+
}
|
|
72
|
+
return text !== "" ? text : `${rowLabel} ${index + 1}`;
|
|
73
|
+
}
|
|
74
|
+
function emptyValue() {
|
|
75
|
+
return isObjectItem ? {} : "";
|
|
76
|
+
}
|
|
77
|
+
function toggle(rowId) {
|
|
78
|
+
expanded = { ...expanded, [rowId]: !expanded[rowId] };
|
|
79
|
+
}
|
|
80
|
+
async function add() {
|
|
81
|
+
const row = { id: nextId++, value: emptyValue() };
|
|
82
|
+
rows = [...rows, row];
|
|
83
|
+
expanded = { ...expanded, [row.id]: true };
|
|
84
|
+
markFieldsDirty();
|
|
85
|
+
announcement = "Row added";
|
|
86
|
+
await tick();
|
|
87
|
+
const firstInput = root?.querySelector(
|
|
88
|
+
`[data-cairn-row="${row.id}"] [data-cairn-row-body] :is(input:not([type=hidden]),textarea,select,button)`
|
|
89
|
+
);
|
|
90
|
+
firstInput?.focus();
|
|
91
|
+
}
|
|
92
|
+
async function remove(index) {
|
|
93
|
+
rows = rows.filter((_, i) => i !== index);
|
|
94
|
+
markFieldsDirty();
|
|
95
|
+
announcement = "Row removed";
|
|
96
|
+
await tick();
|
|
97
|
+
const removeButtons = root?.querySelectorAll("[data-cairn-row-remove]") ?? [];
|
|
98
|
+
if (removeButtons[index]) removeButtons[index].focus();
|
|
99
|
+
else if (removeButtons[index - 1]) removeButtons[index - 1].focus();
|
|
100
|
+
else addButton?.focus();
|
|
101
|
+
}
|
|
102
|
+
async function move(index, dir) {
|
|
103
|
+
const target = index + dir;
|
|
104
|
+
if (target < 0 || target >= rows.length) return;
|
|
105
|
+
rows = sortItems(rows, index, target);
|
|
106
|
+
markFieldsDirty();
|
|
107
|
+
await tick();
|
|
108
|
+
const movedRow = root?.querySelectorAll("[data-cairn-row]")[target];
|
|
109
|
+
const opposite = dir === 1 ? "[data-cairn-row-up]" : "[data-cairn-row-down]";
|
|
110
|
+
const focusTarget = movedRow?.querySelector(`${opposite}:not([disabled])`) ?? movedRow?.querySelector("[data-cairn-row-toggle]");
|
|
111
|
+
focusTarget?.focus();
|
|
112
|
+
}
|
|
113
|
+
</script>
|
|
114
|
+
|
|
115
|
+
<fieldset bind:this={root} class="m-0 flex min-w-0 flex-col gap-2 border-0 p-0">
|
|
116
|
+
<legend class="text-sm font-medium">{rowLabel}</legend>
|
|
117
|
+
|
|
118
|
+
{#if rows.length}
|
|
119
|
+
<ul class="flex flex-col gap-2">
|
|
120
|
+
{#each rows as row, i (row.id)}
|
|
121
|
+
{@const rowSummary = summaryFor(row.value, i, row.id)}
|
|
122
|
+
<li
|
|
123
|
+
class="rounded-[var(--radius-field)] border border-[var(--color-base-300)]"
|
|
124
|
+
data-cairn-row={row.id}
|
|
125
|
+
oninput={(e) => onRowInput(row, i, e)}
|
|
126
|
+
>
|
|
127
|
+
<div class="flex items-center gap-1 p-1">
|
|
128
|
+
<button
|
|
129
|
+
type="button"
|
|
130
|
+
class="btn btn-ghost btn-sm flex-1 justify-start gap-2 font-normal"
|
|
131
|
+
data-cairn-row-toggle
|
|
132
|
+
aria-expanded={expanded[row.id] ? 'true' : 'false'}
|
|
133
|
+
onclick={() => toggle(row.id)}
|
|
134
|
+
>
|
|
135
|
+
{#if expanded[row.id]}
|
|
136
|
+
<ChevronDownIcon class="h-4 w-4 shrink-0" aria-hidden="true" />
|
|
137
|
+
{:else}
|
|
138
|
+
<ChevronRightIcon class="h-4 w-4 shrink-0" aria-hidden="true" />
|
|
139
|
+
{/if}
|
|
140
|
+
<span class="truncate">{rowSummary}</span>
|
|
141
|
+
</button>
|
|
142
|
+
<button
|
|
143
|
+
type="button"
|
|
144
|
+
class="btn btn-ghost btn-sm btn-square"
|
|
145
|
+
data-cairn-row-up
|
|
146
|
+
aria-label={`Move ${rowSummary} up`}
|
|
147
|
+
disabled={i === 0}
|
|
148
|
+
onclick={() => move(i, -1)}
|
|
149
|
+
>
|
|
150
|
+
<ArrowUpIcon class="h-4 w-4" aria-hidden="true" />
|
|
151
|
+
</button>
|
|
152
|
+
<button
|
|
153
|
+
type="button"
|
|
154
|
+
class="btn btn-ghost btn-sm btn-square"
|
|
155
|
+
data-cairn-row-down
|
|
156
|
+
aria-label={`Move ${rowSummary} down`}
|
|
157
|
+
disabled={i === rows.length - 1}
|
|
158
|
+
onclick={() => move(i, 1)}
|
|
159
|
+
>
|
|
160
|
+
<ArrowDownIcon class="h-4 w-4" aria-hidden="true" />
|
|
161
|
+
</button>
|
|
162
|
+
<button
|
|
163
|
+
type="button"
|
|
164
|
+
class="btn btn-ghost btn-sm btn-square"
|
|
165
|
+
data-cairn-row-remove
|
|
166
|
+
aria-label={`Remove ${rowSummary}`}
|
|
167
|
+
onclick={() => remove(i)}
|
|
168
|
+
>
|
|
169
|
+
<Trash2Icon class="h-4 w-4" aria-hidden="true" />
|
|
170
|
+
</button>
|
|
171
|
+
</div>
|
|
172
|
+
{#if expanded[row.id]}
|
|
173
|
+
<div data-cairn-row-body class="flex flex-col gap-3 border-t border-[var(--color-base-300)] p-3">
|
|
174
|
+
{#if field.item.type === 'object'}
|
|
175
|
+
<ObjectGroupField
|
|
176
|
+
field={{ ...(field.item as ObjectField), name: field.name }}
|
|
177
|
+
name={`${name}.${i}`}
|
|
178
|
+
frontmatter={(row.value !== null && typeof row.value === 'object' ? row.value : {}) as Record<string, unknown>}
|
|
179
|
+
{targets}
|
|
180
|
+
{markFieldsDirty}
|
|
181
|
+
{mediaLibrary}
|
|
182
|
+
{conceptId}
|
|
183
|
+
{id}
|
|
184
|
+
{heroFieldRefs}
|
|
185
|
+
{onuploaded}
|
|
186
|
+
{onheroneedsalt}
|
|
187
|
+
{icons}
|
|
188
|
+
/>
|
|
189
|
+
{:else}
|
|
190
|
+
<FieldInput
|
|
191
|
+
field={{ ...field.item, name: '_value' }}
|
|
192
|
+
name={`${name}.${i}`}
|
|
193
|
+
frontmatter={{ _value: row.value }}
|
|
194
|
+
{targets}
|
|
195
|
+
{markFieldsDirty}
|
|
196
|
+
{mediaLibrary}
|
|
197
|
+
{conceptId}
|
|
198
|
+
{id}
|
|
199
|
+
{heroFieldRefs}
|
|
200
|
+
{onuploaded}
|
|
201
|
+
{onheroneedsalt}
|
|
202
|
+
{icons}
|
|
203
|
+
/>
|
|
204
|
+
{/if}
|
|
205
|
+
</div>
|
|
206
|
+
{/if}
|
|
207
|
+
</li>
|
|
208
|
+
{/each}
|
|
209
|
+
</ul>
|
|
210
|
+
{/if}
|
|
211
|
+
|
|
212
|
+
<div>
|
|
213
|
+
<button type="button" class="btn btn-sm btn-ghost gap-1" bind:this={addButton} onclick={add}>
|
|
214
|
+
<PlusIcon class="h-4 w-4" aria-hidden="true" />
|
|
215
|
+
Add {rowLabel}
|
|
216
|
+
</button>
|
|
217
|
+
</div>
|
|
218
|
+
|
|
219
|
+
<!-- Always mounted so add/remove announce consistently; a {#if}-gated region announces unevenly. -->
|
|
220
|
+
<div role="status" aria-live="polite" class="sr-only">{announcement}</div>
|
|
221
|
+
</fieldset>
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import type { NamedField } from '../content/types.js';
|
|
2
|
+
import type { ArrayField } from '../content/fields.js';
|
|
3
|
+
import type { LinkTarget } from '../content/manifest.js';
|
|
4
|
+
import type { MediaEntry } from '../media/manifest.js';
|
|
5
|
+
import type { MediaLibraryEntry } from '../media/library-entry.js';
|
|
6
|
+
import type { IconSet } from '../render/glyph.js';
|
|
7
|
+
import type MediaHeroField from './MediaHeroField.svelte';
|
|
8
|
+
interface Props {
|
|
9
|
+
/** The array descriptor to render; its `item` is the per-row leaf or flat object. */
|
|
10
|
+
field: NamedField & ArrayField;
|
|
11
|
+
/** The form name prefix for this list; each row renders at `${name}.${i}`. */
|
|
12
|
+
name: string;
|
|
13
|
+
/** The seed rows: a list of leaf values, or a list of object slices for an object item. */
|
|
14
|
+
rows: unknown[];
|
|
15
|
+
/** The site link targets the reference arm offers (threaded through to each row). */
|
|
16
|
+
targets: LinkTarget[];
|
|
17
|
+
/** Mark the edit form dirty; called on add, remove, and reorder (these skip the form's oninput). */
|
|
18
|
+
markFieldsDirty: () => void;
|
|
19
|
+
/** The merged committed-plus-uploaded media library, keyed by content hash. */
|
|
20
|
+
mediaLibrary: Record<string, MediaLibraryEntry>;
|
|
21
|
+
/** The concept the entry belongs to (the upload action's route param). */
|
|
22
|
+
conceptId: string;
|
|
23
|
+
/** The entry id (the upload action's route param). */
|
|
24
|
+
id: string;
|
|
25
|
+
/** The host's hero-field refs, keyed by the prefixed `name` so two rows do not collide. */
|
|
26
|
+
heroFieldRefs: Record<string, MediaHeroField>;
|
|
27
|
+
/** Called with the server-owned record on a successful upload, so the host merges it. */
|
|
28
|
+
onuploaded: (record: MediaEntry) => void;
|
|
29
|
+
/** Called when a hero's needs-alt status changes, keyed by the prefixed `name`. */
|
|
30
|
+
onheroneedsalt: (name: string, needsAlt: boolean) => void;
|
|
31
|
+
/** The site's icon set, forwarded to each row's icon arm. */
|
|
32
|
+
icons?: IconSet;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* The repeatable-row editor, the arm for a non-reference `array` container. It renders a list of rows,
|
|
36
|
+
* each row either a single leaf (`array(text)`, `array(image)`) or a flat object group
|
|
37
|
+
* (`array(object({...}))`), with keyboard-operable add, remove, and reorder. Each row collapses to its
|
|
38
|
+
* `itemLabel` summary and expands to edit, the same buries-fewer-fields move the Details panel makes.
|
|
39
|
+
*
|
|
40
|
+
* Rows are wrapped in a `{ id, value }` envelope so node identity follows a row through a reorder or a
|
|
41
|
+
* remove and an in-progress edit (or the keyboard focus) never jumps to the wrong row. The id is a
|
|
42
|
+
* seed-time counter, not a random uuid, so the server and client agree at hydration. The envelope is
|
|
43
|
+
* UI-only; the form names derive from each row's CURRENT position (`${name}.${i}`), so the Task 3
|
|
44
|
+
* decoder reads a compact, ordered set. The component seeds once from `rows`; the `{#key entryKey}`
|
|
45
|
+
* wrapper in EditPage remounts it on an entry change, so it adds no re-seed effect.
|
|
46
|
+
*
|
|
47
|
+
* A structural mutation (add, remove, reorder) marks the form dirty, because those do not fire the
|
|
48
|
+
* form's `oninput`; a leaf edit inside a row does not, because the row inputs sit inside the edit form
|
|
49
|
+
* whose `oninput` bubbles. An always-mounted polite live region announces add and remove.
|
|
50
|
+
*/
|
|
51
|
+
declare const RepeatableField: import("svelte").Component<Props, {}, "">;
|
|
52
|
+
type RepeatableField = ReturnType<typeof RepeatableField>;
|
|
53
|
+
export default RepeatableField;
|
|
@@ -5641,6 +5641,10 @@
|
|
|
5641
5641
|
border-color: var(--cairn-error-border);
|
|
5642
5642
|
}
|
|
5643
5643
|
|
|
5644
|
+
:where([data-theme='cairn-admin'], [data-theme='cairn-admin-dark']) .border-\[var\(--color-base-300\)\] {
|
|
5645
|
+
border-color: var(--color-base-300);
|
|
5646
|
+
}
|
|
5647
|
+
|
|
5644
5648
|
:where([data-theme='cairn-admin'], [data-theme='cairn-admin-dark']) .border-\[var\(--color-positive-ink\)\]\/\[0\.22\] {
|
|
5645
5649
|
border-color: var(--color-positive-ink);
|
|
5646
5650
|
}
|
|
@@ -46,9 +46,13 @@ export function buildPreviewDoc(html, preview) {
|
|
|
46
46
|
// parent's base URL, so a clicked fragment or root link could render the admin login inside
|
|
47
47
|
// the frame. Targeting every link at a new tab turns each click into a popup, and the sandbox
|
|
48
48
|
// (which grants no allow-popups) blocks it, so a proofing click goes nowhere.
|
|
49
|
+
// The marker on the root lets a site scope an entrance animation (driven off [data-rise]) away
|
|
50
|
+
// from the preview, which shows the resting state of content and runs the same pipeline; without
|
|
51
|
+
// it, content would re-animate on every debounced render. cairn provides the hook; the site owns
|
|
52
|
+
// its animation and decides what to suppress under [data-cairn-preview].
|
|
49
53
|
return [
|
|
50
54
|
'<!doctype html>',
|
|
51
|
-
'<html>',
|
|
55
|
+
'<html data-cairn-preview>',
|
|
52
56
|
'<head>',
|
|
53
57
|
'<meta charset="utf-8">',
|
|
54
58
|
'<meta name="viewport" content="width=device-width, initial-scale=1">',
|
|
@@ -36,7 +36,7 @@ const DIVERGENCE_FRACTION = 0.5;
|
|
|
36
36
|
// text rather than going through extractMediaRefs for two reasons. First, a true MULTISET is the
|
|
37
37
|
// invariant a backstop wants: extractMediaRefs dedups by hash, so a doubled token collapsing to one
|
|
38
38
|
// would read as equal, and the validator must catch a dropped duplicate. Second, the raw scan covers
|
|
39
|
-
// the whole text including frontmatter without threading the concept's
|
|
39
|
+
// the whole text including frontmatter without threading the concept's NamedField[] to the call
|
|
40
40
|
// site, which the validator otherwise has no reason to know. A token mangled inside a code fence is
|
|
41
41
|
// caught here too, redundantly with the code check, which is the right posture for a backstop.
|
|
42
42
|
const MEDIA_TOKEN = /media:[A-Za-z0-9.-]+/g;
|
package/dist/content/adapter.js
CHANGED
|
@@ -1,4 +1,22 @@
|
|
|
1
|
+
// Fail closed on an inconsistent island registry: a hydrate component with no live component, or a
|
|
2
|
+
// registered island with no hydrate component. Either is a wiring mistake the site author should see at
|
|
3
|
+
// build time, not a silent forever-fallback. Read-only over the rendering group; imports no runtime.
|
|
4
|
+
function assertIslandsConsistent(rendering) {
|
|
5
|
+
const islands = rendering.islands ?? {};
|
|
6
|
+
const hydrated = new Set((rendering.components?.defs ?? []).filter((d) => d.hydrate).map((d) => d.name));
|
|
7
|
+
for (const name of hydrated) {
|
|
8
|
+
if (!(name in islands)) {
|
|
9
|
+
throw new Error(`cairn: component '${name}' declares hydrate but rendering.islands has no entry for it.`);
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
for (const name of Object.keys(islands)) {
|
|
13
|
+
if (!hydrated.has(name)) {
|
|
14
|
+
throw new Error(`cairn: rendering.islands has '${name}' but no component declares hydrate for it.`);
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
}
|
|
1
18
|
/** Declare a site's adapter while preserving each concept's concrete schema type for typed reads. */
|
|
2
19
|
export function defineAdapter(adapter) {
|
|
20
|
+
assertIslandsConsistent(adapter.rendering);
|
|
3
21
|
return adapter;
|
|
4
22
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { ConceptDescriptor } from './types.js';
|
|
2
|
-
import type {
|
|
2
|
+
import type { Backend } from '../github/backend.js';
|
|
3
3
|
import type { Manifest } from './manifest.js';
|
|
4
4
|
/** One action an advisory offers, as a label and an optional link target. */
|
|
5
5
|
export interface AdvisoryAction {
|
|
@@ -45,7 +45,7 @@ export declare function mainAddressIndex(manifest: Manifest): AddressIndex;
|
|
|
45
45
|
* and skipped, so a transient failure degrades to a thinner index, never a thrown editor or a blocked
|
|
46
46
|
* publish. The branches are read in one Promise.all, the way buildUsageIndex reads them.
|
|
47
47
|
*/
|
|
48
|
-
export declare function buildAddressIndex(
|
|
48
|
+
export declare function buildAddressIndex(backend: Backend, concepts: ConceptDescriptor[], manifest: Manifest): Promise<AddressIndex>;
|
|
49
49
|
/**
|
|
50
50
|
* Find the first other entry that already resolves to an address, or null when the address is free
|
|
51
51
|
* or holds only the entry itself. The self entry is identified by its concept and id together.
|
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
import { listBranches } from '../github/branches.js';
|
|
2
|
-
import { readRaw } from '../github/repo.js';
|
|
3
1
|
import { PENDING_PREFIX, parsePendingBranch } from './pending.js';
|
|
4
2
|
import { findConcept } from './concepts.js';
|
|
5
3
|
import { isValidId, filenameFromId } from './ids.js';
|
|
@@ -37,13 +35,13 @@ export function mainAddressIndex(manifest) {
|
|
|
37
35
|
* and skipped, so a transient failure degrades to a thinner index, never a thrown editor or a blocked
|
|
38
36
|
* publish. The branches are read in one Promise.all, the way buildUsageIndex reads them.
|
|
39
37
|
*/
|
|
40
|
-
export async function buildAddressIndex(
|
|
38
|
+
export async function buildAddressIndex(backend, concepts, manifest) {
|
|
41
39
|
// The main arm: the manifest already carries each entry's resolved permalink, so seed from the
|
|
42
40
|
// synchronous main-only index and union the branch arm on top.
|
|
43
41
|
const index = mainAddressIndex(manifest);
|
|
44
42
|
// The branch arm: read each open cairn/* branch's one edited file and resolve its permalink. The
|
|
45
43
|
// path is derivable from the branch name, so no tree-listing is needed.
|
|
46
|
-
const names = await listBranches(
|
|
44
|
+
const names = await backend.listBranches(PENDING_PREFIX);
|
|
47
45
|
const perBranch = await Promise.all(names.map(async (name) => {
|
|
48
46
|
// Resolve the branch name with the branch tooling's guard: a malformed name, an id that fails
|
|
49
47
|
// the slug rule, or an unconfigured concept is skipped with no read attempted.
|
|
@@ -55,7 +53,7 @@ export async function buildAddressIndex(repo, token, concepts, manifest) {
|
|
|
55
53
|
return null;
|
|
56
54
|
const path = `${concept.dir}/${filenameFromId(ref.id)}`;
|
|
57
55
|
try {
|
|
58
|
-
const raw = await
|
|
56
|
+
const raw = await backend.readFile(path, name);
|
|
59
57
|
if (raw === null)
|
|
60
58
|
return null; // The file is absent on the branch: nothing to resolve.
|
|
61
59
|
const { frontmatter } = parseMarkdown(raw);
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import type { CairnAdapter, CairnExtension, CairnRuntime } from './types.js';
|
|
2
2
|
import { type SiteConfig } from '../nav/site-config.js';
|
|
3
3
|
/**
|
|
4
|
-
* The input to {@link composeRuntime}. `siteConfig` is required
|
|
5
|
-
*
|
|
6
|
-
* adapter's concepts.
|
|
4
|
+
* The input to {@link composeRuntime}. `siteConfig` is required: it is the canonical home for the
|
|
5
|
+
* site name, the spellcheck dialect, and the tidy block, so they can never be silently dropped.
|
|
6
|
+
* `extensions` fold in after the adapter's concepts.
|
|
7
7
|
*/
|
|
8
8
|
export interface ComposeInput {
|
|
9
9
|
adapter: CairnAdapter;
|
|
@@ -11,9 +11,10 @@ export interface ComposeInput {
|
|
|
11
11
|
extensions?: CairnExtension[];
|
|
12
12
|
}
|
|
13
13
|
/**
|
|
14
|
-
* Fold an adapter and any extensions into the composed runtime (seam 2).
|
|
15
|
-
*
|
|
16
|
-
*
|
|
14
|
+
* Fold an adapter and any extensions into the composed runtime (seam 2). This is the one place the
|
|
15
|
+
* grouped adapter maps onto the flat runtime, and the one place the internal manifest and dictionary
|
|
16
|
+
* paths default by convention. Each concept declares its own routing and URL policy, so the runtime
|
|
17
|
+
* and delivery permalinks cannot diverge. Extension concepts merge after the adapter's. The media slot
|
|
17
18
|
* (seam 4) passes through untouched.
|
|
18
19
|
*/
|
|
19
20
|
export declare function composeRuntime({ adapter, siteConfig, extensions }: ComposeInput): CairnRuntime;
|
package/dist/content/compose.js
CHANGED
|
@@ -1,15 +1,23 @@
|
|
|
1
1
|
import { resolveConcepts } from './concepts.js';
|
|
2
2
|
import { normalizeAssets } from '../media/config.js';
|
|
3
3
|
import { dictionaryFileForDialect } from '../nav/site-config.js';
|
|
4
|
+
// The internal artifact paths the adapter does not carry. They share the `.cairn/` content root the
|
|
5
|
+
// manifests use, so `composeRuntime` defaults them by convention rather than reading them off config.
|
|
6
|
+
// The personal dictionary sits beside the manifests, so the spec's `content/.cairn/dictionary.txt`
|
|
7
|
+
// resolves the same configurable way the manifest paths do.
|
|
8
|
+
const CONTENT_MANIFEST_PATH = 'src/content/.cairn/index.json';
|
|
9
|
+
const MEDIA_MANIFEST_PATH = 'src/content/.cairn/media.json';
|
|
10
|
+
const DICTIONARY_PATH = 'src/content/.cairn/dictionary.txt';
|
|
4
11
|
/**
|
|
5
|
-
* Fold an adapter and any extensions into the composed runtime (seam 2).
|
|
6
|
-
*
|
|
7
|
-
*
|
|
12
|
+
* Fold an adapter and any extensions into the composed runtime (seam 2). This is the one place the
|
|
13
|
+
* grouped adapter maps onto the flat runtime, and the one place the internal manifest and dictionary
|
|
14
|
+
* paths default by convention. Each concept declares its own routing and URL policy, so the runtime
|
|
15
|
+
* and delivery permalinks cannot diverge. Extension concepts merge after the adapter's. The media slot
|
|
8
16
|
* (seam 4) passes through untouched.
|
|
9
17
|
*/
|
|
10
18
|
export function composeRuntime({ adapter, siteConfig, extensions = [] }) {
|
|
11
19
|
if (!siteConfig)
|
|
12
|
-
throw new Error('composeRuntime needs a site config
|
|
20
|
+
throw new Error('composeRuntime needs a site config for the site name and editor settings');
|
|
13
21
|
const content = { ...adapter.content };
|
|
14
22
|
const adminPanels = [];
|
|
15
23
|
const fieldTypes = [];
|
|
@@ -24,23 +32,21 @@ export function composeRuntime({ adapter, siteConfig, extensions = [] }) {
|
|
|
24
32
|
fieldTypes.push(...extension.fieldTypes);
|
|
25
33
|
}
|
|
26
34
|
return {
|
|
27
|
-
siteName:
|
|
28
|
-
concepts: resolveConcepts(content
|
|
35
|
+
siteName: siteConfig.siteName,
|
|
36
|
+
concepts: resolveConcepts(content),
|
|
29
37
|
backend: adapter.backend,
|
|
30
|
-
sender: adapter.
|
|
31
|
-
supportContact: adapter.supportContact,
|
|
32
|
-
render: adapter.render,
|
|
33
|
-
manifestPath:
|
|
34
|
-
registry: adapter.
|
|
35
|
-
icons: adapter.icons,
|
|
36
|
-
navMenu: adapter.
|
|
37
|
-
preview: adapter.preview,
|
|
38
|
-
assets: adapter.
|
|
39
|
-
resolvedAssets: normalizeAssets(adapter.
|
|
40
|
-
mediaManifestPath:
|
|
41
|
-
|
|
42
|
-
// spec's `content/.cairn/dictionary.txt` resolves the same configurable way the manifest paths do.
|
|
43
|
-
dictionaryPath: adapter.dictionaryPath ?? 'src/content/.cairn/dictionary.txt',
|
|
38
|
+
sender: adapter.email,
|
|
39
|
+
supportContact: adapter.editor?.supportContact,
|
|
40
|
+
render: adapter.rendering.render,
|
|
41
|
+
manifestPath: CONTENT_MANIFEST_PATH,
|
|
42
|
+
registry: adapter.rendering.components,
|
|
43
|
+
icons: adapter.rendering.icons,
|
|
44
|
+
navMenu: adapter.editor?.nav,
|
|
45
|
+
preview: adapter.editor?.preview,
|
|
46
|
+
assets: adapter.media,
|
|
47
|
+
resolvedAssets: normalizeAssets(adapter.media),
|
|
48
|
+
mediaManifestPath: MEDIA_MANIFEST_PATH,
|
|
49
|
+
dictionaryPath: DICTIONARY_PATH,
|
|
44
50
|
// The spellcheck dictionary is resolved once here from the site config's dialect (default US),
|
|
45
51
|
// so the runtime and the editor never re-derive it. The site config is the one home for the
|
|
46
52
|
// dialect; the editor resolves this filename to a real asset URL on the main thread.
|