feature: vor und nachteile

This commit is contained in:
Hikyu
2026-05-31 13:54:31 +02:00
parent 552a947a1c
commit e3dad17446
23270 changed files with 3024794 additions and 200 deletions
+235
View File
@@ -0,0 +1,235 @@
import type { CodeRunner, CodeRunnerOutput, CodeRunnerOutputs, CodeRunnerOutputText } from '@slidev/types'
import type ts from 'typescript'
import deps from '#slidev/monaco-run-deps'
import setups from '#slidev/setups/code-runners'
import { createSingletonPromise } from '@antfu/utils'
import { ref } from 'vue'
export default createSingletonPromise(async () => {
const runners: Record<string, CodeRunner> = {
javascript: runTypeScript,
js: runTypeScript,
typescript: runTypeScript,
ts: runTypeScript,
}
const { getHighlighter } = await import('#slidev/shiki')
const highlight = await getHighlighter()
const run = async (code: string, lang: string, options: Record<string, unknown>): Promise<CodeRunnerOutputs> => {
try {
const runner = runners[lang]
if (!runner)
throw new Error(`Runner for language "${lang}" not found`)
return await runner(
code,
{
options,
highlight,
run: async (code, lang) => {
return await run(code, lang, options)
},
},
)
}
catch (e) {
console.error(e)
return {
error: `${e}`,
}
}
}
for (const setup of setups) {
const result = await setup(runners)
Object.assign(runners, result)
}
return {
highlight,
run,
}
})
// Ported from https://github.com/microsoft/TypeScript-Website/blob/v2/packages/playground/src/sidebar/runtime.ts
function runJavaScript(code: string): CodeRunnerOutputs {
const result = ref<CodeRunnerOutput[]>([])
const onError = (error: any) => result.value.push({ error: String(error) })
const logger = (...objs: any[]) => result.value.push(objs.map(printObject))
const vmConsole = Object.assign({}, console)
vmConsole.info = vmConsole.log = vmConsole.debug = vmConsole.warn = vmConsole.error = logger
vmConsole.clear = () => result.value.length = 0
try {
const safeJS = `return async (console, __slidev_import, __slidev_on_error) => {
try {
${sanitizeJS(code)}
} catch (e) {
__slidev_on_error(e)
}
}`
// eslint-disable-next-line no-new-func
;(new Function(safeJS)())(vmConsole, (specifier: string) => {
const mod = deps[specifier]
if (!mod)
throw new Error(`Module not found: ${specifier}.\nAvailable modules: ${Object.keys(deps).join(', ')}. Please refer to https://sli.dev/custom/config-code-runners#additional-runner-dependencies`)
return mod
}, onError)
}
catch (error) {
onError(error)
}
function printObject(arg: any): CodeRunnerOutputText {
if (typeof arg === 'string') {
return {
text: arg,
}
}
return {
text: objectToText(arg),
highlightLang: 'javascript',
}
}
function objectToText(arg: any): string {
let textRep = ''
if (arg instanceof Error) {
textRep = `Error: ${JSON.stringify(arg.message)}`
}
else if (arg === null || arg === undefined || typeof arg === 'symbol') {
textRep = String(arg)
}
else if (Array.isArray(arg)) {
textRep = `[${arg.map(objectToText).join(', ')}]`
}
else if (arg instanceof Set) {
const setIter = [...arg]
textRep = `Set (${arg.size}) {${setIter.map(objectToText).join(', ')}}`
}
else if (arg instanceof Map) {
const mapIter = [...arg.entries()]
textRep
= `Map (${arg.size}) {${mapIter
.map(([k, v]) => `${objectToText(k)} => ${objectToText(v)}`)
.join(', ')
}}`
}
else if (arg instanceof RegExp) {
textRep = arg.toString()
}
else if (typeof arg === 'string') {
textRep = JSON.stringify(arg)
}
else if (typeof arg === 'object') {
const name = arg.constructor?.name ?? ''
// No one needs to know an obj is an obj
const nameWithoutObject = name && name === 'Object' ? '' : name
const prefix = nameWithoutObject ? `${nameWithoutObject}: ` : ''
// JSON.stringify omits any keys with a value of undefined. To get around this, we replace undefined with the text __undefined__ and then do a global replace using regex back to keyword undefined
textRep
= prefix
+ JSON.stringify(arg, (_, value) => (value === undefined ? '__undefined__' : value), 2).replace(
/"__undefined__"/g,
'undefined',
)
textRep = String(textRep)
}
else {
textRep = String(arg)
}
return textRep
}
function sanitizeJS(code: string) {
// The reflect-metadata runtime is available, so allow that to go through
code = code.replace(`import "reflect-metadata"`, '').replace(`require("reflect-metadata")`, '')
// Transpiled typescript sometimes contains an empty export, remove it.
code = code.replace('export {};', '')
return code
}
return result
}
let tsModule: typeof import('typescript') | undefined
export async function runTypeScript(code: string) {
tsModule ??= await import('typescript')
code = tsModule.transpileModule(code, {
compilerOptions: {
module: tsModule.ModuleKind.ESNext,
target: tsModule.ScriptTarget.ES2022,
},
transformers: {
after: [transformImports],
},
}).outputText
const importRegex = /import\s*\((.+)\)/g
code = code.replace(importRegex, (_full, specifier) => `__slidev_import(${specifier})`)
return runJavaScript(code)
}
/**
* Transform import statements to dynamic imports
*/
function transformImports(context: ts.TransformationContext): ts.Transformer<ts.SourceFile> {
const { factory } = context
const { isImportDeclaration, isNamedImports, NodeFlags } = tsModule!
return (sourceFile: ts.SourceFile) => {
const statements = [...sourceFile.statements]
for (let i = 0; i < statements.length; i++) {
const statement = statements[i]
if (!isImportDeclaration(statement))
continue
let bindingPattern: ts.ObjectBindingPattern | ts.Identifier
const namedBindings = statement.importClause?.namedBindings
const bindings: ts.BindingElement[] = []
if (statement.importClause?.name)
bindings.push(factory.createBindingElement(undefined, factory.createIdentifier('default'), statement.importClause.name))
if (namedBindings) {
if (isNamedImports(namedBindings)) {
for (const specifier of namedBindings.elements)
bindings.push(factory.createBindingElement(undefined, specifier.propertyName, specifier.name))
bindingPattern = factory.createObjectBindingPattern(bindings)
}
else {
bindingPattern = factory.createIdentifier(namedBindings.name.text)
}
}
else {
bindingPattern = factory.createObjectBindingPattern(bindings)
}
const newStatement = factory.createVariableStatement(
undefined,
factory.createVariableDeclarationList(
[
factory.createVariableDeclaration(
bindingPattern,
undefined,
undefined,
factory.createAwaitExpression(
factory.createCallExpression(
factory.createIdentifier('import'),
undefined,
[statement.moduleSpecifier],
),
),
),
],
NodeFlags.Const,
),
)
statements[i] = newStatement
}
return factory.updateSourceFile(sourceFile, statements)
}
}
+104
View File
@@ -0,0 +1,104 @@
/// <reference types="unplugin-icons/types/vue3" />
import type { ContextMenuItem } from '@slidev/types'
import type { ComputedRef } from 'vue'
import setups from '#slidev/setups/context-menu'
import { computed } from 'vue'
import { useDrawings } from '../composables/useDrawings'
import { useNav } from '../composables/useNav'
import { fullscreen, showEditor, toggleOverview } from '../state'
// @unocss-include
let items: ComputedRef<ContextMenuItem[]> | undefined
export default () => {
if (items)
return items
const {
next,
nextSlide,
prev,
prevSlide,
hasNext,
hasPrev,
currentPage,
total,
isPresenter,
enterPresenter,
exitPresenter,
isEmbedded,
isPresenterAvailable,
} = useNav()
const { drawingEnabled } = useDrawings()
const {
isFullscreen,
toggle: toggleFullscreen,
} = fullscreen
return items = setups.reduce(
(items, fn) => fn(items),
computed(() => [
{
small: true,
icon: 'i-carbon:arrow-left',
label: 'Previous Click',
action: prev,
disabled: !hasPrev.value,
},
{
small: true,
icon: 'i-carbon:arrow-right',
label: 'Next Click',
action: next,
disabled: !hasNext.value,
},
{
small: true,
icon: 'i-carbon:arrow-up',
label: 'Previous Slide',
action: prevSlide,
disabled: currentPage.value <= 1,
},
{
small: true,
icon: 'i-carbon:arrow-down',
label: 'Next Slide',
action: nextSlide,
disabled: currentPage.value >= total.value,
},
'separator',
{
icon: 'i-carbon:text-annotation-toggle', // IconTextNotationToggle,
label: showEditor.value ? 'Hide editor' : 'Show editor',
action: () => (showEditor.value = !showEditor.value),
},
{
icon: 'i-carbon:pen',
label: drawingEnabled.value ? 'Hide drawing toolbar' : 'Show drawing toolbar',
action: () => (drawingEnabled.value = !drawingEnabled.value),
},
{
icon: 'i-carbon:apps',
label: 'Show slide overview',
action: toggleOverview,
},
isPresenter.value && {
icon: 'i-carbon:presentation-file',
label: 'Exit Presenter Mode',
action: exitPresenter,
},
__SLIDEV_FEATURE_PRESENTER__ && isPresenterAvailable.value && {
icon: 'i-carbon:user-speaker',
label: 'Enter Presenter Mode',
action: enterPresenter,
},
!isEmbedded.value && {
icon: isFullscreen.value ? 'i-carbon:minimize' : 'i-carbon:maximize',
label: isFullscreen.value ? 'Close fullscreen' : 'Enter fullscreen',
action: toggleFullscreen,
},
].filter(Boolean) as ContextMenuItem[]),
)
}
+46
View File
@@ -0,0 +1,46 @@
import type { AppContext } from '@slidev/types'
import type { App } from 'vue'
import setups from '#slidev/setups/main'
import TwoSlashFloatingVue from '@shikijs/vitepress-twoslash/client'
import { createHead } from '@unhead/vue'
import { createRouter, createWebHashHistory, createWebHistory } from 'vue-router'
import { createVClickDirectives } from '../modules/v-click'
import { createVDragDirective } from '../modules/v-drag'
import { createVMarkDirective } from '../modules/v-mark'
import { createVMotionDirectives } from '../modules/v-motion'
import setupRoutes from '../setup/routes'
import '#slidev/styles'
export default async function setupMain(app: App) {
function setMaxHeight() {
// disable the mobile navbar scroll
// see https://css-tricks.com/the-trick-to-viewport-units-on-mobile/
document.documentElement.style.setProperty('--vh', `${window.innerHeight * 0.01}px`)
}
setMaxHeight()
window.addEventListener('resize', setMaxHeight)
const router = createRouter({
history: __SLIDEV_HASH_ROUTE__
? createWebHashHistory(import.meta.env.BASE_URL)
: createWebHistory(import.meta.env.BASE_URL),
routes: setupRoutes(),
})
app.use(router)
app.use(createHead())
app.use(createVClickDirectives())
app.use(createVMarkDirective())
app.use(createVDragDirective())
app.use(createVMotionDirectives())
app.use(TwoSlashFloatingVue as any, { container: '#twoslash-container' })
const context: AppContext = {
app,
router,
}
for (const setup of setups)
await setup(context)
}
+14
View File
@@ -0,0 +1,14 @@
import type { MermaidConfig } from 'mermaid'
import setups from '#slidev/setups/mermaid'
import { createSingletonPromise } from '@antfu/utils'
export default createSingletonPromise(async () => {
const setupReturn: MermaidConfig = {
theme: 'default',
}
for (const setup of setups)
Object.assign(setupReturn, await setup())
return setupReturn
})
+141
View File
@@ -0,0 +1,141 @@
import type { MonacoSetupReturn } from '@slidev/types'
import configs from '#slidev/configs'
import setups from '#slidev/setups/monaco'
import { createSingletonPromise } from '@antfu/utils'
import { setupTypeAcquisition } from '@typescript/ata'
import * as monaco from 'monaco-editor'
import EditorWorker from 'monaco-editor/esm/vs/editor/editor.worker?worker'
// @ts-expect-error missing types
import { StandaloneServices } from 'monaco-editor/esm/vs/editor/standalone/browser/standaloneServices'
import CssWorker from 'monaco-editor/esm/vs/language/css/css.worker?worker'
import HtmlWorker from 'monaco-editor/esm/vs/language/html/html.worker?worker'
import JsonWorker from 'monaco-editor/esm/vs/language/json/json.worker?worker'
import TsWorker from 'monaco-editor/esm/vs/language/typescript/ts.worker?worker'
// @ts-expect-error missing types
import { ContextViewService } from 'monaco-editor/esm/vs/platform/contextview/browser/contextViewService'
// @ts-expect-error missing types
import { SyncDescriptor } from 'monaco-editor/esm/vs/platform/instantiation/common/descriptors'
import ts from 'typescript'
import { watchEffect } from 'vue'
import { isDark } from '../logic/dark'
window.MonacoEnvironment = {
getWorker(_, label) {
if (label === 'json')
return new JsonWorker()
if (label === 'css' || label === 'scss' || label === 'less')
return new CssWorker()
if (label === 'html' || label === 'handlebars' || label === 'razor')
return new HtmlWorker()
if (label === 'typescript' || label === 'javascript')
return new TsWorker()
return new EditorWorker()
},
}
class ContextViewService2 extends ContextViewService {
showContextView(...args: any) {
super.showContextView(...args)
// @ts-expect-error missing types
const contextView = this.contextView.view as HTMLElement
contextView.style.left = `calc(${contextView.style.left} / var(--slidev-slide-scale))`
contextView.style.top = `calc(${contextView.style.top} / var(--slidev-slide-scale))`
// Reset the scale to 1. Otherwise, the sub-menu will be in the wrong position.
contextView.style.transform = `scale(calc(1 / var(--slidev-slide-scale)))`
contextView.style.transformOrigin = '0 0'
}
}
// Initialize services first, otherwise we can't override them.
StandaloneServices.initialize({
contextViewService: new SyncDescriptor(ContextViewService2, [], true),
})
const setup = createSingletonPromise(async () => {
const defaults = monaco.languages.typescript.typescriptDefaults
defaults.setCompilerOptions({
...defaults.getCompilerOptions(),
strict: true,
moduleResolution: monaco.languages.typescript.ModuleResolutionKind.NodeJs,
module: monaco.languages.typescript.ModuleKind.ESNext,
})
const ata = configs.monacoTypesSource === 'cdn'
? setupTypeAcquisition({
projectName: 'TypeScript Playground',
typescript: ts as any, // Version mismatch. No problem found so far.
logger: console,
delegate: {
receivedFile: (code: string, path: string) => {
defaults.addExtraLib(code, `file://${path}`)
const uri = monaco.Uri.file(path)
if (monaco.editor.getModel(uri) === null)
monaco.editor.createModel(code, 'javascript', uri)
},
progress: (downloaded: number, total: number) => {
// eslint-disable-next-line no-console
console.debug(`[Typescript ATA] ${downloaded} / ${total}`)
},
},
})
: () => { }
monaco.languages.register({ id: 'vue' })
monaco.languages.register({ id: 'html' })
monaco.languages.register({ id: 'css' })
monaco.languages.register({ id: 'typescript' })
monaco.languages.register({ id: 'javascript' })
const { shiki, languages, themes, shikiToMonaco } = await import('#slidev/shiki')
const highlighter = await shiki
const editorOptions: MonacoSetupReturn['editorOptions'] & object = {}
for (const setup of setups) {
const result = await setup(monaco)
Object.assign(editorOptions, result?.editorOptions)
}
// Use Shiki to highlight Monaco
shikiToMonaco(highlighter, monaco)
if (typeof themes === 'string') {
monaco.editor.setTheme(themes)
}
else {
watchEffect(() => {
monaco.editor.setTheme(isDark.value
? themes.dark || 'vitesse-dark'
: themes.light || 'vitesse-light')
})
}
// Register all languages, otherwise Monaco will not highlight them
for (const lang of languages) {
monaco.languages.register({ id: lang })
}
return {
monaco,
ata,
editorOptions,
}
})
async function _addFile(raw: () => Promise<{ default: string }>, path: string) {
const uri = monaco.Uri.file(path)
const code = (await raw()).default
monaco.languages.typescript.typescriptDefaults.addExtraLib(code, `file:///${path}`)
monaco.editor.createModel(code, 'javascript', uri)
}
const addFileCache = new Map<string, Promise<void>>()
export async function addFile(raw: () => Promise<{ default: string }>, path: string) {
if (addFileCache.has(path))
return addFileCache.get(path)
const promise = _addFile(raw, path)
addFileCache.set(path, promise)
return promise
}
export default setup
+106
View File
@@ -0,0 +1,106 @@
import setups from '#slidev/setups/root'
import { useHead } from '@unhead/vue'
import { computed, getCurrentInstance, reactive, ref, shallowRef, watch } from 'vue'
import { useRouter } from 'vue-router'
import { createFixedClicks } from '../composables/useClicks'
import { useEmbeddedControl } from '../composables/useEmbeddedCtrl'
import { useNav } from '../composables/useNav'
import { usePrintStyles } from '../composables/usePrintStyles'
import { injectionClicksContext, injectionCurrentPage, injectionRenderContext, injectionSlidevContext, TRUST_ORIGINS } from '../constants'
import { configs, slidesTitle } from '../env'
import { skipTransition } from '../logic/hmr'
import { getSlidePath } from '../logic/slides'
import { makeId } from '../logic/utils'
import { initDrawingState } from '../state/drawings'
import { initSharedState, onPatch, patch } from '../state/shared'
export default function setupRoot() {
const app = getCurrentInstance()!.appContext.app
const context = reactive({
nav: useNav(),
configs,
themeConfigs: computed(() => configs.themeConfig),
})
app.provide(injectionRenderContext, ref('none' as const))
app.provide(injectionSlidevContext, context)
app.provide(injectionCurrentPage, computed(() => context.nav.currentSlideNo))
app.provide(injectionClicksContext, shallowRef(createFixedClicks()))
// allows controls from postMessages
if (__DEV__) {
// @ts-expect-error expose global
window.__slidev__ = context
useEmbeddedControl()
}
// User Setups
for (const setup of setups)
setup()
const {
clicksContext,
currentSlideNo,
hasPrimarySlide,
isNotesViewer,
isPresenter,
isPrintMode,
} = useNav()
useHead({
title: slidesTitle,
htmlAttrs: configs.htmlAttrs,
})
usePrintStyles()
initSharedState(`${slidesTitle} - shared`)
initDrawingState(`${slidesTitle} - drawings`)
const id = `${location.origin}_${makeId()}`
// update shared state
function updateSharedState() {
if (isNotesViewer.value || isPrintMode.value)
return
// we allow Presenter mode, or Viewer mode from trusted origins to update the shared state
if (!isPresenter.value && !TRUST_ORIGINS.includes(location.host.split(':')[0]))
return
if (isPresenter.value) {
patch('page', +currentSlideNo.value)
patch('clicks', clicksContext.value.current)
patch('clicksTotal', clicksContext.value.total)
}
else {
patch('viewerPage', +currentSlideNo.value)
patch('viewerClicks', clicksContext.value.current)
patch('viewerClicksTotal', clicksContext.value.total)
}
patch('lastUpdate', {
id,
type: isPresenter.value ? 'presenter' : 'viewer',
time: new Date().getTime(),
})
}
const router = useRouter()
router.afterEach(updateSharedState)
watch(clicksContext, updateSharedState)
onPatch((state) => {
if (!hasPrimarySlide.value || isPrintMode.value)
return
if (state.lastUpdate?.type === 'presenter' && (+state.page !== +currentSlideNo.value || +clicksContext.value.current !== +state.clicks)) {
skipTransition.value = false
router.replace({
path: getSlidePath(state.page, isPresenter.value),
query: {
...router.currentRoute.value.query,
clicks: state.clicks || 0,
},
})
}
})
}
+96
View File
@@ -0,0 +1,96 @@
import type { RouteLocationNormalized, RouteRecordRaw } from 'vue-router'
import configs from '#slidev/configs'
import setups from '#slidev/setups/routes'
export default function setupRoutes() {
const routes: RouteRecordRaw[] = []
function passwordGuard(to: RouteLocationNormalized) {
if (!configs.remote || configs.remote === to.query.password)
return true
if (configs.remote && to.query.password === undefined) {
// eslint-disable-next-line no-alert
const password = prompt('Enter password')
if (configs.remote === password)
return true
}
if (to.params.no)
return { path: `/${to.params.no}` }
return { path: '' }
}
if (__SLIDEV_FEATURE_PRESENTER__) {
routes.push(
{
name: 'entry',
path: '/entry',
component: () => import('../pages/entry.vue'),
},
{
name: 'overview',
path: '/overview',
component: () => import('../pages/overview.vue'),
},
{
name: 'notes',
path: '/notes',
component: () => import('../pages/notes.vue'),
beforeEnter: passwordGuard,
},
{
name: 'presenter',
path: '/presenter/:no',
component: () => import('../pages/presenter.vue'),
beforeEnter: passwordGuard,
},
{
path: '/presenter',
redirect: { path: '/presenter/1' },
},
)
}
if (__SLIDEV_FEATURE_PRINT__) {
routes.push(
{
name: 'print',
path: '/print',
component: () => import('../pages/print.vue'),
},
{
path: '/presenter/print',
component: () => import('../pages/presenter/print.vue'),
},
)
}
if (__SLIDEV_FEATURE_BROWSER_EXPORTER__) {
routes.push(
{
name: 'export',
path: '/export/:no?',
component: () => import('../pages/export.vue'),
beforeEnter: passwordGuard,
},
)
}
routes.push(
{
name: 'play',
path: '/:no',
component: () => import('../pages/play.vue'),
},
{
path: '',
redirect: { path: '/1' },
},
{
path: '/:pathMatch(.*)*',
name: 'NotFound',
component: () => import('../pages/404.vue'),
},
)
return setups.reduce((routes, setup) => setup(routes), routes)
}
+84
View File
@@ -0,0 +1,84 @@
import type { NavOperations, ShortcutOptions } from '@slidev/types'
import setups from '#slidev/setups/shortcuts'
import { and, not, or } from '@vueuse/math'
import { useDrawings } from '../composables/useDrawings'
import { useNav } from '../composables/useNav'
import { toggleDark } from '../logic/dark'
import { activeDragElement, magicKeys, showGotoDialog, showOverview, toggleOverview } from '../state'
import { downloadPDF } from '../utils'
import { currentOverviewPage, downOverviewPage, nextOverviewPage, prevOverviewPage, upOverviewPage } from './../logic/overview'
export default function setupShortcuts() {
const { go, goFirst, goLast, next, nextSlide, prev, prevSlide } = useNav()
const { drawingEnabled } = useDrawings()
const { escape, space, shift, left, right, up, down, enter, d, g, o, '`': backtick } = magicKeys
const context: NavOperations = {
next,
prev,
nextSlide,
prevSlide,
go,
goFirst,
goLast,
downloadPDF,
toggleDark,
toggleOverview,
toggleDrawing: () => drawingEnabled.value = !drawingEnabled.value,
escapeOverview: () => showOverview.value = false,
showGotoDialog: () => showGotoDialog.value = !showGotoDialog.value,
}
const navViaArrowKeys = and(not(showOverview), not(activeDragElement))
let shortcuts: ShortcutOptions[] = [
{ name: 'next_space', key: and(space, not(shift)), fn: next, autoRepeat: true },
{ name: 'prev_space', key: and(space, shift), fn: prev, autoRepeat: true },
{ name: 'next_right', key: and(right, not(shift), navViaArrowKeys), fn: next, autoRepeat: true },
{ name: 'prev_left', key: and(left, not(shift), navViaArrowKeys), fn: prev, autoRepeat: true },
{ name: 'next_page_key', key: 'pageDown', fn: next, autoRepeat: true },
{ name: 'prev_page_key', key: 'pageUp', fn: prev, autoRepeat: true },
{ name: 'next_down', key: and(down, navViaArrowKeys), fn: nextSlide, autoRepeat: true },
{ name: 'prev_up', key: and(up, navViaArrowKeys), fn: prevSlide, autoRepeat: true },
{ name: 'next_shift', key: and(right, shift), fn: nextSlide, autoRepeat: true },
{ name: 'prev_shift', key: and(left, shift), fn: prevSlide, autoRepeat: true },
{ name: 'toggle_dark', key: and(d, not(drawingEnabled)), fn: toggleDark },
{ name: 'toggle_overview', key: and(or(o, backtick), not(drawingEnabled)), fn: toggleOverview },
{ name: 'hide_overview', key: and(escape, not(drawingEnabled)), fn: () => showOverview.value = false },
{ name: 'goto', key: and(g, not(drawingEnabled)), fn: () => showGotoDialog.value = !showGotoDialog.value },
{ name: 'next_overview', key: and(right, showOverview), fn: nextOverviewPage },
{ name: 'prev_overview', key: and(left, showOverview), fn: prevOverviewPage },
{ name: 'up_overview', key: and(up, showOverview), fn: upOverviewPage },
{ name: 'down_overview', key: and(down, showOverview), fn: downOverviewPage },
{
name: 'goto_from_overview',
key: and(enter, showOverview),
fn: () => {
go(currentOverviewPage.value)
showOverview.value = false
},
},
]
const baseShortcutNames = new Set(shortcuts.map(s => s.name))
for (const setup of setups) {
shortcuts = setup(context, shortcuts)
}
const remainingBaseShortcutNames = shortcuts.filter(s => s.name && baseShortcutNames.has(s.name))
if (remainingBaseShortcutNames.length === 0) {
const message = [
'========== WARNING ==========',
'defineShortcutsSetup did not return any of the base shortcuts.',
'See https://sli.dev/custom/config-shortcuts.html for migration.',
'If it is intentional, return at least one shortcut with one of the base names (e.g. name:"goto").',
].join('\n\n')
// eslint-disable-next-line no-alert
alert(message)
console.warn(message)
}
return shortcuts
}