web/satellite: statically serve Vuetify theme styles
Vuetify's way of applying themes uses an inline stylesheet. This is incompatible with our CSP policy, so this change implements a Vite plugin that writes theme styles to CSS files that are statically served. Change-Id: I73e3a032435e46d41248c5181e913a8e04f65881
This commit is contained in:
parent
74757ffc1d
commit
51fefb2882
@ -61,6 +61,7 @@ module.exports = {
|
||||
],
|
||||
'newlines-between': 'always',
|
||||
}],
|
||||
'import/no-unresolved': ['error', { ignore: ['^virtual:'] }],
|
||||
'no-duplicate-imports': 'error',
|
||||
'import/default': 'off',
|
||||
'vue/script-setup-uses-vars': 'error',
|
@ -2,6 +2,7 @@
|
||||
"name": "storj-satellite",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"preview": "vite preview",
|
||||
"dev": "vite",
|
||||
|
@ -40,7 +40,8 @@
|
||||
"src/**/*.ts",
|
||||
"src/**/*.vue",
|
||||
"src/types/*.d.ts",
|
||||
"tests/**/*.ts"
|
||||
"tests/**/*.ts",
|
||||
"vitePlugins/**/*.d.ts"
|
||||
],
|
||||
"exclude": [
|
||||
"node_modules"
|
||||
|
@ -7,6 +7,8 @@ import vue from '@vitejs/plugin-vue';
|
||||
import vuetify, { transformAssetUrls } from 'vite-plugin-vuetify';
|
||||
import { defineConfig } from 'vite';
|
||||
|
||||
import vuetifyThemeCSS from './vitePlugins/vuetifyThemeCSS';
|
||||
|
||||
// https://vitejs.dev/config/
|
||||
export default defineConfig({
|
||||
base: '/static/dist_vuetify_poc',
|
||||
@ -21,6 +23,7 @@ export default defineConfig({
|
||||
configFile: 'vuetify-poc/src/styles/settings.scss',
|
||||
},
|
||||
}),
|
||||
vuetifyThemeCSS(),
|
||||
],
|
||||
define: {
|
||||
'process.env': {},
|
||||
|
@ -19,7 +19,7 @@ const plugins = [
|
||||
plugins: [{ name: 'removeViewBox', fn: () => {} }],
|
||||
},
|
||||
}),
|
||||
vitePluginRequire(),
|
||||
vitePluginRequire.default(),
|
||||
];
|
||||
|
||||
if (process.env['STORJ_DEBUG_BUNDLE_SIZE']) {
|
||||
|
57
web/satellite/vitePlugins/vuetifyThemeCSS/index.ts
Normal file
57
web/satellite/vitePlugins/vuetifyThemeCSS/index.ts
Normal file
@ -0,0 +1,57 @@
|
||||
// Copyright (C) 2023 Storj Labs, Inc.
|
||||
// See LICENSE for copying information.
|
||||
|
||||
import { Plugin } from 'vite';
|
||||
import { build } from 'esbuild';
|
||||
import { createVuetify } from 'vuetify';
|
||||
|
||||
import { THEME_OPTIONS } from '../../vuetify-poc/src/plugins/theme';
|
||||
|
||||
export default function vuetifyThemeCSS(): Plugin {
|
||||
const name = 'vuetify-theme-css';
|
||||
const virtualModuleId = 'virtual:' + name;
|
||||
const resolvedVirtualModuleId = '\0' + virtualModuleId;
|
||||
|
||||
const theme = createVuetify({ theme: THEME_OPTIONS }).theme;
|
||||
const themeURLs: Record<string, string> = {};
|
||||
|
||||
return {
|
||||
name,
|
||||
|
||||
async buildStart() {
|
||||
for (const name of Object.keys(theme.themes.value)) {
|
||||
theme.global.name.value = name;
|
||||
|
||||
const result = await build({
|
||||
stdin: {
|
||||
contents: theme.styles.value,
|
||||
loader: 'css',
|
||||
},
|
||||
write: false,
|
||||
minify: true,
|
||||
});
|
||||
|
||||
const refId = this.emitFile({
|
||||
type: 'asset',
|
||||
name: `theme-${name}.css`,
|
||||
source: result.outputFiles[0].text,
|
||||
});
|
||||
themeURLs[name] = `__VITE_ASSET__${refId}__`;
|
||||
}
|
||||
},
|
||||
|
||||
resolveId(id: string) {
|
||||
if (id === virtualModuleId) return resolvedVirtualModuleId;
|
||||
},
|
||||
|
||||
load(id: string) {
|
||||
if (id === resolvedVirtualModuleId) {
|
||||
return `export const themeURLs = {${
|
||||
Object.entries(themeURLs)
|
||||
.map(([name, url]) => `'${name}':'${url}'`)
|
||||
.join(',')
|
||||
}};`;
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
6
web/satellite/vitePlugins/vuetifyThemeCSS/module.d.ts
vendored
Normal file
6
web/satellite/vitePlugins/vuetifyThemeCSS/module.d.ts
vendored
Normal file
@ -0,0 +1,6 @@
|
||||
// Copyright (C) 2023 Storj Labs, Inc.
|
||||
// See LICENSE for copying information.
|
||||
|
||||
declare module 'virtual:vuetify-theme-css' {
|
||||
export const themeURLs: Record<string, string>;
|
||||
}
|
@ -8,8 +8,9 @@
|
||||
*/
|
||||
|
||||
// Plugins
|
||||
import { App } from 'vue';
|
||||
import { App, watch } from 'vue';
|
||||
import { createPinia, setActivePinia } from 'pinia';
|
||||
import { themeURLs } from 'virtual:vuetify-theme-css';
|
||||
|
||||
import { router, startTitleWatcher } from '../router';
|
||||
|
||||
@ -20,7 +21,46 @@ import NotificatorPlugin from '@/utils/plugins/notificator';
|
||||
const pinia = createPinia();
|
||||
setActivePinia(pinia);
|
||||
|
||||
// Vuetify's way of applying themes uses a dynamic inline stylesheet.
|
||||
// This is incompatible with our CSP policy, so circumvent it.
|
||||
function setupTheme() {
|
||||
const oldAppend = document.head.appendChild.bind(document.head);
|
||||
document.head.appendChild = function<T extends Node>(node: T): T {
|
||||
if (node instanceof HTMLStyleElement && node.id === 'vuetify-theme-stylesheet') {
|
||||
node.remove();
|
||||
return node;
|
||||
}
|
||||
return oldAppend(node);
|
||||
};
|
||||
|
||||
const themeLinks: Record<string, HTMLLinkElement> = {};
|
||||
|
||||
for (const [name, url] of Object.entries(themeURLs)) {
|
||||
let link = document.createElement('link');
|
||||
link.rel = 'stylesheet';
|
||||
link.href = url;
|
||||
link.disabled = name !== vuetify.theme.global.name.value;
|
||||
document.head.appendChild(link);
|
||||
themeLinks[name] = link;
|
||||
|
||||
// If we don't preload the style, there will be a delay after
|
||||
// toggling to it for the first time.
|
||||
link = document.createElement('link');
|
||||
link.rel = 'preload';
|
||||
link.as = 'style';
|
||||
link.href = url;
|
||||
document.head.appendChild(link);
|
||||
}
|
||||
|
||||
watch(() => vuetify.theme.global.name.value, newName => {
|
||||
for (const [name, link] of Object.entries(themeLinks)) {
|
||||
link.disabled = name !== newName;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
export function registerPlugins(app: App<Element>) {
|
||||
setupTheme();
|
||||
app
|
||||
.use(vuetify)
|
||||
.use(router)
|
||||
|
57
web/satellite/vuetify-poc/src/plugins/theme.ts
Normal file
57
web/satellite/vuetify-poc/src/plugins/theme.ts
Normal file
@ -0,0 +1,57 @@
|
||||
// Copyright (C) 2023 Storj Labs, Inc.
|
||||
// See LICENSE for copying information.
|
||||
|
||||
import { createVuetify } from 'vuetify';
|
||||
|
||||
type ThemeOptions = NonNullable<NonNullable<Parameters<typeof createVuetify>[0]>['theme']>;
|
||||
|
||||
export const THEME_OPTIONS: ThemeOptions = {
|
||||
themes: {
|
||||
light: {
|
||||
colors: {
|
||||
primary: '#0149FF',
|
||||
secondary: '#0218A7',
|
||||
background: '#FFF',
|
||||
surface: '#FFF',
|
||||
info: '#0059D0',
|
||||
help: '#FFA800',
|
||||
success: '#00AC26',
|
||||
warning: '#FF7F00',
|
||||
error: '#FF0149',
|
||||
purple: '#7B61FF',
|
||||
blue6: '#091c45',
|
||||
blue5: '#0218A7',
|
||||
blue4: '#0059D0',
|
||||
blue2: '#003ACD',
|
||||
yellow: '#FFC600',
|
||||
yellow2: '#FFB701',
|
||||
orange: '#FFA800',
|
||||
green: '#00B150',
|
||||
purple2: '#502EFF',
|
||||
},
|
||||
},
|
||||
dark: {
|
||||
colors: {
|
||||
primary: '#0149FF',
|
||||
secondary: '#537CFF',
|
||||
background: '#090920',
|
||||
success: '#00AC26',
|
||||
help: '#FFC600',
|
||||
error: '#FF0149',
|
||||
surface: '#090920',
|
||||
purple: '#A18EFF',
|
||||
blue6: '#091c45',
|
||||
blue5: '#2196f3',
|
||||
blue4: '#0059D0',
|
||||
blue2: '#003ACD',
|
||||
yellow: '#FFC600',
|
||||
yellow2: '#FFB701',
|
||||
orange: '#FFA800',
|
||||
warning: '#FF8A00',
|
||||
// green: '#00B150',
|
||||
green: '#00e366',
|
||||
purple2: '#A18EFF',
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
@ -13,62 +13,14 @@ import '@fontsource-variable/inter';
|
||||
import { createVuetify } from 'vuetify';
|
||||
import { md3 } from 'vuetify/blueprints';
|
||||
|
||||
import '../styles/styles.scss';
|
||||
import '@poc/styles/styles.scss';
|
||||
import { THEME_OPTIONS } from '@poc/plugins/theme';
|
||||
|
||||
// https://vuetifyjs.com/en/introduction/why-vuetify/#feature-guides
|
||||
export default createVuetify({
|
||||
// Use blueprint for Material Design 3
|
||||
blueprint: md3,
|
||||
theme: {
|
||||
themes: {
|
||||
light: {
|
||||
colors: {
|
||||
primary: '#0149FF',
|
||||
secondary: '#0218A7',
|
||||
background: '#FFF',
|
||||
surface: '#FFF',
|
||||
info: '#0059D0',
|
||||
help: '#FFA800',
|
||||
success: '#00AC26',
|
||||
warning: '#FF7F00',
|
||||
error: '#FF0149',
|
||||
purple: '#7B61FF',
|
||||
blue6: '#091c45',
|
||||
blue5: '#0218A7',
|
||||
blue4: '#0059D0',
|
||||
blue2: '#003ACD',
|
||||
yellow: '#FFC600',
|
||||
yellow2: '#FFB701',
|
||||
orange: '#FFA800',
|
||||
green: '#00B150',
|
||||
purple2: '#502EFF',
|
||||
},
|
||||
},
|
||||
dark: {
|
||||
colors: {
|
||||
primary: '#0149FF',
|
||||
secondary: '#537CFF',
|
||||
background: '#090920',
|
||||
success: '#00AC26',
|
||||
help: '#FFC600',
|
||||
error: '#FF0149',
|
||||
surface: '#090920',
|
||||
purple: '#A18EFF',
|
||||
blue6: '#091c45',
|
||||
blue5: '#2196f3',
|
||||
blue4: '#0059D0',
|
||||
blue2: '#003ACD',
|
||||
yellow: '#FFC600',
|
||||
yellow2: '#FFB701',
|
||||
orange: '#FFA800',
|
||||
warning: '#FF8A00',
|
||||
// green: '#00B150',
|
||||
green: '#00e366',
|
||||
purple2: '#A18EFF',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
theme: THEME_OPTIONS,
|
||||
defaults: {
|
||||
global: {
|
||||
// ripple: false,
|
||||
|
Loading…
Reference in New Issue
Block a user