Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions app/assets/css/main.css
Original file line number Diff line number Diff line change
Expand Up @@ -211,3 +211,11 @@ html.light::view-transition-new(root) {
animation: none;
}
}

html:not(.light) .shiki span {
color: var(--shiki-dark);
background: var(--shiki-dark-bg);
font-style: var(--shiki-dark-font-style);
font-weight: var(--shiki-dark-font-weight);
text-decoration: var(--shiki-dark-text-decoration);
}
48 changes: 48 additions & 0 deletions app/components/content/Clip.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
<script setup lang="ts">
const props = defineProps<{
src: string
alt: string
poster?: string
width?: string | number
height?: string | number
}>()

const reduced = useMediaQuery('(prefers-reduced-motion: reduce)')

const video = ref<HTMLVideoElement>()
const load = ref(false)

const { stop } = useIntersectionObserver(
video,
([entry]) => {
if (!entry?.isIntersecting)
return
load.value = true
stop()
},
{ rootMargin: '200px' },
)

watchEffect(() => {
if (reduced.value)
load.value = true
})
</script>

<template>
<video
ref="video"
:src="load ? props.src : undefined"
:poster="props.poster"
:aria-label="props.alt"
:title="props.alt"
:width="props.width"
:height="props.height"
:autoplay="!reduced"
:loop="!reduced"
:controls="reduced"
muted
playsinline
preload="none"
/>
</template>
36 changes: 36 additions & 0 deletions app/components/content/ProseA.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
<script setup lang="ts">
defineOptions({ inheritAttrs: false })

const props = defineProps<{ href?: string }>()

const attrs = useAttrs()

const external = computed(() => /^(https?:)?\/\//.test(props.href ?? ''))

const route = computed(() => {
const href = props.href ?? ''
return href.startsWith('/') && !/\.[a-z0-9]+(?:[?#]|$)/i.test(href)
})

const rel = computed(() => {
const current = Array.isArray(attrs.rel) ? attrs.rel : String(attrs.rel ?? '').split(' ')
const merged = external.value ? [...current, 'noopener', 'noreferrer'] : current
const cleaned = [...new Set(merged.filter(Boolean))].join(' ')
return cleaned || undefined
})
</script>

<template>
<NuxtLink v-if="route" v-bind="attrs" :to="props.href">
<slot />
</NuxtLink>
<a
v-else
v-bind="attrs"
:href="props.href"
:target="external ? '_blank' : undefined"
:rel="rel"
>
<slot />
</a>
</template>
52 changes: 52 additions & 0 deletions app/components/content/ProseImg.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
<script setup lang="ts">
defineOptions({ inheritAttrs: false })

const props = defineProps<{
src?: string
alt?: string
width?: string | number
height?: string | number
}>()

const attrs = useAttrs()

const optimizable = computed(() => {
const src = props.src ?? ''
return src.startsWith('/') && /\.(?:jpe?g|png)$/i.test(src)
})

const loading = computed(() => (attrs.loading as string | undefined) ?? 'lazy')

const passthrough = computed(() => {
const rest = { ...attrs }
delete rest.loading
delete rest.fetchpriority
return rest
})
</script>

<template>
<NuxtPicture
v-if="optimizable"
v-bind="passthrough"
:src="props.src"
:alt="props.alt"
:width="props.width"
:height="props.height"
sizes="sm:100vw md:700px"
:img-attrs="{ fetchpriority: attrs.fetchpriority }"
:loading="loading"
decoding="async"
/>
<img
v-else
v-bind="passthrough"
:src="props.src"
:alt="props.alt"
:width="props.width"
:height="props.height"
:fetchpriority="attrs.fetchpriority"
:loading="loading"
decoding="async"
>
</template>
49 changes: 49 additions & 0 deletions app/pages/blog/[...slug].vue
Original file line number Diff line number Diff line change
Expand Up @@ -246,10 +246,59 @@ function fmt(d?: string) {
.prose-content :deep(ul) { list-style: disc; }
.prose-content :deep(ol) { list-style: decimal; }
.prose-content :deep(li) { margin-block: 0.35rem; }
.prose-content :deep(video) {
display: block;
margin-block: 1.5rem;
border-radius: 8px;
max-width: 100%;
height: auto;
}
.prose-content :deep(img) {
display: block;
margin-block: 1.5rem;
border-radius: 8px;
max-width: 100%;
height: auto;
}
.prose-content :deep(hr) {
border: 0;
height: 1px;
margin-block: 2.5rem;
background: linear-gradient(
to right,
transparent,
rgb(var(--ink) / 0.16) 20%,
rgb(var(--ink) / 0.16) 80%,
transparent
);
}
.prose-content :deep(table) {
width: 100%;
margin-block: 1.5rem;
border-collapse: collapse;
font-size: 0.9rem;
line-height: 1.6;
display: block;
overflow-x: auto;
}
.prose-content :deep(th) {
text-align: start;
font-weight: 600;
font-size: 0.75rem;
letter-spacing: 0.06em;
text-transform: uppercase;
color: rgb(var(--ink) / 0.55);
padding: 0.5rem 0.9rem 0.5rem 0;
border-bottom: 1px solid rgb(var(--ink) / 0.12);
}
.prose-content :deep(td) {
vertical-align: top;
padding: 0.7rem 0.9rem 0.7rem 0;
color: rgb(var(--ink) / 0.78);
border-bottom: 1px solid rgb(var(--ink) / 0.06);
}
.prose-content :deep(tr:last-child td) {
border-bottom: 0;
}
.prose-content :deep(h1) {
font-size: 1.8rem;
Expand Down
Loading