Files
FixIt/assets/js/modules/misc.ts
T
Cell 8af8e806d7 feat(assets): integrate UnoCSS for utility classes and presetIcons (#795)
* refactor(assets): integrate UnoCSS for utility classes

- Add UnoCSS with presetWind3 as pre-built CSS (assets/css/unocss.css)
- Add uno.config.ts with theme breakpoints, colors, safelist, and blocklist
- Replace SCSS utility classes with UnoCSS atomic classes:
  - d-none → hidden
  - d-none-desktop → sm:hidden
  - d-none-mobile → max-sm:hidden
  - order-* (safelist)
  - variant-numeric → tabular-nums lining-nums
- Rename .blur to .is-blur to avoid UnoCSS conflict
- Add @unocss-skip comments for SVG path elements to prevent false positives
- Remove redundant _utilities.scss ($orders map) and simplify _common.scss
- Update documentation (CLAUDE.md, CONTRIBUTING.md, copilot-instructions.md)

* refactor(assets): replace 404/offline SCSS with UnoCSS atomic classes

- Replace _404.scss styles with atomic classes on template elements
- Replace _offline.scss styles with atomic classes on template elements
- Keep selectors (id/class) as user customization hooks
- Delete assets/scss/pages/_404.scss and _offline.scss

* refactor(assets): simplify SCSS with UnoCSS atomic classes

- Replace _bilibili.scss with atomic classes (relative, aspect-video, inset-0)
- Replace _version.scss with atomic classes (whitespace-nowrap, align-text-bottom)
- Replace _noscript-warning.scss with atomic classes (bg-danger, fixed, z-200)
- Add semantic z-index shortcuts (z-hide, z-auto, z-base, z-loading, z-sticky, z-fixed)
- Add h1-h6 to UnoCSS blocklist to prevent false positives
- Use Source path for UnoCSS CSS loading to enable Hugo minify

* chore(assets): minor SCSS and template cleanup

* fix(assets): add secondary color to UnoCSS theme config

text-secondary was broken because the secondary color mapping was missing.

* feat(assets): add UnoCSS presetIcons support and z-index shortcuts

- Add presetIcons preset with empty collections (ready for custom icon sets)
- Add @iconify/json and @iconify/utils devDependencies
- Add semantic z-index shortcuts (z-hide, z-auto, z-base, z-loading, z-sticky, z-fixed)
- Add secondary color to UnoCSS theme config

* refactor(assets): replace SVG icons with UnoCSS presetIcons

- Add Lucide and Octicons icon collections to presetIcons config
- Replace arrow-up/down and enter-key SVGs with Lucide icons
- Replace alert icons (info, light-bulb, report, alert, stop) with Octicons
- Delete replaced SVG files from assets/images/icons/
- Keep custom SVGs (csdn, plume, rootme)

* refactor(layouts): redesign 404 and offline pages with UnoCSS

- Replace translate-y-[30vh] with self-stretch flex centering below header
- Remove arbitrary values (text-[3.6rem], text-[1.8rem], etc.)
- Restore @page print styles in _common.scss
- Simplify offline page layout (icon + title + text)
- Fix hardcoded #57606a → text-secondary
2026-06-27 06:27:12 +08:00

134 lines
4.8 KiB
TypeScript

import type { CoreService, MiscService } from '../core/tokens'
import { eventBus } from '../core/event-bus'
import { getScrollTop, isMobile, isValidDate, scrollIntoView } from '../utils'
/**
* Miscellaneous module — site time, PWA, bookmarks, rewards, comments, and PostChat.
*
* Responsibilities:
* - Display site running time with animated counters.
* - Register service worker for PWA support.
* - Auto-bookmark scroll position for page restoration.
* - Initialize reward QR codes and PostChat AI user info.
* - Initialize comment section UI and scroll-into-view.
*/
export class MiscModule implements MiscService {
private siteTime: ReturnType<typeof setInterval> | undefined
constructor(private readonly core: CoreService) {}
/** Calculate and display the elapsed time since site launch. */
getSiteTime() {
const now = new Date()
const run = new Date(this.core.config.siteTime!)
const $runTimes = document.querySelector<HTMLElement>('.run-times')
if (!isValidDate(run) || !$runTimes) {
clearInterval(this.siteTime)
$runTimes && $runTimes.parentNode!.removeChild($runTimes)
return
}
const totalSeconds = Math.floor((now.getTime() - run.getTime()) / 1000)
const days = Math.floor(totalSeconds / 86400)
const hours = Math.floor((totalSeconds % 86400) / 3600)
const minutes = Math.floor((totalSeconds % 3600) / 60)
const seconds = totalSeconds % 60
$runTimes.innerHTML = `${days}, ${String(hours).padStart(2, '0')}:${String(minutes).padStart(2, '0')}:${String(seconds).padStart(2, '0')}`
document.querySelector('.site-time .hidden')?.classList.remove('hidden')
}
/** Start the site-time counter with visibility-change pausing. */
initSiteTime() {
if (this.core.config.siteTime) {
this.siteTime = setInterval(() => this.getSiteTime(), 500)
document.addEventListener('visibilitychange', () => {
if (document.hidden) {
return clearInterval(this.siteTime)
}
this.siteTime = setInterval(() => this.getSiteTime(), 500)
}, false)
}
}
/** Save and restore scroll position as an automatic bookmark. */
initAutoMark() {
if (!this.core.config.autoBookmark)
return
window.addEventListener('beforeunload', () => {
window.sessionStorage?.setItem(`fixit-bookmark/#${location.pathname}`, String(getScrollTop()))
})
const scrollTop = Number(window.sessionStorage?.getItem(`fixit-bookmark/#${location.pathname}`))
if (scrollTop && location.hash === '') {
window.scrollTo({ top: scrollTop, behavior: 'smooth' })
}
}
/** Initialize reward/donation button exclusive-toggle behaviour. */
initReward() {
const $rewards = document.querySelectorAll<HTMLElement>('.post-reward [data-mode="fixed"]')
if (!$rewards.length)
return
if (isMobile()) {
$rewards.forEach($reward => $reward.removeAttribute('data-mode'))
return
}
const _closeRewardExclude = (id?: string | null) => {
$rewards.forEach(($reward) => {
const $rewardInput = $reward.parentElement!.querySelector<HTMLInputElement>('.reward-input')
if ($rewardInput && $rewardInput.id !== id) {
$rewardInput.checked = false
}
})
}
$rewards.forEach(($reward) => {
$reward.previousElementSibling!.addEventListener('click', function (this: HTMLElement) {
_closeRewardExclude(this.getAttribute('for'))
}, false)
})
eventBus.on('fixit:scroll', () => _closeRewardExclude())
}
/** Initialize the comment section UI. */
initComment() {
if (!this.core.config.comment?.enable)
return
if (document.querySelector('#comments')) {
const $viewCommentsBtn = document.querySelector<HTMLElement>('.view-comments')!
$viewCommentsBtn.classList.remove('hidden')
$viewCommentsBtn.addEventListener('click', () => {
scrollIntoView('#comments')
}, false)
}
if (this.core.config.comment.expired)
document.querySelector('#comments')!.remove()
}
/** Initialize PostChat theme sync if configured. */
initPostChatUser() {
if (!window.postChatUser || !window.postChatConfig || window.postChatConfig.userMode === 'magic')
return
window.postChat_theme = this.core.isDark ? 'dark' : 'light'
eventBus.on('fixit:switch-theme', ({ detail }) => {
if (!detail.isChanged)
return
const targetFrame = document.getElementById('postChat_iframeContainer')
if (targetFrame) {
window.postChatUser.setPostChatTheme(detail.isDark ? 'dark' : 'light')
}
else {
window.postChat_theme = detail.isDark ? 'dark' : 'light'
}
})
}
/** Initialize all miscellaneous features. */
setup() {
this.initSiteTime()
this.initAutoMark()
this.initReward()
this.initPostChatUser()
this.initComment()
}
}