Commit 212d918

HPCesia <me@hpcesia.com>
2025-01-18 08:05:01
refactor: darkmode
use css var in darkmode. setup stylelint and eslint.
1 parent eec8325
.vscode/extensions.json
@@ -3,5 +3,4 @@
     "astro-build.astro-vscode",
     "bradlc.vscode-tailwindcss"
   ],
-  "unwantedRecommendations": []
 }
\ No newline at end of file
.vscode/settings.json
@@ -3,4 +3,25 @@
   "editor.tabSize": 2,
   "editor.formatOnSave": true,
   "editor.defaultFormatter": "esbenp.prettier-vscode",
+  "css.validate": false,
+  "scss.validate": false,
+  "less.validate": false,
+  "stylelint.validate": [
+    "css",
+    "postcss",
+    "scss",
+    "astro",
+  ],
+  "eslint.validate": [
+    "javascript",
+    "javascriptreact",
+    "astro",
+    "typescript",
+    "typescriptreact"
+  ],
+  "eslint.useFlatConfig": true,
+  "editor.codeActionsOnSave": {
+    "source.fixAll.eslint": "always",
+    "source.fixAll.stylelint": "always"
+  }
 }
\ No newline at end of file
src/components/widgets/AuthorInfoCard.astro
src/components/widgets/Button.astro
@@ -14,28 +14,30 @@ if (href && onclick) throw new AstroParameterConflictError('href', 'onclick');
   <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
+<style lang="scss">
+  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)]
+    &: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';
+      const href = btn.getAttribute('href') || '/404.html';
       navigate(href);
     });
   });
src/components/widgets/DarkModeButton.astro
@@ -13,24 +13,16 @@ const { class: className, showText, ...rest } = Astro.props;
 ---
 
 <Button
-  class:list={[
-    'darkmode-btn',
-    'border-2 border-neutral-200 dark:border-neutral-800',
-    className,
-  ]}
+  class:list={['darkmode-btn', className]}
   {...rest}
+  data-text-light={i18n(I18nKey.lightMode)}
+  data-text-dark={i18n(I18nKey.darkMode)}
+  data-text-auto={i18n(I18nKey.systemMode)}
 >
   <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>
+  {showText && <span class="darkmode-text px-2 ml-auto mr-2" />}
 </Button>
 
 <script>
@@ -41,31 +33,26 @@ const { class: className, showText, ...rest } = Astro.props;
       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');
+      const text = btn.querySelector('.darkmode-text');
 
       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');
+        if (text) text.textContent = btn.getAttribute('data-text-dark');
+        btn.setAttribute('title', btn.getAttribute('data-text-dark') || '');
       } 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');
+        if (text) text.textContent = btn.getAttribute('data-text-light');
+        btn.setAttribute('title', btn.getAttribute('data-text-light') || '');
       } 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');
+        if (text) text.textContent = btn.getAttribute('data-text-auto');
+        btn.setAttribute('title', btn.getAttribute('data-text-auto') || '');
       }
     });
   }
src/components/AsideContent.astro
@@ -0,0 +1,7 @@
+---
+
+---
+
+<div id="aside-content">
+  {}
+</div>
src/components/misc/GlobalBackground.astro → src/components/GlobalBackground.astro
File renamed without changes
src/components/Navbar.astro
@@ -1,5 +1,5 @@
 ---
-import { navbarConfig, themeConfig } from '@/config';
+import { navbarConfig } from '@/config';
 
 import { Icon } from 'astro-icon/components';
 
@@ -16,7 +16,7 @@ if (!title) title = 'Astral Halo';
 <div
   id="navbar"
   transition:persist
-  class="flex w-full h-[60px] items-center border-b-2 border-b-neutral-200 dark:border-b-neutral-800 bg-neutral-50 dark:bg-neutral-950"
+  class="flex w-full h-[60px] items-center border-b-2 theme-border theme-card-bg"
 >
   <div id="nav-left" class="left-0 flex mr-auto w-fit">
     <Button id="site-name-wrapper" href="/">
@@ -42,12 +42,7 @@ if (!title) title = 'Astral Halo';
     <div class="flex max-md:hidden">
       {
         navbarConfig.navbarRightItems.onlyWide.map((item) => (
-          <Button
-            id="nav-menu-item"
-            href={item.href}
-            onclick={item.onclick}
-            title={item.text}
-          >
+          <Button id="nav-menu-item" href={item.href} onclick={item.onclick} title={item.text}>
             <Icon name={item.icon} class="text-2xl" />
           </Button>
         ))
@@ -56,12 +51,7 @@ if (!title) title = 'Astral Halo';
     <div class="flex">
       {
         navbarConfig.navbarRightItems.always.map((item) => (
-          <Button
-            id="nav-menu-item"
-            href={item.href}
-            onclick={item.onclick}
-            title={item.text}
-          >
+          <Button id="nav-menu-item" href={item.href} onclick={item.onclick} title={item.text}>
             <Icon name={item.icon} class="text-2xl" />
           </Button>
         ))
src/components/Sidebar.astro
@@ -1,5 +1,5 @@
 ---
-import { navbarConfig, themeConfig } from '@/config';
+import { navbarConfig } from '@/config';
 
 import { Icon } from 'astro-icon/components';
 
@@ -15,10 +15,13 @@ import DarkModeButton from './widgets/DarkModeButton.astro';
   </div>
   <div
     id="sidebar-menu"
-    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"
+    class="fixed md:hidden h-full z-50 -right-1/3 w-1/3 duration-500 ease-in-out border-l-2 theme-border theme-card-bg"
   >
     <div id="sidebar-site-data"></div>
-    <DarkModeButton showText={true} class="text-lg !w-[calc(100%-1rem)]" />
+    <DarkModeButton
+      showText={true}
+      class="text-lg !w-[calc(100%-1rem)] border-2 theme-border"
+    />
     <div id="sidebar-menu-text-items">
       {
         navbarConfig.navbarCenterItems.map((item) => (
src/components/SideToolBar.astro
@@ -1,8 +1,56 @@
 ---
-
+import { Icon } from 'astro-icon/components';
+import Button from './widgets/Button.astro';
+import DarkModeButton from './widgets/DarkModeButton.astro';
 ---
 
-<div id="side-menu" class="close">
-  <div id="sm-hide"></div>
-  <div id="sm-show"></div>
+<div id="side-toolbar" transition:persist class="fixed bottom-10 right-0 text-xl">
+  <div id="stb-trigger" class="fixed bottom-0 right-0 w-12 h-32 -z-50"></div>
+  <div id="stb-hide" class="translate-x-full duration-500 ease-in-out">
+    <DarkModeButton id="stb-dark-mode" class="" />
+  </div>
+  <div id="stb-show" class="translate-x-full duration-500 ease-in-out">
+    <Button id="stb-show-more">
+      <Icon name="material-symbols:settings-rounded" class="animate-spin" />
+    </Button>
+    <Button id="stb-back-to-top">
+      <Icon name="material-symbols:arrow-upward-rounded" />
+    </Button>
+  </div>
 </div>
+
+<script>
+  const stbHide = document.getElementById('stb-hide');
+  const stbShow = document.getElementById('stb-show');
+  const stbShowMore = document.getElementById('stb-show-more');
+  const stbBackToTop = document.getElementById('stb-back-to-top');
+  const stbTrigger = document.getElementById('stb-trigger');
+
+  stbTrigger?.addEventListener('click', () => {
+    stbShow?.classList.toggle('translate-x-full');
+  });
+
+  stbShowMore?.addEventListener('click', () => {
+    stbHide?.classList.toggle('translate-x-full');
+  });
+
+  window.addEventListener('scroll', () => {
+    if (window.scrollY > 0) stbShow?.classList.remove('translate-x-full');
+    else {
+      stbShow?.classList.add('translate-x-full');
+      stbHide?.classList.add('translate-x-full');
+    }
+  });
+</script>
+
+<style lang="scss">
+  #side-toolbar {
+    button {
+      @apply bg-[var(--theme-color-light)] dark:bg-[var(--theme-color-dark)];
+
+      &:hover {
+        @apply bg-[var(--theme-color-light-lighten)] dark:bg-[var(--theme-color-dark-lighten)];
+      }
+    }
+  }
+</style>
src/layouts/GlobalLayout.astro
@@ -1,5 +1,5 @@
 ---
-import { profileConfig, siteConfig, themeConfig } from '@/config';
+import { profileConfig, siteConfig } from '@/config';
 import { ClientRouter } from 'astro:transitions';
 
 interface Props {
@@ -45,7 +45,7 @@ const siteLang = lang.replace('_', '-');
 
     <slot name="head" />
   </head>
-  <body class="bg-neutral-50 dark:bg-neutral-950 text-neutral-950 dark:text-neutral-50">
+  <body class="theme-bg theme-text">
     <slot />
   </body>
 </html>
@@ -65,12 +65,6 @@ const siteLang = lang.replace('_', '-');
   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>
+<style is:global lang="scss">
+  @use '../styles/globals';
+</style>
src/layouts/MainLayout.astro
@@ -15,6 +15,7 @@ const { title, description, lang } = Astro.props;
 <GlobalLayout title={title} description={description} lang={lang}>
   <slot slot="head" name="head" />
   <Sidebar />
+  <SideToolBar />
   <div id="body-wrap">
     <Navbar />
     <!-- Main content -->
src/styles/globals.scss
@@ -0,0 +1,22 @@
+@use './variables';
+@tailwind base;
+@tailwind components;
+@tailwind utilities;
+
+@layer components {
+  .theme-bg {
+    @apply bg-[var(--theme-bg-color-light)] dark:bg-[var(--theme-bg-color-dark)];
+  }
+
+  .theme-card-bg {
+    @apply bg-[var(--theme-card-bg-color-light)] dark:bg-[var(--theme-card-bg-color-dark)];
+  }
+
+  .theme-text {
+    @apply text-[var(--theme-text-color-light)] dark:text-[var(--theme-text-color-dark)];
+  }
+
+  .theme-border {
+    @apply border-[var(--theme-border-color-light)] dark:border-[var(--theme-border-color-dark)];
+  }
+}
src/styles/variables.scss
@@ -0,0 +1,31 @@
+@use 'sass:color';
+
+$theme-color-light: #d75b3d;
+$theme-color-dark: #397d9f;
+$theme-bg-color-light: theme('colors.neutral.50');
+$theme-bg-color-dark: theme('colors.neutral.900');
+$theme-card-bg-color-light: $theme-bg-color-light;
+$theme-card-bg-color-dark: $theme-bg-color-dark;
+$theme-text-color-light: theme('colors.neutral.900');
+$theme-text-color-dark: theme('colors.neutral.100');
+$theme-border-color-light: theme('colors.neutral.200');
+$theme-border-color-dark: theme('colors.neutral.800');
+
+:root {
+  --theme-color-light: #{$theme-color-light};
+  --theme-color-dark: #{$theme-color-dark};
+  --theme-color-light-lighten: #{color.adjust($theme-color-light, $lightness: 20%)};
+  --theme-color-dark-lighten: #{color.adjust($theme-color-dark, $lightness: 20%)};
+  --theme-color-light-darken: #{color.adjust($theme-color-light, $lightness: -20%)};
+  --theme-color-dark-darken: #{color.adjust($theme-color-dark, $lightness: -20%)};
+  --theme-color-light-transparent: #{color.adjust($theme-color-light, $alpha: -0.5)};
+  --theme-color-dark-transparent: #{color.adjust($theme-color-dark, $alpha: -0.5)};
+  --theme-bg-color-light: #{$theme-bg-color-light};
+  --theme-bg-color-dark: #{$theme-bg-color-dark};
+  --theme-card-bg-color-light: #{$theme-card-bg-color-light};
+  --theme-card-bg-color-dark: #{$theme-card-bg-color-dark};
+  --theme-text-color-light: #{$theme-text-color-light};
+  --theme-text-color-dark: #{$theme-text-color-dark};
+  --theme-border-color-light: #{$theme-border-color-light};
+  --theme-border-color-dark: #{$theme-border-color-dark};
+}
src/types/config.ts
@@ -30,11 +30,14 @@ export type NavbarConfig = {
   };
 };
 
-export type ThemeConfig = {
-  themeColorLight: Color;
-  themeColorSubLight: Color;
-  themeColorDark: Color;
-  themeColorSubDark: Color;
+export type ToolBarConfig = {
+  enable: boolean;
+  items: {
+    icon: string;
+    text?: string;
+    href?: string;
+    onclick?: string;
+  }[];
 };
 
 export type LicenseConfig = {
src/types/Errors.ts
@@ -2,7 +2,7 @@ import { AstroError } from 'astro/errors';
 
 export class AstroParameterConflictError extends AstroError {
   constructor(...params: string[]) {
-    let message = `The following parameters are in conflict: ${params.join(', ')}. Please ensure that only one of these parameters is provided.`;
+    const message = `The following parameters are in conflict: ${params.join(', ')}. Please ensure that only one of these parameters is provided.`;
     super(message);
     this.name = 'ParameterConflictError';
   }
src/config.ts
@@ -3,7 +3,7 @@ import type {
   ProfileConfig,
   SiteConfig,
   NavbarConfig,
-  ThemeConfig,
+  ToolBarConfig,
 } from './types/config';
 
 import I18nKey from '@i18n/i18nKey';
@@ -24,10 +24,10 @@ export const profileConfig: ProfileConfig = {
 
 export const navbarConfig: NavbarConfig = {
   navbarCenterItems: [
-    { text: i18n(I18nKey.archive), href: '/archive' },
-    { text: i18n(I18nKey.categories), href: '/categories' },
-    { text: i18n(I18nKey.tags), href: '/tags' },
-    { text: i18n(I18nKey.about), href: '/about' },
+    { text: i18n(I18nKey.archive), href: '/archive/' },
+    { text: i18n(I18nKey.categories), href: '/categories/' },
+    { text: i18n(I18nKey.tags), href: '/tags/' },
+    { text: i18n(I18nKey.about), href: '/about/' },
   ],
   navbarRightItems: {
     onlyWide: [
@@ -54,11 +54,9 @@ export const navbarConfig: NavbarConfig = {
   },
 };
 
-export const themeConfig: ThemeConfig = {
-  themeColorLight: '#D75B3D',
-  themeColorSubLight: '#e5d5d5',
-  themeColorDark: '#397D9F',
-  themeColorSubDark: '#033D60',
+export const toolBarConfig: ToolBarConfig = {
+  enable: true,
+  items: [],
 };
 
 export const licenseConfig: LicenseConfig = {
astro.config.mjs
@@ -8,6 +8,7 @@ import icon from 'astro-icon';
 export default defineConfig({
   site: 'https://astral-halo.netilify.app/',
   base: '/',
-  trailingSlash: 'always',
+  output: 'static',
+  trailingSlash: 'ignore',
   integrations: [tailwind({ nesting: true }), icon()],
 });
eslint.config.js
@@ -34,14 +34,11 @@ export default [
     },
   },
   {
-    // Define the configuration for `<script>` tag.
-    // Script in `<script>` is assigned a virtual file name with the `.js` extension.
     files: ['**/*.{ts,tsx}', '**/*.astro/*.js'],
     languageOptions: {
       parser: typescriptParser,
     },
     rules: {
-      // Note: you must disable the base rule as it can report incorrect errors
       'no-unused-vars': 'off',
       '@typescript-eslint/no-unused-vars': [
         'error',
package.json
@@ -11,9 +11,11 @@
   "dependencies": {
     "@astrojs/tailwind": "^5.1.4",
     "@iconify-json/material-symbols": "^1.2.12",
-    "astro": "^5.1.5",
+    "astro": "^5.1.7",
     "astro-compress": "2.3.5",
     "astro-icon": "^1.1.5",
+    "autoprefixer": "^10.4.20",
+    "postcss-load-config": "^6.0.1",
     "sass": "^1.83.4",
     "tailwindcss": "^3.4.17",
     "typescript": "^5.7.3"
@@ -22,10 +24,15 @@
     "@astrojs/check": "^0.9.4",
     "@astrojs/ts-plugin": "^1.10.4",
     "astro-eslint-parser": "^1.1.0",
-    "eslint": "^9.16.0",
+    "eslint": "^9.18.0",
     "eslint-plugin-astro": "^1.3.1",
+    "globals": "^15.14.0",
+    "postcss-html": "^1.8.0",
     "prettier": "^3.4.2",
     "prettier-plugin-astro": "^0.14.1",
-    "typescript-eslint": "^8.17.0"
+    "stylelint": "^16.13.2",
+    "stylelint-config-html": "^1.1.0",
+    "stylelint-config-standard-scss": "^14.0.0",
+    "typescript-eslint": "^8.20.0"
   }
 }
\ No newline at end of file
postcss.config.mjs
@@ -0,0 +1,12 @@
+import postcssImport from 'postcss-import';
+import postcssNesting from 'tailwindcss/nesting/index.js';
+import tailwindcss from 'tailwindcss';
+
+/** @type {import('postcss-load-config').Config} */
+export default {
+  plugins: {
+    'postcss-import': postcssImport, // to combine multiple css files
+    'tailwindcss/nesting': postcssNesting,
+    tailwindcss: tailwindcss,
+  },
+};
stylelint.config.mjs
@@ -0,0 +1,24 @@
+/** @type {import('stylelint').Config} */
+export default {
+  extends: ['stylelint-config-standard-scss', 'stylelint-config-html/astro'],
+  rules: {
+    'scss/at-rule-no-unknown': [
+      true,
+      {
+        ignoreAtRules: [
+          // Tailwind CSS
+          'tailwind',
+          'apply',
+          'layer',
+          'config',
+        ],
+      },
+    ],
+    'function-no-unknown': [
+      true,
+      {
+        ignoreFunctions: ['theme'],
+      },
+    ],
+  },
+};