@glw907/cairn-cms 0.62.2 → 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.
Files changed (196) hide show
  1. package/CHANGELOG.md +216 -0
  2. package/dist/ambient.d.ts +2 -0
  3. package/dist/auth/types.d.ts +7 -0
  4. package/dist/components/CairnAdmin.svelte.d.ts +2 -7
  5. package/dist/components/ComponentForm.svelte +44 -27
  6. package/dist/components/ComponentInsertDialog.svelte +22 -11
  7. package/dist/components/ComponentInsertDialog.svelte.d.ts +2 -6
  8. package/dist/components/ConceptList.svelte +25 -4
  9. package/dist/components/EditPage.svelte +29 -107
  10. package/dist/components/EditPage.svelte.d.ts +2 -7
  11. package/dist/components/EntryPicker.svelte +117 -0
  12. package/dist/components/EntryPicker.svelte.d.ts +35 -0
  13. package/dist/components/FieldInput.svelte +218 -0
  14. package/dist/components/FieldInput.svelte.d.ts +51 -0
  15. package/dist/components/IconPicker.svelte +2 -2
  16. package/dist/components/IconPicker.svelte.d.ts +2 -0
  17. package/dist/components/LinkPicker.svelte +8 -75
  18. package/dist/components/LinkPicker.svelte.d.ts +4 -5
  19. package/dist/components/MediaHeroField.svelte +8 -5
  20. package/dist/components/MediaHeroField.svelte.d.ts +4 -0
  21. package/dist/components/ObjectGroupField.svelte +54 -0
  22. package/dist/components/ObjectGroupField.svelte.d.ts +47 -0
  23. package/dist/components/ReferenceField.svelte +94 -0
  24. package/dist/components/ReferenceField.svelte.d.ts +27 -0
  25. package/dist/components/RepeatableField.svelte +221 -0
  26. package/dist/components/RepeatableField.svelte.d.ts +53 -0
  27. package/dist/components/cairn-admin.css +179 -2
  28. package/dist/components/preview-doc.js +5 -1
  29. package/dist/components/tidy-validate.js +1 -1
  30. package/dist/content/adapter.js +18 -0
  31. package/dist/content/advisories.d.ts +2 -2
  32. package/dist/content/advisories.js +3 -5
  33. package/dist/content/compose.d.ts +7 -6
  34. package/dist/content/compose.js +26 -20
  35. package/dist/content/concepts.d.ts +21 -15
  36. package/dist/content/concepts.js +55 -32
  37. package/dist/content/field-rules.d.ts +15 -0
  38. package/dist/content/field-rules.js +38 -0
  39. package/dist/content/fields.d.ts +169 -0
  40. package/dist/content/fields.js +41 -0
  41. package/dist/content/fieldset.d.ts +107 -0
  42. package/dist/content/fieldset.js +386 -0
  43. package/dist/content/frontmatter-region.d.ts +38 -0
  44. package/dist/content/frontmatter-region.js +75 -0
  45. package/dist/content/frontmatter.d.ts +35 -2
  46. package/dist/content/frontmatter.js +232 -11
  47. package/dist/content/manifest.d.ts +34 -0
  48. package/dist/content/manifest.js +80 -4
  49. package/dist/content/media-refs.d.ts +2 -2
  50. package/dist/content/media-rewrite.js +1 -69
  51. package/dist/content/reference-index.d.ts +56 -0
  52. package/dist/content/reference-index.js +95 -0
  53. package/dist/content/references.d.ts +40 -0
  54. package/dist/content/references.js +0 -0
  55. package/dist/content/standard-schema.d.ts +30 -0
  56. package/dist/content/standard-schema.js +4 -0
  57. package/dist/content/types.d.ts +127 -178
  58. package/dist/delivery/data.d.ts +2 -2
  59. package/dist/delivery/data.js +1 -1
  60. package/dist/delivery/public-routes.d.ts +10 -5
  61. package/dist/delivery/public-routes.js +25 -2
  62. package/dist/delivery/site-descriptors.d.ts +5 -1
  63. package/dist/delivery/site-descriptors.js +8 -3
  64. package/dist/delivery/site-indexes.d.ts +2 -2
  65. package/dist/delivery/site-resolver.d.ts +25 -0
  66. package/dist/delivery/site-resolver.js +49 -0
  67. package/dist/doctor/checks-local.js +6 -11
  68. package/dist/github/backend.d.ts +83 -0
  69. package/dist/github/backend.js +76 -0
  70. package/dist/github/credentials.d.ts +11 -5
  71. package/dist/github/credentials.js +3 -3
  72. package/dist/github/repo.d.ts +8 -19
  73. package/dist/github/repo.js +69 -80
  74. package/dist/github/types.d.ts +1 -1
  75. package/dist/github/types.js +4 -4
  76. package/dist/index.d.ts +18 -10
  77. package/dist/index.js +9 -5
  78. package/dist/islands/index.d.ts +12 -0
  79. package/dist/islands/index.js +83 -0
  80. package/dist/islands/types.d.ts +7 -0
  81. package/dist/islands/types.js +1 -0
  82. package/dist/log/events.d.ts +1 -1
  83. package/dist/media/index.d.ts +1 -1
  84. package/dist/media/index.js +1 -1
  85. package/dist/media/manifest.d.ts +11 -0
  86. package/dist/media/manifest.js +13 -0
  87. package/dist/media/rewrite-plan.d.ts +2 -3
  88. package/dist/media/rewrite-plan.js +2 -3
  89. package/dist/media/usage.d.ts +2 -2
  90. package/dist/media/usage.js +3 -5
  91. package/dist/nav/site-config.d.ts +0 -6
  92. package/dist/nav/site-config.js +6 -4
  93. package/dist/render/component-grammar.js +11 -11
  94. package/dist/render/component-reference.js +5 -3
  95. package/dist/render/component-validate.d.ts +4 -1
  96. package/dist/render/component-validate.js +10 -35
  97. package/dist/render/highlight.d.ts +9 -0
  98. package/dist/render/highlight.js +206 -0
  99. package/dist/render/pipeline.d.ts +0 -6
  100. package/dist/render/pipeline.js +13 -2
  101. package/dist/render/registry.d.ts +44 -36
  102. package/dist/render/registry.js +47 -6
  103. package/dist/render/rehype-dispatch.d.ts +6 -10
  104. package/dist/render/rehype-dispatch.js +38 -17
  105. package/dist/render/remark-directives.js +4 -5
  106. package/dist/render/sanitize-schema.d.ts +10 -0
  107. package/dist/render/sanitize-schema.js +30 -1
  108. package/dist/sveltekit/cairn-admin.d.ts +5 -5
  109. package/dist/sveltekit/cairn-admin.js +3 -4
  110. package/dist/sveltekit/content-routes.d.ts +10 -8
  111. package/dist/sveltekit/content-routes.js +269 -181
  112. package/dist/sveltekit/guard.js +10 -0
  113. package/dist/sveltekit/health.d.ts +7 -3
  114. package/dist/sveltekit/health.js +9 -3
  115. package/dist/sveltekit/index.d.ts +1 -1
  116. package/dist/sveltekit/nav-routes.d.ts +6 -5
  117. package/dist/sveltekit/nav-routes.js +22 -20
  118. package/dist/sveltekit/types.d.ts +2 -0
  119. package/dist/vite/index.d.ts +3 -3
  120. package/dist/vite/index.js +17 -8
  121. package/package.json +17 -2
  122. package/src/lib/ambient.ts +7 -0
  123. package/src/lib/auth/types.ts +7 -0
  124. package/src/lib/components/CairnAdmin.svelte +2 -6
  125. package/src/lib/components/ComponentForm.svelte +48 -27
  126. package/src/lib/components/ComponentInsertDialog.svelte +26 -14
  127. package/src/lib/components/ConceptList.svelte +41 -4
  128. package/src/lib/components/EditPage.svelte +43 -119
  129. package/src/lib/components/EntryPicker.svelte +154 -0
  130. package/src/lib/components/FieldInput.svelte +262 -0
  131. package/src/lib/components/IconPicker.svelte +4 -2
  132. package/src/lib/components/LinkPicker.svelte +10 -81
  133. package/src/lib/components/MediaHeroField.svelte +12 -5
  134. package/src/lib/components/ObjectGroupField.svelte +97 -0
  135. package/src/lib/components/ReferenceField.svelte +126 -0
  136. package/src/lib/components/RepeatableField.svelte +310 -0
  137. package/src/lib/components/preview-doc.ts +5 -1
  138. package/src/lib/components/tidy-validate.ts +1 -1
  139. package/src/lib/content/adapter.ts +21 -0
  140. package/src/lib/content/advisories.ts +4 -7
  141. package/src/lib/content/compose.ts +30 -23
  142. package/src/lib/content/concepts.ts +68 -40
  143. package/src/lib/content/field-rules.ts +39 -0
  144. package/src/lib/content/fields.ts +178 -0
  145. package/src/lib/content/fieldset.ts +470 -0
  146. package/src/lib/content/frontmatter-region.ts +90 -0
  147. package/src/lib/content/frontmatter.ts +231 -15
  148. package/src/lib/content/manifest.ts +101 -4
  149. package/src/lib/content/media-refs.ts +2 -2
  150. package/src/lib/content/media-rewrite.ts +7 -80
  151. package/src/lib/content/reference-index.ts +159 -0
  152. package/src/lib/content/references.ts +0 -0
  153. package/src/lib/content/standard-schema.ts +25 -0
  154. package/src/lib/content/types.ts +128 -195
  155. package/src/lib/delivery/data.ts +2 -2
  156. package/src/lib/delivery/public-routes.ts +36 -4
  157. package/src/lib/delivery/site-descriptors.ts +8 -3
  158. package/src/lib/delivery/site-indexes.ts +2 -2
  159. package/src/lib/delivery/site-resolver.ts +64 -0
  160. package/src/lib/doctor/checks-local.ts +6 -14
  161. package/src/lib/github/backend.ts +161 -0
  162. package/src/lib/github/credentials.ts +10 -7
  163. package/src/lib/github/repo.ts +79 -83
  164. package/src/lib/github/types.ts +5 -5
  165. package/src/lib/index.ts +40 -18
  166. package/src/lib/islands/index.ts +84 -0
  167. package/src/lib/islands/types.ts +11 -0
  168. package/src/lib/log/events.ts +1 -0
  169. package/src/lib/media/index.ts +1 -0
  170. package/src/lib/media/manifest.ts +14 -0
  171. package/src/lib/media/rewrite-plan.ts +4 -6
  172. package/src/lib/media/usage.ts +4 -7
  173. package/src/lib/nav/site-config.ts +8 -9
  174. package/src/lib/render/component-grammar.ts +10 -10
  175. package/src/lib/render/component-reference.ts +4 -3
  176. package/src/lib/render/component-validate.ts +10 -35
  177. package/src/lib/render/highlight.ts +259 -0
  178. package/src/lib/render/pipeline.ts +13 -8
  179. package/src/lib/render/registry.ts +88 -42
  180. package/src/lib/render/rehype-dispatch.ts +47 -16
  181. package/src/lib/render/remark-directives.ts +4 -5
  182. package/src/lib/render/sanitize-schema.ts +32 -1
  183. package/src/lib/sveltekit/cairn-admin.ts +8 -9
  184. package/src/lib/sveltekit/content-routes.ts +330 -221
  185. package/src/lib/sveltekit/guard.ts +15 -0
  186. package/src/lib/sveltekit/health.ts +13 -6
  187. package/src/lib/sveltekit/index.ts +2 -2
  188. package/src/lib/sveltekit/nav-routes.ts +33 -29
  189. package/src/lib/sveltekit/types.ts +5 -1
  190. package/src/lib/vite/index.ts +20 -11
  191. package/dist/content/schema.d.ts +0 -87
  192. package/dist/content/schema.js +0 -89
  193. package/dist/content/validate.d.ts +0 -17
  194. package/dist/content/validate.js +0 -93
  195. package/src/lib/content/schema.ts +0 -167
  196. package/src/lib/content/validate.ts +0 -90
@@ -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;
@@ -3361,6 +3361,10 @@
3361
3361
  top: calc(var(--spacing) * 16);
3362
3362
  }
3363
3363
 
3364
+ :where([data-theme='cairn-admin'], [data-theme='cairn-admin-dark']) .top-\[-3rem\] {
3365
+ top: -3rem;
3366
+ }
3367
+
3364
3368
  :where([data-theme='cairn-admin'], [data-theme='cairn-admin-dark']) .top-\[0\.875rem\] {
3365
3369
  top: .875rem;
3366
3370
  }
@@ -3389,6 +3393,10 @@
3389
3393
  left: calc(var(--spacing) * 2);
3390
3394
  }
3391
3395
 
3396
+ :where([data-theme='cairn-admin'], [data-theme='cairn-admin-dark']) .left-\[var\(--cairn-space-s\)\] {
3397
+ left: var(--cairn-space-s);
3398
+ }
3399
+
3392
3400
  :where([data-theme='cairn-admin'], [data-theme='cairn-admin-dark']) .join {
3393
3401
  --join-ss: 0;
3394
3402
  --join-se: 0;
@@ -3703,6 +3711,10 @@
3703
3711
  z-index: 40;
3704
3712
  }
3705
3713
 
3714
+ :where([data-theme='cairn-admin'], [data-theme='cairn-admin-dark']) .z-50 {
3715
+ z-index: 50;
3716
+ }
3717
+
3706
3718
  :where([data-theme='cairn-admin'], [data-theme='cairn-admin-dark']) .order-1 {
3707
3719
  order: 1;
3708
3720
  }
@@ -4075,6 +4087,10 @@
4075
4087
  margin-top: calc(var(--spacing) * 16);
4076
4088
  }
4077
4089
 
4090
+ :where([data-theme='cairn-admin'], [data-theme='cairn-admin-dark']) .mt-\[var\(--cairn-space-2xl\)\] {
4091
+ margin-top: var(--cairn-space-2xl);
4092
+ }
4093
+
4078
4094
  :where([data-theme='cairn-admin'], [data-theme='cairn-admin-dark']) .mt-auto {
4079
4095
  margin-top: auto;
4080
4096
  }
@@ -4083,6 +4099,10 @@
4083
4099
  margin-top: 1px;
4084
4100
  }
4085
4101
 
4102
+ :where([data-theme='cairn-admin'], [data-theme='cairn-admin-dark']) .-mr-1 {
4103
+ margin-right: calc(var(--spacing) * -1);
4104
+ }
4105
+
4086
4106
  :where([data-theme='cairn-admin'], [data-theme='cairn-admin-dark']) .mr-0\.5 {
4087
4107
  margin-right: calc(var(--spacing) * .5);
4088
4108
  }
@@ -4144,6 +4164,10 @@
4144
4164
  margin-bottom: calc(var(--spacing) * 7);
4145
4165
  }
4146
4166
 
4167
+ :where([data-theme='cairn-admin'], [data-theme='cairn-admin-dark']) .mb-\[var\(--cairn-space-s\)\] {
4168
+ margin-bottom: var(--cairn-space-s);
4169
+ }
4170
+
4147
4171
  :where([data-theme='cairn-admin'], [data-theme='cairn-admin-dark']) .-ml-px {
4148
4172
  margin-left: -1px;
4149
4173
  }
@@ -4702,6 +4726,14 @@
4702
4726
  height: .6875rem;
4703
4727
  }
4704
4728
 
4729
+ :where([data-theme='cairn-admin'], [data-theme='cairn-admin-dark']) .h-\[1\.4rem\] {
4730
+ height: 1.4rem;
4731
+ }
4732
+
4733
+ :where([data-theme='cairn-admin'], [data-theme='cairn-admin-dark']) .h-\[1\.55rem\] {
4734
+ height: 1.55rem;
4735
+ }
4736
+
4705
4737
  :where([data-theme='cairn-admin'], [data-theme='cairn-admin-dark']) .h-\[26px\] {
4706
4738
  height: 26px;
4707
4739
  }
@@ -4758,6 +4790,10 @@
4758
4790
  max-height: 100%;
4759
4791
  }
4760
4792
 
4793
+ :where([data-theme='cairn-admin'], [data-theme='cairn-admin-dark']) .min-h-0 {
4794
+ min-height: calc(var(--spacing) * 0);
4795
+ }
4796
+
4761
4797
  :where([data-theme='cairn-admin'], [data-theme='cairn-admin-dark']) .min-h-6 {
4762
4798
  min-height: calc(var(--spacing) * 6);
4763
4799
  }
@@ -4916,6 +4952,14 @@
4916
4952
  width: .6875rem;
4917
4953
  }
4918
4954
 
4955
+ :where([data-theme='cairn-admin'], [data-theme='cairn-admin-dark']) .w-\[1\.4rem\] {
4956
+ width: 1.4rem;
4957
+ }
4958
+
4959
+ :where([data-theme='cairn-admin'], [data-theme='cairn-admin-dark']) .w-\[1\.55rem\] {
4960
+ width: 1.55rem;
4961
+ }
4962
+
4919
4963
  :where([data-theme='cairn-admin'], [data-theme='cairn-admin-dark']) .w-\[3\.25rem\] {
4920
4964
  width: 3.25rem;
4921
4965
  }
@@ -4968,6 +5012,10 @@
4968
5012
  max-width: 30%;
4969
5013
  }
4970
5014
 
5015
+ :where([data-theme='cairn-admin'], [data-theme='cairn-admin-dark']) .max-w-\[38rem\] {
5016
+ max-width: 38rem;
5017
+ }
5018
+
4971
5019
  :where([data-theme='cairn-admin'], [data-theme='cairn-admin-dark']) .max-w-\[40ch\] {
4972
5020
  max-width: 40ch;
4973
5021
  }
@@ -4992,6 +5040,14 @@
4992
5040
  max-width: 640px;
4993
5041
  }
4994
5042
 
5043
+ :where([data-theme='cairn-admin'], [data-theme='cairn-admin-dark']) .max-w-\[var\(--cairn-measure\)\] {
5044
+ max-width: var(--cairn-measure);
5045
+ }
5046
+
5047
+ :where([data-theme='cairn-admin'], [data-theme='cairn-admin-dark']) .max-w-\[var\(--cairn-measure-wide\)\] {
5048
+ max-width: var(--cairn-measure-wide);
5049
+ }
5050
+
4995
5051
  :where([data-theme='cairn-admin'], [data-theme='cairn-admin-dark']) .max-w-full {
4996
5052
  max-width: 100%;
4997
5053
  }
@@ -5040,7 +5096,7 @@
5040
5096
  flex: none;
5041
5097
  }
5042
5098
 
5043
- :where([data-theme='cairn-admin'], [data-theme='cairn-admin-dark']) .shrink {
5099
+ :where([data-theme='cairn-admin'], [data-theme='cairn-admin-dark']) .flex-shrink, :where([data-theme='cairn-admin'], [data-theme='cairn-admin-dark']) .shrink {
5044
5100
  flex-shrink: 1;
5045
5101
  }
5046
5102
 
@@ -5250,6 +5306,18 @@
5250
5306
  gap: calc(var(--spacing) * 6);
5251
5307
  }
5252
5308
 
5309
+ :where([data-theme='cairn-admin'], [data-theme='cairn-admin-dark']) .gap-\[0\.55rem\] {
5310
+ gap: .55rem;
5311
+ }
5312
+
5313
+ :where([data-theme='cairn-admin'], [data-theme='cairn-admin-dark']) .gap-\[var\(--cairn-space-m\)\] {
5314
+ gap: var(--cairn-space-m);
5315
+ }
5316
+
5317
+ :where([data-theme='cairn-admin'], [data-theme='cairn-admin-dark']) .gap-\[var\(--cairn-space-s\)\] {
5318
+ gap: var(--cairn-space-s);
5319
+ }
5320
+
5253
5321
  :where([data-theme='cairn-admin'], [data-theme='cairn-admin-dark']) .gap-px {
5254
5322
  gap: 1px;
5255
5323
  }
@@ -5476,6 +5544,15 @@
5476
5544
  }
5477
5545
  }
5478
5546
 
5547
+ @layer daisyui.l1.l2 {
5548
+ :where([data-theme='cairn-admin'], [data-theme='cairn-admin-dark']) .badge-outline {
5549
+ color: var(--badge-color);
5550
+ --badge-bg: #0000;
5551
+ background-image: none;
5552
+ border-color: currentColor;
5553
+ }
5554
+ }
5555
+
5479
5556
  :where([data-theme='cairn-admin'], [data-theme='cairn-admin-dark']) .border-\[color-mix\(in_oklab\,var\(--cairn-card-border\)_70\%\,transparent\)\] {
5480
5557
  border-color: var(--cairn-card-border);
5481
5558
  }
@@ -5546,7 +5623,7 @@
5546
5623
  }
5547
5624
  }
5548
5625
 
5549
- :where([data-theme='cairn-admin'], [data-theme='cairn-admin-dark']) .border-\[var\(--cairn-card-border\)\] {
5626
+ :where([data-theme='cairn-admin'], [data-theme='cairn-admin-dark']) .border-\[color\:var\(--cairn-card-border\)\], :where([data-theme='cairn-admin'], [data-theme='cairn-admin-dark']) .border-\[var\(--cairn-card-border\)\] {
5550
5627
  border-color: var(--cairn-card-border);
5551
5628
  }
5552
5629
 
@@ -5564,6 +5641,10 @@
5564
5641
  border-color: var(--cairn-error-border);
5565
5642
  }
5566
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
+
5567
5648
  :where([data-theme='cairn-admin'], [data-theme='cairn-admin-dark']) .border-\[var\(--color-positive-ink\)\]\/\[0\.22\] {
5568
5649
  border-color: var(--color-positive-ink);
5569
5650
  }
@@ -6245,6 +6326,18 @@
6245
6326
  padding-inline: calc(var(--spacing) * 7);
6246
6327
  }
6247
6328
 
6329
+ :where([data-theme='cairn-admin'], [data-theme='cairn-admin-dark']) .px-\[0\.1rem\] {
6330
+ padding-inline: .1rem;
6331
+ }
6332
+
6333
+ :where([data-theme='cairn-admin'], [data-theme='cairn-admin-dark']) .px-\[0\.9rem\] {
6334
+ padding-inline: .9rem;
6335
+ }
6336
+
6337
+ :where([data-theme='cairn-admin'], [data-theme='cairn-admin-dark']) .px-\[var\(--cairn-space-m\)\] {
6338
+ padding-inline: var(--cairn-space-m);
6339
+ }
6340
+
6248
6341
  :where([data-theme='cairn-admin'], [data-theme='cairn-admin-dark']) .px-px {
6249
6342
  padding-inline: 1px;
6250
6343
  }
@@ -6313,10 +6406,26 @@
6313
6406
  padding-block: calc(var(--spacing) * 16);
6314
6407
  }
6315
6408
 
6409
+ :where([data-theme='cairn-admin'], [data-theme='cairn-admin-dark']) .py-\[0\.3rem\] {
6410
+ padding-block: .3rem;
6411
+ }
6412
+
6413
+ :where([data-theme='cairn-admin'], [data-theme='cairn-admin-dark']) .py-\[0\.5rem\] {
6414
+ padding-block: .5rem;
6415
+ }
6416
+
6316
6417
  :where([data-theme='cairn-admin'], [data-theme='cairn-admin-dark']) .py-\[5px\] {
6317
6418
  padding-block: 5px;
6318
6419
  }
6319
6420
 
6421
+ :where([data-theme='cairn-admin'], [data-theme='cairn-admin-dark']) .py-\[var\(--cairn-space-xl\)\] {
6422
+ padding-block: var(--cairn-space-xl);
6423
+ }
6424
+
6425
+ :where([data-theme='cairn-admin'], [data-theme='cairn-admin-dark']) .py-\[var\(--cairn-space-xs\)\] {
6426
+ padding-block: var(--cairn-space-xs);
6427
+ }
6428
+
6320
6429
  :where([data-theme='cairn-admin'], [data-theme='cairn-admin-dark']) .py-px {
6321
6430
  padding-block: 1px;
6322
6431
  }
@@ -6341,6 +6450,18 @@
6341
6450
  padding-top: calc(var(--spacing) * 4);
6342
6451
  }
6343
6452
 
6453
+ :where([data-theme='cairn-admin'], [data-theme='cairn-admin-dark']) .pt-\[var\(--cairn-space-l\)\] {
6454
+ padding-top: var(--cairn-space-l);
6455
+ }
6456
+
6457
+ :where([data-theme='cairn-admin'], [data-theme='cairn-admin-dark']) .pt-\[var\(--cairn-space-s\)\] {
6458
+ padding-top: var(--cairn-space-s);
6459
+ }
6460
+
6461
+ :where([data-theme='cairn-admin'], [data-theme='cairn-admin-dark']) .pr-1 {
6462
+ padding-right: calc(var(--spacing) * 1);
6463
+ }
6464
+
6344
6465
  :where([data-theme='cairn-admin'], [data-theme='cairn-admin-dark']) .pr-3 {
6345
6466
  padding-right: calc(var(--spacing) * 3);
6346
6467
  }
@@ -6353,6 +6474,10 @@
6353
6474
  padding-bottom: calc(var(--spacing) * 1.5);
6354
6475
  }
6355
6476
 
6477
+ :where([data-theme='cairn-admin'], [data-theme='cairn-admin-dark']) .pb-\[var\(--cairn-space-xl\)\] {
6478
+ padding-bottom: var(--cairn-space-xl);
6479
+ }
6480
+
6356
6481
  :where([data-theme='cairn-admin'], [data-theme='cairn-admin-dark']) .pl-3 {
6357
6482
  padding-left: calc(var(--spacing) * 3);
6358
6483
  }
@@ -6377,6 +6502,10 @@
6377
6502
  text-align: right;
6378
6503
  }
6379
6504
 
6505
+ :where([data-theme='cairn-admin'], [data-theme='cairn-admin-dark']) .font-\[family-name\:var\(--font-body\)\] {
6506
+ font-family: var(--font-body);
6507
+ }
6508
+
6380
6509
  :where([data-theme='cairn-admin'], [data-theme='cairn-admin-dark']) .font-\[family-name\:var\(--font-display\)\] {
6381
6510
  font-family: var(--font-display);
6382
6511
  }
@@ -6482,12 +6611,34 @@
6482
6611
  font-size: 1.0625rem;
6483
6612
  }
6484
6613
 
6614
+ :where([data-theme='cairn-admin'], [data-theme='cairn-admin-dark']) .text-\[length\:var\(--cairn-step--1\)\] {
6615
+ font-size: var(--cairn-step--1);
6616
+ }
6617
+
6618
+ :where([data-theme='cairn-admin'], [data-theme='cairn-admin-dark']) .text-\[length\:var\(--cairn-step-1\)\] {
6619
+ font-size: var(--cairn-step-1);
6620
+ }
6621
+
6622
+ :where([data-theme='cairn-admin'], [data-theme='cairn-admin-dark']) .text-\[length\:var\(--cairn-step-5\)\] {
6623
+ font-size: var(--cairn-step-5);
6624
+ }
6625
+
6485
6626
  @layer daisyui.l1.l2 {
6486
6627
  :where([data-theme='cairn-admin'], [data-theme='cairn-admin-dark']) .textarea-sm {
6487
6628
  font-size: max(var(--font-size, .75rem), .75rem);
6488
6629
  }
6489
6630
  }
6490
6631
 
6632
+ :where([data-theme='cairn-admin'], [data-theme='cairn-admin-dark']) .leading-\[var\(--cairn-leading-snug\)\] {
6633
+ --tw-leading: var(--cairn-leading-snug);
6634
+ line-height: var(--cairn-leading-snug);
6635
+ }
6636
+
6637
+ :where([data-theme='cairn-admin'], [data-theme='cairn-admin-dark']) .leading-\[var\(--cairn-leading-tight\)\] {
6638
+ --tw-leading: var(--cairn-leading-tight);
6639
+ line-height: var(--cairn-leading-tight);
6640
+ }
6641
+
6491
6642
  :where([data-theme='cairn-admin'], [data-theme='cairn-admin-dark']) .leading-relaxed {
6492
6643
  --tw-leading: var(--leading-relaxed);
6493
6644
  line-height: var(--leading-relaxed);
@@ -6548,6 +6699,16 @@
6548
6699
  letter-spacing: .12em;
6549
6700
  }
6550
6701
 
6702
+ :where([data-theme='cairn-admin'], [data-theme='cairn-admin-dark']) .tracking-\[var\(--cairn-tracking-eyebrow\)\] {
6703
+ --tw-tracking: var(--cairn-tracking-eyebrow);
6704
+ letter-spacing: var(--cairn-tracking-eyebrow);
6705
+ }
6706
+
6707
+ :where([data-theme='cairn-admin'], [data-theme='cairn-admin-dark']) .tracking-\[var\(--cairn-tracking-tight\)\] {
6708
+ --tw-tracking: var(--cairn-tracking-tight);
6709
+ letter-spacing: var(--cairn-tracking-tight);
6710
+ }
6711
+
6551
6712
  :where([data-theme='cairn-admin'], [data-theme='cairn-admin-dark']) .tracking-tight {
6552
6713
  --tw-tracking: var(--tracking-tight);
6553
6714
  letter-spacing: var(--tracking-tight);
@@ -6598,6 +6759,18 @@
6598
6759
  }
6599
6760
  }
6600
6761
 
6762
+ :where([data-theme='cairn-admin'], [data-theme='cairn-admin-dark']) .text-\[Npx\] {
6763
+ color: Npx;
6764
+ }
6765
+
6766
+ :where([data-theme='cairn-admin'], [data-theme='cairn-admin-dark']) .text-\[Nrem\] {
6767
+ color: Nrem;
6768
+ }
6769
+
6770
+ :where([data-theme='cairn-admin'], [data-theme='cairn-admin-dark']) .text-\[color\:var\(--cairn-muted\)\] {
6771
+ color: var(--cairn-muted);
6772
+ }
6773
+
6601
6774
  :where([data-theme='cairn-admin'], [data-theme='cairn-admin-dark']) .text-\[var\(--cairn-error-ink\)\] {
6602
6775
  color: var(--cairn-error-ink);
6603
6776
  }
@@ -7358,6 +7531,10 @@
7358
7531
  }
7359
7532
  }
7360
7533
 
7534
+ :where([data-theme='cairn-admin'], [data-theme='cairn-admin-dark']) .focus\:top-\[var\(--cairn-space-s\)\]:focus {
7535
+ top: var(--cairn-space-s);
7536
+ }
7537
+
7361
7538
  :where([data-theme='cairn-admin'], [data-theme='cairn-admin-dark']) .focus-visible\:border-\[color-mix\(in_oklab\,var\(--color-primary\)_70\%\,transparent\)\]:focus-visible {
7362
7539
  border-color: var(--color-primary);
7363
7540
  }
@@ -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 FrontmatterField[] to the call
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;
@@ -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 { RepoRef } from '../github/types.js';
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(repo: RepoRef, token: string, concepts: ConceptDescriptor[], manifest: Manifest): Promise<AddressIndex>;
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(repo, token, concepts, manifest) {
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(repo, PENDING_PREFIX, token);
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 readRaw({ ...repo, branch: name }, path, token);
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 so the per-concept URL policy is
5
- * always derived from one source and can never be silently dropped. `extensions` fold in after the
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). The per-concept URL policy
15
- * is derived from the site config, the same source the delivery path uses, so the runtime and
16
- * delivery permalinks cannot diverge. Extension concepts merge after the adapter's. The asset slot
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;
@@ -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). The per-concept URL policy
6
- * is derived from the site config, the same source the delivery path uses, so the runtime and
7
- * delivery permalinks cannot diverge. Extension concepts merge after the adapter's. The asset slot
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 to derive the URL policy');
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: adapter.siteName,
28
- concepts: resolveConcepts(content, siteConfig),
35
+ siteName: siteConfig.siteName,
36
+ concepts: resolveConcepts(content),
29
37
  backend: adapter.backend,
30
- sender: adapter.sender,
31
- supportContact: adapter.supportContact,
32
- render: adapter.render,
33
- manifestPath: adapter.manifestPath ?? 'src/content/.cairn/index.json',
34
- registry: adapter.registry,
35
- icons: adapter.icons,
36
- navMenu: adapter.navMenu,
37
- preview: adapter.preview,
38
- assets: adapter.assets,
39
- resolvedAssets: normalizeAssets(adapter.assets),
40
- mediaManifestPath: adapter.mediaManifestPath ?? 'src/content/.cairn/media.json',
41
- // The personal dictionary sits beside the manifests under the same `.cairn/` content root, so the
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.
@@ -1,24 +1,30 @@
1
1
  import type { ConceptConfig, ConceptDescriptor, ConceptUrlPolicy, RoutingRule } from './types.js';
2
- import { type SiteConfig } from '../nav/site-config.js';
2
+ /** Expand a concept's routing shorthand to a concrete rule. The single resolution point: omitted is `page`. */
3
+ export declare function resolveRouting(routing: ConceptConfig['routing']): RoutingRule;
3
4
  /**
4
- * Concept-fixed routing, keyed by concept id (spec §7.2). Posts are dated feed entries;
5
- * pages are plain navigable structure. Not in adapter config. A future Fragments adds one
6
- * entry here and one key under `content`.
5
+ * Declare a concept while preserving its fieldset type for typed reads, and validate its URL policy at
6
+ * declaration so a bad permalink or datePrefix fails at module load rather than at a defaulted render.
7
+ * Mirrors {@link defineAdapter}; the validation is the build-independent net for a concept with no entries.
7
8
  */
8
- export declare const CONCEPT_ROUTING: Readonly<Record<string, RoutingRule>>;
9
+ export declare function defineConcept<const C extends ConceptConfig>(concept: C): C;
9
10
  /**
10
- * Normalize an adapter's declared concepts into uniform descriptors (seam 1). URL policy
11
- * (`permalink`, `datePrefix`) comes from the YAML site-config, passed here as `urlPolicy` keyed by
12
- * concept id; each value defaults when the YAML omits it (`/:slug` for Pages, `/<id>/:slug`
13
- * otherwise; `datePrefix` defaults to `day`). `routing` is injectable so a contract test can prove
14
- * a new concept attaches additively; production passes the default `CONCEPT_ROUTING`.
11
+ * Validate one concept's URL policy at build, so a misconfigured permalink or datePrefix fails loudly
12
+ * here rather than emitting a wrong or defaulted URL at render. The permalink must be root-relative and
13
+ * use only known tokens, a date token requires a dated concept, and the datePrefix must be in range.
15
14
  */
16
- export declare function normalizeConcepts(content: Record<string, ConceptConfig | undefined>, urlPolicy?: Record<string, ConceptUrlPolicy | undefined>, routing?: Readonly<Record<string, RoutingRule>>): ConceptDescriptor[];
15
+ export declare function validateUrlPolicy(id: string, policy: ConceptUrlPolicy, dated: boolean): void;
17
16
  /**
18
- * Resolve a site's concept descriptors from its content map and parsed site config. The admin runtime
19
- * (composeRuntime) and the delivery layer (siteDescriptors) both call this, so the per-concept URL
20
- * policy is derived once from the YAML and the runtime and delivery permalinks cannot diverge.
17
+ * Normalize an adapter's declared concepts into uniform descriptors (seam 1). Each concept declares its
18
+ * own routing (a shorthand or an explicit rule, resolved by `resolveRouting`) and URL policy
19
+ * (`permalink`, `datePrefix`) on the config; both default when omitted (`/:slug` for Pages, `/<id>/:slug`
20
+ * otherwise; `datePrefix` defaults to `day`). A new concept attaches by adding one key under `content`.
21
21
  */
22
- export declare function resolveConcepts(content: Record<string, ConceptConfig | undefined>, siteConfig: SiteConfig): ConceptDescriptor[];
22
+ export declare function normalizeConcepts(content: Record<string, ConceptConfig | undefined>): ConceptDescriptor[];
23
+ /**
24
+ * Resolve a site's concept descriptors from its content map. The admin runtime (composeRuntime) and the
25
+ * delivery layer (siteDescriptors) both call this, so the per-concept routing and URL policy are derived
26
+ * once from the concept declarations and the runtime and delivery permalinks cannot diverge.
27
+ */
28
+ export declare function resolveConcepts(content: Record<string, ConceptConfig | undefined>): ConceptDescriptor[];
23
29
  /** Look up a normalized concept by id, or undefined when the site does not enable it. */
24
30
  export declare function findConcept(concepts: ConceptDescriptor[], id: string): ConceptDescriptor | undefined;