@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.
- package/LICENSE +373 -0
- package/dist/actions.d.ts +17 -0
- package/dist/actions.d.ts.map +1 -0
- package/dist/actions.js +43 -0
- package/dist/actions.js.map +1 -0
- package/dist/archive.d.ts +85 -0
- package/dist/archive.d.ts.map +1 -0
- package/dist/archive.js +21 -0
- package/dist/archive.js.map +1 -0
- package/dist/chrome.d.ts +161 -0
- package/dist/chrome.d.ts.map +1 -0
- package/dist/chrome.js +198 -0
- package/dist/chrome.js.map +1 -0
- package/dist/comments.d.ts +54 -0
- package/dist/comments.d.ts.map +1 -0
- package/dist/comments.js +68 -0
- package/dist/comments.js.map +1 -0
- package/dist/contract.d.ts +97 -0
- package/dist/contract.d.ts.map +1 -0
- package/dist/contract.js +31 -0
- package/dist/contract.js.map +1 -0
- package/dist/entry-header.d.ts +62 -0
- package/dist/entry-header.d.ts.map +1 -0
- package/dist/entry-header.js +65 -0
- package/dist/entry-header.js.map +1 -0
- package/dist/entry.d.ts +21 -0
- package/dist/entry.d.ts.map +1 -0
- package/dist/entry.js +65 -0
- package/dist/entry.js.map +1 -0
- package/dist/heading.d.ts +14 -0
- package/dist/heading.d.ts.map +1 -0
- package/dist/heading.js +34 -0
- package/dist/heading.js.map +1 -0
- package/dist/html.d.ts +42 -0
- package/dist/html.d.ts.map +1 -0
- package/dist/html.js +94 -0
- package/dist/html.js.map +1 -0
- package/dist/icons.d.ts +37 -0
- package/dist/icons.d.ts.map +1 -0
- package/dist/icons.js +187 -0
- package/dist/icons.js.map +1 -0
- package/dist/index.d.ts +27 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +27 -0
- package/dist/index.js.map +1 -0
- package/dist/media.d.ts +45 -0
- package/dist/media.d.ts.map +1 -0
- package/dist/media.js +70 -0
- package/dist/media.js.map +1 -0
- package/dist/page.d.ts +104 -0
- package/dist/page.d.ts.map +1 -0
- package/dist/page.js +134 -0
- package/dist/page.js.map +1 -0
- package/dist/rich-text.d.ts +5 -0
- package/dist/rich-text.d.ts.map +1 -0
- package/dist/rich-text.js +125 -0
- package/dist/rich-text.js.map +1 -0
- package/dist/strings.d.ts +30 -0
- package/dist/strings.d.ts.map +1 -0
- package/dist/strings.js +124 -0
- package/dist/strings.js.map +1 -0
- package/dist/theme-toggle.d.ts +69 -0
- package/dist/theme-toggle.d.ts.map +1 -0
- package/dist/theme-toggle.js +136 -0
- package/dist/theme-toggle.js.map +1 -0
- package/package.json +43 -0
package/dist/chrome.d.ts
ADDED
|
@@ -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":"AAAA;;;;;;;;;;;GAWG;AAGH,OAAO,EAAE,eAAe,EAAoB,CAAC,EAAE,MAAM,WAAW,CAAA;AAiHhE;;;;;;;;;;;;;GAaG;AACH,MAAM,UAAU,eAAe,CAC7B,KAA8B,EAC9B,OAAO,GAAoC,EAAE;IAE7C,IAAI,KAAK,KAAK,SAAS,IAAI,KAAK,CAAC,IAAI,KAAK,IAAI;QAAE,OAAO,IAAI,CAAA;IAC3D,MAAM,SAAS,GACb,OAAO,CAAC,SAAS,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,WAAW,eAAe,CAAC,OAAO,CAAC,SAAS,CAAC,GAAG,CAAA;IACzF,MAAM,GAAG,GACP,OAAO,SAAS,SAAS,eAAe,CAAC,KAAK,CAAC,IAAI,CAAC,GAAG,CAAC,GAAG;QAC3D,GAAG,KAAK,CAAC,IAAI,CAAC,MAAM,KAAK,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,YAAY,eAAe,CAAC,KAAK,CAAC,IAAI,CAAC,MAAM,CAAC,GAAG,EAAE;QACtF,WAAW,KAAK,CAAC,IAAI,CAAC,KAAK,aAAa,KAAK,CAAC,IAAI,CAAC,MAAM,GAAG;QAC5D,SAAS,eAAe,CAAC,KAAK,CAAC,IAAI,CAAC,qBAAqB,CAAA;IAC3D,IAAI,KAAK,CAAC,QAAQ,KAAK,IAAI;QAAE,OAAO,GAAG,CAAA;IACvC,MAAM,OAAO,GAAG,KAAK,CAAC,QAAQ,CAAC,MAAM,KAAK,EAAE,CAAC,CAAC,CAAC,KAAK,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC,CAAC,KAAK,CAAC,QAAQ,CAAC,MAAM,CAAA;IACzF,OAAO,CACL,4BAA4B,eAAe,CAAC,OAAO,CAAC,yCAAyC;QAC7F,GAAG,GAAG,YAAY,CACnB,CAAA;AACH,CAAC;AAsBD,+GAA+G;AAC/G,SAAS,UAAU,CAAC,EAAU,EAAE,EAAU,EAAE,CAAS;IACnD,OAAO,IAAI,EAAE,GAAG,CAAC,IAAI,EAAE,IAAI,CAAC,IAAI,CAAC,UAAU,EAAE,GAAG,CAAC,IAAI,EAAE,IAAI,CAAC,IAAI,CAAC,UAAU,EAAE,GAAG,CAAC,IAAI,EAAE,GAAG,CAAA;AAC5F,CAAC;AAED,SAAS,WAAW,CAAC,CAAS,EAAE,CAAS,EAAE,CAAS,EAAE,CAAS,EAAE,CAAS;IACxE,OAAO,CACL,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC,IAAI,CAAC,IAAI,CAAC,UAAU,CAAC,GAAG,CAAC,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC,EAAE;QAC5E,IAAI,CAAC,IAAI,CAAC,UAAU,CAAC,GAAG,CAAC,GAAG,CAAC,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,IAAI,CAAC,UAAU,CAAC,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC,EAAE;QACnF,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,IAAI,CAAC,UAAU,CAAC,GAAG,CAAC,IAAI,CAAC,GAAG,CAC3C,CAAA;AACH,CAAC;AAED;;;;;;;;;;;;;GAaG;AACH,MAAM,iBAAiB,GAAqE;IAC1F,CAAC,EAAE,CAAC,CAAC,8EAA8E,CAAC,CAAC;IACrF,QAAQ,EAAE;QACR,CAAC,0FAA0F,CAAC;KAC7F;IACD,SAAS,EAAE;QACT,iEAAiE;QACjE,8DAA8D;QAC9D,CAAC,WAAW,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,EAAE,EAAE,EAAE,CAAC,CAAC,EAAE,WAAW,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,EAAE,EAAE,EAAE,GAAG,CAAC,CAAC;QAC9D,sDAAsD;QACtD,CAAC,UAAU,CAAC,EAAE,EAAE,EAAE,EAAE,GAAG,CAAC,EAAE,UAAU,CAAC,EAAE,EAAE,EAAE,EAAE,GAAG,CAAC,CAAC;QAClD,yEAAyE;QACzE,yEAAyE;QACzE,CAAC,UAAU,CAAC,EAAE,EAAE,CAAC,EAAE,GAAG,CAAC,CAAC;KACzB;IACD,QAAQ,EAAE;QACR,CAAC,UAAU,CAAC,GAAG,EAAE,CAAC,EAAE,GAAG,CAAC,CAAC;QACzB,CAAC,WAAW,CAAC,GAAG,EAAE,EAAE,EAAE,GAAG,EAAE,EAAE,EAAE,CAAC,CAAC,CAAC;QAClC;YACE,8GAA8G;SAC/G;KACF;IACD,OAAO,EAAE;QACP,oEAAoE;QACpE,yBAAyB;QACzB,CAAC,WAAW,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,EAAE,EAAE,EAAE,CAAC,CAAC,EAAE,WAAW,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,EAAE,EAAE,EAAE,GAAG,CAAC,CAAC;QAC9D,kEAAkE;QAClE,+BAA+B;QAC/B,CAAC,gBAAgB,CAAC;KACnB;IACD,MAAM,EAAE;QACN,CAAC,UAAU,CAAC,EAAE,EAAE,EAAE,EAAE,CAAC,CAAC,CAAC;QACvB;YACE,oLAAoL;SACrL;KACF;IACD,QAAQ,EAAE,CAAC,CAAC,gFAAgF,CAAC,CAAC;IAC9F,OAAO,EAAE;QACP;YACE,sJAAsJ;SACvJ;KACF;IACD,MAAM,EAAE;QACN,CAAC,UAAU,CAAC,CAAC,EAAE,EAAE,EAAE,CAAC,CAAC,CAAC;QACtB,CAAC,WAAW,CAAC,GAAG,EAAE,CAAC,EAAE,GAAG,EAAE,EAAE,EAAE,CAAC,CAAC,CAAC;QACjC,CAAC,uDAAuD,CAAC;KAC1D;IACD,yEAAyE;IACzE,wEAAwE;IACxE,4CAA4C;IAC5C,OAAO,EAAE,CAAC,CAAC,UAAU,CAAC,EAAE,EAAE,EAAE,EAAE,CAAC,CAAC,EAAE,UAAU,CAAC,EAAE,EAAE,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC;IACzD,SAAS,EAAE,CAAC,CAAC,WAAW,CAAC,IAAI,EAAE,CAAC,EAAE,GAAG,EAAE,EAAE,EAAE,GAAG,CAAC,CAAC,EAAE,CAAC,UAAU,CAAC,IAAI,EAAE,EAAE,EAAE,GAAG,CAAC,CAAC,CAAC;IAC9E,sEAAsE;IACtE,uEAAuE;IACvE,uEAAuE;IACvE,kCAAkC;IAClC,IAAI,EAAE;QACJ,CAAC,WAAW,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,EAAE,CAAC,EAAE,CAAC,CAAC,EAAE,WAAW,CAAC,GAAG,EAAE,IAAI,EAAE,CAAC,EAAE,CAAC,EAAE,GAAG,CAAC,CAAC;QAChE,CAAC,WAAW,CAAC,EAAE,EAAE,CAAC,EAAE,EAAE,EAAE,CAAC,EAAE,CAAC,CAAC,EAAE,WAAW,CAAC,IAAI,EAAE,IAAI,EAAE,CAAC,EAAE,CAAC,EAAE,GAAG,CAAC,CAAC;KACnE;CACF,CAAA;AAED;;;;;GAKG;AACH,SAAS,iBAAiB,CAAC,IAAY;IACrC,IAAI,GAAQ,CAAA;IACZ,IAAI,CAAC;QACH,GAAG,GAAG,IAAI,GAAG,CAAC,IAAI,CAAC,CAAA;IACrB,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,MAAM,CAAA;IACf,CAAC;IACD,MAAM,IAAI,GAAG,GAAG,CAAC,QAAQ,CAAC,WAAW,EAAE,CAAC,OAAO,CAAC,QAAQ,EAAE,EAAE,CAAC,CAAA;IAC7D,IAAI,IAAI,KAAK,OAAO,IAAI,IAAI,KAAK,aAAa;QAAE,OAAO,GAAG,CAAA;IAC1D,IAAI,IAAI,KAAK,cAAc,IAAI,IAAI,CAAC,QAAQ,CAAC,eAAe,CAAC;QAAE,OAAO,UAAU,CAAA;IAChF,IAAI,IAAI,KAAK,eAAe;QAAE,OAAO,WAAW,CAAA;IAChD,IAAI,IAAI,KAAK,cAAc,IAAI,IAAI,CAAC,QAAQ,CAAC,eAAe,CAAC;QAAE,OAAO,UAAU,CAAA;IAChF,IAAI,IAAI,KAAK,aAAa,IAAI,IAAI,KAAK,UAAU;QAAE,OAAO,SAAS,CAAA;IACnE,IAAI,IAAI,KAAK,YAAY;QAAE,OAAO,QAAQ,CAAA;IAC1C,IAAI,IAAI,KAAK,UAAU;QAAE,OAAO,SAAS,CAAA;IACzC,IAAI,IAAI,KAAK,YAAY,IAAI,IAAI,CAAC,QAAQ,CAAC,aAAa,CAAC;QAAE,OAAO,QAAQ,CAAA;IAC1E,IAAI,IAAI,KAAK,aAAa;QAAE,OAAO,SAAS,CAAA;IAC5C,IAAI,IAAI,KAAK,eAAe,IAAI,IAAI,CAAC,QAAQ,CAAC,gBAAgB,CAAC,IAAI,IAAI,KAAK,cAAc,EAAE,CAAC;QAC3F,OAAO,WAAW,CAAA;IACpB,CAAC;IACD,IAAI,GAAG,CAAC,QAAQ,CAAC,UAAU,CAAC,IAAI,CAAC;QAAE,OAAO,UAAU,CAAA;IACpD,OAAO,MAAM,CAAA;AACf,CAAC;AAED,SAAS,UAAU,CAAC,IAAoB,EAAE,SAA6B;IACrE,OAAO,CAAC,CACN,KAAK,EACL;QACE,KAAK,EAAE,SAAS;QAChB,OAAO,EAAE,WAAW;QACpB,IAAI,EAAE,cAAc;QACpB,WAAW,EAAE,SAAS;QACtB,aAAa,EAAE,MAAM;QACrB,SAAS,EAAE,OAAO;KACnB,EACD,iBAAiB,CAAC,IAAI,CAAC,CAAC,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC,EAAE,KAAK,CAAC,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC,CAC1E,CAAA;AACH,CAAC;AAED;;;;;;;;;;;;;;;;GAgBG;AACH,MAAM,UAAU,iBAAiB,CAC/B,MAAyC,EACzC,OAAO,GAAqE,EAAE;IAE9E,IAAI,MAAM,KAAK,SAAS,IAAI,MAAM,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,IAAI,CAAA;IAC5D,OAAO,CAAC,CACN,IAAI,EACJ,EAAE,KAAK,EAAE,OAAO,CAAC,SAAS,EAAE,EAC5B,MAAM,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAClB,CAAC,CACC,IAAI,EACJ,EAAE,KAAK,EAAE,OAAO,CAAC,aAAa,EAAE,EAChC,CAAC,CACC,GAAG,EACH,EAAE,IAAI,EAAE,IAAI,CAAC,IAAI,EAAE,GAAG,EAAE,wBAAwB,EAAE,MAAM,EAAE,QAAQ,EAAE,EACpE,UAAU,CAAC,iBAAiB,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,iBAAiB,CAAC,EAC3D,CAAC,CAAC,MAAM,EAAE,EAAE,KAAK,EAAE,oBAAoB,EAAE,EAAE,IAAI,CAAC,KAAK,CAAC,CACvD,CACF,CACF,CACF,CAAA;AACH,CAAC"}
|
|
@@ -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"}
|
package/dist/comments.js
ADDED
|
@@ -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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"}
|