diff --git a/bun.lock b/bun.lock index e2c8c82..2141f89 100644 --- a/bun.lock +++ b/bun.lock @@ -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=="], diff --git a/esbuild.config.mjs b/esbuild.config.mjs index 62e7b06..23086cf 100644 --- a/esbuild.config.mjs +++ b/esbuild.config.mjs @@ -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 ", "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)) { diff --git a/package.json b/package.json index df0a18b..01ec476 100644 --- a/package.json +++ b/package.json @@ -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" ] } diff --git a/src/color-picker.ts b/src/color-picker.ts new file mode 100644 index 0000000..e8584e9 --- /dev/null +++ b/src/color-picker.ts @@ -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(), + }; + } +} diff --git a/src/compiler.ts b/src/compiler.ts index 64ac463..6fd08d5 100644 --- a/src/compiler.ts +++ b/src/compiler.ts @@ -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"); } -} \ No newline at end of file +} diff --git a/src/interfaces.ts b/src/interfaces.ts index d81acd3..93e8a83 100644 --- a/src/interfaces.ts +++ b/src/interfaces.ts @@ -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; diff --git a/src/settings.ts b/src/settings.ts index cd4e65c..4a84c22 100644 --- a/src/settings.ts +++ b/src/settings.ts @@ -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 { `) ); - 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(); }; diff --git a/src/styles.css b/src/styles.css index 899e66c..c433c6b 100644 --- a/src/styles.css +++ b/src/styles.css @@ -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 d’un 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; +} diff --git a/src/template.ts b/src/template.ts index 90c2b02..6cc94c0 100644 --- a/src/template.ts +++ b/src/template.ts @@ -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}; `); }