@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,125 @@
1
+ import { h, text } from './html.js';
2
+ import { image } from './media.js';
3
+ const KNOWN_MARKS = new Map([
4
+ ['strong', 'strong'],
5
+ ['em', 'em'],
6
+ ['code', 'code'],
7
+ // Fiche 42 task 2 — `<s>` (HTML5's own tag for "no longer accurate", the
8
+ // closest semantic fit for a strikethrough) rather than `<del>`, which
9
+ // means an edit-tracking deletion this is not.
10
+ ['strikethrough', 's'],
11
+ ]);
12
+ function applyMark(ctx, mark, markDefs, child) {
13
+ const tag = KNOWN_MARKS.get(mark);
14
+ if (tag !== undefined)
15
+ return h(tag, {}, child);
16
+ const definition = markDefs.find((candidate) => candidate._key === mark);
17
+ if (definition === undefined) {
18
+ // An annotation this theme does not know stays text. Guessing markup for it
19
+ // is how a mark becomes an injection vector.
20
+ return child;
21
+ }
22
+ if (definition._type === 'link') {
23
+ return h('a', { href: ctx.link(definition.href), rel: definition.rel }, child);
24
+ }
25
+ const linkHref = ctx.link({ collection: definition.collection, id: definition.id });
26
+ // `'#'` is `RenderContext.link`'s own signal that the target could not be
27
+ // resolved — trashed, still a draft, renamed away, or simply gone.
28
+ // Rendering it as a live `<a>` would ship a dead link or, worse, an anchor
29
+ // to nothing; falling back to plain text is the documented recommendation
30
+ // ("texte simple, jamais un lien mort").
31
+ if (linkHref === '#')
32
+ return child;
33
+ return h('a', { href: linkHref }, child);
34
+ }
35
+ function renderSpan(ctx, span, markDefs) {
36
+ let node = text(span.text);
37
+ // Applied in reverse so the first mark ends up outermost, which keeps
38
+ // `['link', 'strong']` rendering as a link containing bold text.
39
+ for (const mark of [...span.marks].reverse()) {
40
+ node = applyMark(ctx, mark, markDefs, node);
41
+ }
42
+ return node;
43
+ }
44
+ function renderChildren(ctx, block) {
45
+ return block.children.map((span) => renderSpan(ctx, span, block.markDefs));
46
+ }
47
+ function isListItem(node) {
48
+ return node !== undefined && node._type === 'block' && node.listItem !== undefined;
49
+ }
50
+ function levelOf(node) {
51
+ return node.level ?? 1;
52
+ }
53
+ /**
54
+ * Builds one list, and every list nested inside it, from a run of consecutive
55
+ * list items. A nested list lives *inside* the preceding `<li>` — putting it
56
+ * beside one produces a list whose items do not match what is announced.
57
+ */
58
+ function buildList(ctx, nodes, start, level) {
59
+ const first = nodes[start];
60
+ if (!isListItem(first))
61
+ return { node: text(''), next: start + 1 };
62
+ const kind = first.listItem;
63
+ const items = [];
64
+ let index = start;
65
+ while (index < nodes.length) {
66
+ const node = nodes[index];
67
+ if (!isListItem(node))
68
+ break;
69
+ const nodeLevel = levelOf(node);
70
+ if (nodeLevel < level || node.listItem !== kind)
71
+ break;
72
+ if (nodeLevel > level) {
73
+ const nested = buildList(ctx, nodes, index, nodeLevel);
74
+ const last = items.pop();
75
+ items.push(last === undefined || last.kind !== 'element'
76
+ ? h('li', {}, nested.node)
77
+ : h('li', last.attrs, ...last.children, nested.node));
78
+ index = nested.next;
79
+ continue;
80
+ }
81
+ items.push(h('li', {}, ...renderChildren(ctx, node)));
82
+ index += 1;
83
+ }
84
+ return { node: h(kind === 'number' ? 'ol' : 'ul', {}, items), next: index };
85
+ }
86
+ function renderTextBlock(ctx, block) {
87
+ const children = renderChildren(ctx, block);
88
+ if (block.style === 'blockquote')
89
+ return h('blockquote', {}, h('p', {}, ...children));
90
+ if (block.style === 'normal')
91
+ return h('p', {}, ...children);
92
+ return h(block.style, {}, ...children);
93
+ }
94
+ export function renderRichText(ctx, document) {
95
+ const out = [];
96
+ let index = 0;
97
+ while (index < document.length) {
98
+ const node = document[index];
99
+ if (node === undefined)
100
+ break;
101
+ if (isListItem(node)) {
102
+ const list = buildList(ctx, document, index, levelOf(node));
103
+ out.push(list.node);
104
+ index = list.next;
105
+ continue;
106
+ }
107
+ if (node._type === 'media') {
108
+ out.push(h('figure', { class: 'cg-prose__figure' }, image(ctx, node.id, { sizes: '(min-width: 45rem) 40rem, 100vw' }), node.caption === undefined ? null : h('figcaption', {}, node.caption)));
109
+ index += 1;
110
+ continue;
111
+ }
112
+ if (node._type === 'hr') {
113
+ // Fiche 42 task 2: no data to carry, so no branch of `applyMark`/
114
+ // `renderSpan` is ever reachable for it — a thematic break is a bare
115
+ // void element, the same way `media`'s `<figure>` needs no marks.
116
+ out.push(h('hr', { class: 'cg-prose__rule' }));
117
+ index += 1;
118
+ continue;
119
+ }
120
+ out.push(renderTextBlock(ctx, node));
121
+ index += 1;
122
+ }
123
+ return out;
124
+ }
125
+ //# sourceMappingURL=rich-text.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"rich-text.js","sourceRoot":"","sources":["../src/rich-text.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,30 @@
1
+ /**
2
+ * The visitor-facing strings a theme asks for through `RenderContext.t`.
3
+ *
4
+ * Until L25, `cogenta serve` handed every theme `t: (key) => key` — so an
5
+ * empty `collectionList` printed the literal text `collection.empty`, and the
6
+ * reading time `theme@1.4` added to article headers would have printed
7
+ * `entry.readingTime` on every post. This is the one table both the host and
8
+ * a test can build a real translator from.
9
+ *
10
+ * Kept deliberately small and flat: these are the dozen strings the block
11
+ * vocabulary needs in every theme, not a site's editorial copy (which lives
12
+ * in content) and not the admin's i18n (which lives in `@cogenta/admin`).
13
+ * A theme that needs a string this table lacks still gets the key back —
14
+ * `t` never returns an empty string — and the gap shows up in a test rather
15
+ * than as a blank on the page.
16
+ */
17
+ export type ThemeLocale = 'en' | 'fr';
18
+ export type ThemeStrings = Readonly<Record<string, string>>;
19
+ export declare const THEME_STRINGS: Readonly<Record<ThemeLocale, ThemeStrings>>;
20
+ /** `fr-CA` → `fr`; anything this table does not know → `en`. */
21
+ export declare function themeLocaleFor(locale: string): ThemeLocale;
22
+ export declare function interpolate(template: string, values: Readonly<Record<string, string | number>> | undefined): string;
23
+ /**
24
+ * A `RenderContext.t` for one locale. Lookup order: the theme's own
25
+ * `overrides` for that locale, this table for that locale, this table's
26
+ * English, then the key itself — so an unknown key is still visible text,
27
+ * never `undefined` or `''` (the contract's own rule for `t`).
28
+ */
29
+ export declare function createThemeTranslator(locale: string, overrides?: Partial<Record<ThemeLocale, ThemeStrings>>): (key: string, values?: Readonly<Record<string, string | number>>) => string;
30
+ //# sourceMappingURL=strings.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"strings.d.ts","sourceRoot":"","sources":["../src/strings.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAEH,MAAM,MAAM,WAAW,GAAG,IAAI,GAAG,IAAI,CAAA;AAErC,MAAM,MAAM,YAAY,GAAG,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAA;AAkC3D,eAAO,MAAM,aAAa,EAAE,QAAQ,CAAC,MAAM,CAAC,WAAW,EAAE,YAAY,CAAC,CAuCrE,CAAA;AAED,gEAAgE;AAChE,wBAAgB,cAAc,CAAC,MAAM,EAAE,MAAM,GAAG,WAAW,CAG1D;AAED,wBAAgB,WAAW,CACzB,QAAQ,EAAE,MAAM,EAChB,MAAM,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAAC,CAAC,GAAG,SAAS,GAC5D,MAAM,CAMR;AAED;;;;;GAKG;AACH,wBAAgB,qBAAqB,CACnC,MAAM,EAAE,MAAM,EACd,SAAS,GAAE,OAAO,CAAC,MAAM,CAAC,WAAW,EAAE,YAAY,CAAC,CAAM,GACzD,CAAC,GAAG,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAAC,CAAC,KAAK,MAAM,CAmB7E"}
@@ -0,0 +1,124 @@
1
+ /**
2
+ * The visitor-facing strings a theme asks for through `RenderContext.t`.
3
+ *
4
+ * Until L25, `cogenta serve` handed every theme `t: (key) => key` — so an
5
+ * empty `collectionList` printed the literal text `collection.empty`, and the
6
+ * reading time `theme@1.4` added to article headers would have printed
7
+ * `entry.readingTime` on every post. This is the one table both the host and
8
+ * a test can build a real translator from.
9
+ *
10
+ * Kept deliberately small and flat: these are the dozen strings the block
11
+ * vocabulary needs in every theme, not a site's editorial copy (which lives
12
+ * in content) and not the admin's i18n (which lives in `@cogenta/admin`).
13
+ * A theme that needs a string this table lacks still gets the key back —
14
+ * `t` never returns an empty string — and the gap shows up in a test rather
15
+ * than as a blank on the page.
16
+ */
17
+ /** `{{name}}` placeholders, matching the admin's i18next convention so a reader of both sees one syntax. */
18
+ const PLACEHOLDER = /\{\{\s*([a-zA-Z0-9_]+)\s*\}\}/g;
19
+ /**
20
+ * The human name of an `embed` provider (contract B's closed `provider`
21
+ * enum). Themes hand `ctx.t` the raw id (`youtube`, `other`) — and "This
22
+ * other embed loads content from a third party. Open on other" is what a
23
+ * restaurant's map placeholder printed before this table existed.
24
+ */
25
+ const PROVIDER_LABELS = {
26
+ en: {
27
+ youtube: 'YouTube',
28
+ vimeo: 'Vimeo',
29
+ dailymotion: 'Dailymotion',
30
+ spotify: 'Spotify',
31
+ soundcloud: 'SoundCloud',
32
+ bluesky: 'Bluesky',
33
+ mastodon: 'Mastodon',
34
+ other: 'external',
35
+ },
36
+ fr: {
37
+ youtube: 'YouTube',
38
+ vimeo: 'Vimeo',
39
+ dailymotion: 'Dailymotion',
40
+ spotify: 'Spotify',
41
+ soundcloud: 'SoundCloud',
42
+ bluesky: 'Bluesky',
43
+ mastodon: 'Mastodon',
44
+ other: 'externe',
45
+ },
46
+ };
47
+ export const THEME_STRINGS = {
48
+ en: {
49
+ 'a11y.skipToContent': 'Skip to content',
50
+ 'collection.empty': 'Nothing to show yet.',
51
+ 'collection.carousel': 'Scrollable list',
52
+ 'collection.featured': 'Featured',
53
+ 'embed.consentRequired': 'This {{provider}} embed loads content from a third party.',
54
+ 'embed.open': 'Open on {{provider}}',
55
+ 'embed.openOther': 'Open the original',
56
+ 'embed.title': '{{provider}} embed',
57
+ 'embed.unsupported': 'This {{provider}} content cannot be embedded here.',
58
+ 'embed.label': 'Embedded content',
59
+ 'entry.untitled': 'Untitled',
60
+ 'entry.readingTime': '{{minutes}} min read',
61
+ 'gallery.carousel': 'Image carousel',
62
+ 'hero.actions': 'Actions',
63
+ 'theme.toggle.switchToLight': 'Switch to light theme',
64
+ 'theme.toggle.switchToDark': 'Switch to dark theme',
65
+ 'theme.toggle.switchToSystem': 'Use system theme',
66
+ },
67
+ fr: {
68
+ 'a11y.skipToContent': 'Aller au contenu',
69
+ 'collection.empty': 'Rien à afficher pour le moment.',
70
+ 'collection.carousel': 'Liste défilante',
71
+ 'collection.featured': 'À la une',
72
+ 'embed.consentRequired': 'Ce contenu {{provider}} est chargé depuis un service tiers.',
73
+ 'embed.open': 'Ouvrir sur {{provider}}',
74
+ 'embed.openOther': 'Ouvrir l’original',
75
+ 'embed.title': 'Contenu {{provider}} intégré',
76
+ 'embed.unsupported': 'Ce contenu {{provider}} ne peut pas être intégré ici.',
77
+ 'embed.label': 'Contenu intégré',
78
+ 'entry.untitled': 'Sans titre',
79
+ 'entry.readingTime': '{{minutes}} min de lecture',
80
+ 'gallery.carousel': 'Carrousel d’images',
81
+ 'hero.actions': 'Actions',
82
+ 'theme.toggle.switchToLight': 'Passer au thème clair',
83
+ 'theme.toggle.switchToDark': 'Passer au thème sombre',
84
+ 'theme.toggle.switchToSystem': 'Utiliser le thème du système',
85
+ },
86
+ };
87
+ /** `fr-CA` → `fr`; anything this table does not know → `en`. */
88
+ export function themeLocaleFor(locale) {
89
+ const language = locale.toLowerCase().split(/[-_]/)[0];
90
+ return language === 'fr' ? 'fr' : 'en';
91
+ }
92
+ export function interpolate(template, values) {
93
+ if (values === undefined)
94
+ return template;
95
+ return template.replace(PLACEHOLDER, (match, name) => {
96
+ const value = values[name];
97
+ return value === undefined ? match : String(value);
98
+ });
99
+ }
100
+ /**
101
+ * A `RenderContext.t` for one locale. Lookup order: the theme's own
102
+ * `overrides` for that locale, this table for that locale, this table's
103
+ * English, then the key itself — so an unknown key is still visible text,
104
+ * never `undefined` or `''` (the contract's own rule for `t`).
105
+ */
106
+ export function createThemeTranslator(locale, overrides = {}) {
107
+ const themeLocale = themeLocaleFor(locale);
108
+ return (key, values) => {
109
+ // A raw provider id becomes its human name; "Open on other" becomes
110
+ // "Open the original" — themes keep passing what the block stores.
111
+ const provider = values?.provider;
112
+ const resolvedValues = typeof provider === 'string' && PROVIDER_LABELS[themeLocale][provider] !== undefined
113
+ ? { ...values, provider: PROVIDER_LABELS[themeLocale][provider] }
114
+ : values;
115
+ const resolvedKey = key === 'embed.open' && provider === 'other' ? 'embed.openOther' : key;
116
+ const template = overrides[themeLocale]?.[resolvedKey] ??
117
+ THEME_STRINGS[themeLocale][resolvedKey] ??
118
+ overrides.en?.[resolvedKey] ??
119
+ THEME_STRINGS.en[resolvedKey] ??
120
+ key;
121
+ return interpolate(template, resolvedValues);
122
+ };
123
+ }
124
+ //# sourceMappingURL=strings.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"strings.js","sourceRoot":"","sources":["../src/strings.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,69 @@
1
+ /**
2
+ * The manual light/dark/system control ("dark mode is invisible without
3
+ * it").
4
+ *
5
+ * `tokens.css` in every built-in theme already carries the full tri-state
6
+ * pattern used across this project (a bare `:root` for light, a
7
+ * `prefers-color-scheme: dark` block guarded by
8
+ * `:not([data-theme="light"])`, and an explicit `[data-theme="dark"]` block
9
+ * that wins either direction) — it has since each theme's own tokens file
10
+ * was written. What never existed was anything that *sets*
11
+ * `documentElement`'s `data-theme` attribute: a visitor whose OS is in light
12
+ * mode had no way to preview dark, and vice versa, which is indistinguishable
13
+ * from "this theme has no dark mode" during a demo.
14
+ *
15
+ * Two pieces, deliberately kept apart:
16
+ *
17
+ * - `renderThemeToggle` returns markup only (through `h()`/`serialize()`,
18
+ * same as `renderBrandMark`/`renderSocialLinks`) — a theme's own
19
+ * `renderChrome` places it, exactly like those two.
20
+ * - `THEME_TOGGLE_SCRIPT` is raw JavaScript source, not markup, and is not
21
+ * this module's to wrap in a `<script>` tag: the host owns `<head>`, the
22
+ * same reasoning `ChromeBrand.faviconUrl`'s own doc comment gives for why
23
+ * a theme never writes `<link rel="icon">` itself. The host must place it
24
+ * early in `<head>`, before the stylesheet link, so a saved preference
25
+ * applies before first paint — no flash of the wrong theme.
26
+ *
27
+ * Zero dependencies, zero build step: a small inline script, the one
28
+ * deliberate exception to these themes' otherwise-zero-client-JS design —
29
+ * CSS alone cannot let a visitor override an OS preference and have that
30
+ * choice survive a navigation without either JavaScript or a server-set
31
+ * cookie, and a cookie would put theme choice on the request path of every
32
+ * page for every visitor. R9 does not apply (no dependency); R5 does not
33
+ * apply (touches neither the database nor a secret).
34
+ */
35
+ import { type HtmlElement } from './html.js';
36
+ /**
37
+ * The button a theme places in its header or footer. Carries three
38
+ * `data-label-*` attributes (already in the page's own locale, via the same
39
+ * `THEME_STRINGS` table every other visitor-facing theme string comes from)
40
+ * so `THEME_TOGGLE_SCRIPT` can keep the accessible name correct after every
41
+ * click without shipping its own translation table — the script only ever
42
+ * copies a string the server already localised.
43
+ *
44
+ * Icon choice reflects the *current* effective appearance (sun while light,
45
+ * moon while dark), swapped by CSS alone from the same `[data-theme]`
46
+ * attribute and `prefers-color-scheme` query every theme's `tokens.css`
47
+ * already keys its palette on — this module ships no CSS of its own,
48
+ * matching `renderSocialLinks`'s own rule, so each theme styles
49
+ * `.cg-theme-toggle` (sizing, border, the two `.cg-theme-toggle__icon--*`
50
+ * visibility rules) in its own register.
51
+ */
52
+ export declare function renderThemeToggle(locale: string, options?: {
53
+ readonly className?: string;
54
+ readonly iconClassName?: string;
55
+ }): HtmlElement;
56
+ /**
57
+ * Raw JavaScript, not markup — see this module's doc comment for why the
58
+ * host, not a theme, wraps this in a `<script>` tag and where it must go.
59
+ *
60
+ * Cycle: system (no attribute) → light → dark → system. Its restore step
61
+ * runs synchronously and unconditionally (a `<script>` this early in `<head>`
62
+ * blocks parsing until it finishes, which is the point — the attribute is
63
+ * set before the stylesheet is even requested, so there is nothing for a
64
+ * visitor to see flip). The click handler is delegated on `document` itself,
65
+ * so it works for a toggle button added anywhere in the page's header or
66
+ * footer without this script knowing either exists yet.
67
+ */
68
+ export declare const THEME_TOGGLE_SCRIPT = "(function(){\nvar KEY='cg-theme';\nvar root=document.documentElement;\nfunction apply(next){\n if(next===null){root.removeAttribute('data-theme');}else{root.setAttribute('data-theme',next);}\n try{\n if(next===null){localStorage.removeItem(KEY);}else{localStorage.setItem(KEY,next);}\n }catch(e){}\n}\nfunction syncLabel(btn,current){\n var next=current==='light'?'dark':current==='dark'?null:'light';\n var label=next==='light'?btn.getAttribute('data-label-light'):next==='dark'?btn.getAttribute('data-label-dark'):btn.getAttribute('data-label-system');\n if(label){btn.setAttribute('aria-label',label);}\n}\ntry{\n var saved=localStorage.getItem(KEY);\n if(saved==='light'||saved==='dark'){root.setAttribute('data-theme',saved);}\n}catch(e){}\ndocument.addEventListener('DOMContentLoaded',function(){\n var current=root.getAttribute('data-theme');\n var toggles=document.querySelectorAll('[data-cg-theme-toggle]');\n for(var i=0;i<toggles.length;i++){syncLabel(toggles[i],current);}\n});\ndocument.addEventListener('click',function(event){\n var target=event.target;\n var btn=target&&target.closest?target.closest('[data-cg-theme-toggle]'):null;\n if(!btn)return;\n var current=root.getAttribute('data-theme');\n var next=current==='light'?'dark':current==='dark'?null:'light';\n apply(next);\n var toggles=document.querySelectorAll('[data-cg-theme-toggle]');\n for(var i=0;i<toggles.length;i++){syncLabel(toggles[i],next);}\n});\n})();";
69
+ //# sourceMappingURL=theme-toggle.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"theme-toggle.d.ts","sourceRoot":"","sources":["../src/theme-toggle.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAiCG;AAEH,OAAO,EAAE,KAAK,WAAW,EAAK,MAAM,WAAW,CAAA;AAO/C;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,iBAAiB,CAC/B,MAAM,EAAE,MAAM,EACd,OAAO,GAAE;IAAE,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,aAAa,CAAC,EAAE,MAAM,CAAA;CAAO,GAC7E,WAAW,CAgDb;AAED;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,mBAAmB,07CAiC1B,CAAA"}
@@ -0,0 +1,136 @@
1
+ /**
2
+ * The manual light/dark/system control ("dark mode is invisible without
3
+ * it").
4
+ *
5
+ * `tokens.css` in every built-in theme already carries the full tri-state
6
+ * pattern used across this project (a bare `:root` for light, a
7
+ * `prefers-color-scheme: dark` block guarded by
8
+ * `:not([data-theme="light"])`, and an explicit `[data-theme="dark"]` block
9
+ * that wins either direction) — it has since each theme's own tokens file
10
+ * was written. What never existed was anything that *sets*
11
+ * `documentElement`'s `data-theme` attribute: a visitor whose OS is in light
12
+ * mode had no way to preview dark, and vice versa, which is indistinguishable
13
+ * from "this theme has no dark mode" during a demo.
14
+ *
15
+ * Two pieces, deliberately kept apart:
16
+ *
17
+ * - `renderThemeToggle` returns markup only (through `h()`/`serialize()`,
18
+ * same as `renderBrandMark`/`renderSocialLinks`) — a theme's own
19
+ * `renderChrome` places it, exactly like those two.
20
+ * - `THEME_TOGGLE_SCRIPT` is raw JavaScript source, not markup, and is not
21
+ * this module's to wrap in a `<script>` tag: the host owns `<head>`, the
22
+ * same reasoning `ChromeBrand.faviconUrl`'s own doc comment gives for why
23
+ * a theme never writes `<link rel="icon">` itself. The host must place it
24
+ * early in `<head>`, before the stylesheet link, so a saved preference
25
+ * applies before first paint — no flash of the wrong theme.
26
+ *
27
+ * Zero dependencies, zero build step: a small inline script, the one
28
+ * deliberate exception to these themes' otherwise-zero-client-JS design —
29
+ * CSS alone cannot let a visitor override an OS preference and have that
30
+ * choice survive a navigation without either JavaScript or a server-set
31
+ * cookie, and a cookie would put theme choice on the request path of every
32
+ * page for every visitor. R9 does not apply (no dependency); R5 does not
33
+ * apply (touches neither the database nor a secret).
34
+ */
35
+ import { h } from './html.js';
36
+ import { createThemeTranslator } from './strings.js';
37
+ const SUN_PATH = 'M12 4V2M12 22v-2M4 12H2M22 12h-2M5.6 5.6 4.2 4.2M19.8 19.8l-1.4-1.4M5.6 18.4l-1.4 1.4M19.8 4.2l-1.4 1.4M12 7a5 5 0 1 0 0 10 5 5 0 0 0 0-10Z';
38
+ const MOON_PATH = 'M20 14.5A8.5 8.5 0 1 1 9.5 4a7 7 0 0 0 10.5 10.5Z';
39
+ /**
40
+ * The button a theme places in its header or footer. Carries three
41
+ * `data-label-*` attributes (already in the page's own locale, via the same
42
+ * `THEME_STRINGS` table every other visitor-facing theme string comes from)
43
+ * so `THEME_TOGGLE_SCRIPT` can keep the accessible name correct after every
44
+ * click without shipping its own translation table — the script only ever
45
+ * copies a string the server already localised.
46
+ *
47
+ * Icon choice reflects the *current* effective appearance (sun while light,
48
+ * moon while dark), swapped by CSS alone from the same `[data-theme]`
49
+ * attribute and `prefers-color-scheme` query every theme's `tokens.css`
50
+ * already keys its palette on — this module ships no CSS of its own,
51
+ * matching `renderSocialLinks`'s own rule, so each theme styles
52
+ * `.cg-theme-toggle` (sizing, border, the two `.cg-theme-toggle__icon--*`
53
+ * visibility rules) in its own register.
54
+ */
55
+ export function renderThemeToggle(locale, options = {}) {
56
+ const t = createThemeTranslator(locale);
57
+ const labelLight = t('theme.toggle.switchToLight');
58
+ const labelDark = t('theme.toggle.switchToDark');
59
+ const labelSystem = t('theme.toggle.switchToSystem');
60
+ const iconClass = (variant) => ['cg-theme-toggle__icon', `cg-theme-toggle__icon--${variant}`, options.iconClassName]
61
+ .filter((part) => typeof part === 'string' && part !== '')
62
+ .join(' ');
63
+ return h('button', {
64
+ type: 'button',
65
+ class: options.className,
66
+ 'data-cg-theme-toggle': true,
67
+ 'data-label-light': labelLight,
68
+ 'data-label-dark': labelDark,
69
+ 'data-label-system': labelSystem,
70
+ // The page loads with no manual override (system), so the next click
71
+ // switches to light — see `THEME_TOGGLE_SCRIPT`'s own cycle order.
72
+ 'aria-label': labelLight,
73
+ }, h('svg', {
74
+ class: iconClass('sun'),
75
+ viewBox: '0 0 24 24',
76
+ fill: 'none',
77
+ stroke: 'currentColor',
78
+ 'stroke-width': '2',
79
+ 'stroke-linecap': 'round',
80
+ 'aria-hidden': 'true',
81
+ focusable: 'false',
82
+ }, h('path', { d: SUN_PATH })), h('svg', {
83
+ class: iconClass('moon'),
84
+ viewBox: '0 0 24 24',
85
+ fill: 'currentColor',
86
+ 'aria-hidden': 'true',
87
+ focusable: 'false',
88
+ }, h('path', { d: MOON_PATH })));
89
+ }
90
+ /**
91
+ * Raw JavaScript, not markup — see this module's doc comment for why the
92
+ * host, not a theme, wraps this in a `<script>` tag and where it must go.
93
+ *
94
+ * Cycle: system (no attribute) → light → dark → system. Its restore step
95
+ * runs synchronously and unconditionally (a `<script>` this early in `<head>`
96
+ * blocks parsing until it finishes, which is the point — the attribute is
97
+ * set before the stylesheet is even requested, so there is nothing for a
98
+ * visitor to see flip). The click handler is delegated on `document` itself,
99
+ * so it works for a toggle button added anywhere in the page's header or
100
+ * footer without this script knowing either exists yet.
101
+ */
102
+ export const THEME_TOGGLE_SCRIPT = `(function(){
103
+ var KEY='cg-theme';
104
+ var root=document.documentElement;
105
+ function apply(next){
106
+ if(next===null){root.removeAttribute('data-theme');}else{root.setAttribute('data-theme',next);}
107
+ try{
108
+ if(next===null){localStorage.removeItem(KEY);}else{localStorage.setItem(KEY,next);}
109
+ }catch(e){}
110
+ }
111
+ function syncLabel(btn,current){
112
+ var next=current==='light'?'dark':current==='dark'?null:'light';
113
+ var label=next==='light'?btn.getAttribute('data-label-light'):next==='dark'?btn.getAttribute('data-label-dark'):btn.getAttribute('data-label-system');
114
+ if(label){btn.setAttribute('aria-label',label);}
115
+ }
116
+ try{
117
+ var saved=localStorage.getItem(KEY);
118
+ if(saved==='light'||saved==='dark'){root.setAttribute('data-theme',saved);}
119
+ }catch(e){}
120
+ document.addEventListener('DOMContentLoaded',function(){
121
+ var current=root.getAttribute('data-theme');
122
+ var toggles=document.querySelectorAll('[data-cg-theme-toggle]');
123
+ for(var i=0;i<toggles.length;i++){syncLabel(toggles[i],current);}
124
+ });
125
+ document.addEventListener('click',function(event){
126
+ var target=event.target;
127
+ var btn=target&&target.closest?target.closest('[data-cg-theme-toggle]'):null;
128
+ if(!btn)return;
129
+ var current=root.getAttribute('data-theme');
130
+ var next=current==='light'?'dark':current==='dark'?null:'light';
131
+ apply(next);
132
+ var toggles=document.querySelectorAll('[data-cg-theme-toggle]');
133
+ for(var i=0;i<toggles.length;i++){syncLabel(toggles[i],next);}
134
+ });
135
+ })();`;
136
+ //# sourceMappingURL=theme-toggle.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"theme-toggle.js","sourceRoot":"","sources":["../src/theme-toggle.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAiCG;AAEH,OAAO,EAAoB,CAAC,EAAE,MAAM,WAAW,CAAA;AAC/C,OAAO,EAAE,qBAAqB,EAAE,MAAM,cAAc,CAAA;AAEpD,MAAM,QAAQ,GACZ,6IAA6I,CAAA;AAC/I,MAAM,SAAS,GAAG,mDAAmD,CAAA;AAErE;;;;;;;;;;;;;;;GAeG;AACH,MAAM,UAAU,iBAAiB,CAC/B,MAAc,EACd,OAAO,GAAqE,EAAE;IAE9E,MAAM,CAAC,GAAG,qBAAqB,CAAC,MAAM,CAAC,CAAA;IACvC,MAAM,UAAU,GAAG,CAAC,CAAC,4BAA4B,CAAC,CAAA;IAClD,MAAM,SAAS,GAAG,CAAC,CAAC,2BAA2B,CAAC,CAAA;IAChD,MAAM,WAAW,GAAG,CAAC,CAAC,6BAA6B,CAAC,CAAA;IACpD,MAAM,SAAS,GAAG,CAAC,OAAuB,EAAU,EAAE,CACpD,CAAC,uBAAuB,EAAE,0BAA0B,OAAO,EAAE,EAAE,OAAO,CAAC,aAAa,CAAC;SAClF,MAAM,CAAC,CAAC,IAAI,EAAkB,EAAE,CAAC,OAAO,IAAI,KAAK,QAAQ,IAAI,IAAI,KAAK,EAAE,CAAC;SACzE,IAAI,CAAC,GAAG,CAAC,CAAA;IACd,OAAO,CAAC,CACN,QAAQ,EACR;QACE,IAAI,EAAE,QAAQ;QACd,KAAK,EAAE,OAAO,CAAC,SAAS;QACxB,sBAAsB,EAAE,IAAI;QAC5B,kBAAkB,EAAE,UAAU;QAC9B,iBAAiB,EAAE,SAAS;QAC5B,mBAAmB,EAAE,WAAW;QAChC,qEAAqE;QACrE,mEAAmE;QACnE,YAAY,EAAE,UAAU;KACzB,EACD,CAAC,CACC,KAAK,EACL;QACE,KAAK,EAAE,SAAS,CAAC,KAAK,CAAC;QACvB,OAAO,EAAE,WAAW;QACpB,IAAI,EAAE,MAAM;QACZ,MAAM,EAAE,cAAc;QACtB,cAAc,EAAE,GAAG;QACnB,gBAAgB,EAAE,OAAO;QACzB,aAAa,EAAE,MAAM;QACrB,SAAS,EAAE,OAAO;KACnB,EACD,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC,EAAE,QAAQ,EAAE,CAAC,CAC3B,EACD,CAAC,CACC,KAAK,EACL;QACE,KAAK,EAAE,SAAS,CAAC,MAAM,CAAC;QACxB,OAAO,EAAE,WAAW;QACpB,IAAI,EAAE,cAAc;QACpB,aAAa,EAAE,MAAM;QACrB,SAAS,EAAE,OAAO;KACnB,EACD,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC,EAAE,SAAS,EAAE,CAAC,CAC5B,CACF,CAAA;AACH,CAAC;AAED;;;;;;;;;;;GAWG;AACH,MAAM,CAAC,MAAM,mBAAmB,GAAG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;MAiC7B,CAAA"}
package/package.json ADDED
@@ -0,0 +1,43 @@
1
+ {
2
+ "name": "@cogenta/theme-kit",
3
+ "version": "0.3.2",
4
+ "description": "The shared contract D primitives every Cogenta theme package implements against: RenderContext, the HTML tree, rich text, comments and the twelve-block page shape.",
5
+ "license": "MPL-2.0",
6
+ "type": "module",
7
+ "sideEffects": false,
8
+ "exports": {
9
+ ".": {
10
+ "types": "./dist/index.d.ts",
11
+ "default": "./dist/index.js"
12
+ }
13
+ },
14
+ "files": [
15
+ "dist"
16
+ ],
17
+ "engines": {
18
+ "node": ">=22.11.0"
19
+ },
20
+ "publishConfig": {
21
+ "access": "public",
22
+ "provenance": true
23
+ },
24
+ "repository": {
25
+ "type": "git",
26
+ "url": "git+https://github.com/cogenta-cms/cogenta.git",
27
+ "directory": "packages/theme-kit"
28
+ },
29
+ "dependencies": {
30
+ "@cogenta/render": "0.2.3",
31
+ "@cogenta/blocks": "1.0.3"
32
+ },
33
+ "devDependencies": {
34
+ "typescript": "^7.0.2",
35
+ "vitest": "^4.1.10"
36
+ },
37
+ "scripts": {
38
+ "build": "tsc -p tsconfig.json",
39
+ "typecheck": "tsc -p tsconfig.typecheck.json",
40
+ "test": "vitest run --passWithNoTests",
41
+ "test:integration": "vitest run --config vitest.integration.config.ts --passWithNoTests"
42
+ }
43
+ }