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,
{
"ignoreAtRules": [
"tailwind",
"apply",
"theme",
"utility",
"custom-variant",
"source",
"reference",
"variants",
"responsive",
"screen",

View File

@ -95,7 +95,6 @@
"pinia": "3.0.4",
"register-service-worker": "1.7.2",
"sortablejs": "1.15.6",
"tailwindcss": "3.4.19",
"ufo": "1.6.3",
"vue": "3.5.27",
"vue-advanced-cropper": "2.8.9",
@ -111,6 +110,7 @@
"@histoire/plugin-screenshot": "1.0.0-beta.1",
"@histoire/plugin-vue": "1.0.0-beta.1",
"@playwright/test": "1.58.2",
"@tailwindcss/vite": "^4.2.1",
"@tsconfig/node24": "24.0.4",
"@types/codemirror": "5.60.17",
"@types/is-touch-device": "1.0.3",
@ -144,6 +144,7 @@
"stylelint-config-recommended-vue": "1.6.1",
"stylelint-config-standard-scss": "17.0.0",
"stylelint-use-logical": "2.1.3",
"tailwindcss": "^4.2.1",
"typescript": "5.9.3",
"unplugin-inject-preload": "3.0.0",
"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()
</script>
<style src="@/styles/tailwind.css" />
<style lang="scss" src="@/styles/global.scss" />

View File

@ -34,14 +34,14 @@
</template>
<span
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 }}
</span>
</div>
<div
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
v-for="(action, i) in item.data.actions"
@ -64,7 +64,7 @@
z-index: 9999;
}
.tw-flex {
.tw\:flex {
margin-block-start: 0.5rem;
}
</style>

View File

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

View File

@ -1,7 +1,7 @@
<template>
<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 tw-gap-2">
<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">
<ColorBubble
v-if="task.hexColor !== ''"
:color="getHexColor(task.hexColor)"

View File

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

View File

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

View File

@ -1,7 +1,3 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
@import "fonts";
@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 #text>
<p class="tw-text-balance">
<p class="tw:text-balance">
{{ $t('task.detail.delete.text1') }}
</p>
<p class="tw-text-balance">
<p class="tw:text-balance">
{{ $t('task.detail.delete.text2') }}
</p>
</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 postcssPresetEnv from 'postcss-preset-env'
import postcssEasingGradients from 'postcss-easing-gradients'
import tailwindcss from 'tailwindcss'
import tailwindcss from '@tailwindcss/vite'
import vueDevTools from 'vite-plugin-vue-devtools'
const pathSrc = fileURLToPath(new URL('./src', import.meta.url)).replaceAll('\\', '/')
@ -106,7 +106,6 @@ function getBuildConfig(env: Record<string, string>) {
},
postcss: {
plugins: [
tailwindcss(),
postcssEasingGradients(),
postcssPresetEnv({
features: {
@ -117,6 +116,7 @@ function getBuildConfig(env: Record<string, string>) {
},
},
plugins: [
tailwindcss(),
vue(),
svgLoader({
// Since the svgs are already manually optimized via https://jakearchibald.github.io/svgomg/