-
-
Save joshuadavidthomas/d936b1ad946affde7ca82d9aa0afce10 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!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> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
{% 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