upgrade to vuetify 4

This commit is contained in:
Gauthier Roebroeck 2026-03-16 13:20:38 +08:00
parent dccabd5265
commit 0021ae8f88
33 changed files with 136 additions and 124 deletions

View file

@ -25,7 +25,7 @@
"vue": "^3.5.30",
"vue-intl": "^7.1.3",
"vuedraggable": "^4.1.0",
"vuetify": "^3.11.7"
"vuetify": "^4.0.2"
},
"devDependencies": {
"@chromatic-com/storybook": "^5.0.1",
@ -12131,9 +12131,9 @@
}
},
"node_modules/vuetify": {
"version": "3.11.7",
"resolved": "https://registry.npmjs.org/vuetify/-/vuetify-3.11.7.tgz",
"integrity": "sha512-3nK1mKTXQRbU4QXukV4WIbs5YZgMK19flHpFq3pU+6Fpa5YLB8RyyK1BLWAW8JmhSVcaqVUcB/EJ3oJ8g3XNCw==",
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/vuetify/-/vuetify-4.0.2.tgz",
"integrity": "sha512-klgSGmfXoLajdTuuxreilzDQjp0ojzL2U5v6Z3ZbMYtpihPPXT9rkd/FxWL3dIGevnWdgaP2Kpwoz6aS/MISDA==",
"license": "MIT",
"funding": {
"type": "github",

View file

@ -46,7 +46,7 @@
"vue": "^3.5.30",
"vue-intl": "^7.1.3",
"vuedraggable": "^4.1.0",
"vuetify": "^3.11.7"
"vuetify": "^4.0.2"
},
"devDependencies": {
"@chromatic-com/storybook": "^5.0.1",

View file

@ -21,7 +21,7 @@
variant="text"
color="grey"
size="small"
class="text-caption"
class="text-body-small"
href="https://komga.org"
target="_blank"
:text="

View file

@ -5,7 +5,7 @@
variant="text"
color="grey"
size="small"
class="text-caption"
class="text-body-small"
:href="'https://github.com/gotson/komga/commits/' + commitId"
target="_blank"
>

View file

@ -10,7 +10,7 @@
variant="text"
color="grey"
size="small"
class="text-caption"
class="text-body-small"
to="/server/updates"
>
{{ buildVersion || $formatMessage(commonMessages.error) }}

View file

@ -12,6 +12,45 @@
<div class="py-4" />
<v-row>
<div class="d-flex flex-column flex ga-4">
<v-btn
prepend-icon="mdi-delete"
text="Button"
color=""
disabled
size="x-small"
/>
<v-btn
prepend-icon="mdi-delete"
text="Button"
color=""
disabled
size="small"
/>
<v-btn
prepend-icon="mdi-delete"
text="Button"
color=""
disabled
/>
<v-btn
prepend-icon="mdi-delete"
text="Button"
color=""
disabled
size="large"
/>
<v-btn
prepend-icon="mdi-delete"
text="Button"
color=""
disabled
size="x-large"
/>
</div>
</v-row>
<v-row>
<v-col cols="12">
<v-card

View file

@ -4,7 +4,7 @@
<template v-else>
<v-list
:disabled="isLoading"
elevation="2"
elevation="1"
>
<v-progress-linear
v-if="isLoading"

View file

@ -4,7 +4,7 @@
<a
:href="item.url"
target="_blank"
class="text-h3 font-weight-medium link-underline"
class="text-display-small font-weight-medium link-underline"
>{{ item.title }}</a
>
</template>
@ -14,10 +14,7 @@
<template #text>
<!-- eslint-disable vue/no-v-html -->
<div
class="announcement"
v-html="item.content_html"
/>
<div v-html="item.content_html" />
<!-- eslint-enable vue/no-v-html -->
</template>
@ -48,17 +45,3 @@ const emit = defineEmits<{
markRead: [id: string]
}>()
</script>
<style lang="scss">
.announcement p {
margin-bottom: 16px;
}
.announcement ul {
padding-left: 24px;
}
.announcement a {
color: var(--v-anchor-base);
}
</style>

View file

@ -35,15 +35,19 @@ export const Default: Story = {
export const Created: Story = {
play: async ({ userEvent }) => {
const user = userEvent.setup({
pointerEventsCheck: 0,
})
const canvas = within(screen.getByRole('dialog'))
await waitFor(() => expect(canvas.getByText(/kobo sync protocol/i)).toBeVisible())
const comment = canvas.getByLabelText(/comment/i, {
selector: 'input',
})
await userEvent.type(comment, 'new key')
await user.type(comment, 'new key')
await userEvent.click(canvas.getByRole('button', { name: /generate/i }))
await user.click(canvas.getByRole('button', { name: /generate/i }))
},
}
@ -54,15 +58,19 @@ export const Loading: Story = {
},
},
play: async ({ userEvent }) => {
const user = userEvent.setup({
pointerEventsCheck: 0,
})
const canvas = within(screen.getByRole('dialog'))
await waitFor(() => expect(canvas.getByText(/kobo sync protocol/i)).toBeVisible())
const comment = canvas.getByLabelText(/comment/i, {
selector: 'input',
})
await userEvent.type(comment, 'long loading')
await user.type(comment, 'long loading')
await userEvent.click(canvas.getByRole('button', { name: /generate/i }))
await user.click(canvas.getByRole('button', { name: /generate/i }))
},
}
@ -77,14 +85,18 @@ export const DuplicateError: Story = {
},
},
play: async ({ userEvent }) => {
const user = userEvent.setup({
pointerEventsCheck: 0,
})
const canvas = within(screen.getByRole('dialog'))
await waitFor(() => expect(canvas.getByText(/kobo sync protocol/i)).toBeVisible())
const comment = canvas.getByLabelText(/comment/i, {
selector: 'input',
})
await userEvent.type(comment, 'duplicate')
await user.type(comment, 'duplicate')
await userEvent.click(canvas.getByRole('button', { name: /generate/i }))
await user.click(canvas.getByRole('button', { name: /generate/i }))
},
}

View file

@ -22,7 +22,7 @@
<v-container fluid>
<v-row>
<v-col>
<div class="text-subtitle-2">
<div class="text-label-large">
{{
$formatMessage({
description: 'File name picker dialog: source file name field label',
@ -37,7 +37,7 @@
</v-col>
</v-row>
<v-row align="center">
<v-row class="align-center">
<v-col>
<v-text-field
v-model="newName"

View file

@ -20,7 +20,7 @@
<v-card-text>
<v-list
:disabled="isLoading"
elevation="2"
elevation="1"
>
<v-progress-linear
v-if="isLoading"

View file

@ -1,6 +1,6 @@
<template>
<v-container fluid>
<v-row align="center">
<v-row class="align-center">
<v-col
cols="12"
sm=""

View file

@ -175,10 +175,7 @@
</v-data-table>
<v-container fluid>
<v-row
justify="space-between"
align="center"
>
<v-row class="align-center justify-space-between">
<v-col>
<v-btn
:text="
@ -389,6 +386,7 @@ const importBatch = computed(
// only analyze books that are shown
function onDisplayedItems(items: { key: string }[]) {
console.log('onDisplayedItems', items)
importBooks.value
.filter((b) => items.map((it) => it.key).includes(b.transientBook.id))
.forEach((b) => {

View file

@ -142,10 +142,7 @@
<!--region Creation Form-->
<v-container fluid>
<v-row
justify="space-between"
align="end"
>
<v-row class="align-end justify-space-between">
<v-col
cols="12"
sm=""

View file

@ -6,7 +6,7 @@
<v-card
v-on-long-press.prevent="onCardLongPress"
v-bind="props"
:elevation="isHovering ? 3 : 1"
:elevation="isHovering ? 2 : 1"
:class="isPreSelect || selected ? 'cursor-pointer' : 'cursor-default'"
@click="
(event: Event) => (isPreSelect || selected ? emit('selection', !selected, event) : {})
@ -55,7 +55,7 @@
<!-- Top-right icon -->
<div
v-if="topRightIcon || topRight"
class="top-0 right-0 position-absolute translucent text-white px-2 py-1 font-weight-bold text-caption"
class="top-0 right-0 position-absolute translucent text-white px-2 py-1 font-weight-bold text-body-small"
style="border-bottom-left-radius: 4px"
>
<v-icon
@ -79,13 +79,13 @@
<div>
<div
class="text-h6 force-line-count text-wrap"
class="text-headline-small force-line-count text-wrap"
:style="[{ '--lines': 1 }, { '--line-height': 1.6 }]"
>
{{ title }}
</div>
<div
class="text-subtitle-1 force-line-count text-pre-wrap mt-2"
class="text-body-large force-line-count text-pre-wrap mt-2"
:style="[{ '--lines': display.xs.value ? 1 : 3 }, { '--line-height': 1.6 }]"
>
{{ text }}

View file

@ -38,7 +38,7 @@
<!-- Top-right icon -->
<div
v-if="topRightIcon || topRight"
class="top-0 right-0 position-absolute translucent text-white px-2 py-1 font-weight-bold text-caption"
class="top-0 right-0 position-absolute translucent text-white px-2 py-1 font-weight-bold text-body-small"
style="border-bottom-left-radius: 4px"
>
<v-icon
@ -134,7 +134,7 @@
</v-hover>
<v-card-title
:class="['text-subtitle-2 px-2 pb-0 mb-2', { 'force-line-count text-wrap': title.lines }]"
:class="['text-label-large px-2 pb-0 mb-2', { 'force-line-count text-wrap': title.lines }]"
:style="[{ '--lines': title.lines }, { '--line-height': 1.6 }]"
>{{ title.text }}</v-card-title
>

View file

@ -1,5 +1,5 @@
<template>
<v-app-bar elevation="2">
<v-app-bar elevation="1">
<template #prepend>
<v-app-bar-nav-icon @click="appStore.drawer = !appStore.drawer">
<template #default>

View file

@ -1,13 +1,13 @@
<template>
<v-divider />
<div class="d-flex align-center text-caption text-medium-emphasis pa-2">
<div class="d-flex align-center text-body-small text-medium-emphasis pa-2">
<div class="d-flex ms-auto">
<v-btn
prepend-icon="i-mdi:help-circle-outline"
variant="text"
color="grey"
size="small"
class="text-caption"
class="text-body-small"
href="https://komga.org"
target="_blank"
:text="
@ -22,7 +22,7 @@
</div>
<div
v-if="isAdmin"
class="d-flex align-center text-caption text-medium-emphasis pa-2"
class="d-flex align-center text-body-small text-medium-emphasis pa-2"
>
<div class="d-flex ms-auto">
<BuildCommit class="me-2" />

View file

@ -17,7 +17,7 @@
</v-col>
</v-row>
<v-row align="center">
<v-row class="align-center">
<v-col>
<v-text-field
v-model="model.root"

View file

@ -2,7 +2,7 @@
<v-container class="px-0">
<v-row>
<v-col cols="auto">
<div class="text-subtitle-2 d-flex ga-2 align-center">
<div class="text-label-large d-flex ga-2 align-center">
<div>
{{
$formatMessage({
@ -111,7 +111,7 @@
<v-row>
<v-col>
<div class="text-subtitle-2">
<div class="text-label-large">
{{
$formatMessage({
description: 'Form add/edit library: Metadata - section header for epub',
@ -150,7 +150,7 @@
<v-row>
<v-col>
<div class="text-subtitle-2">
<div class="text-label-large">
{{
$formatMessage({
description: 'Form add/edit library: Metadata - section header for mylar series.json',
@ -178,7 +178,7 @@
<v-row>
<v-col>
<div class="text-subtitle-2">
<div class="text-label-large">
{{
$formatMessage({
description:
@ -207,7 +207,7 @@
<v-row>
<v-col>
<div class="text-subtitle-2 d-flex ga-2 align-center">
<div class="text-label-large d-flex ga-2 align-center">
{{
$formatMessage({
description: 'Form add/edit library: Metadata - section header for ISBN barcode',

View file

@ -2,7 +2,7 @@
<v-container class="px-0">
<v-row>
<v-col cols="auto">
<div class="text-subtitle-2 d-flex ga-2 align-center">
<div class="text-label-large d-flex ga-2 align-center">
<div>
{{
$formatMessage({
@ -128,7 +128,7 @@
<v-row>
<v-col>
<div class="text-subtitle-2">
<div class="text-label-large">
{{
$formatMessage({
description: 'Form add/edit library: Options - section header for file management',
@ -167,7 +167,7 @@
<v-row>
<v-col>
<div class="text-subtitle-2">
<div class="text-label-large">
{{
$formatMessage({
description: 'Form add/edit library: Options - section header for series cover',

View file

@ -63,7 +63,7 @@
<v-row>
<v-col>
<div class="text-subtitle-1">
<div class="text-body-large">
{{
$formatMessage({
description: 'Form add/edit library: Scanner - file types selection header',

View file

@ -5,7 +5,7 @@
<a
:href="release.url"
target="_blank"
class="text-h4 font-weight-medium link-underline"
class="text-headline-large font-weight-medium link-underline"
>{{ release.version }}</a
>
<v-chip
@ -45,10 +45,7 @@
<template #text>
<!-- eslint-disable vue/no-v-html -->
<div
class="release"
v-html="marked(release.description)"
/>
<div v-html="marked(release.description)" />
<!-- eslint-enable vue/no-v-html -->
</template>
</v-card>
@ -68,17 +65,3 @@ const {
current?: boolean
}>()
</script>
<style lang="scss">
.release p {
margin-bottom: 16px;
}
.release ul {
padding-left: 24px;
}
.release a {
color: var(--v-anchor-base);
}
</style>

View file

@ -1,6 +1,6 @@
<template>
<v-app-bar
elevation="2"
elevation="1"
color="surface-light"
>
<template #prepend>

View file

@ -11,7 +11,7 @@
>
<v-row>
<v-col>
<div class="text-subtitle-2">
<div class="text-label-large">
{{
$formatMessage({
description: 'Server settings: section header for posters',
@ -42,7 +42,7 @@
<v-row>
<v-col>
<div class="text-subtitle-2">
<div class="text-label-large">
{{
$formatMessage({
description: 'Server settings: section header for scan behaviour',
@ -80,7 +80,7 @@
<v-row>
<v-col>
<div class="text-subtitle-2">
<div class="text-label-large">
{{
$formatMessage({
description: 'Server settings: section header for tasks',
@ -112,7 +112,7 @@
<v-row>
<v-col>
<div class="text-subtitle-2">
<div class="text-label-large">
{{
$formatMessage({
description: 'Server settings: section header for remember me',
@ -121,7 +121,7 @@
})
}}
</div>
<div class="text-caption">{{ messageRequiresRestart }}</div>
<div class="text-body-small">{{ messageRequiresRestart }}</div>
</v-col>
</v-row>
<v-row>
@ -156,7 +156,7 @@
<v-row>
<v-col>
<div class="text-subtitle-2">
<div class="text-label-large">
{{
$formatMessage({
description: 'Server settings: section header for HTTP server',
@ -165,7 +165,7 @@
})
}}
</div>
<div class="text-caption">{{ messageRequiresRestart }}</div>
<div class="text-body-small">{{ messageRequiresRestart }}</div>
</v-col>
</v-row>
@ -243,7 +243,7 @@
<v-row>
<v-col>
<div class="text-subtitle-2">
<div class="text-label-large">
{{
$formatMessage({
description: 'Server settings: section header for Kobo Sync',

View file

@ -4,7 +4,9 @@
<v-avatar
color="primary"
size="x-large"
><span class="text-h5 text-uppercase">{{ user.email.charAt(0) }}</span></v-avatar
><span class="text-headline-medium text-uppercase">{{
user.email.charAt(0)
}}</span></v-avatar
>
</template>

View file

@ -44,7 +44,7 @@
v-if="!user.id"
class="mb-4"
/>
<div class="text-subtitle-2">Permissions</div>
<div class="text-label-large">Permissions</div>
</v-col>
</v-row>
<!-- Roles -->
@ -103,12 +103,12 @@
/>
</template>
<template #selection="{ item }">
<template #selection="{ internalItem }">
<!-- Show the selection only if 'all' is false -->
<v-chip
v-if="!user.sharedLibraries?.all"
size="small"
:text="item.title"
:text="internalItem.title"
/>
</template>
@ -148,7 +148,7 @@
<v-row>
<v-col>
<v-divider class="mb-4" />
<div class="text-subtitle-2">Age restriction</div>
<div class="text-label-large">Age restriction</div>
</v-col>
</v-row>
<v-row>
@ -192,7 +192,7 @@
<v-row>
<v-col>
<v-divider class="mb-4" />
<div class="text-subtitle-2">Label restrictions</div>
<div class="text-label-large">Label restrictions</div>
</v-col>
</v-row>
<!-- Allow labels -->

View file

@ -5,7 +5,7 @@
@submit.prevent="submitForm()"
>
<v-container max-width="400px">
<v-row justify="center">
<v-row class="justify-center">
<v-col
cols="7"
sm="10"
@ -118,7 +118,7 @@
<v-divider class="my-4" />
<v-row justify="center">
<v-row class="justify-center">
<v-col cols="auto">
<div class="d-flex ga-4">
<LocaleSelector />

View file

@ -5,7 +5,7 @@
@submit.prevent="submitForm()"
>
<v-container max-width="400px">
<v-row justify="center">
<v-row class="justify-center">
<v-col
cols="7"
sm="10"
@ -92,12 +92,12 @@
</v-col>
</v-row>
<v-row justify="center">
<v-row class="justify-center">
<v-col cols="auto">
<a
href="https://komga.org/docs/faq#i-forgot-my-password"
target="_blank"
class="link-underline text-body-2"
class="link-underline text-body-medium"
>
{{
$formatMessage({
@ -112,7 +112,7 @@
<v-divider class="my-4" />
<v-row justify="center">
<v-row class="justify-center">
<v-col cols="auto">
<div class="d-flex ga-4">
<LocaleSelector />

View file

@ -1,6 +1,6 @@
<template>
<v-container max-width="550px">
<v-row justify="center">
<v-row class="justify-center">
<v-col>
<v-img
src="@/assets/logo.svg"

View file

@ -6,7 +6,7 @@
// Styles
import 'vuetify/styles'
import { aliases } from 'vuetify/iconsets/mdi'
import { aliases, mdi } from 'vuetify/iconsets/mdi-unocss'
// Composables
import { createVuetify } from 'vuetify'
@ -40,13 +40,11 @@ export const vuetify = createVuetify({
messages,
},
icons: {
// 'class' is a built-in set that just applies the icon value as classes on the v-icon
defaultSet: 'class',
// customize the provided mdi aliases to fit the unocss class names
aliases: Object.keys(aliases).reduce((obj: Record<string, string>, key) => {
obj[key] = `i-mdi:${aliases[key]!.toString().substring(4)}`
return obj
}, {}),
defaultSet: 'mdi',
aliases,
sets: {
mdi,
},
},
theme: {
defaultTheme: 'light',

View file

@ -5,7 +5,7 @@ type Message =
| {
text: string
color?: string
timer?: string | boolean
timer?: boolean | 'top' | 'bottom'
timeout?: string | number
}
| string

View file

@ -1,12 +1,12 @@
import { defineConfig, presetIcons } from 'unocss'
import { aliases } from 'vuetify/iconsets/mdi'
// build the safelist from the vuetify icon aliases
const vuetifyIcons = Object.keys(aliases).map(
(key) => `i-mdi:${aliases[key]!.toString().substring(4)}`,
)
export default defineConfig({
presets: [presetIcons()],
safelist: vuetifyIcons,
presets: [
presetIcons({
// ensures proper CSS layers for icon colors
processor(props) {
delete props.color
},
}),
],
})