This commit is contained in:
@@ -158,7 +158,7 @@ Markdown 图片默认保持比例并限制在内容区,也可以显式覆盖
|
||||
{fit="cover" position="50% 30%" max-height="65vh"}
|
||||
```
|
||||
|
||||
主题默认使用 `1024 × 768` 的 4:3 画布。表格默认字号为 `30px`;代码默认字号为 `28px`,未指定高度时会从当前位置自动延伸到所在内容区底部,长行会正常换行,超出后只上下滚动。需要按页调整时,在该页 frontmatter 中添加尺寸类:
|
||||
主题默认使用 `1024 × 768` 的 4:3 画布。表格默认字号为 `30px`;代码默认字号为 `28px`,未指定高度时会按照内容使用自然高度,并根据前后正文自动限制在所在内容区内,长行会正常换行,超出后只上下滚动。需要按页调整时,在该页 frontmatter 中添加尺寸类:
|
||||
|
||||
```md
|
||||
---
|
||||
@@ -180,7 +180,7 @@ const slides = await discover('slides')
|
||||
</div>
|
||||
````
|
||||
|
||||
同样的包裹方式也适用于表格,例如 `<div class="easy-table-sm">`。不添加固定高度类时,单个代码框会占满标题或前置正文之后的剩余区域;同一区域有多个代码框时保持内容高度并限制在内容区内。
|
||||
同样的包裹方式也适用于表格,例如 `<div class="easy-table-sm">`。不添加固定高度类时,代码框保持内容的自然高度;内容较长时会自动扣除前后正文占用的空间并限制在内容区内。同一区域有多个代码框时也遵循这一规则。
|
||||
|
||||
公开放映页和演示者预览中的图片可以点击进入全屏预览,也可以聚焦图片后按 `Enter` 或空格打开;通过关闭按钮、点击背景或按 `Esc` 返回幻灯片。预览底部提供 `− / 百分比 / +` 工具栏,100% 对应图片原始像素尺寸;还可以使用鼠标滚轮、触控板或触屏双指缩放,并在放大后拖动查看细节,缩放范围为 10%–400%。
|
||||
|
||||
|
||||
@@ -927,7 +927,8 @@ function hasExplicitCodeHeight(code: HTMLPreElement) {
|
||||
|
||||
function availableCodeHeight(code: HTMLPreElement, region: HTMLElement) {
|
||||
const layout = code.closest<HTMLElement>('.slidev-layout')
|
||||
if (!layout || layout.clientHeight <= 0)
|
||||
const host = code.closest<HTMLElement>('.easy-content-code-host')
|
||||
if (!layout || !host || layout.clientHeight <= 0)
|
||||
return 0
|
||||
const layoutRect = layout.getBoundingClientRect()
|
||||
const regionRect = region.getBoundingClientRect()
|
||||
@@ -941,7 +942,33 @@ function availableCodeHeight(code: HTMLPreElement, region: HTMLElement) {
|
||||
const layoutPaddingBottom = Number.parseFloat(getComputedStyle(layout).paddingBottom) || 0
|
||||
const regionBottom = regionRect.bottom - regionPaddingBottom * scale
|
||||
const layoutBottom = layoutRect.bottom - layoutPaddingBottom * scale
|
||||
return Math.max(0, Math.floor((Math.min(regionBottom, layoutBottom) - codeRect.top) / scale))
|
||||
const contentBottom = Math.min(regionBottom, layoutBottom)
|
||||
let trailingBottom = codeRect.bottom
|
||||
|
||||
region.querySelectorAll<HTMLElement>('*').forEach((element) => {
|
||||
if (host.contains(element) || !(host.compareDocumentPosition(element) & Node.DOCUMENT_POSITION_FOLLOWING))
|
||||
return
|
||||
const style = getComputedStyle(element)
|
||||
if (style.display === 'none' || style.position === 'absolute' || style.position === 'fixed')
|
||||
return
|
||||
const rect = element.getBoundingClientRect()
|
||||
if (rect.width <= 0 || rect.height <= 0)
|
||||
return
|
||||
const marginBottom = Number.parseFloat(style.marginBottom) || 0
|
||||
trailingBottom = Math.max(trailingBottom, rect.bottom + marginBottom * scale)
|
||||
})
|
||||
|
||||
const trailingHeight = Math.max(0, trailingBottom - codeRect.bottom)
|
||||
const codeStyle = getComputedStyle(code)
|
||||
const minimumHeight = (
|
||||
(Number.parseFloat(codeStyle.lineHeight) || 0)
|
||||
+ (Number.parseFloat(codeStyle.paddingTop) || 0)
|
||||
+ (Number.parseFloat(codeStyle.paddingBottom) || 0)
|
||||
+ (Number.parseFloat(codeStyle.borderTopWidth) || 0)
|
||||
+ (Number.parseFloat(codeStyle.borderBottomWidth) || 0)
|
||||
)
|
||||
const available = (contentBottom - codeRect.top - trailingHeight) / scale
|
||||
return Math.max(1, Math.floor(Math.max(minimumHeight, available)))
|
||||
}
|
||||
|
||||
function setHostLength(host: HTMLElement, property: string, value?: number) {
|
||||
@@ -961,7 +988,6 @@ function fitCodeHeights() {
|
||||
if (!host)
|
||||
return
|
||||
if (hasExplicitCodeHeight(code)) {
|
||||
setHostLength(host, '--easy-code-auto-height')
|
||||
setHostLength(host, '--easy-code-auto-max-height')
|
||||
return
|
||||
}
|
||||
@@ -974,7 +1000,6 @@ function fitCodeHeights() {
|
||||
})
|
||||
|
||||
groups.forEach((codes, region) => {
|
||||
const single = codes.length === 1
|
||||
codes.forEach((code) => {
|
||||
const host = code.closest<HTMLElement>('.easy-content-code-host')
|
||||
if (!host)
|
||||
@@ -982,7 +1007,6 @@ function fitCodeHeights() {
|
||||
const available = availableCodeHeight(code, region)
|
||||
if (!available)
|
||||
return
|
||||
setHostLength(host, '--easy-code-auto-height', single ? available : undefined)
|
||||
setHostLength(host, '--easy-code-auto-max-height', available)
|
||||
})
|
||||
})
|
||||
@@ -1048,7 +1072,6 @@ function cleanupCodeHosts() {
|
||||
const base = code && codeStyles.get(code)
|
||||
host.querySelector(':scope > .easy-content-scale-toolbar')?.remove()
|
||||
host.style.removeProperty('--easy-code-scaled-font-size')
|
||||
host.style.removeProperty('--easy-code-auto-height')
|
||||
host.style.removeProperty('--easy-code-auto-max-height')
|
||||
host.classList.remove('easy-content-scale-host', 'easy-content-code-host')
|
||||
delete host.dataset.easyScaleKey
|
||||
|
||||
@@ -135,8 +135,8 @@
|
||||
|
||||
.slidev-layout pre {
|
||||
box-sizing: border-box;
|
||||
height: var(--easy-code-height, var(--easy-code-auto-height, auto));
|
||||
max-height: var(--easy-code-height, var(--easy-code-auto-max-height, var(--easy-code-auto-height, none)));
|
||||
height: var(--easy-code-height, auto);
|
||||
max-height: var(--easy-code-height, var(--easy-code-auto-max-height, none));
|
||||
overflow-x: hidden;
|
||||
overflow-y: auto;
|
||||
border-radius: 10px;
|
||||
|
||||
@@ -144,6 +144,25 @@ layout: default
|
||||
表格和代码尺寸既可以按页设置,也可以用 div 包裹后只调整一个内容块。
|
||||
-->
|
||||
|
||||
---
|
||||
layout: default
|
||||
---
|
||||
|
||||
## 智能代码高度
|
||||
|
||||
```text
|
||||
用户提问 → 模型回答
|
||||
|
||||
看起来:一次请求,一台“AI”
|
||||
实际上:网关 → 调度 → 推理 → 检索 → 存储
|
||||
```
|
||||
|
||||
### 代码之后的正文仍保留在内容区内
|
||||
|
||||
<!--
|
||||
未指定高度的代码框按照内容自然伸缩,并为后续正文保留空间。
|
||||
-->
|
||||
|
||||
---
|
||||
layout: quote
|
||||
---
|
||||
|
||||
@@ -927,7 +927,8 @@ function hasExplicitCodeHeight(code: HTMLPreElement) {
|
||||
|
||||
function availableCodeHeight(code: HTMLPreElement, region: HTMLElement) {
|
||||
const layout = code.closest<HTMLElement>('.slidev-layout')
|
||||
if (!layout || layout.clientHeight <= 0)
|
||||
const host = code.closest<HTMLElement>('.easy-content-code-host')
|
||||
if (!layout || !host || layout.clientHeight <= 0)
|
||||
return 0
|
||||
const layoutRect = layout.getBoundingClientRect()
|
||||
const regionRect = region.getBoundingClientRect()
|
||||
@@ -941,7 +942,33 @@ function availableCodeHeight(code: HTMLPreElement, region: HTMLElement) {
|
||||
const layoutPaddingBottom = Number.parseFloat(getComputedStyle(layout).paddingBottom) || 0
|
||||
const regionBottom = regionRect.bottom - regionPaddingBottom * scale
|
||||
const layoutBottom = layoutRect.bottom - layoutPaddingBottom * scale
|
||||
return Math.max(0, Math.floor((Math.min(regionBottom, layoutBottom) - codeRect.top) / scale))
|
||||
const contentBottom = Math.min(regionBottom, layoutBottom)
|
||||
let trailingBottom = codeRect.bottom
|
||||
|
||||
region.querySelectorAll<HTMLElement>('*').forEach((element) => {
|
||||
if (host.contains(element) || !(host.compareDocumentPosition(element) & Node.DOCUMENT_POSITION_FOLLOWING))
|
||||
return
|
||||
const style = getComputedStyle(element)
|
||||
if (style.display === 'none' || style.position === 'absolute' || style.position === 'fixed')
|
||||
return
|
||||
const rect = element.getBoundingClientRect()
|
||||
if (rect.width <= 0 || rect.height <= 0)
|
||||
return
|
||||
const marginBottom = Number.parseFloat(style.marginBottom) || 0
|
||||
trailingBottom = Math.max(trailingBottom, rect.bottom + marginBottom * scale)
|
||||
})
|
||||
|
||||
const trailingHeight = Math.max(0, trailingBottom - codeRect.bottom)
|
||||
const codeStyle = getComputedStyle(code)
|
||||
const minimumHeight = (
|
||||
(Number.parseFloat(codeStyle.lineHeight) || 0)
|
||||
+ (Number.parseFloat(codeStyle.paddingTop) || 0)
|
||||
+ (Number.parseFloat(codeStyle.paddingBottom) || 0)
|
||||
+ (Number.parseFloat(codeStyle.borderTopWidth) || 0)
|
||||
+ (Number.parseFloat(codeStyle.borderBottomWidth) || 0)
|
||||
)
|
||||
const available = (contentBottom - codeRect.top - trailingHeight) / scale
|
||||
return Math.max(1, Math.floor(Math.max(minimumHeight, available)))
|
||||
}
|
||||
|
||||
function setHostLength(host: HTMLElement, property: string, value?: number) {
|
||||
@@ -961,7 +988,6 @@ function fitCodeHeights() {
|
||||
if (!host)
|
||||
return
|
||||
if (hasExplicitCodeHeight(code)) {
|
||||
setHostLength(host, '--easy-code-auto-height')
|
||||
setHostLength(host, '--easy-code-auto-max-height')
|
||||
return
|
||||
}
|
||||
@@ -974,7 +1000,6 @@ function fitCodeHeights() {
|
||||
})
|
||||
|
||||
groups.forEach((codes, region) => {
|
||||
const single = codes.length === 1
|
||||
codes.forEach((code) => {
|
||||
const host = code.closest<HTMLElement>('.easy-content-code-host')
|
||||
if (!host)
|
||||
@@ -982,7 +1007,6 @@ function fitCodeHeights() {
|
||||
const available = availableCodeHeight(code, region)
|
||||
if (!available)
|
||||
return
|
||||
setHostLength(host, '--easy-code-auto-height', single ? available : undefined)
|
||||
setHostLength(host, '--easy-code-auto-max-height', available)
|
||||
})
|
||||
})
|
||||
@@ -1048,7 +1072,6 @@ function cleanupCodeHosts() {
|
||||
const base = code && codeStyles.get(code)
|
||||
host.querySelector(':scope > .easy-content-scale-toolbar')?.remove()
|
||||
host.style.removeProperty('--easy-code-scaled-font-size')
|
||||
host.style.removeProperty('--easy-code-auto-height')
|
||||
host.style.removeProperty('--easy-code-auto-max-height')
|
||||
host.classList.remove('easy-content-scale-host', 'easy-content-code-host')
|
||||
delete host.dataset.easyScaleKey
|
||||
|
||||
@@ -135,8 +135,8 @@
|
||||
|
||||
.slidev-layout pre {
|
||||
box-sizing: border-box;
|
||||
height: var(--easy-code-height, var(--easy-code-auto-height, auto));
|
||||
max-height: var(--easy-code-height, var(--easy-code-auto-max-height, var(--easy-code-auto-height, none)));
|
||||
height: var(--easy-code-height, auto);
|
||||
max-height: var(--easy-code-height, var(--easy-code-auto-max-height, none));
|
||||
overflow-x: hidden;
|
||||
overflow-y: auto;
|
||||
border-radius: 10px;
|
||||
|
||||
+3
-1
@@ -65,7 +65,9 @@ describe('theme defaults', () => {
|
||||
expect(themePackage.dependencies['@slidev/client']).toBe('52.19.1')
|
||||
expect(themeCss).toContain('--easy-accent: #1d4ed8')
|
||||
expect(themeCss.slice(0, themeCss.indexOf('.easy-table-sm'))).not.toContain('--easy-code-height')
|
||||
expect(themeCss).toContain('var(--easy-code-auto-height, auto)')
|
||||
expect(themeCss).toContain('height: var(--easy-code-height, auto)')
|
||||
expect(themeCss).toContain('var(--easy-code-auto-max-height, none)')
|
||||
expect(themeCss).not.toContain('--easy-code-auto-height')
|
||||
expect(themeCss).toContain('--easy-code-scaled-font-size')
|
||||
expect(themeCss).toContain('overflow-x: hidden')
|
||||
expect(themeCss).toContain('white-space: pre-wrap')
|
||||
|
||||
+44
-21
@@ -261,14 +261,14 @@ test('presenter advances animations, switches notes, shows timing, and syncs the
|
||||
await presenter.getByTitle('Go to next slide').click()
|
||||
await expect(page.getByRole('heading', { name: '内容优先' })).toBeVisible()
|
||||
await expect(presenter.getByText('依次介绍四个核心能力')).toBeVisible()
|
||||
await expect(presenter.getByText('2 / 8', { exact: true })).toBeVisible()
|
||||
await expect(presenter.getByText('2 / 9', { exact: true })).toBeVisible()
|
||||
|
||||
await presenter.getByTitle('Go to next slide').click()
|
||||
await expect(presenter.getByText('1/4', { exact: true })).toBeVisible()
|
||||
await expect(page).toHaveURL(/#\/2\?clicks=1$/)
|
||||
})
|
||||
|
||||
test('table and code controls adjust font size while code fills the available height', async ({ page }) => {
|
||||
test('table and code controls adjust font size while code uses smart height', async ({ page }) => {
|
||||
await page.goto('/getting-started/#/7')
|
||||
const table = page.locator('.slidev-layout table').first()
|
||||
const tablePreset = table.locator('..').locator('..')
|
||||
@@ -311,15 +311,8 @@ test('table and code controls adjust font size while code fills the available he
|
||||
await expect.poll(() => code.evaluate(element => Number.parseFloat(getComputedStyle(element).fontSize))).toBe(initialCodeSize)
|
||||
|
||||
await expect.poll(() => code.evaluate((element) => {
|
||||
const region = element.closest('.easy-two-cols-grid > div')!
|
||||
const layout = element.closest('.slidev-layout') as HTMLElement
|
||||
const scale = layout.getBoundingClientRect().height / layout.clientHeight
|
||||
const contentBottom = Math.min(
|
||||
region.getBoundingClientRect().bottom - Number.parseFloat(getComputedStyle(region).paddingBottom) * scale,
|
||||
layout.getBoundingClientRect().bottom - Number.parseFloat(getComputedStyle(layout).paddingBottom) * scale,
|
||||
)
|
||||
return Math.abs(contentBottom - element.getBoundingClientRect().bottom) / scale
|
||||
})).toBeLessThanOrEqual(3)
|
||||
return element.scrollHeight - element.clientHeight
|
||||
})).toBeGreaterThan(0)
|
||||
const overflow = await code.evaluate(element => ({
|
||||
horizontal: getComputedStyle(element).overflowX,
|
||||
lineHeight: Number.parseFloat(getComputedStyle(element).lineHeight),
|
||||
@@ -365,16 +358,46 @@ test('table and code controls adjust font size while code fills the available he
|
||||
await layout.evaluate((element) => {
|
||||
element.classList.remove('easy-code-height-sm', 'easy-code-height-md', 'easy-code-height-lg')
|
||||
})
|
||||
await expect.poll(() => code.evaluate(element => element.scrollHeight - element.clientHeight)).toBeGreaterThan(0)
|
||||
})
|
||||
|
||||
test('auto-height code keeps its natural height and reserves room for following content', async ({ page }) => {
|
||||
await page.goto('/getting-started/#/8')
|
||||
const layout = page.locator('.slidev-page-8 .slidev-layout')
|
||||
const code = layout.locator('pre').first()
|
||||
const following = layout.getByRole('heading', { name: '代码之后的正文仍保留在内容区内' })
|
||||
|
||||
await expect(following).toBeVisible()
|
||||
await expect.poll(() => code.evaluate((element) => {
|
||||
const region = element.closest('.easy-two-cols-grid > div')!
|
||||
const layout = element.closest('.slidev-layout') as HTMLElement
|
||||
const scale = layout.getBoundingClientRect().height / layout.clientHeight
|
||||
const contentBottom = Math.min(
|
||||
region.getBoundingClientRect().bottom - Number.parseFloat(getComputedStyle(region).paddingBottom) * scale,
|
||||
layout.getBoundingClientRect().bottom - Number.parseFloat(getComputedStyle(layout).paddingBottom) * scale,
|
||||
)
|
||||
return Math.abs(contentBottom - element.getBoundingClientRect().bottom) / scale
|
||||
})).toBeLessThanOrEqual(3)
|
||||
const layoutElement = element.closest('.slidev-layout') as HTMLElement
|
||||
const scale = layoutElement.getBoundingClientRect().height / layoutElement.clientHeight
|
||||
return Math.abs(element.getBoundingClientRect().height / scale - element.scrollHeight)
|
||||
})).toBeLessThanOrEqual(1)
|
||||
const bounds = await layout.evaluate((element) => {
|
||||
const codeElement = element.querySelector('pre')!
|
||||
const followingElement = Array.from(element.querySelectorAll('h3')).find(item => item.textContent?.includes('代码之后的正文'))!
|
||||
return {
|
||||
codeBottom: codeElement.getBoundingClientRect().bottom,
|
||||
followingBottom: followingElement.getBoundingClientRect().bottom,
|
||||
followingTop: followingElement.getBoundingClientRect().top,
|
||||
layoutBottom: element.getBoundingClientRect().bottom - Number.parseFloat(getComputedStyle(element).paddingBottom),
|
||||
}
|
||||
})
|
||||
expect(bounds.codeBottom).toBeLessThan(bounds.followingTop)
|
||||
expect(bounds.followingBottom).toBeLessThanOrEqual(bounds.layoutBottom + 1)
|
||||
|
||||
await code.evaluate((element) => {
|
||||
element.textContent = Array.from({ length: 40 }, (_, index) => `line ${index + 1}: a deliberately long code sample`).join('\n')
|
||||
})
|
||||
await expect.poll(() => code.evaluate(element => element.scrollHeight - element.clientHeight)).toBeGreaterThan(100)
|
||||
const longContentBounds = await layout.evaluate((element) => {
|
||||
const followingElement = Array.from(element.querySelectorAll('h3')).find(item => item.textContent?.includes('代码之后的正文'))!
|
||||
return {
|
||||
followingBottom: followingElement.getBoundingClientRect().bottom,
|
||||
layoutBottom: element.getBoundingClientRect().bottom - Number.parseFloat(getComputedStyle(element).paddingBottom),
|
||||
}
|
||||
})
|
||||
expect(longContentBounds.followingBottom).toBeLessThanOrEqual(longContentBounds.layoutBottom + 1)
|
||||
})
|
||||
|
||||
test('image fullscreen preserves layout and table scaling stays out of non-interactive routes', async ({ page }) => {
|
||||
@@ -601,7 +624,7 @@ test('stacked image layouts constrain wide images by available height', async ({
|
||||
|
||||
test('sample slides stay inside the 4:3 canvas', async ({ page }) => {
|
||||
await page.goto('/getting-started/#/1')
|
||||
for (let slide = 1; slide <= 8; slide += 1) {
|
||||
for (let slide = 1; slide <= 9; slide += 1) {
|
||||
const layout = page.locator('.slidev-layout').first()
|
||||
await expect(layout).toBeVisible()
|
||||
const box = await layout.boundingBox()
|
||||
|
||||
Reference in New Issue
Block a user