Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
0c6815f
feat(home): redesign homepage as a type specimen
ronaldtse Jun 18, 2026
91afc91
feat(design): site-wide specimen system + fix specimens concept and n…
ronaldtse Jun 18, 2026
2f8791f
feat(design): specimen redesign for blog index, blog post, about, and…
ronaldtse Jun 18, 2026
40c20a5
fix(design): code block bg uses warm paper-deep in light mode (was da…
ronaldtse Jun 18, 2026
5235c04
fix(design): swap Fraunces→Newsreader, fix dropdown nav, word spacing…
ronaldtse Jun 20, 2026
bea6c49
fix(design): tools first, specimens secondary and compact
ronaldtse Jun 20, 2026
2e23b02
fix(nav): Integrations dropdown button matches other nav items
ronaldtse Jun 20, 2026
3a0416d
fix(nav): match dropdown button + ellipsis button + flyout menu to na…
ronaldtse Jun 20, 2026
a5ce343
fix(nav): target inner .text span for dropdown + ellipsis buttons
ronaldtse Jun 20, 2026
798f57c
fix(nav): out-specify VitePress .VPLink.link on flyout menu items
ronaldtse Jun 20, 2026
f485275
feat(hero): interactive MOTD typewriter with blinking cursor
ronaldtse Jun 20, 2026
ba2c670
feat(hero): rotating typewriter headline + auto-generated formula/fon…
ronaldtse Jun 20, 2026
1d13218
fix(layout): eliminate double horizontal rules + add drop cap + refin…
ronaldtse Jun 21, 2026
1240713
feat(content): correct Fontisan positioning, Fontist powered-by, foun…
ronaldtse Jun 22, 2026
f08390f
fix: Fontist always before Fontisan
ronaldtse Jun 22, 2026
28a7462
refactor(arch): deepen CSS locality, add nav + build tests
ronaldtse Jun 22, 2026
00cf2fb
chore: remove 'riboseopen' footer message
ronaldtse Jun 22, 2026
f91d228
fix(design): Spectral display font + override VitePress Inter globally
ronaldtse Jun 22, 2026
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
105 changes: 17 additions & 88 deletions .vitepress/components/BlogByline.vue
Original file line number Diff line number Diff line change
Expand Up @@ -35,104 +35,33 @@ const authors = computed(() => {

<template>
<div class="blog-byline" v-if="authors || formattedDate">
<div class="byline-content">
<div class="author-info" v-if="authors">
<svg class="author-icon" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"/>
<circle cx="12" cy="7" r="4"/>
</svg>
<span class="author-names">{{ authors }}</span>
</div>
<div class="date-info">
<svg class="date-icon" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<rect x="3" y="4" width="18" height="18" rx="2" ry="2"/>
<line x1="16" y1="2" x2="16" y2="6"/>
<line x1="8" y1="2" x2="8" y2="6"/>
<line x1="3" y1="10" x2="21" y2="10"/>
</svg>
<time :datetime="frontmatter.date">{{ formattedDate }}</time>
</div>
</div>
<div class="last-updated" v-if="formattedLastUpdated">
<svg class="update-icon" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M23 4v6h-6"/>
<path d="M20.49 15a9 9 0 1 1-2.12-9.36L23 10"/>
</svg>
<span>Updated {{ formattedLastUpdated }}</span>
</div>
<span v-if="authors" class="byline-authors">{{ authors }}</span>
<span v-if="authors && formattedDate" class="sep">·</span>
<time v-if="formattedDate" :datetime="frontmatter.date">{{ formattedDate }}</time>
<span v-if="formattedLastUpdated" class="updated">Updated {{ formattedLastUpdated }}</span>
</div>
</template>

<style scoped>
.blog-byline {
display: flex;
flex-direction: column;
gap: 0.75rem;
padding: 1rem 1.25rem;
margin: 1.5rem 0;
background: var(--vp-c-bg-soft);
border-radius: 8px;
border-left: 3px solid var(--vp-c-brand-1);
}

.byline-content {
display: flex;
align-items: center;
flex-wrap: wrap;
gap: 1.5rem;
}

.author-info,
.date-info {
display: flex;
align-items: center;
gap: 0.5rem;
}

.author-icon {
color: var(--vp-c-brand-1);
flex-shrink: 0;
}

.date-icon {
color: var(--vp-c-text-3);
flex-shrink: 0;
gap: 0.6em;
padding: 14px 0;
margin: 0 0 2rem;
border-top: 1px solid var(--spec-rule);
border-bottom: 1px solid var(--spec-rule);
font-family: "IBM Plex Mono", ui-monospace, monospace;
font-size: 12px;
letter-spacing: 0.06em;
color: var(--spec-ink-soft);
}
.byline-authors { color: var(--spec-ink); font-weight: 500; }
.sep { color: var(--spec-rose); }
.updated { margin-left: auto; color: var(--spec-mute); font-size: 11px; }

.author-names {
font-weight: 500;
color: var(--vp-c-text-1);
}

.date-info time {
font-size: 0.9375rem;
color: var(--vp-c-text-2);
}

.last-updated {
display: flex;
align-items: center;
gap: 0.375rem;
font-size: 0.8125rem;
color: var(--vp-c-text-3);
padding-top: 0.5rem;
border-top: 1px solid var(--vp-c-divider);
}

.update-icon {
flex-shrink: 0;
}

/* Mobile responsiveness */
@media (max-width: 640px) {
.blog-byline {
padding: 0.875rem 1rem;
}

.byline-content {
flex-direction: column;
align-items: flex-start;
gap: 0.5rem;
}
.updated { margin-left: 0; width: 100%; margin-top: 4px; }
}
</style>
202 changes: 59 additions & 143 deletions .vitepress/components/BlogIndex.vue
Original file line number Diff line number Diff line change
@@ -1,8 +1,5 @@
<script setup lang="ts">
import { data as posts } from "../posts.data";
import { ref } from "vue";

const hoveredPost = ref<string | null>(null);

function formatDate(dateStr: string): string {
const date = new Date(dateStr);
Expand Down Expand Up @@ -33,42 +30,19 @@ function formatAuthors(authors: string[]): string {

<template>
<div class="blog-index">
<article
v-for="post in posts"
:key="post.url"
class="blog-card"
@mouseenter="hoveredPost = post.url"
@mouseleave="hoveredPost = null"
:class="{ 'is-hovered': hoveredPost === post.url }"
>
<a :href="post.url" class="card-link">
<div class="card-content">
<div class="card-header">
<time class="post-date" :datetime="post.date">
{{ formatDate(post.date) }}
</time>
<span v-if="post.lastUpdated" class="updated-badge">
Updated {{ formatLastUpdated(post.lastUpdated) }}
</span>
</div>

<h2 class="post-title">{{ post.title }}</h2>

<p v-if="post.description" class="post-excerpt">
{{ post.description }}
</p>

<div class="card-footer">
<span class="post-authors">
By {{ formatAuthors(post.authors) }}
</span>
<span class="read-more">
Read article
<svg class="arrow-icon" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M5 12h14M12 5l7 7-7 7"/>
</svg>
</span>
</div>
<article v-for="post in posts" :key="post.url" class="post-row">
<a :href="post.url" class="post-link">
<div class="post-meta">
<time class="post-date" :datetime="post.date">{{ formatDate(post.date) }}</time>
<span v-if="post.lastUpdated" class="post-updated">
Updated {{ formatLastUpdated(post.lastUpdated) }}
</span>
</div>
<h2 class="post-title">{{ post.title }}</h2>
<p v-if="post.description" class="post-excerpt">{{ post.description }}</p>
<div class="post-footer">
<span v-if="post.authors" class="post-authors">{{ formatAuthors(post.authors) }}</span>
<span class="post-read">Read →</span>
</div>
</a>
</article>
Expand All @@ -77,142 +51,84 @@ function formatAuthors(authors: string[]): string {

<style scoped>
.blog-index {
display: flex;
flex-direction: column;
gap: 1.5rem;
margin-top: 2rem;
}

.blog-card {
position: relative;
background: var(--vp-c-bg-soft);
border-radius: 12px;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
border: 1px solid transparent;
.post-row {
border-top: 1px solid var(--spec-rule);
}

.blog-card:hover,
.blog-card.is-hovered {
background: var(--vp-c-bg);
border-color: var(--vp-c-brand-1);
transform: translateY(-2px);
box-shadow:
0 4px 6px -1px rgba(0, 0, 0, 0.05),
0 10px 15px -3px rgba(191, 78, 106, 0.08);
.post-row:last-child {
border-bottom: 1px solid var(--spec-rule);
}

.card-link {
.post-link {
display: block;
padding: clamp(28px, 4vw, 48px) 0;
text-decoration: none;
color: inherit;
}

.card-content {
padding: 1.75rem 2rem;
}

.card-header {
display: flex;
align-items: center;
gap: 1rem;
margin-bottom: 0.75rem;
flex-wrap: wrap;
}

.post-date {
font-size: 0.8125rem;
font-weight: 500;
color: var(--vp-c-brand-1);
letter-spacing: 0.02em;
.post-meta {
font-family: "IBM Plex Mono", ui-monospace, monospace;
font-size: 11px;
letter-spacing: 0.14em;
text-transform: uppercase;
margin-bottom: 12px;
}

.updated-badge {
font-size: 0.75rem;
color: var(--vp-c-text-3);
background: var(--vp-c-bg);
padding: 0.25rem 0.625rem;
border-radius: 999px;
border: 1px solid var(--vp-c-divider);
}
.post-date { color: var(--spec-rose); }
.post-updated { color: var(--spec-mute); margin-left: 1em; }

.post-title {
margin: 0 0 0.75rem 0;
font-size: 1.375rem;
font-weight: 600;
line-height: 1.3;
color: var(--vp-c-text-1);
font-family: "Spectral", Georgia, serif;
font-weight: 380;
font-variation-settings: "opsz" 72;
font-size: clamp(24px, 3vw, 36px);
line-height: 1.15;
letter-spacing: -0.02em;
margin: 0 0 0.5em;
color: var(--spec-ink);
transition: color 0.2s ease;
}

.blog-card:hover .post-title {
color: var(--vp-c-brand-1);
.post-link:hover .post-title {
color: var(--spec-rose);
}

.post-excerpt {
margin: 0 0 1.25rem 0;
font-size: 1rem;
font-family: "IBM Plex Sans", sans-serif;
font-size: 15px;
line-height: 1.6;
color: var(--vp-c-text-2);
color: var(--spec-ink-soft);
margin: 0 0 16px;
}

.card-footer {
.post-footer {
display: flex;
align-items: center;
justify-content: space-between;
gap: 1rem;
flex-wrap: wrap;
}

.post-authors {
font-size: 0.875rem;
color: var(--vp-c-text-3);
}

.read-more {
display: flex;
align-items: center;
gap: 0.375rem;
font-size: 0.875rem;
font-weight: 500;
color: var(--vp-c-brand-1);
opacity: 0;
transform: translateX(-8px);
transition: all 0.3s ease;
}

.blog-card:hover .read-more {
opacity: 1;
transform: translateX(0);
}

.arrow-icon {
transition: transform 0.3s ease;
font-family: "IBM Plex Mono", ui-monospace, monospace;
font-size: 11px;
letter-spacing: 0.06em;
color: var(--spec-mute);
}
.post-read {
font-family: "IBM Plex Mono", ui-monospace, monospace;
font-size: 11px;
letter-spacing: 0.12em;
text-transform: uppercase;
color: var(--spec-ink);
border-bottom: 1px solid var(--spec-rule-strong);
padding-bottom: 2px;
transition: color 0.2s, border-color 0.2s;
}

.blog-card:hover .arrow-icon {
transform: translateX(4px);
.post-link:hover .post-read {
color: var(--spec-rose);
border-color: var(--spec-rose);
}

/* Mobile responsiveness */
@media (max-width: 640px) {
.card-content {
padding: 1.25rem 1.5rem;
}

.post-title {
font-size: 1.125rem;
}

.read-more {
opacity: 1;
transform: translateX(0);
}

.card-footer {
flex-direction: column;
align-items: flex-start;
gap: 0.5rem;
}
.post-footer { flex-direction: column; align-items: flex-start; gap: 0.5rem; }
}
</style>
Loading
Loading