ui fixes
Gitea Actions / Deploy (push) Successful in 1m4s

This commit is contained in:
vadim committed 2026-10-25 20:59:28 +03:00
1 parent f0706432d4
commit b9c5f64989
6 files changed
+1121 -219

No files matched your search

+812 -190
View File
File diff suppressed because it is too large. Load diff
+8 -2
View File
@@ -16,8 +16,14 @@
},
"dependencies": {
"@fontsource/roboto": "^5.3.0",
"@tiptap/extension-list": "^3.32.0",
"@tiptap/extensions": "^3.32.0",
"@tiptap/markdown": "^3.32.0",
"@tiptap/pm": "^3.32.0",
"@tiptap/starter-kit": "^3.32.0",
"@tiptap/vue-3": "^3.32.0",
"@vueuse/core": "^14.4.0",
"better-auth": "^1.7.1",
"better-auth": "^1.7.13",
"chart.js": "^4.5.1",
"firebase": "^13.0.0",
"pinia": "^4.0.3",
@@ -44,7 +50,7 @@
"sass-embedded": "^1.103.1",
"typescript": "^6.0.3",
"unplugin-fonts": "^2.0.0",
"vite": "^8.2.2",
"vite": "^8.3.5",
"vite-plugin-vue-devtools": "^8.2.1",
"vue-eslint-parser": "^10.4.1",
"vue-tsc": "^3.3.11"
+5 -19
View File
@@ -30,19 +30,13 @@ const cancel = () => {
<template>
<div class="issue-content">
<template v-if="!editing">
<v-hover #default="{ isHovering, props }">
<v-sheet
v-bind="props"
:class="isHovering ? 'px-0' : 'px-0'"
class="cursor-pointer d-flex ga-1"
@click="startEdit"
>
<v-sheet v-bind="props" border class="cursor-pointer d-flex ga-1 pa-4" @click="startEdit">
<div class="content-sheet">
{{ text ?? 'click to add content' }}
</div>
<v-icon v-if="isHovering" icon="mdi-pencil" size="small" />
</v-sheet>
</v-hover>
</template>
<template v-else>
@@ -58,12 +52,8 @@ const cancel = () => {
/>
<div class="d-flex justify-end ga-1">
<v-btn density="compact" icon variant="text" @click="cancel">
<v-icon icon="mdi-cancel" />
</v-btn>
<v-btn density="compact" icon color="primary" variant="flat" @click="save">
<v-icon icon="mdi-check" />
</v-btn>
<v-btn density="compact" prepend-icon="mdi-cancel" text="Cancel" variant="text" @click="cancel" />
<v-btn density="compact" prepend-icon="mdi-check" color="primary" variant="flat" @click="save" text="Save" />
</div>
</template>
</div>
@@ -75,10 +65,6 @@ const cancel = () => {
overflow-wrap: anywhere;
}
//.content-sheet:hover {
//background-color: rgba(var(--v-theme-on-surface), 0.06);
//}
.v-sheet {
transition: padding 0.2s ease-in-out;
}
+9 -7
View File
@@ -135,8 +135,15 @@ defineExpose({
<editable-text :text="issue.title" @changed="onTitleChanged" />
<expansion-panel title="Info">
<v-card elevation="0" border>
<v-card-text class="u-grid">
<div class="text-medium-emphasis">Reporter:</div>
<reporter-info :user="issue.reporter" />
<div class="text-medium-emphasis">Assignee:</div>
<assignee-menu :value="issue.assignee" :compact="false" @changed="onAssigneeChanged" />
<div class="text-medium-emphasis">Status:</div>
<status-menu :value="issue.status" @changed="onStatusChanged" />
@@ -146,12 +153,6 @@ defineExpose({
<div class="text-medium-emphasis">Priority:</div>
<priority-menu :value="issue.priority" :compact="false" @changed="onPriorityChanged" />
<div class="text-medium-emphasis">Reporter:</div>
<reporter-info :user="issue.reporter" />
<div class="text-medium-emphasis">Assignee:</div>
<assignee-menu :value="issue.assignee" :compact="false" @changed="onAssigneeChanged" />
<div class="text-medium-emphasis">Created At:</div>
<div>{{ createdAtFormatted }}</div>
@@ -159,6 +160,7 @@ defineExpose({
<issue-date :date="issue.dueDate" @changed="onDueDateChanged" />
</v-card-text>
</v-card>
</expansion-panel>
<expansion-panel title="Details" class="details__content">
<issue-content :text="issue.content" @changed="onContentChanged" />
@@ -242,6 +244,6 @@ defineExpose({
grid-template-columns: auto 1fr;
align-items: center;
column-gap: 1rem;
row-gap: 1.5rem;
row-gap: 0.5rem;
}
</style>
+282
View File
@@ -0,0 +1,282 @@
<script setup lang="ts">
import { watch } from 'vue'
import { EditorContent, useEditor, type Editor } from '@tiptap/vue-3'
import StarterKit from '@tiptap/starter-kit'
import { TaskItem, TaskList } from '@tiptap/extension-list'
import { Placeholder } from '@tiptap/extensions'
import { Markdown } from '@tiptap/markdown'
interface Props {
placeholder?: string
/** false renders the markdown read-only, without the toolbar */
editable?: boolean
autofocus?: boolean
}
const props = withDefaults(defineProps<Props>(), { placeholder: 'Write something…', editable: true, autofocus: false })
/** Markdown text */
const model = defineModel<string>({ default: '' })
const editor = useEditor({
content: model.value,
contentType: 'markdown',
editable: props.editable,
autofocus: props.autofocus ? 'end' : false,
extensions: [
// Markdown has no underline, so it would be lost on save
StarterKit.configure({ underline: false, link: { openOnClick: !props.editable } }),
TaskList,
TaskItem.configure({ nested: true }),
Placeholder.configure({ placeholder: () => props.placeholder }),
Markdown,
],
onUpdate: ({ editor }) => {
model.value = editor.getMarkdown()
},
})
// External changes only: skip when the value came from the editor itself
watch(model, (value) => {
if (!editor.value || value === editor.value.getMarkdown()) return
editor.value.commands.setContent(value, { contentType: 'markdown', emitUpdate: false })
})
watch(
() => props.editable,
(editable) => editor.value?.setEditable(editable),
)
interface ToolbarButton {
icon: string
title: string
run: (editor: Editor) => void
active?: (editor: Editor) => boolean
disabled?: (editor: Editor) => boolean
}
const toolbar: ToolbarButton[][] = [
[
{
icon: 'mdi-format-bold',
title: 'Bold',
run: (e) => e.chain().focus().toggleBold().run(),
active: (e) => e.isActive('bold'),
},
{
icon: 'mdi-format-italic',
title: 'Italic',
run: (e) => e.chain().focus().toggleItalic().run(),
active: (e) => e.isActive('italic'),
},
{
icon: 'mdi-format-strikethrough',
title: 'Strikethrough',
run: (e) => e.chain().focus().toggleStrike().run(),
active: (e) => e.isActive('strike'),
},
{
icon: 'mdi-code-tags',
title: 'Inline code',
run: (e) => e.chain().focus().toggleCode().run(),
active: (e) => e.isActive('code'),
},
],
[
{
icon: 'mdi-format-header-2',
title: 'Heading',
run: (e) => e.chain().focus().toggleHeading({ level: 2 }).run(),
active: (e) => e.isActive('heading', { level: 2 }),
},
{
icon: 'mdi-format-header-3',
title: 'Subheading',
run: (e) => e.chain().focus().toggleHeading({ level: 3 }).run(),
active: (e) => e.isActive('heading', { level: 3 }),
},
],
[
{
icon: 'mdi-format-list-bulleted',
title: 'Bullet list',
run: (e) => e.chain().focus().toggleBulletList().run(),
active: (e) => e.isActive('bulletList'),
},
{
icon: 'mdi-format-list-numbered',
title: 'Numbered list',
run: (e) => e.chain().focus().toggleOrderedList().run(),
active: (e) => e.isActive('orderedList'),
},
{
icon: 'mdi-format-list-checks',
title: 'Task list',
run: (e) => e.chain().focus().toggleTaskList().run(),
active: (e) => e.isActive('taskList'),
},
],
[
{
icon: 'mdi-format-quote-close',
title: 'Quote',
run: (e) => e.chain().focus().toggleBlockquote().run(),
active: (e) => e.isActive('blockquote'),
},
{
icon: 'mdi-code-block-braces',
title: 'Code block',
run: (e) => e.chain().focus().toggleCodeBlock().run(),
active: (e) => e.isActive('codeBlock'),
},
{
icon: 'mdi-minus',
title: 'Horizontal rule',
run: (e) => e.chain().focus().setHorizontalRule().run(),
},
],
[
{
icon: 'mdi-undo',
title: 'Undo',
run: (e) => e.chain().focus().undo().run(),
disabled: (e) => !e.can().undo(),
},
{
icon: 'mdi-redo',
title: 'Redo',
run: (e) => e.chain().focus().redo().run(),
disabled: (e) => !e.can().redo(),
},
],
]
</script>
<template>
<v-sheet class="markdown-editor" :class="{ 'markdown-editor--editable': editable }">
<div v-if="editable && editor" class="toolbar d-flex flex-wrap align-center ga-1 pa-1">
<template v-for="(group, i) in toolbar" :key="i">
<v-divider v-if="i > 0" vertical class="mx-1" />
<v-btn
v-for="button in group"
:key="button.icon"
:title="button.title"
:icon="button.icon"
:active="button.active?.(editor) ?? false"
:disabled="button.disabled?.(editor) ?? false"
density="comfortable"
size="small"
variant="text"
@click="button.run(editor)"
/>
</template>
</div>
<EditorContent :editor="editor" class="content" />
</v-sheet>
</template>
<style scoped lang="scss">
.markdown-editor--editable {
&:focus-within {
border-color: rgb(var(--v-theme-primary));
}
.toolbar {
border-bottom: 1px solid rgba(var(--v-border-color), var(--v-border-opacity));
}
.content :deep(.tiptap) {
min-height: 6rem;
max-height: 30rem;
overflow-y: auto;
padding: 8px 12px;
}
}
.content :deep(.tiptap) {
outline: none;
overflow-wrap: anywhere;
> * + * {
margin-top: 0.5em;
}
h2 {
font-size: 1.25rem;
font-weight: 500;
}
h3 {
font-size: 1.1rem;
font-weight: 500;
}
ul,
ol {
padding-left: 1.5rem;
}
ul[data-type='taskList'] {
list-style: none;
padding-left: 0.25rem;
li {
display: flex;
align-items: flex-start;
gap: 0.5rem;
> label {
user-select: none;
}
> div {
flex: 1;
}
}
}
blockquote {
border-left: 3px solid rgba(var(--v-border-color), var(--v-border-opacity));
padding-left: 0.75rem;
opacity: 0.8;
}
code {
background-color: rgba(var(--v-theme-on-surface), 0.08);
border-radius: 4px;
padding: 0.1em 0.3em;
font-size: 0.9em;
}
pre {
background-color: rgba(var(--v-theme-on-surface), 0.08);
border-radius: 4px;
padding: 0.75rem;
overflow-x: auto;
code {
background: none;
padding: 0;
}
}
hr {
border: none;
border-top: 1px solid rgba(var(--v-border-color), var(--v-border-opacity));
margin: 1rem 0;
}
a {
color: rgb(var(--v-theme-primary));
}
p.is-editor-empty:first-child::before {
content: attr(data-placeholder);
float: left;
height: 0;
pointer-events: none;
color: rgba(var(--v-theme-on-surface), var(--v-medium-emphasis-opacity));
}
}
</style>
+5 -1
View File
@@ -34,6 +34,10 @@ const avatar = computed(() => {
return user?.image ?? undefined
})
const createdAt = computed(() => {
return user?.createdAt ? new Date(user.createdAt).toLocaleString('ru-RU') : ''
})
function showError(text: string, e: unknown) {
snackbarStore.add({ title: text, text: e instanceof Error ? e.message : '', color: 'error' })
}
@@ -130,7 +134,7 @@ async function removeAvatar() {
<div class="flex-grow-1">
<editable-text class="mb-2" :text="user.name" placeholder="Enter your name" required @changed="saveName" />
<div>E-mail: {{ user.email }}</div>
<div>Created At: {{ user.createdAt }}</div>
<div>Created At: {{ createdAt }}</div>
</div>
</div>
<v-skeleton-loader v-else type="avatar, list-item-three-line" />