feat(frontend): upgrade Tailwind CSS from v3 to v4

- Replace tailwindcss v3 with tailwindcss v4 + @tailwindcss/vite plugin
- Create dedicated tailwind.css entry with granular imports (skip preflight)
- Use CSS-first config with prefix(tw) instead of JS config
- Switch from PostCSS plugin to Vite plugin for better performance
- Update class prefix from tw- (dash) to tw: (colon) in all Vue files
- Remove @tailwind directives from global.scss
- Delete tailwind.config.js (replaced by CSS directives)
- Update stylelint at-rules for v4 directives
This commit is contained in:
kolaente 2026-03-03 11:09:35 +01:00
parent a160048cc3
commit a08667b669
14 changed files with 394 additions and 311 deletions

View File

@ -14,8 +14,12 @@
true, true,
{ {
"ignoreAtRules": [ "ignoreAtRules": [
"tailwind",
"apply", "apply",
"theme",
"utility",
"custom-variant",
"source",
"reference",
"variants", "variants",
"responsive", "responsive",
"screen", "screen",

View File

@ -95,7 +95,6 @@
"pinia": "3.0.4", "pinia": "3.0.4",
"register-service-worker": "1.7.2", "register-service-worker": "1.7.2",
"sortablejs": "1.15.6", "sortablejs": "1.15.6",
"tailwindcss": "3.4.19",
"ufo": "1.6.3", "ufo": "1.6.3",
"vue": "3.5.27", "vue": "3.5.27",
"vue-advanced-cropper": "2.8.9", "vue-advanced-cropper": "2.8.9",
@ -111,6 +110,7 @@
"@histoire/plugin-screenshot": "1.0.0-beta.1", "@histoire/plugin-screenshot": "1.0.0-beta.1",
"@histoire/plugin-vue": "1.0.0-beta.1", "@histoire/plugin-vue": "1.0.0-beta.1",
"@playwright/test": "1.58.2", "@playwright/test": "1.58.2",
"@tailwindcss/vite": "^4.2.1",
"@tsconfig/node24": "24.0.4", "@tsconfig/node24": "24.0.4",
"@types/codemirror": "5.60.17", "@types/codemirror": "5.60.17",
"@types/is-touch-device": "1.0.3", "@types/is-touch-device": "1.0.3",
@ -144,6 +144,7 @@
"stylelint-config-recommended-vue": "1.6.1", "stylelint-config-recommended-vue": "1.6.1",
"stylelint-config-standard-scss": "17.0.0", "stylelint-config-standard-scss": "17.0.0",
"stylelint-use-logical": "2.1.3", "stylelint-use-logical": "2.1.3",
"tailwindcss": "^4.2.1",
"typescript": "5.9.3", "typescript": "5.9.3",
"unplugin-inject-preload": "3.0.0", "unplugin-inject-preload": "3.0.0",
"vite": "7.3.1", "vite": "7.3.1",

File diff suppressed because it is too large Load Diff

View File

@ -83,4 +83,6 @@ setLanguage(authStore.settings.language ?? DEFAULT_LANGUAGE)
useColorScheme() useColorScheme()
</script> </script>
<style src="@/styles/tailwind.css" />
<style lang="scss" src="@/styles/global.scss" /> <style lang="scss" src="@/styles/global.scss" />

View File

@ -34,14 +34,14 @@
</template> </template>
<span <span
v-if="item.duplicates > 0" v-if="item.duplicates > 0"
class="tw-text-xs tw-font-bold tw-ml-1" class="tw:text-xs tw:font-bold tw:ml-1"
> >
×{{ item.duplicates + 1 }} ×{{ item.duplicates + 1 }}
</span> </span>
</div> </div>
<div <div
v-if="item.data?.actions?.length > 0" v-if="item.data?.actions?.length > 0"
class="tw-flex tw-justify-end tw-gap-2" class="tw:flex tw:justify-end tw:gap-2"
> >
<XButton <XButton
v-for="(action, i) in item.data.actions" v-for="(action, i) in item.data.actions"
@ -64,7 +64,7 @@
z-index: 9999; z-index: 9999;
} }
.tw-flex { .tw\:flex {
margin-block-start: 0.5rem; margin-block-start: 0.5rem;
} }
</style> </style>

View File

@ -15,7 +15,7 @@
/> />
<div <div
v-if="filterFromView" v-if="filterFromView"
class="tw-text-sm mbe-2" class="tw:text-sm mbe-2"
> >
{{ $t('filters.fromView') }} {{ $t('filters.fromView') }}
<code>{{ filterFromView }}</code><br> <code>{{ filterFromView }}</code><br>

View File

@ -1,7 +1,7 @@
<template> <template>
<div class="heading"> <div class="heading">
<div class="tw-flex tw-items-center md:tw-items-stretch tw-flex-col tw-gap-1 task-properties"> <div class="tw:flex tw:items-center md:tw:items-stretch tw:flex-col tw:gap-1 task-properties">
<div class="tw-flex tw-items-center tw-gap-2"> <div class="tw:flex tw:items-center tw:gap-2">
<ColorBubble <ColorBubble
v-if="task.hexColor !== ''" v-if="task.hexColor !== ''"
:color="getHexColor(task.hexColor)" :color="getHexColor(task.hexColor)"

View File

@ -18,10 +18,10 @@
v-if="coverImageBlobUrl" v-if="coverImageBlobUrl"
:src="coverImageBlobUrl" :src="coverImageBlobUrl"
alt="" alt=""
class="tw-w-full" class="tw:w-full"
> >
<div class="p-2"> <div class="p-2">
<div class="tw-flex tw-justify-between"> <div class="tw:flex tw:justify-between">
<span class="task-id"> <span class="task-id">
<Done <Done
class="kanban-card__done" class="kanban-card__done"
@ -36,7 +36,7 @@
</template> </template>
<span <span
v-if="showTaskPosition" v-if="showTaskPosition"
class="tw-text-red-600 tw-ps-2" class="tw:text-red-600 tw:ps-2"
> >
{{ task.position }} {{ task.position }}
</span> </span>

View File

@ -2,6 +2,7 @@ import {defineSetupVue3} from '@histoire/plugin-vue'
import {i18n} from './i18n' import {i18n} from './i18n'
// import './histoire.css' // Import global CSS // import './histoire.css' // Import global CSS
import './styles/tailwind.css'
import './styles/global.scss' import './styles/global.scss'
import {createPinia} from 'pinia' import {createPinia} from 'pinia'

View File

@ -1,7 +1,3 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
@import "fonts"; @import "fonts";
@import "transitions"; @import "transitions";

View File

@ -0,0 +1,3 @@
@layer theme, base, components, utilities;
@import "tailwindcss/theme.css" layer(theme) prefix(tw);
@import "tailwindcss/utilities.css" layer(utilities) prefix(tw);

View File

@ -601,10 +601,10 @@
</template> </template>
<template #text> <template #text>
<p class="tw-text-balance"> <p class="tw:text-balance">
{{ $t('task.detail.delete.text1') }} {{ $t('task.detail.delete.text1') }}
</p> </p>
<p class="tw-text-balance"> <p class="tw:text-balance">
{{ $t('task.detail.delete.text2') }} {{ $t('task.detail.delete.text2') }}
</p> </p>
</template> </template>

View File

@ -1,17 +0,0 @@
/** @type {import('tailwindcss').Config} */
module.exports = {
corePlugins: {
// TODO: Re-add after removing bulma base styles
preflight: false,
},
prefix: 'tw-',
content: [
'./index.html',
'./src/**/*.{vue,js,ts}',
],
theme: {
extend: {},
},
plugins: [],
}

View File

@ -14,7 +14,7 @@ import viteSentry, {type ViteSentryPluginOptions} from 'vite-plugin-sentry'
import svgLoader from 'vite-svg-loader' import svgLoader from 'vite-svg-loader'
import postcssPresetEnv from 'postcss-preset-env' import postcssPresetEnv from 'postcss-preset-env'
import postcssEasingGradients from 'postcss-easing-gradients' import postcssEasingGradients from 'postcss-easing-gradients'
import tailwindcss from 'tailwindcss' import tailwindcss from '@tailwindcss/vite'
import vueDevTools from 'vite-plugin-vue-devtools' import vueDevTools from 'vite-plugin-vue-devtools'
const pathSrc = fileURLToPath(new URL('./src', import.meta.url)).replaceAll('\\', '/') const pathSrc = fileURLToPath(new URL('./src', import.meta.url)).replaceAll('\\', '/')
@ -106,7 +106,6 @@ function getBuildConfig(env: Record<string, string>) {
}, },
postcss: { postcss: {
plugins: [ plugins: [
tailwindcss(),
postcssEasingGradients(), postcssEasingGradients(),
postcssPresetEnv({ postcssPresetEnv({
features: { features: {
@ -117,6 +116,7 @@ function getBuildConfig(env: Record<string, string>) {
}, },
}, },
plugins: [ plugins: [
tailwindcss(),
vue(), vue(),
svgLoader({ svgLoader({
// Since the svgs are already manually optimized via https://jakearchibald.github.io/svgomg/ // Since the svgs are already manually optimized via https://jakearchibald.github.io/svgomg/