Commit eec8325
Changed files (20)
.vscode
src
components
layouts
pages
.vscode/settings.json
@@ -3,7 +3,4 @@
"editor.tabSize": 2,
"editor.formatOnSave": true,
"editor.defaultFormatter": "esbenp.prettier-vscode",
- "[astro]": {
- "editor.defaultFormatter": "astro-build.astro-vscode"
- },
}
\ No newline at end of file
src/components/widgets/Button.astro
@@ -0,0 +1,42 @@
+---
+import { AstroParameterConflictError } from '@/types/Errors';
+import type { HTMLAttributes } from 'astro/types';
+
+interface Props extends HTMLAttributes<'button'> {
+ href?: string;
+}
+const { href, onclick, class: className, ...rest } = Astro.props;
+
+if (href && onclick) throw new AstroParameterConflictError('href', 'onclick');
+---
+
+<button {...{ href, onclick, ...rest }} class:list={[className]}>
+ <slot />
+</button>
+
+<style lang="sass">
+ button
+ display: flex
+ align-items: center
+ padding: 0.5rem
+ margin: 0.5rem
+ text-align: center
+ word-break: break-word
+ transition-duration: 300ms
+ border-radius: 9999px
+ width: fit-content
+ height: fit-content
+
+ &:hover
+ @apply bg-[var(--theme-color-light)] dark:bg-[var(--theme-color-dark)]
+</style>
+
+<script>
+ import { navigate } from 'astro:transitions/client';
+ document.querySelectorAll('button[href]').forEach((btn) => {
+ btn.addEventListener('click', () => {
+ let href = btn.getAttribute('href') || '/404.html';
+ navigate(href);
+ });
+ });
+</script>
src/components/widgets/DarkModeButton.astro
@@ -0,0 +1,91 @@
+---
+import type { HTMLAttributes } from 'astro/types';
+import Button from './Button.astro';
+import { Icon } from 'astro-icon/components';
+import I18nKey from '@i18n/i18nKey';
+import { i18n } from '@i18n/translation';
+
+interface Props extends Omit<HTMLAttributes<'button'>, 'onclick'> {
+ showText?: boolean;
+}
+
+const { class: className, showText, ...rest } = Astro.props;
+---
+
+<Button
+ class:list={[
+ 'darkmode-btn',
+ 'border-2 border-neutral-200 dark:border-neutral-800',
+ className,
+ ]}
+ {...rest}
+>
+ <Icon class="darkmode-icon-light" name="material-symbols:light-mode-rounded" />
+ <Icon class="darkmode-icon-dark" name="material-symbols:dark-mode-rounded" />
+ <Icon class="darkmode-icon-auto" name="material-symbols:night-sight-auto-rounded" />
+ <span class="px-2 ml-auto mr-2">
+ {
+ showText &&
+ <span class="darkmode-text-light">{i18n(I18nKey.lightMode)}</span>
+ <span class="darkmode-text-dark">{i18n(I18nKey.darkMode)}</span>
+ <span class="darkmode-text-auto">{i18n(I18nKey.systemMode)}</span>
+ }
+ </span>
+</Button>
+
+<script>
+ const darkmodeBtns = document.querySelectorAll('button.darkmode-btn');
+
+ function refreshButtons() {
+ darkmodeBtns.forEach((btn) => {
+ const iconLight = btn.querySelector('.darkmode-icon-light');
+ const iconDark = btn.querySelector('.darkmode-icon-dark');
+ const iconAuto = btn.querySelector('.darkmode-icon-auto');
+ const textLight = btn.querySelector('.darkmode-text-light');
+ const textDark = btn.querySelector('.darkmode-text-dark');
+ const textAuto = btn.querySelector('.darkmode-text-auto');
+
+ if ('darkMode' in localStorage && localStorage.darkMode === 'true') {
+ iconLight?.classList.add('hidden');
+ iconDark?.classList.remove('hidden');
+ iconAuto?.classList.add('hidden');
+ textLight?.classList.add('hidden');
+ textDark?.classList.remove('hidden');
+ textAuto?.classList.add('hidden');
+ } else if ('darkMode' in localStorage && localStorage.darkMode === 'false') {
+ iconLight?.classList.remove('hidden');
+ iconDark?.classList.add('hidden');
+ iconAuto?.classList.add('hidden');
+ textLight?.classList.remove('hidden');
+ textDark?.classList.add('hidden');
+ textAuto?.classList.add('hidden');
+ } else {
+ iconLight?.classList.add('hidden');
+ iconDark?.classList.add('hidden');
+ iconAuto?.classList.remove('hidden');
+ textLight?.classList.add('hidden');
+ textDark?.classList.add('hidden');
+ textAuto?.classList.remove('hidden');
+ }
+ });
+ }
+
+ darkmodeBtns.forEach((btn) => {
+ btn.addEventListener('click', () => {
+ if (!('darkMode' in localStorage)) {
+ localStorage.darkMode = false;
+ document.documentElement.classList.remove('dark');
+ } else if (localStorage.darkMode === 'false') {
+ localStorage.darkMode = true;
+ document.documentElement.classList.add('dark');
+ } else {
+ localStorage.removeItem('darkMode');
+ if (window.matchMedia('(prefers-color-scheme: dark)').matches)
+ document.documentElement.classList.add('dark');
+ else document.documentElement.classList.remove('dark');
+ }
+ refreshButtons();
+ });
+ });
+ refreshButtons();
+</script>
src/components/widgets/IconButton.astro
@@ -1,38 +0,0 @@
----
-import { AstroParameterConflictError } from '@/types/Errors';
-
-interface Props {
- id: string;
- description?: string;
- href?: string;
- onclick?: string;
-}
-let { id, description, href, onclick } = Astro.props;
-
-if (!description) description = '';
-
-if (href && onclick) throw new AstroParameterConflictError('href', 'onclick');
-if (href) onclick = `window.location.assign(\`${href}\`)`;
-else if (!onclick) onclick = '';
----
-
-<button id={id} onclick={onclick} title={description} class="widget-icon-button">
- <slot />
-</button>
-
-<style lang="stylus">
-.widget-icon-button
- display flex
- align-items center
- padding 0.5rem
- margin 0.5rem
- text-align center
- word-break break-word
- transition-duration 300ms
- border-radius 9999px
- width fit-content
- height fit-content
-
- &:hover
- background-color #3b82f6
-</style>
src/components/widgets/TextButton.astro
@@ -1,35 +0,0 @@
----
-import { AstroParameterConflictError } from '@/types/Errors';
-
-interface Props {
- id: string;
- href?: string;
- onclick?: string;
-}
-let { id, href, onclick } = Astro.props;
-
-if (href && onclick) throw new AstroParameterConflictError('href', 'onclick');
-if (href) onclick = `window.location.assign(\`${href}\`)`;
-else onclick = '';
----
-
-<button id={id} onclick={onclick} class="widget-text-button">
- <slot />
-</button>
-
-<style lang="stylus">
-.widget-text-button
- display flex
- align-items center
- padding 0.5rem 0.75rem
- margin 0.5rem
- text-align center
- word-break break-word
- transition-duration 300ms
- border-radius 9999px
- width fit-content
- height fit-content
-
- &:hover
- background-color #3b82f6
-</style>
src/components/CatagoryBar.astro
@@ -0,0 +1,5 @@
+---
+
+---
+
+<div id="catagory-bar" class="flex w-full rounded-md px-2 py-3"></div>
src/components/PostCards.astro
@@ -0,0 +1,1 @@
+<div id="post-cards"></div>
src/components/Sidebar.astro
@@ -1,33 +1,52 @@
---
-import { navbarConfig } from '@/config';
+import { navbarConfig, themeConfig } from '@/config';
import { Icon } from 'astro-icon/components';
-import TextButton from './widgets/TextButton.astro';
-import IconButton from './widgets/IconButton.astro';
+import Button from './widgets/Button.astro';
+import DarkModeButton from './widgets/DarkModeButton.astro';
---
-<div id="sidebar">
- <div id="sidebar-mask" class="fixed z-40 hidden w-full h-full bg-black/20 backdrop-blur-md backdrop-saturate-100">
+<div id="sidebar" transition:persist>
+ <div
+ id="sidebar-mask"
+ class="fixed z-40 hidden w-full h-full bg-black/10 backdrop-blur-md backdrop-saturate-100"
+ >
</div>
<div
id="sidebar-menu"
- class="md:hidden fixed z-50 -right-1/3 w-1/3 h-full duration-500 border-l-2 border-gray-200 bg-white ease-in-out"
+ class="fixed md:hidden h-full z-50 -right-1/3 w-1/3 duration-500 ease-in-out border-l-2 border-l-neutral-200 dark:border-l-neutral-800 bg-neutral-50 dark:bg-neutral-950"
>
- {
- navbarConfig.navbarCenterItems.map((item) => (
- <TextButton id="nav-menu-item" href={item.href} onclick={item.onclick}>
- <span class="text-xl">{item.text}</span>
- </TextButton>
- ))
- }
- {
- navbarConfig.navbarRightItems.onlyWide.map((item) => (
- <IconButton id="nav-menu-item" href={item.href} onclick={item.onclick} description={item.text}>
- <Icon name={item.icon} class="text-2xl" />
- </IconButton>
- ))
- }
+ <div id="sidebar-site-data"></div>
+ <DarkModeButton showText={true} class="text-lg !w-[calc(100%-1rem)]" />
+ <div id="sidebar-menu-text-items">
+ {
+ navbarConfig.navbarCenterItems.map((item) => (
+ <Button
+ id="sidebar-menu-item"
+ href={item.href}
+ onclick={item.onclick}
+ title={item.text}
+ >
+ <span class="text-xl">{item.text}</span>
+ </Button>
+ ))
+ }
+ </div>
+ <div id="sidebar-menu-icon-items" class="flex">
+ {
+ navbarConfig.navbarRightItems.onlyWide.map((item) => (
+ <Button
+ id="sidebar-menu-item"
+ href={item.href}
+ onclick={item.onclick}
+ title={item.text}
+ >
+ <Icon name={item.icon} class="text-2xl" />
+ </Button>
+ ))
+ }
+ </div>
</div>
</div>
src/layouts/GlobalLayout.astro
@@ -1,5 +1,6 @@
---
-import { profileConfig, siteConfig } from '@/config';
+import { profileConfig, siteConfig, themeConfig } from '@/config';
+import { ClientRouter } from 'astro:transitions';
interface Props {
title?: string;
@@ -25,6 +26,8 @@ const siteLang = lang.replace('_', '-');
<meta charset="UTF-8" />
<meta name="author" content={profileConfig.name} />
+ <ClientRouter />
+
<!-- Open Graph / Facebook -->
<meta property="og:site_name" content={siteConfig.title} />
<meta property="og:url" content={Astro.url} />
@@ -42,7 +45,32 @@ const siteLang = lang.replace('_', '-');
<slot name="head" />
</head>
- <body>
+ <body class="bg-neutral-50 dark:bg-neutral-950 text-neutral-950 dark:text-neutral-50">
<slot />
</body>
</html>
+
+<script>
+ // 深色模式切换
+ function applyDarkMode() {
+ if (
+ localStorage.darkMode === 'true' ||
+ (!('darkMode' in localStorage) &&
+ window.matchMedia('(prefers-color-scheme: dark)').matches)
+ ) {
+ document.documentElement.classList.add('dark');
+ } else document.documentElement.classList.remove('dark');
+ }
+ document.addEventListener('astro:after-swap', applyDarkMode);
+ applyDarkMode();
+</script>
+
+<style
+ is:global
+ define:vars={{
+ 'theme-color-light': themeConfig.themeColorLight,
+ 'theme-color-dark': themeConfig.themeColorDark,
+ 'theme-color-sub-light': themeConfig.themeColorSubLight,
+ 'theme-color-sub-dark': themeConfig.themeColorSubDark,
+ }}
+></style>
src/layouts/MainLayout.astro
@@ -16,7 +16,7 @@ const { title, description, lang } = Astro.props;
<slot slot="head" name="head" />
<Sidebar />
<div id="body-wrap">
- <Navbar title={title} />
+ <Navbar />
<!-- Main content -->
<div id="content-wrapper">
<slot />
src/pages/about.astro
@@ -0,0 +1,10 @@
+---
+import MainLayout from '@layouts/MainLayout.astro';
+
+import I18nKey from '@i18n/i18nKey';
+import { i18n } from '@i18n/translation';
+---
+
+<MainLayout title={i18n(I18nKey.about)}>
+ <div></div>
+</MainLayout>
src/pages/index.astro
@@ -1,7 +1,12 @@
---
+import PostCards from '@components/PostCards.astro';
+import CatagoryBar from '@components/widgets/CatagoryBar.astro';
import MainLayout from '@layouts/MainLayout.astro';
---
<MainLayout>
- <h1>Welcome to Astro</h1>
+ <div id="main-content">
+ <CatagoryBar />
+ <PostCards />
+ </div>
</MainLayout>
src/types/Color.ts
@@ -0,0 +1,5 @@
+export type HexColor = `#${string}`;
+export type RGBColor = `rgb(${number}, ${number}, ${number})`;
+export type RGBAColor = `rgba(${number}, ${number}, ${number}, ${number})`;
+
+export type Color = HexColor | RGBColor | RGBAColor;
src/types/config.ts
@@ -1,17 +1,11 @@
+import type { Color } from './Color';
+
export type SiteConfig = {
title: string;
lang: string;
favicon: (string | { src: string; theme?: 'light' | 'dark' })[];
};
-export type NavbarConfig = {
- navbarCenterItems: { text: string; href?: string; onclick?: string }[];
- navbarRightItems: {
- onlyWide: { icon: string; text?: string; href?: string; onclick?: string }[];
- always: { icon: string; text?: string; href?: string; onclick?: string }[];
- };
-};
-
export type ProfileConfig = {
avatar?: string;
name: string;
@@ -23,6 +17,26 @@ export type ProfileConfig = {
}[];
};
+export type NavbarConfig = {
+ navbarCenterItems: { text: string; href?: string; onclick?: string }[];
+ navbarRightItems: {
+ onlyWide: {
+ icon: string;
+ text?: string;
+ href?: string;
+ onclick?: string;
+ }[];
+ always: { icon: string; text?: string; href?: string; onclick?: string }[];
+ };
+};
+
+export type ThemeConfig = {
+ themeColorLight: Color;
+ themeColorSubLight: Color;
+ themeColorDark: Color;
+ themeColorSubDark: Color;
+};
+
export type LicenseConfig = {
enable: boolean;
name: string;
src/config.ts
@@ -1,4 +1,10 @@
-import type { LicenseConfig, ProfileConfig, SiteConfig, NavbarConfig } from './types/config';
+import type {
+ LicenseConfig,
+ ProfileConfig,
+ SiteConfig,
+ NavbarConfig,
+ ThemeConfig,
+} from './types/config';
import I18nKey from '@i18n/i18nKey';
import { i18n } from '@i18n/translation';
@@ -27,15 +33,34 @@ export const navbarConfig: NavbarConfig = {
onlyWide: [
// Items displayed only when the width is greater than 768px.
// 仅在宽度大于 768px 时显示的项目
- { icon: 'material-symbols:rss-feed-rounded', text: i18n(I18nKey.subscribe), onclick: '' },
+ {
+ icon: 'material-symbols:rss-feed-rounded',
+ text: i18n(I18nKey.subscribe),
+ onclick: '',
+ },
],
always: [
- { icon: 'material-symbols:casino', text: i18n(I18nKey.randomPost), onclick: '' },
- { icon: 'material-symbols:search-rounded', text: i18n(I18nKey.search), onclick: '' },
+ {
+ icon: 'material-symbols:casino',
+ text: i18n(I18nKey.randomPost),
+ onclick: '',
+ },
+ {
+ icon: 'material-symbols:search-rounded',
+ text: i18n(I18nKey.search),
+ onclick: '',
+ },
],
},
};
+export const themeConfig: ThemeConfig = {
+ themeColorLight: '#D75B3D',
+ themeColorSubLight: '#e5d5d5',
+ themeColorDark: '#397D9F',
+ themeColorSubDark: '#033D60',
+};
+
export const licenseConfig: LicenseConfig = {
enable: true,
name: 'CC BY-NC-SA 4.0',
.prettierrc.cjs
@@ -1,13 +1,13 @@
/** @type {import('prettier').Config} */
module.exports = {
- printWidth: 120,
+ printWidth: 96,
semi: true,
singleQuote: true,
tabWidth: 2,
trailingComma: 'es5',
useTabs: false,
- plugins: [require.resolve('prettier-plugin-astro')],
+ plugins: ['prettier-plugin-astro'],
overrides: [{ files: '*.astro', options: { parser: 'astro' } }],
};
astro.config.mjs
@@ -1,5 +1,4 @@
// @ts-check
-// @ts-check
import { defineConfig } from 'astro/config';
import tailwind from '@astrojs/tailwind';
@@ -10,5 +9,5 @@ export default defineConfig({
site: 'https://astral-halo.netilify.app/',
base: '/',
trailingSlash: 'always',
- integrations: [tailwind(), icon()],
+ integrations: [tailwind({ nesting: true }), icon()],
});
package.json
@@ -14,7 +14,7 @@
"astro": "^5.1.5",
"astro-compress": "2.3.5",
"astro-icon": "^1.1.5",
- "stylus": "^0.64.0",
+ "sass": "^1.83.4",
"tailwindcss": "^3.4.17",
"typescript": "^5.7.3"
},
tailwind.config.mjs
@@ -8,5 +8,6 @@ export default {
},
},
},
+ darkMode: 'selector',
plugins: [],
};