Merge pull request #311 from cheeaun/main

Update from main
This commit is contained in:
Chee Aun 2023-11-13 22:13:48 +08:00 committed by GitHub
commit 11407d0f3c
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
13 changed files with 320 additions and 75 deletions

View file

@ -20,11 +20,13 @@ jobs:
with:
node-version: 18
- run: npm ci && npm run build
- run: cd dist && zip -r ../phanpy-dist.zip . && cd ..
- run: cd dist && zip -r ../phanpy-dist.zip . && tar -czf ../phanpy-dist.tar.gz . && cd ..
- id: tag_name
run: echo ::set-output name=tag_name::$(date +%Y.%m.%d).$(git rev-parse --short HEAD)
- uses: softprops/action-gh-release@v1
with:
tag_name: ${{ steps.tag_name.outputs.tag_name }}
generate_release_notes: true
files: phanpy-dist.zip
files: |
phanpy-dist.zip
phanpy-dist.tar.gz

1
.gitignore vendored
View file

@ -27,3 +27,4 @@ dist-ssr
.env.dev
src/data/instances-full.json
phanpy-dist.zip
phanpy-dist.tar.gz

View file

@ -172,6 +172,7 @@ And here I am. Building a Mastodon web client.
- [Litterbox](https://litterbox.koyu.space/)
- [Statuzer](https://statuzer.com/)
- [Tusked](https://tusked.app/)
- [Mastodon Glitch Edition (standalone frontend)](https://iceshrimp.dev/iceshrimp/masto-fe-standalone)
- [More...](https://github.com/hueyy/awesome-mastodon/#clients)
## 💁‍♂️ Notice to all other social media client developers

58
package-lock.json generated
View file

@ -8,8 +8,8 @@
"name": "phanpy",
"version": "0.1.0",
"dependencies": {
"@formatjs/intl-localematcher": "~0.4.2",
"@formkit/auto-animate": "~0.8.0",
"@formatjs/intl-localematcher": "~0.5.0",
"@formkit/auto-animate": "~0.8.1",
"@github/text-expander-element": "~2.5.0",
"@iconify-icons/mingcute": "~1.2.9",
"@justinribeiro/lite-youtube": "~1.5.0",
@ -36,7 +36,7 @@
"swiped-events": "~1.1.7",
"toastify-js": "~1.12.0",
"uid": "~2.0.2",
"use-debounce": "~9.0.4",
"use-debounce": "~10.0.0",
"use-long-press": "~3.2.0",
"use-resize-observer": "~9.1.0",
"valtio": "1.9.0"
@ -51,7 +51,7 @@
"vite": "~4.5.0",
"vite-plugin-generate-file": "~0.0.4",
"vite-plugin-html-config": "~1.0.11",
"vite-plugin-pwa": "~0.16.6",
"vite-plugin-pwa": "~0.16.7",
"vite-plugin-remove-console": "~2.1.1",
"workbox-cacheable-response": "~7.0.0",
"workbox-expiration": "~7.0.0",
@ -3082,17 +3082,17 @@
}
},
"node_modules/@formatjs/intl-localematcher": {
"version": "0.4.2",
"resolved": "https://registry.npmjs.org/@formatjs/intl-localematcher/-/intl-localematcher-0.4.2.tgz",
"integrity": "sha512-BGdtJFmaNJy5An/Zan4OId/yR9Ih1OojFjcduX/xOvq798OgWSyDtd6Qd5jqJXwJs1ipe4Fxu9+cshic5Ox2tA==",
"version": "0.5.0",
"resolved": "https://registry.npmjs.org/@formatjs/intl-localematcher/-/intl-localematcher-0.5.0.tgz",
"integrity": "sha512-K1Xpg/8oyfCMxisJQa/fILoeoeyndcM0wcN8QiNG/uM5OAe1BcO1+2yd0gIboDI2tRJEsUi/sSBEYPbgkIdq4A==",
"dependencies": {
"tslib": "^2.4.0"
}
},
"node_modules/@formkit/auto-animate": {
"version": "0.8.0",
"resolved": "https://registry.npmjs.org/@formkit/auto-animate/-/auto-animate-0.8.0.tgz",
"integrity": "sha512-G8f7489ka0mWyi+1IEZT+xgIwcpWtRMmE2x+IrVoQ+KM1cP6VDj/TbujZjwxdb0P8w8b16/qBfViRmydbYHwMw=="
"version": "0.8.1",
"resolved": "https://registry.npmjs.org/@formkit/auto-animate/-/auto-animate-0.8.1.tgz",
"integrity": "sha512-0/Z2cuNXWVVIG/l0SpcHAWFhGdvLJ8DRvEfRWvmojtmRWfEy+LWNwgDazbZqY0qQYtkHcoEK3jBLkhiZaB/4Ig=="
},
"node_modules/@github/combobox-nav": {
"version": "2.1.5",
@ -7232,11 +7232,11 @@
}
},
"node_modules/use-debounce": {
"version": "9.0.4",
"resolved": "https://registry.npmjs.org/use-debounce/-/use-debounce-9.0.4.tgz",
"integrity": "sha512-6X8H/mikbrt0XE8e+JXRtZ8yYVvKkdYRfmIhWZYsP8rcNs9hk3APV8Ua2mFkKRLcJKVdnX2/Vwrmg2GWKUQEaQ==",
"version": "10.0.0",
"resolved": "https://registry.npmjs.org/use-debounce/-/use-debounce-10.0.0.tgz",
"integrity": "sha512-XRjvlvCB46bah9IBXVnq/ACP2lxqXyZj0D9hj4K5OzNroMDpTEBg8Anuh1/UfRTRs7pLhQ+RiNxxwZu9+MVl1A==",
"engines": {
"node": ">= 10.0.0"
"node": ">= 16.0.0"
},
"peerDependencies": {
"react": ">=16.8.0"
@ -7446,9 +7446,9 @@
}
},
"node_modules/vite-plugin-pwa": {
"version": "0.16.6",
"resolved": "https://registry.npmjs.org/vite-plugin-pwa/-/vite-plugin-pwa-0.16.6.tgz",
"integrity": "sha512-bQPDOWvhPMwydMoWqohXvIzvrq4X8iuCF+q95qEiaM4yC0ybViGKWMnWcpWp0vcnoLk7QvxHDlK65KUZvqB3Sg==",
"version": "0.16.7",
"resolved": "https://registry.npmjs.org/vite-plugin-pwa/-/vite-plugin-pwa-0.16.7.tgz",
"integrity": "sha512-4WMA5unuKlHs+koNoykeuCfTcqEGbiTRr8sVYUQMhc6tWxZpSRnv9Ojk4LKmqVhoPGHfBVCdGaMo8t9Qidkc1Q==",
"dev": true,
"dependencies": {
"debug": "^4.3.4",
@ -9602,17 +9602,17 @@
"optional": true
},
"@formatjs/intl-localematcher": {
"version": "0.4.2",
"resolved": "https://registry.npmjs.org/@formatjs/intl-localematcher/-/intl-localematcher-0.4.2.tgz",
"integrity": "sha512-BGdtJFmaNJy5An/Zan4OId/yR9Ih1OojFjcduX/xOvq798OgWSyDtd6Qd5jqJXwJs1ipe4Fxu9+cshic5Ox2tA==",
"version": "0.5.0",
"resolved": "https://registry.npmjs.org/@formatjs/intl-localematcher/-/intl-localematcher-0.5.0.tgz",
"integrity": "sha512-K1Xpg/8oyfCMxisJQa/fILoeoeyndcM0wcN8QiNG/uM5OAe1BcO1+2yd0gIboDI2tRJEsUi/sSBEYPbgkIdq4A==",
"requires": {
"tslib": "^2.4.0"
}
},
"@formkit/auto-animate": {
"version": "0.8.0",
"resolved": "https://registry.npmjs.org/@formkit/auto-animate/-/auto-animate-0.8.0.tgz",
"integrity": "sha512-G8f7489ka0mWyi+1IEZT+xgIwcpWtRMmE2x+IrVoQ+KM1cP6VDj/TbujZjwxdb0P8w8b16/qBfViRmydbYHwMw=="
"version": "0.8.1",
"resolved": "https://registry.npmjs.org/@formkit/auto-animate/-/auto-animate-0.8.1.tgz",
"integrity": "sha512-0/Z2cuNXWVVIG/l0SpcHAWFhGdvLJ8DRvEfRWvmojtmRWfEy+LWNwgDazbZqY0qQYtkHcoEK3jBLkhiZaB/4Ig=="
},
"@github/combobox-nav": {
"version": "2.1.5",
@ -12457,9 +12457,9 @@
}
},
"use-debounce": {
"version": "9.0.4",
"resolved": "https://registry.npmjs.org/use-debounce/-/use-debounce-9.0.4.tgz",
"integrity": "sha512-6X8H/mikbrt0XE8e+JXRtZ8yYVvKkdYRfmIhWZYsP8rcNs9hk3APV8Ua2mFkKRLcJKVdnX2/Vwrmg2GWKUQEaQ==",
"version": "10.0.0",
"resolved": "https://registry.npmjs.org/use-debounce/-/use-debounce-10.0.0.tgz",
"integrity": "sha512-XRjvlvCB46bah9IBXVnq/ACP2lxqXyZj0D9hj4K5OzNroMDpTEBg8Anuh1/UfRTRs7pLhQ+RiNxxwZu9+MVl1A==",
"requires": {}
},
"use-long-press": {
@ -12580,9 +12580,9 @@
"requires": {}
},
"vite-plugin-pwa": {
"version": "0.16.6",
"resolved": "https://registry.npmjs.org/vite-plugin-pwa/-/vite-plugin-pwa-0.16.6.tgz",
"integrity": "sha512-bQPDOWvhPMwydMoWqohXvIzvrq4X8iuCF+q95qEiaM4yC0ybViGKWMnWcpWp0vcnoLk7QvxHDlK65KUZvqB3Sg==",
"version": "0.16.7",
"resolved": "https://registry.npmjs.org/vite-plugin-pwa/-/vite-plugin-pwa-0.16.7.tgz",
"integrity": "sha512-4WMA5unuKlHs+koNoykeuCfTcqEGbiTRr8sVYUQMhc6tWxZpSRnv9Ojk4LKmqVhoPGHfBVCdGaMo8t9Qidkc1Q==",
"dev": true,
"requires": {
"debug": "^4.3.4",

View file

@ -10,8 +10,8 @@
"sourcemap": "npx source-map-explorer dist/assets/*.js"
},
"dependencies": {
"@formatjs/intl-localematcher": "~0.4.2",
"@formkit/auto-animate": "~0.8.0",
"@formatjs/intl-localematcher": "~0.5.0",
"@formkit/auto-animate": "~0.8.1",
"@github/text-expander-element": "~2.5.0",
"@iconify-icons/mingcute": "~1.2.9",
"@justinribeiro/lite-youtube": "~1.5.0",
@ -38,7 +38,7 @@
"swiped-events": "~1.1.7",
"toastify-js": "~1.12.0",
"uid": "~2.0.2",
"use-debounce": "~9.0.4",
"use-debounce": "~10.0.0",
"use-long-press": "~3.2.0",
"use-resize-observer": "~9.1.0",
"valtio": "1.9.0"
@ -53,7 +53,7 @@
"vite": "~4.5.0",
"vite-plugin-generate-file": "~0.0.4",
"vite-plugin-html-config": "~1.0.11",
"vite-plugin-pwa": "~0.16.6",
"vite-plugin-pwa": "~0.16.7",
"vite-plugin-remove-console": "~2.1.1",
"workbox-cacheable-response": "~7.0.0",
"workbox-expiration": "~7.0.0",

View file

@ -1135,6 +1135,7 @@ a[href^='http'][rel*='nofollow']:visited:not(:has(div)) {
touch-action: pan-x;
user-select: none;
width: 100%;
gap: 16px;
}
.carousel::-webkit-scrollbar {
display: none;

View file

@ -13,6 +13,7 @@ function AccountBlock({
skeleton,
account,
avatarSize = 'xl',
useAvatarStatic = false,
instance,
external,
internal,
@ -81,7 +82,11 @@ function AccountBlock({
}
}}
>
<Avatar url={avatar} size={avatarSize} squircle={bot} />
<Avatar
url={useAvatarStatic ? avatarStatic : avatar || avatarStatic}
size={avatarSize}
squircle={bot}
/>
<span class="account-block-content">
{!hideDisplayName && (
<>

View file

@ -137,14 +137,6 @@
border-color: transparent;
&.compose-field {
@media (width < 30em) {
margin-inline: calc(-1 * var(--form-padding-inline));
width: 100vw !important;
max-width: 100vw;
border-radius: 0;
border: 0;
}
@media (min-width: 40em) {
max-height: 65vh;
}
@ -212,11 +204,12 @@
left: -100vw !important;
}
#compose-container .toolbar-button select {
background-color: inherit;
background-color: transparent;
border: 0;
padding: 0 0 0 8px;
margin: 0;
appearance: none;
line-height: 1em;
}
#compose-container .toolbar-button:not(.show-field) select {
right: 0;
@ -619,3 +612,91 @@
#custom-emojis-sheet .custom-emojis-list button:is(:hover, :focus) img {
transform: scale(1.5);
}
.compose-field-container {
display: grid !important;
@media (width < 30em) {
margin-inline: calc(-1 * var(--form-padding-inline));
width: 100vw !important;
max-width: 100vw;
.compose-field {
border-radius: 0;
outline-offset: -2px;
}
}
&.debug {
grid-template-columns: 1fr 1fr;
}
> * {
grid-area: 1 / 1 / 2 / 2;
}
.compose-highlight {
user-drag: none;
user-select: none;
pointer-events: none;
touch-action: none;
padding: 8px;
color: transparent;
background-color: transparent;
border: 2px solid transparent;
line-height: 1.4;
overflow: auto;
unicode-bidi: plaintext;
-webkit-rtl-ordering: logical;
rtl-ordering: logical;
overflow-wrap: break-word;
white-space: pre-wrap;
min-height: 5em;
max-height: 50vh;
/* Follow textarea styles */
@media (min-width: 40em) {
max-height: 65vh;
}
mark {
color: inherit;
}
.compose-highlight-url,
.compose-highlight-hashtag {
background-color: transparent;
text-decoration: underline;
text-decoration-color: var(--link-faded-color);
text-decoration-thickness: 2px;
text-underline-offset: 2px;
}
.compose-highlight-mention,
.compose-highlight-emoji-shortcode,
.compose-highlight-exceeded {
mix-blend-mode: multiply;
border-radius: 4px;
box-shadow: 0 0 0 1px;
}
.compose-highlight-mention {
background-color: var(--orange-light-bg-color);
box-shadow-color: var(--orange-light-bg-color);
}
.compose-highlight-emoji-shortcode {
background-color: var(--bg-faded-color);
box-shadow-color: var(--bg-faded-color);
}
.compose-highlight-exceeded {
background-color: var(--red-bg-color);
box-shadow-color: var(--red-bg-color);
}
@media (prefers-color-scheme: dark) {
.compose-highlight-mention,
.compose-highlight-emoji-shortcode,
.compose-highlight-exceeded {
mix-blend-mode: screen;
}
}
}
}

View file

@ -7,7 +7,7 @@ import { useEffect, useMemo, useRef, useState } from 'preact/hooks';
import { useHotkeys } from 'react-hotkeys-hook';
import stringLength from 'string-length';
import { uid } from 'uid/single';
import { useDebouncedCallback } from 'use-debounce';
import { useDebouncedCallback, useThrottledCallback } from 'use-debounce';
import { useSnapshot } from 'valtio';
import supportedLanguages from '../data/status-supported-languages';
@ -104,6 +104,54 @@ function countableText(inputText) {
.replace(usernameRegex, '$1@$3');
}
// https://github.com/mastodon/mastodon/blob/c03bd2a238741a012aa4b98dc4902d6cf948ab63/app/models/account.rb#L69
const USERNAME_RE = /[a-z0-9_]+([a-z0-9_.-]+[a-z0-9_]+)?/i;
const MENTION_RE = new RegExp(
`(?<![=\\/\\w])@((${USERNAME_RE.source})(?:@[\\w.-]+[\\w]+)?)`,
'ig',
);
// AI-generated, all other regexes are too complicated
const HASHTAG_RE = new RegExp(
`(?<![=\\/\\w])#([a-z0-9_]+([a-z0-9_.-]+[a-z0-9_]+)?)(?![\\/\\w])`,
'ig',
);
// https://github.com/mastodon/mastodon/blob/23e32a4b3031d1da8b911e0145d61b4dd47c4f96/app/models/custom_emoji.rb#L31
const SHORTCODE_RE_FRAGMENT = '[a-zA-Z0-9_]{2,}';
const SCAN_RE = new RegExp(
`(?<=[^A-Za-z0-9_:\\n]|^):(${SHORTCODE_RE_FRAGMENT}):(?=[^A-Za-z0-9_:]|$)`,
'g',
);
function highlightText(text, { maxCharacters = Infinity }) {
// Accept text string, return formatted HTML string
let html = text;
// Exceeded characters limit
const { composerCharacterCount } = states;
let leftoverHTML = '';
if (composerCharacterCount > maxCharacters) {
const leftoverCount = composerCharacterCount - maxCharacters;
// Highlight exceeded characters
leftoverHTML =
'<mark class="compose-highlight-exceeded">' +
html.slice(-leftoverCount) +
'</mark>';
html = html.slice(0, -leftoverCount);
}
html = html
.replace(urlRegexObj, '$2<mark class="compose-highlight-url">$3</mark>') // URLs
.replace(MENTION_RE, '<mark class="compose-highlight-mention">$&</mark>') // Mentions
.replace(HASHTAG_RE, '<mark class="compose-highlight-hashtag">#$1</mark>') // Hashtags
.replace(
SCAN_RE,
'<mark class="compose-highlight-emoji-shortcode">$&</mark>',
); // Emoji shortcodes
return html + leftoverHTML;
}
function Compose({
onClose,
replyToStatus,
@ -565,6 +613,7 @@ function Compose({
account={currentAccountInfo}
accountInstance={currentAccount.instanceURL}
hideDisplayName
useAvatarStatic
/>
)}
{!standalone ? (
@ -1221,7 +1270,8 @@ function autoResizeTextarea(textarea) {
// NOTE: This check is needed because the offsetHeight return 50000 (really large number) on first render
// No idea why it does that, will re-investigate in far future
const offset = offsetHeight - clientHeight;
textarea.style.height = value ? scrollHeight + offset + 'px' : null;
const height = value ? scrollHeight + offset + 'px' : null;
textarea.style.height = height;
}
}
@ -1387,6 +1437,11 @@ const Textarea = forwardRef((props, ref) => {
handleCommited = (e) => {
const { input } = e.detail;
setText(input.value);
// fire input event
if (ref.current) {
const event = new Event('input', { bubbles: true });
ref.current.dispatchEvent(event);
}
};
textExpanderRef.current.addEventListener(
@ -1413,8 +1468,33 @@ const Textarea = forwardRef((props, ref) => {
};
}, []);
useEffect(() => {
// Resize observer for textarea
const textarea = ref.current;
if (!textarea) return;
const resizeObserver = new ResizeObserver(() => {
// Get height of textarea, set height to textExpander
const { height } = textarea.getBoundingClientRect();
textExpanderRef.current.style.height = height + 'px';
});
resizeObserver.observe(textarea);
}, []);
const composeHighlightRef = useRef();
const throttleHighlightText = useThrottledCallback((text) => {
composeHighlightRef.current.innerHTML =
highlightText(text, {
maxCharacters,
}) + '\n';
// Newline to prevent multiple line breaks at the end from being collapsed, no idea why
}, 500);
return (
<text-expander ref={textExpanderRef} keys="@ # :">
<text-expander
ref={textExpanderRef}
keys="@ # :"
class="compose-field-container"
>
<textarea
class="compose-field"
autoCapitalize="sentences"
@ -1466,15 +1546,26 @@ const Textarea = forwardRef((props, ref) => {
}}
onInput={(e) => {
const { target } = e;
setText(target.value);
const text = target.value;
setText(text);
autoResizeTextarea(target);
props.onInput?.(e);
throttleHighlightText(text);
}}
style={{
width: '100%',
height: '4em',
// '--text-weight': (1 + charCount / 140).toFixed(1) || 1,
}}
onScroll={(e) => {
const { scrollTop } = e.target;
composeHighlightRef.current.scrollTop = scrollTop;
}}
/>
<div
ref={composeHighlightRef}
class="compose-highlight"
aria-hidden="true"
/>
</text-expander>
);

View file

@ -1,6 +1,12 @@
import { Menu } from '@szhsin/react-menu';
import { getBlurHashAverageColor } from 'fast-blurhash';
import { useEffect, useLayoutEffect, useRef, useState } from 'preact/hooks';
import {
useEffect,
useLayoutEffect,
useMemo,
useRef,
useState,
} from 'preact/hooks';
import { useHotkeys } from 'react-hotkeys-hook';
import { oklab2rgb, rgb2oklab } from '../utils/color-utils';
@ -102,6 +108,41 @@ function MediaModal({
return () => clearTimeout(timer);
}, []);
const mediaAccentColors = useMemo(() => {
return mediaAttachments?.map((media) => {
const { blurhash } = media;
if (blurhash) {
const averageColor = getBlurHashAverageColor(blurhash);
const labAverageColor = rgb2oklab(averageColor);
return oklab2rgb([0.6, labAverageColor[1], labAverageColor[2]]);
}
return null;
});
}, [mediaAttachments]);
const mediaAccentGradient = useMemo(() => {
const gap = 5;
const range = 100 / mediaAccentColors.length;
return (
mediaAccentColors
?.map((color, i) => {
const start = i * range + gap;
const end = (i + 1) * range - gap;
if (color) {
return `
rgba(${color?.join(',')}, 0.4) ${start}%,
rgba(${color?.join(',')}, 0.4) ${end}%
`;
}
return `
transparent ${start}%,
transparent ${end}%
`;
})
?.join(', ') || 'transparent'
);
}, [mediaAccentColors]);
return (
<div
class={`media-modal-container media-modal-count-${mediaAttachments?.length}`}
@ -120,26 +161,32 @@ function MediaModal({
onClose();
}
}}
style={
mediaAttachments.length > 1
? {
backgroundAttachment: 'local',
backgroundImage: `linear-gradient(
to right, ${mediaAccentGradient})`,
}
: {}
}
>
{mediaAttachments?.map((media, i) => {
const { blurhash } = media;
let accentColor;
if (blurhash) {
const averageColor = getBlurHashAverageColor(blurhash);
const labAverageColor = rgb2oklab(averageColor);
accentColor = oklab2rgb([
0.6,
labAverageColor[1],
labAverageColor[2],
]);
}
const accentColor =
mediaAttachments.length === 1 ? mediaAccentColors[i] : null;
return (
<div
class="carousel-item"
style={{
style={
accentColor
? {
'--accent-color': `rgb(${accentColor?.join(',')})`,
'--accent-alpha-color': `rgba(${accentColor?.join(',')}, 0.4)`,
}}
'--accent-alpha-color': `rgba(${accentColor?.join(
',',
)}, 0.4)`,
}
: {}
}
tabindex="0"
key={media.id}
ref={i === currentIndex ? carouselFocusItem : null}

View file

@ -65,12 +65,28 @@ function Timeline({
try {
let { done, value } = await fetchItems(firstLoad);
if (Array.isArray(value)) {
// Avoid grouping for pinned posts
const [pinnedPosts, otherPosts] = value.reduce(
(acc, item) => {
if (item._pinned) {
acc[0].push(item);
} else {
acc[1].push(item);
}
return acc;
},
[[], []],
);
value = otherPosts;
if (allowGrouping) {
if (boostsCarousel) {
value = groupBoosts(value);
}
value = groupContext(value);
}
if (pinnedPosts.length) {
value = pinnedPosts.concat(value);
}
console.log(value);
if (firstLoad) {
setItems(value);
@ -282,7 +298,9 @@ function Timeline({
// checkForUpdates interval
useInterval(
loadOrCheckUpdates,
visible && !showNew ? checkForUpdatesInterval : null,
visible && !showNew
? checkForUpdatesInterval * (nearReachStart ? 1 : 2)
: null,
);
const hiddenUI = scrollDirection === 'end' && !nearReachStart;

View file

@ -18,7 +18,13 @@
--purple-color: blueviolet;
--green-color: darkgreen;
--orange-color: darkorange;
--orange-light-bg-color: color-mix(
in srgb,
var(--orange-color) 20%,
transparent
);
--red-color: orangered;
--red-bg-color: color-mix(in lch, var(--red-color) 40%, transparent);
--bg-color: #fff;
--bg-faded-color: #f0f2f5;
--bg-blur-color: #fff9;

View file

@ -33,14 +33,6 @@
max-height: 800px;
display: flex;
flex-direction: column;
a {
color: inherit;
&:hover {
color: var(--link-text-color);
}
}
}
h1 {