refactor css + add dark mode + maybe some other stuff idk dont remember what else i did
This commit is contained in:
parent
22e17a92d4
commit
e309455edc
5 changed files with 368 additions and 53 deletions
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -27,7 +27,7 @@ export function createEditor() {
|
|||
{
|
||||
value: 'test',
|
||||
language: 'html',
|
||||
theme: 'vs-light',
|
||||
theme: 'vs-dark',
|
||||
automaticLayout: true
|
||||
}
|
||||
)
|
||||
|
|
|
|||
225
src/main.ts
225
src/main.ts
|
|
@ -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)
|
||||
}
|
||||
|
||||
|
|
|
|||
154
src/style.css
154
src/style.css
|
|
@ -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;
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue