feat: possibility to adjust default-colors with support for alpha (using pickr)

credit to Style Settings for the idea
This commit is contained in:
mara-li 2025-04-16 13:42:26 +02:00
commit 5dfef06b52
11 changed files with 303 additions and 63 deletions

View file

@ -29,9 +29,10 @@ Automagically add color to roots folders, and customize them with Style Settings
## 🎨 Configuration
In the plugin settings, you can customize :
- Folder name prefixes for CSS variables (background and font color)
- Usage of a CSS file in `snippets` folder (instead of injecting into the DOM).
- Custom CSS template
- Export to a css snippets (instead of inject into the DOM)
- Default colors (when creating new folders)
- Folder name prefix for CSS varaible
- Custom CSS templates
- Custom style settings template
![settings](./docs/assets/plugin_settings.png)

View file

@ -5,6 +5,7 @@
"name": "sample-plugin",
"dependencies": {
"@delucis/if-env": "^1.1.2",
"@simonwep/pickr": "^1.9.1",
"ansi-colors": "^4.1.3",
"commander": "^13.1.0",
"commit-and-tag-version": "^12.5.0",
@ -28,8 +29,11 @@
},
},
"trustedDependencies": [
"electron",
"esbuild",
"core-js",
"@biomejs/biome",
"obsidian-typings",
],
"packages": {
"@asamuzakjp/css-color": ["@asamuzakjp/css-color@3.1.1", "", { "dependencies": { "@csstools/css-calc": "^2.1.2", "@csstools/css-color-parser": "^3.0.8", "@csstools/css-parser-algorithms": "^3.0.4", "@csstools/css-tokenizer": "^3.0.3", "lru-cache": "^10.4.3" } }, "sha512-hpRD68SV2OMcZCsrbdkccTw5FXjNDLo5OuqSHyHZfwweGsDWZwDJ2+gONyNAbazZclobMirACLw0lk8WVxIqxA=="],
@ -208,6 +212,8 @@
"@pixi/utils": ["@pixi/utils@7.2.4", "", { "dependencies": { "@pixi/color": "7.2.4", "@pixi/constants": "7.2.4", "@pixi/settings": "7.2.4", "@types/earcut": "^2.1.0", "earcut": "^2.2.4", "eventemitter3": "^4.0.0", "url": "^0.11.0" } }, "sha512-VUGQHBOINIS4ePzoqafwxaGPVRTa3oM/mEutIIHbNGI3b+QvSO+1Dnk40M0zcH6Bo+MxQZbOZK5X/wO9oU5+LQ=="],
"@simonwep/pickr": ["@simonwep/pickr@1.9.1", "", { "dependencies": { "core-js": "3.37.0", "nanopop": "2.4.2" } }, "sha512-fR3qmfAcPf/HSFS7GEnTmZLM3+xERv1+jyMBbzT63ilRRM8veYjI7ELvkHHKk0/du3lHp7uh/FqatjM3646X1g=="],
"@sindresorhus/is": ["@sindresorhus/is@4.6.0", "", {}, "sha512-t09vSN3MdfsyCHoFcTRCH/iUtG7OJ0CsjzB8cjAmKc/va/kIgeDI/TxsigdncE/4be734m0cvIYwNaV4i2XqAw=="],
"@szmarczak/http-timer": ["@szmarczak/http-timer@4.0.6", "", { "dependencies": { "defer-to-connect": "^2.0.0" } }, "sha512-4BAffykYOgO+5nzBWYwE3W90sBgLJoUPRWWcL8wlyiM8IB8ipJz3UMJ9KXQd1RKQXpKp8Tutn80HZtWsu2u76w=="],
@ -348,6 +354,8 @@
"conventional-recommended-bump": ["conventional-recommended-bump@7.0.1", "", { "dependencies": { "concat-stream": "^2.0.0", "conventional-changelog-preset-loader": "^3.0.0", "conventional-commits-filter": "^3.0.0", "conventional-commits-parser": "^4.0.0", "git-raw-commits": "^3.0.0", "git-semver-tags": "^5.0.0", "meow": "^8.1.2" }, "bin": { "conventional-recommended-bump": "cli.js" } }, "sha512-Ft79FF4SlOFvX4PkwFDRnaNiIVX7YbmqGU0RwccUaiGvgp3S0a8ipR2/Qxk31vclDNM+GSdJOVs2KrsUCjblVA=="],
"core-js": ["core-js@3.37.0", "", {}, "sha512-fu5vHevQ8ZG4og+LXug8ulUtVxjOcEYvifJr7L5Bfq9GOztVqsKd9/59hUk2ZSbCrS3BqUr3EpaYGIYzq7g3Ug=="],
"core-util-is": ["core-util-is@1.0.3", "", {}, "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ=="],
"crelt": ["crelt@1.0.6", "", {}, "sha512-VQ2MBenTq1fWZUH9DJNGti7kKv6EeAuYr3cLwxUWhIu1baTaXh4Ib5W2CqHVqib4/MqbYGJqiL3Zb8GJZr3l4g=="],
@ -590,6 +598,8 @@
"ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
"nanopop": ["nanopop@2.4.2", "", {}, "sha512-NzOgmMQ+elxxHeIha+OG/Pv3Oc3p4RU2aBhwWwAqDpXrdTbtRylbRLQztLy8dMMwfl6pclznBdfUhccEn9ZIzw=="],
"neo-async": ["neo-async@2.6.2", "", {}, "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw=="],
"normalize-package-data": ["normalize-package-data@3.0.3", "", { "dependencies": { "hosted-git-info": "^4.0.1", "is-core-module": "^2.5.0", "semver": "^7.3.4", "validate-npm-package-license": "^3.0.1" } }, "sha512-p2W1sgqij3zMMyRC067Dg16bfzVH+w7hyegmpIvZ4JNjqtGOVAIvLmjBx3yP7YTe9vKJgkoNOPjwQGogDoMXFA=="],

View file

@ -32,12 +32,7 @@ dotenv.config({ path: [".env"] });
const program = new Command();
program
.option("-p, --production", "Production build")
.option("-v, --vault [vault]", "Use vault path")
.action((v) => {
if (!v) return false;
if (typeof v === "string") return path.resolve(v.replace(/\\/g, "/"));
return process.env.VAULT;
})
.option("-v, --vault [vault]", "Use vault path", false)
.option("-o, --output-dir <path>", "Output path")
.option("-b, --beta", "Pre-release version")
.parse();
@ -53,9 +48,20 @@ const prod = opt.production ?? false;
const isStyled = fs.existsSync("src/styles.css");
const pluginID = manifest.id;
function getVaultPath(value) {
if (typeof value === "string") return value;
if (value === true) {
const vaultPath = process.env.VAULT;
if (!vaultPath) {
throw new Error("VAULT environment variable not set");
}
return vaultPath;
}
return false;
}
/** FOLDER PATHS **/
const folderPlugin = opt.vault
? path.join(opt.vault, ".obsidian", "plugins", pluginID)
? path.join(getVaultPath(opt.vault), ".obsidian", "plugins", pluginID)
: undefined;
if (folderPlugin && !fs.existsSync(folderPlugin)) {

View file

@ -36,6 +36,7 @@
},
"dependencies": {
"@delucis/if-env": "^1.1.2",
"@simonwep/pickr": "^1.9.1",
"ansi-colors": "^4.1.3",
"commander": "^13.1.0",
"commit-and-tag-version": "^12.5.0",
@ -46,6 +47,9 @@
},
"trustedDependencies": [
"@biomejs/biome",
"esbuild"
"core-js",
"electron",
"esbuild",
"obsidian-typings"
]
}

95
src/color-picker.ts Normal file
View file

@ -0,0 +1,95 @@
import Pickr from "@simonwep/pickr";
import { debounce, Setting } from "obsidian";
import { DEFAULT_COLOR } from "./interfaces";
import type { ThemedColors } from "./interfaces";
export class PickerSettingsComponent extends Setting {
private pickrLight: Pickr;
private pickrDark: Pickr;
constructor(
containerEl: HTMLElement,
label: string,
color: { themeDark: string; themeLight: string },
onChange: (values: { themeDark: string; themeLight: string }) => void
) {
super(containerEl);
this.setName(label).setClass("no-border").setClass("picker-settings-component");
const control = this.controlEl.createDiv({ cls: "dual-alpha-picker-controls" });
const lightWrapper = control.createDiv({ cls: "alpha-picker-wrapper light" });
const darkWrapper = control.createDiv({ cls: "alpha-picker-wrapper dark" });
const lightEl = lightWrapper.createDiv({ cls: "alpha-picker light" });
const darkEl = darkWrapper.createDiv({ cls: "alpha-picker dark" });
const createPickr = (el: HTMLElement, defaultColor: string): Pickr => {
const pickr = Pickr.create({
el,
theme: "nano",
default: defaultColor,
components: {
preview: true,
opacity: true,
hue: true,
interaction: {
input: true,
rgba: true,
hex: true,
save: true,
},
},
});
pickr.on("save", () => {
pickr.hide();
});
return pickr;
};
this.pickrLight = createPickr(lightEl, color.themeLight);
this.pickrDark = createPickr(darkEl, color.themeDark);
lightWrapper
.createEl("button", {
cls: "clickable-icon",
attr: { "aria-label": "Reset light color" },
text: "↺",
})
.addEventListener("click", () => {
this.pickrLight.setColor(DEFAULT_COLOR);
onChange(this.getColors());
});
const q = lightWrapper.querySelector(".pcr-button");
if (q) q.ariaLabel = "Theme light";
darkWrapper
.createEl("button", {
cls: "clickable-icon",
attr: { "aria-label": "Reset dark color" },
text: "↺",
})
.addEventListener("click", () => {
this.pickrDark.setColor(DEFAULT_COLOR);
onChange(this.getColors());
});
const q2 = darkWrapper.querySelector(".pcr-button");
if (q2) q2.ariaLabel = "Theme dark";
const update = debounce(
() => {
onChange({
themeLight: this.pickrLight.getColor().toHEXA().toString(),
themeDark: this.pickrDark.getColor().toHEXA().toString(),
});
},
200,
true
);
this.pickrLight.on("change", update);
this.pickrDark.on("change", update);
}
public setColors({ themeDark, themeLight }: ThemedColors): void {
this.pickrLight.setColor(themeDark);
this.pickrDark.setColor(themeLight);
}
public getColors(): ThemedColors {
return {
themeLight: this.pickrLight.getColor().toHEXA().toString(),
themeDark: this.pickrDark.getColor().toHEXA().toString(),
};
}
}

View file

@ -1,9 +1,20 @@
import { normalizePath, TFolder, type App, type PluginManifest, type TAbstractFile } from "obsidian";
import type { Prefix, SimpleColoredFolderSettings, StyleSettingValue } from "./interfaces";
import {
normalizePath,
TFolder,
type App,
type PluginManifest,
type TAbstractFile,
} from "obsidian";
import type {
Prefix,
SimpleColoredFolderSettings,
StyleSettingValue,
} from "./interfaces";
import type SimpleColoredFolder from "./main";
import { convertStyleSettings, convertToCSS, generateName, themes } from "./template";
import { minifyCss, removeExtraNewLine } from "./utils";
import dedent from "dedent";
import i18next from "i18next";
export class ColorCompiler {
plugin: SimpleColoredFolder;
@ -11,7 +22,7 @@ export class ColorCompiler {
style: HTMLStyleElement | null = null;
app: App;
snippetPath: string = "generated.colored-folder.css";
manifest: PluginManifest
manifest: PluginManifest;
constructor(plugin: SimpleColoredFolder) {
this.plugin = plugin;
this.settings = plugin.settings;
@ -56,13 +67,14 @@ export class ColorCompiler {
for (const folder of folders) {
const folderName = folder.name;
const vn = generateName(this.settings.prefix, folderName, "--");
darkTheme += themes(vn);
lightTheme += themes(vn);
darkTheme += themes(vn, this.settings.defaultColors, "themeDark");
lightTheme += themes(vn, this.settings.defaultColors, "themeLight");
css += convertToCSS(folderName, this.settings.prefix, this.settings.customTemplate);
stylesSettings += convertStyleSettings(
folderName,
this.settings.prefix,
this.settings.customStyleSettings
this.settings.customStyleSettings,
this.settings.defaultColors
);
}
darkTheme += "}";
@ -79,7 +91,6 @@ export class ColorCompiler {
this.style.setAttribute("type", "text/css");
this.style.textContent = content;
document.head.appendChild(this.style);
}
async injectToSnippets(content: string) {
@ -89,7 +100,7 @@ export class ColorCompiler {
console.log("Creating snippets directory");
await this.app.vault.adapter.mkdir(obsidianDir);
}
const rules = `/* This file is generated by Simple Colored Folder. Do not edit it manually\n${content}`;
const rules = `/* This file is generated by Simple Colored Folder. Do not edit it manually */\n${content}`;
await this.app.vault.adapter.write(this.snippetPath, rules);
//enable the snippet
this.app.customCss.setCssEnabledStatus("generated.colored-folder", true);
@ -149,7 +160,6 @@ export class ColorCompiler {
await this.injectStyles();
//@ts-ignore
styleSettingPlugin.settingsManager.setSettings(styleSettingsValues);
}
}
@ -161,4 +171,4 @@ export class ColorCompiler {
reload() {
this.app.workspace.trigger("css-change");
}
}
}

View file

@ -10,8 +10,29 @@ export interface SimpleColoredFolderSettings {
customTemplate: string;
customStyleSettings: string;
exportToCSS: boolean;
defaultColors: Colors;
}
export type ThemedColors = {
themeDark: string;
themeLight: string;
};
export type Colors = {
bg: ThemedColors;
color: ThemedColors;
};
export const DEFAULT_THEMED_COLORS = {
themeDark: DEFAULT_COLOR,
themeLight: DEFAULT_COLOR,
};
export const DEFAULT_COLORS: Colors = {
bg: DEFAULT_THEMED_COLORS,
color: DEFAULT_THEMED_COLORS,
};
export const DEFAULT_SETTINGS: SimpleColoredFolderSettings = {
prefix: {
bg: "bg",
@ -19,7 +40,8 @@ export const DEFAULT_SETTINGS: SimpleColoredFolderSettings = {
},
customTemplate: "",
customStyleSettings: "",
exportToCSS:false,
exportToCSS: false,
defaultColors: DEFAULT_COLORS,
};
export type StyleSettingValue = number | string | boolean;

View file

@ -3,6 +3,7 @@ import type SimpleColoredFolder from "./main";
import i18next from "i18next";
import type { ColorCompiler } from "./compiler";
import type { SimpleColoredFolderSettings } from "./interfaces";
import { PickerSettingsComponent } from "./color-picker";
export class SimpleColoredFolderSettingTab extends PluginSettingTab {
plugin: SimpleColoredFolder;
@ -21,7 +22,43 @@ export class SimpleColoredFolderSettingTab extends PluginSettingTab {
this.containerEl.addClass(`spf`);
containerEl.empty();
new Setting(containerEl)
.setName(i18next.t("settings.snippets.title"))
.setClass("no-border")
.setDesc(i18next.t("settings.snippets.desc"))
.addToggle((cb) =>
cb.setValue(this.settings.exportToCSS).onChange(async (value) => {
this.settings.exportToCSS = value;
await this.plugin.saveSettings();
await this.compiler.injectStyles();
})
);
this.containerEl.createEl("hr");
new Setting(containerEl).setName("Default color").setClass("no-border").setHeading();
new PickerSettingsComponent(
containerEl,
"Background",
this.settings.defaultColors.bg,
async (value) => {
this.settings.defaultColors.bg.themeLight = value.themeLight;
this.settings.defaultColors.bg.themeDark = value.themeDark;
await this.plugin.saveSettings();
await this.compiler.injectStyles();
}
);
new PickerSettingsComponent(
containerEl,
"Text color",
this.settings.defaultColors.color,
async (value) => {
this.settings.defaultColors.color.themeLight = value.themeLight;
this.settings.defaultColors.color.themeDark = value.themeDark;
await this.plugin.saveSettings();
await this.compiler.injectStyles();
}
);
this.containerEl.createEl("hr");
new Setting(containerEl).setName(i18next.t("settings.prefix.title")).setHeading();
this.containerEl.appendChild(
@ -31,21 +68,6 @@ export class SimpleColoredFolderSettingTab extends PluginSettingTab {
</div></div>`)
);
new Setting(containerEl)
.setName(i18next.t("settings.snippets.title"))
.setClass("no-border")
.setDesc(i18next.t("settings.snippets.desc"))
.addToggle((cb) =>
cb
.setValue(this.settings.exportToCSS)
.onChange(async (value) => {
this.settings.exportToCSS = value;
await this.plugin.saveSettings();
await this.compiler.injectStyles();
})
)
new Setting(containerEl)
.setName(i18next.t("common.color"))
.setClass("no-border")
@ -109,12 +131,10 @@ export class SimpleColoredFolderSettingTab extends PluginSettingTab {
.setClass("no-border")
.setNoInfo()
.addTextArea((text) => {
text
.setValue(this.settings.customStyleSettings)
.onChange(async (value) => {
this.settings.customStyleSettings = value;
await this.plugin.saveSettings();
});
text.setValue(this.settings.customStyleSettings).onChange(async (value) => {
this.settings.customStyleSettings = value;
await this.plugin.saveSettings();
});
text.inputEl.onblur = async () => {
await this.compiler.injectStyles();
};

View file

@ -108,3 +108,58 @@ body:not(.is-grabbing) .nav-folder-title:hover {
--hr-color: var(--background-modifier-border);
--hr-thickness: 1px;
}
/* Aligne le composant avec les autres settings */
.spf .picker-settings-component .setting-item-control {
display: flex;
gap: 16px;
align-items: center;
}
/* Le wrapper des deux pickers */
.spf .dual-alpha-picker-controls {
display: flex;
gap: 4px;
align-items: center;
justify-content: flex-end;
}
.spf .setting-item.no-border.picker-settings-component {
left: 10px;
position: relative;
}
/* Conteneur individuel dun picker + bouton reset */
.spf .alpha-picker-wrapper {
display: flex;
flex-direction: column;
align-items: center;
gap: 4px;
border-radius: 5px;
padding: 10px;
}
/* Visuels light/dark */
.spf .alpha-picker-wrapper.light {
background-color: #ffffff;
}
.spf .alpha-picker-wrapper.dark {
background-color: #1e1e1e;
}
/* Boutons de reset (↺) */
.spf .alpha-picker-wrapper .clickable-icon {
font-size: 14px;
opacity: 0.6;
transition: opacity 0.2s ease;
}
.spf .alpha-picker-wrapper .clickable-icon:hover {
opacity: 1;
}
.spf .setting-item .pickr .pcr-button:after,
.spf .setting-item .pickr .pcr-button:before {
border-radius: 0 !important;
}

View file

@ -1,5 +1,5 @@
import dedent from "dedent";
import { DEFAULT_COLOR, type Prefix } from "./interfaces";
import { type Colors, DEFAULT_COLOR, type Prefix } from "./interfaces";
import "uniformize";
import i18next from "i18next";
import { standardize } from "./utils";
@ -54,7 +54,8 @@ export function convertToCSS(folderName: string, prefix: Prefix, template: strin
export function convertStyleSettings(
folderName: string,
prefix: Prefix,
template: string
template: string,
defaultColor: Colors
) {
const variableNames = generateName(prefix, folderName);
return `
@ -68,25 +69,33 @@ export function convertStyleSettings(
opacity: true
type: variable-themed-color
title: ${i18next.t("common.background")}
default-light: "${DEFAULT_COLOR}"
default-dark: "${DEFAULT_COLOR}"
default-light: "${defaultColor.bg.themeLight}"
default-dark: "${defaultColor.bg.themeDark}"
-
id: ${variableNames.color}
title: ${i18next.t("common.color")}
type: variable-themed-color
format: hex
default-light: "${DEFAULT_COLOR}"
default-dark: "${DEFAULT_COLOR}"
opacity: false
default-light: "${defaultColor.color.themeLight}"
default-dark: "${defaultColor.color.themeDark}"
opacity: true
${dedent(remplaceTemplate(template, folderName, variableNames.bg, variableNames.color))}
-`;
}
export function themes(vn: Prefix) {
export function themes(
vn: Prefix,
defaultColor: Colors,
theme: "themeLight" | "themeDark"
) {
const color =
defaultColor.color[theme] === DEFAULT_COLOR
? '"var(--nav-item-color)"'
: `${defaultColor.color[theme]}`;
return dedent(`\n
${vn.bg}: "${DEFAULT_COLOR}";
${vn.color}: var(--nav-item-color);
${vn.bg}: ${defaultColor.bg[theme]};
${vn.color}: ${color};
`);
}

View file

@ -5,8 +5,17 @@ import {
generateName,
themes,
} from "../src/template";
import { DEFAULT_COLOR, type Prefix } from "../src/interfaces";
import type { Colors, Prefix } from "../src/interfaces";
const defaultColor: Colors = {
bg: {
themeLight: "#30479E59",
themeDark: "#5B5D6659",
},
color: {
themeLight: "#30479E",
themeDark: "#5B5D66",
},
};
describe("generateName", () => {
test("should generate standardized CSS variable names", () => {
const prefix: Prefix = { bg: "scf-bg", color: "scf-color" };
@ -70,14 +79,13 @@ describe("convertStyleSettings", () => {
const folderName = "Media";
const prefix: Prefix = { bg: "scf-bg", color: "scf-color" };
const template = "";
const result = convertStyleSettings(folderName, prefix, template);
const result = convertStyleSettings(folderName, prefix, template, defaultColor);
expect(result).toContain(`title: Media`);
expect(result).toContain(`id: scf-bg-media`); // lowercase 'media'
expect(result).toContain(`id: scf-color-media`); // lowercase 'media'
expect(result).toContain(`default-light: "${DEFAULT_COLOR}"`);
expect(result).toContain(`default-dark: "${DEFAULT_COLOR}"`);
expect(result).toContain(`default-light: "${defaultColor.bg.themeLight}"`);
expect(result).toContain(`default-dark: "${defaultColor.bg.themeDark}"`);
});
test("should include custom template content when provided", () => {
@ -85,7 +93,7 @@ describe("convertStyleSettings", () => {
const prefix: Prefix = { bg: "scf-bg", color: "scf-color" };
const template = "custom-id: ${folderName}-custom\ntitle: Custom ${folderName}";
const result = convertStyleSettings(folderName, prefix, template);
const result = convertStyleSettings(folderName, prefix, template, defaultColor);
expect(result).toContain("custom-id: Notes-custom");
expect(result).toContain("title: Custom Notes");
@ -96,9 +104,9 @@ describe("themes", () => {
test("should generate theme variables", () => {
const vn: Prefix = { bg: "scf-bg-Assets", color: "scf-color-Assets" };
const result = themes(vn);
const result = themes(vn, defaultColor, "themeLight");
expect(result).toContain(`${vn.bg}: "${DEFAULT_COLOR}"`);
expect(result).toContain(`${vn.color}: var(--nav-item-color)`);
expect(result).toContain(`${vn.bg}: ${defaultColor.bg.themeLight}`);
expect(result).toContain(`${vn.color}: ${defaultColor.color.themeLight}`);
});
});