diff --git a/components/content/ContentRich.setup.ts b/components/content/ContentRich.setup.ts
index 77835b81..39ee0d39 100644
--- a/components/content/ContentRich.setup.ts
+++ b/components/content/ContentRich.setup.ts
@@ -14,6 +14,6 @@ const emojiObject = emojisArrayToObject(props.emojis || [])
export default () => h(
'div',
- { class: 'rich-content' },
+ { class: 'content-rich' },
contentToVNode(props.content, emojiObject),
)
diff --git a/components/modal/ModalContainer.vue b/components/modal/ModalContainer.vue
index 46b3803c..a512513a 100644
--- a/components/modal/ModalContainer.vue
+++ b/components/modal/ModalContainer.vue
@@ -13,6 +13,6 @@ import { isPreviewHelpOpen, isPublishDialogOpen, isSigninDialogOpen, isUserSwitc
-
+
diff --git a/components/publish/PublishWidget.vue b/components/publish/PublishWidget.vue
index 86a04b42..800edf31 100644
--- a/components/publish/PublishWidget.vue
+++ b/components/publish/PublishWidget.vue
@@ -2,6 +2,7 @@
import type { CreateStatusParams, StatusVisibility } from 'masto'
import { fileOpen } from 'browser-fs-access'
import { useDropZone } from '@vueuse/core'
+import { EditorContent } from '@tiptap/vue-3'
const {
draftKey,
@@ -15,10 +16,19 @@ const {
expanded?: boolean
}>()
-const expanded = $ref(_expanded)
+let isExpanded = $ref(_expanded)
let isSending = $ref(false)
let { draft } = $(useDraft(draftKey, inReplyToId))
+const { editor } = useTiptap({
+ content: toRef(draft.params, 'status'),
+ placeholder,
+ autofocus: isExpanded,
+ onSubimit: publish,
+ onFocus() { isExpanded = true },
+ onPaste: handlePaste,
+})
+
const status = $computed(() => {
return {
...draft.params,
@@ -87,11 +97,16 @@ function chooseVisibility(visibility: StatusVisibility) {
}
async function publish() {
+ if (process.dev) {
+ alert(JSON.stringify(draft.params, null, 2))
+ return
+ }
try {
isSending = true
if (!draft.editingStatus)
await masto.statuses.create(status)
- else await masto.statuses.update(draft.editingStatus.id, status)
+ else
+ await masto.statuses.update(draft.editingStatus.id, status)
draft = getDefaultDraft({ inReplyToId })
isPublishDialogOpen.value = false
@@ -111,6 +126,7 @@ async function onDrop(files: File[] | null) {
const { isOverDropZone } = useDropZone(dropZoneRef, onDrop)
onUnmounted(() => {
+ // Remove draft if it's empty
if (!draft.attachments.length && !draft.params.status) {
nextTick(() => {
delete currentUserDrafts.value[draftKey]
@@ -138,7 +154,7 @@ onUnmounted(() => {
@@ -151,22 +167,17 @@ onUnmounted(() => {
>
-
-
+
+
+
-
-
-
+
+
+
+
+
+
+
diff --git a/components/tiptap/TiptapMentionList.vue b/components/tiptap/TiptapMentionList.vue
new file mode 100644
index 00000000..0b286994
--- /dev/null
+++ b/components/tiptap/TiptapMentionList.vue
@@ -0,0 +1,58 @@
+
+
+
+
+
+
+
+
+ No result
+
+
+
diff --git a/composables/tiptap.ts b/composables/tiptap.ts
new file mode 100644
index 00000000..dc751ffd
--- /dev/null
+++ b/composables/tiptap.ts
@@ -0,0 +1,93 @@
+import { Extension, useEditor } from '@tiptap/vue-3'
+import Placeholder from '@tiptap/extension-placeholder'
+import Document from '@tiptap/extension-document'
+import Paragraph from '@tiptap/extension-paragraph'
+import Text from '@tiptap/extension-text'
+import Mention from '@tiptap/extension-mention'
+import CodeBlock from '@tiptap/extension-code-block'
+import CharacterCount from '@tiptap/extension-character-count'
+import { Plugin } from 'prosemirror-state'
+
+import type { Ref } from 'vue'
+import { HashSuggestion, MentionSuggestion } from './tiptap/suggestion'
+import { POST_CHARS_LIMIT } from '~/constants'
+
+export interface UseTiptapOptions {
+ content: Ref
+ placeholder: string
+ onSubimit: () => void
+ onFocus: () => void
+ onPaste: (event: ClipboardEvent) => void
+ autofocus: boolean
+}
+
+export function useTiptap(options: UseTiptapOptions) {
+ const {
+ autofocus,
+ content,
+ placeholder,
+ } = options
+
+ const editor = useEditor({
+ content: content.value,
+ extensions: [
+ Document,
+ Paragraph,
+ Text,
+ Mention.configure({
+ suggestion: MentionSuggestion,
+ }),
+ Mention.configure({
+ suggestion: HashSuggestion,
+ }),
+ Placeholder.configure({
+ placeholder,
+ }),
+ CharacterCount.configure({
+ limit: POST_CHARS_LIMIT,
+ }),
+ CodeBlock,
+ Extension.create({
+ name: 'api',
+ addKeyboardShortcuts() {
+ return {
+ 'Mod-Enter': () => {
+ options.onSubimit()
+ return true
+ },
+ }
+ },
+ onFocus() {
+ options.onFocus()
+ },
+ addProseMirrorPlugins() {
+ return [
+ new Plugin({
+ props: {
+ handleDOMEvents: {
+ paste(view, event) {
+ options.onPaste(event)
+ },
+ },
+ },
+ }),
+ ]
+ },
+ }),
+ ],
+ onUpdate({ editor }) {
+ content.value = editor.getHTML()
+ },
+ editorProps: {
+ attributes: {
+ class: 'content-editor content-rich',
+ },
+ },
+ autofocus,
+ editable: true,
+ })
+
+ return {
+ editor,
+ }
+}
diff --git a/composables/tiptap/suggestion.ts b/composables/tiptap/suggestion.ts
new file mode 100644
index 00000000..48d9f675
--- /dev/null
+++ b/composables/tiptap/suggestion.ts
@@ -0,0 +1,83 @@
+import type { GetReferenceClientRect, Instance } from 'tippy.js'
+import tippy from 'tippy.js'
+import { VueRenderer } from '@tiptap/vue-3'
+import type { SuggestionOptions } from '@tiptap/suggestion'
+import { PluginKey } from 'prosemirror-state'
+import TiptapMentionList from '~/components/tiptap/TiptapMentionList.vue'
+
+export const MentionSuggestion: Partial = {
+ pluginKey: new PluginKey('mention'),
+ char: '@',
+ items({ query }) {
+ // TODO: query
+ return [
+ 'TODO MENTION QUERY', 'Lea Thompson', 'Cyndi Lauper', 'Tom Cruise', 'Madonna', 'Jerry Hall', 'Joan Collins', 'Winona Ryder', 'Christina Applegate', 'Alyssa Milano', 'Molly Ringwald', 'Ally Sheedy', 'Debbie Harry', 'Olivia Newton-John', 'Elton John', 'Michael J. Fox', 'Axl Rose', 'Emilio Estevez', 'Ralph Macchio', 'Rob Lowe', 'Jennifer Grey', 'Mickey Rourke', 'John Cusack', 'Matthew Broderick', 'Justine Bateman', 'Lisa Bonet',
+ ].filter(item => item.toLowerCase().startsWith(query.toLowerCase())).slice(0, 5)
+ },
+ render: createSuggestionRenderer(),
+}
+
+export const HashSuggestion: Partial = {
+ pluginKey: new PluginKey('hashtag'),
+ char: '#',
+ items({ query }) {
+ // TODO: query
+ return [
+ 'TODO HASH QUERY',
+ ].filter(item => item.toLowerCase().startsWith(query.toLowerCase())).slice(0, 5)
+ },
+ render: createSuggestionRenderer(),
+}
+
+function createSuggestionRenderer(): SuggestionOptions['render'] {
+ return () => {
+ let component: VueRenderer
+ let popup: Instance
+
+ return {
+ onStart: (props) => {
+ component = new VueRenderer(TiptapMentionList, {
+ props,
+ editor: props.editor,
+ })
+
+ if (!props.clientRect)
+ return
+
+ popup = tippy(document.body, {
+ getReferenceClientRect: props.clientRect as GetReferenceClientRect,
+ appendTo: () => document.body,
+ content: component.element,
+ showOnCreate: true,
+ interactive: true,
+ trigger: 'manual',
+ placement: 'bottom-start',
+ })
+ },
+
+ onUpdate(props) {
+ component.updateProps(props)
+
+ if (!props.clientRect)
+ return
+
+ popup?.setProps({
+ getReferenceClientRect: props.clientRect as GetReferenceClientRect,
+ })
+ },
+
+ onKeyDown(props) {
+ if (props.event.key === 'Escape') {
+ popup?.hide()
+ return true
+ }
+ return component?.ref?.onKeyDown(props.event)
+ },
+
+ onExit() {
+ popup?.destroy()
+ component?.destroy()
+ },
+ }
+ }
+}
diff --git a/constants/index.ts b/constants/index.ts
index 6a074d95..f28125ce 100644
--- a/constants/index.ts
+++ b/constants/index.ts
@@ -4,6 +4,8 @@ export const HOST_DOMAIN = process.dev
? 'http://localhost:3000'
: 'https://elk.zone'
+export const POST_CHARS_LIMIT = 500
+
export const DEFAULT_SERVER = 'mas.to'
export const STORAGE_KEY_DRAFTS = 'elk-drafts'
diff --git a/nuxt.config.ts b/nuxt.config.ts
index 24b87123..84832a35 100644
--- a/nuxt.config.ts
+++ b/nuxt.config.ts
@@ -15,6 +15,7 @@ export default defineNuxtConfig({
'floating-vue/dist/style.css',
'~/styles/vars.css',
'~/styles/global.css',
+ '~/styles/tiptap.css',
'~/styles/dropdown.css',
],
alias: {
diff --git a/package.json b/package.json
index d4c4e580..43032c7c 100644
--- a/package.json
+++ b/package.json
@@ -23,6 +23,15 @@
"@iconify-json/ri": "^1.1.4",
"@iconify-json/twemoji": "^1.1.6",
"@pinia/nuxt": "^0.4.5",
+ "@tiptap/extension-character-count": "2.0.0-beta.203",
+ "@tiptap/extension-code-block": "2.0.0-beta.203",
+ "@tiptap/extension-mention": "2.0.0-beta.203",
+ "@tiptap/extension-paragraph": "2.0.0-beta.203",
+ "@tiptap/extension-placeholder": "2.0.0-beta.203",
+ "@tiptap/extension-text": "2.0.0-beta.203",
+ "@tiptap/starter-kit": "2.0.0-beta.203",
+ "@tiptap/suggestion": "2.0.0-beta.203",
+ "@tiptap/vue-3": "2.0.0-beta.203",
"@types/fs-extra": "^9.0.13",
"@types/js-yaml": "^4.0.5",
"@types/prettier": "^2.7.1",
@@ -51,6 +60,7 @@
"sanitize-html": "^2.7.3",
"shiki": "^0.11.1",
"theme-vitesse": "^0.6.0",
+ "tippy.js": "^6.3.7",
"typescript": "^4.9.3",
"ufo": "^1.0.0",
"unplugin-auto-import": "^0.11.5",
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 2ba8c630..fc602de4 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -8,6 +8,15 @@ specifiers:
'@iconify-json/ri': ^1.1.4
'@iconify-json/twemoji': ^1.1.6
'@pinia/nuxt': ^0.4.5
+ '@tiptap/extension-character-count': 2.0.0-beta.203
+ '@tiptap/extension-code-block': 2.0.0-beta.203
+ '@tiptap/extension-mention': 2.0.0-beta.203
+ '@tiptap/extension-paragraph': 2.0.0-beta.203
+ '@tiptap/extension-placeholder': 2.0.0-beta.203
+ '@tiptap/extension-text': 2.0.0-beta.203
+ '@tiptap/starter-kit': 2.0.0-beta.203
+ '@tiptap/suggestion': 2.0.0-beta.203
+ '@tiptap/vue-3': 2.0.0-beta.203
'@types/fs-extra': ^9.0.13
'@types/js-yaml': ^4.0.5
'@types/prettier': ^2.7.1
@@ -36,6 +45,7 @@ specifiers:
sanitize-html: ^2.7.3
shiki: ^0.11.1
theme-vitesse: ^0.6.0
+ tippy.js: ^6.3.7
typescript: ^4.9.3
ufo: ^1.0.0
unplugin-auto-import: ^0.11.5
@@ -49,6 +59,15 @@ devDependencies:
'@iconify-json/ri': 1.1.4
'@iconify-json/twemoji': 1.1.6
'@pinia/nuxt': 0.4.5_typescript@4.9.3
+ '@tiptap/extension-character-count': 2.0.0-beta.203
+ '@tiptap/extension-code-block': 2.0.0-beta.203
+ '@tiptap/extension-mention': 2.0.0-beta.203_66kmopqpbsmmkalw2shrvalvci
+ '@tiptap/extension-paragraph': 2.0.0-beta.203
+ '@tiptap/extension-placeholder': 2.0.0-beta.203
+ '@tiptap/extension-text': 2.0.0-beta.203
+ '@tiptap/starter-kit': 2.0.0-beta.203
+ '@tiptap/suggestion': 2.0.0-beta.203
+ '@tiptap/vue-3': 2.0.0-beta.203
'@types/fs-extra': 9.0.13
'@types/js-yaml': 4.0.5
'@types/prettier': 2.7.1
@@ -77,6 +96,7 @@ devDependencies:
sanitize-html: 2.7.3
shiki: 0.11.1
theme-vitesse: 0.6.0
+ tippy.js: 6.3.7
typescript: 4.9.3
ufo: 1.0.0
unplugin-auto-import: 0.11.5
@@ -926,6 +946,10 @@ packages:
resolution: {integrity: sha512-a5Sab1C4/icpTZVzZc5Ghpz88yQtGOyNqYXcZgOssB2uuAr+wF/MvN6bgtW32q7HHrvBki+BsZ0OuNv6EV3K9g==}
dev: true
+ /@popperjs/core/2.11.6:
+ resolution: {integrity: sha512-50/17A98tWUfQ176raKiOGXuYpLyyVMkxxG6oylzL3BPOlA6ADGdK7EYunSa4I064xerltq9TGXs8HmOk5E+vw==}
+ dev: true
+
/@rollup/plugin-alias/4.0.2_rollup@2.79.1:
resolution: {integrity: sha512-1hv7dBOZZwo3SEupxn4UA2N0EDThqSSS+wI1St1TNTBtOZvUchyIClyHcnDcjjrReTPZ47Faedrhblv4n+T5UQ==}
engines: {node: '>=14.0.0'}
@@ -1066,6 +1090,283 @@ packages:
rollup: 2.79.1
dev: true
+ /@tiptap/core/2.0.0-beta.203:
+ resolution: {integrity: sha512-iRkFv4jjRtI7b18quyO3C8rilD8T24S3KYrZ3idRRw+ifO0dTeuDsRKjlcDT815zJRYdz99s5/lGq2ES0vC2gA==}
+ dependencies:
+ prosemirror-commands: 1.3.1
+ prosemirror-keymap: 1.2.0
+ prosemirror-model: 1.18.3
+ prosemirror-schema-list: 1.2.2
+ prosemirror-state: 1.4.2
+ prosemirror-transform: 1.7.0
+ prosemirror-view: 1.29.1
+ dev: true
+
+ /@tiptap/extension-blockquote/2.0.0-beta.203_ywil7uncaz323pl33xz5cqvwo4:
+ resolution: {integrity: sha512-e8jE9AZ5L21AO6exMxFWFgU0c0ygxWVcuYJRMiThNhj38NWCNoHGHDqXbOjh2kM9NoRnad9erFuilVgYRmXcTw==}
+ peerDependencies:
+ '@tiptap/core': ^2.0.0-beta.1
+ dependencies:
+ '@tiptap/core': 2.0.0-beta.203
+ dev: true
+
+ /@tiptap/extension-bold/2.0.0-beta.203_ywil7uncaz323pl33xz5cqvwo4:
+ resolution: {integrity: sha512-MuhBt7O44hJZ/5N42wVSN/9YB0iXkFgRkltbXPaWDqrXQjbZh9NRXMbQw7pOuW+3gn4NmUP5cd6pe9qHII1MtA==}
+ peerDependencies:
+ '@tiptap/core': ^2.0.0-beta.193
+ dependencies:
+ '@tiptap/core': 2.0.0-beta.203
+ dev: true
+
+ /@tiptap/extension-bubble-menu/2.0.0-beta.203:
+ resolution: {integrity: sha512-5EdYWCi6SyKVpY6xPqD5S0u7Jz8CG/FjgnFng0FBBJ2dCvxbeVdwTWL/WwN3KmIkY8T91ScQtbJb0bruC+GIUw==}
+ peerDependencies:
+ '@tiptap/core': ^2.0.0-beta.193
+ dependencies:
+ prosemirror-state: 1.4.2
+ prosemirror-view: 1.29.1
+ tippy.js: 6.3.7
+ dev: true
+
+ /@tiptap/extension-bullet-list/2.0.0-beta.203_ywil7uncaz323pl33xz5cqvwo4:
+ resolution: {integrity: sha512-u9uY4XL0y9cIwEsm8008fpMPGXr9IVxbbmRXGh19oRnBPS1C3vnxGmgThrXojYwEWkp+5NimoH/E6ljUbuNbBQ==}
+ peerDependencies:
+ '@tiptap/core': ^2.0.0-beta.193
+ dependencies:
+ '@tiptap/core': 2.0.0-beta.203
+ dev: true
+
+ /@tiptap/extension-character-count/2.0.0-beta.203:
+ resolution: {integrity: sha512-j/XzlqVXATzKqbStJyU2VlykIIjLraki8TK9vTTLagQB6lYTv/AJT6L/Y0Ei7yUSjKYOL6sL7/Rze8czqeykNw==}
+ peerDependencies:
+ '@tiptap/core': ^2.0.0-beta.193
+ dependencies:
+ prosemirror-model: 1.18.3
+ prosemirror-state: 1.4.2
+ dev: true
+
+ /@tiptap/extension-code-block/2.0.0-beta.203:
+ resolution: {integrity: sha512-wXN1POlJBA9NG0eTKRGoBQFX40+TQUvfYi3i1mDk47sNdtbITJIFR3WRkljqSWrFbKdLFKPADUaQ+k6f2KZm/w==}
+ peerDependencies:
+ '@tiptap/core': ^2.0.0-beta.193
+ dependencies:
+ prosemirror-state: 1.4.2
+ dev: true
+
+ /@tiptap/extension-code-block/2.0.0-beta.203_ywil7uncaz323pl33xz5cqvwo4:
+ resolution: {integrity: sha512-wXN1POlJBA9NG0eTKRGoBQFX40+TQUvfYi3i1mDk47sNdtbITJIFR3WRkljqSWrFbKdLFKPADUaQ+k6f2KZm/w==}
+ peerDependencies:
+ '@tiptap/core': ^2.0.0-beta.193
+ dependencies:
+ '@tiptap/core': 2.0.0-beta.203
+ prosemirror-state: 1.4.2
+ dev: true
+
+ /@tiptap/extension-code/2.0.0-beta.203_ywil7uncaz323pl33xz5cqvwo4:
+ resolution: {integrity: sha512-iYC26EI4V4aAh10dq6LuCbPgHHrNCURotn2jA90fOjFnCspIuAXdQsPPV6syCLIIUdjFT8t/dscijlhzMYzVOg==}
+ peerDependencies:
+ '@tiptap/core': ^2.0.0-beta.193
+ dependencies:
+ '@tiptap/core': 2.0.0-beta.203
+ dev: true
+
+ /@tiptap/extension-document/2.0.0-beta.203_ywil7uncaz323pl33xz5cqvwo4:
+ resolution: {integrity: sha512-H0HyFvnlMl0dHujGO+/+xjmOgu3aTslWVNLT2hOxBvfCAwW4hLmxQVaaMr+75so68rr1ndTPGUnPtXlQHvtzbA==}
+ peerDependencies:
+ '@tiptap/core': ^2.0.0-beta.193
+ dependencies:
+ '@tiptap/core': 2.0.0-beta.203
+ dev: true
+
+ /@tiptap/extension-dropcursor/2.0.0-beta.203_ywil7uncaz323pl33xz5cqvwo4:
+ resolution: {integrity: sha512-oR4WjZdNcxYeYuKDzugMgEZGH7c6uzkV6InewPpHSuKVcbzjF1HbS/EHgpBSRFvLRYJ+nrbJMzERLWn9ZS5njA==}
+ peerDependencies:
+ '@tiptap/core': ^2.0.0-beta.193
+ dependencies:
+ '@tiptap/core': 2.0.0-beta.203
+ prosemirror-dropcursor: 1.5.0
+ dev: true
+
+ /@tiptap/extension-floating-menu/2.0.0-beta.203:
+ resolution: {integrity: sha512-QiWXX9vmDTvP6jo/lwZpQ/7Sf2XCeD1RQQmWIC+cohfxdd606dMVvFhYt8OofjoWn+e4uxobEtfxkvA418zUkg==}
+ peerDependencies:
+ '@tiptap/core': ^2.0.0-beta.193
+ dependencies:
+ prosemirror-state: 1.4.2
+ prosemirror-view: 1.29.1
+ tippy.js: 6.3.7
+ dev: true
+
+ /@tiptap/extension-gapcursor/2.0.0-beta.203_ywil7uncaz323pl33xz5cqvwo4:
+ resolution: {integrity: sha512-HwY2dwAyIBOy+V2+Dbyw59POtAxz7UER9R470SGabi9/M7rBhZYrzL0gs+V9OB4AH1OO5hib02l8Dcnq+KKF7A==}
+ peerDependencies:
+ '@tiptap/core': ^2.0.0-beta.193
+ dependencies:
+ '@tiptap/core': 2.0.0-beta.203
+ prosemirror-gapcursor: 1.3.1
+ dev: true
+
+ /@tiptap/extension-hard-break/2.0.0-beta.203_ywil7uncaz323pl33xz5cqvwo4:
+ resolution: {integrity: sha512-FfMufoiIwzNTsTZkb6qaNpJbyh6dOYagnGzlDmjyvc6+wqdJWE4sxwVAcMO0j1tvCdkaozWeWSuJzYv4xZKMFA==}
+ peerDependencies:
+ '@tiptap/core': ^2.0.0-beta.193
+ dependencies:
+ '@tiptap/core': 2.0.0-beta.203
+ dev: true
+
+ /@tiptap/extension-heading/2.0.0-beta.203_ywil7uncaz323pl33xz5cqvwo4:
+ resolution: {integrity: sha512-pjBQNwWya+eGffesgn/Fz+7xC+RoiRVNo5xjahZVSP2MVZwbvcq/UV+fIut1nu9WJgPfAPkYnBSXmbPp0SRB0g==}
+ peerDependencies:
+ '@tiptap/core': ^2.0.0-beta.193
+ dependencies:
+ '@tiptap/core': 2.0.0-beta.203
+ dev: true
+
+ /@tiptap/extension-history/2.0.0-beta.203_ywil7uncaz323pl33xz5cqvwo4:
+ resolution: {integrity: sha512-3gaplasYTuHepP1gnP/p5qjN5Sz9QudXz3Vue+2j1XulTFDjoB83j4FSujnAPshw2hePXNxv1mdHeeJ219Odxw==}
+ peerDependencies:
+ '@tiptap/core': ^2.0.0-beta.193
+ dependencies:
+ '@tiptap/core': 2.0.0-beta.203
+ prosemirror-history: 1.3.0
+ dev: true
+
+ /@tiptap/extension-horizontal-rule/2.0.0-beta.203_ywil7uncaz323pl33xz5cqvwo4:
+ resolution: {integrity: sha512-mvERa4IfFBPVG1he1b+urtQD8mk9nGzZif9yPMfcpDIW8ewJv/MUH/mEASxZbb7cqXOMAWZqp1rVpH/MLBgtfw==}
+ peerDependencies:
+ '@tiptap/core': ^2.0.0-beta.193
+ dependencies:
+ '@tiptap/core': 2.0.0-beta.203
+ prosemirror-state: 1.4.2
+ dev: true
+
+ /@tiptap/extension-italic/2.0.0-beta.203_ywil7uncaz323pl33xz5cqvwo4:
+ resolution: {integrity: sha512-7X/Z6V2DnziNfHhIoCLHI+EKQoaz0nyjPvNvs7wfSno6LTYUz33bXBpPF7gNZPyBJK/F/znC2Mg2l6XzcI7c+g==}
+ peerDependencies:
+ '@tiptap/core': ^2.0.0-beta.193
+ dependencies:
+ '@tiptap/core': 2.0.0-beta.203
+ dev: true
+
+ /@tiptap/extension-list-item/2.0.0-beta.203_ywil7uncaz323pl33xz5cqvwo4:
+ resolution: {integrity: sha512-hfVxILSkLGKH4AVkj1imyHu1hAJjV6gWPDm7zQDi9MtQEoxU3fH+5nLwedsrveDZZHguwjHc/B+JyGcljzVeeg==}
+ peerDependencies:
+ '@tiptap/core': ^2.0.0-beta.193
+ dependencies:
+ '@tiptap/core': 2.0.0-beta.203
+ dev: true
+
+ /@tiptap/extension-mention/2.0.0-beta.203_66kmopqpbsmmkalw2shrvalvci:
+ resolution: {integrity: sha512-GR29PExMNE0mnQ4r30fgi+6QStrtxErRomnxKWlMMCdb+pHEgKrk1rbcZeLMc+efoainDMzN4HkFSAWmdKTSDw==}
+ peerDependencies:
+ '@tiptap/core': ^2.0.0-beta.193
+ '@tiptap/suggestion': ^2.0.0-beta.193
+ dependencies:
+ '@tiptap/suggestion': 2.0.0-beta.203
+ prosemirror-model: 1.18.3
+ prosemirror-state: 1.4.2
+ dev: true
+
+ /@tiptap/extension-ordered-list/2.0.0-beta.203_ywil7uncaz323pl33xz5cqvwo4:
+ resolution: {integrity: sha512-Dp+SzrJ4yrFheng8pAtow19/wviC2g4QcxT88ZVz4wjC6JTo0M6sOQg9slxvx+Q+VbqrmPdikbqTiE/Ef416ig==}
+ peerDependencies:
+ '@tiptap/core': ^2.0.0-beta.193
+ dependencies:
+ '@tiptap/core': 2.0.0-beta.203
+ dev: true
+
+ /@tiptap/extension-paragraph/2.0.0-beta.203:
+ resolution: {integrity: sha512-kqsW7KPl2orCEJiNjCRCY/p06TrTTiq2n2hxatFRbHwvpQC4Z71JgaRJ/28WCL61QVy9v2UwNmCT2NFxUvMLgg==}
+ peerDependencies:
+ '@tiptap/core': ^2.0.0-beta.193
+ dev: true
+
+ /@tiptap/extension-paragraph/2.0.0-beta.203_ywil7uncaz323pl33xz5cqvwo4:
+ resolution: {integrity: sha512-kqsW7KPl2orCEJiNjCRCY/p06TrTTiq2n2hxatFRbHwvpQC4Z71JgaRJ/28WCL61QVy9v2UwNmCT2NFxUvMLgg==}
+ peerDependencies:
+ '@tiptap/core': ^2.0.0-beta.193
+ dependencies:
+ '@tiptap/core': 2.0.0-beta.203
+ dev: true
+
+ /@tiptap/extension-placeholder/2.0.0-beta.203:
+ resolution: {integrity: sha512-MMwCzCZdapY6+LIubo/c4KmdXM1NKc2sBu8ahWF97h9pfs7UGxYDtWoAAUQlV4IzFiC5OhpYHwhStOaD3LhWjw==}
+ peerDependencies:
+ '@tiptap/core': ^2.0.0-beta.193
+ dependencies:
+ prosemirror-model: 1.18.3
+ prosemirror-state: 1.4.2
+ prosemirror-view: 1.29.1
+ dev: true
+
+ /@tiptap/extension-strike/2.0.0-beta.203_ywil7uncaz323pl33xz5cqvwo4:
+ resolution: {integrity: sha512-CxgaJybQs36AUn1PrXbiNbqliYkf4n7LM/NvqtkoXPLISvndqAEQGmx1hS0NdoqERoAIz2FTOBdoWrL0b60vFA==}
+ peerDependencies:
+ '@tiptap/core': ^2.0.0-beta.193
+ dependencies:
+ '@tiptap/core': 2.0.0-beta.203
+ dev: true
+
+ /@tiptap/extension-text/2.0.0-beta.203:
+ resolution: {integrity: sha512-hOAPb3C2nIFZNJaFCaWj72sgcXqxJNTazXcsiei9A/p0L4NAIVa0ySub7H3NxRvxY/hRLUniA6u3QTzMo7Xsug==}
+ peerDependencies:
+ '@tiptap/core': ^2.0.0-beta.193
+ dev: true
+
+ /@tiptap/extension-text/2.0.0-beta.203_ywil7uncaz323pl33xz5cqvwo4:
+ resolution: {integrity: sha512-hOAPb3C2nIFZNJaFCaWj72sgcXqxJNTazXcsiei9A/p0L4NAIVa0ySub7H3NxRvxY/hRLUniA6u3QTzMo7Xsug==}
+ peerDependencies:
+ '@tiptap/core': ^2.0.0-beta.193
+ dependencies:
+ '@tiptap/core': 2.0.0-beta.203
+ dev: true
+
+ /@tiptap/starter-kit/2.0.0-beta.203:
+ resolution: {integrity: sha512-tKnQW1MA+9MijptQuIUlJYIeulMLhKRFbcR++UM/K1oRw6nlOyyvFz07prehIPwsjV0RsZg0TYYiuNTWOaEOAg==}
+ dependencies:
+ '@tiptap/core': 2.0.0-beta.203
+ '@tiptap/extension-blockquote': 2.0.0-beta.203_ywil7uncaz323pl33xz5cqvwo4
+ '@tiptap/extension-bold': 2.0.0-beta.203_ywil7uncaz323pl33xz5cqvwo4
+ '@tiptap/extension-bullet-list': 2.0.0-beta.203_ywil7uncaz323pl33xz5cqvwo4
+ '@tiptap/extension-code': 2.0.0-beta.203_ywil7uncaz323pl33xz5cqvwo4
+ '@tiptap/extension-code-block': 2.0.0-beta.203_ywil7uncaz323pl33xz5cqvwo4
+ '@tiptap/extension-document': 2.0.0-beta.203_ywil7uncaz323pl33xz5cqvwo4
+ '@tiptap/extension-dropcursor': 2.0.0-beta.203_ywil7uncaz323pl33xz5cqvwo4
+ '@tiptap/extension-gapcursor': 2.0.0-beta.203_ywil7uncaz323pl33xz5cqvwo4
+ '@tiptap/extension-hard-break': 2.0.0-beta.203_ywil7uncaz323pl33xz5cqvwo4
+ '@tiptap/extension-heading': 2.0.0-beta.203_ywil7uncaz323pl33xz5cqvwo4
+ '@tiptap/extension-history': 2.0.0-beta.203_ywil7uncaz323pl33xz5cqvwo4
+ '@tiptap/extension-horizontal-rule': 2.0.0-beta.203_ywil7uncaz323pl33xz5cqvwo4
+ '@tiptap/extension-italic': 2.0.0-beta.203_ywil7uncaz323pl33xz5cqvwo4
+ '@tiptap/extension-list-item': 2.0.0-beta.203_ywil7uncaz323pl33xz5cqvwo4
+ '@tiptap/extension-ordered-list': 2.0.0-beta.203_ywil7uncaz323pl33xz5cqvwo4
+ '@tiptap/extension-paragraph': 2.0.0-beta.203_ywil7uncaz323pl33xz5cqvwo4
+ '@tiptap/extension-strike': 2.0.0-beta.203_ywil7uncaz323pl33xz5cqvwo4
+ '@tiptap/extension-text': 2.0.0-beta.203_ywil7uncaz323pl33xz5cqvwo4
+ dev: true
+
+ /@tiptap/suggestion/2.0.0-beta.203:
+ resolution: {integrity: sha512-Pqk8QgKB08Rinvpd0dQnWLr+SPwwlZF5NX/v3cGqZ18ZJvE3UahVJD+Suj6oTLsgMba5hsXbPAIdGMiy0Q9PUw==}
+ peerDependencies:
+ '@tiptap/core': ^2.0.0-beta.193
+ dependencies:
+ prosemirror-model: 1.18.3
+ prosemirror-state: 1.4.2
+ prosemirror-view: 1.29.1
+ dev: true
+
+ /@tiptap/vue-3/2.0.0-beta.203:
+ resolution: {integrity: sha512-JkRNyVJMnENZVYQRV6vvR6IO3UXq2sqwLbu3WeRKeTaqZtb1Tzt+80UA2vTELN+TB5PUGtaqs+MNrB94bdPGrA==}
+ peerDependencies:
+ '@tiptap/core': ^2.0.0-beta.193
+ vue: ^3.0.0
+ dependencies:
+ '@tiptap/extension-bubble-menu': 2.0.0-beta.203
+ '@tiptap/extension-floating-menu': 2.0.0-beta.203
+ prosemirror-state: 1.4.2
+ prosemirror-view: 1.29.1
+ dev: true
+
/@trysound/sax/0.2.0:
resolution: {integrity: sha512-L7z9BgrNEcYyUYtF+HaEfiS5ebkh9jXqbszz7pC0hRBPaatV0XjSD3+eHrpqFemQfgwiFF0QPIarnIihIDn7OA==}
engines: {node: '>=10.13.0'}
@@ -5424,6 +5725,10 @@ packages:
wcwidth: 1.0.1
dev: true
+ /orderedmap/2.1.0:
+ resolution: {integrity: sha512-/pIFexOm6S70EPdznemIz3BQZoJ4VTFrhqzu0ACBqBgeLsLxq8e6Jim63ImIfwW/zAD1AlXpRMlOv3aghmo4dA==}
+ dev: true
+
/os-tmpdir/1.0.2:
resolution: {integrity: sha512-D2FR03Vir7FIu45XBY20mTb+/ZSWB00sjU9jdQXt83gDrI4Ztz5Fs7/yy74g2N5SVQY4xY1qDr4rNddwYRVX0g==}
engines: {node: '>=0.10.0'}
@@ -5997,6 +6302,82 @@ packages:
resolution: {integrity: sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==}
dev: true
+ /prosemirror-commands/1.3.1:
+ resolution: {integrity: sha512-XTporPgoECkOQACVw0JTe3RZGi+fls3/byqt+tXwGTkD7qLuB4KdVrJamDMJf4kfKga3uB8hZ+kUUyZ5oWpnfg==}
+ dependencies:
+ prosemirror-model: 1.18.3
+ prosemirror-state: 1.4.2
+ prosemirror-transform: 1.7.0
+ dev: true
+
+ /prosemirror-dropcursor/1.5.0:
+ resolution: {integrity: sha512-vy7i77ddKyXlu8kKBB3nlxLBnsWyKUmQIPB5x8RkYNh01QNp/qqGmdd5yZefJs0s3rtv5r7Izfu2qbtr+tYAMQ==}
+ dependencies:
+ prosemirror-state: 1.4.2
+ prosemirror-transform: 1.7.0
+ prosemirror-view: 1.29.1
+ dev: true
+
+ /prosemirror-gapcursor/1.3.1:
+ resolution: {integrity: sha512-GKTeE7ZoMsx5uVfc51/ouwMFPq0o8YrZ7Hx4jTF4EeGbXxBveUV8CGv46mSHuBBeXGmvu50guoV2kSnOeZZnUA==}
+ dependencies:
+ prosemirror-keymap: 1.2.0
+ prosemirror-model: 1.18.3
+ prosemirror-state: 1.4.2
+ prosemirror-view: 1.29.1
+ dev: true
+
+ /prosemirror-history/1.3.0:
+ resolution: {integrity: sha512-qo/9Wn4B/Bq89/YD+eNWFbAytu6dmIM85EhID+fz9Jcl9+DfGEo8TTSrRhP15+fFEoaPqpHSxlvSzSEbmlxlUA==}
+ dependencies:
+ prosemirror-state: 1.4.2
+ prosemirror-transform: 1.7.0
+ rope-sequence: 1.3.3
+ dev: true
+
+ /prosemirror-keymap/1.2.0:
+ resolution: {integrity: sha512-TdSfu+YyLDd54ufN/ZeD1VtBRYpgZnTPnnbY+4R08DDgs84KrIPEPbJL8t1Lm2dkljFx6xeBE26YWH3aIzkPKg==}
+ dependencies:
+ prosemirror-state: 1.4.2
+ w3c-keyname: 2.2.6
+ dev: true
+
+ /prosemirror-model/1.18.3:
+ resolution: {integrity: sha512-yUVejauEY3F1r7PDy4UJKEGeIU+KFc71JQl5sNvG66CLVdKXRjhWpBW6KMeduGsmGOsw85f6EGrs6QxIKOVILA==}
+ dependencies:
+ orderedmap: 2.1.0
+ dev: true
+
+ /prosemirror-schema-list/1.2.2:
+ resolution: {integrity: sha512-rd0pqSDp86p0MUMKG903g3I9VmElFkQpkZ2iOd3EOVg1vo5Cst51rAsoE+5IPy0LPXq64eGcCYlW1+JPNxOj2w==}
+ dependencies:
+ prosemirror-model: 1.18.3
+ prosemirror-state: 1.4.2
+ prosemirror-transform: 1.7.0
+ dev: true
+
+ /prosemirror-state/1.4.2:
+ resolution: {integrity: sha512-puuzLD2mz/oTdfgd8msFbe0A42j5eNudKAAPDB0+QJRw8cO1ygjLmhLrg9RvDpf87Dkd6D4t93qdef00KKNacQ==}
+ dependencies:
+ prosemirror-model: 1.18.3
+ prosemirror-transform: 1.7.0
+ prosemirror-view: 1.29.1
+ dev: true
+
+ /prosemirror-transform/1.7.0:
+ resolution: {integrity: sha512-O4T697Cqilw06Zvc3Wm+e237R6eZtJL/xGMliCi+Uo8VL6qHk6afz1qq0zNjT3eZMuYwnP8ZS0+YxX/tfcE9TQ==}
+ dependencies:
+ prosemirror-model: 1.18.3
+ dev: true
+
+ /prosemirror-view/1.29.1:
+ resolution: {integrity: sha512-OhujVZSDsh0l0PyHNdfaBj6DBkbhYaCfbaxmTeFrMKd/eWS+G6IC+OAbmR9IsLC8Se1HSbphMaXnsXjupHL3UQ==}
+ dependencies:
+ prosemirror-model: 1.18.3
+ prosemirror-state: 1.4.2
+ prosemirror-transform: 1.7.0
+ dev: true
+
/protocols/2.0.1:
resolution: {integrity: sha512-/XJ368cyBJ7fzLMwLKv1e4vLxOju2MNAIokcr7meSaNcVbWz/CPcW22cP04mwxOErdA5mwjA8Q6w/cdAQxVn7Q==}
dev: true
@@ -6236,6 +6617,10 @@ packages:
fsevents: 2.3.2
dev: true
+ /rope-sequence/1.3.3:
+ resolution: {integrity: sha512-85aZYCxweiD5J8yTEbw+E6A27zSnLPNDL0WfPdw3YYodq7WjnTKo0q4dtyQ2gz23iPT8Q9CUyJtAaUNcTxRf5Q==}
+ dev: true
+
/run-async/2.4.1:
resolution: {integrity: sha512-tvVnVv01b8c1RrA6Ep7JkStj85Guv/YrMcwqYQnwjsAS2cTmmPGBBjAjpCW7RrSodNSoE2/qg9O4bceNvUuDgQ==}
engines: {node: '>=0.12.0'}
@@ -6732,6 +7117,12 @@ packages:
engines: {node: '>=14.0.0'}
dev: true
+ /tippy.js/6.3.7:
+ resolution: {integrity: sha512-E1d3oP2emgJ9dRQZdf3Kkn0qJgI6ZLpyS5z6ZkY1DF3kaQaBsGZsndEpHwx+eC+tYM41HaSNvNtLx8tU57FzTQ==}
+ dependencies:
+ '@popperjs/core': 2.11.6
+ dev: true
+
/tmp/0.0.33:
resolution: {integrity: sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==}
engines: {node: '>=0.6.0'}
@@ -7448,6 +7839,10 @@ packages:
'@vue/shared': 3.2.45
dev: true
+ /w3c-keyname/2.2.6:
+ resolution: {integrity: sha512-f+fciywl1SJEniZHD6H+kUO8gOnwIr7f4ijKA6+ZvJFjeGi1r4PDLl53Ayud9O/rk64RqgoQine0feoeOU0kXg==}
+ dev: true
+
/wcwidth/1.0.1:
resolution: {integrity: sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg==}
dependencies:
diff --git a/styles/global.css b/styles/global.css
index d4d67a35..124c53be 100644
--- a/styles/global.css
+++ b/styles/global.css
@@ -24,11 +24,23 @@
background: #8886;
}
+::-moz-selection {
+ background: var(--c-bg-selection);
+}
+
+::selection {
+ background: var(--c-bg-selection);
+}
+
/* Force vertical scrollbar to be always visible to avoid layout shift while loading the content */
html {
overflow-y: scroll;
}
+.zen .zen-hide {
+ --at-apply: op0 hover:op100 transition duration-600;
+}
+
.custom-emoji {
display: inline-block;
max-height: 1.2em;
@@ -36,7 +48,7 @@ html {
vertical-align: middle;
}
-.rich-content {
+.content-rich {
a {
--at-apply: text-primary hover:underline hover:text-primary-active;
.invisible {
@@ -46,7 +58,7 @@ html {
--at-apply: truncate overflow-hidden ws-nowrap;
}
}
- b {
+ b, strong {
--at-apply: font-bold;
}
p {
@@ -62,6 +74,14 @@ html {
}
}
-.zen .zen-hide {
- --at-apply: op0 hover:op100 transition duration-600;
+.content-editor {
+ --at-apply: outline-none;
+
+ pre {
+ --at-apply: font-mono bg-code rounded px3 py2;
+
+ code {
+ --at-apply: bg-transparent text-0.8rem p0;
+ }
+ }
}
diff --git a/styles/tiptap.css b/styles/tiptap.css
new file mode 100644
index 00000000..adafdc30
--- /dev/null
+++ b/styles/tiptap.css
@@ -0,0 +1,7 @@
+.ProseMirror p.is-editor-empty:first-child::before {
+ content: attr(data-placeholder);
+ float: left;
+ pointer-events: none;
+ height: 0;
+ opacity: 0.4;
+}
diff --git a/styles/vars.css b/styles/vars.css
index 57644227..6c1bef2a 100644
--- a/styles/vars.css
+++ b/styles/vars.css
@@ -5,6 +5,7 @@
--c-bg-base: #fff;
--c-bg-active: #f6f6f6;
--c-bg-code: #00000006;
+ --c-bg-selection: #8885;
--c-text-base: #222;
--c-text-secondary: #888;
}
diff --git a/tests/content.test.ts b/tests/content.test.ts
index 142563c6..2fecf9cb 100644
--- a/tests/content.test.ts
+++ b/tests/content.test.ts
@@ -4,7 +4,7 @@ import { renderToString } from 'vue/server-renderer'
import { format } from 'prettier'
import { contentToVNode } from '~/composables/content'
-describe('rich-content', () => {
+describe('content-rich', () => {
it('empty', async () => {
const { formatted } = await render('')
expect(formatted).toMatchSnapshot()