Skip to content

Instantly share code, notes, and snippets.

@joshuadavidthomas
Created June 23, 2024 17:19
Show Gist options
  • Save joshuadavidthomas/d936b1ad946affde7ca82d9aa0afce10 to your computer and use it in GitHub Desktop.
Save joshuadavidthomas/d936b1ad946affde7ca82d9aa0afce10 to your computer and use it in GitHub Desktop.
<!doctype html>
<html lang="en" class="bg-gray-50 dark:bg-gray-950 h-full">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width,initial-scale=1" />
<title>Hello!</title>
{% block head_precss %}
{% endblock head_precss %}
<link rel="preconnect" href="https://rsms.me" preconnect />
<link rel="stylesheet" href="https://rsms.me/inter/inter.css" />
<link rel="preconnect" href="https://static.joshthomas.dev" preconnect />
<link rel="stylesheet" href="https://static.joshthomas.dev/css/monolisa-v2.015.css" />
{% block head_postcss %}
{% endblock head_postcss %}
<script src="https://cdn.tailwindcss.com?plugins=forms,typography"></script>
<script type="module">
import defaultColors from 'https://esm.sh/tailwindcss/colors'
import defaultTheme from 'https://esm.sh/tailwindcss/defaultTheme'
import plugin from 'https://esm.sh/tailwindcss/plugin'
tailwind.config = {
theme: {
extend: {
colors: {
primary: defaultColors.blue,
secondary: defaultColors.gray,
tertiary: defaultColors.green,
aspect: defaultColors.orange,
success: defaultColors.green,
warning: defaultColors.yellow,
danger: defaultColors.red,
},
fontFamily: {
mono: ["MonoLisa Variable", ...defaultTheme.fontFamily.mono],
sans: ['Inter var', ...defaultTheme.fontFamily.sans],
},
},
},
plugins: [
plugin(function ({ addComponents, theme }) {
const baseButtonStyles = {
display: "inline-flex",
alignItems: "center",
borderRadius: theme("borderRadius.md"),
padding: `${theme("spacing.2")} ${theme("spacing.3")}`,
fontSize: theme("fontSize.sm"),
lineHeight: theme("lineHeight.5"),
fontWeight: theme("fontWeight.semibold"),
"&:focus-visible": {
outlineOffset: "2px",
outlineStyle: "solid",
outlineWidth: "2px",
},
boxShadow: theme("boxShadow.sm"),
};
const buttonVariants = {
primary: {
backgroundColor: theme("colors.primary.500"),
color: theme("colors.white"),
"&:hover": {
backgroundColor: theme("colors.primary.600"),
},
"&:focus-visible": {
outlineColor: theme("colors.primary.500"),
},
},
secondary: {
backgroundColor: theme("colors.secondary.200"),
color: theme("colors.secondary.800"),
"&:hover": {
backgroundColor: theme("colors.secondary.300"),
},
"&:focus-visible": {
outlineColor: theme("colors.secondary.400"),
},
},
destructive: {
backgroundColor: theme("colors.danger.600"),
color: theme("colors.white"),
"&:hover": {
backgroundColor: theme("colors.danger.700"),
},
"&:focus-visible": {
outlineColor: theme("colors.danger.600"),
},
},
outline: {
border: `1px solid ${theme("colors.gray.300")}`,
color: theme("colors.gray.800"),
"&:hover": {
backgroundColor: theme("colors.gray.200"),
},
"&:focus-visible": {
outlineColor: theme("colors.gray.400"),
},
},
ghost: {
"&:hover": {
backgroundColor: theme("colors.gray.200"),
},
},
link: {
color: theme("colors.primary.500"),
textUnderlineOffset: "4px",
"&:hover": {
textDecoration: "underline",
},
},
};
let components = {};
components[".btn"] = baseButtonStyles;
Object.keys(buttonVariants).forEach((variant) => {
components[`.btn-${variant}`] = {
...baseButtonStyles,
...buttonVariants[variant],
};
});
addComponents(components);
}),
],
}
</script>
</head>
<body class="h-full">
<main class="max-w-5xl mx-auto h-full">
{% block content %}
<h1>Hello World!</h1>
{% endblock content %}
</main>
{% block body_extra %}
{% endblock body_extra %}
</body>
</html>
{% extends "base.html" %}
{% block head_precss %}
{# https://github.com/ueberdosis/tiptap/issues/4873 #}
{# https://github.com/ueberdosis/tiptap/issues/3800#issuecomment-2131980062 #}
<script type="importmap">
{
"imports": {
"https://esm.sh/v135/prosemirror-model@1.20.0/es2022/prosemirror-model.mjs": "https://esm.sh/v135/prosemirror-model@1.19.3/es2022/prosemirror-model.mjs"
}
}
</script>
{% endblock head_precss %}
{% block head_postcss %}
<style>
.tiptap {
*:first-child {
margin-top: 0;
}
[data-type="taskList"] {
list-style-type: none;
padding-inline-start: 0;
& li {
align-items: center;
display: flex;
flex: 1 1 auto;
gap: 0.5rem;
& p {
margin-bottom: 0;
}
}
}
p.is-editor-empty:first-child::before {
color: #adb5bd;
content: attr(data-placeholder);
float: left;
height: 0;
pointer-events: none;
}
}
.ProseMirror-focused {
outline: 0px solid transparent;
}
</style>
{% endblock head_postcss %}
{% block content %}
<div x-data="editor({% if draft.content %}{{ draft.content }}{% endif %})" class="p-2 flex flex-col h-full w-full text-gray-900 dark:text-gray-50">
<form method="post" class="flex items-center gap-2 justify-between" x-ref="form">
{% csrf_token %}
<div class="grow relative">
<input type="text" name="title" value="{{ draft.title }}" class="cursor-text peer block bg-transparent w-full border-0 py-1.5 focus:ring-0 sm:text-4xl sm:font-extrabold sm:leading-5 placeholder:text-gray-200 dark:placeholder:text-gray-800 px-0" placeholder="Title">
<div class="absolute inset-x-0 bottom-0 border-t border-transparent peer-focus:border-t-2 peer-focus:border-primary-600" aria-hidden="true"></div>
</div>
<input type="hidden" name="content" value="{{ draft.content }}" x-ref="content">
<button type="submit" class="btn btn-primary">Save</button>
</form>
<template x-if="isLoaded()">
<div class="flex items-center gap-2 mt-2">
<button
@click="undo()"
class="p-1 hover:bg-gray-200 dark:hover:bg-gray-700 disabled:text-gray-600 disabled:hover:bg-transparent"
:disabled="!canUndo"
>
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" viewBox="0 0 24 24">
<g fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2">
<path d="M9 14L4 9l5-5" />
<path d="M4 9h10.5a5.5 5.5 0 0 1 5.5 5.5a5.5 5.5 0 0 1-5.5 5.5H11" />
</g>
</svg>
</button>
<button
@click="redo()"
class="p-1 hover:bg-gray-200 dark:hover:bg-gray-700 disabled:text-gray-600 disabled:hover:bg-transparent"
:disabled="!canRedo"
>
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" viewBox="0 0 24 24">
<g fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2">
<path d="m15 14l5-5l-5-5" />
<path d="M20 9H9.5A5.5 5.5 0 0 0 4 14.5A5.5 5.5 0 0 0 9.5 20H13" />
</g>
</svg>
</button>
<span class="h-full w-px bg-gray-200 dark:bg-gray-700 rounded-md"></span>
<button
@click="toggleHeading({ level: 2 })"
class="p-1 hover:bg-gray-200 dark:hover:bg-gray-700"
:class="{ 'bg-gray-100 dark:bg-gray-800 text-primary-500': isActive('heading', { level: 2 }, updatedAt) }"
>
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" viewBox="0 0 24 24">
<path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 12h8m-8 6V6m8 12V6m9 12h-4c0-4 4-3 4-6c0-1.5-2-2.5-4-1" />
</svg>
</button>
<button
@click="toggleHeading({ level: 3 })"
class="p-1 hover:bg-gray-200 dark:hover:bg-gray-700"
:class="{ 'bg-gray-100 dark:bg-gray-800 text-primary-500': isActive('heading', { level: 3 }, updatedAt) }"
>
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" viewBox="0 0 24 24">
<path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 12h8m-8 6V6m8 12V6m5.5 4.5c1.7-1 3.5 0 3.5 1.5a2 2 0 0 1-2 2m-2 3.5c2 1.5 4 .3 4-1.5a2 2 0 0 0-2-2" />
</svg>
</button>
<button
@click="toggleHeading({ level: 4 })"
class="p-1 hover:bg-gray-200 dark:hover:bg-gray-700"
:class="{ 'bg-gray-100 dark:bg-gray-800 text-primary-500': isActive('heading', { level: 4 }, updatedAt) }"
>
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" viewBox="0 0 24 24">
<path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 12h8m-8 6V6m8 12V6m5 4v4h4m0-4v8" />
</svg>
</button>
<span class="h-full w-px bg-gray-200 dark:bg-gray-700 rounded-md"></span>
<button
@click="toggleBold()"
class="p-1 hover:bg-gray-200 dark:hover:bg-gray-700"
:class="{ 'bg-gray-100 dark:bg-gray-800 text-primary-500' : isActive('bold', updatedAt) }"
>
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" viewBox="0 0 24 24">
<path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 12h9a4 4 0 0 1 0 8H7a1 1 0 0 1-1-1V5a1 1 0 0 1 1-1h7a4 4 0 0 1 0 8" />
</svg>
</button>
<button
@click="toggleItalic()"
class="p-1 hover:bg-gray-200 dark:hover:bg-gray-700"
:class="{ 'bg-gray-100 dark:bg-gray-800 text-primary-500' : isActive('italic', updatedAt) }"
>
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" viewBox="0 0 24 24">
<path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 4h-9m4 16H5M15 4L9 20" />
</svg>
</button>
<button
@click="toggleUnderline()"
class="p-1 hover:bg-gray-200 dark:hover:bg-gray-700"
:class="{ 'bg-gray-100 dark:bg-gray-800 text-primary-500' : isActive('underline', updatedAt) }"
>
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" viewBox="0 0 24 24">
<path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 4v6a6 6 0 0 0 12 0V4M4 20h16" />
</svg>
</button>
<button
@click="toggleStrike()"
class="p-1 hover:bg-gray-200 dark:hover:bg-gray-700"
:class="{ 'bg-gray-100 dark:bg-gray-800 text-primary-500' : isActive('strike', updatedAt) }"
>
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" viewBox="0 0 24 24">
<path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M16 4H9a3 3 0 0 0-2.83 4M14 12a4 4 0 0 1 0 8H6m-2-8h16" />
</svg>
</button>
<button
@click="toggleCode()"
class="p-1 hover:bg-gray-200 dark:hover:bg-gray-700"
:class="{ 'bg-gray-100 dark:bg-gray-800 text-primary-500' : isActive('code', updatedAt) }"
>
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" viewBox="0 0 24 24">
<path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="m16 18l6-6l-6-6M8 6l-6 6l6 6" />
</svg>
</button>
<span class="h-full w-px bg-gray-200 dark:bg-gray-700 rounded-md"></span>
<button
@click="toggleBulletList()"
class="p-1 hover:bg-gray-200 dark:hover:bg-gray-700"
:class="{ 'bg-gray-100 dark:bg-gray-800 text-primary-500': isActive('bulletList', updatedAt) }"
>
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" viewBox="0 0 24 24">
<path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 6h13M8 12h13M8 18h13M3 6h.01M3 12h.01M3 18h.01" />
</svg>
</button>
<button
@click="toggleOrderedList()"
class="p-1 hover:bg-gray-200 dark:hover:bg-gray-700"
:class="{ 'bg-gray-100 dark:bg-gray-800 text-primary-500': isActive('orderedList', updatedAt) }"
>
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" viewBox="0 0 24 24">
<path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 6h11m-11 6h11m-11 6h11M4 6h1v4m-1 0h2m0 8H4c0-1 2-2 2-3s-1-1.5-2-1" />
</svg>
</button>
<button
@click="toggleTaskList()"
class="p-1 hover:bg-gray-200 dark:hover:bg-gray-700"
:class="{ 'bg-gray-100 dark:bg-gray-800 text-primary-500': isActive('taskList', updatedAt) }"
>
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" viewBox="0 0 24 24">
<g fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2">
<rect width="6" height="6" x="3" y="5" rx="1" />
<path d="m3 17l2 2l4-4m4-9h8m-8 6h8m-8 6h8" />
</g>
</svg>
</button>
<span class="h-full w-px bg-gray-200 dark:bg-gray-700 rounded-md"></span>
<button
@click="toggleBlockquote()"
class="p-1 hover:bg-gray-200 dark:hover:bg-gray-700"
:class="{ 'bg-gray-100 dark:bg-gray-800 text-primary-500' : isActive('blockquote', updatedAt) }"
>
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" viewBox="0 0 24 24">
<path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17 6H3m18 6H8m13 6H8m-5-6v6" />
</svg>
</button>
<button
@click="toggleCodeBlock()"
class="p-1 hover:bg-gray-200 dark:hover:bg-gray-700"
:class="{ 'bg-gray-100 dark:bg-gray-800 text-primary-500' : isActive('codeBlock', updatedAt) }"
>
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" viewBox="0 0 24 24">
<g fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2">
<path d="M10 9.5L8 12l2 2.5m4-5l2 2.5l-2 2.5" />
<rect width="18" height="18" x="3" y="3" rx="2" />
</g>
</svg>
</button>
</div>
</template>
<div x-ref="element" class="mt-2 prose dark:prose-invert max-w-none w-full h-full flex overflow-hidden *:w-full *:h-full *:overflow-auto"></div>
</div>
{% endblock content %}
{% block body_extra %}
<script type="module">
import Alpine from 'https://esm.sh/alpinejs'
import focus from 'https://esm.sh/@alpinejs/focus'
import ui from 'https://esm.sh/@alpinejs/ui'
import { Editor, Extension } from 'https://esm.sh/@tiptap/core'
import { generateHTML } from 'https://esm.sh/@tiptap/html'
import StarterKit from 'https://esm.sh/@tiptap/starter-kit'
import Link from 'https://esm.sh/@tiptap/extension-link'
import Placeholder from 'https://esm.sh/@tiptap/extension-placeholder'
import TaskList from 'https://esm.sh/@tiptap/extension-task-list'
import TaskItem from 'https://esm.sh/@tiptap/extension-task-item'
import Typography from 'https://esm.sh/@tiptap/extension-typography'
import Underline from 'https://esm.sh/@tiptap/extension-underline'
document.addEventListener('alpine:init', () => {
Alpine.data('editor', (content) => {
let editor // Alpine's reactive engine automatically wraps component properties in proxy objects. Attempting to use a proxied editor instance to apply a transaction will cause a "Range Error: Applying a mismatched transaction", so be sure to unwrap it using Alpine.raw(), or simply avoid storing your editor as a component property, as shown in this example.
return {
updatedAt: Date.now(), // force Alpine to rerender on selection change
canUndo: false,
canRedo: false,
init() {
const _this = this
const extensions = [
Link,
Placeholder.configure({
placeholder: 'Write something …',
}),
StarterKit.configure({
heading: {
levels: [2, 3, 4],
},
}),
TaskItem,
TaskList,
Typography,
Underline,
Extension.create({
name: 'submitExtension',
addKeyboardShortcuts() {
return {
'Mod-Enter': () => {
_this.$refs.form.submit()
return true
}
}
},
}),
]
editor = new Editor({
element: this.$refs.element,
extensions: extensions,
content: content && generateHTML(content, extensions),
onCreate({ editor }) {
_this.updatedAt = Date.now()
editor.chain().focus('end').run()
},
onUpdate({ editor }) {
_this.updatedAt = Date.now()
_this.$refs.content.value = JSON.stringify(editor.getJSON())
_this.canUndo = editor.can().undo()
_this.canRedo = editor.can().redo()
},
onSelectionUpdate({ editor }) {
_this.updatedAt = Date.now()
}
})
},
isLoaded() {
return editor
},
isActive(type, opts = {}) {
return editor.isActive(type, opts)
},
undo() {
editor.chain().focus().undo().run()
},
redo() {
editor.chain().focus().redo().run()
},
toggleHeading(opts) {
editor.chain().focus().toggleHeading(opts).run()
},
toggleBold() {
editor.chain().focus().toggleBold().run()
},
toggleItalic() {
editor.chain().focus().toggleItalic().run()
},
toggleUnderline() {
editor.chain().focus().toggleUnderline().run()
},
toggleStrike() {
editor.chain().focus().toggleStrike().run()
},
toggleBulletList() {
editor.chain().focus().toggleBulletList().run()
},
toggleOrderedList() {
editor.chain().focus().toggleOrderedList().run()
},
toggleTaskList() {
editor.chain().focus().toggleTaskList().run()
},
toggleCode() {
editor.chain().focus().toggleCode().run()
},
toggleBlockquote() {
editor.chain().focus().toggleBlockquote().run()
},
toggleCodeBlock() {
editor.chain().focus().toggleCodeBlock().run()
}
}
})
})
window.Alpine = Alpine
Alpine.plugin(focus)
Alpine.plugin(ui)
Alpine.start()
</script>
{% endblock body_extra %}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment