+
{
-
+
{
-
+
{
@click="accountStore.updateOs()"
/>
-
+
({
-
-
diff --git a/web/components/UpdateOs/Update.vue b/web/components/UpdateOs/Update.vue
index b341f75c1..5332fd62e 100644
--- a/web/components/UpdateOs/Update.vue
+++ b/web/components/UpdateOs/Update.vue
@@ -262,9 +262,3 @@ watchEffect(() => {
-
-
diff --git a/web/components/UpdateOs/UpdateIneligible.vue b/web/components/UpdateOs/UpdateIneligible.vue
index b37695fd7..2a387ba7a 100644
--- a/web/components/UpdateOs/UpdateIneligible.vue
+++ b/web/components/UpdateOs/UpdateIneligible.vue
@@ -121,9 +121,3 @@ watchEffect(() => {
-
-
diff --git a/web/components/UserProfile.ce.vue b/web/components/UserProfile.ce.vue
index 308dbaff5..515aeb859 100644
--- a/web/components/UserProfile.ce.vue
+++ b/web/components/UserProfile.ce.vue
@@ -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(() => {
/>
@@ -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"
>
-
-
+
+
-
-
diff --git a/web/components/UserProfile/CallbackFeedback.vue b/web/components/UserProfile/CallbackFeedback.vue
index 0bd87e81a..edf7a7b3a 100644
--- a/web/components/UserProfile/CallbackFeedback.vue
+++ b/web/components/UserProfile/CallbackFeedback.vue
@@ -410,57 +410,3 @@ const showUpdateEligibility = computed(() => {
-
-
diff --git a/web/components/UserProfile/DropdownLaunchpad.vue b/web/components/UserProfile/DropdownLaunchpad.vue
index f2ad5d52d..572071107 100644
--- a/web/components/UserProfile/DropdownLaunchpad.vue
+++ b/web/components/UserProfile/DropdownLaunchpad.vue
@@ -57,74 +57,3 @@ const showExpireTime = computed(
-
-
diff --git a/web/components/UserProfile/ServerState.vue b/web/components/UserProfile/ServerState.vue
index b86f51b15..8b868420f 100644
--- a/web/components/UserProfile/ServerState.vue
+++ b/web/components/UserProfile/ServerState.vue
@@ -36,7 +36,7 @@ const upgradeAction = computed((): ServerStateDataAction | undefined => {
{{ t('Purchase') }}
diff --git a/web/components/WanIpCheck.ce.vue b/web/components/WanIpCheck.ce.vue
index 174f43bbd..39e21ecfb 100644
--- a/web/components/WanIpCheck.ce.vue
+++ b/web/components/WanIpCheck.ce.vue
@@ -79,9 +79,3 @@ watchEffect(async () => {
-
-
diff --git a/web/components/Wrapper/web-component-plugins.ts b/web/components/Wrapper/web-component-plugins.ts
index d45663025..0fe5109ad 100644
--- a/web/components/Wrapper/web-component-plugins.ts
+++ b/web/components/Wrapper/web-component-plugins.ts
@@ -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);
+ }
+ }
+ }
+ });
+}
diff --git a/web/nuxt.config.ts b/web/nuxt.config.ts
index 7ca7f9896..7838fb6f3 100644
--- a/web/nuxt.config.ts
+++ b/web/nuxt.config.ts
@@ -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'),
],
},
],
diff --git a/web/package.json b/web/package.json
index 776191387..bed352395 100644
--- a/web/package.json
+++ b/web/package.json
@@ -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",
diff --git a/web/pages/index.vue b/web/pages/index.vue
index c3c227c3d..437323282 100644
--- a/web/pages/index.vue
+++ b/web/pages/index.vue
@@ -220,17 +220,3 @@ watch(
-
-
diff --git a/web/scripts/validate-custom-elements-css.js b/web/scripts/validate-custom-elements-css.js
new file mode 100644
index 000000000..d9a042a4c
--- /dev/null
+++ b/web/scripts/validate-custom-elements-css.js
@@ -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();