@rathnasgala/theme 0.0.6 → 0.0.9
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/package.json +1 -1
- package/payload/.gala/managed-files.json +16 -10
- package/payload/eleventy.config.js +2 -0
- package/payload/lib/provider-fixtures/embeds.v1.json +48 -0
- package/payload/lib/provider-fixtures/share-intents.v1.json +3 -3
- package/payload/lib/render-markdown.js +138 -12
- package/payload/lib/share-targets.js +3 -1
- package/payload/lib/site-config.js +35 -0
- package/payload/src/_includes/layouts/base.njk +9 -2
- package/payload/src/assets/embed-codepen.svg +1 -0
- package/payload/src/assets/embed-gist.svg +1 -0
- package/payload/src/assets/embed-x.svg +1 -0
- package/payload/src/assets/embed-youtube.svg +1 -0
- package/payload/src/assets/interactions.js +49 -1
- package/payload/src/assets/theme.css +60 -0
- package/payload/src/contact.njk +22 -0
- package/payload/src/posts.11ty.js +3 -0
package/package.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"schemaVersion": 1,
|
|
3
3
|
"themePackage": {
|
|
4
4
|
"name": "@rathnasgala/theme",
|
|
5
|
-
"version": "0.0.
|
|
5
|
+
"version": "0.0.9",
|
|
6
6
|
"availableDesignThemes": [
|
|
7
7
|
"editorial"
|
|
8
8
|
],
|
|
@@ -11,20 +11,21 @@
|
|
|
11
11
|
"files": {
|
|
12
12
|
".gitignore": "b5fa24e10155097a0d372926fd2e59e6072e7cc2cb6399bbfb4beb8b33ea315b",
|
|
13
13
|
".gala/publish.yml.template": "73080241d89bd09964b797a8bcc8e18471412e11bc873efad76305b60f497d08",
|
|
14
|
-
"eleventy.config.js": "
|
|
14
|
+
"eleventy.config.js": "efe587447e3dc374a220c7fa037fdf2fffc02d09ba43a4d2b96d6c6aae0e3325",
|
|
15
15
|
"package.json": "cccea6fc4edf4b4b653bd5cfc0901ab20dd92ac8975408fb01ae3670005da641",
|
|
16
16
|
"package-lock.json": "c97a6a2f794aef083a39a8d2628eccc4a3c484e1c6225899b77ec1fb7c466d9e",
|
|
17
17
|
"lib/build-manifest.js": "88949f0112d683ca809bb67ca969e15634792423559a67bc79a534cd807e5535",
|
|
18
|
-
"lib/site-config.js": "
|
|
18
|
+
"lib/site-config.js": "ab7cc170dc30c74ea5ef08b8ff6a82f9820998ae4190a06d64375dfd7d515864",
|
|
19
19
|
"lib/performance-budget.js": "124c43494c092a956accec46d1463ea063ed73363d0db01f24aaea987459d683",
|
|
20
20
|
"lib/engagement-snapshot.js": "298c11b1ab135a3a98a4e31b6320ab60dfd7fda6a9d9053df37d86282a5a6687",
|
|
21
21
|
"lib/publication-state.js": "1b59c29a18bbb1193bddea898dac3a4fb832c7bc4751263be086f3d64392dea0",
|
|
22
|
-
"lib/render-markdown.js": "
|
|
22
|
+
"lib/render-markdown.js": "40e6d6743b76de6c16d138475f188a4263967530c370d8b26a363f4df58c2caf",
|
|
23
23
|
"lib/seo.js": "107de8804931945b8e73894877fc394d6b535a9d0d969a4ba5b68f2e136e0a17",
|
|
24
|
-
"lib/share-targets.js": "
|
|
25
|
-
"lib/provider-fixtures/
|
|
24
|
+
"lib/share-targets.js": "458aff86fab2b62a364442cd8c612565c6e0b53eef2f20cb6237cc562b1e0731",
|
|
25
|
+
"lib/provider-fixtures/embeds.v1.json": "5f26909fb433135321ddf7fdd14e45aeb702805c88f5676941166c5735143ee2",
|
|
26
|
+
"lib/provider-fixtures/share-intents.v1.json": "66a8eedfcff711253ee45ceb94b53f7d5a6be3ddbbb3d1f0d4e31a114326cc0a",
|
|
26
27
|
"src/404.njk": "be0d44370ac5402156aaf3d32ea581057bc61ec94c1a5f92bb809733af7a8a20",
|
|
27
|
-
"src/_includes/layouts/base.njk": "
|
|
28
|
+
"src/_includes/layouts/base.njk": "a37a0dfe534412bcade43719b2e2d6338463110105f84f3887e21ecc27e44b6c",
|
|
28
29
|
"src/_includes/layouts/post.njk": "1accd2832064ad19ee7bf18daf5fa1e60569f32c9b7a547934a6361f747d7eb7",
|
|
29
30
|
"src/_includes/components/ui.njk": "3a0eeb0f7b8f0ea46ed39d4c53b810a33b997766cbae540ef063c8ecdf641809",
|
|
30
31
|
"src/_data/buildManifest.js": "628163305984e0d3537e27b0d4050f5ba5ead12a834878266d2ce8004ffe1b1e",
|
|
@@ -33,14 +34,19 @@
|
|
|
33
34
|
"src/_data/languages.js": "1358748f7fa2f74406d3295a030827d6229a53e94da72cf712e719d7a356aeac",
|
|
34
35
|
"src/_data/site.js": "d9c001f4cc12a34027dd92303530b11b5402d47613459bb997e22c4bcbea326d",
|
|
35
36
|
"src/assets/theme-mode.js": "3797c84dbaa090b5b21427a31928b5dd97f13d04d99833f5e1e74c6b50e01dcc",
|
|
36
|
-
"src/assets/interactions.js": "
|
|
37
|
+
"src/assets/interactions.js": "7b9db09494518813189c17e92e5b9621b8b17a455c895d8670452fdfa0ead557",
|
|
38
|
+
"src/assets/embed-codepen.svg": "e9def697f38c466fb2b6652535c4c814e8cbbbefc6e052a69f335aeb27e8356b",
|
|
39
|
+
"src/assets/embed-gist.svg": "fcd9bc2bf29a5c118492b853c0a01390b7539c38ddb985da3c2a6e2f246a39f7",
|
|
40
|
+
"src/assets/embed-x.svg": "0ae74edc6790ec7e933067fe5392bce756e93fa7ad71caacf1c1ca99fa4ec9b6",
|
|
41
|
+
"src/assets/embed-youtube.svg": "10d192f2b14b67664fea88c8cf246321d20190def51d59fad5762e552219b067",
|
|
37
42
|
"src/assets/preferences.js": "789127bee013ed6a1bbf8cefd7b9109291c16220e7a6e09314e3c93c54731d1d",
|
|
38
43
|
"src/assets/search.js": "df425f0239277792ad39f2138bc89cb66cc4ecda5ed59042d46abc2f05372088",
|
|
39
|
-
"src/assets/theme.css": "
|
|
44
|
+
"src/assets/theme.css": "66f4793e857af61cbee63d847418aac03ce948361025d78853ec4d05e0117f43",
|
|
45
|
+
"src/contact.njk": "f4d74d038781e15af0acef571901c91e7753a2e21b6bef617429585698839a34",
|
|
40
46
|
"src/index.njk": "5c313f0fcdad3ddbfb79376bc9671b3f37539ef70c14bdbbd26d6e2e6de250e8",
|
|
41
47
|
"src/feed.11ty.js": "009bd2384e08e02a9405683bab1653bab3aeaec16accb0fd01ba69b24c777da9",
|
|
42
48
|
"src/languages.11ty.js": "399559199c65570d7d5efd88e20254838e316f81c06d7b28ec34990e963db915",
|
|
43
|
-
"src/posts.11ty.js": "
|
|
49
|
+
"src/posts.11ty.js": "89c0e4742a67cd03f9fd13c8c8d4ca56375d97628b3cb439af7f9c92c240104a",
|
|
44
50
|
"src/redirects.11ty.js": "3c163246a59f5364861db6a6f05ae88afc938a5379d86cf42776d8d49b0031f3",
|
|
45
51
|
"src/search-index.11ty.js": "700dc2a2be0ef20de3a828a4612fcf7a082691eade29c6e1d75f09c24b6c6024",
|
|
46
52
|
"src/search.njk": "63f2e8aeebb63c90da2e375eece639ce096cad1eff7f28628cafc46faf7ceee6",
|
|
@@ -23,6 +23,8 @@ async function verifiedMediaSource(postSource, mediaSource) {
|
|
|
23
23
|
|
|
24
24
|
export default async function (eleventyConfig) {
|
|
25
25
|
const [manifest, site] = await Promise.all([readBuildManifest(), loadSiteConfiguration()]);
|
|
26
|
+
const attributionTier = process.env.GALA_ATTRIBUTION_TIER === 'PAID' ? 'PAID' : 'FREE';
|
|
27
|
+
eleventyConfig.addGlobalData('attributionTier', attributionTier);
|
|
26
28
|
eleventyConfig.setLibrary('md', markdownLibrary);
|
|
27
29
|
eleventyConfig.addPassthroughCopy({ static: '/' });
|
|
28
30
|
eleventyConfig.addPassthroughCopy({ 'src/assets': 'assets' });
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
{
|
|
2
|
+
"schemaVersion": 1,
|
|
3
|
+
"providers": [
|
|
4
|
+
{
|
|
5
|
+
"id": "youtube",
|
|
6
|
+
"source": "https://support.google.com/youtube/answer/171780",
|
|
7
|
+
"verifiedOn": "2026-08-14",
|
|
8
|
+
"status": "verified-iframe",
|
|
9
|
+
"patterns": [
|
|
10
|
+
"^https://(?:www\\.)?youtube\\.com/watch\\?v=([A-Za-z0-9_-]{11})(?:&[^\\s]*)?$",
|
|
11
|
+
"^https://youtu\\.be/([A-Za-z0-9_-]{11})(?:\\?[^\\s]*)?$"
|
|
12
|
+
],
|
|
13
|
+
"activationTemplate": "https://www.youtube-nocookie.com/embed/{1}",
|
|
14
|
+
"label": "Watch on YouTube"
|
|
15
|
+
},
|
|
16
|
+
{
|
|
17
|
+
"id": "codepen",
|
|
18
|
+
"source": "https://blog.codepen.io/documentation/embedded-pens/",
|
|
19
|
+
"verifiedOn": "2026-08-14",
|
|
20
|
+
"status": "verified-iframe",
|
|
21
|
+
"patterns": [
|
|
22
|
+
"^https://codepen\\.io/([A-Za-z0-9_-]+)/pen/([A-Za-z0-9_-]+)/?$"
|
|
23
|
+
],
|
|
24
|
+
"activationTemplate": "https://codepen.io/{1}/embed/{2}",
|
|
25
|
+
"label": "Run on CodePen"
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
"id": "gist",
|
|
29
|
+
"source": "https://docs.github.com/en/get-started/writing-on-github/editing-and-sharing-content-with-gists/creating-gists",
|
|
30
|
+
"verifiedOn": "2026-08-14",
|
|
31
|
+
"status": "verified-script-only",
|
|
32
|
+
"patterns": [
|
|
33
|
+
"^https://gist\\.github\\.com/([A-Za-z0-9_-]+)/([0-9a-fA-F]+)(?:#[A-Za-z0-9_-]+)?$"
|
|
34
|
+
],
|
|
35
|
+
"labelTemplate": "View Gist by {1}"
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
"id": "x",
|
|
39
|
+
"source": "https://help.x.com/en/using-x/how-to-embed-a-post",
|
|
40
|
+
"verifiedOn": "2026-08-14",
|
|
41
|
+
"status": "verified-script-only",
|
|
42
|
+
"patterns": [
|
|
43
|
+
"^https://x\\.com/([A-Za-z0-9_]{1,15})/status/([0-9]+)$"
|
|
44
|
+
],
|
|
45
|
+
"label": "View post on X"
|
|
46
|
+
}
|
|
47
|
+
]
|
|
48
|
+
}
|
|
@@ -3,10 +3,10 @@
|
|
|
3
3
|
"providers": {
|
|
4
4
|
"bluesky": { "source": "https://docs.bsky.app/docs/advanced-guides/intent-links", "verifiedOn": "2026-08-13", "status": "verified", "template": "https://bsky.app/intent/compose?text={text}" },
|
|
5
5
|
"x": { "source": "https://developer.x.com/en/docs/twitter-for-websites/tweet-button/guides/web-intent", "verifiedOn": "2026-08-13", "status": "verified", "template": "https://twitter.com/intent/tweet?text={text}&url={url}" },
|
|
6
|
-
"linkedin": { "source": "https://
|
|
7
|
-
"reddit": { "source": "https://www.reddit.com/
|
|
6
|
+
"linkedin": { "source": "https://learn.microsoft.com/en-us/linkedin/consumer/integrations/self-serve/plugins/share-plugin", "verifiedOn": "2026-08-14", "status": "verified", "template": "https://www.linkedin.com/sharing/share-offsite/?url={url}", "verification": "live redirect retained the encoded target URL" },
|
|
7
|
+
"reddit": { "source": "https://www.reddit.com/r/bugs/comments/1cobgwm/simple_reddit_share_links_on_external_sites_no/", "checkedOn": "2026-08-14", "status": "blocked-public-intent-contract-unverified", "reason": "the public submit URL loads, but desktop title and URL prefill are not a stable provider contract" },
|
|
8
8
|
"hacker-news": { "source": "https://news.ycombinator.com/bookmarklet.html", "verifiedOn": "2026-08-13", "status": "verified", "template": "https://news.ycombinator.com/submitlink?u={url}&t={title}" },
|
|
9
|
-
"whatsapp": { "source": "https://faq.whatsapp.com/5913398998672934", "
|
|
9
|
+
"whatsapp": { "source": "https://faq.whatsapp.com/5913398998672934", "verifiedOn": "2026-08-14", "status": "verified", "template": "https://wa.me/?text={text}", "verification": "live redirect retained the encoded message text" },
|
|
10
10
|
"email": { "source": "https://www.rfc-editor.org/rfc/rfc6068", "verifiedOn": "2026-08-13", "status": "verified", "template": "mailto:?subject={title}&body={text}" },
|
|
11
11
|
"mastodon": { "source": "https://docs.joinmastodon.org/user/external/#share", "verifiedOn": "2026-08-13", "status": "verified", "template": "https://{instance}/share?text={text}", "requiresInstance": true }
|
|
12
12
|
}
|
|
@@ -4,6 +4,12 @@ import sanitizeHtml from 'sanitize-html';
|
|
|
4
4
|
import { createHighlighter } from 'shiki';
|
|
5
5
|
import GithubSlugger from 'github-slugger';
|
|
6
6
|
import { randomBytes } from 'node:crypto';
|
|
7
|
+
import { readFileSync } from 'node:fs';
|
|
8
|
+
|
|
9
|
+
const embedFixture = JSON.parse(readFileSync(
|
|
10
|
+
new URL('./provider-fixtures/embeds.v1.json', import.meta.url),
|
|
11
|
+
'utf8'
|
|
12
|
+
));
|
|
7
13
|
|
|
8
14
|
const SHIKI_THEMES = Object.freeze({ light: 'github-light', dark: 'github-dark' });
|
|
9
15
|
const SHIKI_LANGUAGES = Object.freeze([
|
|
@@ -67,10 +73,115 @@ function headingAnchors(markdown) {
|
|
|
67
73
|
});
|
|
68
74
|
}
|
|
69
75
|
|
|
76
|
+
function fixtureEmbed(url, environment) {
|
|
77
|
+
for (const provider of embedFixture.providers) {
|
|
78
|
+
for (const pattern of provider.patterns) {
|
|
79
|
+
const match = new RegExp(pattern).exec(url);
|
|
80
|
+
if (!match) continue;
|
|
81
|
+
const replaceCaptures = (template) => template.replace(/\{([1-9])}/g,
|
|
82
|
+
(_placeholder, index) => match[Number(index)] ?? '');
|
|
83
|
+
return {
|
|
84
|
+
provider: provider.id,
|
|
85
|
+
source: url,
|
|
86
|
+
label: provider.label ?? replaceCaptures(provider.labelTemplate),
|
|
87
|
+
activation: provider.status === 'verified-iframe'
|
|
88
|
+
? replaceCaptures(provider.activationTemplate)
|
|
89
|
+
: null
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
environment.galaWarnings ??= [];
|
|
94
|
+
environment.galaWarnings.push(`Unsupported embed provider: ${url}`);
|
|
95
|
+
return null;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function safeEmbedLink(url) {
|
|
99
|
+
try {
|
|
100
|
+
const parsed = new URL(url);
|
|
101
|
+
return parsed.protocol === 'https:' && parsed.username === '' && parsed.password === '';
|
|
102
|
+
} catch {
|
|
103
|
+
return false;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function embedFacade(embed) {
|
|
108
|
+
const provider = markdown.utils.escapeHtml(embed.provider);
|
|
109
|
+
const source = markdown.utils.escapeHtml(embed.source);
|
|
110
|
+
const label = markdown.utils.escapeHtml(embed.label);
|
|
111
|
+
if (embed.activation == null) {
|
|
112
|
+
return `<figure class="gala-embed gala-embed--${provider}"><span class="gala-embed__icon" aria-hidden="true"></span><a href="${source}" rel="noopener noreferrer">${label}</a></figure>`;
|
|
113
|
+
}
|
|
114
|
+
const activation = markdown.utils.escapeHtml(embed.activation);
|
|
115
|
+
return `<figure class="gala-embed gala-embed--${provider}" data-gala-embed="${provider}"><span class="gala-embed__icon" aria-hidden="true"></span><button type="button" data-gala-embed-load="${provider}" data-gala-embed-src="${activation}">${label}</button><figcaption><a href="${source}" rel="noopener noreferrer">Open directly on ${provider === 'youtube' ? 'YouTube' : 'CodePen'}</a></figcaption></figure>`;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function embeds(markdown) {
|
|
119
|
+
markdown.block.ruler.before('paragraph', 'gala_embed', (state, startLine, _endLine, silent) => {
|
|
120
|
+
const start = state.bMarks[startLine] + state.tShift[startLine];
|
|
121
|
+
const end = state.eMarks[startLine];
|
|
122
|
+
const match = state.src.slice(start, end).match(/^\{%\s+embed\s+(https:\/\/\S+)\s+%}$/);
|
|
123
|
+
if (!match) return false;
|
|
124
|
+
if (silent) return true;
|
|
125
|
+
const resolved = fixtureEmbed(match[1], state.env);
|
|
126
|
+
if (resolved == null) {
|
|
127
|
+
const token = state.push('paragraph_open', 'p', 1);
|
|
128
|
+
token.map = [startLine, startLine + 1];
|
|
129
|
+
const inline = state.push('inline', '', 0);
|
|
130
|
+
inline.content = safeEmbedLink(match[1])
|
|
131
|
+
? `[${match[1]}](${match[1]})`
|
|
132
|
+
: `\`${match[1].replaceAll('`', '\\`')}\``;
|
|
133
|
+
inline.children = [];
|
|
134
|
+
state.push('paragraph_close', 'p', -1);
|
|
135
|
+
} else {
|
|
136
|
+
const placeholder = randomBytes(18).toString('hex');
|
|
137
|
+
state.env.galaEmbeds ??= new Map();
|
|
138
|
+
state.env.galaEmbeds.set(placeholder, embedFacade(resolved));
|
|
139
|
+
const token = state.push('html_block', '', 0);
|
|
140
|
+
token.content = `<div class="gala-embed-placeholder" data-embed-token="${placeholder}"></div>`;
|
|
141
|
+
token.map = [startLine, startLine + 1];
|
|
142
|
+
}
|
|
143
|
+
state.line = startLine + 1;
|
|
144
|
+
return true;
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function rejectRawHtmlMedia(markdown) {
|
|
149
|
+
markdown.core.ruler.after('inline', 'gala_reject_raw_html_media', (state) => {
|
|
150
|
+
const media = /<(?:img|audio|video|source|picture)\b/i;
|
|
151
|
+
for (const token of state.tokens) {
|
|
152
|
+
if ((token.type === 'html_block' || token.type === 'html_inline') && media.test(token.content)) {
|
|
153
|
+
throw new TypeError('Raw HTML media is not allowed; use Markdown image syntax');
|
|
154
|
+
}
|
|
155
|
+
for (const child of token.children ?? []) {
|
|
156
|
+
if (child.type === 'html_inline' && media.test(child.content)) {
|
|
157
|
+
throw new TypeError('Raw HTML media is not allowed; use Markdown image syntax');
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
|
|
70
164
|
const markdown = new MarkdownIt({ html: true, linkify: true, typographer: false })
|
|
71
165
|
.use(footnote)
|
|
72
166
|
.use(admonitions)
|
|
73
|
-
.use(headingAnchors)
|
|
167
|
+
.use(headingAnchors)
|
|
168
|
+
.use(embeds)
|
|
169
|
+
.use(rejectRawHtmlMedia);
|
|
170
|
+
|
|
171
|
+
const defaultImage = markdown.renderer.rules.image
|
|
172
|
+
?? ((tokens, index, options, _environment, self) => self.renderToken(tokens, index, options));
|
|
173
|
+
markdown.renderer.rules.image = (tokens, index, options, environment, self) => {
|
|
174
|
+
const token = randomBytes(18).toString('hex');
|
|
175
|
+
const image = defaultImage(tokens, index, options, environment, self);
|
|
176
|
+
environment.galaImages ??= new Map();
|
|
177
|
+
environment.galaImages.set(token, sanitizeHtml(image, {
|
|
178
|
+
allowedTags: ['img'],
|
|
179
|
+
allowedAttributes: { img: ['src', 'alt', 'title'] },
|
|
180
|
+
allowedSchemesByTag: { img: ['https'] },
|
|
181
|
+
allowProtocolRelative: false
|
|
182
|
+
}));
|
|
183
|
+
return `<span class="gala-image-placeholder" data-image-token="${token}"></span>`;
|
|
184
|
+
};
|
|
74
185
|
|
|
75
186
|
const defaultFence = markdown.renderer.rules.fence.bind(markdown.renderer.rules);
|
|
76
187
|
markdown.renderer.rules.fence = (tokens, index, options, environment, self) => {
|
|
@@ -93,7 +204,7 @@ markdown.renderer.rules.fence = (tokens, index, options, environment, self) => {
|
|
|
93
204
|
const sanitizeOptions = {
|
|
94
205
|
allowedTags: [
|
|
95
206
|
'a', 'abbr', 'aside', 'blockquote', 'br', 'code', 'del', 'div', 'em',
|
|
96
|
-
'figcaption', 'figure', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'hr',
|
|
207
|
+
'figcaption', 'figure', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'hr',
|
|
97
208
|
'li', 'mark', 'ol', 'p', 'pre', 'span', 'strong', 'sub', 'sup', 'table',
|
|
98
209
|
'tbody', 'td', 'th', 'thead', 'tr', 'ul'
|
|
99
210
|
],
|
|
@@ -101,17 +212,16 @@ const sanitizeOptions = {
|
|
|
101
212
|
a: ['href', 'title', 'rel'],
|
|
102
213
|
aside: ['class', 'role'],
|
|
103
214
|
code: ['class'],
|
|
104
|
-
div: ['class', 'id', 'data-highlight-token'],
|
|
215
|
+
div: ['class', 'id', 'data-highlight-token', 'data-embed-token'],
|
|
105
216
|
h1: ['id'],
|
|
106
217
|
h2: ['id'],
|
|
107
218
|
h3: ['id'],
|
|
108
219
|
h4: ['id'],
|
|
109
220
|
h5: ['id'],
|
|
110
221
|
h6: ['id'],
|
|
111
|
-
img: ['src', 'alt', 'title', 'width', 'height', 'loading', 'decoding'],
|
|
112
222
|
li: ['id'],
|
|
113
223
|
ol: ['class'],
|
|
114
|
-
span: ['class'],
|
|
224
|
+
span: ['class', 'data-image-token'],
|
|
115
225
|
sup: ['class'],
|
|
116
226
|
td: ['colspan', 'rowspan'],
|
|
117
227
|
th: ['colspan', 'rowspan', 'scope']
|
|
@@ -121,17 +231,14 @@ const sanitizeOptions = {
|
|
|
121
231
|
'admonition', 'admonition-note', 'admonition-tip', 'admonition-important',
|
|
122
232
|
'admonition-warning', 'admonition-caution'
|
|
123
233
|
],
|
|
124
|
-
div: ['gala-highlight-placeholder'],
|
|
234
|
+
div: ['gala-highlight-placeholder', 'gala-embed-placeholder'],
|
|
125
235
|
code: [/^language-[a-z0-9_-]+$/],
|
|
126
236
|
ol: ['footnotes-list'],
|
|
127
|
-
span: ['footnote-backref'],
|
|
237
|
+
span: ['footnote-backref', 'gala-image-placeholder'],
|
|
128
238
|
sup: ['footnote-ref']
|
|
129
239
|
},
|
|
130
240
|
allowedSchemes: ['https', 'mailto'],
|
|
131
|
-
allowedSchemesByTag: {
|
|
132
|
-
a: ['https', 'mailto'],
|
|
133
|
-
img: ['https']
|
|
134
|
-
},
|
|
241
|
+
allowedSchemesByTag: { a: ['https', 'mailto'] },
|
|
135
242
|
allowProtocolRelative: false,
|
|
136
243
|
enforceHtmlBoundary: true
|
|
137
244
|
};
|
|
@@ -140,6 +247,9 @@ function render(source, inline, suppliedEnvironment) {
|
|
|
140
247
|
const environment = suppliedEnvironment ?? {};
|
|
141
248
|
environment.galaHighlights = [];
|
|
142
249
|
environment.galaHighlightTokens = [];
|
|
250
|
+
environment.galaEmbeds = new Map();
|
|
251
|
+
environment.galaImages = new Map();
|
|
252
|
+
environment.galaWarnings = [];
|
|
143
253
|
const unsafe = inline
|
|
144
254
|
? markdown.renderInline(source, environment)
|
|
145
255
|
: markdown.render(source, environment);
|
|
@@ -150,6 +260,18 @@ function render(source, inline, suppliedEnvironment) {
|
|
|
150
260
|
environment.galaHighlights[index]
|
|
151
261
|
);
|
|
152
262
|
}
|
|
263
|
+
for (const [token, facade] of environment.galaEmbeds) {
|
|
264
|
+
safe = safe.replace(
|
|
265
|
+
`<div class="gala-embed-placeholder" data-embed-token="${token}"></div>`,
|
|
266
|
+
facade
|
|
267
|
+
);
|
|
268
|
+
}
|
|
269
|
+
for (const [token, image] of environment.galaImages) {
|
|
270
|
+
safe = safe.replace(
|
|
271
|
+
`<span class="gala-image-placeholder" data-image-token="${token}"></span>`,
|
|
272
|
+
image
|
|
273
|
+
);
|
|
274
|
+
}
|
|
153
275
|
return safe;
|
|
154
276
|
}
|
|
155
277
|
|
|
@@ -162,7 +284,11 @@ export function renderMarkdownDocument(source) {
|
|
|
162
284
|
if (typeof source !== 'string') throw new TypeError('Markdown source must be a string');
|
|
163
285
|
const environment = {};
|
|
164
286
|
const html = render(source, false, environment);
|
|
165
|
-
return Object.freeze({
|
|
287
|
+
return Object.freeze({
|
|
288
|
+
html,
|
|
289
|
+
tableOfContents: Object.freeze(environment.galaToc ?? []),
|
|
290
|
+
warnings: Object.freeze(environment.galaWarnings ?? [])
|
|
291
|
+
});
|
|
166
292
|
}
|
|
167
293
|
|
|
168
294
|
export const markdownLibrary = Object.assign(Object.create(markdown), {
|
|
@@ -8,6 +8,8 @@ const fixture = JSON.parse(readFileSync(
|
|
|
8
8
|
const LABELS = Object.freeze({
|
|
9
9
|
bluesky: 'Bluesky',
|
|
10
10
|
x: 'X',
|
|
11
|
+
linkedin: 'LinkedIn',
|
|
12
|
+
whatsapp: 'WhatsApp',
|
|
11
13
|
'hacker-news': 'Hacker News',
|
|
12
14
|
email: 'Email',
|
|
13
15
|
mastodon: 'Mastodon'
|
|
@@ -30,7 +32,7 @@ function resolveProvider(provider, title, canonical, socialProfiles) {
|
|
|
30
32
|
const profile = requireHttpsUrl(socialProfiles.mastodon, 'sharing.socialProfiles.mastodon');
|
|
31
33
|
instance = profile.host;
|
|
32
34
|
}
|
|
33
|
-
const text = provider === 'bluesky' || provider === 'mastodon'
|
|
35
|
+
const text = provider === 'bluesky' || provider === 'mastodon' || provider === 'whatsapp'
|
|
34
36
|
? `${title}\n${canonical.href}`
|
|
35
37
|
: provider === 'email' ? canonical.href : title;
|
|
36
38
|
const replacements = {
|
|
@@ -7,6 +7,22 @@ const BUDGET_KEYS = Object.freeze([
|
|
|
7
7
|
'managedCssBytes',
|
|
8
8
|
'ordinaryHtmlBytes'
|
|
9
9
|
]);
|
|
10
|
+
const DESIGN_VALUES = Object.freeze({
|
|
11
|
+
layout: Object.freeze(['article-first', 'portfolio']),
|
|
12
|
+
palette: Object.freeze(['default', 'ocean'])
|
|
13
|
+
});
|
|
14
|
+
const CONTACT_KEYS = Object.freeze([
|
|
15
|
+
'enabled', 'websiteEnabled', 'phoneEnabled'
|
|
16
|
+
]);
|
|
17
|
+
|
|
18
|
+
function validateImplementedDesign(config) {
|
|
19
|
+
if (config?.design == null) return;
|
|
20
|
+
for (const [field, allowed] of Object.entries(DESIGN_VALUES)) {
|
|
21
|
+
if (!allowed.includes(config.design[field])) {
|
|
22
|
+
throw new TypeError(`Unsupported design.${field}: ${config.design[field]}`);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
}
|
|
10
26
|
|
|
11
27
|
export function validatePerformanceBudgets(value) {
|
|
12
28
|
if (value == null || Array.isArray(value) || typeof value !== 'object') {
|
|
@@ -40,6 +56,23 @@ export function validateStatistics(value) {
|
|
|
40
56
|
return Object.freeze({ publicViewCounts: value.publicViewCounts === true });
|
|
41
57
|
}
|
|
42
58
|
|
|
59
|
+
export function validateContact(value) {
|
|
60
|
+
if (value == null) return Object.freeze({ enabled: false, websiteEnabled: false, phoneEnabled: false });
|
|
61
|
+
if (Array.isArray(value) || typeof value !== 'object') throw new TypeError('contact must be a mapping');
|
|
62
|
+
const unknown = Object.keys(value).filter((key) => !CONTACT_KEYS.includes(key));
|
|
63
|
+
if (unknown.length > 0) throw new TypeError(`Unsupported contact option: ${unknown.join(', ')}`);
|
|
64
|
+
for (const field of ['enabled', 'websiteEnabled', 'phoneEnabled']) {
|
|
65
|
+
if (value[field] != null && typeof value[field] !== 'boolean') {
|
|
66
|
+
throw new TypeError(`contact.${field} must be a boolean`);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
return Object.freeze({
|
|
70
|
+
enabled: value.enabled === true,
|
|
71
|
+
websiteEnabled: value.websiteEnabled === true,
|
|
72
|
+
phoneEnabled: value.phoneEnabled === true
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
|
|
43
76
|
export async function loadSiteConfiguration({
|
|
44
77
|
root = process.cwd(),
|
|
45
78
|
configPath = process.env.GALA_CONFIG_PATH ?? 'site.config.yml'
|
|
@@ -58,7 +91,9 @@ export async function loadSiteConfiguration({
|
|
|
58
91
|
throw new TypeError('site configuration must be a regular file');
|
|
59
92
|
}
|
|
60
93
|
const config = parse(await readFile(file, 'utf8'));
|
|
94
|
+
validateImplementedDesign(config);
|
|
61
95
|
validatePerformanceBudgets(config?.performance?.budgets);
|
|
62
96
|
config.statistics = validateStatistics(config.statistics);
|
|
97
|
+
config.contact = validateContact(config.contact);
|
|
63
98
|
return config;
|
|
64
99
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
<!doctype html>
|
|
2
|
-
<html lang="{{ post.language or site.site.defaultLanguage }}" data-theme="{{ site.design.theme }}" data-palette="{{ site.design.palette }}" data-mode="system">
|
|
2
|
+
<html lang="{{ post.language or site.site.defaultLanguage }}" data-theme="{{ site.design.theme }}" data-layout="{{ site.design.layout }}" data-palette="{{ site.design.palette }}" data-mode="system">
|
|
3
3
|
<head>
|
|
4
4
|
<meta charset="utf-8">
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
@@ -35,6 +35,9 @@
|
|
|
35
35
|
<svg aria-hidden="true" viewBox="0 0 24 24"><path d="M3 11.5 12 4l9 7.5v8a1 1 0 0 1-1 1h-5v-6H9v6H4a1 1 0 0 1-1-1z"/></svg>
|
|
36
36
|
</a>
|
|
37
37
|
<nav class="gala-site-header__actions" aria-label="Site controls">
|
|
38
|
+
{% if site.contact.enabled %}<a class="gala-icon-control" href="{{ '/contact/' | url }}" aria-label="Contact author" title="Contact">
|
|
39
|
+
<svg aria-hidden="true" viewBox="0 0 24 24"><path d="M3 5h18v14H3V5Zm2 2v.4l7 5 7-5V7H5Zm14 10V9.8l-7 5-7-5V17h14Z"/></svg>
|
|
40
|
+
</a>{% endif %}
|
|
38
41
|
<button class="gala-icon-control" type="button" data-theme-mode-toggle aria-label="Color mode: system. Activate for light." title="Appearance">
|
|
39
42
|
<svg aria-hidden="true" viewBox="0 0 24 24"><path d="M12 3a9 9 0 1 0 0 18V3Zm0 2v14a7 7 0 0 1 0-14Z"/></svg>
|
|
40
43
|
<span class="gala-visually-hidden" data-theme-mode-label>Theme: system</span>
|
|
@@ -51,6 +54,10 @@
|
|
|
51
54
|
</nav>
|
|
52
55
|
</header>
|
|
53
56
|
<main id="main-content">{{ content | safe }}</main>
|
|
57
|
+
<footer class="gala-page-footer">
|
|
58
|
+
<p>© {{ site.site.name }}</p>
|
|
59
|
+
{% if attributionTier != 'PAID' %}<p><a href="https://gala67.com/?utm_source=gala-site&utm_medium=referral" rel="external">Built with Gala</a></p>{% endif %}
|
|
60
|
+
</footer>
|
|
54
61
|
<dialog id="gala-settings-dialog" class="gala-dialog" aria-labelledby="gala-settings-title">
|
|
55
62
|
<form method="dialog" class="gala-dialog__close"><button type="submit" aria-label="Close settings">Close</button></form>
|
|
56
63
|
<h2 id="gala-settings-title">Reader settings</h2>
|
|
@@ -84,7 +91,7 @@
|
|
|
84
91
|
<dialog id="gala-account-dialog" class="gala-dialog gala-account-dialog" aria-labelledby="gala-account-title">
|
|
85
92
|
<form method="dialog" class="gala-dialog__close"><button type="submit" aria-label="Close account">Close</button></form>
|
|
86
93
|
<h2 id="gala-account-title">Gala account</h2>
|
|
87
|
-
<iframe data-gala-session-frame title="Gala account and sign in" src="{{ site.platform.apiBaseUrl or 'https://api.gala67.com' }}/v1/widget/session?siteId={{ site.site.id | escape }}"></iframe>
|
|
94
|
+
<iframe data-gala-session-frame title="Gala account and sign in" allow="identity-credentials-get" src="{{ site.platform.apiBaseUrl or 'https://api.gala67.com' }}/v1/widget/session?siteId={{ site.site.id | escape }}"></iframe>
|
|
88
95
|
</dialog>
|
|
89
96
|
{% endif %}
|
|
90
97
|
</body>
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" role="img" aria-label="CodePen"><path d="m12 1 10 6.5v9L12 23 2 16.5v-9zm0 3.4L6.2 8.2 12 12l5.8-3.8zm-7 6.2v3l4.5-1.5zm14 0-4.5 1.5 4.5 1.5zM6.2 15.8l5.8 3.8 5.8-3.8-5.8-3.7z" fill="currentColor"/></svg>
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" role="img" aria-label="GitHub Gist"><path d="M7 4 2 12l5 8h3l-5-8 5-8zm10 0-5 8 5 8h3l-5-8 5-8z" fill="currentColor"/></svg>
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" role="img" aria-label="X"><path d="M3 2h5.4l4.7 6.3L18.5 2H21l-6.7 8.2L22 22h-5.4l-5.2-7L5.5 22H3l7.2-8.9zm4.1 2 10.5 16h1.3L8.4 4z" fill="currentColor"/></svg>
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" role="img" aria-label="YouTube"><rect x="1" y="4" width="22" height="16" rx="4" fill="#f00"/><path d="m10 8 6 4-6 4z" fill="#fff"/></svg>
|
|
@@ -8,6 +8,26 @@ function selectableFallback(control) {
|
|
|
8
8
|
}
|
|
9
9
|
|
|
10
10
|
document.addEventListener('click', async (event) => {
|
|
11
|
+
const embed = event.target.closest('[data-gala-embed-load]');
|
|
12
|
+
if (embed) {
|
|
13
|
+
const container = embed.closest('[data-gala-embed]');
|
|
14
|
+
const source = embed.dataset.galaEmbedSrc;
|
|
15
|
+
const provider = embed.dataset.galaEmbedLoad;
|
|
16
|
+
if (!container || !source || !['youtube', 'codepen'].includes(provider)) return;
|
|
17
|
+
const frame = document.createElement('iframe');
|
|
18
|
+
frame.src = source;
|
|
19
|
+
frame.title = provider === 'youtube' ? 'YouTube video' : 'CodePen example';
|
|
20
|
+
frame.loading = 'eager';
|
|
21
|
+
frame.referrerPolicy = 'strict-origin-when-cross-origin';
|
|
22
|
+
frame.setAttribute('sandbox', 'allow-forms allow-popups allow-presentation allow-same-origin allow-scripts');
|
|
23
|
+
frame.setAttribute('allow', provider === 'youtube'
|
|
24
|
+
? 'accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture; web-share'
|
|
25
|
+
: 'fullscreen');
|
|
26
|
+
frame.setAttribute('allowfullscreen', '');
|
|
27
|
+
container.replaceChildren(frame);
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
|
|
11
31
|
const dialogControl = event.target.closest('[data-open-dialog]');
|
|
12
32
|
if (dialogControl) {
|
|
13
33
|
const dialog = document.getElementById(dialogControl.dataset.openDialog);
|
|
@@ -374,8 +394,9 @@ function engagementErrorMessage(code) {
|
|
|
374
394
|
if (code === 'AUTHENTICATION_REQUIRED' || code === 'INVALID_BEARER_TOKEN'
|
|
375
395
|
|| code === 'REAUTHENTICATION_REQUIRED') return 'Sign in again to continue.';
|
|
376
396
|
if (code === 'ENGAGEMENT_RATE_LIMITED') return 'You are posting too quickly; try again shortly.';
|
|
397
|
+
if (code === 'CONTACT_RATE_LIMITED') return 'You are sending too quickly; try again later.';
|
|
377
398
|
if (code === 'RESOURCE_NOT_FOUND') return 'That item is no longer available.';
|
|
378
|
-
if (code === 'INVALID_ENGAGEMENT_WRITE' || code === 'INVALID_REQUEST') return 'Check your entry and try again.';
|
|
399
|
+
if (code === 'INVALID_ENGAGEMENT_WRITE' || code === 'INVALID_CONTACT_SUBMISSION' || code === 'INVALID_REQUEST') return 'Check your entry and try again.';
|
|
379
400
|
if (code === 'ACCESS_DENIED') return 'That action is not available for this account.';
|
|
380
401
|
if (code === 'IDEMPOTENCY_CONFLICT' || code === 'ENGAGEMENT_STATE_CONFLICT') return 'The item changed; reload and try again.';
|
|
381
402
|
return 'The action could not be completed. Try again.';
|
|
@@ -392,6 +413,33 @@ function sendEngagementWrite(operation, payload) {
|
|
|
392
413
|
});
|
|
393
414
|
}
|
|
394
415
|
|
|
416
|
+
document.addEventListener('submit', async (event) => {
|
|
417
|
+
const form = event.target.closest('[data-contact-form] form');
|
|
418
|
+
if (!form) return;
|
|
419
|
+
event.preventDefault();
|
|
420
|
+
const region = form.closest('[data-contact-form]');
|
|
421
|
+
const status = region.querySelector('[data-contact-status]');
|
|
422
|
+
if (!sessionUser) {
|
|
423
|
+
status.textContent = 'Sign in with the account button before sending.';
|
|
424
|
+
return;
|
|
425
|
+
}
|
|
426
|
+
const data = new FormData(form);
|
|
427
|
+
try {
|
|
428
|
+
status.textContent = 'Sending…';
|
|
429
|
+
await sendEngagementWrite('contact.submit', {
|
|
430
|
+
siteId: region.dataset.siteId,
|
|
431
|
+
subject: data.get('subject'),
|
|
432
|
+
message: data.get('message'),
|
|
433
|
+
website: data.get('website') || null,
|
|
434
|
+
phone: data.get('phone') || null
|
|
435
|
+
});
|
|
436
|
+
form.reset();
|
|
437
|
+
status.textContent = 'Message sent.';
|
|
438
|
+
} catch (error) {
|
|
439
|
+
status.textContent = engagementErrorMessage(error.message);
|
|
440
|
+
}
|
|
441
|
+
});
|
|
442
|
+
|
|
395
443
|
async function mutateEngagement(region, operation, payload) {
|
|
396
444
|
const status = region.querySelector('[data-engagement-status]');
|
|
397
445
|
try {
|
|
@@ -30,6 +30,32 @@
|
|
|
30
30
|
--gala-color-border: #aaa;
|
|
31
31
|
}
|
|
32
32
|
|
|
33
|
+
:root[data-palette='ocean'] {
|
|
34
|
+
--gala-color-background: light-dark(#f7fbff, #071b2d);
|
|
35
|
+
--gala-color-text: light-dark(#102a43, #e8f4ff);
|
|
36
|
+
--gala-color-surface: light-dark(#d9eaf7, #102f49);
|
|
37
|
+
--gala-color-accent: light-dark(#005ea8, #78c8ff);
|
|
38
|
+
--gala-color-border: light-dark(#526d82, #8aa8bf);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
:root[data-palette='ocean'][data-mode='light'] {
|
|
42
|
+
--gala-color-background: #f7fbff;
|
|
43
|
+
--gala-color-text: #102a43;
|
|
44
|
+
--gala-color-surface: #d9eaf7;
|
|
45
|
+
--gala-color-accent: #005ea8;
|
|
46
|
+
--gala-color-border: #526d82;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
:root[data-palette='ocean'][data-mode='dark'] {
|
|
50
|
+
--gala-color-background: #071b2d;
|
|
51
|
+
--gala-color-text: #e8f4ff;
|
|
52
|
+
--gala-color-surface: #102f49;
|
|
53
|
+
--gala-color-accent: #78c8ff;
|
|
54
|
+
--gala-color-border: #8aa8bf;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
:root[data-layout='portfolio'] { --gala-content-width: 82rem; }
|
|
58
|
+
|
|
33
59
|
body {
|
|
34
60
|
margin: 0;
|
|
35
61
|
padding: var(--gala-space-page);
|
|
@@ -126,6 +152,8 @@ button:focus-visible {
|
|
|
126
152
|
.gala-page-footer ul,
|
|
127
153
|
.gala-share ul { display: flex; flex-wrap: wrap; gap: 1rem; padding: 0; list-style: none; }
|
|
128
154
|
.gala-card-index { display: grid; grid-template-columns: repeat(auto-fit, minmax(min(100%, 18rem), 1fr)); gap: 1rem; padding: 0; list-style: none; }
|
|
155
|
+
[data-layout='portfolio'] .gala-card-index { grid-template-columns: repeat(auto-fit, minmax(min(100%, 22rem), 1fr)); gap: 1.5rem; }
|
|
156
|
+
[data-layout='portfolio'] .gala-card { padding: clamp(1.25rem, 3vw, 2rem); }
|
|
129
157
|
.gala-card { block-size: 100%; box-sizing: border-box; padding: 1.25rem; background: var(--gala-color-surface); border-radius: var(--gala-radius-control); }
|
|
130
158
|
.gala-stats-graph { min-block-size: var(--gala-widget-min-block-size); margin-inline: 0; }
|
|
131
159
|
.gala-stats-graph svg { inline-size: 100%; block-size: 8rem; }
|
|
@@ -164,6 +192,33 @@ button:focus-visible {
|
|
|
164
192
|
.gala-code-block { position: relative; }
|
|
165
193
|
.gala-code-block > button { position: absolute; inset-block-start: 0.5rem; inset-inline-end: 0.5rem; background: var(--gala-color-background); }
|
|
166
194
|
.gala-code-block pre { overflow: auto; padding: 3.5rem 1rem 1rem; background: var(--gala-color-surface); border-radius: var(--gala-radius-control); }
|
|
195
|
+
.gala-embed {
|
|
196
|
+
display: grid;
|
|
197
|
+
place-items: center;
|
|
198
|
+
gap: 0.75rem;
|
|
199
|
+
box-sizing: border-box;
|
|
200
|
+
inline-size: 100%;
|
|
201
|
+
margin-inline: 0;
|
|
202
|
+
padding: 1rem;
|
|
203
|
+
overflow: hidden;
|
|
204
|
+
background: var(--gala-color-surface);
|
|
205
|
+
border: 0.125rem solid var(--gala-color-border);
|
|
206
|
+
border-radius: var(--gala-radius-control);
|
|
207
|
+
}
|
|
208
|
+
.gala-embed--youtube { aspect-ratio: 16 / 9; }
|
|
209
|
+
.gala-embed--codepen { min-block-size: 25rem; }
|
|
210
|
+
.gala-embed--gist,
|
|
211
|
+
.gala-embed--x { min-block-size: 8rem; }
|
|
212
|
+
.gala-embed iframe { inline-size: 100%; block-size: 100%; border: 0; }
|
|
213
|
+
.gala-embed__icon {
|
|
214
|
+
inline-size: 2rem;
|
|
215
|
+
block-size: 2rem;
|
|
216
|
+
background: center / contain no-repeat;
|
|
217
|
+
}
|
|
218
|
+
.gala-embed--youtube .gala-embed__icon { background-image: url('./embed-youtube.svg'); }
|
|
219
|
+
.gala-embed--codepen .gala-embed__icon { background-image: url('./embed-codepen.svg'); }
|
|
220
|
+
.gala-embed--gist .gala-embed__icon { background-image: url('./embed-gist.svg'); }
|
|
221
|
+
.gala-embed--x .gala-embed__icon { background-image: url('./embed-x.svg'); }
|
|
167
222
|
.shiki,
|
|
168
223
|
.shiki span { color: var(--shiki-light); background-color: var(--shiki-light-bg); }
|
|
169
224
|
:root[data-mode='dark'] .shiki,
|
|
@@ -173,6 +228,11 @@ button:focus-visible {
|
|
|
173
228
|
:root[data-mode='system'] .shiki span { color: var(--shiki-dark); background-color: var(--shiki-dark-bg); }
|
|
174
229
|
}
|
|
175
230
|
.gala-visually-hidden { position: absolute; inline-size: 1px; block-size: 1px; overflow: hidden; clip-path: inset(50%); white-space: nowrap; }
|
|
231
|
+
.gala-contact form { display: grid; gap: 1rem; max-inline-size: 42rem; }
|
|
232
|
+
.gala-contact label { display: grid; gap: .35rem; }
|
|
233
|
+
.gala-contact input,
|
|
234
|
+
.gala-contact textarea { font: inherit; padding: .65rem; }
|
|
235
|
+
.gala-contact textarea { min-block-size: 12rem; resize: vertical; }
|
|
176
236
|
|
|
177
237
|
@keyframes gala-spin { to { transform: rotate(1turn); } }
|
|
178
238
|
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
---
|
|
2
|
+
layout: layouts/base.njk
|
|
3
|
+
title: Contact
|
|
4
|
+
permalink: /contact/index.html
|
|
5
|
+
eleventyExcludeFromCollections: true
|
|
6
|
+
---
|
|
7
|
+
{% if site.contact.enabled %}
|
|
8
|
+
<article class="gala-contact" data-contact-form data-site-id="{{ site.site.id | escape }}">
|
|
9
|
+
<h1>Contact</h1>
|
|
10
|
+
<p>Sign in with your Gala account to send a private message to the author.</p>
|
|
11
|
+
<form>
|
|
12
|
+
<label>Subject <input name="subject" maxlength="200" required></label>
|
|
13
|
+
<label>Message <textarea name="message" maxlength="5000" required></textarea></label>
|
|
14
|
+
{% if site.contact.websiteEnabled %}<label>Website (optional) <input name="website" type="url" maxlength="2048" autocomplete="url"></label>{% endif %}
|
|
15
|
+
{% if site.contact.phoneEnabled %}<label>Phone (optional) <input name="phone" type="tel" maxlength="32" autocomplete="tel"></label>{% endif %}
|
|
16
|
+
<button type="submit">Send message</button>
|
|
17
|
+
</form>
|
|
18
|
+
<p data-contact-status role="status" aria-live="polite"></p>
|
|
19
|
+
</article>
|
|
20
|
+
{% else %}
|
|
21
|
+
<h1>Contact</h1><p>This site is not accepting contact messages.</p>
|
|
22
|
+
{% endif %}
|
|
@@ -52,6 +52,9 @@ function renderedDocument(post) {
|
|
|
52
52
|
let document = renderedDocuments.get(post);
|
|
53
53
|
if (!document) {
|
|
54
54
|
document = renderMarkdownDocument(post.body);
|
|
55
|
+
for (const warning of document.warnings) {
|
|
56
|
+
console.warn(`${post.source}: warning: ${warning}`);
|
|
57
|
+
}
|
|
55
58
|
renderedDocuments.set(post, document);
|
|
56
59
|
}
|
|
57
60
|
return document;
|