@anoyomoose/q2-fresh-paint-core
v0.1.3
Published
Quasar theming engine — Vite plugin for Sass-based theme overrides
Downloads
418
Readme
@anoyomoose/q2-fresh-paint-core
Quasar theming engine — a Vite plugin that applies Sass-based theme overrides to Quasar components. Supports multiple themes stacked in sequence.
This is a pure build-time package. It has no runtime browser code and no dependency on quasar or vue.
Installation & Quick Start
Install the core engine alongside a theme package:
# or equivalent for your package manager
pnpm add @anoyomoose/q2-fresh-paint-core @anoyomoose/q2-fresh-paint-md3eAdd freshPaint() to your quasar.config.js. In a Quasar project, Vite plugins are registered inside build.vitePlugins, and boot files (runtime JS that runs at app startup) go in the boot array. The ~ prefix tells Quasar to resolve from node_modules:
import { freshPaint } from '@anoyomoose/q2-fresh-paint-core'
import { md3eTheme } from '@anoyomoose/q2-fresh-paint-md3e'
export default defineConfig({
boot: [
// Theme boot file — patches component prop defaults at runtime
'~@anoyomoose/q2-fresh-paint-md3e/boot'
],
build: {
vitePlugins: [
// Theme engine — intercepts Sass compilation to inject theme overrides
freshPaint({
themes: [ md3eTheme({ sourceColor: '#6750a4' }) ],
})
]
}
})freshPaint() must appear in build.vitePlugins after Quasar's own Vite plugin (which Quasar registers automatically, so just adding it to the array is sufficient). Boot files are separate because they're a Quasar runtime concept — the Vite plugin only handles build-time Sass injection.
API Reference
freshPaint(options: FreshPaintOptions): Plugin
Returns a Vite plugin that intercepts Quasar's Sass compilation to inject theme overrides.
FreshPaintOptions
| Property | Type | Default | Description |
|---|---|---|---|
| themes | ThemeDescriptor[] | (required) | Ordered array of themes to apply. |
| userThemeDir | string | 'src/theme' | Base directory for user overrides, relative to app root. |
ThemeDescriptor
Describes a single theme. Theme packages export a factory function that returns this object.
interface ThemeDescriptor {
/** Unique name — used for generated vars filename and user override dir resolution */
name: string
/** Absolute path to theme's SCSS directory (variables.scss, base.scss, components/) */
dir: string
/** Optional: returns Sass variable content, written to .quasar/theme.<name>.scss */
generateVariables?: () => string
}| Field | Description |
|---|---|
| name | Unique identifier. Determines the user override subdirectory (src/theme/<name>/) and the generated variables filename (.quasar/theme.<name>.scss). |
| dir | Absolute path to the directory containing the theme's SCSS files. Must contain some combination of variables.scss, base.scss, and a components/ subdirectory. |
| generateVariables | Optional function called at build time. Its return value (a string of Sass content) is written to .quasar/theme.<name>.scss and included in the variable import chain. Useful for computed values like palette generation from a source color. |
Building Themes
This section explains how to author a theme package for Fresh Paint.
Directory Structure
A theme package provides a directory with these files:
theme/
variables.scss — Sass variable overrides (using !default)
base.scss — global styles, CSS custom properties, utility classes
components/
QBtn.scss — override for QBtn
QCard.scss — override for QCard
...All files are optional. Include only what the theme needs to override.
The theme package exports a factory function that returns a ThemeDescriptor pointing to this directory:
import { dirname, resolve } from 'node:path'
import { fileURLToPath } from 'node:url'
import type { ThemeDescriptor } from '@anoyomoose/q2-fresh-paint-core'
export function myTheme(): ThemeDescriptor {
const pkgDir = dirname(fileURLToPath(import.meta.url))
return {
name: 'my-theme',
dir: resolve(pkgDir, 'theme'),
}
}Component Override Resolution
Using the QBtn component as example:
Override files are placed in theme/components/QBtn.scss. The filename must match the Quasar component name exactly, with either .scss or .sass extension.
In development mode the engine registers a custom Sass importer that intercepts Quasar's own component files. When Sass resolves a path like components/QBtn/QBtn.sass, the importer:
- Reads the original component Sass content
- Appends
@importstatements for each theme's override file - Returns the combined content to Sass
The original file's directory context is preserved, so Quasar's own @import statements within the component still resolve correctly. The override file is appended after the original, so its CSS rules take precedence by source order.
In distribution mode it doesn't actually matter. When the distribution sass is seen by the transform hook, it appends all the theme CSS. However, it is still a good structure to follow.
Development mode
One way to develop themes is to have the full Quasar source available, and install your theme package in link mode with (p)npm. That way the importer will do its job slightly differently and you can run Quasar source's built-in UI playground to systematically check your work. You will still have to constantly rebuild your package and restart the dev server for various changes to take effect, but it is still an easy way to work.
Of course, if you're developing inside your own web project, you can create a very basic package and then use "user overrides" (see below) to style any component directly from your tree, before copying the final version into your actual package. You'll likely still have to restart the dev server regularly.
Variable Priority
Quasar's built-in Sass variables all use the !default flag, which means the first definition wins. The engine exploits this by prepending theme variable imports before Quasar's own variable file.
The engine provides three user override points for variables, each at a different position in the import chain. For a single theme, the full import order in each Sass file is:
quasar.variables.scss— the project's own Quasar variable file (prepended by Quasar's Vite plugin)- User
variables.pre.scss— before generated variables - Generated variables from
generateVariables()(if provided) - User
variables.scss— after generated, before package - Theme package
variables.scss - User
variables.post.scss— after package variables - Quasar's
variables.sasswith!default(lowest priority)
Since !default means "only set if not yet defined," earlier definitions win for !default variables. Hard assignments (no !default) always take effect regardless of position.
The three user variable files serve different purposes:
variables.pre.scss— imported before the generated palette. Use this to override individual generated tokens (e.g., force$md3-primaryto a specific value while letting the rest of the palette generate normally). Use!defaultsoquasar.variables.scsscan still win.variables.scss— imported after generated variables but before the package's own variables. Can reference generated tokens (e.g.,$md3-primary). Use!defaultto override the package's default mappings while preserving the fallback chain.variables.post.scss— imported after everything. Can reference all tokens from all sources — generated palette, package variables, shape tokens, motion tokens, everything. Use hard assignments (no!default) here, since all variables are already defined by this point.
All three files are optional. Most users will only need variables.post.scss (to tweak fully-resolved values) or variables.scss (to remap how generated tokens are applied).
Important for theme users: Quasar's quasar.variables.scss is loaded before any theme variables, and typically uses hard assignments (no !default). Any variable defined there — such as $primary: #1976D2 — will override the theme's value for that variable. Theme packages should document which Quasar variables they manage, so users know what to remove from quasar.variables.scss.
For theme authors: always use !default in your package's variables.scss so that user overrides at every level can take priority:
// In a theme's variables.scss
$primary: $my-generated-primary !default;
$button-border-radius: 20px !default;Base File Injection
base.scss is appended to Quasar's main CSS entry point (the file that imports all components). Use it for:
- CSS custom properties (e.g.,
--md3-primary: #{$md3-primary}) - Global styles and resets
- Utility classes
// In a theme's base.scss
:root {
--my-theme-primary: #{$primary};
--my-theme-radius: 12px;
}
.body--dark {
--my-theme-primary: #{$primary-dark};
}generateVariables()
An optional function on ThemeDescriptor that returns a string of Sass variable content. Called at build time; output is written to .quasar/theme.<name>.scss.
This is useful when variable values need to be computed — for example, generating an entire color palette from a single source color:
export function myTheme(options: { color: string }): ThemeDescriptor {
return {
name: 'my-theme',
dir: resolve(__dirname, 'theme'),
generateVariables() {
const palette = generatePalette(options.color)
return `$my-primary: ${palette.primary} !default;\n`
+ `$my-secondary: ${palette.secondary} !default;\n`
},
}
}Generated variables sit between user overrides and package variables in priority order, so users can still override them.
Boot Files
Some theme behavior cannot be expressed in CSS alone — for example, setting default component props (noCaps, unelevated) or attaching DOM observers. These are handled by boot files: runtime JavaScript that runs when the app starts.
Boot files are exported separately from the theme package (e.g., @anoyomoose/q2-fresh-paint-md3e/boot) and registered by the user in the quasar.config.js boot array. The core engine does not manage boot files — they are a convention between the theme package and the user.
// quasar.config.js
boot: [
'~@anoyomoose/q2-fresh-paint-md3e/boot'
]Multi-Theme Stacking
Multiple themes can be applied simultaneously by passing them in the themes array. The order matters.
Given themes: [themeA, themeB]:
Variables (first !default definition wins; hard assignments always take effect):
- themeA
variables.pre.scss - themeA generated variables
- themeA
variables.scss - themeA package variables
- themeA
variables.post.scss - themeB
variables.pre.scss - themeB generated variables
- themeB
variables.scss - themeB package variables
- themeB
variables.post.scss - Quasar defaults (lowest priority)
Earlier themes have higher !default priority. Hard assignments in variables.post.scss override everything above them.
Component overrides (appended to original — later CSS wins by source order):
- Original component content
- themeA override (user file replaces package file within this theme)
- themeB override (user file replaces package file within this theme)
Later themes' CSS rules override earlier themes' rules.
Base files (appended to main entry — later CSS wins by source order):
- themeA base (user file replaces package file within this theme)
- themeB base (user file replaces package file within this theme)
This means variables and CSS rules have opposite priority directions: for variables, earlier themes win; for CSS rules, later themes win.
User Overrides
Users can override any theme file without forking the theme package.
Configuration
The base directory defaults to src/theme (relative to app root). Override it with:
freshPaint({
themes: [md3eTheme()],
userThemeDir: 'src/my-overrides',
})Directory Layout
For each theme, place overrides in a subdirectory matching the theme's name:
src/theme/<themeName>/
variables.pre.scss — before generated palette (optional)
variables.scss — after generated, before package (optional)
variables.post.scss — after package variables (optional)
base.scss — replaces the theme's base stylesheet
components/
QBtn.scss — replaces the theme's QBtn override
QCard.scssAll files are optional. Only include the files you need.
Override Behavior
User overrides are designed primarily to ease theme development and debugging — they let you iterate on a single component's styling or tweak variables without forking and rebuilding the theme package. They're also useful for project-specific adjustments, but their primary purpose is the development workflow.
The file types have different override semantics:
Variable files (
variables.pre.scss,variables.scss,variables.post.scss) — extend. All user variable files, generated variables, and the package's variables are imported together. Each user file is inserted at a specific point in the chain (see Variable Priority). You only need to define the variables you want to change — the package's variables remain as fallbacks.base.scss— replaces. If the user providesbase.scss, the package'sbase.scssis not used at all. This gives you a clean slate for CSS custom properties and global styles. If you want to extend rather than replace, import the package file from within your override:// src/theme/md3e/base.scss @import '@anoyomoose/q2-fresh-paint-md3e/dist/theme/base.scss'; // Additional global styles .my-custom-class { ... }components/<Name>.scss— replaces. If the user providesQBtn.scss, the package'sQBtn.scssis not used for that component. Same clean-slate approach — import the package file explicitly if you want to extend:// src/theme/md3e/components/QBtn.scss @import '@anoyomoose/q2-fresh-paint-md3e/dist/theme/components/QBtn.scss'; // Additional overrides .q-btn { letter-spacing: 0.05em; }
License
MIT
