Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
15 commits
Select commit Hold shift + click to select a range
2115e0f
fix(tutorial): improve pager overflow detection and scroll positioning
nschimme Sep 21, 2026
11e92a0
fix(tutorial): improve pager overflow detection, image loading, and s…
nschimme Sep 21, 2026
3255859
fix(tutorial): improve pager overflow, image loading, and scroll posi…
nschimme Sep 21, 2026
d1c6c84
fix(tutorial): improve pager overflow, image loading, and scroll posi…
nschimme Sep 21, 2026
2ae9b26
fix(tutorial): improve pager overflow, image loading, and scroll posi…
nschimme Sep 21, 2026
cdf1d5e
fix(tutorial): improve terminal pagination and mobile touch UX
nschimme Sep 21, 2026
ecae955
fix(tutorial): enable scroll-to-top and pager for chapter intro content
nschimme Sep 21, 2026
a05eaef
fix(tutorial): implement state machine for pager transitions and impr…
nschimme Sep 21, 2026
a8a55c2
fix(tutorial): normalize Chapter Complete prompt bar UX and state tra…
nschimme Sep 21, 2026
dd49ba1
Fix tutorial player output pagination and top scroll alignment
nschimme Sep 21, 2026
6baad6f
fix: resolve tutorial output pagination, scroll alignment, and prompt…
nschimme Sep 21, 2026
66e3bbf
fix: resolve tutorial output pagination and polish chapter completion…
nschimme Sep 21, 2026
ff73c02
fix(tutorial): introduce pager state machine & improve overflow scrol…
nschimme Sep 21, 2026
2e64d74
fix(tutorial): refine markdown link escaping and scroll completion de…
nschimme Sep 21, 2026
69767bd
fix(tutorial): ensure Enter key advances pagination and chapter compl…
nschimme Sep 21, 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
599 changes: 437 additions & 162 deletions docs/.vitepress/theme/components/TutorialPlayer.vue

Large diffs are not rendered by default.

85 changes: 85 additions & 0 deletions docs/.vitepress/theme/utils/markdown.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
/**
* Utility functions for inline markdown formatting in tutorial elements.
*/

export type WithBaseFn = (url: string) => string

/**
* Formats inline markdown strings into HTML safely.
* Supports:
* - HTML entity escaping for & < >
* - Embedded images: ![alt](src) with lazy loading, aspect ratio placeholders, and lightbox triggers
* - Links: [text](url)
* - Bold: **text** or __text__
* - Italics: *text* or _text_
* - Inline code: `code`
* - Line breaks: \n -> <br>
*/
export function formatInlineMarkdown(text: string | null | undefined, withBaseFn: WithBaseFn = (url) => url): string {
if (!text) return ''

// Escape HTML entities
let safe = text
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')

// Images: ![alt](src)
safe = safe.replace(/!\[([^\]]*)\]\(([^)]+)\)/g, (_match, alt, src) => {
const cleanAlt = alt.replace(/"/g, '&quot;')
let cleanSrc = src.trim().replace(/"/g, '&quot;')

if (!/^(?:https?:\/\/|data:)/i.test(cleanSrc)) {
if (cleanSrc.includes('assets/')) {
cleanSrc = '/assets/' + cleanSrc.split('assets/')[1]
}

if (cleanSrc.startsWith('/') && typeof withBaseFn === 'function') {
cleanSrc = withBaseFn(cleanSrc)
}
}

return `<span class="tut-img-container"><img src="${cleanSrc}" alt="${cleanAlt}" class="tut-img-embed" loading="lazy" onload="this.dispatchEvent(new Event('load', { bubbles: true }))" /></span>`
})

// Links: [text](url)
safe = safe.replace(/\[([^\]]+)\]\(([^)]+)\)/g, (_match, linkText, url) => {
const cleanText = linkText
const trimmed = url.trim()

// Validate safe schemes: http, https, mailto, relative paths, or hash anchors
const isHttp = /^https?:\/\//i.test(trimmed)
const isMailto = /^mailto:/i.test(trimmed)
const isRelativeOrHash = /^(?:\/|\.\.|\.|\#)/.test(trimmed)

if (!isHttp && !isMailto && !isRelativeOrHash) {
return cleanText
}

let finalUrl = trimmed
if (finalUrl.startsWith('/') && typeof withBaseFn === 'function') {
finalUrl = withBaseFn(finalUrl)
}

// Escape URL for HTML attribute insertion
const escapedUrl = finalUrl
.replace(/"/g, '&quot;')
.replace(/'/g, '&#39;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')

return `<a href="${escapedUrl}" ${isHttp ? 'target="_blank" rel="noopener noreferrer"' : ''} class="tut-link">${cleanText}</a>`
})

// Bold: **text** or __text__
safe = safe.replace(/(\*\*|__)(.*?)\1/g, '<strong>$2</strong>')

// Italics: *text* or _text_
safe = safe.replace(/(?<!\w)(\*|_)(.*?)\1(?!\w)/g, '<em>$2</em>')

// Code: `code`
safe = safe.replace(/`([^`]+)`/g, '<code class="tut-cmd-inline">$1</code>')

// Newlines
return safe.replace(/\n/g, '<br>')
}
21 changes: 21 additions & 0 deletions docs/.vitepress/theme/utils/pager.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
/**
* Helper function to determine pager overflow state for scrollable containers.
* Uses sub-pixel tolerance to handle high-DPI displays and browser zoom.
*/

export function isOverflowActive(
container: HTMLElement | null,
scrollThreshold: number = 40,
remainingThreshold: number = 35
): boolean {
if (!container) return false
const scrollHeight = container.scrollHeight
const clientHeight = container.clientHeight
const scrollTop = container.scrollTop

const scrollableDistance = scrollHeight - clientHeight
// Use Math.ceil to protect against sub-pixel fractional scrolling discrepancies
const remainingScroll = scrollableDistance - Math.ceil(scrollTop)

return scrollableDistance > scrollThreshold && remainingScroll > remainingThreshold
}
43 changes: 43 additions & 0 deletions docs/.vitepress/theme/utils/tutorial.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
/**
* Utility functions for tutorial routing and chapter calculations.
*/

export enum PlayerState {
AWAITING_COMMAND = 'AWAITING_COMMAND',
PAGING_OUTPUT = 'PAGING_OUTPUT',
PLAYING_BEAT = 'PLAYING_BEAT',
CHAPTER_COMPLETE = 'CHAPTER_COMPLETE'
}

export interface Chapter {
chapterNum: number
title: string
url: string
filename: string
teach?: Array<{ command: string; desc: string }>
steps?: any[]
practice?: string
[key: string]: any
}

/**
* Normalizes route path to match chapter URLs, removing base prefix and trailing slashes.
*/
export function normalizeChapterPath(rawPath: string | undefined, basePrefix: string = '/'): string {
if (!rawPath) return '/'
let currentPath = rawPath.replace(/\.html$/, '').replace(/\/$/, '')
const base = basePrefix || '/'
const basePath = base.replace(/\/$/, '')
if (base !== '/' && basePath && (currentPath === basePath || currentPath.startsWith(basePath + '/'))) {
currentPath = '/' + currentPath.slice(basePath.length).replace(/^\//, '')
}
Comment thread
sourcery-ai[bot] marked this conversation as resolved.
return currentPath || '/'
}

/**
* Finds current chapter object from all chapters list.
*/
export function findChapterByPath(chapters: Chapter[], normalizedPath: string): Chapter | null {
if (!chapters || !chapters.length) return null
return chapters.find(c => c.url === normalizedPath || normalizedPath.endsWith(c.filename)) || chapters[0] || null
}
Loading
Loading