From 85b250eb809f672a148aee0ceff81b90ff692b48 Mon Sep 17 00:00:00 2001 From: Eli Bosley Date: Sat, 30 Aug 2025 20:49:31 -0400 Subject: [PATCH 01/25] feat: mount vue apps, not web components --- api/generated-schema.graphql | 2 + .../include/web-components-extractor.php | 40 ++- web/components/Wrapper/vue-mount-app.ts | 199 +++++++++++ web/components/standalone-mount.ts | 123 +++++++ web/helpers/create-apollo-client.ts | 37 +- web/nuxt.config.ts | 100 ++---- web/package.json | 5 +- web/public/images/UN-logotype-gradient.svg | 1 + .../add-timestamp-standalone-manifest.js | 29 ++ .../add-timestamp-webcomponent-manifest.js | 7 + web/scripts/deploy-dev.sh | 22 +- web/scripts/validate-custom-elements-css.js | 22 +- web/test-standalone.html | 330 ++++++++++++++++++ web/vite.standalone.config.ts | 85 +++++ web/vite.test.config.ts | 26 ++ 15 files changed, 939 insertions(+), 89 deletions(-) create mode 100644 web/components/Wrapper/vue-mount-app.ts create mode 100644 web/components/standalone-mount.ts create mode 100644 web/public/images/UN-logotype-gradient.svg create mode 100644 web/scripts/add-timestamp-standalone-manifest.js create mode 100644 web/test-standalone.html create mode 100644 web/vite.standalone.config.ts create mode 100644 web/vite.test.config.ts diff --git a/api/generated-schema.graphql b/api/generated-schema.graphql index b996b8ffc..c8663a580 100644 --- a/api/generated-schema.graphql +++ b/api/generated-schema.graphql @@ -1798,6 +1798,8 @@ type Server implements Node { guid: String! apikey: String! name: String! + + """Whether this server is online or offline""" status: ServerStatus! wanip: String! lanip: String! diff --git a/plugin/source/dynamix.unraid.net/usr/local/emhttp/plugins/dynamix.my.servers/include/web-components-extractor.php b/plugin/source/dynamix.unraid.net/usr/local/emhttp/plugins/dynamix.my.servers/include/web-components-extractor.php index ae8e439e0..a40491ae7 100644 --- a/plugin/source/dynamix.unraid.net/usr/local/emhttp/plugins/dynamix.my.servers/include/web-components-extractor.php +++ b/plugin/source/dynamix.unraid.net/usr/local/emhttp/plugins/dynamix.my.servers/include/web-components-extractor.php @@ -8,6 +8,7 @@ class WebComponentsExtractor private const RICH_COMPONENTS_ENTRY_JS = 'unraid-components.client.js'; private const UI_ENTRY = 'src/register.ts'; private const UI_STYLES_ENTRY = 'style.css'; + private const STANDALONE_APPS_ENTRY = 'standalone-apps.js'; private static ?WebComponentsExtractor $instance = null; @@ -98,6 +99,41 @@ class WebComponentsExtractor '; } + private function getStandaloneAppsScript(): string + { + $manifestFiles = $this->findManifestFiles('standalone.manifest.json'); + + if (empty($manifestFiles)) { + // No standalone apps, return empty + return ''; + } + + $manifestPath = $manifestFiles[0]; + $manifest = $this->getManifestContents($manifestPath); + $subfolder = $this->getRelativePath($manifestPath); + + if (!isset($manifest[self::STANDALONE_APPS_ENTRY])) { + return ''; + } + + $jsFile = ($subfolder ? $subfolder . '/' : '') . $manifest[self::STANDALONE_APPS_ENTRY]['file']; + + // Use a unique identifier to prevent duplicate script loading + $scriptId = 'unraid-standalone-apps-script'; + return ' + '; + } + private function getUnraidUiScriptHtml(): string { $manifestFiles = $this->findManifestFiles('ui.manifest.json'); @@ -173,7 +209,9 @@ class WebComponentsExtractor try { $scriptsOutput = true; - return $this->getRichComponentsScript() . $this->getUnraidUiScriptHtml(); + return $this->getRichComponentsScript() . + $this->getUnraidUiScriptHtml() . + $this->getStandaloneAppsScript(); } catch (\Exception $e) { error_log("Error in WebComponentsExtractor::getScriptTagHtml: " . $e->getMessage()); $scriptsOutput = false; // Reset on error diff --git a/web/components/Wrapper/vue-mount-app.ts b/web/components/Wrapper/vue-mount-app.ts new file mode 100644 index 000000000..5e1101fa8 --- /dev/null +++ b/web/components/Wrapper/vue-mount-app.ts @@ -0,0 +1,199 @@ +import { createApp } from 'vue'; +import type { App as VueApp, Component } from 'vue'; +import { createI18n } from 'vue-i18n'; +import { DefaultApolloClient } from '@vue/apollo-composable'; + +// Import Tailwind CSS for 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'; +import { client } from '~/helpers/create-apollo-client'; + +// Global store for mounted apps +const mountedApps = new Map(); + +// Shared style injection tracking +const styleInjected = new WeakSet(); + +// Expose globally for debugging +declare global { + interface Window { + mountedApps: Map; + globalPinia: typeof globalPinia; + } +} + +if (typeof window !== 'undefined') { + window.mountedApps = mountedApps; + window.globalPinia = globalPinia; +} + +function injectStyles(root: Document | ShadowRoot) { + // Always inject to document for teleported elements + if (!styleInjected.has(document)) { + const globalStyleElement = document.createElement('style'); + globalStyleElement.setAttribute('data-tailwind-global', 'true'); + globalStyleElement.textContent = tailwindStyles; + document.head.appendChild(globalStyleElement); + styleInjected.add(document); + } + + // Also inject to shadow root if needed + if (root !== document && !styleInjected.has(root)) { + const styleElement = document.createElement('style'); + styleElement.setAttribute('data-tailwind', 'true'); + styleElement.textContent = tailwindStyles; + root.appendChild(styleElement); + styleInjected.add(root); + } +} + +function setupI18n() { + const defaultLocale = 'en_US'; + let parsedLocale = ''; + let parsedMessages = {}; + let nonDefaultLocale = false; + + // Check for window locale data + if (typeof window !== 'undefined') { + const windowLocaleData = (window as unknown as { LOCALE_DATA?: string }).LOCALE_DATA || null; + if (windowLocaleData) { + try { + parsedMessages = JSON.parse(decodeURIComponent(windowLocaleData)); + parsedLocale = Object.keys(parsedMessages)[0]; + nonDefaultLocale = parsedLocale !== defaultLocale; + } catch (error) { + console.error('[VueMountApp] error parsing messages', error); + } + } + } + + return createI18n({ + legacy: false, + locale: nonDefaultLocale ? parsedLocale : defaultLocale, + fallbackLocale: defaultLocale, + messages: { + en_US, + ...(nonDefaultLocale ? parsedMessages : {}), + }, + postTranslation: createHtmlEntityDecoder(), + }); +} + +export interface MountOptions { + component: Component; + selector: string; + appId?: string; + useShadowRoot?: boolean; + props?: Record; +} + +export function mountVueApp(options: MountOptions): VueApp | null { + const { component, selector, appId = selector, useShadowRoot = false, props = {} } = options; + + // Check if app is already mounted + if (mountedApps.has(appId)) { + console.warn(`[VueMountApp] App ${appId} is already mounted`); + return mountedApps.get(appId)!; + } + + // Find all mount targets + const targets = document.querySelectorAll(selector); + if (targets.length === 0) { + console.warn(`[VueMountApp] No elements found for selector: ${selector}`); + return null; + } + + // Create the Vue app + const app = createApp(component, props); + + // Setup i18n + const i18n = setupI18n(); + app.use(i18n); + + // Use the shared Pinia instance + app.use(globalPinia); + + // Provide Apollo client + app.provide(DefaultApolloClient, client); + + // Mount to all targets + targets.forEach((target) => { + const mountTarget = target as HTMLElement; + + if (useShadowRoot) { + // Create shadow root if needed + if (!mountTarget.shadowRoot) { + mountTarget.attachShadow({ mode: 'open' }); + } + + // Create mount container in shadow root + const container = document.createElement('div'); + container.id = 'app'; + mountTarget.shadowRoot!.appendChild(container); + + // Inject styles into shadow root + injectStyles(mountTarget.shadowRoot!); + + // Clone and mount the app to this container + const clonedApp = createApp(component, props); + clonedApp.use(i18n); + clonedApp.use(globalPinia); + clonedApp.provide(DefaultApolloClient, client); + clonedApp.mount(container); + } else { + // Direct mount without shadow root + injectStyles(document); + + // For multiple targets, we need to create separate app instances + // but they'll share the same Pinia store + if (Array.from(targets).indexOf(mountTarget) === 0) { + // First target, use the main app + app.mount(mountTarget); + } else { + // Additional targets, create cloned apps + const clonedApp = createApp(component, props); + clonedApp.use(i18n); + clonedApp.use(globalPinia); // Shared Pinia instance + clonedApp.provide(DefaultApolloClient, client); + clonedApp.mount(mountTarget); + } + } + }); + + // Store the app reference + mountedApps.set(appId, app); + + return app; +} + +export function unmountVueApp(appId: string): boolean { + const app = mountedApps.get(appId); + if (!app) { + console.warn(`[VueMountApp] No app found with id: ${appId}`); + return false; + } + + app.unmount(); + mountedApps.delete(appId); + return true; +} + +export function getMountedApp(appId: string): VueApp | undefined { + return mountedApps.get(appId); +} + +// Auto-mount function for script tags +export function autoMountComponent(component: Component, selector: string, options?: Partial) { + // Wait for DOM to be ready + if (document.readyState === 'loading') { + document.addEventListener('DOMContentLoaded', () => { + mountVueApp({ component, selector, ...options }); + }); + } else { + // DOM is already ready + mountVueApp({ component, selector, ...options }); + } +} diff --git a/web/components/standalone-mount.ts b/web/components/standalone-mount.ts new file mode 100644 index 000000000..d001775d7 --- /dev/null +++ b/web/components/standalone-mount.ts @@ -0,0 +1,123 @@ +// Import all components +import type { Component } from 'vue'; +import Auth from './Auth.ce.vue'; +import ConnectSettings from './ConnectSettings/ConnectSettings.ce.vue'; +import DownloadApiLogs from './DownloadApiLogs.ce.vue'; +import HeaderOsVersion from './HeaderOsVersion.ce.vue'; +import Modals from './Modals.ce.vue'; +import UserProfile from './UserProfile.ce.vue'; +import UpdateOs from './UpdateOs.ce.vue'; +import DowngradeOs from './DowngradeOs.ce.vue'; +import Registration from './Registration.ce.vue'; +import WanIpCheck from './WanIpCheck.ce.vue'; +import WelcomeModal from './Activation/WelcomeModal.ce.vue'; +import SsoButton from './SsoButton.ce.vue'; +import LogViewer from './Logs/LogViewer.ce.vue'; +import ThemeSwitcher from './ThemeSwitcher.ce.vue'; +import ApiKeyPage from './ApiKeyPage.ce.vue'; +import DevModalTest from './DevModalTest.ce.vue'; +import ApiKeyAuthorize from './ApiKeyAuthorize.ce.vue'; + +// Import utilities +import { autoMountComponent, mountVueApp, getMountedApp } from './Wrapper/vue-mount-app'; +import { useThemeStore } from '~/store/theme'; +import { globalPinia } from '~/store/globalPinia'; + +// Initialize theme store and set CSS variables +if (typeof window !== 'undefined') { + // Ensure pinia is ready + const themeStore = useThemeStore(globalPinia); + themeStore.setTheme(); + themeStore.setCssVars(); +} + +// Define component mappings +const componentMappings = [ + { component: Auth, selector: 'unraid-auth', appId: 'auth' }, + { component: ConnectSettings, selector: 'unraid-connect-settings', appId: 'connect-settings' }, + { component: DownloadApiLogs, selector: 'unraid-download-api-logs', appId: 'download-api-logs' }, + { component: HeaderOsVersion, selector: 'unraid-header-os-version', appId: 'header-os-version' }, + { component: Modals, selector: 'unraid-modals', appId: 'modals' }, + { component: UserProfile, selector: 'unraid-user-profile', appId: 'user-profile' }, + { component: UpdateOs, selector: 'unraid-update-os', appId: 'update-os' }, + { component: DowngradeOs, selector: 'unraid-downgrade-os', appId: 'downgrade-os' }, + { component: Registration, selector: 'unraid-registration', appId: 'registration' }, + { component: WanIpCheck, selector: 'unraid-wan-ip-check', appId: 'wan-ip-check' }, + { component: WelcomeModal, selector: 'unraid-welcome-modal', appId: 'welcome-modal' }, + { component: SsoButton, selector: 'unraid-sso-button', appId: 'sso-button' }, + { component: LogViewer, selector: 'unraid-log-viewer', appId: 'log-viewer' }, + { component: ThemeSwitcher, selector: 'unraid-theme-switcher', appId: 'theme-switcher' }, + { component: ApiKeyPage, selector: 'unraid-api-key-manager', appId: 'api-key-manager' }, + { component: DevModalTest, selector: 'unraid-dev-modal-test', appId: 'dev-modal-test' }, + { component: ApiKeyAuthorize, selector: 'unraid-api-key-authorize', appId: 'api-key-authorize' }, +]; + +// Auto-mount all components +componentMappings.forEach(({ component, selector, appId }) => { + autoMountComponent(component, selector, { + appId, + useShadowRoot: false, // Mount directly to avoid shadow DOM issues + }); +}); + +// Special handling for Modals - also mount to #modals if it exists +if (document.querySelector('#modals')) { + mountVueApp({ + component: Modals, + selector: '#modals', + appId: 'modals-direct', + useShadowRoot: false, + }); +} + +// Expose functions globally for testing and dynamic mounting +declare global { + interface Window { + UnraidComponents: Record; + mountVueApp: typeof mountVueApp; + getMountedApp: typeof getMountedApp; + } +} + +if (typeof window !== 'undefined') { + // Expose all components + window.UnraidComponents = { + Auth, + ConnectSettings, + DownloadApiLogs, + HeaderOsVersion, + Modals, + UserProfile, + UpdateOs, + DowngradeOs, + Registration, + WanIpCheck, + WelcomeModal, + SsoButton, + LogViewer, + ThemeSwitcher, + ApiKeyPage, + DevModalTest, + ApiKeyAuthorize, + }; + + // Expose utility functions + window.mountVueApp = mountVueApp; + window.getMountedApp = getMountedApp; + + // Create dynamic mount functions for each component + componentMappings.forEach(({ component, selector, appId }) => { + const componentName = appId.split('-').map(word => + word.charAt(0).toUpperCase() + word.slice(1) + ).join(''); + + (window as unknown as Record)[`mount${componentName}`] = (customSelector?: string) => { + return mountVueApp({ + component, + selector: customSelector || selector, + appId: `${appId}-${Date.now()}`, + useShadowRoot: false, + }); + }; + }); +} diff --git a/web/helpers/create-apollo-client.ts b/web/helpers/create-apollo-client.ts index ef5907bff..240f0f9a2 100644 --- a/web/helpers/create-apollo-client.ts +++ b/web/helpers/create-apollo-client.ts @@ -4,11 +4,29 @@ import { RetryLink } from '@apollo/client/link/retry/index.js'; import { GraphQLWsLink } from '@apollo/client/link/subscriptions/index.js'; import { getMainDefinition } from '@apollo/client/utilities/index.js'; import { createClient } from 'graphql-ws'; + +import type { ErrorResponse } from '@apollo/client/link/error/index.js'; +import type { GraphQLFormattedError } from 'graphql'; + import { createApolloCache } from './apollo-cache'; import { WEBGUI_GRAPHQL } from './urls'; -const httpEndpoint = WEBGUI_GRAPHQL; -const wsEndpoint = new URL(WEBGUI_GRAPHQL.toString().replace('http', 'ws')); +// Allow overriding the GraphQL endpoint for development/testing +declare global { + interface Window { + GRAPHQL_ENDPOINT?: string; + } +} + +const getGraphQLEndpoint = () => { + if (typeof window !== 'undefined' && window.GRAPHQL_ENDPOINT) { + return new URL(window.GRAPHQL_ENDPOINT); + } + return WEBGUI_GRAPHQL; +}; + +const httpEndpoint = getGraphQLEndpoint(); +const wsEndpoint = new URL(httpEndpoint.toString().replace('http', 'ws')); const DEV_MODE = (globalThis as unknown as { __DEV__: boolean }).__DEV__ ?? false; const headers = { @@ -28,17 +46,15 @@ const wsLink = new GraphQLWsLink( }) ); -// eslint-disable-next-line @typescript-eslint/no-explicit-any -const errorLink = onError(({ graphQLErrors, networkError }: any) => { +const errorLink = onError(({ graphQLErrors, networkError }: ErrorResponse) => { if (graphQLErrors) { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - graphQLErrors.map((error: any) => { + graphQLErrors.forEach((error: GraphQLFormattedError) => { console.error('[GraphQL error]', error); - const errorMsg = error.error?.message ?? error.message; + const errorMsg = + (error as GraphQLFormattedError & { error?: { message?: string } }).error?.message ?? error.message; if (errorMsg?.includes('offline')) { // @todo restart the api, but make sure not to trigger infinite loop } - return error.message; }); } @@ -46,9 +62,8 @@ const errorLink = onError(({ graphQLErrors, networkError }: any) => { console.error(`[Network error]: ${networkError}`); const msg = networkError.message ? networkError.message : networkError; if (typeof msg === 'string' && msg.includes('Unexpected token < in JSON at position 0')) { - return 'Unraid API • CORS Error'; + console.error('Unraid API • CORS Error'); } - return msg; } }); @@ -69,7 +84,7 @@ const retryLink = new RetryLink({ // Disable Apollo Client if not in DEV Mode and server state says unraid-api is not running const disableQueryLink = new ApolloLink((operation, forward) => { if (!DEV_MODE && operation.getContext().serverState?.unraidApi?.status === 'offline') { - return null; + return null; } return forward(operation); }); diff --git a/web/nuxt.config.ts b/web/nuxt.config.ts index 42835796f..bf5a4e657 100644 --- a/web/nuxt.config.ts +++ b/web/nuxt.config.ts @@ -3,7 +3,7 @@ import path from 'path'; import tailwindcss from '@tailwindcss/vite'; import removeConsole from 'vite-plugin-remove-console'; -import type { PluginOption, UserConfig } from 'vite'; +import type { PluginOption } from 'vite'; /** * Used to avoid redeclaring variables in the webgui codebase. @@ -34,15 +34,13 @@ 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 -}); +// REMOVED: No longer needed with standalone mount approach +// const createWebComponentTag = (name: string, path: string, appContext: string) => ({ +// async: false, +// name, +// path, +// appContext +// }); /** * Shared terser options for consistent minification @@ -118,26 +116,24 @@ const sharedDefine = { __VUE_PROD_DEVTOOLS__: false, }; -/** - * Apply shared Vite configuration to a config object - */ -const applySharedViteConfig = (config: UserConfig, includeJQueryIsolation = false) => { - if (!config.plugins) config.plugins = []; - if (!config.define) config.define = {}; - if (!config.build) config.build = {}; - - // Add shared plugins - config.plugins.push(...getSharedPlugins(includeJQueryIsolation)); - - // Merge define values - Object.assign(config.define, sharedDefine); - - // Apply build configuration - config.build.minify = 'terser'; - config.build.terserOptions = sharedTerserOptions; - - return config; -}; +// REMOVED: No longer needed with standalone mount approach +// const applySharedViteConfig = (config: UserConfig, includeJQueryIsolation = false) => { +// if (!config.plugins) config.plugins = []; +// if (!config.define) config.define = {}; +// if (!config.build) config.build = {}; +// +// // Add shared plugins +// config.plugins.push(...getSharedPlugins(includeJQueryIsolation)); +// +// // Merge define values +// Object.assign(config.define, sharedDefine); +// +// // Apply build configuration +// config.build.minify = 'terser'; +// config.build.terserOptions = sharedTerserOptions; +// +// return config; +// }; // https://nuxt.com/docs/api/configuration/nuxt-config export default defineNuxtConfig({ @@ -205,45 +201,11 @@ export default defineNuxtConfig({ }, }, - customElements: { - analyzer: process.env.NODE_ENV !== 'test', - entries: [ - // @ts-expect-error The nuxt-custom-elements module types don't perfectly match our configuration object structure. - // The custom elements configuration requires specific properties and methods that may not align with the - // module's TypeScript definitions, particularly around the viteExtend function and tag configuration format. - { - name: 'UnraidComponents', - viteExtend(config: UserConfig) { - 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: [ - 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'), - createWebComponentTag('UnraidDevModalTest', '@/components/DevModalTest.ce', '@/components/Wrapper/web-component-plugins'), - createWebComponentTag('UnraidApiKeyAuthorize', '@/components/ApiKeyAuthorize.ce', '@/components/Wrapper/web-component-plugins'), - ], - }, - ], - }, + // DISABLED: Using standalone mount approach instead + // customElements: { + // analyzer: process.env.NODE_ENV !== 'test', + // entries: [], + // }, compatibilityDate: '2024-12-05', diff --git a/web/package.json b/web/package.json index f150185b8..d80db4c36 100644 --- a/web/package.json +++ b/web/package.json @@ -13,11 +13,13 @@ "prebuild:dev": "pnpm predev", "build:dev": "nuxi build --dotenv .env.production && 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 && pnpm run validate:css", + "build": "NODE_ENV=production nuxi build --dotenv .env.production && pnpm run build:standalone && pnpm run manifest-ts && pnpm run validate:css", + "build:standalone": "vite build --config vite.standalone.config.ts && pnpm run manifest-standalone", "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", + "manifest-standalone": "node ./scripts/add-timestamp-standalone-manifest.js", "validate:css": "node ./scripts/validate-custom-elements-css.js", "// Deployment": "", "unraid:deploy": "pnpm build:dev", @@ -35,6 +37,7 @@ "test": "vitest run", "test:watch": "vitest", "test:ci": "vitest run", + "test:standalone": "pnpm run build:standalone && vite --config vite.test.config.ts", "// Nuxt": "", "postinstall": "nuxt prepare" }, diff --git a/web/public/images/UN-logotype-gradient.svg b/web/public/images/UN-logotype-gradient.svg new file mode 100644 index 000000000..fe0873037 --- /dev/null +++ b/web/public/images/UN-logotype-gradient.svg @@ -0,0 +1 @@ +unraid.net diff --git a/web/scripts/add-timestamp-standalone-manifest.js b/web/scripts/add-timestamp-standalone-manifest.js new file mode 100644 index 000000000..9e57f75a7 --- /dev/null +++ b/web/scripts/add-timestamp-standalone-manifest.js @@ -0,0 +1,29 @@ +#!/usr/bin/env node + +import fs from 'fs'; +import path from 'path'; + +const distPath = '.nuxt/standalone-apps'; +const manifestPath = path.join(distPath, 'standalone.manifest.json'); + +// Get all JS files in the dist directory +const files = fs.readdirSync(distPath); +const manifest = {}; + +files.forEach(file => { + if (file.endsWith('.js') || file.endsWith('.css')) { + const key = file.replace(/\.(js|css)$/, '.$1'); + manifest[key] = { + file: file, + src: file, + }; + } +}); + +// Add timestamp +manifest.ts = Date.now(); + +// Write manifest +fs.writeFileSync(manifestPath, JSON.stringify(manifest, null, 2)); + +console.log('Standalone apps manifest created:', manifestPath); diff --git a/web/scripts/add-timestamp-webcomponent-manifest.js b/web/scripts/add-timestamp-webcomponent-manifest.js index 3b32b6efe..a797c176a 100644 --- a/web/scripts/add-timestamp-webcomponent-manifest.js +++ b/web/scripts/add-timestamp-webcomponent-manifest.js @@ -2,6 +2,13 @@ const fs = require('fs'); // Read the JSON file const filePath = '../web/.nuxt/nuxt-custom-elements/dist/unraid-components/manifest.json'; + +// Check if file exists (web components are now disabled in favor of standalone) +if (!fs.existsSync(filePath)) { + console.log('Web components manifest not found (using standalone mount instead)'); + process.exit(0); +} + const jsonData = JSON.parse(fs.readFileSync(filePath, 'utf-8')); // Add timestamp (ts) to the JSON data diff --git a/web/scripts/deploy-dev.sh b/web/scripts/deploy-dev.sh index 0c1fa7230..f62a6e990 100755 --- a/web/scripts/deploy-dev.sh +++ b/web/scripts/deploy-dev.sh @@ -10,8 +10,9 @@ fi # Set server name from command-line argument server_name="$1" -# Source directory path +# Source directory paths source_directory=".nuxt/nuxt-custom-elements/dist/unraid-components/" +standalone_directory=".nuxt/standalone-apps/" if [ ! -d "$source_directory" ]; then echo "The web components directory does not exist." @@ -24,6 +25,11 @@ ssh "root@${server_name}" "rm -rf /usr/local/emhttp/plugins/dynamix.my.servers/u rsync_command="rsync -avz -e ssh $source_directory root@${server_name}:/usr/local/emhttp/plugins/dynamix.my.servers/unraid-components/nuxt" +# Also sync standalone apps if they exist +if [ -d "$standalone_directory" ]; then + rsync_standalone="rsync -avz -e ssh $standalone_directory root@${server_name}:/usr/local/emhttp/plugins/dynamix.my.servers/unraid-components/standalone" +fi + echo "Executing the following command:" echo "$rsync_command" @@ -31,6 +37,13 @@ echo "$rsync_command" eval "$rsync_command" exit_code=$? +# Execute standalone rsync if directory exists +if [ -n "$rsync_standalone" ]; then + echo "Executing standalone apps sync:" + echo "$rsync_standalone" + eval "$rsync_standalone" +fi + # Update the auth-request.php file to include the new web component JS update_auth_request() { local server_name="$1" @@ -38,9 +51,16 @@ update_auth_request() { ssh "root@${server_name}" " AUTH_REQUEST_FILE='/usr/local/emhttp/auth-request.php' WEB_COMPS_DIR='/usr/local/emhttp/plugins/dynamix.my.servers/unraid-components/nuxt/_nuxt/' + STANDALONE_DIR='/usr/local/emhttp/plugins/dynamix.my.servers/unraid-components/standalone/' # Find JS files and modify paths mapfile -t JS_FILES < <(find \"\$WEB_COMPS_DIR\" -type f -name \"*.js\" | sed 's|/usr/local/emhttp||' | sort -u) + + # Find standalone JS files if directory exists + if [ -d \"\$STANDALONE_DIR\" ]; then + mapfile -t STANDALONE_JS < <(find \"\$STANDALONE_DIR\" -type f -name \"*.js\" | sed 's|/usr/local/emhttp||' | sort -u) + FILES_TO_ADD+=(\"\${STANDALONE_JS[@]}\") + fi FILES_TO_ADD+=(\"\${JS_FILES[@]}\") diff --git a/web/scripts/validate-custom-elements-css.js b/web/scripts/validate-custom-elements-css.js index d9a042a4c..3d4f5f39d 100644 --- a/web/scripts/validate-custom-elements-css.js +++ b/web/scripts/validate-custom-elements-css.js @@ -29,16 +29,26 @@ function findJSFiles(dir, 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...'); + console.log('🔍 Validating JS bundle includes inlined Tailwind styles...'); try { - // Find the custom elements JS files - const customElementsDir = '.nuxt/nuxt-custom-elements/dist'; - const jsFiles = findJSFiles(customElementsDir); - + // Check standalone apps first (new approach) + const standaloneDir = '.nuxt/standalone-apps'; + let jsFiles = findJSFiles(standaloneDir); + let usingStandalone = true; + + // Fallback to custom elements if standalone doesn't exist if (jsFiles.length === 0) { - throw new Error('No custom elements JS files found in ' + customElementsDir); + const customElementsDir = '.nuxt/nuxt-custom-elements/dist'; + jsFiles = findJSFiles(customElementsDir); + usingStandalone = false; + + if (jsFiles.length === 0) { + throw new Error('No JS files found in standalone apps or custom elements dist'); + } } + + console.log(`📦 Using ${usingStandalone ? 'standalone apps' : 'custom elements'} bundle`); // Find the largest JS file (likely the main bundle with inlined CSS) const jsFile = jsFiles.reduce((largest, current) => { diff --git a/web/test-standalone.html b/web/test-standalone.html new file mode 100644 index 000000000..12f326263 --- /dev/null +++ b/web/test-standalone.html @@ -0,0 +1,330 @@ + + + + + + Standalone Vue Apps Test Page + + + + +
+ + +
+ + + + +
+

🧪 Standalone Vue Apps Test Page

+
Loading...
+ + +
+

Test 1: Single Component Mount

+

Testing single instance of HeaderOsVersion component

+
+ +
+
+ + +
+

Test 2: Multiple Component Mounts (Shared Pinia Store)

+

Testing that multiple instances share the same Pinia store

+
+
+ +
+
+ +
+
+ +
+
+
+ + +
+

Test 3: Dynamic Component Creation

+

Test dynamically adding components after page load

+ + + +
+ +
+
+ + +
+

Test 4: Modal Components

+

Test modal functionality

+ + + +
+ Note: Modals require proper store state to display +
+
+ + +
+

Debug Information

+
+ Waiting for initialization... +
+
+
+ + + + + + + + \ No newline at end of file diff --git a/web/vite.standalone.config.ts b/web/vite.standalone.config.ts new file mode 100644 index 000000000..a9496c004 --- /dev/null +++ b/web/vite.standalone.config.ts @@ -0,0 +1,85 @@ +import { defineConfig } from 'vite'; +import vue from '@vitejs/plugin-vue'; +import path, { resolve } from 'path'; +import fs from 'fs'; + +// Read CSS content at build time +const getCssContent = () => { + const cssFiles = [ + '.nuxt/dist/client/_nuxt/entry.DXd6OtrS.css', + '.output/public/_nuxt/entry.DXd6OtrS.css', + 'assets/main.css' + ]; + + for (const file of cssFiles) { + const fullPath = path.resolve(__dirname, file); + if (fs.existsSync(fullPath)) { + console.log(`Reading CSS from: ${fullPath}`); + return fs.readFileSync(fullPath, 'utf-8'); + } + } + + console.warn('No CSS file found, using empty string'); + return ''; +}; + +export default defineConfig({ + plugins: [ + vue(), + { + name: 'inject-css-content', + transform(code, id) { + // Replace CSS import with actual content + if (id.includes('vue-mount-app')) { + const cssContent = getCssContent(); + const replacement = `const tailwindStyles = ${JSON.stringify(cssContent)};`; + + // Replace the import statement + code = code.replace( + /import tailwindStyles from ['"]~\/assets\/main\.css\?inline['"];?/, + replacement + ); + + return code; + } + return null; + }, + }, + ], + resolve: { + alias: { + '~': resolve(__dirname, './'), + '@': resolve(__dirname, './'), + }, + }, + build: { + outDir: '.nuxt/standalone-apps', + emptyOutDir: true, + lib: { + entry: resolve(__dirname, 'components/standalone-mount.ts'), + name: 'UnraidStandaloneApps', + fileName: 'standalone-apps', + formats: ['es'], + }, + rollupOptions: { + external: [], + output: { + format: 'es', + entryFileNames: 'standalone-apps.js', + chunkFileNames: '[name]-[hash].js', + assetFileNames: '[name]-[hash][extname]', + inlineDynamicImports: false, + }, + }, + cssCodeSplit: false, + minify: 'terser', + terserOptions: { + mangle: { + toplevel: true, + }, + }, + }, + define: { + 'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV || 'production'), + }, +}); diff --git a/web/vite.test.config.ts b/web/vite.test.config.ts new file mode 100644 index 000000000..c49709e97 --- /dev/null +++ b/web/vite.test.config.ts @@ -0,0 +1,26 @@ +import { defineConfig } from 'vite'; +import vue from '@vitejs/plugin-vue'; +import { resolve } from 'path'; + +export default defineConfig({ + plugins: [vue()], + root: '.', + server: { + port: 5173, + open: '/test-standalone.html', + cors: true, + fs: { + strict: false, + }, + }, + resolve: { + alias: { + '~': resolve(__dirname, './'), + '@': resolve(__dirname, './'), + 'vue': 'vue/dist/vue.esm-bundler.js', + }, + }, + optimizeDeps: { + include: ['vue', 'pinia', '@vue/apollo-composable'], + }, +}); From a1d91a0b4dc8c2ebc9f01cec808c26a6f8af14db Mon Sep 17 00:00:00 2001 From: Eli Bosley Date: Sat, 30 Aug 2025 21:03:15 -0400 Subject: [PATCH 02/25] fix: update artifact path and manifest validation logic - Changed the artifact path in the GitHub Actions workflow to point to the new standalone apps directory. - Enhanced the manifest file validation to include support for standalone.manifest.json, allowing for more flexible manifest file requirements. --- .github/workflows/main.yml | 2 +- plugin/builder/build-txz.ts | 14 +++++++++----- 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 6de550a8c..569a9cfa3 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -359,7 +359,7 @@ jobs: uses: actions/upload-artifact@v4 with: name: unraid-wc-rich - path: web/.nuxt/nuxt-custom-elements/dist/unraid-components + path: web/.nuxt/standalone-apps build-plugin-staging-pr: name: Build and Deploy Plugin diff --git a/plugin/builder/build-txz.ts b/plugin/builder/build-txz.ts index 524676e8a..be97ab311 100644 --- a/plugin/builder/build-txz.ts +++ b/plugin/builder/build-txz.ts @@ -29,7 +29,9 @@ const findManifestFiles = async (dir: string): Promise => { } } else if ( entry.isFile() && - (entry.name === "manifest.json" || entry.name === "ui.manifest.json") + (entry.name === "manifest.json" || + entry.name === "ui.manifest.json" || + entry.name === "standalone.manifest.json") ) { files.push(entry.name); } @@ -124,19 +126,21 @@ const validateSourceDir = async (validatedEnv: TxzEnv) => { const manifestFiles = await findManifestFiles(webcomponentDir); const hasManifest = manifestFiles.includes("manifest.json"); + const hasStandaloneManifest = manifestFiles.includes("standalone.manifest.json"); const hasUiManifest = manifestFiles.includes("ui.manifest.json"); - if (!hasManifest || !hasUiManifest) { + // Accept either manifest.json (old web components) or standalone.manifest.json (new standalone apps) + if ((!hasManifest && !hasStandaloneManifest) || !hasUiManifest) { console.log("Existing Manifest Files:", manifestFiles); const missingFiles: string[] = []; - if (!hasManifest) missingFiles.push("manifest.json"); + if (!hasManifest && !hasStandaloneManifest) missingFiles.push("manifest.json or standalone.manifest.json"); if (!hasUiManifest) missingFiles.push("ui.manifest.json"); throw new Error( `Webcomponents missing required file(s): ${missingFiles.join(", ")} - ` + `${!hasUiManifest ? "run 'pnpm build:wc' in unraid-ui for ui.manifest.json" : ""}` + - `${!hasManifest && !hasUiManifest ? " and " : ""}` + - `${!hasManifest ? "run 'pnpm build' in web for manifest.json" : ""}` + `${(!hasManifest && !hasStandaloneManifest) && !hasUiManifest ? " and " : ""}` + + `${(!hasManifest && !hasStandaloneManifest) ? "run 'pnpm build' in web for standalone.manifest.json" : ""}` ); } From b9632b97744eff53f4b0fb875738ad73104ebeac Mon Sep 17 00:00:00 2001 From: Eli Bosley Date: Sat, 30 Aug 2025 21:18:49 -0400 Subject: [PATCH 03/25] style: add unraid-reset class and CSS rules for component styling - Introduced a new CSS class `.unraid-reset` to reset inherited styles for Unraid components, ensuring consistent styling across the application. - Updated `vue-mount-app.ts` to apply the `.unraid-reset` class to mount targets, preventing webgui styles from leaking into Unraid components. --- web/assets/main.css | 33 +++++++++++++++++++++++++ web/components/Wrapper/vue-mount-app.ts | 3 +++ 2 files changed, 36 insertions(+) diff --git a/web/assets/main.css b/web/assets/main.css index d01240b28..6c98f5d40 100644 --- a/web/assets/main.css +++ b/web/assets/main.css @@ -7,3 +7,36 @@ @source "../../unraid-ui/dist/**/*.{js,mjs}"; @source "../../unraid-ui/src/**/*.{vue,ts}"; @source "../**/*.{vue,ts,js}"; + +/* Reset all inherited styles from webgui for Unraid components */ +.unraid-reset, +.unraid-reset * { + /* Reset all inherited properties to initial values */ + all: unset; + + /* Restore essential display properties */ + display: revert; + box-sizing: border-box; + + /* Ensure text is visible */ + color: inherit; + font-family: inherit; + line-height: inherit; +} + +/* Specific resets for interactive elements to restore functionality */ +.unraid-reset button, +.unraid-reset input, +.unraid-reset select, +.unraid-reset textarea { + cursor: revert; +} + +.unraid-reset a { + cursor: pointer; + text-decoration: revert; +} + +.unraid-reset [hidden] { + display: none !important; +} diff --git a/web/components/Wrapper/vue-mount-app.ts b/web/components/Wrapper/vue-mount-app.ts index 5e1101fa8..10d1ffcb6 100644 --- a/web/components/Wrapper/vue-mount-app.ts +++ b/web/components/Wrapper/vue-mount-app.ts @@ -123,6 +123,9 @@ export function mountVueApp(options: MountOptions): VueApp | null { targets.forEach((target) => { const mountTarget = target as HTMLElement; + // Add unraid-reset class to ensure webgui styles don't leak in + mountTarget.classList.add('unraid-reset'); + if (useShadowRoot) { // Create shadow root if needed if (!mountTarget.shadowRoot) { From 32bc79b93dbfc77a30bb494752fd085aafc100f0 Mon Sep 17 00:00:00 2001 From: Eli Bosley Date: Sat, 30 Aug 2025 21:26:49 -0400 Subject: [PATCH 04/25] fix: update CSS validation patterns for Tailwind classes - Enhanced regex patterns in `validate-custom-elements-css.js` to accommodate minified CSS formats, ensuring accurate detection of Tailwind utility classes and other CSS properties. - Adjusted patterns for flex, margin, padding, color, background utilities, CSS custom properties, and responsive breakpoints to support both spaced and non-spaced formats. --- web/scripts/validate-custom-elements-css.js | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/web/scripts/validate-custom-elements-css.js b/web/scripts/validate-custom-elements-css.js index 3d4f5f39d..2e3ca048e 100644 --- a/web/scripts/validate-custom-elements-css.js +++ b/web/scripts/validate-custom-elements-css.js @@ -62,40 +62,41 @@ function validateCustomElementsCSS() { const jsContent = fs.readFileSync(jsFile, 'utf8'); // Define required Tailwind indicators (looking for inlined CSS in JS) + // Updated patterns to work with minified CSS (no spaces) const requiredIndicators = [ { name: 'Tailwind utility classes (inline)', - pattern: /\.flex\s*\{[^}]*display:\s*flex/, + pattern: /\.flex\s*\{[^}]*display:\s*flex|\.flex{display:flex/, description: 'Basic Tailwind utility classes inlined' }, { name: 'Tailwind margin utilities (inline)', - pattern: /\.m-\d+\s*\{[^}]*margin:/, + pattern: /\.m-\d+\s*\{[^}]*margin:|\.m-\d+{[^}]*margin:/, description: 'Tailwind margin utilities inlined' }, { name: 'Tailwind padding utilities (inline)', - pattern: /\.p-\d+\s*\{[^}]*padding:/, + pattern: /\.p-\d+\s*\{[^}]*padding:|\.p-\d+{[^}]*padding:/, description: 'Tailwind padding utilities inlined' }, { name: 'Tailwind color utilities (inline)', - pattern: /\.text-\w+\s*\{[^}]*color:/, + pattern: /\.text-\w+\s*\{[^}]*color:|\.text-\w+{[^}]*color:/, description: 'Tailwind text color utilities inlined' }, { name: 'Tailwind background utilities (inline)', - pattern: /\.bg-\w+\s*\{[^}]*background/, + pattern: /\.bg-\w+\s*\{[^}]*background|\.bg-\w+{[^}]*background/, description: 'Tailwind background utilities inlined' }, { name: 'CSS custom properties', - pattern: /--[\w-]+:\s*[^;]+;/, + pattern: /--[\w-]+:\s*[^;]+;|--[\w-]+:[^;]+;/, description: 'CSS custom properties (variables)' }, { name: 'Responsive breakpoints', - pattern: /@media\s*\([^)]*min-width/, + pattern: /@media\s*\([^)]*min-width|@media\([^)]*min-width/, description: 'Responsive media queries' }, { From 41f11b0f8d138cf8a379fdcf640a2f75d8ae1ac0 Mon Sep 17 00:00:00 2001 From: Eli Bosley Date: Sat, 30 Aug 2025 21:28:09 -0400 Subject: [PATCH 05/25] refactor: enhance CSS content retrieval in Vite config - Updated the CSS content retrieval logic in `vite.standalone.config.ts` to dynamically find and read entry CSS files from specified directories, improving flexibility and maintainability. - Removed hardcoded CSS file paths in favor of a directory-based approach, allowing for easier updates and better organization of CSS assets. --- web/vite.standalone.config.ts | 24 +++++++++++++++--------- 1 file changed, 15 insertions(+), 9 deletions(-) diff --git a/web/vite.standalone.config.ts b/web/vite.standalone.config.ts index a9496c004..568ddf2f9 100644 --- a/web/vite.standalone.config.ts +++ b/web/vite.standalone.config.ts @@ -5,17 +5,23 @@ import fs from 'fs'; // Read CSS content at build time const getCssContent = () => { - const cssFiles = [ - '.nuxt/dist/client/_nuxt/entry.DXd6OtrS.css', - '.output/public/_nuxt/entry.DXd6OtrS.css', - 'assets/main.css' + const directories = [ + '.nuxt/dist/client/_nuxt', + '.output/public/_nuxt' ]; - for (const file of cssFiles) { - const fullPath = path.resolve(__dirname, file); - if (fs.existsSync(fullPath)) { - console.log(`Reading CSS from: ${fullPath}`); - return fs.readFileSync(fullPath, 'utf-8'); + for (const dir of directories) { + const fullDir = path.resolve(__dirname, dir); + if (fs.existsSync(fullDir)) { + // Find entry.*.css files dynamically + const files = fs.readdirSync(fullDir); + const entryFile = files.find(f => f.startsWith('entry.') && f.endsWith('.css')); + + if (entryFile) { + const fullPath = path.join(fullDir, entryFile); + console.log(`Reading CSS from: ${fullPath}`); + return fs.readFileSync(fullPath, 'utf-8'); + } } } From eeed20215f137fe603e2c45d2f377a21f8fbb331 Mon Sep 17 00:00:00 2001 From: Eli Bosley Date: Sat, 30 Aug 2025 21:52:43 -0400 Subject: [PATCH 06/25] feat: add prop parsing from HTML attributes in Vue app mounting - Introduced a helper function `parsePropsFromElement` to extract props from HTML attributes, enhancing the flexibility of prop handling. - Updated `mountVueApp` to utilize parsed props for both the main app and additional targets, allowing for dynamic prop assignment based on the HTML structure. - Improved overall integration of props with Vue components, ensuring a more seamless mounting process. --- web/components/Wrapper/vue-mount-app.ts | 49 +++++++++++++++++++++++-- 1 file changed, 45 insertions(+), 4 deletions(-) diff --git a/web/components/Wrapper/vue-mount-app.ts b/web/components/Wrapper/vue-mount-app.ts index 10d1ffcb6..1207c65f9 100644 --- a/web/components/Wrapper/vue-mount-app.ts +++ b/web/components/Wrapper/vue-mount-app.ts @@ -90,6 +90,42 @@ export interface MountOptions { props?: Record; } +// Helper function to parse props from HTML attributes +function parsePropsFromElement(element: Element): Record { + const props: Record = {}; + + for (const attr of element.attributes) { + const name = attr.name; + const value = attr.value; + + // Skip Vue internal attributes and common HTML attributes + if (name.startsWith('data-v-') || name === 'class' || name === 'id' || name === 'style') { + continue; + } + + // Try to parse JSON values (handles HTML-encoded JSON) + if (value.startsWith('{') || value.startsWith('[')) { + try { + // Decode HTML entities first + const decoded = value + .replace(/"/g, '"') + .replace(/&/g, '&') + .replace(/</g, '<') + .replace(/>/g, '>') + .replace(/'/g, "'"); + props[name] = JSON.parse(decoded); + } catch (_e) { + // If JSON parsing fails, use as string + props[name] = value; + } + } else { + props[name] = value; + } + } + + return props; +} + export function mountVueApp(options: MountOptions): VueApp | null { const { component, selector, appId = selector, useShadowRoot = false, props = {} } = options; @@ -106,8 +142,12 @@ export function mountVueApp(options: MountOptions): VueApp | null { return null; } - // Create the Vue app - const app = createApp(component, props); + // For the first target, parse props from HTML attributes + const firstTarget = targets[0]; + const parsedProps = { ...parsePropsFromElement(firstTarget), ...props }; + + // Create the Vue app with parsed props + const app = createApp(component, parsedProps); // Setup i18n const i18n = setupI18n(); @@ -156,8 +196,9 @@ export function mountVueApp(options: MountOptions): VueApp | null { // First target, use the main app app.mount(mountTarget); } else { - // Additional targets, create cloned apps - const clonedApp = createApp(component, props); + // Additional targets, create cloned apps with their own props + const targetProps = { ...parsePropsFromElement(mountTarget), ...props }; + const clonedApp = createApp(component, targetProps); clonedApp.use(i18n); clonedApp.use(globalPinia); // Shared Pinia instance clonedApp.provide(DefaultApolloClient, client); From 7c59c03786c0e9184a6cc7c107c335726780f431 Mon Sep 17 00:00:00 2001 From: Eli Bosley Date: Sat, 30 Aug 2025 21:58:24 -0400 Subject: [PATCH 07/25] refactor: improve standalone app manifest handling and Vue app mounting - Updated `WebComponentsExtractor` to iterate over all manifest files, ensuring valid standalone apps entries are processed and preventing duplicate script loading. - Enhanced `mountVueApp` to manage multiple clones and their respective shadow-root containers, improving cleanup and organization of mounted Vue apps. - Modified deployment script to capture exit codes from standalone app synchronization, ensuring accurate error reporting during deployment. --- .../include/web-components-extractor.php | 38 ++++++++++++------- web/components/Wrapper/vue-mount-app.ts | 20 ++++++++++ web/helpers/create-apollo-client.ts | 3 +- web/scripts/deploy-dev.sh | 7 +++- 4 files changed, 53 insertions(+), 15 deletions(-) diff --git a/plugin/source/dynamix.unraid.net/usr/local/emhttp/plugins/dynamix.my.servers/include/web-components-extractor.php b/plugin/source/dynamix.unraid.net/usr/local/emhttp/plugins/dynamix.my.servers/include/web-components-extractor.php index a40491ae7..61295ff4c 100644 --- a/plugin/source/dynamix.unraid.net/usr/local/emhttp/plugins/dynamix.my.servers/include/web-components-extractor.php +++ b/plugin/source/dynamix.unraid.net/usr/local/emhttp/plugins/dynamix.my.servers/include/web-components-extractor.php @@ -108,19 +108,27 @@ class WebComponentsExtractor return ''; } - $manifestPath = $manifestFiles[0]; - $manifest = $this->getManifestContents($manifestPath); - $subfolder = $this->getRelativePath($manifestPath); - - if (!isset($manifest[self::STANDALONE_APPS_ENTRY])) { - return ''; - } - - $jsFile = ($subfolder ? $subfolder . '/' : '') . $manifest[self::STANDALONE_APPS_ENTRY]['file']; - - // Use a unique identifier to prevent duplicate script loading - $scriptId = 'unraid-standalone-apps-script'; - return ' + // Iterate over all manifest files to find valid standalone apps entry + foreach ($manifestFiles as $manifestPath) { + $manifest = $this->getManifestContents($manifestPath); + $subfolder = $this->getRelativePath($manifestPath); + + // Check if STANDALONE_APPS_ENTRY exists and has a valid 'file' key + if (!isset($manifest[self::STANDALONE_APPS_ENTRY])) { + continue; // Skip this manifest if entry doesn't exist + } + + $entry = $manifest[self::STANDALONE_APPS_ENTRY]; + if (!isset($entry['file']) || empty($entry['file'])) { + continue; // Skip if 'file' key is missing or empty + } + + // Build the JS file path + $jsFile = ($subfolder ? $subfolder . '/' : '') . $entry['file']; + + // Use a unique identifier to prevent duplicate script loading + $scriptId = 'unraid-standalone-apps-script'; + return ' '; + } + + // Return empty string if no valid standalone apps entry found + return ''; } private function getUnraidUiScriptHtml(): string diff --git a/web/components/Wrapper/vue-mount-app.ts b/web/components/Wrapper/vue-mount-app.ts index 1207c65f9..9d6816100 100644 --- a/web/components/Wrapper/vue-mount-app.ts +++ b/web/components/Wrapper/vue-mount-app.ts @@ -13,6 +13,8 @@ import { client } from '~/helpers/create-apollo-client'; // Global store for mounted apps const mountedApps = new Map(); +const mountedAppClones = new Map(); +const mountedAppContainers = new Map(); // shadow-root containers for cleanup // Shared style injection tracking const styleInjected = new WeakSet(); @@ -160,6 +162,8 @@ export function mountVueApp(options: MountOptions): VueApp | null { app.provide(DefaultApolloClient, client); // Mount to all targets + const clones: VueApp[] = []; + const containers: HTMLElement[] = []; targets.forEach((target) => { const mountTarget = target as HTMLElement; @@ -175,7 +179,9 @@ export function mountVueApp(options: MountOptions): VueApp | null { // Create mount container in shadow root const container = document.createElement('div'); container.id = 'app'; + container.setAttribute('data-app-id', appId); mountTarget.shadowRoot!.appendChild(container); + containers.push(container); // Inject styles into shadow root injectStyles(mountTarget.shadowRoot!); @@ -186,6 +192,7 @@ export function mountVueApp(options: MountOptions): VueApp | null { clonedApp.use(globalPinia); clonedApp.provide(DefaultApolloClient, client); clonedApp.mount(container); + clones.push(clonedApp); } else { // Direct mount without shadow root injectStyles(document); @@ -203,12 +210,15 @@ export function mountVueApp(options: MountOptions): VueApp | null { clonedApp.use(globalPinia); // Shared Pinia instance clonedApp.provide(DefaultApolloClient, client); clonedApp.mount(mountTarget); + clones.push(clonedApp); } } }); // Store the app reference mountedApps.set(appId, app); + if (clones.length) mountedAppClones.set(appId, clones); + if (containers.length) mountedAppContainers.set(appId, containers); return app; } @@ -220,6 +230,16 @@ export function unmountVueApp(appId: string): boolean { return false; } + // Unmount clones first + const clones = mountedAppClones.get(appId) ?? []; + for (const c of clones) c.unmount(); + mountedAppClones.delete(appId); + + // Remove shadow containers + const containers = mountedAppContainers.get(appId) ?? []; + for (const el of containers) el.remove(); + mountedAppContainers.delete(appId); + app.unmount(); mountedApps.delete(appId); return true; diff --git a/web/helpers/create-apollo-client.ts b/web/helpers/create-apollo-client.ts index 240f0f9a2..539ab5d6f 100644 --- a/web/helpers/create-apollo-client.ts +++ b/web/helpers/create-apollo-client.ts @@ -26,7 +26,8 @@ const getGraphQLEndpoint = () => { }; const httpEndpoint = getGraphQLEndpoint(); -const wsEndpoint = new URL(httpEndpoint.toString().replace('http', 'ws')); +const wsEndpoint = new URL(httpEndpoint.toString()); +wsEndpoint.protocol = httpEndpoint.protocol === 'https:' ? 'wss:' : 'ws:'; const DEV_MODE = (globalThis as unknown as { __DEV__: boolean }).__DEV__ ?? false; const headers = { diff --git a/web/scripts/deploy-dev.sh b/web/scripts/deploy-dev.sh index f62a6e990..da65202a6 100755 --- a/web/scripts/deploy-dev.sh +++ b/web/scripts/deploy-dev.sh @@ -42,6 +42,11 @@ if [ -n "$rsync_standalone" ]; then echo "Executing standalone apps sync:" echo "$rsync_standalone" eval "$rsync_standalone" + standalone_exit_code=$? + # If standalone rsync failed, update exit_code + if [ $standalone_exit_code -ne 0 ]; then + exit_code=$standalone_exit_code + fi fi # Update the auth-request.php file to include the new web component JS @@ -114,5 +119,5 @@ elif [[ "$OSTYPE" == "msys" || "$OSTYPE" == "win32" ]]; then powershell.exe -c "(New-Object Media.SoundPlayer 'C:\Windows\Media\Windows Default.wav').PlaySync()" fi -# Exit with the rsync command's exit code +# Exit with the final exit code (non-zero if any command failed) exit $exit_code \ No newline at end of file From f7ad5824369441b8b1f7d6784d416ae64a3c6964 Mon Sep 17 00:00:00 2001 From: Eli Bosley Date: Sat, 30 Aug 2025 22:02:02 -0400 Subject: [PATCH 08/25] refactor: streamline standalone app deployment and manifest generation - Removed redundant modal div from `test-standalone.html`, simplifying the structure for Vue component mounting. - Added a check in `add-timestamp-standalone-manifest.js` to ensure the existence of the standalone apps directory before manifest generation, improving error handling. - Updated `deploy-dev.sh` to enhance the rsync command for standalone apps, ensuring proper synchronization and cleanup of old files during deployment. --- .../add-timestamp-standalone-manifest.js | 6 +++ web/scripts/deploy-dev.sh | 48 +++++++++++-------- web/test-standalone.html | 3 -- 3 files changed, 33 insertions(+), 24 deletions(-) diff --git a/web/scripts/add-timestamp-standalone-manifest.js b/web/scripts/add-timestamp-standalone-manifest.js index 9e57f75a7..a3b2409f5 100644 --- a/web/scripts/add-timestamp-standalone-manifest.js +++ b/web/scripts/add-timestamp-standalone-manifest.js @@ -6,6 +6,12 @@ import path from 'path'; const distPath = '.nuxt/standalone-apps'; const manifestPath = path.join(distPath, 'standalone.manifest.json'); +// Check if directory exists +if (!fs.existsSync(distPath)) { + console.warn(`Directory ${distPath} does not exist. Skipping manifest generation.`); + process.exit(0); +} + // Get all JS files in the dist directory const files = fs.readdirSync(distPath); const manifest = {}; diff --git a/web/scripts/deploy-dev.sh b/web/scripts/deploy-dev.sh index da65202a6..ad27735e3 100755 --- a/web/scripts/deploy-dev.sh +++ b/web/scripts/deploy-dev.sh @@ -27,7 +27,7 @@ rsync_command="rsync -avz -e ssh $source_directory root@${server_name}:/usr/loca # Also sync standalone apps if they exist if [ -d "$standalone_directory" ]; then - rsync_standalone="rsync -avz -e ssh $standalone_directory root@${server_name}:/usr/local/emhttp/plugins/dynamix.my.servers/unraid-components/standalone" + rsync_standalone="rsync -avz --delete -e ssh $standalone_directory root@${server_name}:/usr/local/emhttp/plugins/dynamix.my.servers/unraid-components/standalone/" fi echo "Executing the following command:" @@ -53,59 +53,65 @@ fi update_auth_request() { local server_name="$1" # SSH into server and update auth-request.php - ssh "root@${server_name}" " + ssh "root@${server_name}" bash -s << 'EOF' AUTH_REQUEST_FILE='/usr/local/emhttp/auth-request.php' WEB_COMPS_DIR='/usr/local/emhttp/plugins/dynamix.my.servers/unraid-components/nuxt/_nuxt/' STANDALONE_DIR='/usr/local/emhttp/plugins/dynamix.my.servers/unraid-components/standalone/' # Find JS files and modify paths - mapfile -t JS_FILES < <(find \"\$WEB_COMPS_DIR\" -type f -name \"*.js\" | sed 's|/usr/local/emhttp||' | sort -u) + mapfile -t JS_FILES < <(find "$WEB_COMPS_DIR" -type f -name "*.js" | sed 's|/usr/local/emhttp||' | sort -u) # Find standalone JS files if directory exists - if [ -d \"\$STANDALONE_DIR\" ]; then - mapfile -t STANDALONE_JS < <(find \"\$STANDALONE_DIR\" -type f -name \"*.js\" | sed 's|/usr/local/emhttp||' | sort -u) - FILES_TO_ADD+=(\"\${STANDALONE_JS[@]}\") + if [ -d "$STANDALONE_DIR" ]; then + mapfile -t STANDALONE_JS < <(find "$STANDALONE_DIR" -type f -name "*.js" | sed 's|/usr/local/emhttp||' | sort -u) + FILES_TO_ADD+=("${STANDALONE_JS[@]}") fi - FILES_TO_ADD+=(\"\${JS_FILES[@]}\") + FILES_TO_ADD+=("${JS_FILES[@]}") - if grep -q '\$arrWhitelist' \"\$AUTH_REQUEST_FILE\"; then + if grep -q '\$arrWhitelist' "$AUTH_REQUEST_FILE"; then awk ' BEGIN { in_array = 0 } - /\\\$arrWhitelist\s*=\s*\[/ { + /\$arrWhitelist\s*=\s*\[/ { in_array = 1 - print \$0 + print $0 next } in_array && /^\s*\]/ { in_array = 0 - print \$0 + print $0 next } !in_array || !/\/plugins\/dynamix\.my\.servers\/unraid-components\/nuxt\/_nuxt\/unraid-components\.client-/ { - print \$0 + print $0 } - ' \"\$AUTH_REQUEST_FILE\" > \"\${AUTH_REQUEST_FILE}.tmp\" + ' "$AUTH_REQUEST_FILE" > "${AUTH_REQUEST_FILE}.tmp" # Now add new entries right after the opening bracket - awk -v files_to_add=\"\$(printf '%s\n' \"\${FILES_TO_ADD[@]}\" | sort -u | awk '{printf \" \\\x27%s\\\x27,\n\", \$0}')\" ' - /\\\$arrWhitelist\s*=\s*\[/ { - print \$0 + awk -v files_to_add="$(printf '%s\n' "${FILES_TO_ADD[@]}" | sort -u | awk '{printf " \047%s\047,\n", $0}')" ' + /\$arrWhitelist\s*=\s*\[/ { + print $0 print files_to_add next } { print } - ' \"\${AUTH_REQUEST_FILE}.tmp\" > \"\${AUTH_REQUEST_FILE}\" + ' "${AUTH_REQUEST_FILE}.tmp" > "${AUTH_REQUEST_FILE}" - rm \"\${AUTH_REQUEST_FILE}.tmp\" - echo \"Updated \$AUTH_REQUEST_FILE with new web component JS files\" + rm "${AUTH_REQUEST_FILE}.tmp" + echo "Updated $AUTH_REQUEST_FILE with new web component JS files" else - echo \"\\\$arrWhitelist array not found in \$AUTH_REQUEST_FILE\" + echo "\$arrWhitelist array not found in $AUTH_REQUEST_FILE" fi - " +EOF } update_auth_request "$server_name" +auth_request_exit_code=$? + +# If auth request update failed, update exit_code +if [ $auth_request_exit_code -ne 0 ]; then + exit_code=$auth_request_exit_code +fi # Play built-in sound based on the operating system if [[ "$OSTYPE" == "darwin"* ]]; then diff --git a/web/test-standalone.html b/web/test-standalone.html index 12f326263..18290764d 100644 --- a/web/test-standalone.html +++ b/web/test-standalone.html @@ -106,9 +106,6 @@
-
- -
From 4a39cd986247f854bee6845d003e33ea02b4b12a Mon Sep 17 00:00:00 2001 From: Eli Bosley Date: Sat, 30 Aug 2025 22:06:25 -0400 Subject: [PATCH 09/25] refactor: enhance manifest validation and Vue app mounting logic - Improved validation in `WebComponentsExtractor` to log errors for missing standalone apps entries and file keys, ensuring better error handling during manifest processing. - Updated CSS content retrieval in `vite.standalone.config.ts` to include a fallback mechanism for missing Nuxt CSS files, enhancing robustness. - Simplified modal component mounting in `standalone-mount.ts` by utilizing a dedicated function for better readability and maintainability. - Refined `mountVueApp` logic in `vue-mount-app.ts` to differentiate between the main app and clones, optimizing the mounting process for multiple targets. --- .../include/web-components-extractor.php | 10 +++++--- web/components/Wrapper/vue-mount-app.ts | 23 +++++++++++-------- web/components/standalone-mount.ts | 14 ++++------- web/vite.standalone.config.ts | 7 ++++++ 4 files changed, 33 insertions(+), 21 deletions(-) diff --git a/plugin/source/dynamix.unraid.net/usr/local/emhttp/plugins/dynamix.my.servers/include/web-components-extractor.php b/plugin/source/dynamix.unraid.net/usr/local/emhttp/plugins/dynamix.my.servers/include/web-components-extractor.php index 61295ff4c..01ae5590e 100644 --- a/plugin/source/dynamix.unraid.net/usr/local/emhttp/plugins/dynamix.my.servers/include/web-components-extractor.php +++ b/plugin/source/dynamix.unraid.net/usr/local/emhttp/plugins/dynamix.my.servers/include/web-components-extractor.php @@ -113,14 +113,18 @@ class WebComponentsExtractor $manifest = $this->getManifestContents($manifestPath); $subfolder = $this->getRelativePath($manifestPath); - // Check if STANDALONE_APPS_ENTRY exists and has a valid 'file' key + // Check if STANDALONE_APPS_ENTRY exists if (!isset($manifest[self::STANDALONE_APPS_ENTRY])) { - continue; // Skip this manifest if entry doesn't exist + error_log("Standalone apps manifest at '{$manifestPath}' is missing the '" . self::STANDALONE_APPS_ENTRY . "' entry key"); + return ''; } $entry = $manifest[self::STANDALONE_APPS_ENTRY]; + + // Check if 'file' key exists if (!isset($entry['file']) || empty($entry['file'])) { - continue; // Skip if 'file' key is missing or empty + error_log("Standalone apps manifest at '{$manifestPath}' has entry '" . self::STANDALONE_APPS_ENTRY . "' but is missing the 'file' field"); + return ''; } // Build the JS file path diff --git a/web/components/Wrapper/vue-mount-app.ts b/web/components/Wrapper/vue-mount-app.ts index 9d6816100..549b5d4b0 100644 --- a/web/components/Wrapper/vue-mount-app.ts +++ b/web/components/Wrapper/vue-mount-app.ts @@ -164,7 +164,7 @@ export function mountVueApp(options: MountOptions): VueApp | null { // Mount to all targets const clones: VueApp[] = []; const containers: HTMLElement[] = []; - targets.forEach((target) => { + targets.forEach((target, index) => { const mountTarget = target as HTMLElement; // Add unraid-reset class to ensure webgui styles don't leak in @@ -186,20 +186,25 @@ export function mountVueApp(options: MountOptions): VueApp | null { // Inject styles into shadow root injectStyles(mountTarget.shadowRoot!); - // Clone and mount the app to this container - const clonedApp = createApp(component, props); - clonedApp.use(i18n); - clonedApp.use(globalPinia); - clonedApp.provide(DefaultApolloClient, client); - clonedApp.mount(container); - clones.push(clonedApp); + // For the first target, use the main app, otherwise create clones + if (index === 0) { + app.mount(container); + } else { + const targetProps = { ...parsePropsFromElement(mountTarget), ...props }; + const clonedApp = createApp(component, targetProps); + clonedApp.use(i18n); + clonedApp.use(globalPinia); + clonedApp.provide(DefaultApolloClient, client); + clonedApp.mount(container); + clones.push(clonedApp); + } } else { // Direct mount without shadow root injectStyles(document); // For multiple targets, we need to create separate app instances // but they'll share the same Pinia store - if (Array.from(targets).indexOf(mountTarget) === 0) { + if (index === 0) { // First target, use the main app app.mount(mountTarget); } else { diff --git a/web/components/standalone-mount.ts b/web/components/standalone-mount.ts index d001775d7..015c8e106 100644 --- a/web/components/standalone-mount.ts +++ b/web/components/standalone-mount.ts @@ -60,15 +60,11 @@ componentMappings.forEach(({ component, selector, appId }) => { }); }); -// Special handling for Modals - also mount to #modals if it exists -if (document.querySelector('#modals')) { - mountVueApp({ - component: Modals, - selector: '#modals', - appId: 'modals-direct', - useShadowRoot: false, - }); -} +// Special handling for Modals - also mount to #modals +autoMountComponent(Modals, '#modals', { + appId: 'modals-direct', + useShadowRoot: false, +}); // Expose functions globally for testing and dynamic mounting declare global { diff --git a/web/vite.standalone.config.ts b/web/vite.standalone.config.ts index 568ddf2f9..69858556d 100644 --- a/web/vite.standalone.config.ts +++ b/web/vite.standalone.config.ts @@ -25,6 +25,13 @@ const getCssContent = () => { } } + // Fallback to source asset + const fallback = path.resolve(__dirname, 'assets/main.css'); + if (fs.existsSync(fallback)) { + console.warn('Nuxt CSS not found; falling back to assets/main.css'); + return fs.readFileSync(fallback, 'utf-8'); + } + console.warn('No CSS file found, using empty string'); return ''; }; From ce67257526e00a69265589eed2470130c0159158 Mon Sep 17 00:00:00 2001 From: Eli Bosley Date: Sat, 30 Aug 2025 22:19:17 -0400 Subject: [PATCH 10/25] refactor: enhance CSS reset and improve Vue app mounting logic - Updated the `.unraid-reset` class in `main.css` to include additional properties for better styling consistency across Unraid components. - Refined the `autoMountComponent` function in `vue-mount-app.ts` to check for element existence before mounting, improving robustness and preventing errors during the mounting process. --- web/assets/main.css | 35 ++++++++++++++++--------- web/components/Wrapper/vue-mount-app.ts | 14 +++++++--- 2 files changed, 33 insertions(+), 16 deletions(-) diff --git a/web/assets/main.css b/web/assets/main.css index 6c98f5d40..d1d1b1fb3 100644 --- a/web/assets/main.css +++ b/web/assets/main.css @@ -8,20 +8,31 @@ @source "../../unraid-ui/src/**/*.{vue,ts}"; @source "../**/*.{vue,ts,js}"; -/* Reset all inherited styles from webgui for Unraid components */ -.unraid-reset, -.unraid-reset * { - /* Reset all inherited properties to initial values */ - all: unset; +/* Reset inherited styles from webgui for Unraid components */ +.unraid-reset { + /* Create a new stacking context */ + isolation: isolate; - /* Restore essential display properties */ - display: revert; + /* Reset text and spacing properties that commonly leak from webgui */ + font-size: 1rem; + font-weight: normal; + text-align: left; + text-transform: none; + letter-spacing: normal; + word-spacing: normal; + + /* Reset box model */ + margin: 0; + padding: 0; + border: 0; + + /* Ensure proper box sizing */ + box-sizing: border-box; +} + +.unraid-reset * { + /* Ensure all children use border-box */ box-sizing: border-box; - - /* Ensure text is visible */ - color: inherit; - font-family: inherit; - line-height: inherit; } /* Specific resets for interactive elements to restore functionality */ diff --git a/web/components/Wrapper/vue-mount-app.ts b/web/components/Wrapper/vue-mount-app.ts index 549b5d4b0..bd330c235 100644 --- a/web/components/Wrapper/vue-mount-app.ts +++ b/web/components/Wrapper/vue-mount-app.ts @@ -256,13 +256,19 @@ export function getMountedApp(appId: string): VueApp | undefined { // Auto-mount function for script tags export function autoMountComponent(component: Component, selector: string, options?: Partial) { + const tryMount = () => { + // Check if elements exist before attempting to mount + if (document.querySelector(selector)) { + mountVueApp({ component, selector, ...options }); + } + // Silently skip if no elements found - this is expected for most components + }; + // Wait for DOM to be ready if (document.readyState === 'loading') { - document.addEventListener('DOMContentLoaded', () => { - mountVueApp({ component, selector, ...options }); - }); + document.addEventListener('DOMContentLoaded', tryMount); } else { // DOM is already ready - mountVueApp({ component, selector, ...options }); + tryMount(); } } From c60f7b72046a13861f586d52aecaa4934a177194 Mon Sep 17 00:00:00 2001 From: Eli Bosley Date: Sat, 30 Aug 2025 22:28:35 -0400 Subject: [PATCH 11/25] refactor: enhance CSS isolation and z-index management for modals - Updated the `.unraid-reset` class to apply isolation to non-modal components, preventing style leakage. - Added z-index rules to ensure modals and their backdrops appear above all other content, improving UI layering. - Refined button styles within the `.unraid-reset` class to reset inherited properties for better consistency. --- web/assets/main.css | 41 ++++++++++++++++++++++++++++++++++++++--- 1 file changed, 38 insertions(+), 3 deletions(-) diff --git a/web/assets/main.css b/web/assets/main.css index d1d1b1fb3..e53637953 100644 --- a/web/assets/main.css +++ b/web/assets/main.css @@ -10,9 +10,6 @@ /* Reset inherited styles from webgui for Unraid components */ .unraid-reset { - /* Create a new stacking context */ - isolation: isolate; - /* Reset text and spacing properties that commonly leak from webgui */ font-size: 1rem; font-weight: normal; @@ -30,6 +27,12 @@ box-sizing: border-box; } +/* Apply isolation to non-modal components to prevent style leakage */ +.unraid-reset:not(#teleports):not(#modals):not(unraid-modals) { + /* Create a new stacking context for style isolation */ + isolation: isolate; +} + .unraid-reset * { /* Ensure all children use border-box */ box-sizing: border-box; @@ -41,6 +44,14 @@ .unraid-reset select, .unraid-reset textarea { cursor: revert; + /* Reset button borders that leak from webgui */ + border: none; + background: none; + padding: 0; + margin: 0; + font: inherit; + color: inherit; + text-align: inherit; } .unraid-reset a { @@ -51,3 +62,27 @@ .unraid-reset [hidden] { display: none !important; } + +/* Ensure modals and their backdrops appear above all other content */ +[role="dialog"] { + z-index: 99999 !important; +} + +/* Modal backdrop */ +.fixed.inset-0[aria-hidden="true"], +[data-headlessui-portal] .fixed.inset-0 { + z-index: 99998 !important; +} + +/* Teleport container children */ +#teleports > *, +#modals > *, +unraid-modals > * { + z-index: 99999 !important; +} + +/* Portal roots from HeadlessUI */ +[data-headlessui-portal] { + position: relative; + z-index: 99999 !important; +} From 3cd5c0e8fd51504ebd1d2968f0bc3d9646a56c9a Mon Sep 17 00:00:00 2001 From: Eli Bosley Date: Sat, 30 Aug 2025 22:40:30 -0400 Subject: [PATCH 12/25] refactor: improve CSS reset strategy and deployment script logic - Refined the `.unraid-reset` class in `main.css` to create a CSS layer for resets, enhancing style isolation and preventing leakage from webgui styles. - Updated the deployment script `deploy-dev.sh` to improve checks for the existence of web components and standalone apps, ensuring accurate deployment and error handling. --- web/assets/main.css | 49 +++++++++++++++++-------------------- web/scripts/deploy-dev.sh | 51 +++++++++++++++++++++++---------------- 2 files changed, 52 insertions(+), 48 deletions(-) diff --git a/web/assets/main.css b/web/assets/main.css index e53637953..e60473a51 100644 --- a/web/assets/main.css +++ b/web/assets/main.css @@ -8,34 +8,29 @@ @source "../../unraid-ui/src/**/*.{vue,ts}"; @source "../**/*.{vue,ts,js}"; -/* Reset inherited styles from webgui for Unraid components */ -.unraid-reset { - /* Reset text and spacing properties that commonly leak from webgui */ - font-size: 1rem; - font-weight: normal; - text-align: left; - text-transform: none; - letter-spacing: normal; - word-spacing: normal; - - /* Reset box model */ - margin: 0; - padding: 0; - border: 0; - - /* Ensure proper box sizing */ - box-sizing: border-box; -} +/* Create a CSS layer for resets with lower priority than Tailwind */ +@layer base { + /* Reset inherited styles from webgui for Unraid components */ + .unraid-reset, + .unraid-reset *:not(svg):not(path):not(g):not(circle):not(rect):not(line):not(polyline):not(polygon):not(ellipse):not(text):not(tspan):not(stop):not(defs):not(clipPath):not(mask):not(marker):not(symbol):not(use):not(image):not(pattern) { + /* Reset all CSS properties to browser defaults */ + all: unset; + + /* Restore essential properties */ + display: revert; + box-sizing: border-box; + } -/* Apply isolation to non-modal components to prevent style leakage */ -.unraid-reset:not(#teleports):not(#modals):not(unraid-modals) { - /* Create a new stacking context for style isolation */ - isolation: isolate; -} - -.unraid-reset * { - /* Ensure all children use border-box */ - box-sizing: border-box; + /* Apply isolation to non-modal components to prevent style leakage */ + .unraid-reset:not(#teleports):not(#modals):not(unraid-modals) { + /* Create a new stacking context for style isolation */ + isolation: isolate; + } + + /* Ensure SVG icons render properly - don't reset them */ + .unraid-reset svg { + fill: currentColor; + } } /* Specific resets for interactive elements to restore functionality */ diff --git a/web/scripts/deploy-dev.sh b/web/scripts/deploy-dev.sh index ad27735e3..38de5f317 100755 --- a/web/scripts/deploy-dev.sh +++ b/web/scripts/deploy-dev.sh @@ -11,35 +11,44 @@ fi server_name="$1" # Source directory paths -source_directory=".nuxt/nuxt-custom-elements/dist/unraid-components/" +webcomponents_directory=".nuxt/nuxt-custom-elements/dist/unraid-components/" standalone_directory=".nuxt/standalone-apps/" -if [ ! -d "$source_directory" ]; then - echo "The web components directory does not exist." +# Check what we have to deploy +has_webcomponents=false +has_standalone=false + +if [ -d "$webcomponents_directory" ]; then + has_webcomponents=true +fi + +if [ -d "$standalone_directory" ]; then + has_standalone=true +fi + +# Exit if neither exists +if [ "$has_webcomponents" = false ] && [ "$has_standalone" = false ]; then + echo "Error: Neither web components nor standalone apps directory exists." + echo "Please run 'pnpm build' or 'pnpm build:standalone' first." exit 1 fi -# Replace the value inside the rsync command with the user's input -# Delete existing web components in the target directory -ssh "root@${server_name}" "rm -rf /usr/local/emhttp/plugins/dynamix.my.servers/unraid-components/nuxt/*" +exit_code=0 -rsync_command="rsync -avz -e ssh $source_directory root@${server_name}:/usr/local/emhttp/plugins/dynamix.my.servers/unraid-components/nuxt" - -# Also sync standalone apps if they exist -if [ -d "$standalone_directory" ]; then - rsync_standalone="rsync -avz --delete -e ssh $standalone_directory root@${server_name}:/usr/local/emhttp/plugins/dynamix.my.servers/unraid-components/standalone/" +# Deploy web components if they exist +if [ "$has_webcomponents" = true ]; then + echo "Deploying web components..." + ssh "root@${server_name}" "rm -rf /usr/local/emhttp/plugins/dynamix.my.servers/unraid-components/nuxt/*" + rsync_command="rsync -avz -e ssh $webcomponents_directory root@${server_name}:/usr/local/emhttp/plugins/dynamix.my.servers/unraid-components/nuxt" + echo "$rsync_command" + eval "$rsync_command" + exit_code=$? fi -echo "Executing the following command:" -echo "$rsync_command" - -# Execute the rsync command and capture the exit code -eval "$rsync_command" -exit_code=$? - -# Execute standalone rsync if directory exists -if [ -n "$rsync_standalone" ]; then - echo "Executing standalone apps sync:" +# Deploy standalone apps if they exist +if [ "$has_standalone" = true ]; then + echo "Deploying standalone apps..." + rsync_standalone="rsync -avz --delete -e ssh $standalone_directory root@${server_name}:/usr/local/emhttp/plugins/dynamix.my.servers/unraid-components/standalone/" echo "$rsync_standalone" eval "$rsync_standalone" standalone_exit_code=$? From 9dcd05748e708856ac26d7d8b854e40d780a4506 Mon Sep 17 00:00:00 2001 From: Eli Bosley Date: Sun, 31 Aug 2025 09:00:40 -0400 Subject: [PATCH 13/25] refactor: enhance CSS patching and restoration logic in deployment script - Added a new installation script to patch webgui CSS files, ensuring that styles for specific elements are wrapped with `:not(.unraid-reset)` to prevent style leakage. - Implemented a backup and restoration mechanism for original CSS files, allowing for easy recovery after patching. - Improved the handling of CSS directories and added warnings for missing directories to enhance robustness during deployment. --- plugin/plugins/dynamix.unraid.net.plg | 114 ++++++++++++++++++++++++++ web/assets/main.css | 107 +++++++++++++----------- 2 files changed, 175 insertions(+), 46 deletions(-) diff --git a/plugin/plugins/dynamix.unraid.net.plg b/plugin/plugins/dynamix.unraid.net.plg index 6ebddb974..3ab9ca59e 100755 --- a/plugin/plugins/dynamix.unraid.net.plg +++ b/plugin/plugins/dynamix.unraid.net.plg @@ -161,7 +161,100 @@ exit 0 sed -i 's||\n|' "/usr/local/emhttp/plugins/dynamix/include/DefaultPageLayout.php" fi fi + + ]]> + + + + + ]|$)/", $trimmedLine)) { + // Add :not(.unraid-reset) prefix + $modifiedLines[] = $leadingWhitespace . ":not(.unraid-reset) " . $trimmedLine; + $modified = true; + break; + } + } + + if (!$modified) { + $modifiedLines[] = $line; + } + } + + // Write modified content back + file_put_contents($cssFile, implode("\n", $modifiedLines)); + } + + echo "CSS patching complete.\n"; + ?> ]]> @@ -272,6 +365,27 @@ exit 0 [ -f "$FILE-" ] && mv -f "$FILE-" "$FILE" done + # Restore CSS files from backup + echo "Restoring original CSS files..." + CSS_DIR="/usr/local/emhttp/plugins/dynamix/styles" + BACKUP_DIR="$CSS_DIR/.unraid-api-backup" + + if [ -d "$BACKUP_DIR" ]; then + for backup_file in "$BACKUP_DIR"/*.css; do + if [ -f "$backup_file" ]; then + filename=$(basename "$backup_file") + original_file="$CSS_DIR/$filename" + echo " Restoring $filename..." + cp "$backup_file" "$original_file" + fi + done + # Remove backup directory after restoration + rm -rf "$BACKUP_DIR" + echo "CSS restoration complete." + else + echo "No CSS backup found, skipping restoration." + fi + # Handle the unraid-components directory DIR=/usr/local/emhttp/plugins/dynamix.my.servers/unraid-components # Remove the archive's contents before restoring diff --git a/web/assets/main.css b/web/assets/main.css index e60473a51..d8b470aa9 100644 --- a/web/assets/main.css +++ b/web/assets/main.css @@ -8,76 +8,91 @@ @source "../../unraid-ui/src/**/*.{vue,ts}"; @source "../**/*.{vue,ts,js}"; -/* Create a CSS layer for resets with lower priority than Tailwind */ -@layer base { - /* Reset inherited styles from webgui for Unraid components */ - .unraid-reset, - .unraid-reset *:not(svg):not(path):not(g):not(circle):not(rect):not(line):not(polyline):not(polygon):not(ellipse):not(text):not(tspan):not(stop):not(defs):not(clipPath):not(mask):not(marker):not(symbol):not(use):not(image):not(pattern) { - /* Reset all CSS properties to browser defaults */ - all: unset; - - /* Restore essential properties */ - display: revert; - box-sizing: border-box; - } +/* + * Strategy: Only reset the specific properties that webgui sets + * This preserves Tailwind's ability to style elements + */ - /* Apply isolation to non-modal components to prevent style leakage */ - .unraid-reset:not(#teleports):not(#modals):not(unraid-modals) { - /* Create a new stacking context for style isolation */ - isolation: isolate; - } +/* Container wrapper */ +.unraid-reset { + /* Create isolation and set base styles */ + isolation: isolate; + display: contents; - /* Ensure SVG icons render properly - don't reset them */ - .unraid-reset svg { - fill: currentColor; - } + /* Override webgui font settings */ + font-family: Inter, system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif; + font-size: 16px; + line-height: 1.5; + color: rgb(17 24 39); + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; } -/* Specific resets for interactive elements to restore functionality */ +/* Dark mode */ +.dark .unraid-reset { + color: rgb(243 244 246); +} + +/* Reset only the properties that webgui commonly sets */ +.unraid-reset * { + /* Reset text transforms */ + text-transform: none; + letter-spacing: normal; + + /* Ensure box-sizing is correct */ + box-sizing: border-box; +} + +/* Specific resets for form elements that webgui heavily styles */ .unraid-reset button, .unraid-reset input, .unraid-reset select, .unraid-reset textarea { - cursor: revert; - /* Reset button borders that leak from webgui */ - border: none; - background: none; - padding: 0; - margin: 0; + /* Remove webgui form styling */ + appearance: none; + background-image: none; font: inherit; color: inherit; - text-align: inherit; + min-width: initial; } -.unraid-reset a { +/* Ensure buttons are clickable */ +.unraid-reset button, +.unraid-reset [role="button"], +.unraid-reset input[type="button"], +.unraid-reset input[type="submit"], +.unraid-reset input[type="reset"] { cursor: pointer; - text-decoration: revert; } +/* Links */ +.unraid-reset a { + color: inherit; + text-decoration: none; + cursor: pointer; +} + +/* Hidden elements */ .unraid-reset [hidden] { display: none !important; } -/* Ensure modals and their backdrops appear above all other content */ -[role="dialog"] { - z-index: 99999 !important; +/* SVG icons should inherit color */ +.unraid-reset svg { + fill: currentColor; } -/* Modal backdrop */ -.fixed.inset-0[aria-hidden="true"], -[data-headlessui-portal] .fixed.inset-0 { - z-index: 99998 !important; -} - -/* Teleport container children */ +/* Modal z-index management */ +[role="dialog"], +[data-headlessui-portal], #teleports > *, #modals > *, unraid-modals > * { z-index: 99999 !important; } -/* Portal roots from HeadlessUI */ -[data-headlessui-portal] { - position: relative; - z-index: 99999 !important; -} +/* Modal backdrops */ +.fixed.inset-0[aria-hidden="true"], +[data-headlessui-portal] .fixed.inset-0 { + z-index: 99998 !important; +} \ No newline at end of file From 2594df7e9c785cacd44d3ce5b571d5d898322945 Mon Sep 17 00:00:00 2001 From: Eli Bosley Date: Sun, 31 Aug 2025 09:03:40 -0400 Subject: [PATCH 14/25] refactor: enhance CSS layering and specificity for improved style management - Updated the CSS patching script to wrap styles in a new `@layer` structure, ensuring better isolation and priority management for webgui and unraid-api styles. - Refined the `.unraid-reset` class in `main.css` to utilize CSS layers, enhancing specificity and preventing style conflicts with webgui elements. - Improved the handling of CSS content during patching to ensure all styles are correctly wrapped and prioritized. --- plugin/plugins/dynamix.unraid.net.plg | 63 ++++------ web/assets/main.css | 167 ++++++++++++++++---------- 2 files changed, 127 insertions(+), 103 deletions(-) diff --git a/plugin/plugins/dynamix.unraid.net.plg b/plugin/plugins/dynamix.unraid.net.plg index 3ab9ca59e..8ed331283 100755 --- a/plugin/plugins/dynamix.unraid.net.plg +++ b/plugin/plugins/dynamix.unraid.net.plg @@ -185,18 +185,6 @@ exit 0 mkdir($backupDir, 0755, true); } - // Elements to wrap with :not(.unraid-reset) - $elementsToWrap = [ - // Form inputs - "input", "textarea", "select", "button", "label", - // Tables - "table", "thead", "tbody", "tfoot", "tr", "td", "th", - // Links styled as buttons - "a.button", - // Common structural elements that might have global styles - "form", "fieldset", "legend" - ]; - // Process only default-* and dynamix-* CSS files foreach (glob("$cssDir/*.css") as $cssFile) { $filename = basename($cssFile); @@ -212,7 +200,7 @@ exit 0 $content = file_get_contents($cssFile); // Skip if already patched - if (strpos($content, ":not(.unraid-reset)") !== false) { + if (strpos($content, "@layer webgui") !== false) { echo " $filename already patched, skipping...\n"; continue; } @@ -224,33 +212,34 @@ exit 0 echo " Patching $filename...\n"; - // Process each line - $lines = explode("\n", $content); - $modifiedLines = []; + // Wrap entire CSS file content in a @layer + // This puts all webgui styles in a lower priority layer + $layerPrefix = " +/* Added by Unraid API to prevent style conflicts */ +/* Define layer order - unraid-api layer takes precedence over webgui */ +@layer webgui, unraid-api; + +/* Wrap all existing webgui styles in the webgui layer */ +@layer webgui { +"; - foreach ($lines as $line) { - $modified = false; - $trimmedLine = ltrim($line); - $leadingWhitespace = substr($line, 0, strlen($line) - strlen($trimmedLine)); - - // Check if line starts with any element selector - foreach ($elementsToWrap as $element) { - // Match element at start of selector (with possible pseudo-classes) - if (preg_match("/^" . preg_quote($element, "/") . "([\[\:\.\#\s\,\>]|$)/", $trimmedLine)) { - // Add :not(.unraid-reset) prefix - $modifiedLines[] = $leadingWhitespace . ":not(.unraid-reset) " . $trimmedLine; - $modified = true; - break; - } - } - - if (!$modified) { - $modifiedLines[] = $line; - } - } + $layerSuffix = " +} /* End @layer webgui */ + +/* Styles for Unraid API components */ +@layer unraid-api { + /* Any .unraid-reset elements will have styles that override webgui layer */ + .unraid-reset { + /* This layer has higher priority than webgui layer */ + } +} +"; + + // Wrap the content in the layer + $content = $layerPrefix . $content . $layerSuffix; // Write modified content back - file_put_contents($cssFile, implode("\n", $modifiedLines)); + file_put_contents($cssFile, $content); } echo "CSS patching complete.\n"; diff --git a/web/assets/main.css b/web/assets/main.css index d8b470aa9..52f13a6de 100644 --- a/web/assets/main.css +++ b/web/assets/main.css @@ -9,78 +9,113 @@ @source "../**/*.{vue,ts,js}"; /* - * Strategy: Only reset the specific properties that webgui sets - * This preserves Tailwind's ability to style elements + * Strategy: Use higher specificity to override webgui styles + * without breaking Tailwind utilities */ -/* Container wrapper */ -.unraid-reset { - /* Create isolation and set base styles */ - isolation: isolate; - display: contents; +/* Define layer order explicitly */ +@layer reset, base, components, utilities; + +/* Put resets in lowest priority layer */ +@layer reset { + /* Container with proper isolation */ + .unraid-reset { + isolation: isolate; + display: contents; + + /* Set base typography that webgui can't override */ + font-family: Inter, system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif; + font-size: 16px; + line-height: 1.5; + color: rgb(17 24 39); + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + } - /* Override webgui font settings */ - font-family: Inter, system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif; - font-size: 16px; - line-height: 1.5; - color: rgb(17 24 39); - -webkit-font-smoothing: antialiased; - -moz-osx-font-smoothing: grayscale; -} - -/* Dark mode */ -.dark .unraid-reset { - color: rgb(243 244 246); -} - -/* Reset only the properties that webgui commonly sets */ -.unraid-reset * { - /* Reset text transforms */ - text-transform: none; - letter-spacing: normal; + /* Dark mode */ + .dark .unraid-reset { + color: rgb(243 244 246); + } - /* Ensure box-sizing is correct */ - box-sizing: border-box; + /* Inherit font through all elements */ + .unraid-reset * { + font-family: inherit; + font-size: inherit; + line-height: inherit; + color: inherit; + box-sizing: border-box; + } + + /* Override webgui button styles with higher specificity */ + .unraid-reset button, + .unraid-reset button[type="button"], + .unraid-reset button[type="submit"], + .unraid-reset button[type="reset"], + .unraid-reset input[type="button"], + .unraid-reset input[type="submit"], + .unraid-reset input[type="reset"], + .unraid-reset a.button { + /* Reset ALL webgui button properties including CSS variables */ + font-family: inherit !important; + font-size: inherit !important; + font-weight: normal !important; + letter-spacing: normal !important; + text-transform: none !important; + min-width: auto !important; + margin: 0 !important; + padding: 0 !important; + text-align: center !important; + text-decoration: none !important; + white-space: nowrap !important; + cursor: pointer !important; + outline: none !important; + border-radius: 0 !important; + border: none !important; + color: inherit !important; + background: transparent !important; + background-size: auto !important; + appearance: none !important; + box-sizing: border-box !important; + + /* Override CSS variables from webgui */ + --button-border: none !important; + --button-text-color: inherit !important; + --button-background: transparent !important; + --button-background-size: auto !important; + } + + /* Form elements */ + .unraid-reset input:not([type="button"]):not([type="submit"]):not([type="reset"]), + .unraid-reset select, + .unraid-reset textarea { + font-family: inherit; + font-size: inherit; + color: inherit; + appearance: none; + background: transparent; + border: none; + margin: 0; + padding: 0; + } + + /* Links */ + .unraid-reset a:not(.button) { + color: inherit; + text-decoration: none; + cursor: pointer; + } + + /* SVG icons */ + .unraid-reset svg { + fill: currentColor; + } + + /* Hidden elements */ + .unraid-reset [hidden] { + display: none; + } } -/* Specific resets for form elements that webgui heavily styles */ -.unraid-reset button, -.unraid-reset input, -.unraid-reset select, -.unraid-reset textarea { - /* Remove webgui form styling */ - appearance: none; - background-image: none; - font: inherit; - color: inherit; - min-width: initial; -} - -/* Ensure buttons are clickable */ -.unraid-reset button, -.unraid-reset [role="button"], -.unraid-reset input[type="button"], -.unraid-reset input[type="submit"], -.unraid-reset input[type="reset"] { - cursor: pointer; -} - -/* Links */ -.unraid-reset a { - color: inherit; - text-decoration: none; - cursor: pointer; -} - -/* Hidden elements */ -.unraid-reset [hidden] { - display: none !important; -} - -/* SVG icons should inherit color */ -.unraid-reset svg { - fill: currentColor; -} /* Modal z-index management */ [role="dialog"], From 771dcef4f7b37b19652977e1b747a61a3e8844b0 Mon Sep 17 00:00:00 2001 From: Eli Bosley Date: Sun, 31 Aug 2025 09:10:41 -0400 Subject: [PATCH 15/25] fix: update PHP path in plugin and enhance deployment script for web components - Changed the PHP executable path in the plugin from `/bin/php` to `/usr/bin/php` for better compatibility. - Improved the `deploy-dev.sh` script by ensuring proper quoting in the rsync command and adding a check to create the remote directory for standalone apps, enhancing deployment reliability. --- plugin/plugins/dynamix.unraid.net.plg | 2 +- web/scripts/deploy-dev.sh | 15 +++++++-------- 2 files changed, 8 insertions(+), 9 deletions(-) diff --git a/plugin/plugins/dynamix.unraid.net.plg b/plugin/plugins/dynamix.unraid.net.plg index 8ed331283..f0fe7f55c 100755 --- a/plugin/plugins/dynamix.unraid.net.plg +++ b/plugin/plugins/dynamix.unraid.net.plg @@ -166,7 +166,7 @@ exit 0 - + "${AUTH_REQUEST_FILE}.tmp" From 7ec487468068266ee3c111c5d09626ff08bd62bb Mon Sep 17 00:00:00 2001 From: Eli Bosley Date: Sun, 31 Aug 2025 09:29:27 -0400 Subject: [PATCH 16/25] refactor: enhance CSS backup and restoration logic in deployment script - Added functionality to restore existing CSS backups before creating new ones, ensuring a clean state during deployment. - Updated comments for clarity on the backup process and the creation of the backup directory. - Improved the handling of CSS imports in `main.css` to prevent global resets and enhance style specificity. --- plugin/plugins/dynamix.unraid.net.plg | 13 ++++- web/assets/main.css | 71 +++++++++++++++++++-------- 2 files changed, 63 insertions(+), 21 deletions(-) diff --git a/plugin/plugins/dynamix.unraid.net.plg b/plugin/plugins/dynamix.unraid.net.plg index f0fe7f55c..df0c9c5f5 100755 --- a/plugin/plugins/dynamix.unraid.net.plg +++ b/plugin/plugins/dynamix.unraid.net.plg @@ -180,7 +180,18 @@ exit 0 exit(0); } - // Create backup directory + // First restore any existing backups to ensure clean state + if (is_dir($backupDir)) { + echo "Restoring original CSS files from backup...\n"; + foreach (glob("$backupDir/*.css") as $backupFile) { + $filename = basename($backupFile); + $originalFile = "$cssDir/$filename"; + echo " Restoring $filename...\n"; + copy($backupFile, $originalFile); + } + } + + // Create backup directory if it doesn't exist if (!is_dir($backupDir)) { mkdir($backupDir, 0755, true); } diff --git a/web/assets/main.css b/web/assets/main.css index 52f13a6de..65c2f7602 100644 --- a/web/assets/main.css +++ b/web/assets/main.css @@ -1,4 +1,14 @@ -@import 'tailwindcss'; +/* + * Tailwind v4 configuration without global preflight + * This prevents Tailwind from applying global resets that affect webgui + */ + +/* Define layers first */ +@layer theme, base, components, utilities; + +/* Import only the parts of Tailwind we need - NO PREFLIGHT */ +@import 'tailwindcss/theme.css' layer(theme); +@import 'tailwindcss/utilities.css' layer(utilities); @import 'tw-animate-css'; @import '../../@tailwind-shared/index.css'; @import '@nuxt/ui'; @@ -9,15 +19,10 @@ @source "../**/*.{vue,ts,js}"; /* - * Strategy: Use higher specificity to override webgui styles - * without breaking Tailwind utilities + * Scoped resets - only apply within our components + * This replaces Tailwind's global preflight */ - -/* Define layer order explicitly */ -@layer reset, base, components, utilities; - -/* Put resets in lowest priority layer */ -@layer reset { +@layer base { /* Container with proper isolation */ .unraid-reset { isolation: isolate; @@ -32,18 +37,44 @@ -moz-osx-font-smoothing: grayscale; } + /* Scoped box-sizing reset */ + .unraid-reset *, + .unraid-reset *::before, + .unraid-reset *::after { + box-sizing: border-box; + border-width: 0; + border-style: solid; + border-color: currentColor; + } + + /* Scoped margin reset */ + .unraid-reset blockquote, + .unraid-reset dl, + .unraid-reset dd, + .unraid-reset h1, + .unraid-reset h2, + .unraid-reset h3, + .unraid-reset h4, + .unraid-reset h5, + .unraid-reset h6, + .unraid-reset hr, + .unraid-reset figure, + .unraid-reset p, + .unraid-reset pre { + margin: 0; + } + /* Dark mode */ .dark .unraid-reset { color: rgb(243 244 246); } - /* Inherit font through all elements */ + /* Inherit typography through all elements */ .unraid-reset * { font-family: inherit; font-size: inherit; line-height: inherit; color: inherit; - box-sizing: border-box; } /* Override webgui button styles with higher specificity */ @@ -117,17 +148,17 @@ } -/* Modal z-index management */ -[role="dialog"], -[data-headlessui-portal], -#teleports > *, -#modals > *, -unraid-modals > * { +/* Modal z-index management - scoped to Unraid containers */ +.unraid [role="dialog"], +.unraid [data-headlessui-portal], +.unraid #teleports > *, +.unraid #modals > *, +.unraid unraid-modals > * { z-index: 99999 !important; } -/* Modal backdrops */ -.fixed.inset-0[aria-hidden="true"], -[data-headlessui-portal] .fixed.inset-0 { +/* Modal backdrops - scoped to Unraid containers */ +.unraid .fixed.inset-0[aria-hidden="true"], +.unraid [data-headlessui-portal] .fixed.inset-0 { z-index: 99998 !important; } \ No newline at end of file From fdacc21d0e1aeda12a93cb6b857bccf084130714 Mon Sep 17 00:00:00 2001 From: Eli Bosley Date: Sun, 31 Aug 2025 09:43:54 -0400 Subject: [PATCH 17/25] refactor: improve CSS layer management for better style precedence - Updated the CSS layer definitions in both the plugin and main.css to ensure a clear hierarchy, preventing style conflicts and enhancing specificity. - Revised comments to clarify the intended layer order and its impact on style application, ensuring better maintainability and understanding of the CSS structure. --- plugin/plugins/dynamix.unraid.net.plg | 4 +- web/assets/main.css | 60 ++++++++++++++------------- 2 files changed, 34 insertions(+), 30 deletions(-) diff --git a/plugin/plugins/dynamix.unraid.net.plg b/plugin/plugins/dynamix.unraid.net.plg index df0c9c5f5..21e5b29ba 100755 --- a/plugin/plugins/dynamix.unraid.net.plg +++ b/plugin/plugins/dynamix.unraid.net.plg @@ -227,8 +227,8 @@ exit 0 // This puts all webgui styles in a lower priority layer $layerPrefix = " /* Added by Unraid API to prevent style conflicts */ -/* Define layer order - unraid-api layer takes precedence over webgui */ -@layer webgui, unraid-api; +/* Define layer order - base resets first, then webgui overrides them, then our app styles */ +@layer properties, theme, base, webgui, components, utilities, unraid-api; /* Wrap all existing webgui styles in the webgui layer */ @layer webgui { diff --git a/web/assets/main.css b/web/assets/main.css index 65c2f7602..57beab45a 100644 --- a/web/assets/main.css +++ b/web/assets/main.css @@ -3,8 +3,10 @@ * This prevents Tailwind from applying global resets that affect webgui */ -/* Define layers first */ -@layer theme, base, components, utilities; +/* Layer order is defined by the plugin to ensure proper cascade: + * @layer properties, theme, base, webgui, components, utilities, unraid-api; + * This ensures: base resets < webgui styles < our component styles + * Do not define layers here as it will conflict with the plugin's definition */ /* Import only the parts of Tailwind we need - NO PREFLIGHT */ @import 'tailwindcss/theme.css' layer(theme); @@ -21,8 +23,10 @@ /* * Scoped resets - only apply within our components * This replaces Tailwind's global preflight + * Placed in components layer: higher than webgui but lower than utilities + * This allows Tailwind utilities to override these resets */ -@layer base { +@layer components { /* Container with proper isolation */ .unraid-reset { isolation: isolate; @@ -77,7 +81,7 @@ color: inherit; } - /* Override webgui button styles with higher specificity */ + /* Override webgui button styles - layer precedence handles specificity */ .unraid-reset button, .unraid-reset button[type="button"], .unraid-reset button[type="submit"], @@ -87,32 +91,32 @@ .unraid-reset input[type="reset"], .unraid-reset a.button { /* Reset ALL webgui button properties including CSS variables */ - font-family: inherit !important; - font-size: inherit !important; - font-weight: normal !important; - letter-spacing: normal !important; - text-transform: none !important; - min-width: auto !important; - margin: 0 !important; - padding: 0 !important; - text-align: center !important; - text-decoration: none !important; - white-space: nowrap !important; - cursor: pointer !important; - outline: none !important; - border-radius: 0 !important; - border: none !important; - color: inherit !important; - background: transparent !important; - background-size: auto !important; - appearance: none !important; - box-sizing: border-box !important; + font-family: inherit; + font-size: inherit; + font-weight: normal; + letter-spacing: normal; + text-transform: none; + min-width: auto; + margin: 0; + padding: 0; + text-align: center; + text-decoration: none; + white-space: nowrap; + cursor: pointer; + outline: none; + border-radius: 0; + border: none; + color: inherit; + background: transparent; + background-size: auto; + appearance: none; + box-sizing: border-box; /* Override CSS variables from webgui */ - --button-border: none !important; - --button-text-color: inherit !important; - --button-background: transparent !important; - --button-background-size: auto !important; + --button-border: none; + --button-text-color: inherit; + --button-background: transparent; + --button-background-size: auto; } /* Form elements */ From aa9648105fc1935743e51be0aaf1d5d8fcd0bc76 Mon Sep 17 00:00:00 2001 From: Eli Bosley Date: Sun, 31 Aug 2025 10:00:07 -0400 Subject: [PATCH 18/25] refactor: update CSS patching for improved compatibility and specificity - Modified the CSS patching script to enhance compatibility by updating echo statements and adjusting the patching logic. - Removed unnecessary layer wrapping in the CSS content, simplifying the structure while maintaining style specificity. - Updated comments for clarity on the purpose of the compatibility patch and its impact on CSS management. --- plugin/plugins/dynamix.unraid.net.plg | 31 +++----------- web/assets/main.css | 61 +++++++++++++-------------- 2 files changed, 34 insertions(+), 58 deletions(-) diff --git a/plugin/plugins/dynamix.unraid.net.plg b/plugin/plugins/dynamix.unraid.net.plg index 21e5b29ba..3298c2acf 100755 --- a/plugin/plugins/dynamix.unraid.net.plg +++ b/plugin/plugins/dynamix.unraid.net.plg @@ -170,7 +170,7 @@ exit 0 Date: Sun, 31 Aug 2025 12:37:53 -0400 Subject: [PATCH 19/25] feat: implement CSS class prefixing and exclusion logic for improved style management - Introduced a new Vite plugin to prefix CSS classes, ensuring that styles from the webgui do not interfere with our components. - Enhanced the CSS patching script to apply exclusion selectors, preventing style leakage from `.unapi` containers. - Updated the Nuxt configuration to integrate the new postcssPrefixPlugin, allowing for better control over CSS class names. - Modified the Vue app mounting logic to add the `.unapi` class for improved style isolation and backward compatibility with `.unraid-reset`. --- plugin/plugins/dynamix.unraid.net.plg | 110 ++++++++++++++++++++++-- web/components/Wrapper/vue-mount-app.ts | 4 +- web/nuxt.config.ts | 21 +++++ web/vite-plugin-prefix-classes.ts | 95 ++++++++++++++++++++ 4 files changed, 223 insertions(+), 7 deletions(-) create mode 100644 web/vite-plugin-prefix-classes.ts diff --git a/plugin/plugins/dynamix.unraid.net.plg b/plugin/plugins/dynamix.unraid.net.plg index 3298c2acf..470ad36dd 100755 --- a/plugin/plugins/dynamix.unraid.net.plg +++ b/plugin/plugins/dynamix.unraid.net.plg @@ -196,6 +196,105 @@ exit 0 mkdir($backupDir, 0755, true); } + // Define selectors to exclude from webgui styles + // Simply exclude anything inside .unapi containers + $exclusionSelectors = [ + '.unapi', + '.unraid-reset' // Keep for backwards compatibility + ]; + + // Function to patch CSS rules to exclude our components + function patchCssContent($content, $exclusionSelectors) { + // Split CSS into rules while preserving structure + $lines = explode("\n", $content); + $inRule = false; + $currentRule = []; + $output = []; + $braceDepth = 0; + + foreach ($lines as $line) { + // Count braces to track rule boundaries + $openBraces = substr_count($line, '{'); + $closeBraces = substr_count($line, '}'); + + // Check if this line starts a new rule + if (!$inRule && strpos($line, '{') !== false && !preg_match('/^\s*@/', $line)) { + $inRule = true; + $currentRule = [$line]; + $braceDepth = $openBraces - $closeBraces; + } elseif ($inRule) { + $currentRule[] = $line; + $braceDepth += $openBraces - $closeBraces; + + // Check if rule is complete + if ($braceDepth <= 0) { + // Process the complete rule + $ruleText = implode("\n", $currentRule); + + // Extract selector (everything before first {) + if (preg_match('/^([^{]+)\{/', $ruleText, $matches)) { + $selector = trim($matches[1]); + + // Skip if this is an @-rule or already has :not() + if (strpos($selector, '@') === 0 || strpos($selector, ':not(') !== false) { + $output[] = $ruleText; + } else { + // Add exclusions to problematic selectors + $needsPatch = false; + + // Check if this selector could affect our components + // Target generic selectors, element selectors, and broad class selectors + if (preg_match('/^(\*|body|html|div|button|input|a|span|p|h[1-6]|ul|li|form|label|select|textarea)(\s|,|$|\.|#|\[)/', $selector) || + strpos($selector, '*') !== false || + preg_match('/^\.[a-z]/', $selector)) { + $needsPatch = true; + } + + if ($needsPatch) { + // Parse individual selectors + $selectors = array_map('trim', explode(',', $selector)); + $patchedSelectors = []; + + foreach ($selectors as $sel) { + // Add :not(.unapi) for element selectors + // Or prepend :not(.unapi) * for other selectors + if (preg_match('/^[a-z]+/i', $sel) && strpos($sel, '.') === false && strpos($sel, '#') === false) { + // Pure element selector (button, input, etc) + // Exclude elements that have .unapi class or are inside .unapi + $patchedSel = $sel . ':not(.unapi):not(.unapi ' . $sel . ')'; + } else { + // Class or ID selector - ensure it doesn't apply within .unapi + $patchedSel = ':not(.unapi) ' . $sel . ':not(.unapi)'; + } + $patchedSelectors[] = $patchedSel; + } + + // Rebuild the rule with patched selectors + $newSelector = implode(', ', $patchedSelectors); + $ruleText = preg_replace('/^[^{]+\{/', $newSelector . ' {', $ruleText); + } + } + } + + $output[] = $ruleText; + $inRule = false; + $currentRule = []; + $braceDepth = 0; + } + } else { + // Not in a rule, just pass through + $output[] = $line; + } + } + + // Add any remaining lines + if (!empty($currentRule)) { + $output = array_merge($output, $currentRule); + } + + return implode("\n", $output); + } + // Process only default-* and dynamix-* CSS files foreach (glob("$cssDir/*.css") as $cssFile) { $filename = basename($cssFile); @@ -211,7 +310,7 @@ exit 0 $content = file_get_contents($cssFile); // Skip if already patched - if (strpos($content, "/* Unraid API compatibility patch */") !== false) { + if (strpos($content, "/* Unraid API compatibility patch - exclusion based */") !== false) { echo " $filename already patched, skipping...\n"; continue; } @@ -223,13 +322,12 @@ exit 0 echo " Patching $filename...\n"; - // Add compatibility patch comment - $content = "/* Unraid API compatibility patch */\n" . $content; - - // No layer wrapping - keep original CSS as-is + // Add compatibility patch comment and apply exclusion patches + $patchedContent = "/* Unraid API compatibility patch - exclusion based */\n"; + $patchedContent .= patchCssContent($content, $exclusionSelectors); // Write modified content back - file_put_contents($cssFile, $content); + file_put_contents($cssFile, $patchedContent); } echo "CSS patching complete.\n"; diff --git a/web/components/Wrapper/vue-mount-app.ts b/web/components/Wrapper/vue-mount-app.ts index bd330c235..afba5d483 100644 --- a/web/components/Wrapper/vue-mount-app.ts +++ b/web/components/Wrapper/vue-mount-app.ts @@ -167,7 +167,9 @@ export function mountVueApp(options: MountOptions): VueApp | null { targets.forEach((target, index) => { const mountTarget = target as HTMLElement; - // Add unraid-reset class to ensure webgui styles don't leak in + // Add unapi class to ensure our prefixed styles apply and webgui styles are excluded + mountTarget.classList.add('unapi'); + // Keep unraid-reset for backwards compatibility mountTarget.classList.add('unraid-reset'); if (useShadowRoot) { diff --git a/web/nuxt.config.ts b/web/nuxt.config.ts index bf5a4e657..52317edcc 100644 --- a/web/nuxt.config.ts +++ b/web/nuxt.config.ts @@ -2,6 +2,7 @@ import path from 'path'; import tailwindcss from '@tailwindcss/vite'; import removeConsole from 'vite-plugin-remove-console'; +import postcssPrefixPlugin from './postcss-prefix-plugin.js'; import type { PluginOption } from 'vite'; @@ -199,6 +200,26 @@ export default defineNuxtConfig({ minify: 'terser', terserOptions: sharedTerserOptions, }, + css: { + postcss: { + plugins: [ + postcssPrefixPlugin({ + prefix: '.unapi', + exclude: [ + // Don't prefix Tailwind's reset styles + /^:root/, + /^html/, + /^body$/, + /^\*/, + // Don't prefix keyframes + /^@/, + // Already prefixed + /^\.unapi/ + ] + }) + ] + } + } }, // DISABLED: Using standalone mount approach instead diff --git a/web/vite-plugin-prefix-classes.ts b/web/vite-plugin-prefix-classes.ts new file mode 100644 index 000000000..03ee92b7f --- /dev/null +++ b/web/vite-plugin-prefix-classes.ts @@ -0,0 +1,95 @@ +import type { Plugin } from 'vite'; + +/** + * Vite plugin to prefix all CSS classes with a unique identifier + * This allows us to exclude our components from webgui styles + */ +export function prefixClasses(prefix = 'unapi'): Plugin { + return { + name: 'vite-plugin-prefix-classes', + + // Transform CSS files + transform(code, id) { + // Only process CSS files and Vue SFC styles + if (!id.match(/\.(css|scss|sass|less|styl|stylus|postcss)$/) && !id.includes('?vue&type=style')) { + return null; + } + + // Skip node_modules + if (id.includes('node_modules')) { + return null; + } + + // Prefix class selectors in CSS + const prefixedCss = code.replace( + /\.([a-zA-Z_][\w-]*)/g, + (match, className) => { + // Skip if already prefixed + if (className.startsWith(prefix)) { + return match; + } + // Skip Tailwind internals and special classes + if (className.startsWith('__') || className.startsWith('group-') || className.startsWith('peer-')) { + return match; + } + return `.${prefix}-${className}`; + } + ); + + return { + code: prefixedCss, + map: null + }; + }, + + // Transform HTML/templates in Vue files + async transform(code, id) { + // Only process Vue files + if (!id.endsWith('.vue')) { + return null; + } + + // Prefix classes in template + const prefixedCode = code.replace( + /class="([^"]*)"/g, + (match, classes) => { + const prefixedClasses = classes + .split(/\s+/) + .filter(Boolean) + .map(cls => { + // Skip if already prefixed or is a binding + if (cls.startsWith(prefix) || cls.includes(':') || cls.includes('[') || cls.includes('{')) { + return cls; + } + return `${prefix}-${cls}`; + }) + .join(' '); + return `class="${prefixedClasses}"`; + } + ); + + // Also handle :class bindings with static strings + const finalCode = prefixedCode.replace( + /:class="'([^']+)'"/g, + (match, classes) => { + const prefixedClasses = classes + .split(/\s+/) + .filter(Boolean) + .map(cls => { + if (cls.startsWith(prefix)) { + return cls; + } + return `${prefix}-${cls}`; + }) + .join(' '); + return `:class="'${prefixedClasses}'"`; + } + ); + + return { + code: finalCode, + map: null + }; + } + }; +} From 3faa637d9793defe7057fcb906070d9717518682 Mon Sep 17 00:00:00 2001 From: Eli Bosley Date: Sun, 31 Aug 2025 13:06:00 -0400 Subject: [PATCH 20/25] refactor: streamline CSS patching and layer management for improved style isolation - Simplified the CSS patching function to wrap styles in a single `@layer`, enhancing control over cascade order and ensuring Tailwind styles can override as needed. - Removed the previous exclusion selectors logic, focusing on a more efficient layer-based approach to prevent style conflicts with webgui elements. - Updated the Nuxt configuration to eliminate the postcssPrefixPlugin, reflecting the shift towards layer management for CSS class handling. - Enhanced the main.css file to define layer order explicitly, ensuring that webgui styles are overridden by Tailwind utilities effectively. --- plugin/plugins/dynamix.unraid.net.plg | 108 ++------------------------ web/assets/main.css | 51 +++++++++++- web/nuxt.config.ts | 21 ----- web/vite-plugin-prefix-classes.ts | 95 ---------------------- 4 files changed, 57 insertions(+), 218 deletions(-) delete mode 100644 web/vite-plugin-prefix-classes.ts diff --git a/plugin/plugins/dynamix.unraid.net.plg b/plugin/plugins/dynamix.unraid.net.plg index 470ad36dd..f24783b66 100755 --- a/plugin/plugins/dynamix.unraid.net.plg +++ b/plugin/plugins/dynamix.unraid.net.plg @@ -196,103 +196,11 @@ exit 0 mkdir($backupDir, 0755, true); } - // Define selectors to exclude from webgui styles - // Simply exclude anything inside .unapi containers - $exclusionSelectors = [ - '.unapi', - '.unraid-reset' // Keep for backwards compatibility - ]; - - // Function to patch CSS rules to exclude our components - function patchCssContent($content, $exclusionSelectors) { - // Split CSS into rules while preserving structure - $lines = explode("\n", $content); - $inRule = false; - $currentRule = []; - $output = []; - $braceDepth = 0; - - foreach ($lines as $line) { - // Count braces to track rule boundaries - $openBraces = substr_count($line, '{'); - $closeBraces = substr_count($line, '}'); - - // Check if this line starts a new rule - if (!$inRule && strpos($line, '{') !== false && !preg_match('/^\s*@/', $line)) { - $inRule = true; - $currentRule = [$line]; - $braceDepth = $openBraces - $closeBraces; - } elseif ($inRule) { - $currentRule[] = $line; - $braceDepth += $openBraces - $closeBraces; - - // Check if rule is complete - if ($braceDepth <= 0) { - // Process the complete rule - $ruleText = implode("\n", $currentRule); - - // Extract selector (everything before first {) - if (preg_match('/^([^{]+)\{/', $ruleText, $matches)) { - $selector = trim($matches[1]); - - // Skip if this is an @-rule or already has :not() - if (strpos($selector, '@') === 0 || strpos($selector, ':not(') !== false) { - $output[] = $ruleText; - } else { - // Add exclusions to problematic selectors - $needsPatch = false; - - // Check if this selector could affect our components - // Target generic selectors, element selectors, and broad class selectors - if (preg_match('/^(\*|body|html|div|button|input|a|span|p|h[1-6]|ul|li|form|label|select|textarea)(\s|,|$|\.|#|\[)/', $selector) || - strpos($selector, '*') !== false || - preg_match('/^\.[a-z]/', $selector)) { - $needsPatch = true; - } - - if ($needsPatch) { - // Parse individual selectors - $selectors = array_map('trim', explode(',', $selector)); - $patchedSelectors = []; - - foreach ($selectors as $sel) { - // Add :not(.unapi) for element selectors - // Or prepend :not(.unapi) * for other selectors - if (preg_match('/^[a-z]+/i', $sel) && strpos($sel, '.') === false && strpos($sel, '#') === false) { - // Pure element selector (button, input, etc) - // Exclude elements that have .unapi class or are inside .unapi - $patchedSel = $sel . ':not(.unapi):not(.unapi ' . $sel . ')'; - } else { - // Class or ID selector - ensure it doesn't apply within .unapi - $patchedSel = ':not(.unapi) ' . $sel . ':not(.unapi)'; - } - $patchedSelectors[] = $patchedSel; - } - - // Rebuild the rule with patched selectors - $newSelector = implode(', ', $patchedSelectors); - $ruleText = preg_replace('/^[^{]+\{/', $newSelector . ' {', $ruleText); - } - } - } - - $output[] = $ruleText; - $inRule = false; - $currentRule = []; - $braceDepth = 0; - } - } else { - // Not in a rule, just pass through - $output[] = $line; - } - } - - // Add any remaining lines - if (!empty($currentRule)) { - $output = array_merge($output, $currentRule); - } - - return implode("\n", $output); + // Function to patch CSS - we'll just add a layer wrapper + function patchCssContent($content) { + // Simply wrap everything in a @layer to control cascade order + // Our Tailwind styles in later layers will override + return "@layer webgui {\n" . $content . "\n}"; } // Process only default-* and dynamix-* CSS files @@ -322,9 +230,9 @@ exit 0 echo " Patching $filename...\n"; - // Add compatibility patch comment and apply exclusion patches - $patchedContent = "/* Unraid API compatibility patch - exclusion based */\n"; - $patchedContent .= patchCssContent($content, $exclusionSelectors); + // Add compatibility patch comment and wrap in layer + $patchedContent = "/* Unraid API compatibility patch - layer based */\n"; + $patchedContent .= patchCssContent($content); // Write modified content back file_put_contents($cssFile, $patchedContent); diff --git a/web/assets/main.css b/web/assets/main.css index 6024b8f55..3f68109ed 100644 --- a/web/assets/main.css +++ b/web/assets/main.css @@ -3,8 +3,8 @@ * This prevents Tailwind from applying global resets that affect webgui */ -/* Define layers for Tailwind v4 */ -@layer theme, base, components, utilities; +/* Define layers for Tailwind v4 - webgui first so our layers override */ +@layer webgui, theme, base, components, utilities; /* Import only the parts of Tailwind we need - NO PREFLIGHT */ @import 'tailwindcss/theme.css' layer(theme); @@ -70,6 +70,53 @@ color: rgb(243 244 246); } + /* + * Reset interactive elements to beat webgui styles + * Using @layer to ensure Tailwind utilities can still override + */ + @layer base { + .unapi button, + .unapi input, + .unapi select, + .unapi textarea, + .unapi a, + .unraid-reset button, + .unraid-reset input, + .unraid-reset select, + .unraid-reset textarea, + .unraid-reset a { + /* Use 'revert-layer' to undo webgui styles but keep Tailwind */ + all: revert-layer; + /* Base resets that Tailwind utilities can override */ + font-family: inherit; + font-size: 100%; + font-weight: inherit; + line-height: inherit; + color: inherit; + background-color: transparent; + background-image: none; + border: 0; + padding: 0; + margin: 0; + cursor: auto; + text-align: inherit; + text-transform: none; + appearance: none; + outline: 2px solid transparent; + outline-offset: 2px; + } + + /* Specific button reset for webgui button styles */ + .unapi button:not([class*="btn"]), + .unraid-reset button:not([class*="btn"]) { + /* Double specificity to beat webgui */ + background: transparent; + box-shadow: none; + border-radius: 0; + min-width: auto; + } + } + /* Inherit typography through all elements */ .unraid-reset * { font-family: inherit; diff --git a/web/nuxt.config.ts b/web/nuxt.config.ts index 52317edcc..bf5a4e657 100644 --- a/web/nuxt.config.ts +++ b/web/nuxt.config.ts @@ -2,7 +2,6 @@ import path from 'path'; import tailwindcss from '@tailwindcss/vite'; import removeConsole from 'vite-plugin-remove-console'; -import postcssPrefixPlugin from './postcss-prefix-plugin.js'; import type { PluginOption } from 'vite'; @@ -200,26 +199,6 @@ export default defineNuxtConfig({ minify: 'terser', terserOptions: sharedTerserOptions, }, - css: { - postcss: { - plugins: [ - postcssPrefixPlugin({ - prefix: '.unapi', - exclude: [ - // Don't prefix Tailwind's reset styles - /^:root/, - /^html/, - /^body$/, - /^\*/, - // Don't prefix keyframes - /^@/, - // Already prefixed - /^\.unapi/ - ] - }) - ] - } - } }, // DISABLED: Using standalone mount approach instead diff --git a/web/vite-plugin-prefix-classes.ts b/web/vite-plugin-prefix-classes.ts deleted file mode 100644 index 03ee92b7f..000000000 --- a/web/vite-plugin-prefix-classes.ts +++ /dev/null @@ -1,95 +0,0 @@ -import type { Plugin } from 'vite'; - -/** - * Vite plugin to prefix all CSS classes with a unique identifier - * This allows us to exclude our components from webgui styles - */ -export function prefixClasses(prefix = 'unapi'): Plugin { - return { - name: 'vite-plugin-prefix-classes', - - // Transform CSS files - transform(code, id) { - // Only process CSS files and Vue SFC styles - if (!id.match(/\.(css|scss|sass|less|styl|stylus|postcss)$/) && !id.includes('?vue&type=style')) { - return null; - } - - // Skip node_modules - if (id.includes('node_modules')) { - return null; - } - - // Prefix class selectors in CSS - const prefixedCss = code.replace( - /\.([a-zA-Z_][\w-]*)/g, - (match, className) => { - // Skip if already prefixed - if (className.startsWith(prefix)) { - return match; - } - // Skip Tailwind internals and special classes - if (className.startsWith('__') || className.startsWith('group-') || className.startsWith('peer-')) { - return match; - } - return `.${prefix}-${className}`; - } - ); - - return { - code: prefixedCss, - map: null - }; - }, - - // Transform HTML/templates in Vue files - async transform(code, id) { - // Only process Vue files - if (!id.endsWith('.vue')) { - return null; - } - - // Prefix classes in template - const prefixedCode = code.replace( - /class="([^"]*)"/g, - (match, classes) => { - const prefixedClasses = classes - .split(/\s+/) - .filter(Boolean) - .map(cls => { - // Skip if already prefixed or is a binding - if (cls.startsWith(prefix) || cls.includes(':') || cls.includes('[') || cls.includes('{')) { - return cls; - } - return `${prefix}-${cls}`; - }) - .join(' '); - return `class="${prefixedClasses}"`; - } - ); - - // Also handle :class bindings with static strings - const finalCode = prefixedCode.replace( - /:class="'([^']+)'"/g, - (match, classes) => { - const prefixedClasses = classes - .split(/\s+/) - .filter(Boolean) - .map(cls => { - if (cls.startsWith(prefix)) { - return cls; - } - return `${prefix}-${cls}`; - }) - .join(' '); - return `:class="'${prefixedClasses}'"`; - } - ); - - return { - code: finalCode, - map: null - }; - } - }; -} From 67a6a2e7c8b3e7bb0d22989cf3cbbf014c4891a7 Mon Sep 17 00:00:00 2001 From: Eli Bosley Date: Sun, 31 Aug 2025 15:23:17 -0400 Subject: [PATCH 21/25] refactor: enhance CSS variable management and layer structure for improved theme integration - Introduced overrides for Tailwind v4 global styles to utilize webgui variables, ensuring better compatibility and theming. - Scoped border colors and other styles to specific components, preventing unintended style leakage. - Updated layer definitions in main.css to prioritize webgui styles effectively, enhancing overall style management. - Added new Tailwind v4 color variables for utility classes in the theme store, improving customization options. --- @tailwind-shared/css-variables.css | 96 ++++++++++----------------- plugin/plugins/dynamix.unraid.net.plg | 44 ------------ web/assets/main.css | 71 ++++++++++---------- web/package.json | 2 +- web/store/theme.ts | 9 +++ web/themes/types.d.ts | 6 ++ 6 files changed, 89 insertions(+), 139 deletions(-) diff --git a/@tailwind-shared/css-variables.css b/@tailwind-shared/css-variables.css index 9ddc00dcf..c4a4713f3 100644 --- a/@tailwind-shared/css-variables.css +++ b/@tailwind-shared/css-variables.css @@ -1,7 +1,27 @@ /* Hybrid theme system: Native CSS + Theme Store fallback */ @layer base { + /* Override Tailwind v4 global styles that leak outside components */ + *, ::after, ::before, ::backdrop { + /* Reset border-color to initial to prevent Tailwind from affecting webgui */ + border-color: initial; + } + + /* Properly scope border colors to our components only */ + .unraid-reset *, + .unraid-reset ::after, + .unraid-reset ::before, + .unapi *, + .unapi ::after, + .unapi ::before { + border-color: hsl(var(--border)); + } + /* Light mode defaults */ :root { + /* Override Tailwind v4 global styles to use webgui variables */ + --ui-bg: var(--background-color) !important; + --ui-text: var(--text-color) !important; + --background: 0 0% 100%; --foreground: 0 0% 3.9%; --muted: 0 0% 96.1%; @@ -30,6 +50,10 @@ /* Dark mode */ .dark { + /* Override Tailwind v4 global styles to use webgui variables */ + --ui-bg: var(--background-color) !important; + --ui-text: var(--text-color) !important; + --background: 0 0% 3.9%; --foreground: 0 0% 98%; --muted: 0 0% 14.9%; @@ -64,67 +88,19 @@ --border: 0 0% 14.9%; } - /* For web components: inherit CSS variables from the host */ - :host { - --background: inherit; - --foreground: inherit; - --muted: inherit; - --muted-foreground: inherit; - --popover: inherit; - --popover-foreground: inherit; - --card: inherit; - --card-foreground: inherit; - --border: inherit; - --input: inherit; - --primary: inherit; - --primary-foreground: inherit; - --secondary: inherit; - --secondary-foreground: inherit; - --accent: inherit; - --accent-foreground: inherit; - --destructive: inherit; - --destructive-foreground: inherit; - --ring: inherit; - --chart-1: inherit; - --chart-2: inherit; - --chart-3: inherit; - --chart-4: inherit; - --chart-5: inherit; + /* For embedded components: just override the Tailwind v4 globals */ + .unraid-reset, + .unapi { + /* Override Tailwind v4 global styles to use webgui variables */ + --ui-bg: var(--background-color) !important; + --ui-text: var(--text-color) !important; } - /* Class-based dark mode support for web components using :host-context */ - :host-context(.dark) { - --background: 0 0% 3.9%; - --foreground: 0 0% 98%; - --muted: 0 0% 14.9%; - --muted-foreground: 0 0% 63.9%; - --popover: 0 0% 3.9%; - --popover-foreground: 0 0% 98%; - --card: 0 0% 3.9%; - --card-foreground: 0 0% 98%; - --border: 0 0% 14.9%; - --input: 0 0% 14.9%; - --primary: 0 0% 98%; - --primary-foreground: 0 0% 9%; - --secondary: 0 0% 14.9%; - --secondary-foreground: 0 0% 98%; - --accent: 0 0% 14.9%; - --accent-foreground: 0 0% 98%; - --destructive: 0 62.8% 30.6%; - --destructive-foreground: 0 0% 98%; - --ring: 0 0% 83.1%; - --chart-1: 220 70% 50%; - --chart-2: 160 60% 45%; - --chart-3: 30 80% 55%; - --chart-4: 280 65% 60%; - --chart-5: 340 75% 55%; - } - - /* Alternative class-based dark mode support for specific Unraid themes */ - :host-context(.dark[data-theme='black']), - :host-context(.dark[data-theme='gray']) { - --background: 0 0% 3.9%; - --foreground: 0 0% 98%; - --border: 0 0% 14.9%; + /* Class-based dark mode support for embedded components */ + .dark .unraid-reset, + .dark .unapi { + /* Override Tailwind v4 global styles to use webgui variables */ + --ui-bg: var(--background-color) !important; + --ui-text: var(--text-color) !important; } } \ No newline at end of file diff --git a/plugin/plugins/dynamix.unraid.net.plg b/plugin/plugins/dynamix.unraid.net.plg index f24783b66..e86b0e114 100755 --- a/plugin/plugins/dynamix.unraid.net.plg +++ b/plugin/plugins/dynamix.unraid.net.plg @@ -195,50 +195,6 @@ exit 0 if (!is_dir($backupDir)) { mkdir($backupDir, 0755, true); } - - // Function to patch CSS - we'll just add a layer wrapper - function patchCssContent($content) { - // Simply wrap everything in a @layer to control cascade order - // Our Tailwind styles in later layers will override - return "@layer webgui {\n" . $content . "\n}"; - } - - // Process only default-* and dynamix-* CSS files - foreach (glob("$cssDir/*.css") as $cssFile) { - $filename = basename($cssFile); - - // Only process default-* and dynamix-* files - if (!preg_match('/^(default-|dynamix-).*\.css$/', $filename)) { - continue; - } - - $backupFile = "$backupDir/$filename"; - - // Read file content - $content = file_get_contents($cssFile); - - // Skip if already patched - if (strpos($content, "/* Unraid API compatibility patch - exclusion based */") !== false) { - echo " $filename already patched, skipping...\n"; - continue; - } - - // Create backup if it doesn't exist - if (!file_exists($backupFile)) { - copy($cssFile, $backupFile); - } - - echo " Patching $filename...\n"; - - // Add compatibility patch comment and wrap in layer - $patchedContent = "/* Unraid API compatibility patch - layer based */\n"; - $patchedContent .= patchCssContent($content); - - // Write modified content back - file_put_contents($cssFile, $patchedContent); - } - - echo "CSS patching complete.\n"; ?> ]]> diff --git a/web/assets/main.css b/web/assets/main.css index 3f68109ed..a0a6b3559 100644 --- a/web/assets/main.css +++ b/web/assets/main.css @@ -3,8 +3,8 @@ * This prevents Tailwind from applying global resets that affect webgui */ -/* Define layers for Tailwind v4 - webgui first so our layers override */ -@layer webgui, theme, base, components, utilities; +/* Define layers for Tailwind v4 - base first, webgui later for higher priority */ +@layer base, theme, components, utilities, webgui; /* Import only the parts of Tailwind we need - NO PREFLIGHT */ @import 'tailwindcss/theme.css' layer(theme); @@ -27,7 +27,7 @@ /* Container with proper isolation */ .unraid-reset { isolation: isolate; - display: contents; + /* Removed display: contents to maintain stacking context for modals */ /* Set base typography that webgui can't override */ font-family: Inter, system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif; @@ -134,29 +134,29 @@ .unraid-reset input[type="submit"], .unraid-reset input[type="reset"], .unraid-reset a.button { - /* Reset ALL webgui button properties including CSS variables */ - font-family: inherit !important; - font-size: inherit !important; - font-weight: normal !important; - letter-spacing: normal !important; - text-transform: none !important; - min-width: auto !important; - margin: 0 !important; - padding: 0 !important; - text-align: center !important; - text-decoration: none !important; - white-space: nowrap !important; - cursor: pointer !important; - outline: none !important; - border-radius: 0 !important; - border: none !important; - color: inherit !important; - background: transparent !important; - background-size: auto !important; - appearance: none !important; - box-sizing: border-box !important; + /* Reset webgui button properties - use !important only where absolutely necessary */ + font-family: inherit; + font-size: inherit; + font-weight: normal; + letter-spacing: normal; + text-transform: none; + min-width: auto; + margin: 0; + padding: 0; + text-align: center; + text-decoration: none; + white-space: nowrap; + cursor: pointer; + outline: none; + border-radius: 0; + border: none; + color: inherit; + background: transparent; + background-size: auto; + appearance: none; + box-sizing: border-box; - /* Override CSS variables from webgui */ + /* Override CSS variables from webgui - these need !important */ --button-border: none !important; --button-text-color: inherit !important; --button-background: transparent !important; @@ -196,17 +196,20 @@ } -/* Modal z-index management - scoped to Unraid containers */ -.unraid [role="dialog"], -.unraid [data-headlessui-portal], -.unraid #teleports > *, -.unraid #modals > *, -.unraid unraid-modals > * { +/* Modal z-index management - ensure modals appear above webgui */ +[role="dialog"], +[data-headlessui-portal], +#teleports, +#modals, +unraid-modals, +.z-50 { z-index: 99999 !important; + position: relative; } -/* Modal backdrops - scoped to Unraid containers */ -.unraid .fixed.inset-0[aria-hidden="true"], -.unraid [data-headlessui-portal] .fixed.inset-0 { +/* Modal backdrops */ +.fixed.inset-0[aria-hidden="true"], +[data-headlessui-portal] .fixed.inset-0, +.fixed.inset-0.z-50 { z-index: 99998 !important; } \ No newline at end of file diff --git a/web/package.json b/web/package.json index d80db4c36..485fceb21 100644 --- a/web/package.json +++ b/web/package.json @@ -11,7 +11,7 @@ "serve": "NODE_ENV=production PORT=${PORT:-4321} node .output/server/index.mjs", "// Build": "", "prebuild:dev": "pnpm predev", - "build:dev": "nuxi build --dotenv .env.production && pnpm run manifest-ts && pnpm run deploy-to-unraid:dev", + "build:dev": "pnpm run build && 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 build:standalone && pnpm run manifest-ts && pnpm run validate:css", "build:standalone": "vite build --config vite.standalone.config.ts && pnpm run manifest-standalone", diff --git a/web/store/theme.ts b/web/store/theme.ts index 957b8eae3..6241f25d8 100644 --- a/web/store/theme.ts +++ b/web/store/theme.ts @@ -117,15 +117,24 @@ export const useThemeStore = defineStore('theme', () => { // overwrite with hex colors set in webGUI @ /Settings/DisplaySettings if (theme.value.textColor) { customTheme['--header-text-primary'] = theme.value.textColor; + // Also set the Tailwind v4 color variable for utility classes + customTheme['--color-header-text-primary'] = theme.value.textColor; } if (theme.value.metaColor) { customTheme['--header-text-secondary'] = theme.value.metaColor; + // Also set the Tailwind v4 color variable for utility classes + customTheme['--color-header-text-secondary'] = theme.value.metaColor; } if (theme.value.bgColor) { customTheme['--header-background-color'] = theme.value.bgColor; + // Also set the Tailwind v4 color variable for utility classes + customTheme['--color-header-background'] = theme.value.bgColor; customTheme['--header-gradient-start'] = hexToRgba(theme.value.bgColor, 0); customTheme['--header-gradient-end'] = hexToRgba(theme.value.bgColor, 0.7); + // Also set the Tailwind v4 color variables for gradient utility classes + customTheme['--color-header-gradient-start'] = hexToRgba(theme.value.bgColor, 0); + customTheme['--color-header-gradient-end'] = hexToRgba(theme.value.bgColor, 0.7); } requestAnimationFrame(() => { diff --git a/web/themes/types.d.ts b/web/themes/types.d.ts index f055786d4..ea39be039 100644 --- a/web/themes/types.d.ts +++ b/web/themes/types.d.ts @@ -35,6 +35,12 @@ type BaseThemeVariables = { '--header-gradient-start': string; '--header-gradient-end': string; '--banner-gradient': string | null; + // Tailwind v4 color variables for utility classes + '--color-header-text-primary'?: string; + '--color-header-text-secondary'?: string; + '--color-header-background'?: string; + '--color-header-gradient-start'?: string; + '--color-header-gradient-end'?: string; }; type LegacyThemeVariables = { From e719780ee85e70543622fb3973ba02f414d561b0 Mon Sep 17 00:00:00 2001 From: Eli Bosley Date: Mon, 1 Sep 2025 20:06:48 -0400 Subject: [PATCH 22/25] refactor: enhance component styles and introduce responsive modal - Updated CSS variables and utility classes for improved theme integration and style consistency across components. - Introduced a new responsive modal component to enhance user experience on various screen sizes. - Refined button and badge styles to ensure better visual hierarchy and interaction feedback. - Adjusted component imports and structure for better modularity and maintainability. - Removed deprecated styles and streamlined CSS for improved performance and clarity. --- @tailwind-shared/base-utilities.css | 34 ++- @tailwind-shared/css-variables.css | 41 +-- @tailwind-shared/index.css | 3 +- @tailwind-shared/reka-resets.css | 21 ++ unraid-ui/eslint.config.ts | 7 + unraid-ui/src/components.ts | 1 + .../src/components/brand/BrandButton.vue | 21 +- .../components/common/badge/badge.variants.ts | 2 +- .../src/components/common/button/Button.vue | 43 ++- .../common/button/button.variants.ts | 6 +- unraid-ui/src/components/common/index.ts | 1 + .../responsive-modal/ResponsiveModal.vue | 67 +++++ .../ResponsiveModalFooter.vue | 31 +++ .../ResponsiveModalHeader.vue | 31 +++ .../responsive-modal/ResponsiveModalTitle.vue | 26 ++ .../common/responsive-modal/index.ts | 8 + .../components/common/sheet/SheetContent.vue | 13 +- .../components/common/sheet/sheet.variants.ts | 4 +- .../components/common/tabs/TabsTrigger.vue | 10 +- .../common/tooltip/TooltipContent.vue | 2 +- .../common/tooltip/TooltipTrigger.vue | 4 +- .../components/form/combobox/ComboboxList.vue | 2 +- .../src/components/form/switch/Switch.vue | 2 + .../ui/accordion/AccordionTrigger.vue | 4 +- .../src/components/ui/dialog/DialogClose.vue | 25 ++ .../components/ui/dialog/DialogContent.vue | 18 +- .../components/ui/dialog/DialogTrigger.vue | 2 +- .../ui/dropdown-menu/DropdownMenuContent.vue | 2 +- .../dropdown-menu/DropdownMenuSubTrigger.vue | 4 +- .../ui/dropdown-menu/DropdownMenuTrigger.vue | 2 + .../components/ui/select/SelectContent.vue | 2 +- .../components/ui/select/SelectTrigger.vue | 4 +- unraid-ui/src/forms/AccordionLayout.vue | 2 +- unraid-ui/src/forms/ObjectArrayField.vue | 6 +- unraid-ui/src/forms/SteppedLayout.vue | 2 +- unraid-ui/src/forms/UnraidSettingsLayout.vue | 2 +- .../Activation/WelcomeModal.test.ts | 17 +- .../components/HeaderOsVersion.test.ts | 30 --- web/__test__/components/Modal.test.ts | 3 - web/__test__/components/Registration.test.ts | 64 ++--- web/__test__/components/UserProfile.test.ts | 56 ++-- web/__test__/mocks/ui-components.ts | 50 ++++ web/assets/main.css | 254 +++++------------- web/components/ApiKey/ApiKeyCreate.vue | 78 ++++-- web/components/ApiKey/ApiKeyManager.vue | 63 +++-- web/components/ApiKeyAuthorize.ce.vue | 2 +- web/components/Auth.ce.vue | 6 +- web/components/Brand/Avatar.vue | 6 +- web/components/ColorSwitcher.ce.vue | 2 +- .../ConnectSettings/ConnectSettings.ce.vue | 12 +- web/components/DownloadApiLogs.ce.vue | 4 +- web/components/HeaderOsVersion.ce.vue | 53 +++- web/components/Modal.vue | 17 +- web/components/Notifications/Indicator.vue | 4 +- web/components/Notifications/Item.vue | 2 +- web/components/Notifications/List.vue | 27 +- web/components/Notifications/Sidebar.vue | 57 +++- web/components/Registration.ce.vue | 247 ++++++++++------- web/components/Registration/Item.vue | 53 ---- web/components/UpdateOs/ChangelogModal.vue | 55 ++-- .../UpdateOs/CheckUpdateResponseModal.vue | 197 +++++++------- .../UpdateOs/RawChangelogRenderer.vue | 4 +- web/components/UpdateOs/Status.vue | 20 +- web/components/UserProfile.ce.vue | 88 +++--- web/components/UserProfile/Beta.vue | 4 +- .../UserProfile/DropdownContent.vue | 9 +- web/components/UserProfile/DropdownError.vue | 2 +- web/components/UserProfile/DropdownItem.vue | 15 +- .../UserProfile/DropdownTrigger.vue | 9 +- web/components/UserProfile/ServerState.vue | 6 +- web/components/UserProfile/ServerStateBuy.vue | 11 +- web/components/UserProfile/ServerStatus.vue | 31 +++ web/components/UserProfile/Trial.vue | 9 +- web/components/Wrapper/vue-mount-app.ts | 13 +- web/components/standalone-mount.ts | 19 +- web/composables/useClipboardWithToast.ts | 17 +- web/layouts/default.vue | 6 +- web/pages/index.vue | 20 +- web/pages/login.vue | 2 +- web/pages/webComponents.vue | 22 +- web/pages/welcome.vue | 2 +- web/store/theme.ts | 5 + web/themes/default.ts | 24 ++ web/themes/types.d.ts | 2 + 84 files changed, 1256 insertions(+), 898 deletions(-) create mode 100644 @tailwind-shared/reka-resets.css create mode 100644 unraid-ui/src/components/common/responsive-modal/ResponsiveModal.vue create mode 100644 unraid-ui/src/components/common/responsive-modal/ResponsiveModalFooter.vue create mode 100644 unraid-ui/src/components/common/responsive-modal/ResponsiveModalHeader.vue create mode 100644 unraid-ui/src/components/common/responsive-modal/ResponsiveModalTitle.vue create mode 100644 unraid-ui/src/components/common/responsive-modal/index.ts delete mode 100644 web/components/Registration/Item.vue create mode 100644 web/components/UserProfile/ServerStatus.vue diff --git a/@tailwind-shared/base-utilities.css b/@tailwind-shared/base-utilities.css index 6818e30d6..89437858c 100644 --- a/@tailwind-shared/base-utilities.css +++ b/@tailwind-shared/base-utilities.css @@ -1,7 +1,7 @@ @custom-variant dark (&:where(.dark, .dark *)); -@layer utilities { - :host { +/* Utility defaults for web components (when we were using shadow DOM) */ +:host { --tw-divide-y-reverse: 0; --tw-border-style: solid; --tw-font-weight: initial; @@ -48,21 +48,20 @@ --tw-drop-shadow: initial; --tw-duration: initial; --tw-ease: initial; - } } -@layer base { - *, - ::after, - ::before, - ::backdrop, - ::file-selector-button { - border-color: hsl(var(--border)); - } +/* Global border color - this is what's causing the issue! */ +/* Commenting out since it affects all elements globally +*, +::after, +::before, +::backdrop, +::file-selector-button { + border-color: hsl(var(--border)); +} +*/ - - - body { +body { --color-alpha: #1c1b1b; --color-beta: #f2f2f2; --color-gamma: #999999; @@ -74,8 +73,7 @@ --ring-shadow: 0 0 var(--color-beta); } - button:not(:disabled), - [role='button']:not(:disabled) { - cursor: pointer; - } +button:not(:disabled), +[role='button']:not(:disabled) { + cursor: pointer; } \ No newline at end of file diff --git a/@tailwind-shared/css-variables.css b/@tailwind-shared/css-variables.css index c4a4713f3..9861d76ff 100644 --- a/@tailwind-shared/css-variables.css +++ b/@tailwind-shared/css-variables.css @@ -1,23 +1,7 @@ /* Hybrid theme system: Native CSS + Theme Store fallback */ -@layer base { - /* Override Tailwind v4 global styles that leak outside components */ - *, ::after, ::before, ::backdrop { - /* Reset border-color to initial to prevent Tailwind from affecting webgui */ - border-color: initial; - } - - /* Properly scope border colors to our components only */ - .unraid-reset *, - .unraid-reset ::after, - .unraid-reset ::before, - .unapi *, - .unapi ::after, - .unapi ::before { - border-color: hsl(var(--border)); - } - - /* Light mode defaults */ - :root { + +/* Light mode defaults */ +:root { /* Override Tailwind v4 global styles to use webgui variables */ --ui-bg: var(--background-color) !important; --ui-text: var(--text-color) !important; @@ -86,21 +70,4 @@ --background: 0 0% 3.9%; --foreground: 0 0% 98%; --border: 0 0% 14.9%; - } - - /* For embedded components: just override the Tailwind v4 globals */ - .unraid-reset, - .unapi { - /* Override Tailwind v4 global styles to use webgui variables */ - --ui-bg: var(--background-color) !important; - --ui-text: var(--text-color) !important; - } - - /* Class-based dark mode support for embedded components */ - .dark .unraid-reset, - .dark .unapi { - /* Override Tailwind v4 global styles to use webgui variables */ - --ui-bg: var(--background-color) !important; - --ui-text: var(--text-color) !important; - } -} \ No newline at end of file + } \ No newline at end of file diff --git a/@tailwind-shared/index.css b/@tailwind-shared/index.css index b26482e77..f4af8b492 100644 --- a/@tailwind-shared/index.css +++ b/@tailwind-shared/index.css @@ -2,4 +2,5 @@ @import './css-variables.css'; @import './unraid-theme.css'; @import './base-utilities.css'; -@import './sonner.css'; \ No newline at end of file +@import './sonner.css'; +@import './reka-resets.css'; \ No newline at end of file diff --git a/@tailwind-shared/reka-resets.css b/@tailwind-shared/reka-resets.css new file mode 100644 index 000000000..52d32b38c --- /dev/null +++ b/@tailwind-shared/reka-resets.css @@ -0,0 +1,21 @@ +/* + * Minimal resets for reka-ui components + * Only override the problematic webgui button styles + */ + +/* Target all reka-ui buttons by their common attributes */ +button[id^="reka-accordion-trigger"], +button[role="combobox"], +button[aria-haspopup="menu"], +[role="dialog"] button[type="button"] { + /* Only override the truly problematic styles */ + font-family: inherit !important; /* Don't force clear-sans */ + font-size: inherit !important; /* Don't force 1.1rem */ + font-weight: normal !important; /* Don't force bold */ + letter-spacing: normal !important; /* Don't force 1.8px spacing */ + text-transform: none !important; /* Don't force uppercase */ + min-width: auto !important; /* Don't force 86px minimum */ + margin: 0 !important; /* Don't add 10px margins */ + border: none !important; /* Remove forced border */ + /* Let components handle their own padding through Tailwind classes */ +} \ No newline at end of file diff --git a/unraid-ui/eslint.config.ts b/unraid-ui/eslint.config.ts index 83f4c47dd..3f2797bf7 100644 --- a/unraid-ui/eslint.config.ts +++ b/unraid-ui/eslint.config.ts @@ -87,6 +87,13 @@ const commonGlobals = { HTMLElement: 'readonly', HTMLInputElement: 'readonly', CustomEvent: 'readonly', + MouseEvent: 'readonly', + KeyboardEvent: 'readonly', + FocusEvent: 'readonly', + PointerEvent: 'readonly', + TouchEvent: 'readonly', + WheelEvent: 'readonly', + DragEvent: 'readonly', }; export default [// Base config from recommended configs diff --git a/unraid-ui/src/components.ts b/unraid-ui/src/components.ts index 182d8c1f0..5501213dc 100644 --- a/unraid-ui/src/components.ts +++ b/unraid-ui/src/components.ts @@ -17,6 +17,7 @@ export * from '@/components/common/tabs'; export * from '@/components/common/tooltip'; export * from '@/components/common/toast'; export * from '@/components/common/popover'; +export * from '@/components/common/responsive-modal'; export * from '@/components/modals'; export * from '@/components/common/accordion'; export * from '@/components/common/dialog'; diff --git a/unraid-ui/src/components/brand/BrandButton.vue b/unraid-ui/src/components/brand/BrandButton.vue index abc881889..53998657d 100644 --- a/unraid-ui/src/components/brand/BrandButton.vue +++ b/unraid-ui/src/components/brand/BrandButton.vue @@ -7,7 +7,6 @@ export interface BrandButtonProps { variant?: BrandButtonVariants['variant']; size?: BrandButtonVariants['size']; padding?: BrandButtonVariants['padding']; - btnType?: 'button' | 'submit' | 'reset'; class?: string; click?: () => void; disabled?: boolean; @@ -26,7 +25,6 @@ const props = withDefaults(defineProps(), { variant: 'fill', size: '16px', padding: 'default', - btnType: 'button', class: undefined, click: undefined, disabled: false, @@ -58,15 +56,26 @@ const needsBrandGradientBackground = computed(() => {