@cogenta/theme-kit 0.3.2

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 (66) hide show
  1. package/LICENSE +373 -0
  2. package/dist/actions.d.ts +17 -0
  3. package/dist/actions.d.ts.map +1 -0
  4. package/dist/actions.js +43 -0
  5. package/dist/actions.js.map +1 -0
  6. package/dist/archive.d.ts +85 -0
  7. package/dist/archive.d.ts.map +1 -0
  8. package/dist/archive.js +21 -0
  9. package/dist/archive.js.map +1 -0
  10. package/dist/chrome.d.ts +161 -0
  11. package/dist/chrome.d.ts.map +1 -0
  12. package/dist/chrome.js +198 -0
  13. package/dist/chrome.js.map +1 -0
  14. package/dist/comments.d.ts +54 -0
  15. package/dist/comments.d.ts.map +1 -0
  16. package/dist/comments.js +68 -0
  17. package/dist/comments.js.map +1 -0
  18. package/dist/contract.d.ts +97 -0
  19. package/dist/contract.d.ts.map +1 -0
  20. package/dist/contract.js +31 -0
  21. package/dist/contract.js.map +1 -0
  22. package/dist/entry-header.d.ts +62 -0
  23. package/dist/entry-header.d.ts.map +1 -0
  24. package/dist/entry-header.js +65 -0
  25. package/dist/entry-header.js.map +1 -0
  26. package/dist/entry.d.ts +21 -0
  27. package/dist/entry.d.ts.map +1 -0
  28. package/dist/entry.js +65 -0
  29. package/dist/entry.js.map +1 -0
  30. package/dist/heading.d.ts +14 -0
  31. package/dist/heading.d.ts.map +1 -0
  32. package/dist/heading.js +34 -0
  33. package/dist/heading.js.map +1 -0
  34. package/dist/html.d.ts +42 -0
  35. package/dist/html.d.ts.map +1 -0
  36. package/dist/html.js +94 -0
  37. package/dist/html.js.map +1 -0
  38. package/dist/icons.d.ts +37 -0
  39. package/dist/icons.d.ts.map +1 -0
  40. package/dist/icons.js +187 -0
  41. package/dist/icons.js.map +1 -0
  42. package/dist/index.d.ts +27 -0
  43. package/dist/index.d.ts.map +1 -0
  44. package/dist/index.js +27 -0
  45. package/dist/index.js.map +1 -0
  46. package/dist/media.d.ts +45 -0
  47. package/dist/media.d.ts.map +1 -0
  48. package/dist/media.js +70 -0
  49. package/dist/media.js.map +1 -0
  50. package/dist/page.d.ts +104 -0
  51. package/dist/page.d.ts.map +1 -0
  52. package/dist/page.js +134 -0
  53. package/dist/page.js.map +1 -0
  54. package/dist/rich-text.d.ts +5 -0
  55. package/dist/rich-text.d.ts.map +1 -0
  56. package/dist/rich-text.js +125 -0
  57. package/dist/rich-text.js.map +1 -0
  58. package/dist/strings.d.ts +30 -0
  59. package/dist/strings.d.ts.map +1 -0
  60. package/dist/strings.js +124 -0
  61. package/dist/strings.js.map +1 -0
  62. package/dist/theme-toggle.d.ts +69 -0
  63. package/dist/theme-toggle.d.ts.map +1 -0
  64. package/dist/theme-toggle.js +136 -0
  65. package/dist/theme-toggle.js.map +1 -0
  66. package/package.json +43 -0
@@ -0,0 +1,161 @@
1
+ /**
2
+ * The chrome extension point — the one piece of a rendered page that used to
3
+ * live outside every theme entirely: `cogenta serve` used to hand-write the
4
+ * `<header>`/`<footer>` markup itself, hardcoded to one theme's CSS classes,
5
+ * which is exactly what made a second theme impossible to install without
6
+ * either fighting the first theme's chrome or losing chrome altogether.
7
+ *
8
+ * A theme now owns its own header and footer, the same way it owns its own
9
+ * block layouts — `cogenta serve` resolves the navigation data and the
10
+ * CMS-wide branding fragment (never a theme's business, see `brandingHtml`
11
+ * below) and hands both to the active theme's `renderChrome`.
12
+ */
13
+ import type { ImageSource } from './contract.js';
14
+ import { type HtmlElement } from './html.js';
15
+ /**
16
+ * The site's own visual identity, as the appearance screen already stores it
17
+ * — logo, dark-scheme logo, favicon (contract D `theme@1.3`).
18
+ *
19
+ * Optional on `ChromeInput` on purpose: a host that predates this field, and
20
+ * a theme that chooses to ignore it, both keep the pre-1.3 behaviour of
21
+ * setting the site *name* as text. A theme that does use it shows `logo`
22
+ * **instead of** the name, and keeps the name as the image's accessible name
23
+ * — never both, and never a logo with no accessible name.
24
+ *
25
+ * `logo`/`logoDark` are `ImageSource`s, not raw URLs, so a logo gets the same
26
+ * `srcset` and intrinsic-size guarantees every other image on the page has.
27
+ * `logoDark` is a *second* source, never a replacement: choosing between the
28
+ * two is a `prefers-color-scheme` decision the browser makes at render time,
29
+ * never one the server takes on the visitor's behalf — the server has no way
30
+ * of knowing which scheme the visitor is in.
31
+ *
32
+ * `faviconUrl` travels here so a site's whole identity is one object, but it
33
+ * is the *host* that writes `<link rel="icon">`: a theme owns `<header>` and
34
+ * `<footer>`, never `<head>`.
35
+ */
36
+ export interface ChromeBrand {
37
+ /** The site's own name — what a theme shows when there is no logo, and the logo's accessible name when there is one. */
38
+ readonly name: string;
39
+ /** `null` when no logo is set, when the chosen media is gone, or when it is not an image. */
40
+ readonly logo: ImageSource | null;
41
+ /** The dark-scheme variant, or `null`. Offered beside `logo`, never instead of it. */
42
+ readonly logoDark: ImageSource | null;
43
+ /** Site-relative URL of the favicon, or `null` for the host's own default. Rendered by the host, not by a theme. */
44
+ readonly faviconUrl: string | null;
45
+ }
46
+ export interface ChromeNavLink {
47
+ readonly label: string;
48
+ /** `null` for a `submenu-placeholder` — an unlinked heading, not a dead link (a dead link is dropped before it reaches here). */
49
+ readonly href: string | null;
50
+ readonly openInNewTab: boolean;
51
+ readonly kind: string;
52
+ /** The HTML `title` attribute (a tooltip), or `null`. Never this link's visible label. */
53
+ readonly title: string | null;
54
+ }
55
+ /** A labelled destination — a social profile, a header call-to-action. Nothing more, on purpose: contract D never carries an icon choice or an emphasis for these, only a URL and the words that name it. */
56
+ export interface ChromeLink {
57
+ readonly label: string;
58
+ readonly href: string;
59
+ }
60
+ export interface ChromeInput {
61
+ readonly site: {
62
+ readonly name: string;
63
+ };
64
+ readonly locale: string;
65
+ /** `/`, already locale-resolved — the target of the site's own "home" link. */
66
+ readonly homeHref: string;
67
+ readonly headerNav: readonly ChromeNavLink[];
68
+ readonly footerNav: readonly ChromeNavLink[];
69
+ /**
70
+ * Cogenta's own credit, or its white-label replacement, or `''` for
71
+ * neither (fiche L21 task 8) — already-escaped HTML, computed once by
72
+ * `cogenta serve` from a site-wide setting no theme may reinterpret. A
73
+ * theme places this fragment somewhere inside its own footer; it must not
74
+ * alter or drop it on its own initiative.
75
+ */
76
+ readonly brandingHtml: string;
77
+ /**
78
+ * The site's own identity (contract D `theme@1.3`). Absent means the
79
+ * pre-1.3 behaviour — the site name, as text — which is what a host that
80
+ * never wired this and a site that never uploaded a logo both get.
81
+ */
82
+ readonly brand?: ChromeBrand;
83
+ /**
84
+ * `general.tagline` (contract D `theme@1.4`), in the page's own locale.
85
+ * Optional and additive: a theme that never reads it, and a site that has
86
+ * never set one, both render exactly as they did under `theme@1.3` — this
87
+ * is what makes the version bump minor rather than major. A theme that does
88
+ * show it places it beside the brand mark or in the footer's own "about"
89
+ * column; it is prose, not a second `<h1>`.
90
+ */
91
+ readonly tagline?: string;
92
+ /**
93
+ * `general.socialLinks` (contract D `theme@1.4`) — the site's own social
94
+ * profiles, already resolved to `{label, href}` pairs. Absent or empty
95
+ * means "nothing configured", identical to a pre-1.4 render. A theme is
96
+ * free to render these itself, but `renderSocialLinks` below is the shared,
97
+ * icon-carrying way every built-in theme does it, so five themes do not
98
+ * each draw their own X/Mastodon/Bluesky glyphs from scratch.
99
+ */
100
+ readonly social?: readonly ChromeLink[];
101
+ /**
102
+ * `general.footerNote` (contract D `theme@1.4`) — a short, site-wide line
103
+ * an editor writes once (a legal mention, an "about" sentence, an address)
104
+ * and every theme's footer is free to show as its own short "about" column.
105
+ * Plain text, already the caller's to escape like any other string field —
106
+ * this module carries no HTML-bearing fields (R3 applies to a theme's own
107
+ * markup exactly as it does to a block's).
108
+ */
109
+ readonly footerNote?: string;
110
+ /**
111
+ * The first link of the menu assigned to the `header-action` location
112
+ * (`resolveChromeExtras`, `@cogenta/cli`), if a site has assigned one —
113
+ * absent otherwise. A theme that shows it renders a single button-styled
114
+ * link at the end of its header nav ("Book a demo", "Get started",
115
+ * "Contact"); a theme that ignores it loses nothing a pre-1.4 site had.
116
+ */
117
+ readonly headerAction?: ChromeLink;
118
+ }
119
+ export interface ChromeResult {
120
+ readonly header: string;
121
+ readonly footer: string;
122
+ }
123
+ /**
124
+ * The site's logo, as one already-escaped HTML fragment — or `null` when the
125
+ * site has no logo, which is the signal to fall back to the site name in
126
+ * text.
127
+ *
128
+ * Shared here rather than written five times: the light/dark pair is a
129
+ * `<picture>` with a `prefers-color-scheme` `<source>`, and getting that
130
+ * wrong (picking a variant server-side, or dropping the accessible name) is
131
+ * the same mistake in every theme. What each theme still owns is *where* the
132
+ * mark goes and how it is sized — this returns markup, never layout.
133
+ *
134
+ * `alt` is the site name, always written: a logo announced by its file name
135
+ * is the WCAG 1.1.1 failure this helper exists to make impossible.
136
+ */
137
+ export declare function renderBrandMark(brand: ChromeBrand | undefined, options?: {
138
+ readonly className?: string;
139
+ }): string | null;
140
+ /**
141
+ * `general.socialLinks` (contract D `theme@1.4`), as a `<ul>` of icon links —
142
+ * `null` when there is nothing to show, so a theme can write
143
+ * `renderSocialLinks(input.social)` straight into its footer without a
144
+ * conditional of its own.
145
+ *
146
+ * Every icon carries a visually-hidden text label (`cg-visually-hidden` — a
147
+ * theme that renders this must define that class, the standard
148
+ * clip-to-1px-and-hide technique, since `@cogenta/theme-kit` ships no CSS of
149
+ * its own) so the link's accessible name is still the platform *and* the
150
+ * label the site owner chose (`aria-label` would drop the wording an editor
151
+ * wrote; hidden text keeps it). `rel="me noopener noreferrer"` on every link:
152
+ * `me` is the [IndieWeb/Mastodon verification
153
+ * relation](https://indieweb.org/rel-me), harmless everywhere else, and the
154
+ * other two are the same external-link protection every theme already gives
155
+ * an off-site `<a>`.
156
+ */
157
+ export declare function renderSocialLinks(social: readonly ChromeLink[] | undefined, options?: {
158
+ readonly className?: string;
159
+ readonly itemClassName?: string;
160
+ }): HtmlElement | null;
161
+ //# sourceMappingURL=chrome.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"chrome.d.ts","sourceRoot":"","sources":["../src/chrome.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,eAAe,CAAA;AAChD,OAAO,EAAmB,KAAK,WAAW,EAAK,MAAM,WAAW,CAAA;AAEhE;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,MAAM,WAAW,WAAW;IAC1B,wHAAwH;IACxH,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;IACrB,6FAA6F;IAC7F,QAAQ,CAAC,IAAI,EAAE,WAAW,GAAG,IAAI,CAAA;IACjC,sFAAsF;IACtF,QAAQ,CAAC,QAAQ,EAAE,WAAW,GAAG,IAAI,CAAA;IACrC,oHAAoH;IACpH,QAAQ,CAAC,UAAU,EAAE,MAAM,GAAG,IAAI,CAAA;CACnC;AAED,MAAM,WAAW,aAAa;IAC5B,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAA;IACtB,iIAAiI;IACjI,QAAQ,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,CAAA;IAC5B,QAAQ,CAAC,YAAY,EAAE,OAAO,CAAA;IAC9B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;IACrB,0FAA0F;IAC1F,QAAQ,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,CAAA;CAC9B;AAED,6MAA6M;AAC7M,MAAM,WAAW,UAAU;IACzB,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAA;IACtB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;CACtB;AAED,MAAM,WAAW,WAAW;IAC1B,QAAQ,CAAC,IAAI,EAAE;QAAE,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;KAAE,CAAA;IACxC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;IACvB,+EAA+E;IAC/E,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAA;IACzB,QAAQ,CAAC,SAAS,EAAE,SAAS,aAAa,EAAE,CAAA;IAC5C,QAAQ,CAAC,SAAS,EAAE,SAAS,aAAa,EAAE,CAAA;IAC5C;;;;;;OAMG;IACH,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAA;IAC7B;;;;OAIG;IACH,QAAQ,CAAC,KAAK,CAAC,EAAE,WAAW,CAAA;IAC5B;;;;;;;OAOG;IACH,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,CAAA;IACzB;;;;;;;OAOG;IACH,QAAQ,CAAC,MAAM,CAAC,EAAE,SAAS,UAAU,EAAE,CAAA;IACvC;;;;;;;OAOG;IACH,QAAQ,CAAC,UAAU,CAAC,EAAE,MAAM,CAAA;IAC5B;;;;;;OAMG;IACH,QAAQ,CAAC,YAAY,CAAC,EAAE,UAAU,CAAA;CACnC;AAED,MAAM,WAAW,YAAY;IAC3B,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;IACvB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;CACxB;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAgB,eAAe,CAC7B,KAAK,EAAE,WAAW,GAAG,SAAS,EAC9B,OAAO,GAAE;IAAE,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,CAAA;CAAO,GAC5C,MAAM,GAAG,IAAI,CAef;AA4JD;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,iBAAiB,CAC/B,MAAM,EAAE,SAAS,UAAU,EAAE,GAAG,SAAS,EACzC,OAAO,GAAE;IAAE,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,aAAa,CAAC,EAAE,MAAM,CAAA;CAAO,GAC7E,WAAW,GAAG,IAAI,CAkBpB"}
package/dist/chrome.js ADDED
@@ -0,0 +1,198 @@
1
+ /**
2
+ * The chrome extension point — the one piece of a rendered page that used to
3
+ * live outside every theme entirely: `cogenta serve` used to hand-write the
4
+ * `<header>`/`<footer>` markup itself, hardcoded to one theme's CSS classes,
5
+ * which is exactly what made a second theme impossible to install without
6
+ * either fighting the first theme's chrome or losing chrome altogether.
7
+ *
8
+ * A theme now owns its own header and footer, the same way it owns its own
9
+ * block layouts — `cogenta serve` resolves the navigation data and the
10
+ * CMS-wide branding fragment (never a theme's business, see `brandingHtml`
11
+ * below) and hands both to the active theme's `renderChrome`.
12
+ */
13
+ import { escapeAttribute, h } from './html.js';
14
+ /**
15
+ * The site's logo, as one already-escaped HTML fragment — or `null` when the
16
+ * site has no logo, which is the signal to fall back to the site name in
17
+ * text.
18
+ *
19
+ * Shared here rather than written five times: the light/dark pair is a
20
+ * `<picture>` with a `prefers-color-scheme` `<source>`, and getting that
21
+ * wrong (picking a variant server-side, or dropping the accessible name) is
22
+ * the same mistake in every theme. What each theme still owns is *where* the
23
+ * mark goes and how it is sized — this returns markup, never layout.
24
+ *
25
+ * `alt` is the site name, always written: a logo announced by its file name
26
+ * is the WCAG 1.1.1 failure this helper exists to make impossible.
27
+ */
28
+ export function renderBrandMark(brand, options = {}) {
29
+ if (brand === undefined || brand.logo === null)
30
+ return null;
31
+ const classAttr = options.className === undefined ? '' : ` class="${escapeAttribute(options.className)}"`;
32
+ const img = `<img${classAttr} src="${escapeAttribute(brand.logo.src)}"` +
33
+ `${brand.logo.srcset === '' ? '' : ` srcset="${escapeAttribute(brand.logo.srcset)}"`}` +
34
+ ` width="${brand.logo.width}" height="${brand.logo.height}"` +
35
+ ` alt="${escapeAttribute(brand.name)}" decoding="async">`;
36
+ if (brand.logoDark === null)
37
+ return img;
38
+ const darkSet = brand.logoDark.srcset === '' ? brand.logoDark.src : brand.logoDark.srcset;
39
+ return (`<picture><source srcset="${escapeAttribute(darkSet)}" media="(prefers-color-scheme: dark)">` +
40
+ `${img}</picture>`);
41
+ }
42
+ /** A circle, as two arcs — every icon below is built from this and `roundedRect`, never copied vector data. */
43
+ function circlePath(cx, cy, r) {
44
+ return `M${cx - r} ${cy}A${r} ${r} 0 1 0 ${cx + r} ${cy}A${r} ${r} 0 1 0 ${cx - r} ${cy}Z`;
45
+ }
46
+ function roundedRect(x, y, w, h, r) {
47
+ return (`M${x + r} ${y}H${x + w - r}A${r} ${r} 0 0 1 ${x + w} ${y + r}V${y + h - r}` +
48
+ `A${r} ${r} 0 0 1 ${x + w - r} ${y + h}H${x + r}A${r} ${r} 0 0 1 ${x} ${y + h - r}` +
49
+ `V${y + r}A${r} ${r} 0 0 1 ${x + r} ${y}Z`);
50
+ }
51
+ /**
52
+ * One or more **groups** of `d` strings per platform, each group rendered as
53
+ * its own `<path>` element with `fill-rule="evenodd"` — every icon's own
54
+ * `<svg>` root also sets it, but that alone is not what makes a nested
55
+ * contour a ring rather than a solid blob: `evenodd` only cancels overlap
56
+ * *within a single path's own subpaths*, never across sibling `<path>`
57
+ * elements. Two contours meant to punch a hole in each other (instagram's
58
+ * frame and lens, YouTube's frame around its play triangle, threads' loop,
59
+ * the generic fallback's two chain-link rings) must be joined into **one**
60
+ * group's `d`; a shape meant to sit solid *beside* another (linkedin's dot,
61
+ * stem and bowl; github's head and body; instagram's own flash dot) stays
62
+ * its own group so it is never accidentally subtracted from a neighbour it
63
+ * happens to overlap.
64
+ */
65
+ const SOCIAL_ICON_PATHS = {
66
+ x: [['M4 4l6.7 8.1L4.2 20H7l5-6.1L17 20h2.8l-6.9-8.3L20.5 4h-2.8l-4.6 5.6L8.8 4H4z']],
67
+ facebook: [
68
+ ['M14 4h-2.2A4.3 4.3 0 0 0 7.5 8.3V11H5v3h2.5v7h3.4v-7h2.6l.5-3h-3.1V8.6a1 1 0 0 1 1-1H14z'],
69
+ ],
70
+ instagram: [
71
+ // The square frame: outer body minus a smaller, concentric inner
72
+ // rectangle, one path so `evenodd` actually punches the hole.
73
+ [roundedRect(3, 3, 18, 18, 5), roundedRect(6, 6, 12, 12, 3.5)],
74
+ // The lens: outer minus inner circle, same technique.
75
+ [circlePath(12, 12, 4.2), circlePath(12, 12, 2.6)],
76
+ // The flash dot — solid, and deliberately its own group: joining it into
77
+ // either ring above would let `evenodd` cancel wherever it overlaps one.
78
+ [circlePath(17, 7, 1.3)],
79
+ ],
80
+ linkedin: [
81
+ [circlePath(6.5, 7, 1.6)],
82
+ [roundedRect(5.2, 10, 2.6, 10, 1)],
83
+ [
84
+ 'M10.5 20V10h3v1.5c1-1.3 2.3-2 4-2c2.8 0 4.5 1.9 4.5 5V20h-3v-5c0-1.6-.7-2.6-2.2-2.6c-1.4 0-2.3 1-2.3 2.6V20z',
85
+ ],
86
+ ],
87
+ youtube: [
88
+ // The frame: outer minus inner rounded rect, same ring technique as
89
+ // instagram's own frame.
90
+ [roundedRect(2, 5, 20, 14, 5), roundedRect(4, 7, 16, 10, 3.5)],
91
+ // The play triangle — solid, its own group for the same reason as
92
+ // instagram's flash dot above.
93
+ ['M10 9l6 3-6 3z'],
94
+ ],
95
+ github: [
96
+ [circlePath(12, 11, 7)],
97
+ [
98
+ 'M8.3 20v-2.1c-2-.5-3-1.4-3-3.3c0-1 .3-1.7 1-2.4c-.1-.5-.4-1.7.1-2.8c0 0 .9-.3 2.8 1a10 10 0 0 1 5.1 0c1.9-1.3 2.8-1 2.8-1c.5 1.1.2 2.3.1 2.8c.7.7 1 1.4 1 2.4c0 1.9-1 2.8-3 3.3V20',
99
+ ],
100
+ ],
101
+ mastodon: [['M4 8a4 4 0 0 1 4-4h8a4 4 0 0 1 4 4v4a4 4 0 0 1-4 4H10l-3 3v-3H8a4 4 0 0 1-4-4z']],
102
+ bluesky: [
103
+ [
104
+ 'M12 9c-1.6-3-4.7-5-7-4c-1 3 .5 6.5 3.4 8c-2.9 1.5-4.4 5-3.4 8c2.3 1 5.4-1 7-4c1.6 3 4.7 5 7 4c1-3-.5-6.5-3.4-8c2.9-1.5 4.4-5 3.4-8c-2.3-1-5.4 1-7 4z',
105
+ ],
106
+ ],
107
+ tiktok: [
108
+ [circlePath(8, 17, 3)],
109
+ [roundedRect(9.4, 4, 2.4, 13, 1)],
110
+ ['M11.8 4c.3 3.2 2.7 5.4 5.7 5.6v3c-2.1-.1-4-.8-5.7-2.1'],
111
+ ],
112
+ // The loop: two overlapping, off-centre circles resolved into a crescent
113
+ // ring by `evenodd` — the whole reason this icon needs the technique at
114
+ // all, per this constant's own doc comment.
115
+ threads: [[circlePath(12, 12, 8), circlePath(13, 12, 4)]],
116
+ pinterest: [[roundedRect(10.4, 9, 2.4, 11, 1.2)], [circlePath(12.6, 10, 2.6)]],
117
+ // The generic fallback — two overlapping rounded-rectangle rings, the
118
+ // conventional "chain link" silhouette, each ring its own group (outer
119
+ // minus inner, resolved by `evenodd` within that one path) rather than
120
+ // four separate solid rectangles.
121
+ link: [
122
+ [roundedRect(1, 8, 12, 8, 4), roundedRect(3.5, 10.5, 7, 3, 1.5)],
123
+ [roundedRect(11, 8, 12, 8, 4), roundedRect(13.5, 10.5, 7, 3, 1.5)],
124
+ ],
125
+ };
126
+ /**
127
+ * `x.com`/`twitter.com`, Facebook, Instagram, LinkedIn, YouTube, GitHub,
128
+ * a Mastodon instance (any host whose path starts `/@`, since Mastodon is
129
+ * federated and has no one domain), Bluesky, TikTok, Threads, Pinterest — and
130
+ * `'link'` for anything else, including an unparseable URL.
131
+ */
132
+ function socialIconKindFor(href) {
133
+ let url;
134
+ try {
135
+ url = new URL(href);
136
+ }
137
+ catch {
138
+ return 'link';
139
+ }
140
+ const host = url.hostname.toLowerCase().replace(/^www\./, '');
141
+ if (host === 'x.com' || host === 'twitter.com')
142
+ return 'x';
143
+ if (host === 'facebook.com' || host.endsWith('.facebook.com'))
144
+ return 'facebook';
145
+ if (host === 'instagram.com')
146
+ return 'instagram';
147
+ if (host === 'linkedin.com' || host.endsWith('.linkedin.com'))
148
+ return 'linkedin';
149
+ if (host === 'youtube.com' || host === 'youtu.be')
150
+ return 'youtube';
151
+ if (host === 'github.com')
152
+ return 'github';
153
+ if (host === 'bsky.app')
154
+ return 'bluesky';
155
+ if (host === 'tiktok.com' || host.endsWith('.tiktok.com'))
156
+ return 'tiktok';
157
+ if (host === 'threads.net')
158
+ return 'threads';
159
+ if (host === 'pinterest.com' || host.endsWith('.pinterest.com') || host === 'pinterest.fr') {
160
+ return 'pinterest';
161
+ }
162
+ if (url.pathname.startsWith('/@'))
163
+ return 'mastodon';
164
+ return 'link';
165
+ }
166
+ function socialIcon(kind, className) {
167
+ return h('svg', {
168
+ class: className,
169
+ viewBox: '0 0 24 24',
170
+ fill: 'currentColor',
171
+ 'fill-rule': 'evenodd',
172
+ 'aria-hidden': 'true',
173
+ focusable: 'false',
174
+ }, SOCIAL_ICON_PATHS[kind].map((group) => h('path', { d: group.join(' ') })));
175
+ }
176
+ /**
177
+ * `general.socialLinks` (contract D `theme@1.4`), as a `<ul>` of icon links —
178
+ * `null` when there is nothing to show, so a theme can write
179
+ * `renderSocialLinks(input.social)` straight into its footer without a
180
+ * conditional of its own.
181
+ *
182
+ * Every icon carries a visually-hidden text label (`cg-visually-hidden` — a
183
+ * theme that renders this must define that class, the standard
184
+ * clip-to-1px-and-hide technique, since `@cogenta/theme-kit` ships no CSS of
185
+ * its own) so the link's accessible name is still the platform *and* the
186
+ * label the site owner chose (`aria-label` would drop the wording an editor
187
+ * wrote; hidden text keeps it). `rel="me noopener noreferrer"` on every link:
188
+ * `me` is the [IndieWeb/Mastodon verification
189
+ * relation](https://indieweb.org/rel-me), harmless everywhere else, and the
190
+ * other two are the same external-link protection every theme already gives
191
+ * an off-site `<a>`.
192
+ */
193
+ export function renderSocialLinks(social, options = {}) {
194
+ if (social === undefined || social.length === 0)
195
+ return null;
196
+ return h('ul', { class: options.className }, social.map((link) => h('li', { class: options.itemClassName }, h('a', { href: link.href, rel: 'me noopener noreferrer', target: '_blank' }, socialIcon(socialIconKindFor(link.href), 'cg-social__icon'), h('span', { class: 'cg-visually-hidden' }, link.label)))));
197
+ }
198
+ //# sourceMappingURL=chrome.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"chrome.js","sourceRoot":"","sources":["../src/chrome.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,54 @@
1
+ import { type HtmlElement } from './html.js';
2
+ /**
3
+ * The comment thread and its submission form (fiche 15 task 6, ADR-0025).
4
+ *
5
+ * Contract B is frozen and a `comments` block would be a contract addition —
6
+ * this renders through the page template instead, called from the CLI's own
7
+ * render path (`packages/cli/src/commands/theme-render.ts`), appended after
8
+ * the page's own `<main>` rather than folded into it — never on the L16
9
+ * page-builder preview, deliberately: the form's own `_ts` anti-spam field is
10
+ * a render timestamp, legitimately different on every render, so it cannot
11
+ * be part of a fidelity claim that compares two separate renders byte for
12
+ * byte.
13
+ *
14
+ * Every piece of visitor-authored text — `authorName`, `body` — goes through
15
+ * this file's `h()`/`text()` tree, never a template string: there is no
16
+ * `raw()` escape hatch in `html.ts` (see its own module comment), which is
17
+ * what makes "no HTML submitted by a visitor ever reaches the rendered page"
18
+ * a structural property of this function rather than a habit to remember —
19
+ * and, since this module is shared, a property every theme package gets for
20
+ * free rather than one it has to reimplement correctly on its own.
21
+ */
22
+ export interface PublicComment {
23
+ readonly id: string;
24
+ readonly parentId: string | null;
25
+ readonly authorName: string;
26
+ readonly authorUrl: string | null;
27
+ /** Plain text (R3) — rendered as-is inside a `<p>`, never interpreted as markup. */
28
+ readonly body: string;
29
+ readonly createdAt: string;
30
+ }
31
+ export interface CommentsSectionOptions {
32
+ readonly comments: readonly PublicComment[];
33
+ /** Whether this entry currently accepts new comments (settings inheritance already resolved by the caller). */
34
+ readonly open: boolean;
35
+ /** `POST` target — `/api/comments` in every real deployment, parameterised so a test can point elsewhere. */
36
+ readonly action: string;
37
+ readonly collection: string;
38
+ readonly entryId: string;
39
+ readonly locale: string | null;
40
+ /** The page's own path, echoed back as `redirectTo` so a no-JS submission returns here (`CommentsRouter`'s 303 branch) instead of showing raw JSON. */
41
+ readonly pagePath: string;
42
+ /** The hidden field a bot fills and a human never sees. Must match the router's own `honeypotField` (defaults to `website` on both sides). */
43
+ readonly honeypotField?: string;
44
+ /** Server time this page was rendered, ms since epoch — the minimum-fill-delay hidden field. */
45
+ readonly renderedAt: number;
46
+ }
47
+ /**
48
+ * The whole section: heading, thread, and — only when `open` — the form.
49
+ * Returned as an `HtmlElement`, appended by the caller after the page's own
50
+ * `<main>`, never inside it — a comment thread is not page content, it is a
51
+ * property of the route the same way `/search`'s results are.
52
+ */
53
+ export declare function renderCommentsSection(options: CommentsSectionOptions): HtmlElement;
54
+ //# sourceMappingURL=comments.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"comments.d.ts","sourceRoot":"","sources":["../src/comments.ts"],"names":[],"mappings":"AAAA,OAAO,EAAmB,KAAK,WAAW,EAAW,MAAM,WAAW,CAAA;AAEtE;;;;;;;;;;;;;;;;;;;GAmBG;AAEH,MAAM,WAAW,aAAa;IAC5B,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAA;IACnB,QAAQ,CAAC,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAA;IAChC,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAA;IAC3B,QAAQ,CAAC,SAAS,EAAE,MAAM,GAAG,IAAI,CAAA;IACjC,oFAAoF;IACpF,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;IACrB,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAA;CAC3B;AAED,MAAM,WAAW,sBAAsB;IACrC,QAAQ,CAAC,QAAQ,EAAE,SAAS,aAAa,EAAE,CAAA;IAC3C,+GAA+G;IAC/G,QAAQ,CAAC,IAAI,EAAE,OAAO,CAAA;IACtB,6GAA6G;IAC7G,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;IACvB,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAA;IAC3B,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAA;IACxB,QAAQ,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI,CAAA;IAC9B,uJAAuJ;IACvJ,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAA;IACzB,8IAA8I;IAC9I,QAAQ,CAAC,aAAa,CAAC,EAAE,MAAM,CAAA;IAC/B,gGAAgG;IAChG,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAA;CAC5B;AA2HD;;;;;GAKG;AACH,wBAAgB,qBAAqB,CAAC,OAAO,EAAE,sBAAsB,GAAG,WAAW,CAclF"}
@@ -0,0 +1,68 @@
1
+ import { h, text } from './html.js';
2
+ function buildTree(comments) {
3
+ const byParent = new Map();
4
+ for (const comment of comments) {
5
+ const bucket = byParent.get(comment.parentId) ?? [];
6
+ bucket.push(comment);
7
+ byParent.set(comment.parentId, bucket);
8
+ }
9
+ function attach(parentId) {
10
+ return (byParent.get(parentId) ?? []).map((comment) => ({
11
+ ...comment,
12
+ replies: attach(comment.id),
13
+ }));
14
+ }
15
+ return attach(null);
16
+ }
17
+ function renderComment(node) {
18
+ return h('li', { class: 'cg-comment', id: `comment-${node.id}` }, h('article', {}, h('header', { class: 'cg-comment__meta' }, h('span', { class: 'cg-comment__author' }, text(node.authorName)), h('time', { class: 'cg-comment__date', datetime: node.createdAt }, text(new Date(node.createdAt).toLocaleDateString()))), h('p', { class: 'cg-comment__body' }, text(node.body))), node.replies.length === 0
19
+ ? null
20
+ : h('ol', { class: 'cg-comment__replies' }, node.replies.map(renderComment)));
21
+ }
22
+ function hidden(name, value) {
23
+ return h('input', { type: 'hidden', name, value });
24
+ }
25
+ /** The submission form — a plain HTML `<form method="post">`, no JavaScript required (fiche 15 task 6). */
26
+ function renderForm(options, parentId) {
27
+ const honeypot = options.honeypotField ?? 'website';
28
+ const formId = parentId === undefined ? 'cg-comment-form' : `cg-comment-form-${parentId}`;
29
+ return h('form', {
30
+ id: formId,
31
+ class: 'cg-comment__form',
32
+ method: 'post',
33
+ action: options.action,
34
+ }, hidden('collection', options.collection), hidden('entryId', options.entryId), ...(options.locale === null ? [] : [hidden('locale', options.locale)]), ...(parentId === undefined ? [] : [hidden('parentId', parentId)]), hidden('redirectTo', options.pagePath), hidden('_ts', String(options.renderedAt)),
35
+ // The honeypot: a real visitor never sees or fills this in. Hidden by an
36
+ // attribute a theme's own stylesheet never has to know about — the
37
+ // element is not `type="hidden"` on purpose, so a bot's naive "fill
38
+ // every input" script still finds and fills it.
39
+ h('div', { class: 'cg-comment__honeypot', 'aria-hidden': 'true' }, h('label', { for: `${formId}-hp` }, text('Leave this field empty')), h('input', {
40
+ id: `${formId}-hp`,
41
+ type: 'text',
42
+ name: honeypot,
43
+ tabindex: -1,
44
+ autocomplete: 'off',
45
+ })), h('p', {}, h('label', { for: `${formId}-name` }, text('Name')), h('input', {
46
+ id: `${formId}-name`,
47
+ type: 'text',
48
+ name: 'name',
49
+ required: true,
50
+ maxlength: 200,
51
+ })), h('p', {}, h('label', { for: `${formId}-email` }, text('E-mail (not published)')), h('input', { id: `${formId}-email`, type: 'email', name: 'email', required: true })), h('p', {}, h('label', { for: `${formId}-url` }, text('Website (optional)')), h('input', { id: `${formId}-url`, type: 'url', name: 'authorUrl' })), h('p', {}, h('label', { for: `${formId}-body` }, text(parentId === undefined ? 'Comment' : 'Reply')), h('textarea', { id: `${formId}-body`, name: 'body', required: true, rows: 5 })), h('p', {}, h('button', { type: 'submit' }, text(parentId === undefined ? 'Post comment' : 'Post reply'))));
52
+ }
53
+ /**
54
+ * The whole section: heading, thread, and — only when `open` — the form.
55
+ * Returned as an `HtmlElement`, appended by the caller after the page's own
56
+ * `<main>`, never inside it — a comment thread is not page content, it is a
57
+ * property of the route the same way `/search`'s results are.
58
+ */
59
+ export function renderCommentsSection(options) {
60
+ const tree = buildTree(options.comments);
61
+ const headingAttrs = { id: 'cg-comments-heading' };
62
+ return h('section', { class: 'cg-comments', 'aria-labelledby': 'cg-comments-heading' }, h('h2', headingAttrs, text(`Comments (${options.comments.length})`)), tree.length === 0
63
+ ? h('p', { class: 'cg-comments__empty' }, text('No comments yet.'))
64
+ : h('ol', { class: 'cg-comments__list' }, tree.map(renderComment)), options.open
65
+ ? renderForm(options)
66
+ : h('p', { class: 'cg-comments__closed' }, text('Comments are closed on this page.')));
67
+ }
68
+ //# sourceMappingURL=comments.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"comments.js","sourceRoot":"","sources":["../src/comments.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,97 @@
1
+ /**
2
+ * Contract D, transcribed once for every theme package to implement against.
3
+ *
4
+ * `ThemeManifest`/`defineTheme`/`SkinTokens` are `@cogenta/render`'s own,
5
+ * already-validated definitions (`theme/manifest.ts`, `skin/tokens.ts`) —
6
+ * re-exported here rather than duplicated, since `@cogenta/render` is also
7
+ * what `loadTheme`/`verifyTheme`/the plugin theme marketplace already check
8
+ * a manifest against. A second, drifting copy of either is exactly the
9
+ * mistake this package exists to stop repeating.
10
+ *
11
+ * `RenderContext`/`ContentEntry`/`ContentClient`/`QueryRequest`/`Page` below
12
+ * are declared fresh here rather than imported from `@cogenta/render`: that
13
+ * package's own `context/types.ts` shapes a *different*, still-unused
14
+ * pipeline (the deferred Astro static build, `astro/integration.ts`) whose
15
+ * `ContentEntry` nests fields under `.values` and whose `MediaReference` is a
16
+ * full asset object — incompatible with the flat, string-keyed contract
17
+ * `cogenta serve`'s real, tested SSR path has used since L3 and every theme
18
+ * (including this repository's own reference theme) is built against.
19
+ * Unifying onto the Astro shape would change live, working behaviour for a
20
+ * pipeline nothing serves through yet — this contract is the one actually in
21
+ * production, transcribed rather than guessed at.
22
+ *
23
+ * `ImageOptions`/`ImageSource` *are* taken from `@cogenta/render` — those two
24
+ * are genuinely the same contract on both pipelines (`describeMedia`, which
25
+ * `cogenta serve` already calls, returns exactly this shape), and reusing
26
+ * them picks up `kind`/`poster` (`theme@1.1`): the fix that lets a theme tell
27
+ * a video apart from a broken image, which the very first theme built against
28
+ * the pre-1.1 copy of this contract could not.
29
+ */
30
+ import type { ImageOptions, ImageSource } from '@cogenta/render';
31
+ export type { ImageOptions, ImageSource, SkinTokens, ThemeManifest, ThemeRuntime, } from '@cogenta/render';
32
+ export { defineTheme } from '@cogenta/render';
33
+ /**
34
+ * Contract B stores a media field as the media library's identifier, never as
35
+ * an URL and never as a rendition (`f.media` in `@cogenta/blocks`). So a
36
+ * reference a theme holds is that identifier.
37
+ */
38
+ export type MediaReference = string;
39
+ /**
40
+ * Contract A fixes the system fields but not the schema-defined ones — a
41
+ * theme only ever sees published entries (the read token carries the
42
+ * `public` role), so `status` is narrow in practice but kept whole here
43
+ * rather than guessed at.
44
+ */
45
+ export interface ContentEntry {
46
+ readonly id: string;
47
+ readonly collection: string;
48
+ readonly locale: string;
49
+ readonly status: 'draft' | 'scheduled' | 'published' | 'archived';
50
+ readonly [field: string]: unknown;
51
+ }
52
+ export interface QueryRequest {
53
+ readonly collection: string;
54
+ readonly filter?: Readonly<Record<string, unknown>>;
55
+ readonly sort?: {
56
+ readonly field: string;
57
+ readonly direction: 'asc' | 'desc';
58
+ };
59
+ readonly limit?: number;
60
+ readonly cursor?: string;
61
+ }
62
+ export interface Page<T> {
63
+ readonly items: readonly T[];
64
+ readonly nextCursor: string | null;
65
+ }
66
+ export interface ContentClient {
67
+ entry(collection: string, id: string): Promise<ContentEntry | null>;
68
+ byPath(path: string): Promise<ContentEntry | null>;
69
+ list(request: QueryRequest): Promise<Page<ContentEntry>>;
70
+ }
71
+ export type LinkTargetInput = {
72
+ collection: string;
73
+ id: string;
74
+ } | {
75
+ path: string;
76
+ } | string;
77
+ export interface RenderContext {
78
+ readonly site: {
79
+ readonly name: string;
80
+ readonly url: string;
81
+ readonly locales: readonly string[];
82
+ readonly defaultLocale: string;
83
+ };
84
+ /** The locale being rendered. */
85
+ readonly locale: string;
86
+ /** The URL being rendered, already resolved. */
87
+ readonly url: URL;
88
+ /** Translation. An unknown key returns the key, never an empty string. */
89
+ t(key: string, values?: Readonly<Record<string, string | number>>): string;
90
+ /** Image variants. Returns what a responsive `<img>` (or `<video>`) needs, nothing more. */
91
+ image(media: MediaReference, options?: ImageOptions): ImageSource;
92
+ /** URL of an entry, of a path, or of an external target. Locale-aware. */
93
+ link(target: LinkTargetInput): string;
94
+ /** Read-only content access. The only door to data a theme has. */
95
+ readonly content: ContentClient;
96
+ }
97
+ //# sourceMappingURL=contract.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"contract.d.ts","sourceRoot":"","sources":["../src/contract.ts"],"names":[],"mappings":"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"}