1
0
Fork 0
forked from jan/psslides

refactor css + add dark mode + maybe some other stuff idk dont remember what else i did

This commit is contained in:
jan 2026-09-24 20:18:18 +02:00
parent 22e17a92d4
commit e309455edc
5 changed files with 368 additions and 53 deletions

View file

@ -11,7 +11,13 @@
<body>
<div class="container">
<div class="menubar"><b>p/slides</b></div>
<div class="slidelist"></div>
<div class="slidelist">
<div class="slide-list-header">
<span>Slides</span>
<button class="add-slide" type="button" aria-label="Add slide">+</button>
</div>
<div class="slide-list-items"></div>
</div>
<div class="code"></div>
<div class="preview">
<div class="slide"></div>

View file

@ -11,9 +11,11 @@ export function codeToElement(
options: CodeToElementOptions = {}
): HTMLIFrameElement {
const iframe = document.createElement('iframe')
const sandbox = new Set<string>(['allow-same-origin'])
const sandbox = new Set<string>()
if (options.allowScripts) {
if (!options.allowScripts) {
sandbox.add('allow-same-origin')
} else {
sandbox.add('allow-scripts')
}
@ -28,7 +30,7 @@ export async function createCodeThumbnail(
code: string,
options: CodeToElementOptions = {}
): Promise<HTMLImageElement> {
const iframe = codeToElement(code, { title: 'Code thumbnail', ...options })
const iframe = codeToElement(code, { title: 'Code thumbnail', ...options, allowScripts: false })
iframe.style.width = '640px'
iframe.style.height = '360px'
iframe.style.position = 'fixed'
@ -36,13 +38,19 @@ export async function createCodeThumbnail(
iframe.style.border = '0'
document.body.appendChild(iframe)
await new Promise<void>((resolve) => iframe.addEventListener('load', () => resolve(), { once: true }))
try {
await new Promise<void>((resolve, reject) => {
iframe.addEventListener('load', () => resolve(), { once: true })
iframe.addEventListener('error', () => reject(new Error('Unable to render code thumbnail')), { once: true })
})
const content = iframe.contentDocument?.body
const thumbnail = document.createElement('img')
if (content) {
thumbnail.src = domToSvgDataUrl(content, { width: 640, height: 360 })
}
iframe.remove()
return thumbnail
} finally {
iframe.remove()
}
}

View file

@ -27,7 +27,7 @@ export function createEditor() {
{
value: 'test',
language: 'html',
theme: 'vs-light',
theme: 'vs-dark',
automaticLayout: true
}
)

View file

@ -15,8 +15,21 @@ const slides: Slide[] = [
]
const editor = createEditor()
const slideList = document.querySelector<HTMLElement>('.slidelist')!
const slideList = document.querySelector<HTMLElement>('.slide-list-items')!
const addSlideButton = document.querySelector<HTMLButtonElement>('.add-slide')!
let currentSlide = 0
let draggedSlide: number | null = null
let globalSlideElement: HTMLElement | null = null
const slideElements = new Map<Slide, HTMLElement>()
const thumbnailRequests = new Map<number, number>()
let thumbnailDebounceTimer: number | undefined
function updateSelection() {
slideList.querySelectorAll<HTMLElement>('.slide-list-item').forEach((element) => {
const index = Number(element.dataset.slideId)
element.classList.toggle('selected', index === currentSlide)
})
}
function setSlide(index: number) {
const slide = index !== -1 ? slides[index] : { code: globalSlide }
@ -26,58 +39,214 @@ function setSlide(index: number) {
currentSlide = index
editor.getModel()?.setValue(slide.code)
updateSelection()
}
async function generateSlideThumbnail(slide: any, i: number) {
async function generateSlideThumbnail(slide: Slide, i: number) {
const slideElement = document.createElement('div')
slideElement.classList.add('slide-list-item')
slideElement.dataset.slideId = i.toString()
if (i === -1) {
slideElement.classList.add('global-slide')
} else {
slideElement.draggable = true
slideElements.set(slide, slideElement)
}
const slideInList = document.createElement('button')
slideInList.type = 'button'
slideInList.classList.add('slide-in-list')
slideInList.setAttribute('aria-label', i === -1 ? 'Global slide' : `Slide ${i + 1}`)
slideInList.addEventListener('click', () => {
setSlide(i)
setSlide(Number(slideElement.dataset.slideId))
})
slideInList.dataset.slideId = i.toString()
const slideNumber = document.createElement('span')
slideNumber.classList.add('slide-number')
slideNumber.textContent = i === -1 ? 'G' : `${i + 1}`
const thumbnail = await createCodeThumbnail(slide.code)
slideInList.appendChild(thumbnail)
if (i === -1) {
const p = document.createElement("p")
p.innerText = "(global)"
slideInList.appendChild(p)
slideInList.append(slideNumber, thumbnail)
slideElement.appendChild(slideInList)
if (i !== -1) {
const deleteButton = document.createElement('button')
deleteButton.type = 'button'
deleteButton.classList.add('delete-slide')
deleteButton.setAttribute('aria-label', `Delete slide ${i + 1}`)
deleteButton.textContent = '×'
deleteButton.addEventListener('click', () => {
deleteSlide(Number(slideElement.dataset.slideId))
})
slideElement.appendChild(deleteButton)
}
return slideInList
if (i === -1) {
globalSlideElement = slideElement
}
if (i === currentSlide) {
slideElement.classList.add('selected')
}
return slideElement
}
async function refreshThumbnail(index: number, slide: Slide) {
const request = (thumbnailRequests.get(index) ?? 0) + 1
thumbnailRequests.set(index, request)
const oldSlideElement = index === -1 ? globalSlideElement : slideElements.get(slide)
const slideElement = await generateSlideThumbnail(slide, index)
if (thumbnailRequests.get(index) !== request) {
return
}
oldSlideElement?.replaceWith(slideElement)
if (index === -1) {
globalSlideElement = slideElement
} else {
slideElements.set(slide, slideElement)
}
updateSelection()
}
async function setSlideList() {
const slideListSlidesList = [{ code: globalSlide }, ...slides]
slideList.replaceChildren()
slideElements.clear()
globalSlideElement = null
for (let i = -1; i < slides.length; i++) {
const slideInList = await generateSlideThumbnail(slideListSlidesList[i + 1], i)
slideList.appendChild(slideInList)
slideList.appendChild(await generateSlideThumbnail(slideListSlidesList[i + 1], i))
}
updateSelection()
}
editor.onDidChangeModelContent(async () => {
const code = editor.getValue()
let slide = null
if (currentSlide === -1) {
globalSlide = code
slide = { code }
} else {
slides[currentSlide].code = code
slide = slides[currentSlide]
function updateSlideOrder() {
if (globalSlideElement) {
globalSlideElement.dataset.slideId = '-1'
slideList.appendChild(globalSlideElement)
}
const slideEl = document.querySelector(`[data-slide-id="${currentSlide}"]`)
const slthumb = await generateSlideThumbnail(slide, currentSlide)
slideEl?.replaceWith(slthumb)
for (const [index, slide] of slides.entries()) {
const slideElement = slideElements.get(slide)
if (!slideElement) {
continue
}
slideElement.dataset.slideId = index.toString()
const slideButton = slideElement.querySelector<HTMLButtonElement>('.slide-in-list')
slideButton?.setAttribute('aria-label', `Slide ${index + 1}`)
const slideNumber = slideElement.querySelector<HTMLElement>('.slide-number')
if (slideNumber) {
slideNumber.textContent = `${index + 1}`
}
slideElement.querySelector<HTMLButtonElement>('.delete-slide')?.setAttribute('aria-label', `Delete slide ${index + 1}`)
slideList.appendChild(slideElement)
}
updateSelection()
}
function moveSlide(from: number, to: number) {
if (from === to || from < 0 || to < 0) {
return
}
const [movedSlide] = slides.splice(from, 1)
slides.splice(to, 0, movedSlide)
if (currentSlide === from) {
currentSlide = to
} else if (from < currentSlide && to >= currentSlide) {
currentSlide--
} else if (from > currentSlide && to <= currentSlide) {
currentSlide++
}
updateSlideOrder()
editor.getModel()?.setValue(slides[currentSlide]?.code ?? globalSlide)
}
function deleteSlide(index: number) {
if (index < 0 || slides.length <= 1) {
return
}
const [deletedSlide] = slides.splice(index, 1)
slideElements.get(deletedSlide)?.remove()
slideElements.delete(deletedSlide)
if (index < currentSlide) {
currentSlide--
} else if (index === currentSlide) {
currentSlide = Math.min(currentSlide, slides.length - 1)
}
updateSlideOrder()
setSlide(currentSlide)
}
addSlideButton.addEventListener('click', async () => {
const slide = { code: '' }
slides.push(slide)
const slideElement = await generateSlideThumbnail(slide, slides.length - 1)
slideList.appendChild(slideElement)
updateSlideOrder()
setSlide(slides.length - 1)
})
function render() {
setSlideList()
slideList.addEventListener('dragstart', (event) => {
const slideElement = (event.target as HTMLElement).closest<HTMLElement>('.slide-list-item')
const index = slideElement ? Number(slideElement.dataset.slideId) : -1
if (index < 0) {
event.preventDefault()
return
}
draggedSlide = index
event.dataTransfer?.setData('text/plain', index.toString())
})
slideList.addEventListener('dragover', (event) => {
if (draggedSlide !== null) {
event.preventDefault()
}
})
slideList.addEventListener('drop', (event) => {
event.preventDefault()
const target = (event.target as HTMLElement).closest<HTMLElement>('.slide-list-item')
const targetIndex = target ? Number(target.dataset.slideId) : -1
if (draggedSlide !== null && targetIndex >= 0) {
moveSlide(draggedSlide, targetIndex)
}
draggedSlide = null
})
slideList.addEventListener('dragend', () => {
draggedSlide = null
})
editor.onDidChangeModelContent(() => {
const code = editor.getValue()
const slideIndex = currentSlide
const slide = slideIndex === -1 ? { code } : slides[slideIndex]
if (slideIndex === -1) {
globalSlide = code
} else {
slides[slideIndex].code = code
}
if (thumbnailDebounceTimer !== undefined) {
window.clearTimeout(thumbnailDebounceTimer)
}
thumbnailDebounceTimer = window.setTimeout(() => {
thumbnailDebounceTimer = undefined
void refreshThumbnail(slideIndex, slide)
}, 250)
})
async function render() {
await setSlideList()
setSlide(currentSlide)
}

View file

@ -1,17 +1,34 @@
:root {
--border-color: #3f3f3f
color-scheme: dark;
--bg: #181818;
--bg-raised: #202020;
--bg-sunken: #101010;
--text: #e8e8e8;
--text-muted: #a0a0a0;
--border-color: #3f3f3f;
--accent: #008800;
--accent-text: #ffffff;
--slide-bg: #ffffff;
--slide-border: #000000;
}
body {
margin: 0;
font-family: sans-serif;
background: var(--bg);
color: var(--text);
}
.container {
display: grid;
grid-template-columns: minmax(0, 0.6fr) minmax(0, 40%) minmax(0, 1fr);
grid-template-rows: 0.03fr 1fr;
gap: 0px 0px;
gap: 0;
grid-auto-flow: row;
grid-template-areas:
"menubar menubar menubar"
@ -22,8 +39,8 @@ body {
.menubar {
grid-area: menubar;
background-color: #008800;
color: white;
background-color: var(--accent);
color: var(--accent-text);
display: flex;
align-items: center;
padding-left: 1rem;
@ -34,13 +51,55 @@ body {
.slidelist {
grid-area: slidelist;
min-width: 0;
background-color: var(--bg);
border-right: 1px solid var(--border-color);
padding: 2rem;
overflow-y: auto;
}
.slide-list-header {
display: flex;
align-items: center;
justify-content: space-between;
min-height: 3rem;
padding: 0 1rem;
border-bottom: 1px solid var(--border-color);
color: var(--text);
font-weight: 600;
}
.add-slide {
width: 1.75rem;
height: 1.75rem;
padding: 0;
border: 1px solid var(--border-color);
background: var(--bg-raised);
color: var(--text);
font-size: 1.1rem;
line-height: 1;
cursor: pointer;
}
.add-slide:hover,
.add-slide:focus-visible,
.delete-slide:hover,
.delete-slide:focus-visible {
border-color: var(--accent);
color: var(--accent-text);
background: var(--accent);
outline: none;
}
.slide-list-items {
display: flex;
flex-direction: column;
gap: 0.75rem;
padding: 1rem;
}
.code {
grid-area: code;
min-width: 0;
background-color: var(--bg-sunken);
border-right: 1px solid var(--border-color);
}
@ -51,27 +110,100 @@ body {
align-items: center;
justify-content: center;
height: 100%;
background-color: var(--bg);
}
.slide {
display: block;
width: 100%;
aspect-ratio: 16 / 9;
border: #000 solid 1px;
background: var(--slide-bg);
border: 1px solid var(--slide-border);
}
.slide-list-item {
position: relative;
display: grid;
grid-template-columns: 1.25rem minmax(0, 1fr);
gap: 0.4rem;
align-items: start;
min-width: 0;
}
.slide-in-list {
display: block;
grid-column: 1 / -1;
display: grid;
grid-template-columns: 1.25rem minmax(0, 1fr);
gap: 0.4rem;
align-items: start;
width: 100%;
border: 1px solid #000;
padding: 0;
margin-bottom: 1rem;
background: transparent;
min-width: 0;
padding: 0.35rem;
border: 1px solid var(--slide-border);
background: var(--bg-raised);
color: var(--text);
text-align: left;
cursor: pointer;
}
.slide-in-list:hover,
.slide-in-list:focus-visible {
border-color: var(--accent);
outline: none;
}
.slide-list-item.selected .slide-in-list {
border-color: var(--accent);
box-shadow: inset 2px 0 0 var(--accent);
}
.slide-number {
display: block;
min-height: 1rem;
color: var(--text-muted);
font-size: 0.75rem;
line-height: 1rem;
text-align: center;
}
.global-slide {
padding-left: 0.25rem;
border-left: 2px solid var(--accent);
}
.global-slide .slide-number {
color: var(--accent);
font-weight: 700;
}
.slide-in-list img {
display: block;
width: 100%;
height: auto;
aspect-ratio: 16 / 9;
object-fit: contain;
background: var(--slide-bg);
}
.delete-slide {
position: absolute;
top: 0.25rem;
right: 0.25rem;
display: grid;
place-items: center;
width: 1.4rem;
height: 1.4rem;
padding: 0;
border: 1px solid var(--border-color);
background: var(--bg-raised);
color: var(--text);
font-size: 1rem;
line-height: 1;
opacity: 0;
cursor: pointer;
}
.slide-list-item:hover .delete-slide,
.delete-slide:focus-visible {
opacity: 1;
}