fix: inject Tailwind CSS into client entry point (#1537)

Added a Vite plugin to automatically inject the Tailwind CSS import into
the `unraid-components.client.js` entry file, enhancing the integration
of Tailwind CSS within the application. This change improves the setup
for styling components consistently across the project.

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **New Features**
* Added automated validation to ensure Tailwind CSS styles are correctly
included in the custom elements build output.

* **Chores**
* Updated the build process to include a CSS validation step after
manifest generation.
* Enhanced development build configuration to enable CSS source maps and
optimize Tailwind CSS injection into web components.
  * Extended CSS theme with new responsive breakpoint variables.
* Improved CSS class specificity in user profile, server state, and
update modal components for consistent styling.
* Removed redundant style blocks and global CSS imports from multiple
components to streamline styling and reduce duplication.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
Eli Bosley
2025-07-23 15:30:57 -04:00
committed by GitHub
parent 45bd73698b
commit 86b6c4f85b
36 changed files with 242 additions and 580 deletions

View File

@@ -12,6 +12,12 @@
@custom-variant dark (&:where(.dark, .dark *));
@theme {
/* Breakpoints */
--breakpoint-xs: 30rem;
--breakpoint-2xl: 100rem;
--breakpoint-3xl: 120rem;
/* Colors */
--color-primary-50: #fff7ed;
--color-primary-100: #ffedd5;
--color-primary-200: #fed7aa;

View File

@@ -101,57 +101,3 @@ watchEffect(() => {
</Dialog>
</div>
</template>
<style>
/* Import unraid-ui globals first */
@import '@unraid/ui/styles';
@import '~/assets/main.css';
.unraid_mark_2,
.unraid_mark_4 {
animation: mark_2 1.5s ease infinite;
}
.unraid_mark_3 {
animation: mark_3 1.5s ease infinite;
}
.unraid_mark_6,
.unraid_mark_8 {
animation: mark_6 1.5s ease infinite;
}
.unraid_mark_7 {
animation: mark_7 1.5s ease infinite;
}
@keyframes mark_2 {
50% {
transform: translateY(-40px);
}
100% {
transform: translateY(0);
}
}
@keyframes mark_3 {
50% {
transform: translateY(-62px);
}
100% {
transform: translateY(0);
}
}
@keyframes mark_6 {
50% {
transform: translateY(40px);
}
100% {
transform: translateY(0);
}
}
@keyframes mark_7 {
50% {
transform: translateY(62px);
}
100% {
transform: translateY(0);
}
}
</style>

View File

@@ -6,8 +6,3 @@ import ApiKeyManager from '~/components/ApiKey/ApiKeyManager.vue';
<ApiKeyManager />
</div>
</template>
<style>
/* Import unraid-ui globals first */
@import '@unraid/ui/styles';
@import '~/assets/main.css';
</style>

View File

@@ -30,9 +30,3 @@ const { authAction, stateData } = storeToRefs(serverStore);
</span>
</div>
</template>
<style>
/* Import unraid-ui globals first */
@import '@unraid/ui/styles';
@import '~/assets/main.css';
</style>

View File

@@ -14,9 +14,3 @@ onBeforeMount(() => {
<slot />
</div>
</template>
<style >
/* Import unraid-ui globals first */
@import '@unraid/ui/styles';
@import '~/assets/main.css';
</style>

View File

@@ -74,9 +74,3 @@ const items = [
<Switch id="banner" v-model:checked="form.banner" />
</div>
</template>
<style >
/* Import unraid-ui globals first */
@import '@unraid/ui/styles';
@import '~/assets/main.css';
</style>

View File

@@ -148,8 +148,3 @@ const onChange = ({ data }: { data: Record<string, unknown> }) => {
</div>
</div>
</template>
<style >
/* Import unraid-ui globals first */
@import '@unraid/ui/styles';
@import '../../assets/main.css';
</style>

View File

@@ -23,9 +23,3 @@ import { CogIcon } from '@heroicons/vue/24/solid';
</template>
<style >
/* Import unraid-ui globals first */
@import '@unraid/ui/styles';
@import '~/assets/main.css';
</style>

View File

@@ -77,9 +77,3 @@ onBeforeMount(() => {
</PageContainer>
</div>
</template>
<style >
/* Import unraid-ui globals first */
@import '@unraid/ui/styles';
@import '~/assets/main.css';
</style>

View File

@@ -71,9 +71,3 @@ const downloadUrl = computed(() => {
</span>
</div>
</template>
<style >
/* Import unraid-ui globals first */
@import '@unraid/ui/styles';
@import '~/assets/main.css';
</style>

View File

@@ -24,9 +24,3 @@ const items = [
<Select v-model="selector" :items="items" placeholder="Select an initial state" />
</div>
</template>
<style >
/* Import unraid-ui globals first */
@import '@unraid/ui/styles';
@import '~/assets/main.css';
</style>

View File

@@ -127,10 +127,3 @@ const updateOsStatus = computed(() => {
</div>
</div>
</template>
<style >
/* Import unraid-ui globals first */
@import '@unraid/ui/styles';
@import '~/assets/main.css';
</style>

View File

@@ -191,9 +191,3 @@ watch(selectedLogFile, (newValue) => {
</div>
</div>
</template>
<style >
/* Import unraid-ui globals first */
@import '@unraid/ui/styles';
@import '~/assets/main.css';
</style>

View File

@@ -31,57 +31,3 @@ const { modalVisible: apiKeyModalVisible } = storeToRefs(useApiKeyStore());
<ApiKeyCreate :open="apiKeyModalVisible" :t="t" />
</div>
</template>
<style >
/* Import unraid-ui globals first */
@import '@unraid/ui/styles';
@import '~/assets/main.css';
.unraid_mark_2,
.unraid_mark_4 {
animation: mark_2 1.5s ease infinite;
}
.unraid_mark_3 {
animation: mark_3 1.5s ease infinite;
}
.unraid_mark_6,
.unraid_mark_8 {
animation: mark_6 1.5s ease infinite;
}
.unraid_mark_7 {
animation: mark_7 1.5s ease infinite;
}
@keyframes mark_2 {
50% {
transform: translateY(-40px);
}
100% {
transform: translateY(0);
}
}
@keyframes mark_3 {
50% {
transform: translateY(-62px);
}
100% {
transform: translateY(0);
}
}
@keyframes mark_6 {
50% {
transform: translateY(40px);
}
100% {
transform: translateY(0);
}
}
@keyframes mark_7 {
50% {
transform: translateY(62px);
}
100% {
transform: translateY(0);
}
}
</style>

View File

@@ -238,8 +238,3 @@ provide('isSubmitting', isCreating);
</div>
</div>
</template>
<style >
/* Import unraid-ui globals first */
@import '@unraid/ui/styles';
</style>

View File

@@ -324,9 +324,3 @@ const items = computed((): RegistrationItemProps[] => {
</PageContainer>
</div>
</template>
<style >
/* Import unraid-ui globals first */
@import '@unraid/ui/styles';
@import '~/assets/main.css';
</style>

View File

@@ -150,9 +150,3 @@ const navigateToExternalSSOUrl = () => {
</template>
</div>
</template>
<style >
/* Import unraid-ui globals first */
@import '@unraid/ui/styles';
@import '~/assets/main.css';
</style>

View File

@@ -91,9 +91,3 @@ const handleThemeChange = (event: Event) => {
</select>
</div>
</template>
<style >
/* Import unraid-ui globals first */
@import '@unraid/ui/styles';
@import '~/assets/main.css';
</style>

View File

@@ -73,57 +73,3 @@ onBeforeMount(() => {
<UpdateOsThirdPartyDrivers v-if="rebootType === 'thirdPartyDriversDownloading'" :t="t" />
</PageContainer>
</template>
<style >
/* Import unraid-ui globals first */
@import '@unraid/ui/styles';
@import '~/assets/main.css';
.unraid_mark_2,
.unraid_mark_4 {
animation: mark_2 1.5s ease infinite;
}
.unraid_mark_3 {
animation: mark_3 1.5s ease infinite;
}
.unraid_mark_6,
.unraid_mark_8 {
animation: mark_6 1.5s ease infinite;
}
.unraid_mark_7 {
animation: mark_7 1.5s ease infinite;
}
@keyframes mark_2 {
50% {
transform: translateY(-40px);
}
100% {
transform: translateY(0);
}
}
@keyframes mark_3 {
50% {
transform: translateY(-62px);
}
100% {
transform: translateY(0);
}
}
@keyframes mark_6 {
50% {
transform: translateY(40px);
}
100% {
transform: translateY(0);
}
}
@keyframes mark_7 {
50% {
transform: translateY(62px);
}
100% {
transform: translateY(0);
}
}
</style>

View File

@@ -9,7 +9,7 @@ import {
KeyIcon,
ServerStackIcon,
} from '@heroicons/vue/24/solid';
import { BrandButton, BrandLoading } from '@unraid/ui';
import { BrandButton, BrandLoading, cn } from '@unraid/ui';
import { allowedDocsOriginRegex, allowedDocsUrlRegex } from '~/helpers/urls';
import type { ComposerTranslation } from 'vue-i18n';
@@ -176,8 +176,8 @@ watch(darkMode, () => {
</template>
<template #footer>
<div class="flex flex-col-reverse xs:flex-row justify-between gap-3 md:gap-4">
<div class="flex flex-col-reverse xs:flex-row xs:justify-start gap-3 md:gap-4">
<div :class="cn('flex flex-col-reverse xs:!flex-row justify-between gap-3 md:gap-4')">
<div :class="cn('flex flex-col-reverse xs:!flex-row xs:justify-start gap-3 md:gap-4')">
<!-- Back to changelog button (when navigated away) -->
<BrandButton
v-if="hasNavigated && docsChangelogUrl"

View File

@@ -10,7 +10,7 @@ import {
KeyIcon,
XMarkIcon,
} from '@heroicons/vue/24/solid';
import { BrandButton, BrandLoading } from '@unraid/ui';
import { BrandButton, BrandLoading, cn } from '@unraid/ui';
import { Switch, SwitchGroup, SwitchLabel } from '@headlessui/vue';
import type { BrandButtonProps } from '@unraid/ui';
@@ -276,7 +276,7 @@ const modalWidth = computed(() => {
<template v-if="renderMainSlot" #main>
<BrandLoading v-if="checkForUpdatesLoading" class="w-[150px] mx-auto" />
<div v-else class="flex flex-col gap-y-4">
<div v-if="extraLinks.length > 0" class="flex flex-col xs:flex-row justify-center gap-2">
<div v-if="extraLinks.length > 0" :class="cn('flex flex-col xs:!flex-row justify-center gap-2')">
<BrandButton
v-for="item in extraLinks"
:key="item.text"
@@ -335,13 +335,12 @@ const modalWidth = computed(() => {
<template #footer>
<div
class="w-full flex gap-2 mx-auto"
:class="{
'flex-col-reverse xs:flex-row justify-between': actionButtons,
'justify-center': !actionButtons,
}"
:class="cn(
'w-full flex gap-2 mx-auto',
actionButtons ? 'flex-col-reverse xs:!flex-row justify-between' : 'justify-center'
)"
>
<div class="flex flex-col-reverse xs:flex-row justify-start gap-2">
<div :class="cn('flex flex-col-reverse xs:!flex-row justify-start gap-2')">
<BrandButton
variant="underline-hover-red"
:icon="XMarkIcon"
@@ -355,7 +354,7 @@ const modalWidth = computed(() => {
@click="accountStore.updateOs()"
/>
</div>
<div v-if="actionButtons" class="flex flex-col xs:flex-row justify-end gap-2">
<div v-if="actionButtons" :class="cn('flex flex-col xs:!flex-row justify-end gap-2')">
<BrandButton
v-for="item in actionButtons"
:key="item.text"

View File

@@ -130,9 +130,3 @@ const downgradeButton = ref<UserProfileLink>({
</div>
</CardWrapper>
</template>
<style >
/* Import unraid-ui globals first */
@import '@unraid/ui/styles';
@import '~/assets/main.css';
</style>

View File

@@ -262,9 +262,3 @@ watchEffect(() => {
</div>
</CardWrapper>
</template>
<style >
/* Import unraid-ui globals first */
@import '@unraid/ui/styles';
@import '~/assets/main.css';
</style>

View File

@@ -121,9 +121,3 @@ watchEffect(() => {
</div>
</CardWrapper>
</template>
<style >
/* Import unraid-ui globals first */
@import '@unraid/ui/styles';
@import '~/assets/main.css';
</style>

View File

@@ -4,7 +4,7 @@ import { useI18n } from 'vue-i18n';
import { storeToRefs } from 'pinia';
import { useClipboard } from '@vueuse/core';
import { DropdownMenu } from '@unraid/ui';
import { DropdownMenu, cn } from '@unraid/ui';
import { devConfig } from '~/helpers/env';
import type { Server } from '~/types/server';
@@ -109,10 +109,10 @@ onMounted(() => {
/>
<div
class="text-xs text-header-text-secondary text-right font-semibold leading-normal relative z-10 flex flex-wrap items-baseline justify-end gap-x-1 xs:flex-row xs:gap-x-4"
:class="cn('text-xs text-header-text-secondary text-right font-semibold leading-normal relative z-10 flex flex-wrap xs:!flex-row items-baseline justify-end gap-x-1 xs:gap-x-4')"
>
<UpcUptimeExpire :as="'span'" :t="t" class="text-xs" />
<span class="hidden xs:block">&bull;</span>
<span class="hidden xs:!block">&bull;</span>
<UpcServerState :t="t" class="text-xs" />
</div>
@@ -121,8 +121,8 @@ onMounted(() => {
class="text-md sm:text-lg relative flex flex-col-reverse items-end md:flex-row border-0 text-header-text-primary"
>
<template v-if="description && theme?.descriptionShow">
<span class="text-right text-xs sm:text-lg hidden 2xs:block" v-html="description" />
<span class="text-header-text-secondary hidden md:inline-block px-2">&bull;</span>
<span class="text-right text-xs sm:text-lg hidden 2xs:!block" v-html="description" />
<span class="text-header-text-secondary hidden md:!inline-block px-2">&bull;</span>
</template>
<button
v-if="lanIp"
@@ -164,57 +164,3 @@ onMounted(() => {
</div>
</div>
</template>
<style >
/* Import unraid-ui globals first */
@import '@unraid/ui/styles';
@import '~/assets/main.css';
.unraid_mark_2,
.unraid_mark_4 {
animation: mark_2 1.5s ease infinite;
}
.unraid_mark_3 {
animation: mark_3 1.5s ease infinite;
}
.unraid_mark_6,
.unraid_mark_8 {
animation: mark_6 1.5s ease infinite;
}
.unraid_mark_7 {
animation: mark_7 1.5s ease infinite;
}
@keyframes mark_2 {
50% {
transform: translateY(-40px);
}
100% {
transform: translateY(0);
}
}
@keyframes mark_3 {
50% {
transform: translateY(-62px);
}
100% {
transform: translateY(0);
}
}
@keyframes mark_6 {
50% {
transform: translateY(40px);
}
100% {
transform: translateY(0);
}
}
@keyframes mark_7 {
50% {
transform: translateY(62px);
}
100% {
transform: translateY(0);
}
}
</style>

View File

@@ -410,57 +410,3 @@ const showUpdateEligibility = computed(() => {
</template>
</Modal>
</template>
<style >
/* Import unraid-ui globals first */
@import '@unraid/ui/styles';
@import '~/assets/main.css';
.unraid_mark_2,
.unraid_mark_4 {
animation: mark_2 1.5s ease infinite;
}
.unraid_mark_3 {
animation: mark_3 1.5s ease infinite;
}
.unraid_mark_6,
.unraid_mark_8 {
animation: mark_6 1.5s ease infinite;
}
.unraid_mark_7 {
animation: mark_7 1.5s ease infinite;
}
@keyframes mark_2 {
50% {
transform: translateY(-40px);
}
100% {
transform: translateY(0);
}
}
@keyframes mark_3 {
50% {
transform: translateY(-62px);
}
100% {
transform: translateY(0);
}
}
@keyframes mark_6 {
50% {
transform: translateY(40px);
}
100% {
transform: translateY(0);
}
}
@keyframes mark_7 {
50% {
transform: translateY(62px);
}
100% {
transform: translateY(0);
}
}
</style>

View File

@@ -57,74 +57,3 @@ const showExpireTime = computed(
</template>
</div>
</template>
<style >
/* Import unraid-ui globals first */
@import '@unraid/ui/styles';
@import '~/assets/main.css';
.DropdownWrapper_blip {
box-shadow: var(--ring-offset-shadow), var(--ring-shadow), var(--shadow-foreground);
&::before {
@apply absolute z-20 block;
content: '';
width: 0;
height: 0;
top: -10px;
right: 42px;
border-right: 11px solid transparent;
border-bottom: 11px solid var(--color-popover);
border-left: 11px solid transparent;
}
}
.unraid_mark_2,
.unraid_mark_4 {
animation: mark_2 1.5s ease infinite;
}
.unraid_mark_3 {
animation: mark_3 1.5s ease infinite;
}
.unraid_mark_6,
.unraid_mark_8 {
animation: mark_6 1.5s ease infinite;
}
.unraid_mark_7 {
animation: mark_7 1.5s ease infinite;
}
@keyframes mark_2 {
50% {
transform: translateY(-40px);
}
100% {
transform: translateY(0);
}
}
@keyframes mark_3 {
50% {
transform: translateY(-62px);
}
100% {
transform: translateY(0);
}
}
@keyframes mark_6 {
50% {
transform: translateY(40px);
}
100% {
transform: translateY(0);
}
}
@keyframes mark_7 {
50% {
transform: translateY(62px);
}
100% {
transform: translateY(0);
}
}
</style>

View File

@@ -36,7 +36,7 @@ const upgradeAction = computed((): ServerStateDataAction | undefined => {
<template v-if="purchaseAction">
<UpcServerStateBuy
class="text-orange-dark relative top-px hidden sm:block"
class="text-orange-dark relative top-px hidden sm:!block"
:title="t('Purchase Key')"
@click="purchaseAction.click?.()"
>{{ t('Purchase') }}</UpcServerStateBuy>

View File

@@ -79,9 +79,3 @@ watchEffect(async () => {
</template>
</div>
</template>
<style >
/* Import unraid-ui globals first */
@import '@unraid/ui/styles';
@import '~/assets/main.css';
</style>

View File

@@ -2,6 +2,9 @@ import { createI18n } from 'vue-i18n';
import type { App } from 'vue';
import { DefaultApolloClient } from '@vue/apollo-composable';
// Import Tailwind CSS for web components shadow DOM injection
import tailwindStyles from '~/assets/main.css?inline';
import en_US from '~/locales/en_US.json';
import { createHtmlEntityDecoder } from '~/helpers/i18n-utils';
import { globalPinia } from '~/store/globalPinia';
@@ -46,4 +49,19 @@ export default function (Vue: App) {
// Provide Apollo client for all web components
Vue.provide(DefaultApolloClient, client);
}
// Inject Tailwind CSS into the shadow DOM
Vue.mixin({
mounted() {
if (typeof window !== 'undefined' && this.$el) {
const shadowRoot = this.$el.getRootNode();
if (shadowRoot && shadowRoot !== document && !shadowRoot.querySelector('style[data-tailwind]')) {
const styleElement = document.createElement('style');
styleElement.setAttribute('data-tailwind', 'true');
styleElement.textContent = tailwindStyles;
shadowRoot.prepend(styleElement);
}
}
}
});
}

View File

@@ -34,6 +34,16 @@ console.log(dropConsole ? 'WARN: Console logs are disabled' : 'INFO: Console log
const assetsDir = path.join(__dirname, '../api/dev/webGui/');
/**
* Create a tag configuration
*/
const createWebComponentTag = (name: string, path: string, appContext: string) => ({
async: false,
name,
path,
appContext
});
/**
* Shared terser options for consistent minification
*/
@@ -190,99 +200,30 @@ export default defineNuxtConfig({
{
name: 'UnraidComponents',
viteExtend(config: UserConfig) {
return applySharedViteConfig(config, true);
const sharedConfig = applySharedViteConfig(config, true);
// Optimize CSS while keeping it inlined for functionality
if (!sharedConfig.css) sharedConfig.css = {};
sharedConfig.css.devSourcemap = process.env.NODE_ENV === 'development';
return sharedConfig;
},
tags: [
{
async: false,
name: 'UnraidAuth',
path: '@/components/Auth.ce',
appContext: '@/components/Wrapper/web-component-plugins',
},
{
async: false,
name: 'UnraidConnectSettings',
path: '@/components/ConnectSettings/ConnectSettings.ce',
appContext: '@/components/Wrapper/web-component-plugins',
},
{
async: false,
name: 'UnraidDownloadApiLogs',
path: '@/components/DownloadApiLogs.ce',
appContext: '@/components/Wrapper/web-component-plugins',
},
{
async: false,
name: 'UnraidHeaderOsVersion',
path: '@/components/HeaderOsVersion.ce',
appContext: '@/components/Wrapper/web-component-plugins',
},
{
async: false,
name: 'UnraidModals',
path: '@/components/Modals.ce',
appContext: '@/components/Wrapper/web-component-plugins',
},
{
async: false,
name: 'UnraidUserProfile',
path: '@/components/UserProfile.ce',
appContext: '@/components/Wrapper/web-component-plugins',
},
{
async: false,
name: 'UnraidUpdateOs',
path: '@/components/UpdateOs.ce',
appContext: '@/components/Wrapper/web-component-plugins',
},
{
async: false,
name: 'UnraidDowngradeOs',
path: '@/components/DowngradeOs.ce',
appContext: '@/components/Wrapper/web-component-plugins',
},
{
async: false,
name: 'UnraidRegistration',
path: '@/components/Registration.ce',
appContext: '@/components/Wrapper/web-component-plugins',
},
{
async: false,
name: 'UnraidWanIpCheck',
path: '@/components/WanIpCheck.ce',
appContext: '@/components/Wrapper/web-component-plugins',
},
{
async: false,
name: 'UnraidWelcomeModal',
path: '@/components/Activation/WelcomeModal.ce',
appContext: '@/components/Wrapper/web-component-plugins',
},
{
async: false,
name: 'UnraidSsoButton',
path: '@/components/SsoButton.ce',
appContext: '@/components/Wrapper/web-component-plugins',
},
{
async: false,
name: 'UnraidLogViewer',
path: '@/components/Logs/LogViewer.ce',
appContext: '@/components/Wrapper/web-component-plugins',
},
{
async: false,
name: 'UnraidThemeSwitcher',
path: '@/components/ThemeSwitcher.ce',
appContext: '@/components/Wrapper/web-component-plugins',
},
{
async: false,
name: 'UnraidApiKeyManager',
path: '@/components/ApiKeyPage.ce',
appContext: '@/components/Wrapper/web-component-plugins',
},
createWebComponentTag('UnraidAuth', '@/components/Auth.ce', '@/components/Wrapper/web-component-plugins'),
createWebComponentTag('UnraidConnectSettings', '@/components/ConnectSettings/ConnectSettings.ce', '@/components/Wrapper/web-component-plugins'),
createWebComponentTag('UnraidDownloadApiLogs', '@/components/DownloadApiLogs.ce', '@/components/Wrapper/web-component-plugins'),
createWebComponentTag('UnraidHeaderOsVersion', '@/components/HeaderOsVersion.ce', '@/components/Wrapper/web-component-plugins'),
createWebComponentTag('UnraidModals', '@/components/Modals.ce', '@/components/Wrapper/web-component-plugins'),
createWebComponentTag('UnraidUserProfile', '@/components/UserProfile.ce', '@/components/Wrapper/web-component-plugins'),
createWebComponentTag('UnraidUpdateOs', '@/components/UpdateOs.ce', '@/components/Wrapper/web-component-plugins'),
createWebComponentTag('UnraidDowngradeOs', '@/components/DowngradeOs.ce', '@/components/Wrapper/web-component-plugins'),
createWebComponentTag('UnraidRegistration', '@/components/Registration.ce', '@/components/Wrapper/web-component-plugins'),
createWebComponentTag('UnraidWanIpCheck', '@/components/WanIpCheck.ce', '@/components/Wrapper/web-component-plugins'),
createWebComponentTag('UnraidWelcomeModal', '@/components/Activation/WelcomeModal.ce', '@/components/Wrapper/web-component-plugins'),
createWebComponentTag('UnraidSsoButton', '@/components/SsoButton.ce', '@/components/Wrapper/web-component-plugins'),
createWebComponentTag('UnraidLogViewer', '@/components/Logs/LogViewer.ce', '@/components/Wrapper/web-component-plugins'),
createWebComponentTag('UnraidThemeSwitcher', '@/components/ThemeSwitcher.ce', '@/components/Wrapper/web-component-plugins'),
createWebComponentTag('UnraidApiKeyManager', '@/components/ApiKeyPage.ce', '@/components/Wrapper/web-component-plugins'),
],
},
],

View File

@@ -13,11 +13,12 @@
"prebuild:dev": "pnpm predev",
"build:dev": "nuxi build --dotenv .env.staging && pnpm run manifest-ts && pnpm run deploy-to-unraid:dev",
"build:webgui": "pnpm run type-check && nuxi build --dotenv .env.production && pnpm run manifest-ts && pnpm run copy-to-webgui-repo",
"build": "NODE_ENV=production nuxi build --dotenv .env.production && pnpm run manifest-ts",
"build": "NODE_ENV=production nuxi build --dotenv .env.production && pnpm run manifest-ts && pnpm run validate:css",
"prebuild:watch": "pnpm predev",
"build:watch": "nuxi build --dotenv .env.production --watch && pnpm run manifest-ts",
"generate": "nuxt generate",
"manifest-ts": "node ./scripts/add-timestamp-webcomponent-manifest.js",
"validate:css": "node ./scripts/validate-custom-elements-css.js",
"// Deployment": "",
"unraid:deploy": "pnpm build:dev",
"deploy-to-unraid:dev": "./scripts/deploy-dev.sh",

View File

@@ -220,17 +220,3 @@ watch(
<Toaster rich-colors close-button />
</div>
</template>
<style>
/* Import unraid-ui globals first */
@import '@unraid/ui/styles';
@import '~/assets/main.css';
code {
@apply rounded-lg bg-gray-200 p-1 text-black shadow;
}
pre {
@apply overflow-x-scroll py-3;
}
</style>

View File

@@ -0,0 +1,158 @@
#!/usr/bin/env node
const fs = require('fs');
const path = require('path');
/**
* Recursively find JS files in a directory
*/
function findJSFiles(dir, jsFiles = []) {
if (!fs.existsSync(dir)) {
return jsFiles;
}
const items = fs.readdirSync(dir);
for (const item of items) {
const fullPath = path.join(dir, item);
const stat = fs.statSync(fullPath);
if (stat.isDirectory()) {
findJSFiles(fullPath, jsFiles);
} else if (item.endsWith('.js')) {
jsFiles.push(fullPath);
}
}
return jsFiles;
}
/**
* Validates that Tailwind CSS styles are properly inlined in the JavaScript bundle
*/
function validateCustomElementsCSS() {
console.log('🔍 Validating custom elements JS bundle includes inlined Tailwind styles...');
try {
// Find the custom elements JS files
const customElementsDir = '.nuxt/nuxt-custom-elements/dist';
const jsFiles = findJSFiles(customElementsDir);
if (jsFiles.length === 0) {
throw new Error('No custom elements JS files found in ' + customElementsDir);
}
// Find the largest JS file (likely the main bundle with inlined CSS)
const jsFile = jsFiles.reduce((largest, current) => {
const currentSize = fs.statSync(current).size;
const largestSize = fs.statSync(largest).size;
return currentSize > largestSize ? current : largest;
});
console.log(`📁 Checking JS bundle: ${jsFile}`);
// Read the JS content
const jsContent = fs.readFileSync(jsFile, 'utf8');
// Define required Tailwind indicators (looking for inlined CSS in JS)
const requiredIndicators = [
{
name: 'Tailwind utility classes (inline)',
pattern: /\.flex\s*\{[^}]*display:\s*flex/,
description: 'Basic Tailwind utility classes inlined'
},
{
name: 'Tailwind margin utilities (inline)',
pattern: /\.m-\d+\s*\{[^}]*margin:/,
description: 'Tailwind margin utilities inlined'
},
{
name: 'Tailwind padding utilities (inline)',
pattern: /\.p-\d+\s*\{[^}]*padding:/,
description: 'Tailwind padding utilities inlined'
},
{
name: 'Tailwind color utilities (inline)',
pattern: /\.text-\w+\s*\{[^}]*color:/,
description: 'Tailwind text color utilities inlined'
},
{
name: 'Tailwind background utilities (inline)',
pattern: /\.bg-\w+\s*\{[^}]*background/,
description: 'Tailwind background utilities inlined'
},
{
name: 'CSS custom properties',
pattern: /--[\w-]+:\s*[^;]+;/,
description: 'CSS custom properties (variables)'
},
{
name: 'Responsive breakpoints',
pattern: /@media\s*\([^)]*min-width/,
description: 'Responsive media queries'
},
{
name: 'CSS reset styles',
pattern: /\*[^}]*box-sizing|box-sizing[^}]*border-box/,
description: 'Tailwind CSS reset/normalize styles'
}
];
// Validate each indicator
const results = [];
let allPassed = true;
for (const indicator of requiredIndicators) {
const found = indicator.pattern.test(jsContent);
results.push({
name: indicator.name,
description: indicator.description,
passed: found
});
if (!found) {
allPassed = false;
}
}
// Report results
console.log('\n📊 Validation Results:');
console.log('====================');
for (const result of results) {
const status = result.passed ? '✅' : '❌';
console.log(`${status} ${result.name}`);
if (!result.passed) {
console.log(` └─ Missing: ${result.description}`);
}
}
// File size check
const fileSizeKB = Math.round(fs.statSync(jsFile).size / 1024);
console.log(`\n📏 JS bundle size: ${fileSizeKB} KB`);
if (fileSizeKB < 1000) {
console.log('⚠️ WARNING: JS bundle seems too small, inlined Tailwind styles might not be included');
allPassed = false;
} else {
console.log('✅ JS bundle size looks good');
}
// Final result
if (allPassed) {
console.log('\n🎉 SUCCESS: All Tailwind styles are properly inlined in the JS bundle!');
process.exit(0);
} else {
console.log('\n❌ FAILURE: Some Tailwind styles are missing from the JS bundle!');
console.log('\n💡 This might indicate:');
console.log(' - The CSS inline import in viteExtend is not working properly');
console.log(' - Tailwind configuration is not being processed');
console.log(' - CSS is not being injected into shadow DOM components');
process.exit(1);
}
} catch (error) {
console.error('\n❌ ERROR during validation:', error.message);
process.exit(1);
}
}
// Run the validation
validateCustomElementsCSS();