Fix light theme, fix animation bugs, add programmatic scrolling, improve custom card blog component

This commit is contained in:
Andrew Illarionov 2024-06-19 18:06:17 +03:00
parent 3e39bda014
commit 2204709c0e
12 changed files with 313 additions and 222 deletions

View File

@ -56,6 +56,6 @@ useHead({
</div> </div>
<ClientOnly> <ClientOnly>
<LazyPortal v-model="animate" layout="#ender-layout" randomize fade /> <Portal v-model="animate" layout="#ender-layout" randomize fade />
</ClientOnly> </ClientOnly>
</template> </template>

View File

@ -18,19 +18,19 @@
.list-style-type { .list-style-type {
&-none { &-none {
list-style-type: none; list-style-type: none !important;
} }
&-do { &-do {
list-style-type: do; list-style-type: do !important;
} }
&-enjoy { &-enjoy {
list-style-type: enjoy; list-style-type: enjoy !important;
} }
&-faq { &-faq {
list-style-type: faq; list-style-type: faq !important;
> li:nth-child(2n):not(:last-child) { > li:nth-child(2n):not(:last-child) {
@apply mb-2; @apply mb-2;

View File

@ -40,6 +40,7 @@ html {
::-webkit-scrollbar { ::-webkit-scrollbar {
width: 8px; width: 8px;
height: 8px;
} }
::-webkit-scrollbar-track { ::-webkit-scrollbar-track {
@ -52,13 +53,21 @@ html {
background-color: rgb(255 255 255 / 15%); background-color: rgb(255 255 255 / 15%);
} }
::-webkit-scrollbar-corner {
border-radius: 3px;
background-color: rgb(255 255 255 / 15%);
}
&.light { &.light {
::-webkit-scrollbar-track { ::-webkit-scrollbar-track {
background-color: rgb(255 255 255 / 15%); background-color: rgb(255 255 255 / 15%);
} }
::-webkit-scrollbar-thumb { ::-webkit-scrollbar-thumb {
border-radius: 8px; background-color: rgb(0 0 0 / 30%);
}
::-webkit-scrollbar-corner {
background-color: rgb(0 0 0 / 30%); background-color: rgb(0 0 0 / 30%);
} }
} }
@ -137,12 +146,19 @@ body {
margin: auto; margin: auto;
} }
:not(nav) > ul { :not(nav) {
list-style-type: disc; > ul {
padding-left: 1.5em; @apply ps-6;
list-style-type: disc;
}
> ol {
@apply ps-6;
list-style-type: decimal;
}
} }
:is(section, article, aside).page { :where(section, article, aside).page {
:is(div, p):not(:last-child) { :is(div, p):not(:last-child) {
@apply mb-4; @apply mb-4;
} }
@ -155,30 +171,37 @@ body {
color: indianred; color: indianred;
} }
} }
&:not(:last-child) { &:not(:last-child) {
@apply mb-2; @apply mb-2;
} }
} }
:is(ul, ol):not(:last-child) { ul {
@apply mb-4; list-style-type: disc;
} }
:where(p, li) { ol {
code { list-style-type: decimal;
&:hover { }
background-color: rgb(138 71 245 / 20%);
}
padding: 0.25em; :where(ul, ol) {
&:not(:last-child) {
@apply mb-4;
}
li:not(:last-child) {
@apply mb-1;
}
}
border-radius: 0.5em; code:not(pre *) {
@apply px-1 py-0.5 break-words cursor-pointer rounded-lg transition-ease;
cursor: pointer; background-color: rgb(83 35 162 / 10%);
background-color: rgb(83 35 162 / 10%);
transition: 0.3s ease; &:hover {
background-color: rgb(138 71 245 / 20%);
} }
} }
@ -187,56 +210,49 @@ body {
} }
blockquote { blockquote {
padding: 0.75em 1em; @apply px-4 py-3 mb-4 border-l-4 rounded-r-xl transition-ease;
border-left: 0.25em solid rgb(153 153 255 / 60%);
border-top-right-radius: 0.5em;
border-bottom-right-radius: 0.5em;
border-left-color: rgb(153 153 255 / 60%);
background-color: rgb(153 153 255 / 10%); background-color: rgb(153 153 255 / 10%);
transition: 0.3s ease;
&:hover { &:hover {
border-left-color: rgb(153 153 255 / 100%); border-left-color: rgb(153 153 255 / 100%);
background-color: rgb(153 153 255 / 15%); background-color: rgb(153 153 255 / 15%);
} }
:last-child {
margin-bottom: 0;
}
} }
pre { pre {
font-size: 14px; @apply relative font-small overflow-auto min-h-14 max-h-[80rem] rounded-xl p-2 mb-4;
padding: 1em;
border-radius: 0.5em;
background-color: rgb(83 35 162 / 5%); background-color: rgb(83 35 162 / 5%);
position: relative; &:hover > .absolute {
@apply opacity-100;
overflow-x: auto; }
code { > code {
@apply scroll-p-2;
counter-reset: step; counter-reset: step;
counter-increment: step 0; counter-increment: step 0;
scroll-padding: 0.5em;
.line::before { .line::before {
@apply inline-block w-4 mr-6 text-right;
content: counter(step); content: counter(step);
counter-increment: step; counter-increment: step;
display: inline-block;
text-align: right;
color: rgb(238 246 250 / 40%); color: rgb(238 246 250 / 40%);
width: 1rem;
margin-right: 1.5rem;
} }
> span span:last-child {
@apply me-4;
}
}
.overlay-action {
background-color: rgb(153 153 255 / 10%);
border-color: rgb(153 153 255 / 60%);
} }
} }
} }
@ -480,6 +496,41 @@ rt {
border-color: rgb(123 123 255 / 80%); border-color: rgb(123 123 255 / 80%);
} }
} }
:is(section, article, aside).page {
code:not(pre *) {
background-color: rgb(138 71 245 / 20%);
&:hover {
background-color: rgb(83 35 162 / 20%);
}
}
blockquote {
border-left-color: rgb(153 153 255 / 60%);
background-color: rgb(153 153 255 / 10%);
&:hover {
border-left-color: rgb(153 153 255 / 100%);
background-color: rgb(153 153 255 / 15%);
}
}
pre {
background-color: rgb(83 35 162 / 10%);
code {
.line::before {
color: rgb(83 85 89 / 40%);
}
}
.overlay-action {
background-color: rgb(153 153 255 / 30%);
border-color: rgb(153 153 255 / 80%);
}
}
}
} }
// Dynamic classes. // Dynamic classes.

View File

@ -11,6 +11,18 @@
} }
} }
.switch {
&-enter-active,
&-leave-active {
transition: all 0.15s ease-in-out;
}
&-enter-from,
&-leave-to {
opacity: 0;
}
}
.transition { .transition {
&-ease { &-ease {
transition: all 0.3s ease; transition: all 0.3s ease;

View File

@ -11,7 +11,7 @@ import ideaIcon from '~/assets/images/icons/accent/idea.png'
type IconOptions = 'warning' | 'error' | 'info' | 'help' | 'checkmark' | 'idea' type IconOptions = 'warning' | 'error' | 'info' | 'help' | 'checkmark' | 'idea'
const props = defineProps({ const props = defineProps({
icon: { type: {
type: String as PropType<IconOptions>, type: String as PropType<IconOptions>,
default: 'warning', default: 'warning',
}, },
@ -21,42 +21,49 @@ const props = defineProps({
}, },
}) })
let cardIcon = warningIcon const cards = {
let color = 'rgb(255 246 147 / 60%)' warning: {
color: 'border-warning',
switch (props.icon) { icon: {
case 'error': src: warningIcon,
cardIcon = errorIcon alt: 'Warning triangle',
color = 'rgb(255 113 113 / 60%)' },
break },
error: {
case 'info': color: 'border-error',
cardIcon = infoIcon icon: {
color = 'rgb(94 162 255 / 60%)' src: errorIcon,
alt: 'Error circle',
break },
},
case 'help': info: {
cardIcon = helpIcon color: 'border-info',
color = 'rgb(178 104 255 / 60%)' icon: {
src: infoIcon,
break alt: 'Information circle',
},
case 'checkmark': },
cardIcon = checkIcon help: {
color = 'rgb(69 255 255 / 60%)' color: 'border-help',
icon: {
break src: helpIcon,
alt: 'Help circle',
case 'idea': },
cardIcon = ideaIcon },
color = 'rgb(255 255 255 / 60%)' checkmark: {
color: 'border-checkmark',
break icon: {
src: checkIcon,
case 'warning': alt: 'Checkmark',
default: },
break },
idea: {
color: 'border-idea',
icon: {
src: ideaIcon,
alt: 'Lightbulb',
},
},
} }
</script> </script>
@ -64,13 +71,15 @@ switch (props.icon) {
<div class="tilt flex flex-row items-center gap-2 mb-0 sm:mb-2 md:mb-4"> <div class="tilt flex flex-row items-center gap-2 mb-0 sm:mb-2 md:mb-4">
<img <img
draggable="false" draggable="false"
:src="cardIcon" :src="cards[props.type].icon.src"
:alt="`Download`" :alt="cards[props.type].icon.alt"
class="icon-image" class="w-12 h-12"
/> />
<h3 class="whitespace-nowrap mb-0">{{ props.title }}</h3> <h3 class="whitespace-nowrap mb-0">{{ props.title }}</h3>
</div> </div>
<div class="box p-4 rounded-xl mb-4"> <div
:class="`box p-4 mb-4 ${cards[props.type].color} border-b border-r rounded-xl`"
>
<slot /> <slot />
</div> </div>
</template> </template>
@ -79,22 +88,4 @@ switch (props.icon) {
.tilt { .tilt {
transform: rotate(-2deg); transform: rotate(-2deg);
} }
.box {
> p {
margin-bottom: 0;
}
margin-bottom: 1rem;
border-bottom: 1px ridge;
border-right: 1px ridge;
border-color: v-bind(color);
}
.icon {
&-image {
width: 48px;
height: 48px;
}
}
</style> </style>

View File

@ -1,32 +1,35 @@
<script setup lang="ts"></script> <script setup lang="ts">
const emit = defineEmits(['copy'])
const clicked = ref<boolean>(false)
function p() {
emit('copy')
if (!clicked.value) setTimeout(() => (clicked.value = false), 2000)
clicked.value = true
}
</script>
<template> <template>
<div <div
class="absolute flex flex-col justify-start top-0 left-0 w-full h-full pointer-events-none" class="absolute flex flex-col justify-start top-0 left-0 w-full h-full pointer-events-none opacity-0 hover:opacity-100 transition-ease"
> >
<div class="flex flex-row justify-end items-center p-2"> <div class="flex flex-row justify-end items-center p-2">
<iconify-icon icon="mdi:content-copy" inline class="button" /> <span
class="overlay-action flex-grow-0 pointer-events-auto cursor-pointer p-2 border rounded-xl"
@click="p"
>
<Transition name="switch" mode="out-in">
<iconify-icon v-if="!clicked" icon="mdi:content-copy" inline />
<iconify-icon
v-else
icon="mdi:check-bold"
class="text-green-500"
inline
/>
</Transition>
</span>
</div> </div>
</div> </div>
</template> </template>
<style scoped lang="scss">
.button {
@apply flex-grow-0;
pointer-events: auto;
padding: 0.5em;
cursor: pointer;
background-color: rgb(153 153 255 / 10%);
border: 1px solid rgb(153 153 255 / 60%);
border-radius: 0.5em;
opacity: 0.5;
transition: 0.3s ease;
}
</style>

View File

@ -48,7 +48,7 @@ and you are as much of a perfectionist as I am, buckle up and prepare for severa
::card ::card
--- ---
icon: warning type: warning
title: Before we begin title: Before we begin
--- ---
Invoking `sfc /scannow` or `dism /restorehealth` will likely interfere with your context menu, Invoking `sfc /scannow` or `dism /restorehealth` will likely interfere with your context menu,
@ -141,7 +141,7 @@ Children of the `Shell` subkey are the «shell entries» — locally provided cu
::card ::card
--- ---
icon: info type: info
title: Clearing up the confusion title: Clearing up the confusion
--- ---
The terminology may be a little confusing, since Microsoft hasn't ever offered a standard nomenclature for the The terminology may be a little confusing, since Microsoft hasn't ever offered a standard nomenclature for the
@ -195,7 +195,7 @@ with an exception to the following two macros:
::card ::card
--- ---
icon: error type: error
title: Inconsistency title: Inconsistency
--- ---
Keep in mind that the `%1` macro is the Batch version of `argv[1]` — the first item in the C argument vector. Keep in mind that the `%1` macro is the Batch version of `argv[1]` — the first item in the C argument vector.
@ -316,7 +316,7 @@ shell entry **do not seem to correspond**.
::card ::card
--- ---
icon: idea type: idea
title: Tip title: Tip
--- ---
You can use the Explorer folder icon picker to acquire the icon DLL and visually calculate the offset You can use the Explorer folder icon picker to acquire the icon DLL and visually calculate the offset
@ -669,7 +669,7 @@ The following are fairly common areas Winaero Tweaker doesn't cover.
::card ::card
--- ---
icon: idea type: idea
title: Tip title: Tip
--- ---
As a rule of thumb, whenever you're dealing with existing shell entries, I _really_ suggest As a rule of thumb, whenever you're dealing with existing shell entries, I _really_ suggest

View File

@ -53,17 +53,18 @@ onMounted(() => {
class="dimensions accent-background transition-ease overflow-auto flex flex-col gap-4 lm:gap-0 sm:gap-2 py-2 px-6 sm:p-4" class="dimensions accent-background transition-ease overflow-auto flex flex-col gap-4 lm:gap-0 sm:gap-2 py-2 px-6 sm:p-4"
:class="{ :class="{
'animate__animated-sm animate__delay-1-5s animate__fadeInDown': 'animate__animated-sm animate__delay-1-5s animate__fadeInDown':
!animationComplete, !reader && !animationComplete,
'animate__animated-sm animate__fadeIn': reader && !animationComplete,
'lm:rounded-none sm:rounded-xl sm:mt-8 lm:mt-0 lt:mt-0': !reader, 'lm:rounded-none sm:rounded-xl sm:mt-8 lm:mt-0 lt:mt-0': !reader,
'!max-h-full h-full': reader, '!max-h-full h-full': reader,
}" }"
> >
<Settings v-if="animationComplete" /> <Settings />
<Navigation /> <Navigation />
<slot name="header" /> <slot name="header" />
<NuxtPage <NuxtPage
class="sm:fade-mask flex-grow overflow-y-auto h-full sm:py-4 sm:pe-4" class="sm:fade-mask flex-grow overflow-y-auto h-full sm:py-4 sm:pe-4"
/> />
<slot v-if="animationComplete" name="footer" /> <slot name="footer" />
</main> </main>
</template> </template>

View File

@ -1,4 +1,4 @@
export default defineNuxtRouteMiddleware((to, _from) => { export default defineNuxtRouteMiddleware((to, from) => {
const { reader } = storeToRefs(usePageStore()) const { reader } = storeToRefs(usePageStore())
reader.value = /\/blog\/(.*)/.test(to.path) reader.value = /\/blog\/(.*)/.test(to.path)

View File

@ -1,35 +1,35 @@
import config from "./config"; import config from './config'
// https://nuxt.com/docs/api/configuration/nuxt-config // https://nuxt.com/docs/api/configuration/nuxt-config
export default defineNuxtConfig({ export default defineNuxtConfig({
app: { app: {
head: { head: {
htmlAttrs: { htmlAttrs: {
lang: config.locale || "en", lang: config.locale || 'en',
}, },
link: [ link: [
{ {
rel: "icon", rel: 'icon',
type: "image/x-icon", type: 'image/x-icon',
href: "/favicon.ico", href: '/favicon.ico',
}, },
], ],
meta: [ meta: [
{ {
charset: "utf-8", charset: 'utf-8',
}, },
{ {
name: "viewport", name: 'viewport',
content: "width=device-width, initial-scale=1", content: 'width=device-width, initial-scale=1',
}, },
{ {
"http-equiv": "X-UA-Compatible", 'http-equiv': 'X-UA-Compatible',
content: "ie=edge", content: 'ie=edge',
}, },
], ],
}, },
pageTransition: { name: "page", mode: "out-in" }, pageTransition: { name: 'page', mode: 'out-in' },
rootId: "ender-app", rootId: 'ender-app',
}, },
devtools: { devtools: {
enabled: true, enabled: true,
@ -43,64 +43,68 @@ export default defineNuxtConfig({
components: { components: {
dirs: [ dirs: [
{ {
path: "~/components", path: '~/components',
pathPrefix: false, pathPrefix: false,
extensions: [".vue"], extensions: ['.vue'],
}, },
], ],
}, },
css: ["~/assets/styles/main.scss"], css: ['~/assets/styles/main.scss'],
plugins: [], plugins: [],
modules: [ modules: [
"@pinia/nuxt", '@pinia/nuxt',
"@nuxt/content", '@nuxt/content',
"@nuxtjs/seo", '@nuxtjs/seo',
"@nuxtjs/google-fonts", '@nuxtjs/google-fonts',
"@nuxtjs/tailwindcss", '@nuxtjs/tailwindcss',
"@nuxtjs/color-mode", '@nuxtjs/color-mode',
"@nuxt/eslint", '@nuxt/eslint',
["@nuxtjs/stylelint-module", { failOnError: true, lintOnStart: false }], ['@nuxtjs/stylelint-module', { failOnError: true, lintOnStart: false }],
], ],
colorMode: { colorMode: {
preference: "system", preference: 'system',
fallback: "dark", fallback: 'dark',
classPrefix: "", classPrefix: '',
classSuffix: "", classSuffix: '',
componentName: "NuxtTheme", componentName: 'NuxtTheme',
storageKey: "ecmatheme", storageKey: 'ecmatheme',
}, },
content: { content: {
markdown: { markdown: {
remarkPlugins: ["remark-reading-time"], remarkPlugins: ['remark-reading-time'],
}, },
highlight: { highlight: {
theme: "github-dark", theme: {
default: 'github-dark',
light: 'github-light',
sepia: 'monokai',
},
langs: [ langs: [
"shell", 'shell',
"batch", 'batch',
"vb", 'vb',
"ini", 'ini',
"asm", 'asm',
"c", 'c',
"cpp", 'cpp',
"java", 'java',
"python", 'python',
"csv", 'csv',
"xml", 'xml',
"json", 'json',
"yaml", 'yaml',
"html", 'html',
"css", 'css',
"sass", 'sass',
"php", 'php',
"js", 'js',
"ts", 'ts',
"vue", 'vue',
"md", 'md',
"mdc", 'mdc',
"pascal", 'pascal',
"lisp", 'lisp',
"sql", 'sql',
], ],
}, },
}, },
@ -109,7 +113,7 @@ export default defineNuxtConfig({
crawlLinks: true, crawlLinks: true,
autoSubfolderIndex: true, autoSubfolderIndex: true,
failOnError: true, failOnError: true,
routes: ["/robots.txt", "/sitemap.xml"], routes: ['/robots.txt', '/sitemap.xml'],
}, },
}, },
googleFonts: { googleFonts: {
@ -160,22 +164,22 @@ export default defineNuxtConfig({
// }, // },
// }, // },
sitemap: { sitemap: {
sources: ["/api/__sitemap__/content"], sources: ['/api/__sitemap__/content'],
cacheMaxAgeSeconds: 360, cacheMaxAgeSeconds: 360,
exclude: [], exclude: [],
credits: false, credits: false,
xslColumns: [ xslColumns: [
{ label: "URL", width: "50%" }, { label: 'URL', width: '50%' },
{ label: "Last Modified", select: "sitemap:lastmod", width: "25%" }, { label: 'Last Modified', select: 'sitemap:lastmod', width: '25%' },
{ label: "Priority", select: "sitemap:priority", width: "12.5%" }, { label: 'Priority', select: 'sitemap:priority', width: '12.5%' },
{ {
label: "Change Frequency", label: 'Change Frequency',
select: "sitemap:changefreq", select: 'sitemap:changefreq',
width: "12.5%", width: '12.5%',
}, },
], ],
defaults: { defaults: {
changefreq: "yearly", changefreq: 'yearly',
priority: 0.7, priority: 0.7,
lastmod: config.build.date, lastmod: config.build.date,
}, },
@ -194,7 +198,7 @@ export default defineNuxtConfig({
}, },
vue: { vue: {
compilerOptions: { compilerOptions: {
isCustomElement: (tag) => tag === "iconify-icon", isCustomElement: (tag) => tag === 'iconify-icon',
}, },
}, },
}); })

View File

@ -5,6 +5,7 @@ import { render } from 'vue'
import { CodeControls } from '#components' import { CodeControls } from '#components'
const config = useAppConfig() const config = useAppConfig()
const router = useRouter()
const route = useRoute() const route = useRoute()
let slug = route.params.slug let slug = route.params.slug
@ -40,14 +41,12 @@ useSeoMeta({
const clipboard = (el: HTMLElement) => { const clipboard = (el: HTMLElement) => {
if (el.textContent) if (el.textContent)
navigator.clipboard navigator.clipboard.writeText(el.textContent).catch(() => {
.writeText(el.textContent) console.error(
.then(() => { 'Failed to copy element data to the clipboard! Element data:',
alert(`successfully copied ${el.textContent}`) el,
}) )
.catch(() => { })
alert('something went wrong')
})
} }
const content = ref<Element | null>(null) const content = ref<Element | null>(null)
@ -73,6 +72,11 @@ defineOgImageComponent(
// Hydrate the rendered items. // Hydrate the rendered items.
onMounted(() => { onMounted(() => {
if (route.hash)
content.value
?.querySelector(route.hash.toLowerCase())
?.scrollIntoView({ behavior: 'smooth' })
content.value content.value
?.querySelectorAll('code:not(pre *)') ?.querySelectorAll('code:not(pre *)')
.forEach((code: Element) => { .forEach((code: Element) => {
@ -80,10 +84,27 @@ onMounted(() => {
code.addEventListener('click', () => clipboard(code)) code.addEventListener('click', () => clipboard(code))
}) })
content.value?.querySelectorAll('pre').forEach((pre: HTMLElement) => { content.value?.querySelectorAll('pre').forEach((pre: HTMLPreElement) => {
const icon = h(CodeControls) const overlay = h(CodeControls, { onCopy: () => clipboard(pre) })
render(icon, pre) render(overlay, pre)
})
content.value?.querySelectorAll('a').forEach((a: HTMLAnchorElement) => {
if (
a.hash &&
a.host === window.location.host &&
a.pathname.toLowerCase() === route.path.toLowerCase()
)
a.addEventListener('click', (e: Event) => {
e.preventDefault()
content.value
?.querySelector(a.hash.toLowerCase())
?.scrollIntoView({ behavior: 'smooth' })
history.replaceState(history.state, '', a.hash)
})
}) })
}) })
@ -117,7 +138,7 @@ useHead({
</div> </div>
<article <article
v-else-if="data" v-else-if="data"
class="flex-grow post snap-normal fade-mask-sm flex flex-col gap-4 overflow-x-hidden overflow-y-auto sm:py-4 sm:pe-4" class="flex-grow post snap-normal fade-mask-sm flex flex-col gap-4 overflow-y-auto sm:py-4 sm:pe-4"
> >
<div class="grid-thumbnail grid max-w-[768px] snap-end"> <div class="grid-thumbnail grid max-w-[768px] snap-end">
<img <img

View File

@ -20,6 +20,14 @@ export default <Partial<Config>>{
raw: '(max-height: 996px) and (min-width: 601px) and (max-width: 1280px)', raw: '(max-height: 996px) and (min-width: 601px) and (max-width: 1280px)',
}, },
}, },
colors: {
warning: '#FFF693',
error: '#FF7171',
info: '#5EA2FF',
help: '#B268FF',
checkmark: '#45FFFF',
idea: '#FFFFFF',
},
fontFamily: { fontFamily: {
sans: [ sans: [
'Lato', 'Lato',