snapshot of https://github.com/ueberdosis/tiptap.git (main, 2026-09-15; blobs > 131072B omitted)
Some checks failed
Verify / Install node dependencies (push) Failing after 2s
Verify / Build packages (push) Has been skipped
Verify / Check linting & formatting (push) Has been skipped
Verify / Run unit tests (push) Has been skipped
Verify / Run e2e tests (shard 1/4) (push) Has been skipped
Verify / Run e2e tests (shard 2/4) (push) Has been skipped
Verify / Run e2e tests (shard 3/4) (push) Has been skipped
Verify / Run e2e tests (shard 4/4) (push) Has been skipped
Publish / Resolve publish config (push) Failing after 2s
Publish / Send notification to Slack (push) Has been skipped
Publish / Send failure notification to Slack (push) Failing after 8s
Verify / Merge e2e reports (push) Has been skipped
Publish / Build (push) Has been skipped
Publish / Bump Versions OR Release to Registry (push) Has been skipped

This commit is contained in:
upstream-mirror
2026-09-15 21:11:02 +05:00
commit 083a46e93b
2115 changed files with 245958 additions and 0 deletions

233
demos/preview/Demo.vue Normal file
View File

@@ -0,0 +1,233 @@
<template>
<div class="overflow-hidden antialiased rounded-lg">
<div class="px-3 py-1 bg-black flex items-center gap-0.5">
<button
v-for="(language, index) in sortedTabs"
:key="index"
@click="setTab(language.name)"
class="px-3 py-2 text-sm text-white leading-[125%] font-semibold rounded-[0.625rem] transition-all"
:class="[
currentTab === language.name
? 'opacity-100 bg-[#1C1917]'
: 'opacity-50 bg-transparent hover:opacity-100 hover:bg-[#1C1917]',
]"
>
{{ language.name }}
</button>
</div>
<div class="overflow-hidden">
<div class="bg-white" :class="[hidePreview ? 'hidden' : '']">
<demo-frame :src="currentIframeUrl" :key="currentIframeUrl" />
</div>
<div class="text-white bg-black" v-if="!hideSource && currentFile">
<div class="flex overflow-x-auto">
<div class="flex flex-auto px-4 border-b-2 border-gray-800">
<button
class="inline-flex relative mr-4 py-2 pb-[calc(0.3rem + 2px)] mb-[-2px] border-b-2 border-transparent font-mono text-sm whitespace-nowrap"
:class="[
!showDebug && currentFile.content === file.content
? 'text-white border-white font-bold'
: 'text-gray-400',
]"
v-for="(file, index) in source"
:key="index"
@click="setFile(file.name)"
>
{{ file.name }}
</button>
<button
v-if="debugJSON"
class="inline-flex relative py-2 pb-[calc(0.3rem + 2px)] mb-[-2px] border-b-2 border-transparent font-mono text-sm ml-auto"
:class="[showDebug ? 'text-white border-white font-bold' : 'text-gray-400']"
@click="showDebug = !showDebug"
>
Inspect
</button>
</div>
</div>
<div class="overflow-dark overflow-auto max-h-[500px] relative text-white">
<shiki
class="p-4 overflow-visible"
:language="debugJSON && showDebug ? 'js' : getFileExtension(currentFile.name)"
:code="debugJSON && showDebug ? debugJSON : currentFile.content"
/>
</div>
<div class="flex justify-between px-4 py-2 text-gray-400 border-t border-gray-800 text-md">
<a
class="flex-shrink min-w-0 overflow-hidden overflow-ellipsis whitespace-nowrap"
:href="currentIframeUrl"
>
{{ name }}/{{ currentTab }}
</a>
<a class="pl-4 whitespace-nowrap" :href="githubUrl" target="_blank"> Edit on GitHub → </a>
</div>
</div>
</div>
</div>
</template>
<script>
import { getDebugJSON } from '@tiptap/core'
import DemoFrame from './DemoFrame.vue'
import Shiki from './Shiki.vue'
export default {
components: {
DemoFrame,
Shiki,
},
props: {
name: {
type: String,
required: true,
},
tabs: {
type: Object,
required: true,
},
},
data() {
return {
data: [],
sources: {},
currentTab: null,
currentFile: null,
tabOrder: ['React', 'Vue', 'Svelte', 'JS'],
debugJSON: null,
showDebug: false,
}
},
computed: {
currentIframeUrl() {
return `/src/${this.name}/${this.currentTab}/`
},
firstTabSelected() {
return this.sortedTabs[0].name === this.currentTab
},
sortedTabs() {
return [...this.tabs].sort((a, b) => {
return this.tabOrder.indexOf(a.name) - this.tabOrder.indexOf(b.name)
})
},
query() {
return Object.fromEntries(
Object.entries(this.$route.query).map(([key, value]) => [key, this.fromString(value)]),
)
},
inline() {
return this.query.inline || false
},
hideSource() {
return this.query.hideSource || false
},
hidePreview() {
return this.query.hidePreview || false
},
githubUrl() {
return `https://github.com/ueberdosis/tiptap/tree/main/demos/src/${this.name}`
},
source() {
return this.sources[this.currentTab]
},
},
methods: {
getFileExtension(name) {
return name.split('.').pop()
},
setTab(name, persist = true) {
this.currentTab = name
if (persist) {
localStorage.tab = name
}
},
setFile(name) {
this.showDebug = false
this.currentFile = this.source.find(item => item.name === name)
},
onSource(event) {
this.sources[this.currentTab] = event.detail
this.setFile(this.source[0].name)
},
onEditor(event) {
const editor = event.detail
if (!editor) {
this.debugJSON = null
return
}
this.debugJSON = JSON.stringify(getDebugJSON(editor.state.doc), null, ' ')
editor.on('update', () => {
this.debugJSON = JSON.stringify(getDebugJSON(editor.state.doc), null, ' ')
})
},
fromString(value) {
if (value === null) {
return true
}
if (value.match(/^\d*(\.\d+)?$/)) {
return Number(value)
}
if (value === 'true') {
return true
}
if (value === 'false') {
return false
}
if (value === 'null') {
return null
}
return value
},
},
mounted() {
// TODO: load language from url params
const intitialTab =
localStorage.tab && this.tabs.some(tab => tab.name === localStorage.tab)
? localStorage.tab
: this.sortedTabs[0]?.name
this.setTab(intitialTab, false)
window.document.addEventListener('editor', this.onEditor, false)
window.document.addEventListener('source', this.onSource, false)
},
beforeUnmount() {
window.document.removeEventListener('editor', this.onEditor)
window.document.removeEventListener('source', this.onSource)
},
}
</script>

View File

@@ -0,0 +1,56 @@
<template>
<div class="flex flex-col relative min-h-[5rem]">
<div
class="absolute top-0 left-0 w-full h-full flex justify-center items-center pointer-events-none"
v-if="isLoading"
>
<svg
class="animate-spin -ml-1 mr-3 h-5 w-5"
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
>
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4" />
<path
class="opacity-75"
fill="currentColor"
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
/>
</svg>
</div>
<iframe
class="bg-transparent max-h-[28rem]"
v-resize.quiet="{ scrolling: 'omit' }"
:src="src"
width="100%"
height="0"
frameborder="0"
@load="onLoad"
/>
</div>
</template>
<script>
export default {
props: {
src: {
required: true,
type: String,
},
},
data() {
return {
isLoading: true,
}
},
methods: {
onLoad() {
this.isLoading = false
},
},
}
</script>
<style></style>

73
demos/preview/Shiki.vue Normal file
View File

@@ -0,0 +1,73 @@
<template>
<div v-if="html" v-html="html" />
<pre v-else><code>{{ code }}</code></pre>
</template>
<script>
// this import is a bugfix
// otherwise the `onig.wasm` file is missing in the dist folder
import 'shiki/dist/onig.wasm?url'
import Worker from './shiki.worker.js?worker'
export default {
props: {
code: {
default: '',
type: String,
},
language: {
default: 'js',
type: String,
},
},
data() {
return {
worker: new Worker(),
html: null,
highlighter: window?.highlighter,
}
},
watch: {
code: {
immediate: true,
handler() {
this.render()
},
},
highlighter: {
immediate: true,
handler() {
this.render()
},
},
},
methods: {
render() {
this.worker.postMessage({
code: this.code,
language: this.language,
})
},
},
created() {
this.worker.addEventListener('message', event => {
const { html } = event.data
this.html = html
})
},
}
</script>
<style>
.shiki {
background-color: transparent !important;
}
</style>

12
demos/preview/index.html Normal file
View File

@@ -0,0 +1,12 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Preview</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="/preview/index.js"></script>
</body>
</html>

61
demos/preview/index.js Normal file
View File

@@ -0,0 +1,61 @@
import 'iframe-resizer/js/iframeResizer.contentWindow'
import './style.css'
import { demos } from '@demos'
import iframeResize from 'iframe-resizer/js/iframeResizer'
import { createApp } from 'vue'
import { createRouter, createWebHistory } from 'vue-router'
import Demo from './Demo.vue'
import App from './index.vue'
const routes = demos.map(({ name, tabs }) => {
return {
path: `/${name}`,
component: Demo,
props: {
name,
tabs,
},
}
})
const router = createRouter({
history: createWebHistory('preview'),
routes,
})
createApp(App)
.directive('resize', {
beforeMount: (el, { value = {} }) => {
el.addEventListener('load', () => {
iframeResize(
{
...value,
// messageCallback(messageData) {
// if (messageData.message.type !== 'resize') {
// return
// }
// const style = window.getComputedStyle(el.parentElement)
// const maxHeight = parseInt(style.getPropertyValue('max-height'), 10)
// if (messageData.message.height > maxHeight) {
// el.setAttribute('scrolling', 'auto')
// } else {
// el.setAttribute('scrolling', 'no')
// }
// el?.iFrameResizer?.resize?.()
// },
},
el,
)
})
},
unmounted(el) {
el?.iFrameResizer?.removeListeners?.()
},
})
.use(router)
.mount('#app')

95
demos/preview/index.vue Normal file
View File

@@ -0,0 +1,95 @@
<script setup>
import { ref, watch } from 'vue'
const searchQuery = new URLSearchParams(window.location.search)
const searchValue = ref(searchQuery.get('search') || '')
// Update the window search param when the searchValue changes
// this allows the user to bookmark / return to a specific search
// and also allows the browser to handle back/forward navigation
// without losing the search state
watch(searchValue, (newValue, oldValue) => {
if (newValue !== oldValue) {
searchQuery.set('search', newValue)
window.history.replaceState({}, '', `?${searchQuery.toString()}`)
}
})
</script>
<template>
<template v-if="$route.path === '/'">
<input
class="w-full p-3 my-3 focus:outline-none border-b"
type="search"
placeholder="Search for a demo..."
autofocus
v-model="searchValue"
/>
<ul>
<li
class="p-5 border-b-2 border-black"
v-for="route in $router.options.routes.filter(route =>
searchValue === ''
? true
: route.props.name.toLowerCase().includes(searchValue.toLowerCase()),
)"
:key="route.path"
>
<router-link class="block mb-2 font-medium" :to="route.path">
{{ route.props.name }}
</router-link>
<div class="flex">
<a
class="mr-4 text-sm text-gray-300 font-medium"
v-for="(tab, index) in route.props.tabs"
:key="index"
:href="`/src/${route.props.name}/${tab.name}/`"
>
{{ tab.name }}
</a>
</div>
</li>
</ul>
</template>
<router-view v-else />
</template>
<script>
export default {
methods: {
fromString(value) {
if (value === null) {
return true
}
if (value.match(/^\d*(\.\d+)?$/)) {
return Number(value)
}
if (value === 'true') {
return true
}
if (value === 'false') {
return false
}
if (value === 'null') {
return null
}
return value
},
},
computed: {
query() {
return Object.fromEntries(
Object.entries(this.$route.query).map(([key, value]) => [key, this.fromString(value)]),
)
},
},
}
</script>

View File

@@ -0,0 +1,30 @@
import * as shiki from 'shiki'
let highlighter = null
async function init() {
if (highlighter) {
return highlighter
}
highlighter = await shiki.createHighlighter({
themes: ['material-theme-darker'],
langs: ['html', 'js', 'jsx', 'ts', 'tsx', 'css', 'vue-html', 'vue', 'scss'],
})
return highlighter
}
// oxlint-disable-next-line
self.addEventListener('message', async event => {
init().then(async () => {
const { code, language } = event.data
await highlighter.loadLanguage(language)
const html = highlighter.codeToHtml(code, { lang: language, theme: 'material-theme-darker' })
// oxlint-disable-next-line
self.postMessage({ code, language, html })
})
})

148
demos/preview/style.css Normal file
View File

@@ -0,0 +1,148 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
@font-face {
font-family: 'Inter';
font-style: normal;
font-weight: 400;
font-display: swap;
src:
url('https://rsms.me/inter/font-files/Inter-Regular.woff2?v=3.19') format('woff2'),
url('https://rsms.me/inter/font-files/Inter-Regular.woff?v=3.19') format('woff');
}
@font-face {
font-family: 'Inter';
font-style: italic;
font-weight: 400;
font-display: swap;
src:
url('https://rsms.me/inter/font-files/Inter-Italic.woff2?v=3.19') format('woff2'),
url('https://rsms.me/inter/font-files/Inter-Italic.woff?v=3.19') format('woff');
}
@font-face {
font-family: 'Inter';
font-style: normal;
font-weight: 500;
font-display: swap;
src:
url('https://rsms.me/inter/font-files/Inter-Medium.woff2?v=3.19') format('woff2'),
url('https://rsms.me/inter/font-files/Inter-Medium.woff?v=3.19') format('woff');
}
@font-face {
font-family: 'Inter';
font-style: italic;
font-weight: 500;
font-display: swap;
src:
url('https://rsms.me/inter/font-files/Inter-MediumItalic.woff2?v=3.19') format('woff2'),
url('https://rsms.me/inter/font-files/Inter-MediumItalic.woff?v=3.19') format('woff');
}
@font-face {
font-family: 'Inter';
font-style: normal;
font-weight: 600;
font-display: swap;
src:
url('https://rsms.me/inter/font-files/Inter-SemiBold.woff2?v=3.19') format('woff2'),
url('https://rsms.me/inter/font-files/Inter-SemiBold.woff?v=3.19') format('woff');
}
@font-face {
font-family: 'Inter';
font-style: italic;
font-weight: 600;
font-display: swap;
src:
url('https://rsms.me/inter/font-files/Inter-SemiBoldItalic.woff2?v=3.19') format('woff2'),
url('https://rsms.me/inter/font-files/Inter-SemiBoldItalic.woff?v=3.19') format('woff');
}
@font-face {
font-family: 'Inter';
font-style: normal;
font-weight: 700;
font-display: swap;
src:
url('https://rsms.me/inter/font-files/Inter-Bold.woff2?v=3.19') format('woff2'),
url('https://rsms.me/inter/font-files/Inter-Bold.woff?v=3.19') format('woff');
}
@font-face {
font-family: 'Inter';
font-style: italic;
font-weight: 700;
font-display: swap;
src:
url('https://rsms.me/inter/font-files/Inter-BoldItalic.woff2?v=3.19') format('woff2'),
url('https://rsms.me/inter/font-files/Inter-BoldItalic.woff?v=3.19') format('woff');
}
@font-face {
font-family: 'JetBrains Mono';
font-style: normal;
font-weight: 400;
font-display: swap;
src:
url('https://cdn.jsdelivr.net/gh/JetBrains/JetBrainsMono/web/woff2/JetBrainsMono-Regular.woff2')
format('woff2'),
url('https://cdn.jsdelivr.net/gh/JetBrains/JetBrainsMono/web/woff2/JetBrainsMono-Regular.woff')
format('woff');
}
@font-face {
font-family: 'JetBrains Mono';
font-style: normal;
font-weight: 700;
font-display: swap;
src:
url('https://cdn.jsdelivr.net/gh/JetBrains/JetBrainsMono/web/woff2/JetBrainsMono-Bold.woff2')
format('woff2'),
url('https://cdn.jsdelivr.net/gh/JetBrains/JetBrainsMono/web/woff2/JetBrainsMono-Bold.woff')
format('woff');
}
::-webkit-scrollbar {
width: 14px;
height: 14px;
}
::-webkit-scrollbar-track {
border: 4px solid transparent;
background-clip: padding-box;
border-radius: 8px;
background-color: transparent;
}
::-webkit-scrollbar-thumb {
border: 4px solid rgba(0, 0, 0, 0);
background-clip: padding-box;
border-radius: 8px;
background-color: rgba(0, 0, 0, 0);
}
:hover::-webkit-scrollbar-thumb {
background-color: rgba(0, 0, 0, 0.1);
}
::-webkit-scrollbar-thumb:hover {
background-color: rgba(0, 0, 0, 0.15);
}
.overflow-dark:hover::-webkit-scrollbar-thumb {
background-color: rgba(255, 255, 255, 0.2);
}
.overflow-dark::-webkit-scrollbar-thumb:hover {
background-color: rgba(255, 255, 255, 0.3);
}
::-webkit-scrollbar-button {
display: none;
width: 0;
height: 0;
}
::-webkit-scrollbar-corner {
background-color: transparent;
}