@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
|
@@ -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"}
|
package/dist/strings.js
ADDED
|
@@ -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
|
+
}
|