Skip to content
Closed
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
48 changes: 45 additions & 3 deletions src/templates/bespoke/bespoke.scss
Original file line number Diff line number Diff line change
Expand Up @@ -208,8 +208,6 @@ $progress-height: 5px;
position: absolute;
top: 0;
width: 100%;

// Grid layout for presenter view
display: grid;
grid-template:
'current dragbar next' minmax(140px, 1fr)
Expand Down Expand Up @@ -242,7 +240,7 @@ $progress-height: 5px;
// Dragbar
.bespoke-marp-presenter-dragbar-container {
grid-area: dragbar;
background: #0288d1; // Marp brand color
background: #0288d1;
cursor: col-resize;
width: 6px;
margin-left: -3px;
Expand Down Expand Up @@ -456,6 +454,50 @@ $progress-height: 5px;
}
}
}

// Overview view
/* stylelint-disable no-descending-specificity */
body[data-bespoke-view='overview'] {
background: #161616;

.bespoke-marp-parent {
inset: 0;
position: absolute;

svg.bespoke-marp-slide {
content-visibility: visible;
z-index: 0;
pointer-events: auto;
opacity: 1;
cursor: pointer;
height: 100%;
left: 0;
position: absolute;
top: 0;
width: 100%;
transition: filter 0.2s ease;
filter: drop-shadow(0 2px 8px rgba(#000, 0.3));

&:hover {
filter: drop-shadow(0 4px 16px rgba(#000, 0.5));
}

&.bespoke-marp-overview-active {
outline: 3px solid #0288d1;
outline-offset: 4px;
}

[data-bespoke-marp-fragment='inactive'] {
visibility: visible;
}

* {
view-transition-name: none !important;
}
}
}
}
/* stylelint-enable no-descending-specificity */
}

@media print {
Expand Down
41 changes: 24 additions & 17 deletions src/templates/bespoke/bespoke.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import bespokeInteractive from './interactive'
import bespokeLoad from './load'
import bespokeNavigation from './navigation'
import bespokeOSC from './osc'
import bespokeOverview from './overview/'
import bespokePresenter from './presenter/'
import bespokeProgress from './progress'
import bespokeState from './state'
Expand All @@ -18,7 +19,12 @@ import bespokeWakeLock from './wake-lock'

const parse = (
...patterns: [
[normalView: 1 | 0, presenterView: 1 | 0, nextView: 1 | 0],
[
normalView: 1 | 0,
presenterView: 1 | 0,
nextView: 1 | 0,
overviewView: 1 | 0,
],
(...args: unknown[]) => void,
][]
) => {
Expand All @@ -33,22 +39,23 @@ const bespokeTemplate = (target = document.getElementById(':$p')!) => {
const deck = bespoke.from(
target,
parse(
// P N
[[1, 1, 0], bespokeSync({ key })],
[[1, 1, 1], bespokePresenter(target)],
[[1, 1, 0], bespokeInteractive],
[[1, 1, 1], bespokeClasses],
[[1, 0, 0], bespokeInactive()],
[[1, 1, 1], bespokeLoad],
[[1, 1, 1], bespokeState({ history: false })],
[[1, 1, 0], bespokeNavigation()],
[[1, 1, 0], bespokeFullscreen],
[[1, 0, 0], bespokeProgress],
[[1, 1, 0], bespokeTouch()],
[[1, 0, 0], bespokeOSC()],
[[1, 0, 0], bespokeTransition],
[[1, 1, 1], bespokeFragments],
[[1, 1, 0], bespokeWakeLock]
// P N O
[[1, 1, 0, 1], bespokeSync({ key })],
[[1, 1, 1, 0], bespokePresenter(target)],
[[1, 1, 0, 0], bespokeInteractive],
[[1, 1, 1, 1], bespokeClasses],
[[1, 0, 0, 0], bespokeInactive()],
[[1, 1, 1, 1], bespokeLoad],
[[1, 1, 1, 1], bespokeState({ history: false })],
[[1, 1, 0, 0], bespokeNavigation()],
[[1, 1, 0, 0], bespokeFullscreen],
[[1, 0, 0, 0], bespokeProgress],
[[1, 1, 0, 0], bespokeTouch()],
[[1, 0, 0, 0], bespokeOSC()],
[[1, 1, 0, 1], bespokeOverview()],
[[1, 0, 0, 0], bespokeTransition],
[[1, 1, 1, 1], bespokeFragments],
[[1, 1, 0, 0], bespokeWakeLock]
)
)

Expand Down
20 changes: 20 additions & 0 deletions src/templates/bespoke/overview/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import {
getViewMode,
ViewModeNormal,
ViewModeOverview,
ViewModePresenter,
} from '../utils'
import normalView from './normal-view'
import overviewView from './overview-view'

const bespokeOverview = () => {
const mode = getViewMode()

return {
[ViewModeNormal]: normalView,
[ViewModePresenter]: normalView,
[ViewModeOverview]: overviewView(),
}[mode]
}

export default bespokeOverview
51 changes: 51 additions & 0 deletions src/templates/bespoke/overview/normal-view.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import { classPrefix, generateURLfromParams, storage } from '../utils'

export const overviewPrefix = `${classPrefix}overview-` as const

interface BespokeForOverview {
syncKey: string
[key: string]: any
}

const validateDeck = (deck: any): deck is BespokeForOverview =>
deck.syncKey && typeof deck.syncKey === 'string'

const normalView = (deck) => {
if (!validateDeck(deck)) return

Object.defineProperties(deck, {
openOverviewView: { enumerable: true, value: openOverviewView },
overviewUrl: { enumerable: true, get: overviewUrl },
})

if (storage.available)
document.addEventListener('keydown', (e) => {
if (e.key === 'o' && !e.altKey && !e.ctrlKey && !e.metaKey) {
e.preventDefault()
deck.openOverviewView()
}
})
}

function openOverviewView(this: BespokeForOverview) {
const { max, floor } = Math
const w = max(floor(window.innerWidth * 0.85), 640)
const h = max(floor(window.innerHeight * 0.85), 360)

return window.open(
this.overviewUrl,
overviewPrefix + this.syncKey,
`width=${w},height=${h},menubar=no,toolbar=no`
)
Comment on lines +30 to +39
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it's better to be overlay on the current view instead of opening new window.

}

function overviewUrl(this: BespokeForOverview) {
const params = new URLSearchParams(location.search)

params.set('view', 'overview')
params.set('sync', this.syncKey)

return generateURLfromParams(params)
}

export default normalView
86 changes: 86 additions & 0 deletions src/templates/bespoke/overview/overview-view.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
import { classPrefix } from '../utils'

const overviewActiveClass = `${classPrefix}overview-active`

const overviewView = () => (deck) => {
const { title } = document
document.title = `[Overview]${title ? ` - ${title}` : ''}`

const slides = (): SVGElement[] => deck.slides
const slideCount = () => slides().length
const columns = () => Math.ceil(Math.sqrt(slideCount()))

const applyLayout = () => {
const cols = columns()
const rows = Math.ceil(slideCount() / cols)
const parent = deck.parent as HTMLElement
const rect = parent.getBoundingClientRect()
const gap = 20
const cellW = (rect.width - gap * (cols + 1)) / cols
const cellH = (rect.height - gap * (rows + 1)) / rows

const scaleX = cellW / rect.width
const scaleY = cellH / rect.height
const cellScale = Math.min(scaleX, scaleY)

const renderedW = rect.width * cellScale
const renderedH = rect.height * cellScale

slides().forEach((slide, i) => {
const col = i % cols
const row = Math.floor(i / cols)
const x = gap + col * (renderedW + gap)
const y = gap + row * (renderedH + gap)

slide.style.transformOrigin = '0 0'
slide.style.transform = `translate(${x}px, ${y}px) scale(${cellScale})`
})
}
Comment on lines +13 to +38
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Use CSS Grid to make simpler JS.


const updateHighlight = (index: number) => {
slides().forEach((slide, i) => {
slide.classList.toggle(overviewActiveClass, i === index)
})
}

// Click a slide to navigate (syncs to other windows via bespokeSync)
slides().forEach((slide, i) => {
slide.addEventListener('click', () => {
deck.slide(i)
})
Comment on lines +48 to +50
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

a11y: Each slide should be focusable by setting tabindex="0", and navigatable by hitting Entry or Space in addition to the click event.

})

// Arrow key navigation within the grid
document.addEventListener('keydown', (e) => {
const cols = columns()
const current = deck.slide()

if (e.key === 'ArrowRight') {
e.preventDefault()
deck.slide(Math.min(current + 1, slideCount() - 1))
} else if (e.key === 'ArrowLeft') {
e.preventDefault()
deck.slide(Math.max(current - 1, 0))
} else if (e.key === 'ArrowDown') {
e.preventDefault()
deck.slide(Math.min(current + cols, slideCount() - 1))
} else if (e.key === 'ArrowUp') {
e.preventDefault()
deck.slide(Math.max(current - cols, 0))
}
})

// Update highlight on any slide change (including sync from other windows)
deck.on('fragment', ({ index }) => {
updateHighlight(index)
})

// Initial layout and highlight
applyLayout()
updateHighlight(deck.slide())

// Recalculate layout on resize
window.addEventListener('resize', () => applyLayout())
}

export default overviewView
3 changes: 2 additions & 1 deletion src/templates/bespoke/presenter/presenter-view.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,7 @@ const presenterView = (deck) => {
}

const subscribe = (deck) => {
// Splitter
// Horizontal splitter
let isDragging = false

const startDragging = () => {
Expand Down Expand Up @@ -183,6 +183,7 @@ const presenterView = (deck) => {
noteButtonBigger.blur()
biggerNotesFont()
})

noteButtonSmaller.addEventListener('click', () => {
noteButtonSmaller.blur()
smallerNotesFont()
Expand Down
8 changes: 7 additions & 1 deletion src/templates/bespoke/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,13 @@ const replacer: QuerySetter = (...args) => history.replaceState(...args)
export const ViewModeNormal = ''
export const ViewModePresenter = 'presenter'
export const ViewModeNext = 'next'
export const ViewModeOverview = 'overview'

export const viewModes = [
ViewModeNormal,
ViewModePresenter,
ViewModeNext,
ViewModeOverview,
] as const

export const classPrefix = 'bespoke-marp-'
Expand Down Expand Up @@ -78,7 +80,11 @@ export const setViewMode = () => {
const view = readQuery('view')

body.dataset.bespokeView =
view === ViewModeNext || view === ViewModePresenter ? view : ViewModeNormal
view === ViewModeNext ||
view === ViewModePresenter ||
view === ViewModeOverview
? view
: ViewModeNormal
}

export const storage = (() => {
Expand Down
Loading