Skip to content
Merged
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
20 changes: 17 additions & 3 deletions .vitepress/config.mts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
import { defineConfig } from 'vitepress'
import container from 'markdown-it-container'
import { renderSandbox } from 'vitepress-plugin-sandpack'
import { withMermaid } from 'vitepress-plugin-mermaid'
import { MermaidMarkdown, MermaidPlugin } from 'vitepress-plugin-mermaid'
import { head, nav, sidebar, transformHead } from './configs/index.mts'

const LIVE_SANDBOX_CONTAINER_PATTERN = /:{3,}\s*textmode-(?:api-)?sandbox\b[^\n]*\n[\s\S]*?\n:{3,}/g
Expand Down Expand Up @@ -61,7 +61,7 @@ const themeConfig = {
}

// https://vitepress.dev/reference/site-config
export default withMermaid(defineConfig({
export default defineConfig({
lang: 'en-US',
appearance: 'dark',
lastUpdated: true,
Expand All @@ -84,8 +84,20 @@ export default withMermaid(defineConfig({
hostname: 'https://code.textmode.art'
},
transformHead,
transformHtml: (html, _id, { pageData }) => pageData.frontmatter.layout === 'home'
? html.replace(/(<div[^>]*id="VPContent")/, '$1 role="main"') : html,

vite: {
// Retain the plugin's configuration module; the theme loads its renderer lazily.
plugins: [{ ...MermaidPlugin(), transform: undefined }],
resolve: {
alias: {
'dayjs/plugin/advancedFormat.js': 'dayjs/esm/plugin/advancedFormat',
'dayjs/plugin/customParseFormat.js': 'dayjs/esm/plugin/customParseFormat',
'dayjs/plugin/isoWeek.js': 'dayjs/esm/plugin/isoWeek',
'cytoscape/dist/cytoscape.umd.js': 'cytoscape/dist/cytoscape.esm.js',
},
},
optimizeDeps: {
// Mermaid is only reached through a lazily-loaded component, so Vite's
// dependency scanner misses it. Pre-bundle it (and its CommonJS
Expand All @@ -96,6 +108,8 @@ export default withMermaid(defineConfig({

markdown: {
config(md) {
// Keep the plugin's Markdown syntax without its eager browser runtime.
MermaidMarkdown(md, {})
md
.use(container, 'textmode-sandbox', {
render(tokens: any[], idx: number) {
Expand All @@ -121,4 +135,4 @@ export default withMermaid(defineConfig({
},

themeConfig,
}))
})
4 changes: 1 addition & 3 deletions .vitepress/configs/head.mts
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,6 @@ const baseHeaders: HeadConfig[] = [
['link', { rel: 'icon', type: 'image/png', sizes: '64x64', href: '/icon-dark-64.png', media: '(prefers-color-scheme: dark)' }],
['link', { rel: 'manifest', href: '/site.webmanifest' }],
['script', {}, "(()=>{const m=window.matchMedia('(prefers-color-scheme: dark)');const u=()=>{const mode=m.matches?'dark':'light';document.querySelectorAll('link[rel*=\"icon\"][sizes]').forEach(l=>{l.href=l.href.replace(/-(light|dark)-/,`-${mode}-`)});};m.addEventListener('change',u);})();"],
['link', { rel: 'preconnect', href: 'https://fonts.googleapis.com' }],
['link', { rel: 'preconnect', href: 'https://fonts.gstatic.com', crossorigin: '' }],
['link', { href: 'https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap', rel: 'stylesheet' }]
]

export const head: HeadConfig[] = [...baseHeaders]
Expand Down Expand Up @@ -181,6 +178,7 @@ export const transformHead = ({ pageData }: TransformContext): HeadConfig[] => {

// Schema: Home page
if (isHomePage) {
head.push(['link', { rel: 'preload', href: '/fonts/Bescii-Mono.woff', as: 'font', type: 'font/woff', crossorigin: '' }])
head.push(['script', { type: 'application/ld+json' }, JSON.stringify({
"@context": "https://schema.org",
"@graph": [
Expand Down
90 changes: 87 additions & 3 deletions .vitepress/theme/components/TextmodeHero.vue
Original file line number Diff line number Diff line change
@@ -1,17 +1,24 @@
<template>
<div class="textmode-hero-container" ref="containerRef">
<section class="textmode-hero-container" ref="containerRef" aria-labelledby="textmode-hero-title">
<canvas
ref="canvasRef"
class="textmode-hero-canvas"
aria-hidden="true"
/>
</div>
<div class="textmode-hero-labels">
<h1 id="textmode-hero-title" class="textmode-hero-title">{{ frontmatter.hero.name }}</h1>
<p class="textmode-hero-description">{{ frontmatter.hero.tagline }}</p>
</div>
</section>
</template>

<script setup lang="ts">
import { nextTick, onBeforeUnmount, onMounted, ref } from 'vue'
import { useData } from 'vitepress'
import type { SketchInstance } from '../sketches/hero-wave/types'

const { frontmatter } = useData()

const DEFAULT_WIDTH = 800
const DEFAULT_HEIGHT = 400

Expand All @@ -22,6 +29,16 @@ let sketchInstance: SketchInstance | null = null
let resizeObserver: ResizeObserver | null = null
let resizeFrame = 0
let isUnmounted = false
let rendererReady = false
let inView = false
let visibilityObserver: IntersectionObserver | null = null
let motion: MediaQueryList | null = null

function syncPlayback() {
if (!rendererReady) return
if (inView && !document.hidden && !motion?.matches) sketchInstance?.tm?.loop?.()
else sketchInstance?.tm?.noLoop?.()
}

function getCanvasSize() {
const container = containerRef.value
Expand Down Expand Up @@ -56,13 +73,29 @@ onMounted(async () => {
if (!canvas || !container) return

try {
// Paint the HTML labels before starting the decorative WebGL renderer.
await document.fonts.ready
await new Promise<void>(resolve => requestAnimationFrame(() => requestAnimationFrame(() => resolve())))
if (isUnmounted) return
const { createHeroWaveSketch } = await import('../sketches/hero-wave/')
if (isUnmounted || !canvas.isConnected) return

const { width, height } = getCanvasSize()
canvas.width = width
canvas.height = height
sketchInstance = createHeroWaveSketch(canvas, width, height)
sketchInstance = createHeroWaveSketch(canvas, width, height, {
mode: 'background',
onReady: () => { rendererReady = true; syncPlayback() }
})

motion = window.matchMedia('(prefers-reduced-motion: reduce)')
motion.addEventListener('change', syncPlayback)
document.addEventListener('visibilitychange', syncPlayback)
visibilityObserver = new IntersectionObserver(entries => {
inView = entries[0].isIntersecting
syncPlayback()
})
visibilityObserver.observe(container)

resizeObserver = new ResizeObserver(resizeSketch)
resizeObserver.observe(container)
Expand All @@ -73,10 +106,61 @@ onMounted(async () => {

onBeforeUnmount(() => {
isUnmounted = true
visibilityObserver?.disconnect()
motion?.removeEventListener('change', syncPlayback)
document.removeEventListener('visibilitychange', syncPlayback)
cancelAnimationFrame(resizeFrame)
resizeObserver?.disconnect()
resizeObserver = null
sketchInstance?.destroy?.()
sketchInstance = null
})
</script>

<style scoped>
.textmode-hero-labels {
position: relative;
z-index: 1;
width: min(100%, 68ch);
padding: 24px;
font-family: 'Bescii Mono', var(--textmode-font);
font-synthesis: none;
text-align: left;
}
.textmode-hero-title {
display: table;
width: fit-content;
margin: 0;
padding: 8px 12px;
border: 3px solid #009fca;
border-bottom: 0;
background: #1b1b1f;
color: #29d8ff;
font-family: inherit;
font-size: 28px;
font-weight: 400;
line-height: 1.2;
letter-spacing: 0;
}
.textmode-hero-description {
width: fit-content;
max-width: 100%;
margin: 0;
padding: 10px 12px;
border: 3px solid #009fca;
background: #1b1b1f;
color: #82d3dc;
font-size: 20px;
line-height: 1.4;
overflow-wrap: break-word;
}
@media (max-width: 640px) {
.textmode-hero-labels { padding: 16px; }
.textmode-hero-title { font-size: 24px; }
.textmode-hero-description { font-size: 18px; }
}
@media (max-width: 480px) {
.textmode-hero-title { font-size: 20px; }
.textmode-hero-description { font-size: 16px; }
}
</style>
91 changes: 67 additions & 24 deletions .vitepress/theme/custom.css
Original file line number Diff line number Diff line change
@@ -1,5 +1,66 @@
/* Import a good monospace font for ASCII aesthetics */
@import url('https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;600;700&display=swap');
@font-face {
font-family: 'Bescii Mono';
src: url('/fonts/Bescii-Mono.woff') format('woff');
font-style: normal;
font-weight: 400;
font-display: swap;
}

/* Same JetBrains Mono face, served locally; browsers load only needed subsets. */
/* cyrillic-ext */
@font-face {
font-family: 'JetBrains Mono';
font-style: normal;
font-weight: 400 700;
font-display: swap;
src: url(/fonts/JetBrainsMono-cyrillic-ext.woff2) format('woff2');
unicode-range: U+0460-052F, U+1C80-1C8A, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;
}
/* cyrillic */
@font-face {
font-family: 'JetBrains Mono';
font-style: normal;
font-weight: 400 700;
font-display: swap;
src: url(/fonts/JetBrainsMono-cyrillic.woff2) format('woff2');
unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
}
/* greek */
@font-face {
font-family: 'JetBrains Mono';
font-style: normal;
font-weight: 400 700;
font-display: swap;
src: url(/fonts/JetBrainsMono-greek.woff2) format('woff2');
unicode-range: U+0370-0377, U+037A-037F, U+0384-038A, U+038C, U+038E-03A1, U+03A3-03FF;
}
/* vietnamese */
@font-face {
font-family: 'JetBrains Mono';
font-style: normal;
font-weight: 400 700;
font-display: swap;
src: url(/fonts/JetBrainsMono-vietnamese.woff2) format('woff2');
unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB;
}
/* latin-ext */
@font-face {
font-family: 'JetBrains Mono';
font-style: normal;
font-weight: 400 700;
font-display: swap;
src: url(/fonts/JetBrainsMono-latin-ext.woff2) format('woff2');
unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C4, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
/* latin */
@font-face {
font-family: 'JetBrains Mono';
font-style: normal;
font-weight: 400 700;
font-display: swap;
src: url(/fonts/JetBrainsMono-latin.woff2) format('woff2');
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}

/* Custom CSS variables for textmode.js theme */
:root {
Expand Down Expand Up @@ -73,13 +134,6 @@ body::-webkit-scrollbar-thumb:hover {
display: none !important;
}

/* Hide default hero heading since we have a custom UI */
.VPHome .VPHero .heading,
.VPHome .VPHero h1.text,
.VPHome .VPHero .main .name {
display: none !important;
}

/* Enhanced hero section styles - only apply to home page */
.VPHome .VPHero {
position: relative;
Expand All @@ -97,15 +151,12 @@ body::-webkit-scrollbar-thumb:hover {
.textmode-hero-container {
position: relative;
width: 100%;
height: 400px;
min-height: 400px;
margin: 1.5rem 0;
border-radius: 8px;
overflow: hidden;
background: linear-gradient(135deg, var(--vp-c-bg-soft) 0%, var(--vp-c-bg) 100%);
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
background: #1b1b1f;
isolation: isolate;
}

.textmode-hero-canvas {
Expand All @@ -126,7 +177,7 @@ body::-webkit-scrollbar-thumb:hover {

/* TextmodeHero mobile styles */
.textmode-hero-container {
height: 300px;
min-height: 300px;
margin: 1rem 0;
}
}
Expand Down Expand Up @@ -171,14 +222,6 @@ body::-webkit-scrollbar-thumb:hover {
text-underline-offset: 2px;
}

@media (max-width: 480px) {

/* TextmodeHero mobile styles */
.textmode-hero-container {
height: 250px;
}
}

/* Textmode Sandbox Overlay Styling */
.textmode-sandbox-wrapper {
position: relative;
Expand Down
5 changes: 3 additions & 2 deletions .vitepress/theme/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import DefaultTheme from 'vitepress/theme'
import type { EnhanceAppContext, Theme } from 'vitepress'
import { useData, useRoute } from 'vitepress'
import './custom.css'
import { defineComponent, h } from 'vue'
import { defineAsyncComponent, defineComponent, h } from 'vue'
import codeblocksFold from 'vitepress-plugin-codeblocks-fold'
import 'vitepress-plugin-codeblocks-fold/style/index.css'
import { useCodeSandbox } from 'vitepress-plugin-codesandbox'
Expand Down Expand Up @@ -53,7 +53,7 @@ const HeroLayout = defineComponent({
setup(_, { slots }) {
return () => h(DefaultTheme.Layout, null, {
...slots,
'home-hero-info-after': () => h(TextmodeHero),
'home-hero-info': () => h(TextmodeHero),
'aside-bottom': () => h(AsideLinks),
'doc-after': () => h(DocFooter),
'layout-bottom': () => h('div', [
Expand All @@ -71,6 +71,7 @@ const baseTheme: Theme = {
const { app } = ctx

// Register components
app.component('Mermaid', defineAsyncComponent(() => import('vitepress-plugin-mermaid/Mermaid.vue')))
app.component('TextmodeLiveSandbox', TextmodeLiveSandbox)
app.component('TextmodeSandbox', TextmodeSandbox)
app.component('TextmodeApiSandbox', TextmodeApiSandbox)
Expand Down
4 changes: 3 additions & 1 deletion .vitepress/theme/sketches/hero-wave/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,8 @@ export function createHeroWaveSketch(
tm.draw(() => {
if (!isReady || isDestroyed) return

if (!hasRenderedFrame) {
const firstFrame = !hasRenderedFrame
if (firstFrame) {
hasRenderedFrame = true
schedulePendingResize()
}
Expand All @@ -81,6 +82,7 @@ export function createHeroWaveSketch(
}
renderBrandingOverlay(tm, time, subheaderLines)
}
if (firstFrame) options.onReady?.()
})

const applyResize = (width: number, height: number) => {
Expand Down
2 changes: 2 additions & 0 deletions .vitepress/theme/sketches/hero-wave/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,6 @@ export interface HeroWaveOptions {
* "hero" shows the logo + tagline overlay, "background" only renders the waves.
*/
mode?: 'hero' | 'background'
/** Called after the first complete frame, before playback may be paused. */
onReady?: () => void
}
Loading
Loading