@cogenta/theme-canonical 0.2.0 → 1.1.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 (108) hide show
  1. package/dist/src/index.d.ts +4 -0
  2. package/dist/src/index.d.ts.map +1 -1
  3. package/dist/src/index.js +4 -0
  4. package/dist/src/index.js.map +1 -1
  5. package/dist/src/render/actions.d.ts +2 -16
  6. package/dist/src/render/actions.d.ts.map +1 -1
  7. package/dist/src/render/actions.js +2 -42
  8. package/dist/src/render/actions.js.map +1 -1
  9. package/dist/src/render/blocks/accordion.d.ts +5 -0
  10. package/dist/src/render/blocks/accordion.d.ts.map +1 -0
  11. package/dist/src/render/blocks/accordion.js +19 -0
  12. package/dist/src/render/blocks/accordion.js.map +1 -0
  13. package/dist/src/render/blocks/collection-list.d.ts +9 -7
  14. package/dist/src/render/blocks/collection-list.d.ts.map +1 -1
  15. package/dist/src/render/blocks/collection-list.js +8 -14
  16. package/dist/src/render/blocks/collection-list.js.map +1 -1
  17. package/dist/src/render/blocks/feature-grid.d.ts.map +1 -1
  18. package/dist/src/render/blocks/feature-grid.js +7 -2
  19. package/dist/src/render/blocks/feature-grid.js.map +1 -1
  20. package/dist/src/render/blocks/logo-strip.d.ts +5 -0
  21. package/dist/src/render/blocks/logo-strip.d.ts.map +1 -0
  22. package/dist/src/render/blocks/logo-strip.js +18 -0
  23. package/dist/src/render/blocks/logo-strip.js.map +1 -0
  24. package/dist/src/render/blocks/pricing-table.d.ts +5 -0
  25. package/dist/src/render/blocks/pricing-table.d.ts.map +1 -0
  26. package/dist/src/render/blocks/pricing-table.js +29 -0
  27. package/dist/src/render/blocks/pricing-table.js.map +1 -0
  28. package/dist/src/render/blocks/stat-counter.d.ts +5 -0
  29. package/dist/src/render/blocks/stat-counter.d.ts.map +1 -0
  30. package/dist/src/render/blocks/stat-counter.js +17 -0
  31. package/dist/src/render/blocks/stat-counter.js.map +1 -0
  32. package/dist/src/render/blocks/testimonial.d.ts +14 -0
  33. package/dist/src/render/blocks/testimonial.d.ts.map +1 -0
  34. package/dist/src/render/blocks/testimonial.js +24 -0
  35. package/dist/src/render/blocks/testimonial.js.map +1 -0
  36. package/dist/src/render/chrome.d.ts +3 -0
  37. package/dist/src/render/chrome.d.ts.map +1 -0
  38. package/dist/src/render/chrome.js +82 -0
  39. package/dist/src/render/chrome.js.map +1 -0
  40. package/dist/src/render/comments.d.ts +3 -0
  41. package/dist/src/render/comments.d.ts.map +1 -0
  42. package/dist/src/render/comments.js +3 -0
  43. package/dist/src/render/comments.js.map +1 -0
  44. package/dist/src/render/entry.d.ts +2 -6
  45. package/dist/src/render/entry.d.ts.map +1 -1
  46. package/dist/src/render/entry.js +2 -34
  47. package/dist/src/render/entry.js.map +1 -1
  48. package/dist/src/render/heading.d.ts +2 -13
  49. package/dist/src/render/heading.d.ts.map +1 -1
  50. package/dist/src/render/heading.js +2 -33
  51. package/dist/src/render/heading.js.map +1 -1
  52. package/dist/src/render/html.d.ts +2 -43
  53. package/dist/src/render/html.d.ts.map +1 -1
  54. package/dist/src/render/html.js +2 -95
  55. package/dist/src/render/html.js.map +1 -1
  56. package/dist/src/render/icons.d.ts +3 -0
  57. package/dist/src/render/icons.d.ts.map +1 -0
  58. package/dist/src/render/icons.js +3 -0
  59. package/dist/src/render/icons.js.map +1 -0
  60. package/dist/src/render/media.d.ts +2 -31
  61. package/dist/src/render/media.d.ts.map +1 -1
  62. package/dist/src/render/media.js +2 -41
  63. package/dist/src/render/media.js.map +1 -1
  64. package/dist/src/render/render-block.d.ts +11 -22
  65. package/dist/src/render/render-block.d.ts.map +1 -1
  66. package/dist/src/render/render-block.js +63 -62
  67. package/dist/src/render/render-block.js.map +1 -1
  68. package/dist/src/render/rich-text.d.ts +2 -4
  69. package/dist/src/render/rich-text.d.ts.map +1 -1
  70. package/dist/src/render/rich-text.js +2 -104
  71. package/dist/src/render/rich-text.js.map +1 -1
  72. package/dist/src/render/term-archive.d.ts +12 -0
  73. package/dist/src/render/term-archive.d.ts.map +1 -0
  74. package/dist/src/render/term-archive.js +39 -0
  75. package/dist/src/render/term-archive.js.map +1 -0
  76. package/dist/src/theme-contract.d.ts +10 -162
  77. package/dist/src/theme-contract.d.ts.map +1 -1
  78. package/dist/src/theme-contract.js +9 -17
  79. package/dist/src/theme-contract.js.map +1 -1
  80. package/dist/theme.config.d.ts +5 -16
  81. package/dist/theme.config.d.ts.map +1 -1
  82. package/dist/theme.config.js +16 -5
  83. package/dist/theme.config.js.map +1 -1
  84. package/package.json +4 -2
  85. package/src/index.ts +4 -0
  86. package/src/render/actions.ts +2 -57
  87. package/src/render/blocks/accordion.ts +44 -0
  88. package/src/render/blocks/collection-list.ts +9 -15
  89. package/src/render/blocks/feature-grid.ts +11 -2
  90. package/src/render/blocks/logo-strip.ts +38 -0
  91. package/src/render/blocks/pricing-table.ts +65 -0
  92. package/src/render/blocks/stat-counter.ts +34 -0
  93. package/src/render/blocks/testimonial.ts +41 -0
  94. package/src/render/chrome.ts +105 -0
  95. package/src/render/comments.ts +6 -0
  96. package/src/render/entry.ts +2 -40
  97. package/src/render/heading.ts +7 -44
  98. package/src/render/html.ts +15 -122
  99. package/src/render/icons.ts +2 -0
  100. package/src/render/media.ts +2 -62
  101. package/src/render/render-block.ts +85 -75
  102. package/src/render/rich-text.ts +2 -150
  103. package/src/render/term-archive.ts +85 -0
  104. package/src/styles/archive.css +40 -0
  105. package/src/styles/base.css +525 -3
  106. package/src/styles/blocks.css +344 -14
  107. package/src/styles/theme.css +1 -0
  108. package/src/theme-contract.ts +23 -155
@@ -9,16 +9,17 @@
9
9
 
10
10
  /* --- hero ----------------------------------------------------------------
11
11
  The page's first impression, so it is the one block that gets a decorative
12
- element of its own: a soft wash of the skin's accent behind the media. It is
13
- drawn on a pseudo-element and sized to stay inside the block's own box
14
- a decoration that widens the page produces a horizontal scrollbar, which is
12
+ element of its own: a flat solid disc of the skin's accent behind the
13
+ media a geometric accent rather than a wash. It is drawn on a
14
+ pseudo-element and sized to stay inside the block's own box a
15
+ decoration that widens the page produces a horizontal scrollbar, which is
15
16
  the classic way a hero breaks a phone.
16
17
  ------------------------------------------------------------------------- */
17
18
 
18
19
  .cg-hero {
19
20
  position: relative;
20
- /* A stacking context of its own, so `z-index: -1` on the wash puts it behind
21
- the hero's content and not behind the page background. */
21
+ /* A stacking context of its own, so `z-index: -1` on the disc puts it
22
+ behind the hero's content and not behind the page background. */
22
23
  isolation: isolate;
23
24
  display: grid;
24
25
  gap: var(--cg-gap-loose);
@@ -34,7 +35,8 @@
34
35
  inline-size: min(30rem, 70%);
35
36
  aspect-ratio: 1;
36
37
  z-index: -1;
37
- background: radial-gradient(circle at 60% 40%, var(--cg-accent-soft), transparent 68%);
38
+ border-radius: 50%;
39
+ background: var(--cg-accent-soft);
38
40
  pointer-events: none;
39
41
  }
40
42
 
@@ -336,17 +338,27 @@
336
338
  color: var(--cg-accent);
337
339
  }
338
340
 
339
- /* The vocabulary's `icon` names a symbol, never markup, and this theme ships no
340
- icon set — so the chip is what the name gets: a tinted, ringed square that
341
- holds the space an icon would, rather than a broken glyph or an invented one. */
341
+ /* The vocabulary's `icon` names a symbol a tinted, ringed square that holds
342
+ the space an icon would, whether or not `renderIcon` (`@cogenta/theme-kit`,
343
+ L25) recognises the name: an unrecognised one still gets the chip, empty,
344
+ exactly the pre-L25 look; a recognised one gets a real glyph, centred and
345
+ coloured to the same accent as the ring around it. */
342
346
  .cg-feature__icon {
343
- display: block;
347
+ display: flex;
348
+ align-items: center;
349
+ justify-content: center;
344
350
  inline-size: var(--cg-s10);
345
351
  block-size: var(--cg-s10);
346
352
  margin-block-end: var(--cg-s5);
347
353
  background: var(--cg-accent-soft);
348
354
  border-radius: var(--cg-radius-md);
349
355
  box-shadow: var(--cg-ring-accent);
356
+ color: var(--cg-accent);
357
+ }
358
+
359
+ .cg-feature__icon svg {
360
+ inline-size: var(--cg-s6);
361
+ block-size: var(--cg-s6);
350
362
  }
351
363
 
352
364
  .cg-feature__title {
@@ -377,13 +389,16 @@
377
389
  text-align: center;
378
390
  }
379
391
 
380
- /* A single diagonal sheen of the accent. `overflow: hidden` on the panel keeps
381
- it inside the radius, so it never leaks past a rounded corner. */
392
+ /* A flat accent bar across the top of the panel structure instead of a
393
+ sheen. `overflow: hidden` on the panel keeps it inside the radius, so it
394
+ never leaks past a rounded corner. */
382
395
  .cg-cta::before {
383
396
  content: "";
384
397
  position: absolute;
385
- inset: 0;
386
- background: linear-gradient(135deg, var(--cg-accent-soft), transparent 55%);
398
+ inset-block-start: 0;
399
+ inset-inline: 0;
400
+ block-size: var(--cg-s2);
401
+ background: var(--cg-accent-soft);
387
402
  pointer-events: none;
388
403
  }
389
404
 
@@ -888,3 +903,318 @@
888
903
  background: var(--cg-accent);
889
904
  color: var(--cg-accent-fg);
890
905
  }
906
+
907
+ /* --- testimonial (blocks@2.0) --------------------------------------------
908
+ Same silhouette as `quote`, left as a distinct rule block for the day the
909
+ two diverge visually — a testimonial wall reads differently from a single
910
+ pull quote once a theme has more than one on a page.
911
+ ------------------------------------------------------------------------- */
912
+
913
+ .cg-testimonial {
914
+ max-inline-size: var(--cg-page-narrow);
915
+ margin-inline: auto;
916
+ text-align: center;
917
+ }
918
+
919
+ .cg-testimonial__quote {
920
+ margin: 0;
921
+ font-family: var(--cogenta-font-serif);
922
+ font-size: var(--cg-text-xl);
923
+ line-height: var(--cg-leading-snug);
924
+ text-wrap: balance;
925
+ }
926
+
927
+ .cg-testimonial__quote > :first-child {
928
+ margin-block-start: 0;
929
+ }
930
+
931
+ .cg-testimonial__quote > :last-child {
932
+ margin-block-end: 0;
933
+ }
934
+
935
+ .cg-testimonial__attribution {
936
+ display: flex;
937
+ align-items: center;
938
+ justify-content: center;
939
+ gap: var(--cg-s3);
940
+ margin-block-start: var(--cg-s6);
941
+ color: var(--cg-ink-subtle);
942
+ font-size: var(--cg-text-sm);
943
+ }
944
+
945
+ .cg-testimonial__avatar {
946
+ inline-size: var(--cg-s12);
947
+ block-size: var(--cg-s12);
948
+ border-radius: var(--cg-radius-pill);
949
+ box-shadow: var(--cg-ring);
950
+ }
951
+
952
+ .cg-testimonial__names {
953
+ display: flex;
954
+ flex-direction: column;
955
+ align-items: flex-start;
956
+ line-height: var(--cg-leading-snug);
957
+ }
958
+
959
+ .cg-testimonial__name {
960
+ font-weight: var(--cg-weight-semibold);
961
+ color: var(--cg-ink);
962
+ }
963
+
964
+ /* --- pricingTable (blocks@2.0) -------------------------------------------
965
+ Cards in a row, the highlighted tier lifted and ringed in the accent —
966
+ the one place in this theme a card earns a border of its own, because a
967
+ pricing table's whole point is telling the tiers apart at a glance.
968
+ ------------------------------------------------------------------------- */
969
+
970
+ .cg-pricing__tiers {
971
+ display: grid;
972
+ gap: var(--cg-gap);
973
+ grid-template-columns: repeat(auto-fit, minmax(15rem, 1fr));
974
+ align-items: start;
975
+ }
976
+
977
+ .cg-pricing__tier {
978
+ display: flex;
979
+ flex-direction: column;
980
+ padding: var(--cg-s8) var(--cg-s6);
981
+ background: var(--cg-surface-raised);
982
+ border-radius: var(--cg-radius-xl);
983
+ box-shadow: var(--cg-ring);
984
+ }
985
+
986
+ .cg-pricing__tier[data-highlighted="true"] {
987
+ background: var(--cg-surface-tinted);
988
+ box-shadow: var(--cg-elevation-2), var(--cg-ring-accent);
989
+ transform: translateY(calc(var(--cg-s3) * -1));
990
+ }
991
+
992
+ .cg-pricing__name {
993
+ margin: 0;
994
+ font-size: var(--cg-text-md);
995
+ font-weight: var(--cg-weight-semibold);
996
+ letter-spacing: var(--cg-tracking-snug);
997
+ }
998
+
999
+ .cg-pricing__price {
1000
+ display: flex;
1001
+ align-items: baseline;
1002
+ gap: var(--cg-s1);
1003
+ margin: var(--cg-s4) 0 var(--cg-s6);
1004
+ }
1005
+
1006
+ .cg-pricing__amount {
1007
+ font-size: var(--cg-text-2xl);
1008
+ font-weight: var(--cg-weight-bold);
1009
+ letter-spacing: var(--cg-tracking-tight);
1010
+ font-variant-numeric: tabular-nums;
1011
+ }
1012
+
1013
+ .cg-pricing__interval {
1014
+ color: var(--cg-ink-muted);
1015
+ font-size: var(--cg-text-sm);
1016
+ }
1017
+
1018
+ .cg-pricing__features {
1019
+ flex: 1;
1020
+ margin: 0 0 var(--cg-s6);
1021
+ padding: 0;
1022
+ list-style: none;
1023
+ display: flex;
1024
+ flex-direction: column;
1025
+ gap: var(--cg-s2);
1026
+ color: var(--cg-ink-muted);
1027
+ line-height: var(--cg-leading-relaxed);
1028
+ }
1029
+
1030
+ .cg-pricing__features li {
1031
+ padding-inline-start: var(--cg-s6);
1032
+ position: relative;
1033
+ }
1034
+
1035
+ /* A drawn checkmark — no icon set, exactly the `faq` chevron's own approach. */
1036
+ .cg-pricing__features li::before {
1037
+ content: "";
1038
+ position: absolute;
1039
+ inset-inline-start: 0;
1040
+ inset-block-start: 0.4em;
1041
+ inline-size: 0.6em;
1042
+ block-size: 0.35em;
1043
+ border-inline-start: 2px solid var(--cg-accent);
1044
+ border-block-end: 2px solid var(--cg-accent);
1045
+ transform: rotate(-45deg);
1046
+ }
1047
+
1048
+ .cg-pricing__action {
1049
+ margin-block-start: auto;
1050
+ }
1051
+
1052
+ .cg-pricing__action .cg-action {
1053
+ inline-size: 100%;
1054
+ justify-content: center;
1055
+ }
1056
+
1057
+ /* --- accordion (blocks@2.0) ----------------------------------------------
1058
+ Identical mechanics to `faq`'s panel, kept as its own rule block so the
1059
+ two can diverge visually without either one's CSS becoming a conditional.
1060
+ ------------------------------------------------------------------------- */
1061
+
1062
+ .cg-accordion {
1063
+ max-inline-size: var(--cg-page-narrow);
1064
+ margin-inline: auto;
1065
+ }
1066
+
1067
+ .cg-accordion__items {
1068
+ background: var(--cg-surface-raised);
1069
+ border-radius: var(--cg-radius-lg);
1070
+ box-shadow: var(--cg-ring);
1071
+ overflow: hidden;
1072
+ }
1073
+
1074
+ .cg-accordion__item + .cg-accordion__item {
1075
+ border-block-start: 1px solid var(--cg-line-soft);
1076
+ }
1077
+
1078
+ .cg-accordion__question {
1079
+ display: flex;
1080
+ align-items: center;
1081
+ gap: var(--cg-s4);
1082
+ padding: var(--cg-s5) var(--cg-s6);
1083
+ font-weight: var(--cg-weight-semibold);
1084
+ cursor: pointer;
1085
+ list-style: none;
1086
+ transition: background-color var(--cg-duration) var(--cg-ease);
1087
+ }
1088
+
1089
+ .cg-accordion__question::-webkit-details-marker {
1090
+ display: none;
1091
+ }
1092
+
1093
+ .cg-accordion__question:hover {
1094
+ background: var(--cg-accent-soft);
1095
+ color: var(--cg-accent-soft-fg);
1096
+ }
1097
+
1098
+ .cg-accordion__question::after {
1099
+ content: "";
1100
+ flex: none;
1101
+ margin-inline-start: auto;
1102
+ inline-size: 0.6em;
1103
+ block-size: 0.6em;
1104
+ border-radius: var(--cg-radius-pill);
1105
+ border: 2px solid currentColor;
1106
+ position: relative;
1107
+ }
1108
+
1109
+ /* A plus that becomes a minus, rather than the `faq` chevron — visually
1110
+ distinct from its sibling block, on the same zero-JavaScript mechanics. */
1111
+ .cg-accordion__question::before {
1112
+ content: "";
1113
+ position: absolute;
1114
+ inset-inline-end: calc(var(--cg-s6) + 0.2em);
1115
+ inline-size: 0.32em;
1116
+ block-size: 0;
1117
+ border-block-start: 2px solid currentColor;
1118
+ }
1119
+
1120
+ .cg-accordion__details[open] .cg-accordion__question::before {
1121
+ content: none;
1122
+ }
1123
+
1124
+ .cg-accordion__details[open] .cg-accordion__question {
1125
+ color: var(--cg-accent);
1126
+ }
1127
+
1128
+ .cg-accordion__answer {
1129
+ padding: 0 var(--cg-s6) var(--cg-s6);
1130
+ color: var(--cg-ink-muted);
1131
+ line-height: var(--cg-leading-relaxed);
1132
+ }
1133
+
1134
+ .cg-accordion__answer > :first-child {
1135
+ margin-block-start: 0;
1136
+ }
1137
+
1138
+ .cg-accordion__answer > :last-child {
1139
+ margin-block-end: 0;
1140
+ }
1141
+
1142
+ /* --- statCounter (blocks@2.0) ---------------------------------------------
1143
+ Same `<dl>` mechanics as `stats`, without a background card: a counter row
1144
+ reads as a single strip of figures rather than as `stats`'s set of panels.
1145
+ ------------------------------------------------------------------------- */
1146
+
1147
+ .cg-stat-counters__items {
1148
+ display: grid;
1149
+ gap: var(--cg-gap);
1150
+ grid-template-columns: repeat(auto-fit, minmax(9rem, 1fr));
1151
+ margin: 0;
1152
+ text-align: center;
1153
+ }
1154
+
1155
+ .cg-stat-counter {
1156
+ display: flex;
1157
+ flex-direction: column-reverse;
1158
+ gap: var(--cg-s2);
1159
+ }
1160
+
1161
+ .cg-stat-counter__value {
1162
+ margin: 0;
1163
+ color: var(--cg-accent);
1164
+ font-size: calc(var(--cg-text-3xl) * 1.15);
1165
+ font-weight: var(--cg-weight-bold);
1166
+ line-height: var(--cg-leading-tight);
1167
+ letter-spacing: var(--cg-tracking-tight);
1168
+ font-variant-numeric: tabular-nums;
1169
+ }
1170
+
1171
+ .cg-stat-counter__label {
1172
+ color: var(--cg-ink-muted);
1173
+ font-size: var(--cg-text-xs);
1174
+ font-weight: var(--cg-weight-semibold);
1175
+ letter-spacing: var(--cg-tracking-wide);
1176
+ text-transform: uppercase;
1177
+ }
1178
+
1179
+ /* --- logoStrip (blocks@2.0) ------------------------------------------------
1180
+ The lighter-weight sibling of `logos`: one row, no card, no link — the
1181
+ "as seen in" strip rather than the fuller partner grid.
1182
+ ------------------------------------------------------------------------- */
1183
+
1184
+ .cg-logo-strip {
1185
+ margin-block: 0;
1186
+ text-align: center;
1187
+ }
1188
+
1189
+ .cg-logo-strip__items {
1190
+ display: flex;
1191
+ flex-wrap: wrap;
1192
+ align-items: center;
1193
+ justify-content: center;
1194
+ gap: var(--cg-gap-loose);
1195
+ margin: 0;
1196
+ padding: 0;
1197
+ list-style: none;
1198
+ }
1199
+
1200
+ .cg-logo-strip__item {
1201
+ display: flex;
1202
+ align-items: center;
1203
+ }
1204
+
1205
+ .cg-logo-strip__image {
1206
+ block-size: var(--cg-s8);
1207
+ inline-size: auto;
1208
+ max-inline-size: 100%;
1209
+ object-fit: contain;
1210
+ opacity: 0.6;
1211
+ }
1212
+
1213
+ .cg-logo-strip__caption {
1214
+ margin-block-start: var(--cg-s4);
1215
+ color: var(--cg-ink-subtle);
1216
+ font-size: var(--cg-text-xs);
1217
+ font-weight: var(--cg-weight-semibold);
1218
+ letter-spacing: var(--cg-tracking-wide);
1219
+ text-transform: uppercase;
1220
+ }
@@ -23,3 +23,4 @@
23
23
  @import "./tokens.css";
24
24
  @import "./base.css";
25
25
  @import "./blocks.css";
26
+ @import "./archive.css";
@@ -1,159 +1,27 @@
1
1
  /**
2
- * Contract D — Thème, frozen at `theme@1.0` on 2026-08-13.
2
+ * Contract D — Thème, `theme@1.0`/`1.1`.
3
3
  *
4
- * TEMPORARY HOME. These types and `defineTheme` belong to `@cogenta/render`,
5
- * which is being written in parallel (tasks 1–3 of L3). They live here so the
6
- * canonical theme can be written, typechecked and tested today, and must be
7
- * replaced by an import from `@cogenta/render` at merge time — not duplicated.
8
- *
9
- * Nothing below adds to `ctx`. Two shapes the contract names but does not spell
10
- * out — `MediaReference` and `ContentEntry` — are given the narrowest reading
11
- * that matches what is already frozen elsewhere; both are flagged in the code.
12
- */
13
-
14
- /**
15
- * Contract B stores a media field as the media library's identifier, never as
16
- * an URL and never as a rendition (`f.media` in `@cogenta/blocks`). So a
17
- * reference the theme can hold is that identifier.
18
- */
19
- export type MediaReference = string
20
-
21
- export interface ImageOptions {
22
- readonly width?: number
23
- readonly height?: number
24
- readonly format?: 'avif' | 'webp' | 'jpeg' | 'png'
25
- readonly fit?: 'cover' | 'contain'
26
- }
27
-
28
- export interface ImageSource {
29
- readonly src: string
30
- readonly srcset: string
31
- readonly width: number
32
- readonly height: number
33
- /** Alt text and focal point come from the media entity, never invented here. */
34
- readonly alt: string
35
- readonly focal: { readonly x: number; readonly y: number } | null
36
- }
37
-
38
- /**
39
- * Contract A, "Champs système". A theme only ever sees published entries — the
40
- * read token carries the `public` role — so `status` is narrow in practice but
41
- * kept whole here rather than guessed at.
42
- *
43
- * The schema-defined fields are open: which of them exist depends on the
44
- * collection, and a theme must not assume one is present. `entryTitle` in
45
- * `src/render/entry.ts` is the only place that reaches for one.
46
- */
47
- export interface ContentEntry {
48
- readonly id: string
49
- readonly collection: string
50
- readonly locale: string
51
- readonly status: 'draft' | 'scheduled' | 'published' | 'archived'
52
- readonly [field: string]: unknown
53
- }
54
-
55
- export interface QueryRequest {
56
- readonly collection: string
57
- readonly filter?: Readonly<Record<string, unknown>>
58
- readonly sort?: { readonly field: string; readonly direction: 'asc' | 'desc' }
59
- readonly limit?: number
60
- readonly cursor?: string
61
- }
62
-
63
- export interface Page<T> {
64
- readonly items: readonly T[]
65
- readonly nextCursor: string | null
66
- }
67
-
68
- export interface ContentClient {
69
- entry(collection: string, id: string): Promise<ContentEntry | null>
70
- byPath(path: string): Promise<ContentEntry | null>
71
- list(request: QueryRequest): Promise<Page<ContentEntry>>
72
- }
73
-
74
- export type LinkTargetInput = { collection: string; id: string } | { path: string } | string
75
-
76
- export interface RenderContext {
77
- readonly site: {
78
- readonly name: string
79
- readonly url: string
80
- readonly locales: readonly string[]
81
- readonly defaultLocale: string
82
- }
83
- /** The locale being rendered. */
84
- readonly locale: string
85
- /** The URL being rendered, already resolved. */
86
- readonly url: URL
87
-
88
- /** Translation. An unknown key returns the key, never an empty string. */
89
- t(key: string, values?: Readonly<Record<string, string | number>>): string
90
-
91
- /** Image variants. Returns what a responsive `<img>` needs, nothing more. */
92
- image(media: MediaReference, options?: ImageOptions): ImageSource
93
-
94
- /** URL of an entry, of a path, or of an external target. Locale-aware. */
95
- link(target: LinkTargetInput): string
96
-
97
- /** Read-only content access. The only door to data a theme has. */
98
- readonly content: ContentClient
99
- }
100
-
101
- /** Contract D, "Besoins runtime". */
102
- export type ThemeRuntime = 'static' | 'server' | 'edge'
103
-
104
- export interface ThemeManifest {
105
- readonly name: string
106
- readonly version: string
107
- /** Version of the theme contract. */
108
- readonly engine: string
109
- /** Version of the block vocabulary supported. */
110
- readonly blocks: string
111
- /** Every vocabulary block the theme renders. A gap fails installation. */
112
- readonly implements: readonly string[]
113
- /** Content types expected, or `'*'`. */
114
- readonly collections: readonly string[] | '*'
115
- readonly runtime: ThemeRuntime
116
- /** Path to the default skin, relative to the manifest. */
117
- readonly tokens: string
118
- readonly a11y: { readonly verified: 'WCAG-2.2-AA' | 'WCAG-2.2-AAA' | 'none' }
119
- }
120
-
121
- /**
122
- * Identity function. It exists for the inference and for the single place a
123
- * manifest is recognised, not to validate: validation happens at installation,
124
- * on the theme's sources, where a lie can actually be caught.
4
+ * Re-exported from `@cogenta/theme-kit`, which now holds the one real copy of
5
+ * this contract every theme package (this one included) implements against
6
+ * see that package's own `contract.ts` for why. This file stays only because
7
+ * `@cogenta/theme-canonical`'s own public surface (`index.ts`'s
8
+ * `export * from './theme-contract.js'`) has always named these symbols, and
9
+ * a theme installed from this package before today must keep resolving them
10
+ * from the same place.
125
11
  */
126
- export function defineTheme<const M extends ThemeManifest>(manifest: M): M {
127
- return manifest
128
- }
129
12
 
130
- /**
131
- * Contract D, "Tokens de skin". The set is **closed and complete**: a skin that
132
- * omits a token is refused. Rendered as `--cogenta-<group>-<name>` into a single
133
- * stylesheet, so changing skin rewrites one file and needs no build.
134
- */
135
- export interface SkinTokens {
136
- readonly color: {
137
- readonly bg: string
138
- readonly fg: string
139
- readonly accent: string
140
- readonly accentFg: string
141
- readonly muted: string
142
- readonly mutedFg: string
143
- readonly border: string
144
- }
145
- readonly font: {
146
- readonly sans: string
147
- readonly serif: string
148
- readonly mono: string
149
- readonly scale: number
150
- readonly baseSize: string
151
- }
152
- readonly space: {
153
- readonly unit: string
154
- readonly density: 'compact' | 'comfortable' | 'spacious'
155
- }
156
- readonly radius: { readonly sm: string; readonly md: string; readonly lg: string }
157
- readonly motion: { readonly duration: string; readonly easing: string; readonly reduced: boolean }
158
- readonly shadow: { readonly sm: string; readonly md: string }
159
- }
13
+ export {
14
+ type ContentClient,
15
+ type ContentEntry,
16
+ defineTheme,
17
+ type ImageOptions,
18
+ type ImageSource,
19
+ type LinkTargetInput,
20
+ type MediaReference,
21
+ type Page,
22
+ type QueryRequest,
23
+ type RenderContext,
24
+ type SkinTokens,
25
+ type ThemeManifest,
26
+ type ThemeRuntime,
27
+ } from '@cogenta/theme-kit'