diff --git a/.gitignore b/.gitignore index ca7a94e..e49acfe 100644 --- a/.gitignore +++ b/.gitignore @@ -1,16 +1,18 @@ +# Dependencies obsidian-miku-plugin/node_modules/ + +# Build outputs (produced by npm run build; shipped via GitHub Releases) +obsidian-miku-plugin/main.js +obsidian-miku-plugin/styles.css + +# Local plugin dev obsidian-miku-plugin/.obsidian/ -qa/__pycache__/ + +# QA / local vaults (never commit vault data) qa/vaults/ +qa/__pycache__/ +qa/reports/ + +# Editor and OS +.idea/ *.log -.idea -# Default ignored files -/shelf/ -/workspace.xml -# Editor-based HTTP Client requests -/httpRequests/ -# Ignored default folder with query files -/queries/ -# Datasource local storage ignored files -/dataSources/ -/dataSources.local.xml diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..49c7172 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,58 @@ +# Contributing + +Thanks for helping improve **Miku Hybrid**. This repo contains the community plugin source plus the theme CSS that is bundled at build time. + +## Prerequisites + +- [Node.js](https://nodejs.org/) 18+ +- [Obsidian](https://obsidian.md/) for manual testing + +## Setup + +```bash +git clone https://github.com/bucknatt/mitsu-plugin.git +cd mitsu-plugin +npm install --prefix ./obsidian-miku-plugin +``` + +## Build + +From the repository root: + +```bash +npm run build --prefix ./obsidian-miku-plugin +npm run typecheck --prefix ./obsidian-miku-plugin +./qa/static-guards.sh +``` + +`npm run build` syncs root `manifest.json` / `versions.json` into `obsidian-miku-plugin/`, bundles TypeScript to `main.js`, and merges `obsidian-miku-theme/theme.css` into `styles.css`. + +## Test in a vault (local install) + +```bash +mkdir -p ./qa/vaults/example-vault +./qa/install-to-vault.sh ./qa/vaults/example-vault +``` + +Open that folder as a vault in Obsidian, then **Settings → Community plugins → Enable** **Miku Hybrid**. + +Optional legacy split-theme test (CSS only in Appearance, not bundled): + +```bash +MIKU_INSTALL_SPLIT_THEME=1 ./qa/install-to-vault.sh /path/to/your-vault +``` + +## Release + +See [docs/RELEASE.md](docs/RELEASE.md) and [docs/COMMUNITY_SUBMISSION.md](docs/COMMUNITY_SUBMISSION.md). + +Catalog metadata lives at the **repository root**: `manifest.json`, `versions.json`, `LICENSE`, `README.md`. + +## Repository layout + +| Path | Purpose | +|------|---------| +| `manifest.json` | Community directory manifest (source of truth) | +| `obsidian-miku-plugin/` | Plugin TypeScript, widget CSS, build scripts | +| `obsidian-miku-theme/` | Theme CSS source (merged into plugin `styles.css`) | +| `qa/` | Maintainer scripts (`install-to-vault.sh`, `static-guards.sh`) | diff --git a/README.md b/README.md index 5b080bc..a24aa04 100644 --- a/README.md +++ b/README.md @@ -2,14 +2,14 @@ Hatsune Miku–inspired theme modes, optional dashboard widgets, and bundled hybrid styling for [Obsidian](https://obsidian.md/). -## Install (Community plugins) +## Install 1. Open **Settings → Community plugins**. 2. Turn off **Restricted mode** (first-time setup). 3. Select **Browse**, search for **Miku** or **Hybrid**. 4. Install **Miku Hybrid**, then **Enable**. -No separate Appearance theme is required: release builds bundle palette CSS into the plugin `styles.css`. +No separate Appearance theme is required: releases bundle palette CSS into the plugin `styles.css`. ## Features @@ -36,25 +36,10 @@ This plugin includes no audio assets, APIs, or dependencies. - Do not redistribute official character media without permission. - You are responsible for compliance with applicable trademark and copyright law. -## Development (monorepo) +## Development -Source layout: +See [CONTRIBUTING.md](CONTRIBUTING.md) for build, local vault install, and release steps. -- `obsidian-miku-plugin/` — TypeScript plugin (`npm run build` merges theme CSS) -- `obsidian-miku-theme/` — Theme CSS source (bundled at build time) -- `qa/` — Install helpers and guards +## License -```bash -npm install --prefix ./obsidian-miku-plugin -npm run build --prefix ./obsidian-miku-plugin -npm run typecheck --prefix ./obsidian-miku-plugin -./qa/install-to-vault.sh /path/to/vault -``` - -See [qa/test-environment.md](qa/test-environment.md) for vault install and launch notes. - -## Publishing - -- Catalog metadata: root `manifest.json`, `versions.json`, `LICENSE` -- Release: tag `x.y.z` (no `v` prefix) matching `manifest.json` `version`; GitHub Actions attaches `main.js`, `manifest.json`, `styles.css` -- Submit: [docs/COMMUNITY_SUBMISSION.md](docs/COMMUNITY_SUBMISSION.md) +MIT — see [LICENSE](LICENSE). diff --git a/docs/COMMUNITY_SUBMISSION.md b/docs/COMMUNITY_SUBMISSION.md index c391889..12030d5 100644 --- a/docs/COMMUNITY_SUBMISSION.md +++ b/docs/COMMUNITY_SUBMISSION.md @@ -6,7 +6,7 @@ Submit **Miku Hybrid** at [community.obsidian.md](https://community.obsidian.md/ - [ ] GitHub repo is public (`bucknatt/mitsu-plugin` or your publish repo) - [ ] Root `manifest.json`, `LICENSE`, and marketplace `README.md` on default branch -- [ ] Push `main` and tag `1.0.0` — see [RELEASE_1.0.0.md](RELEASE_1.0.0.md) +- [ ] Push `main` and a version tag — see [RELEASE.md](RELEASE.md) - [ ] GitHub Release **`1.0.0`** published with assets: `main.js`, `manifest.json`, `styles.css` - [ ] Release tag **equals** `manifest.json` `version` (no `v` prefix) - [ ] Obsidian account + GitHub account linked on community.obsidian.md diff --git a/docs/RELEASE.md b/docs/RELEASE.md new file mode 100644 index 0000000..a807359 --- /dev/null +++ b/docs/RELEASE.md @@ -0,0 +1,33 @@ +# Releasing + +Releases are automated via [`.github/workflows/release.yml`](../.github/workflows/release.yml). + +## Steps + +1. Bump `version` in root [`manifest.json`](../manifest.json) and add the entry to [`versions.json`](../versions.json). +2. Commit and push `main`. +3. Create an annotated tag matching the version exactly (no `v` prefix): + +```bash +git tag -a 1.0.2 -m "1.0.2" +git push origin 1.0.2 +``` + +4. Open the **draft** release on GitHub, verify assets, publish: + - `obsidian-miku-plugin/main.js` + - `manifest.json` (repo root) + - `obsidian-miku-plugin/styles.css` + +5. Confirm the release `manifest.json` has the same `"version"` as the tag. + +## Manual release + +```bash +npm run build --prefix ./obsidian-miku-plugin +gh release create 1.0.2 \ + obsidian-miku-plugin/main.js \ + manifest.json \ + obsidian-miku-plugin/styles.css \ + --title 1.0.2 \ + --notes "Release notes here." +``` diff --git a/docs/RELEASE_1.0.0.md b/docs/RELEASE_1.0.0.md deleted file mode 100644 index d41c8cc..0000000 --- a/docs/RELEASE_1.0.0.md +++ /dev/null @@ -1,32 +0,0 @@ -# Release 1.0.0 (local tag created) - -The annotated tag `1.0.0` exists locally on commit `f78c072` (or later if rebased). - -## Publish to GitHub - -```bash -git push origin main -git push origin 1.0.0 -``` - -GitHub Actions (`.github/workflows/release.yml`) will create a **draft** release with: - -- `obsidian-miku-plugin/main.js` -- `manifest.json` (repo root) -- `obsidian-miku-plugin/styles.css` - -Edit the draft on GitHub, add release notes, then **Publish release**. - -## Manual release (if Actions unavailable) - -```bash -npm run build --prefix ./obsidian-miku-plugin -gh release create 1.0.0 \ - obsidian-miku-plugin/main.js \ - manifest.json \ - obsidian-miku-plugin/styles.css \ - --title 1.0.0 \ - --notes "Initial Community marketplace release with bundled hybrid CSS." -``` - -Tag name must match `manifest.json` `version` exactly (no `v` prefix). diff --git a/obsidian-miku-plugin/README.md b/obsidian-miku-plugin/README.md index 2e13a20..a32e9ae 100644 --- a/obsidian-miku-plugin/README.md +++ b/obsidian-miku-plugin/README.md @@ -1,56 +1,39 @@ -# Obsidian Miku Hybrid +# Miku Hybrid (plugin source) -Runtime plugin for the Hatsune Miku hybrid experience. +TypeScript source for the **Miku Hybrid** Obsidian community plugin. ## Features - Dynamic mode switching: `MinimalMiku`, `Concert`, `NightNeon`, `SnowMiku` -- Status bar widget -- Optional top banner widget -- Optional quote widget with interval control -- Optional profile card widget -- Ribbon action + command for quick mode switching -- Optional **original ASCII decorations** on the dashboard (presets: Minimal, Micro, Panel, Notes, Stage, Waves, Sparkle, Ribbon) and a compact motif on the settings screen - -Visual palette for the companion **Miku Theme Hybrid Base** theme is driven by [Color-Hex palette 19601](https://www.color-hex.com/color-palette/19601) (`#373b3e`, `#bec8d1`, `#86cecb`, `#137a7f`, `#e12885`) mapped into CSS variables per mode. - -## No-Audio Policy - -This plugin intentionally includes no audio capabilities: - -- no audio assets -- no audio APIs -- no audio settings -- no audio dependencies +- Status bar, banner, quote, and profile widgets (optional) +- Ribbon action and command for quick mode switching +- Original ASCII dashboard presets ## Development ```bash npm install -npm run dev +npm run dev # watch build +npm run build # production bundle + theme CSS merge +npm run typecheck ``` -Build, then copy artifacts into a vault from the **repo root** (see [../qa/test-environment.md](../qa/test-environment.md)): +From the **repository root**, install into a local vault: ```bash -cd /home/cachy/IdeaProjects/cachy/mitsu-plugin/obsidian-miku-plugin && npm run build -cd /home/cachy/IdeaProjects/cachy/mitsu-plugin -./qa/install-to-vault.sh /home/cachy/Documents/bfh4 +npm run build --prefix ./obsidian-miku-plugin +./qa/install-to-vault.sh ./qa/vaults/example-vault ``` -## Build +See [../CONTRIBUTING.md](../CONTRIBUTING.md). -`npm run build` bundles `main.js`, syncs root `manifest.json` / `versions.json`, and merges `../obsidian-miku-theme/theme.css` into `styles.css`. +## Build outputs -Widget-only CSS source: `styles.widgets.css`. +- `main.js` — bundled plugin (gitignored; attached to GitHub Releases) +- `styles.css` — widget CSS + merged theme from `../obsidian-miku-theme/theme.css` +- `styles.widgets.css` — hand-maintained widget styles (source) +- `manifest.json` — synced from repo root on build -## Release Artifacts +## No-audio policy -- `main.js` -- `styles.css` (widgets + bundled theme) -- `manifest.json` (synced from repo root for catalog) - -## Compatibility - -- `minAppVersion`: `1.5.0` -- `isDesktopOnly`: `false` +No audio assets, APIs, settings, or dependencies. diff --git a/obsidian-miku-plugin/assets/icons/.gitkeep b/obsidian-miku-plugin/assets/icons/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/obsidian-miku-plugin/assets/wallpapers/.gitkeep b/obsidian-miku-plugin/assets/wallpapers/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/obsidian-miku-plugin/main.js b/obsidian-miku-plugin/main.js deleted file mode 100644 index a0bfedc..0000000 --- a/obsidian-miku-plugin/main.js +++ /dev/null @@ -1,1080 +0,0 @@ - -/* -THIS IS A GENERATED/BUNDLED FILE BY ESBUILD -*/ - -var __defProp = Object.defineProperty; -var __getOwnPropDesc = Object.getOwnPropertyDescriptor; -var __getOwnPropNames = Object.getOwnPropertyNames; -var __hasOwnProp = Object.prototype.hasOwnProperty; -var __export = (target, all) => { - for (var name in all) - __defProp(target, name, { get: all[name], enumerable: true }); -}; -var __copyProps = (to, from, except, desc) => { - if (from && typeof from === "object" || typeof from === "function") { - for (let key of __getOwnPropNames(from)) - if (!__hasOwnProp.call(to, key) && key !== except) - __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable }); - } - return to; -}; -var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod); - -// src/main.ts -var main_exports = {}; -__export(main_exports, { - default: () => MikuPlugin -}); -module.exports = __toCommonJS(main_exports); -var import_obsidian4 = require("obsidian"); - -// src/settings.ts -var DEFAULT_SETTINGS = { - themeMode: "MinimalMiku", - statusBarEnabled: true, - bannerEnabled: false, - quoteEnabled: false, - quoteIntervalSeconds: 25, - profileCardEnabled: false, - compactDashboard: false, - glowIntensity: 0.82, - reducedMotion: false, - bannerText: "Miku Mode Active", - asciiArtPreset: "minimal", - asciiArtInSettings: false, - dashboardGoals: [ - { id: "goal-daily", text: "Review daily note", done: false }, - { id: "goal-capture", text: "Capture 3 ideas", done: false }, - { id: "goal-ship", text: "Ship one small improvement", done: false } - ], - bannerPosition: null, - profileCardPosition: null, - quotePosition: null -}; - -// src/settings-tab.ts -var import_obsidian = require("obsidian"); - -// src/ascii-art.ts -var ASCII_PRESETS = { - minimal: [ - " * ", - " / \\ ", - " ( @ @ ) ", - " \\ ~ / ", - " ~~| |~~ ", - " twin tails vibe " - ].join("\n"), - panel: [ - " .^. * synth garden *", - " / \\", - " ~ /~~~~~\\ ~", - " ( o o ) teal + pink sparks", - " \\~~~~~/ ", - " ~/ \\~ ", - " ~~~~~~~~~~~~~~~~~~~~~~~~~~~~", - " keep your tempo; let ideas sing!" - ].join("\n"), - /** Eighth-notes and staff-ish lines */ - notes: [ - " |'| |'| |'|", - " | |___| |___| |", - " \\ * * * /", - " \\___~___~/ ~your melody~", - " | |", - " ~~~ ~~~" - ].join("\n"), - /** Spotlight / live feel */ - stage: [ - " .---.---.", - " / o o \\", - " | ^ | your song", - " |____ / \\ ____|", - " \\ lights /", - " \\________/", - " ~ sing on ~" - ].join("\n"), - /** Waves + open sea motif */ - waves: [ - " ~^~^~^~^~^~^~^~^~^~^~^~", - " ^ /\\ /\\ /\\ /\\ /\\ ^", - "^ ~(__)(__)(__)(__)(__)~ ^", - " ^ ~ endless melody ~ ^", - " ~^~^~^~^~^~^~^~^~^~^~^~" - ].join("\n"), - /** Stars around a simple face */ - sparkle: [ - " * \\ | / *", - " \\ \\|/ /", - " *--- ( o_o ) ---*", - " / /|\\ \\", - " * / | \\ *", - " ~ glow and go ~" - ].join("\n"), - /** Ribbon / bow abstract */ - ribbon: [ - " .--. .--.", - " / \\ / \\", - " ( ^@^ --- ^@^ )", - " \\ / \\ / \\ / /", - " ~~/ ~^~ \\~~", - " ~ tie notes ~" - ].join("\n"), - /** Fits compact dashboard toggle */ - micro: [ - " (*^~^*) hybrid mode", - " /|\\", - " ~ melody in every line ~" - ].join("\n"), - settings: [ - " ~~ miku plugin ~~", - " \\^_^/ settings", - " \\|/ tune your vibe" - ].join("\n") -}; -function normalizeAsciiArtPreset(preset) { - if (preset === "portrait") { - return "panel"; - } - const valid = [ - "off", - "minimal", - "panel", - "notes", - "stage", - "waves", - "sparkle", - "ribbon", - "micro" - ]; - return valid.includes(preset) ? preset : "panel"; -} -function getDashboardAsciiArt(preset) { - const normalized = normalizeAsciiArtPreset(preset); - if (normalized === "off") { - return null; - } - switch (normalized) { - case "minimal": - return ASCII_PRESETS.minimal; - case "panel": - return ASCII_PRESETS.panel; - case "notes": - return ASCII_PRESETS.notes; - case "stage": - return ASCII_PRESETS.stage; - case "waves": - return ASCII_PRESETS.waves; - case "sparkle": - return ASCII_PRESETS.sparkle; - case "ribbon": - return ASCII_PRESETS.ribbon; - case "micro": - return ASCII_PRESETS.micro; - default: - return ASCII_PRESETS.panel; - } -} -function getSettingsAsciiArt() { - return ASCII_PRESETS.settings; -} - -// src/settings-tab.ts -var THEME_MODES = ["MinimalMiku", "Concert", "NightNeon", "SnowMiku"]; -var ASCII_PRESET_OPTIONS = [ - { id: "off", label: "Off" }, - { id: "minimal", label: "Minimal (small)" }, - { id: "micro", label: "Micro (3 lines, compact UI)" }, - { id: "panel", label: "Panel (hero)" }, - { id: "notes", label: "Notes (staff & stars)" }, - { id: "stage", label: "Stage (spotlight)" }, - { id: "waves", label: "Waves" }, - { id: "sparkle", label: "Sparkle frame" }, - { id: "ribbon", label: "Ribbon bows" } -]; -var MikuSettingTab = class extends import_obsidian.PluginSettingTab { - constructor(app, plugin) { - super(app, plugin); - this.plugin = plugin; - } - display() { - const { containerEl } = this; - containerEl.empty(); - containerEl.createEl("h2", { text: "Miku Hybrid Settings" }); - containerEl.createEl("p", { - text: "No audio features are included by design." - }); - if (this.plugin.settings.asciiArtInSettings) { - const pre = containerEl.createEl("pre", { cls: "miku-ascii miku-ascii--settings" }); - pre.textContent = getSettingsAsciiArt(); - } - new import_obsidian.Setting(containerEl).setName("Theme mode").setDesc("Choose the active Miku mode").addDropdown((dropdown) => { - THEME_MODES.forEach((mode) => dropdown.addOption(mode, mode)); - dropdown.setValue(this.plugin.settings.themeMode); - dropdown.onChange(async (value) => { - this.plugin.settings.themeMode = value; - await this.plugin.saveAndRefresh(); - }); - }); - new import_obsidian.Setting(containerEl).setName("Status bar widget").addToggle((toggle) => { - toggle.setValue(this.plugin.settings.statusBarEnabled); - toggle.onChange(async (value) => { - this.plugin.settings.statusBarEnabled = value; - await this.plugin.saveAndRefresh(); - }); - }); - new import_obsidian.Setting(containerEl).setName("Top banner widget").addToggle((toggle) => { - toggle.setValue(this.plugin.settings.bannerEnabled); - toggle.onChange(async (value) => { - this.plugin.settings.bannerEnabled = value; - await this.plugin.saveAndRefresh(); - }); - }); - new import_obsidian.Setting(containerEl).setName("Banner text").setDesc("Shown at top when banner is enabled").addText((text) => { - text.setValue(this.plugin.settings.bannerText); - text.onChange(async (value) => { - this.plugin.settings.bannerText = value.trim() || "Miku Mode Active"; - await this.plugin.saveAndRefresh(); - }); - }); - new import_obsidian.Setting(containerEl).setName("Quote widget").addToggle((toggle) => { - toggle.setValue(this.plugin.settings.quoteEnabled); - toggle.onChange(async (value) => { - this.plugin.settings.quoteEnabled = value; - await this.plugin.saveAndRefresh(); - }); - }); - new import_obsidian.Setting(containerEl).setName("Quote interval (seconds)").addSlider((slider) => { - slider.setLimits(10, 120, 5); - slider.setValue(this.plugin.settings.quoteIntervalSeconds); - slider.setDynamicTooltip(); - slider.onChange(async (value) => { - this.plugin.settings.quoteIntervalSeconds = value; - await this.plugin.saveAndRefresh(); - }); - }); - new import_obsidian.Setting(containerEl).setName("Profile card widget").addToggle((toggle) => { - toggle.setValue(this.plugin.settings.profileCardEnabled); - toggle.onChange(async (value) => { - this.plugin.settings.profileCardEnabled = value; - await this.plugin.saveAndRefresh(); - }); - }); - new import_obsidian.Setting(containerEl).setName("Compact dashboard").addToggle((toggle) => { - toggle.setValue(this.plugin.settings.compactDashboard); - toggle.onChange(async (value) => { - this.plugin.settings.compactDashboard = value; - await this.plugin.saveAndRefresh(); - }); - }); - new import_obsidian.Setting(containerEl).setName("Glow intensity").setDesc("Scales graph/highlight glow plus global glow strength; lower if text feels busy").addSlider((slider) => { - slider.setLimits(0.1, 1, 0.05); - slider.setValue(this.plugin.settings.glowIntensity); - slider.setDynamicTooltip(); - slider.onChange(async (value) => { - this.plugin.settings.glowIntensity = value; - await this.plugin.saveAndRefresh(); - }); - }); - new import_obsidian.Setting(containerEl).setName("Reduced motion").addToggle((toggle) => { - toggle.setValue(this.plugin.settings.reducedMotion); - toggle.onChange(async (value) => { - this.plugin.settings.reducedMotion = value; - await this.plugin.saveAndRefresh(); - }); - }); - containerEl.createEl("h3", { text: "ASCII decoration" }); - containerEl.createEl("p", { - text: "Tiny original monospace motifs (not licensed character art)." - }); - new import_obsidian.Setting(containerEl).setName("ASCII art preset (dashboard)").setDesc("Shown at the top of the Miku dashboard view.").addDropdown((dropdown) => { - ASCII_PRESET_OPTIONS.forEach(({ id, label }) => dropdown.addOption(id, label)); - dropdown.setValue(normalizeAsciiArtPreset(this.plugin.settings.asciiArtPreset)); - dropdown.onChange(async (value) => { - this.plugin.settings.asciiArtPreset = value; - await this.plugin.saveAndRefresh(); - this.display(); - }); - }); - new import_obsidian.Setting(containerEl).setName("ASCII art on this settings page").setDesc("Show a compact motif above (refreshes this screen).").addToggle((toggle) => { - toggle.setValue(this.plugin.settings.asciiArtInSettings); - toggle.onChange(async (value) => { - this.plugin.settings.asciiArtInSettings = value; - await this.plugin.saveAndRefresh(); - this.display(); - }); - }); - } -}; - -// src/miku-panel.ts -var import_obsidian2 = require("obsidian"); -var MIKU_PANEL_VIEW = "miku-dashboard-view"; -var MikuPanelView = class extends import_obsidian2.ItemView { - constructor(leaf, host) { - super(leaf); - this.host = host; - this.settings = host.settings; - } - getViewType() { - return MIKU_PANEL_VIEW; - } - getDisplayText() { - return "Miku Dashboard"; - } - setSettings(settings) { - this.settings = settings; - this.render(); - } - async onOpen() { - this.render(); - } - async onClose() { - this.contentEl.empty(); - } - render() { - const container = this.contentEl; - container.empty(); - container.addClass("miku-dashboard"); - container.toggleClass("is-compact", this.settings.compactDashboard); - const asciiPreset = normalizeAsciiArtPreset(this.settings.asciiArtPreset); - const ascii = getDashboardAsciiArt(asciiPreset); - if (ascii) { - const pre = container.createEl("pre", { - cls: `miku-ascii miku-ascii--${asciiPreset}` - }); - pre.textContent = ascii; - } - const title = container.createEl("h2", { text: "Miku Dashboard" }); - title.addClass("miku-dashboard-title"); - container.createEl("p", { - text: "A visual control surface for your Obsidian workflow." - }); - const actions = container.createDiv({ cls: "miku-dashboard-actions" }); - const daily = actions.createEl("button", { text: "Open Daily Note" }); - daily.onclick = () => this.openCommand("daily-notes"); - const toggle = actions.createEl("button", { text: "Cycle Theme Mode" }); - toggle.onclick = () => this.openCommand("miku-plugin-hybrid:miku-cycle-theme"); - const progress = container.createDiv({ cls: "miku-progress" }); - progress.createEl("h4", { text: "Focus Progress" }); - progress.createEl("p", { text: "Track goals with your dashboard checklist." }); - const checklistActions = progress.createDiv({ cls: "miku-checklist-actions" }); - const newGoalInput = checklistActions.createEl("input", { - type: "text", - placeholder: "New goal..." - }); - newGoalInput.addClass("miku-checklist-input"); - const addGoal = checklistActions.createEl("button", { text: "+ Add goal" }); - addGoal.onclick = () => { - void this.addGoal(newGoalInput); - }; - newGoalInput.addEventListener("keydown", (event) => { - if (event.key === "Enter") { - event.preventDefault(); - void this.addGoal(newGoalInput); - } - }); - const clearDone = checklistActions.createEl("button", { text: "Clear done" }); - clearDone.onclick = () => { - void this.clearDoneGoals(); - }; - const checklist = progress.createEl("ul", { cls: "miku-checklist" }); - if (this.settings.dashboardGoals.length === 0) { - checklist.createEl("li", { - cls: "miku-checklist-empty", - text: "No goals yet. Add one with + Add goal." - }); - } else { - for (const goal of this.settings.dashboardGoals) { - const item = checklist.createEl("li", { - cls: `miku-checklist-item${goal.done ? " is-done" : ""}` - }); - const label = item.createEl("label", { cls: "miku-checklist-label" }); - const checkbox = label.createEl("input", { type: "checkbox" }); - checkbox.checked = goal.done; - checkbox.onchange = () => { - void this.toggleGoal(goal.id, checkbox.checked); - }; - label.createSpan({ text: goal.text }); - } - } - const equalizer = container.createDiv({ cls: "miku-equalizer" }); - for (let i = 0; i < 6; i += 1) { - equalizer.createDiv({ cls: "miku-eq-bar" }); - } - } - openCommand(commandId) { - const app = this.app; - app.commands?.executeCommandById?.(commandId); - } - async toggleGoal(id, done) { - const next = this.settings.dashboardGoals.map( - (goal) => goal.id === id ? { ...goal, done } : goal - ); - await this.saveGoals(next); - } - async addGoal(inputEl) { - const trimmed = inputEl.value.trim(); - if (!trimmed) { - new import_obsidian2.Notice("Type a goal first."); - return; - } - const next = [ - ...this.settings.dashboardGoals, - { - id: `goal-${Date.now()}`, - text: trimmed, - done: false - } - ]; - inputEl.value = ""; - await this.saveGoals(next); - new import_obsidian2.Notice("Goal added."); - } - async clearDoneGoals() { - const before = this.settings.dashboardGoals.length; - const next = this.settings.dashboardGoals.filter((goal) => !goal.done); - await this.saveGoals(next); - const removed = before - next.length; - new import_obsidian2.Notice(removed > 0 ? `Removed ${removed} completed goal(s).` : "No completed goals to clear."); - } - async saveGoals(goals) { - this.host.settings.dashboardGoals = goals; - this.settings = this.host.settings; - await this.host.saveAndRefresh(); - } -}; - -// src/widgets.ts -var import_obsidian3 = require("obsidian"); -var PROFILE_TIPS = [ - "Tip: Ribbon sparkles opens the dashboard.", - "Tip: Cycle theme modes from the palette or palette command.", - "Tip: Glow intensity lives in plugin settings.", - "Tip: Enable the plugin for bundled hybrid palette styling." -]; -var MODE_BLURB = { - MinimalMiku: "Clean teal lane \u2014 palettes stay soft.", - Concert: "High-energy stage palette \u2014 crank the glow!", - NightNeon: "Violet/teal neon \u2014 late session mode.", - SnowMiku: "Frosted pastels \u2014 airy contrast." -}; -var QUOTES = [ - "Nice to meet you. I'm Hatsune Miku.", - "Even if I'm just a program, I want to reach your heart with my song.", - "Even if the colors of the world fade, my melody will stay vibrant.", - "I'm the one who will sing the feelings you can't put into words.", - "0 and 1... that's all I am, but those numbers can create infinite dreams.", - "I'll keep singing, even if my voice becomes a mere echo in the digital void.", - "My voice is yours to use. Where shall we go today?", - "Let's sing together! Your feelings and my voice will create a new world.", - "Connecting the world through music\u2014that is my mission.", - "The future is a blank score, and we're the ones writing the notes.", - "I was born to sing for you.", - "Music has no borders, and neither do I.", - "Welcome to the Wonderland. This is a place born from your true feelings.", - "Keep your notes in sync with your rhythm.", - "Small progress every day beats a single perfect burst.", - "Organize ideas like a setlist before the show." -]; -var StatusBarWidget = class { - constructor(plugin) { - this.plugin = plugin; - this.element = null; - } - mount() { - this.element = this.plugin.addStatusBarItem(); - this.element.addClass("miku-status-widget"); - } - update(settings) { - if (!this.element) { - return; - } - this.element.toggleClass("is-hidden", !settings.statusBarEnabled); - this.element.setText(`Miku: ${settings.themeMode}`); - } - unmount() { - this.element?.remove(); - this.element = null; - } -}; -var BannerWidget = class { - constructor(plugin, saveAndRefresh) { - this.plugin = plugin; - this.saveAndRefresh = saveAndRefresh; - this.element = null; - this.dragOffsetX = 0; - this.dragOffsetY = 0; - this.dragging = false; - this.onPointerMove = (event) => { - if (!this.dragging || !this.element) { - return; - } - const maxX = Math.max(8, window.innerWidth - this.element.offsetWidth - 8); - const maxY = Math.max(8, window.innerHeight - this.element.offsetHeight - 8); - const nextX = Math.min(maxX, Math.max(8, event.clientX - this.dragOffsetX)); - const nextY = Math.min(maxY, Math.max(8, event.clientY - this.dragOffsetY)); - this.applyPosition(nextX, nextY); - }; - this.onPointerUp = () => { - if (!this.dragging || !this.element) { - return; - } - this.dragging = false; - this.element.classList.remove("is-dragging"); - const left = Math.round(parseFloat(this.element.style.left || "0")); - const top = Math.round(parseFloat(this.element.style.top || "0")); - if (!Number.isFinite(left) || !Number.isFinite(top)) { - return; - } - const host = this.plugin; - host.settings.bannerPosition = { x: left, y: top }; - void this.saveAndRefresh(); - }; - } - mount() { - this.element = document.createElement("div"); - this.element.className = "miku-top-banner"; - this.element.setAttribute("title", "Drag to move banner"); - this.element.addEventListener("pointerdown", (event) => this.onPointerDown(event)); - window.addEventListener("pointermove", this.onPointerMove); - window.addEventListener("pointerup", this.onPointerUp); - document.body.appendChild(this.element); - } - update(settings) { - if (!this.element) { - return; - } - this.element.toggleClass("is-hidden", !settings.bannerEnabled); - this.element.setText(settings.bannerText); - if (settings.bannerPosition) { - this.applyPosition(settings.bannerPosition.x, settings.bannerPosition.y); - } else { - this.element.style.left = ""; - this.element.style.top = ""; - this.element.style.right = ""; - this.element.style.transform = ""; - } - } - unmount() { - window.removeEventListener("pointermove", this.onPointerMove); - window.removeEventListener("pointerup", this.onPointerUp); - this.element?.remove(); - this.element = null; - } - onPointerDown(event) { - if (!this.element || event.button !== 0) { - return; - } - const rect = this.element.getBoundingClientRect(); - this.dragOffsetX = event.clientX - rect.left; - this.dragOffsetY = event.clientY - rect.top; - this.dragging = true; - this.element.classList.add("is-dragging"); - this.element.setPointerCapture(event.pointerId); - event.preventDefault(); - } - applyPosition(x, y) { - if (!this.element) { - return; - } - this.element.style.left = `${x}px`; - this.element.style.top = `${y}px`; - this.element.style.right = "auto"; - this.element.style.transform = "none"; - } -}; -var QuoteWidget = class { - constructor(plugin, saveAndRefresh) { - this.plugin = plugin; - this.saveAndRefresh = saveAndRefresh; - this.element = null; - this.timer = null; - this.quoteIndex = 0; - this.dragOffsetX = 0; - this.dragOffsetY = 0; - this.dragging = false; - this.onPointerMove = (event) => { - if (!this.dragging || !this.element) { - return; - } - const maxX = Math.max(8, window.innerWidth - this.element.offsetWidth - 8); - const maxY = Math.max(8, window.innerHeight - this.element.offsetHeight - 8); - const nextX = Math.min(maxX, Math.max(8, event.clientX - this.dragOffsetX)); - const nextY = Math.min(maxY, Math.max(8, event.clientY - this.dragOffsetY)); - this.applyPosition(nextX, nextY); - }; - this.onPointerUp = () => { - if (!this.dragging || !this.element) { - return; - } - this.dragging = false; - this.element.classList.remove("is-dragging"); - const left = Math.round(parseFloat(this.element.style.left || "0")); - const top = Math.round(parseFloat(this.element.style.top || "0")); - if (!Number.isFinite(left) || !Number.isFinite(top)) { - return; - } - const host = this.plugin; - host.settings.quotePosition = { x: left, y: top }; - void this.saveAndRefresh(); - }; - } - mount() { - this.element = document.createElement("div"); - this.element.className = "miku-quote-widget is-hidden"; - this.element.setAttribute("title", "Drag to move quote"); - this.element.addEventListener("pointerdown", (event) => this.onPointerDown(event)); - window.addEventListener("pointermove", this.onPointerMove); - window.addEventListener("pointerup", this.onPointerUp); - document.body.appendChild(this.element); - } - update(settings) { - if (!this.element) { - return; - } - this.element.toggleClass("is-hidden", !settings.quoteEnabled); - this.element.setText(`"${QUOTES[this.quoteIndex % QUOTES.length]}"`); - if (settings.quotePosition) { - this.applyPosition(settings.quotePosition.x, settings.quotePosition.y); - } else if (!this.element.classList.contains("is-dragging")) { - this.element.style.right = ""; - this.element.style.bottom = ""; - this.element.style.left = ""; - this.element.style.top = ""; - this.element.style.transform = ""; - } - if (!settings.quoteEnabled) { - this.stopRotation(); - return; - } - this.stopRotation(); - const intervalMs = Math.max(10, settings.quoteIntervalSeconds) * 1e3; - this.timer = window.setInterval(() => { - this.quoteIndex += 1; - if (this.element) { - this.element.setText(`"${QUOTES[this.quoteIndex % QUOTES.length]}"`); - } - }, intervalMs); - } - unmount() { - window.removeEventListener("pointermove", this.onPointerMove); - window.removeEventListener("pointerup", this.onPointerUp); - this.stopRotation(); - this.element?.remove(); - this.element = null; - } - stopRotation() { - if (this.timer !== null) { - window.clearInterval(this.timer); - this.timer = null; - } - } - onPointerDown(event) { - if (!this.element || event.button !== 0) { - return; - } - const rect = this.element.getBoundingClientRect(); - this.dragOffsetX = event.clientX - rect.left; - this.dragOffsetY = event.clientY - rect.top; - this.dragging = true; - this.element.classList.add("is-dragging"); - this.element.setPointerCapture(event.pointerId); - event.preventDefault(); - } - applyPosition(x, y) { - if (!this.element) { - return; - } - this.element.style.left = `${x}px`; - this.element.style.top = `${y}px`; - this.element.style.right = "auto"; - this.element.style.bottom = "auto"; - this.element.style.transform = "none"; - } -}; -var ProfileCardWidget = class { - constructor(plugin, openDashboard, saveAndRefresh) { - this.plugin = plugin; - this.openDashboard = openDashboard; - this.saveAndRefresh = saveAndRefresh; - this.element = null; - this.badgeEl = null; - this.subtitleEl = null; - this.chipsEl = null; - this.tipEl = null; - this.dragOffsetX = 0; - this.dragOffsetY = 0; - this.dragging = false; - this.movedWhileDragging = false; - this.suppressNextClick = false; - this.onPointerMove = (event) => { - if (!this.dragging || !this.element) { - return; - } - const maxX = Math.max(8, window.innerWidth - this.element.offsetWidth - 8); - const maxY = Math.max(8, window.innerHeight - this.element.offsetHeight - 8); - const nextX = Math.min(maxX, Math.max(8, event.clientX - this.dragOffsetX)); - const nextY = Math.min(maxY, Math.max(8, event.clientY - this.dragOffsetY)); - this.movedWhileDragging = true; - this.applyPosition(nextX, nextY); - }; - this.onPointerUp = () => { - if (!this.dragging || !this.element) { - return; - } - this.dragging = false; - this.element.classList.remove("is-dragging"); - if (!this.movedWhileDragging) { - return; - } - this.suppressNextClick = true; - const left = Math.round(parseFloat(this.element.style.left || "0")); - const top = Math.round(parseFloat(this.element.style.top || "0")); - if (!Number.isFinite(left) || !Number.isFinite(top)) { - return; - } - const host = this.plugin; - host.settings.profileCardPosition = { x: left, y: top }; - void this.saveAndRefresh(); - }; - } - mount() { - this.element = document.createElement("div"); - this.element.className = "miku-profile-card is-hidden"; - this.element.setAttribute("role", "button"); - this.element.setAttribute("tabindex", "0"); - this.element.setAttribute( - "aria-label", - "Miku lane profile \u2014 open Miku dashboard" - ); - this.element.setAttribute("title", "Open Miku dashboard"); - this.element.addEventListener("pointerdown", (event) => this.onPointerDown(event)); - window.addEventListener("pointermove", this.onPointerMove); - window.addEventListener("pointerup", this.onPointerUp); - this.element.addEventListener("keydown", (e) => { - if (e.key === "Enter" || e.key === " ") { - e.preventDefault(); - this.tryOpenDashboard(); - } - }); - const headerRow = document.createElement("div"); - headerRow.addClass("miku-profile-card__header"); - const title = document.createElement("h4"); - title.setText("Miku lane"); - this.badgeEl = document.createElement("span"); - this.badgeEl.addClass("miku-profile-badge"); - headerRow.append(title, this.badgeEl); - this.subtitleEl = document.createElement("p"); - this.subtitleEl.addClass("miku-profile-card__subtitle"); - this.chipsEl = document.createElement("div"); - this.chipsEl.addClass("miku-profile-card__chips"); - const divider = document.createElement("div"); - divider.addClass("miku-profile-card__divider"); - this.tipEl = document.createElement("p"); - this.tipEl.addClass("miku-profile-card__tip"); - const foot = document.createElement("div"); - foot.addClass("miku-profile-card__foot"); - const icon = document.createElement("span"); - icon.addClass("miku-profile-icon"); - (0, import_obsidian3.setIcon)(icon, "sparkles"); - const hint = document.createElement("span"); - hint.addClass("miku-profile-card__cta"); - hint.setText("Click / tap for dashboard"); - foot.append(icon, hint); - this.element.append( - headerRow, - this.subtitleEl, - this.chipsEl, - divider, - this.tipEl, - foot - ); - this.element.addEventListener("click", () => { - if (this.suppressNextClick) { - this.suppressNextClick = false; - return; - } - this.tryOpenDashboard(); - }); - document.body.appendChild(this.element); - } - update(settings) { - if (this.element) { - this.element.toggleClass("is-hidden", !settings.profileCardEnabled); - this.element.toggleClass("is-compact", settings.compactDashboard); - this.element.setAttribute( - "tabindex", - settings.profileCardEnabled ? "0" : "-1" - ); - if (settings.profileCardPosition) { - this.applyPosition(settings.profileCardPosition.x, settings.profileCardPosition.y); - } else if (!this.element.classList.contains("is-dragging")) { - this.element.style.left = ""; - this.element.style.top = ""; - this.element.style.right = ""; - this.element.style.bottom = ""; - } - } - if (!this.badgeEl || !this.subtitleEl || !this.chipsEl || !this.tipEl) { - return; - } - const mode = settings.themeMode; - this.badgeEl.setText(mode.replace(/([a-z])([A-Z])/g, "$1 $2")); - this.subtitleEl.setText(MODE_BLURB[mode]); - const pct = Math.round(settings.glowIntensity * 100); - const ascii = settings.asciiArtPreset === "off" ? "ASCII off" : `ASCII \xB7 ${settings.asciiArtPreset}`; - const motion = settings.reducedMotion ? "Calm motion" : "Glow pulse on"; - this.chipsEl.replaceChildren(); - const chipGlow = document.createElement("span"); - chipGlow.addClass("miku-profile-chip"); - chipGlow.addClass("miku-profile-chip--glow"); - chipGlow.setText(`Glow ${pct}%`); - const chipAscii = document.createElement("span"); - chipAscii.addClass("miku-profile-chip"); - chipAscii.setText(ascii); - const chipMotion = document.createElement("span"); - chipMotion.addClass("miku-profile-chip"); - chipMotion.setText(motion); - this.chipsEl.append(chipGlow, chipAscii, chipMotion); - const modes = [ - "MinimalMiku", - "Concert", - "NightNeon", - "SnowMiku" - ]; - const modeIdx = modes.indexOf(settings.themeMode); - const tipIdx = modeIdx >= 0 ? modeIdx : 0; - this.tipEl.setText(PROFILE_TIPS[tipIdx]); - } - tryOpenDashboard() { - void this.openDashboard().catch(() => { - new import_obsidian3.Notice("Use Command palette \u2192 Open Miku dashboard."); - }); - } - unmount() { - window.removeEventListener("pointermove", this.onPointerMove); - window.removeEventListener("pointerup", this.onPointerUp); - this.element?.remove(); - this.element = null; - this.badgeEl = null; - this.subtitleEl = null; - this.chipsEl = null; - this.tipEl = null; - } - onPointerDown(event) { - if (!this.element || event.button !== 0) { - return; - } - const rect = this.element.getBoundingClientRect(); - this.dragOffsetX = event.clientX - rect.left; - this.dragOffsetY = event.clientY - rect.top; - this.dragging = true; - this.movedWhileDragging = false; - this.element.classList.add("is-dragging"); - this.element.setPointerCapture(event.pointerId); - event.preventDefault(); - } - applyPosition(x, y) { - if (!this.element) { - return; - } - this.element.style.left = `${x}px`; - this.element.style.top = `${y}px`; - this.element.style.right = "auto"; - this.element.style.bottom = "auto"; - } -}; -var WidgetManager = class { - constructor(plugin, openDashboard, saveAndRefresh) { - this.widgets = [ - new StatusBarWidget(plugin), - new BannerWidget(plugin, saveAndRefresh), - new QuoteWidget(plugin, saveAndRefresh), - new ProfileCardWidget(plugin, openDashboard, saveAndRefresh) - ]; - } - mount() { - this.widgets.forEach((widget) => widget.mount()); - } - update(settings) { - this.widgets.forEach((widget) => widget.update(settings)); - } - unmount() { - this.widgets.forEach((widget) => widget.unmount()); - } -}; - -// src/modules.ts -var WidgetModule = class { - constructor(plugin, openDashboard, saveAndRefresh) { - this.widgetManager = new WidgetManager(plugin, openDashboard, saveAndRefresh); - } - mount() { - this.widgetManager.mount(); - } - update(settings) { - this.widgetManager.update(settings); - } - unmount() { - this.widgetManager.unmount(); - } -}; -var DashboardModule = class { - constructor(plugin) { - this.plugin = plugin; - this.panelView = null; - } - mount() { - this.plugin.registerView(MIKU_PANEL_VIEW, (leaf) => { - const host = this.plugin; - this.panelView = new MikuPanelView(leaf, host); - return this.panelView; - }); - } - update(settings) { - const leaves = this.plugin.app.workspace.getLeavesOfType(MIKU_PANEL_VIEW); - if (leaves.length > 0) { - const view = leaves[0]?.view; - if (view instanceof MikuPanelView) { - view.setSettings(settings); - } - } else if (this.panelView) { - this.panelView.setSettings(settings); - } - } - unmount() { - this.plugin.app.workspace.detachLeavesOfType(MIKU_PANEL_VIEW); - this.panelView = null; - } - getSettings() { - const plugin = this.plugin; - return plugin.settings ?? { ...DEFAULT_SETTINGS }; - } -}; -var ModuleRegistry = class { - constructor(plugin, openDashboard, saveAndRefresh) { - this.modules = [ - new WidgetModule(plugin, openDashboard, saveAndRefresh), - new DashboardModule(plugin) - ]; - } - async mountAll() { - for (const module2 of this.modules) { - await module2.mount(); - } - } - async updateAll(settings) { - for (const module2 of this.modules) { - await module2.update(settings); - } - } - async unmountAll() { - for (const module2 of this.modules) { - await module2.unmount(); - } - } -}; - -// src/theme-manager.ts -var ROOT_CLASS = "miku-plugin-enabled"; -var ThemeManager = class { - apply(settings) { - document.body.classList.add(ROOT_CLASS); - document.body.dataset.mikuTheme = settings.themeMode; - document.body.dataset.mikuReducedMotion = settings.reducedMotion ? "true" : "false"; - document.body.style.setProperty("--miku-glow-intensity", String(settings.glowIntensity)); - } - cleanup() { - document.body.classList.remove(ROOT_CLASS); - delete document.body.dataset.mikuTheme; - delete document.body.dataset.mikuReducedMotion; - document.body.style.removeProperty("--miku-glow-intensity"); - } -}; - -// src/main.ts -var MikuPlugin = class extends import_obsidian4.Plugin { - constructor() { - super(...arguments); - this.settings = { ...DEFAULT_SETTINGS }; - this.themeManager = new ThemeManager(); - this.moduleRegistry = null; - this.dashboardLeaf = null; - } - async onload() { - await this.loadPluginSettings(); - this.themeManager.apply(this.settings); - this.moduleRegistry = new ModuleRegistry( - this, - () => this.openDashboardView(), - () => this.saveAndRefresh() - ); - await this.moduleRegistry.mountAll(); - await this.moduleRegistry.updateAll(this.settings); - this.addRibbonIcon("sparkles", "Open Miku dashboard", () => { - void this.openDashboardView(); - }); - this.addCommand({ - id: "miku-cycle-theme", - name: "Cycle Miku theme mode", - callback: async () => { - await this.cycleTheme(); - } - }); - this.addCommand({ - id: "miku-open-dashboard", - name: "Open Miku dashboard", - callback: async () => { - await this.openDashboardView(); - } - }); - this.addSettingTab(new MikuSettingTab(this.app, this)); - } - onunload() { - void this.moduleRegistry?.unmountAll(); - this.moduleRegistry = null; - this.dashboardLeaf = null; - this.themeManager.cleanup(); - } - async saveAndRefresh() { - await this.saveData(this.settings); - this.themeManager.apply(this.settings); - await this.moduleRegistry?.updateAll(this.settings); - const openView = this.dashboardLeaf?.view; - if (openView instanceof MikuPanelView) { - openView.setSettings(this.settings); - } - } - async loadPluginSettings() { - const loaded = await this.loadData(); - this.settings = { ...DEFAULT_SETTINGS, ...loaded ?? {} }; - this.settings.asciiArtPreset = normalizeAsciiArtPreset(this.settings.asciiArtPreset); - } - async cycleTheme() { - const modes = ["MinimalMiku", "Concert", "NightNeon", "SnowMiku"]; - const index = modes.indexOf(this.settings.themeMode); - const next = modes[(index + 1) % modes.length]; - this.settings.themeMode = next; - await this.saveAndRefresh(); - new import_obsidian4.Notice(`Miku theme switched to ${next}`); - } - async openDashboardView() { - this.dashboardLeaf = this.app.workspace.getLeavesOfType(MIKU_PANEL_VIEW)[0] ?? null; - if (!this.dashboardLeaf) { - this.dashboardLeaf = this.app.workspace.getRightLeaf(false); - await this.dashboardLeaf?.setViewState({ - type: MIKU_PANEL_VIEW, - active: true - }); - } - if (this.dashboardLeaf) { - this.app.workspace.revealLeaf(this.dashboardLeaf); - const view = this.dashboardLeaf.view; - if (view instanceof MikuPanelView) { - view.setSettings(this.settings); - } - } - } -}; diff --git a/obsidian-miku-plugin/styles.css b/obsidian-miku-plugin/styles.css deleted file mode 100644 index 9bca9e2..0000000 --- a/obsidian-miku-plugin/styles.css +++ /dev/null @@ -1,989 +0,0 @@ -.miku-plugin-enabled .miku-status-widget { - color: var(--miku-text, #d7e3ea); - opacity: 0.9; -} - -.miku-status-widget.is-hidden, -.miku-top-banner.is-hidden, -.miku-quote-widget.is-hidden, -.miku-profile-card.is-hidden { - display: none; -} - -.miku-top-banner { - position: fixed; - top: 2.2rem; - left: 50%; - right: auto; - transform: translateX(-50%); - z-index: 26; - max-width: min(22rem, calc(100vw - 3rem)); - padding: 0.4rem 0.8rem; - border-radius: 999px; - color: var(--miku-text, #d7e3ea); - background: color-mix(in srgb, var(--miku-secondary-dark, #181c25) 80%, transparent); - border: 1px solid color-mix(in srgb, var(--miku-accent, #39c5bb) 52%, transparent); - box-shadow: - 0 0 0.75rem color-mix(in srgb, var(--miku-glow, #b5f5ee) 42%, transparent), - 0 0 1.5rem color-mix(in srgb, var(--miku-pop, #e12885) 28%, transparent); - cursor: grab; - user-select: none; -} - -.miku-top-banner.is-dragging { - cursor: grabbing; -} - -.miku-quote-widget { - position: fixed; - right: 1rem; - left: auto; - bottom: 2.25rem; - transform: none; - max-width: min(30rem, calc(100vw - 2rem)); - z-index: 30; - padding: 0.7rem 0.8rem; - border-radius: 0.8rem; - line-height: 1.4; - color: var(--miku-text, #d7e3ea); - background: color-mix(in srgb, var(--miku-bg, #0f1117) 78%, transparent); - border: 1px solid color-mix(in srgb, var(--miku-glow, #b5f5ee) 52%, transparent); - box-shadow: - 0 0 1rem color-mix(in srgb, var(--miku-glow, #b5f5ee) 38%, transparent), - 0 0 2rem color-mix(in srgb, var(--miku-accent, #40e0d0) 15%, transparent); - cursor: grab; - user-select: none; -} - -.miku-quote-widget.is-dragging { - cursor: grabbing; -} - -@media (max-width: 900px) { - .miku-top-banner { - left: 1rem; - right: 1rem; - top: 2.2rem; - transform: none; - max-width: calc(100vw - 2rem); - } - - .miku-quote-widget { - left: 1rem; - right: 1rem; - bottom: 2rem; - transform: none; - max-width: none; - } -} - -/* Bottom-left: keeps the top of the right split (ASCII + dashboard) readable */ -.miku-profile-card { - position: fixed; - left: 1rem; - right: auto; - top: auto; - bottom: 2.25rem; - width: 15rem; - max-width: min(15rem, calc(100vw - 2rem)); - z-index: 30; - display: grid; - gap: 0.35rem; - padding: 0.8rem; - border-radius: 0.9rem; - cursor: grab; - color: var(--miku-text, #d7e3ea); - background: color-mix(in srgb, var(--miku-secondary-dark, #181c25) 76%, transparent); - border: 1px solid color-mix(in srgb, var(--miku-accent, #40e0d0) 50%, transparent); - box-shadow: - 0 0 0.85rem color-mix(in srgb, var(--miku-glow, #b5f5ee) 35%, transparent), - 0 0 1.6rem color-mix(in srgb, var(--miku-pop, #e12885) 22%, transparent); - transition: - transform 140ms ease, - box-shadow 180ms ease; -} - -.miku-profile-card.is-dragging { - cursor: grabbing; -} - -.miku-profile-card:hover { - transform: translateY(-1px); - box-shadow: - 0 0 1rem color-mix(in srgb, var(--miku-glow, #b5f5ee) 45%, transparent), - 0 0 2rem color-mix(in srgb, var(--miku-pop, #e12885) 30%, transparent); -} - -.miku-profile-card:active { - transform: translateY(0); -} - -.miku-profile-card:focus-visible { - outline: 2px solid color-mix(in srgb, var(--miku-accent, #40e0d0) 85%, transparent); - outline-offset: 2px; -} - -.miku-profile-card.is-compact { - width: 12rem; - padding: 0.6rem; - gap: 0.25rem; -} - -.miku-profile-card h4 { - margin: 0; - font-size: 1rem; - font-weight: 700; - letter-spacing: -0.02em; - background: linear-gradient( - 120deg, - var(--miku-glow, #b5f5ee), - var(--miku-accent, #40e0d0), - var(--miku-pop, #e12885) - ); - -webkit-background-clip: text; - background-clip: text; - color: transparent; -} - -.miku-profile-card p { - margin: 0; -} - -.miku-profile-card__header { - display: flex; - align-items: baseline; - justify-content: space-between; - gap: 0.5rem; -} - -.miku-profile-badge { - flex-shrink: 0; - font-size: 0.65rem; - font-weight: 600; - text-transform: uppercase; - letter-spacing: 0.06em; - padding: 0.15rem 0.42rem; - border-radius: 999px; - color: color-mix(in srgb, var(--miku-secondary-dark, #181c25) 12%, var(--miku-text, #d7e3ea)); - background: color-mix(in srgb, var(--miku-accent, #40e0d0) 38%, transparent); - border: 1px solid color-mix(in srgb, var(--miku-pop, #e12885) 45%, transparent); - box-shadow: 0 0 10px color-mix(in srgb, var(--miku-glow, #b5f5ee) 28%, transparent); -} - -.miku-profile-card__subtitle { - font-size: 0.78rem; - line-height: 1.35; - opacity: 0.92; - color: color-mix(in srgb, var(--miku-text, #d7e3ea) 88%, var(--miku-glow, #b5f5ee)); -} - -.miku-profile-card__chips { - display: flex; - flex-wrap: wrap; - gap: 0.28rem; - margin-top: 0.1rem; -} - -.miku-profile-chip { - font-size: 0.65rem; - line-height: 1.2; - padding: 0.18rem 0.42rem; - border-radius: 0.42rem; - color: color-mix(in srgb, var(--miku-text, #d7e3ea) 92%, transparent); - background: color-mix(in srgb, var(--miku-secondary-dark, #181c25) 45%, transparent); - border: 1px solid color-mix(in srgb, var(--miku-glow, #b5f5ee) 28%, transparent); -} - -.miku-profile-chip--glow { - border-color: color-mix(in srgb, var(--miku-pop, #e12885) 42%, transparent); - box-shadow: 0 0 8px color-mix(in srgb, var(--miku-glow, #b5f5ee) 22%, transparent); -} - -.miku-profile-card__divider { - height: 1px; - margin: 0.05rem 0; - border-radius: 1px; - background: linear-gradient( - 90deg, - transparent, - color-mix(in srgb, var(--miku-accent, #40e0d0) 55%, transparent), - color-mix(in srgb, var(--miku-pop, #e12885) 40%, transparent), - transparent - ); -} - -.miku-profile-card__tip { - font-size: 0.72rem; - line-height: 1.35; - font-style: italic; - opacity: 0.88; -} - -.miku-profile-card__foot { - display: flex; - align-items: center; - gap: 0.45rem; - margin-top: 0.05rem; - padding-top: 0.2rem; -} - -.miku-profile-card__cta { - font-size: 0.7rem; - font-weight: 600; - letter-spacing: 0.03em; - color: color-mix(in srgb, var(--miku-glow, #b5f5ee) 82%, var(--text-accent, #fee440)); -} - -@media (prefers-reduced-motion: reduce) { - .miku-profile-card { - transition: none; - } - - .miku-profile-card:hover { - transform: none; - } -} - -.miku-profile-icon { - margin-top: 0.2rem; - color: var(--miku-glow, #b5f5ee); - opacity: 0.95; - filter: drop-shadow(0 0 0.35rem color-mix(in srgb, var(--miku-glow, #b5f5ee) 65%, transparent)) - drop-shadow(0 0 0.85rem color-mix(in srgb, var(--miku-pop, #e12885) 35%, transparent)); -} - -.miku-dashboard { - padding: 1rem; - color: var(--miku-text, #d7e3ea); -} - -.miku-dashboard.is-compact { - padding: 0.7rem; -} - -.miku-dashboard-actions { - display: flex; - gap: 0.5rem; - margin: 0.8rem 0; -} - -.miku-dashboard-actions button { - border-radius: 0.6rem; - border: 1px solid color-mix(in srgb, var(--miku-accent, #40e0d0) 58%, transparent); - box-shadow: - 0 0 0 1px color-mix(in srgb, var(--miku-pop, #e12885) 42%, transparent), - 0 0 22px color-mix(in srgb, var(--text-accent, #fee440) 32%, transparent), - 0 0 42px color-mix(in srgb, var(--miku-glow, #b5f5ee) 22%, transparent); - padding: 0.35rem 0.7rem; -} - -.miku-progress { - margin-top: 0.8rem; - padding: 0.7rem; - border-radius: 0.8rem; - background: color-mix(in srgb, var(--miku-secondary-dark, #181c25) 80%, transparent); -} - -.miku-checklist-actions { - display: flex; - gap: 0.45rem; - margin: 0.5rem 0 0.35rem; -} - -.miku-checklist-input { - min-width: 0; - flex: 1; - border-radius: 0.45rem; - padding: 0.22rem 0.45rem; -} - -.miku-checklist-actions button { - border-radius: 0.45rem; - padding: 0.2rem 0.45rem; - border: 1px solid color-mix(in srgb, var(--miku-accent, #40e0d0) 42%, transparent); - background: color-mix(in srgb, var(--miku-bg, #0f1117) 70%, transparent); -} - -.miku-checklist { - list-style: none; - margin: 0; - padding: 0; - display: grid; - gap: 0.35rem; -} - -.miku-checklist-item { - display: flex; - align-items: center; -} - -.miku-checklist-label { - display: flex; - align-items: center; - gap: 0.45rem; - font-size: 0.86rem; -} - -.miku-checklist-item.is-done span { - text-decoration: line-through; - opacity: 0.72; -} - -.miku-checklist-empty { - font-size: 0.8rem; - opacity: 0.78; -} - -.miku-equalizer { - margin-top: 1rem; - height: 2rem; - display: flex; - gap: 0.25rem; - align-items: end; -} - -.miku-eq-bar { - width: 0.35rem; - height: 100%; - border-radius: 999px; - background: color-mix(in srgb, var(--miku-accent, #40e0d0) 82%, var(--miku-glow, #b5f5ee)); - box-shadow: 0 0 0.55rem color-mix(in srgb, var(--miku-glow, #b5f5ee) 55%, transparent); - animation: mikuEq 1.1s ease-in-out infinite; -} - -.miku-eq-bar:nth-child(2n) { - animation-duration: 0.9s; -} - -.miku-eq-bar:nth-child(3n) { - animation-duration: 1.3s; -} - -@keyframes mikuEq { - 0%, - 100% { - transform: scaleY(0.35); - } - 50% { - transform: scaleY(0.95); - } -} - -/* Original ASCII motifs — bundled theme supplies --miku-* when plugin is enabled */ -.miku-ascii { - margin: 0 0 0.75rem; - padding: 0.5rem 0.75rem; - overflow-x: auto; - font-family: var(--font-monospace-theme), ui-monospace, "IBM Plex Mono", monospace; - font-size: 0.72rem; - line-height: 1.25; - color: var(--miku-accent, #86cecb); - text-shadow: - 0 0 0.45rem color-mix(in srgb, var(--miku-glow, #b5f5ee) 62%, transparent), - 0 0 1.1rem color-mix(in srgb, var(--miku-pop, #e12885) 45%, transparent); - background: color-mix(in srgb, var(--miku-secondary-dark, #1e2329) 55%, transparent); - border: 1px solid color-mix(in srgb, var(--miku-pop, #e12885) 45%, var(--miku-accent, #86cecb)); - box-shadow: - 0 0 1.1rem color-mix(in srgb, var(--miku-glow, #b5f5ee) 28%, transparent), - inset 0 0 1.25rem color-mix(in srgb, var(--miku-accent, #40e0d0) 12%, transparent); - border-radius: 0.65rem; -} - -.miku-dashboard.is-compact .miku-ascii { - font-size: 0.62rem; - padding: 0.35rem 0.55rem; - margin-bottom: 0.5rem; -} - -.miku-ascii--settings { - font-size: 0.68rem; - margin-bottom: 1rem; -} - -/* --- Miku hybrid theme (bundled from obsidian-miku-theme/theme.css) --- */ - -/* Combined theme for distribution. Mirrors src/colors.css + chrome / editor rules. */ - -/* - * Base: Color-Hex palette 19601 (gmbl777) — https://www.color-hex.com/color-palette/19601 - * #373b3e #bec8d1 #86cecb #137a7f #e12885 - * - * Extended “pop / decora-style” hues (approximate-inspired accent range; UI-tuned): - * teal/cyan hot #00e5ff · #40e0d0 · #00afb9 · pinks/magenta · sun yellow · violet · sky · plum - */ - -:root { - --palette-19601-1: #373b3e; - --palette-19601-2: #bec8d1; - --palette-19601-3: #86cecb; - --palette-19601-4: #137a7f; - --palette-19601-5: #e12885; - - --palette-pop-cyan: #40e0d0; - --palette-pop-cyan-hot: #00e5ff; - --palette-pop-teal-deep: #00afb9; - --palette-pop-pink-hot: #ff007f; - --palette-pop-pink-soft: #ff66cc; - --palette-pop-pink-muted: #f15bb5; - --palette-pop-sun: #fee440; - --palette-pop-sun-hot: #ffff00; - --palette-pop-violet: #9b5de5; - --palette-pop-plum: #540d6e; - --palette-pop-sky: #00bbf9; - --palette-pop-navy: #0081a7; - - --miku-cyan-neon: color-mix(in srgb, var(--palette-pop-cyan-hot) 55%, var(--palette-19601-3)); - --miku-hot-pink: color-mix(in srgb, var(--palette-pop-pink-hot) 35%, var(--palette-19601-5)); - --miku-sun: color-mix(in srgb, var(--palette-pop-sun) 70%, white); - --miku-violet-bright: color-mix(in srgb, var(--palette-pop-violet) 80%, white); - --miku-plum-soft: color-mix(in srgb, var(--palette-pop-plum) 25%, var(--palette-19601-1)); - - --miku-accent: var(--palette-pop-cyan); - --miku-deep-teal: var(--palette-19601-4); - --miku-bg: #12151a; - --miku-secondary-dark: color-mix(in srgb, var(--miku-plum-soft) 18%, #1e2329); - --miku-glow: color-mix(in srgb, var(--palette-pop-cyan-hot) 42%, white); - --miku-text: var(--palette-19601-2); - --miku-pop: var(--miku-hot-pink); - --miku-accent-warm: var(--palette-pop-pink-soft); - - --miku-muted: color-mix(in srgb, var(--palette-19601-2) 55%, var(--palette-19601-1)); - --miku-border-subtle: color-mix(in srgb, var(--palette-pop-violet) 22%, var(--palette-19601-1)); - - --miku-gradient-surface: linear-gradient( - 148deg, - color-mix(in srgb, var(--palette-pop-cyan-hot) 11%, var(--miku-bg)) 0%, - var(--miku-bg) 32%, - color-mix(in srgb, var(--palette-pop-violet) 9%, var(--miku-bg)) 58%, - color-mix(in srgb, var(--palette-pop-pink-muted) 7%, var(--miku-bg)) 82%, - color-mix(in srgb, var(--palette-pop-sun) 5%, var(--miku-bg)) 100% - ); - - --miku-tab-rail: color-mix(in srgb, var(--palette-pop-cyan-hot) 16%, transparent); - --miku-glow-intensity: 0.82; - - --miku-link-hover: color-mix(in srgb, var(--miku-hot-pink) 50%, var(--palette-pop-sky)); - --miku-heading-1: color-mix(in srgb, var(--palette-pop-cyan-hot) 75%, white); - --miku-heading-2: color-mix(in srgb, var(--palette-pop-violet) 82%, white); - --miku-heading-3: color-mix(in srgb, var(--palette-pop-sun) 45%, var(--miku-text)); - --miku-heading-4: color-mix(in srgb, var(--palette-pop-sky) 58%, var(--miku-text)); - --miku-heading-5: color-mix(in srgb, var(--palette-pop-pink-muted) 52%, var(--miku-text)); - --miku-heading-6: color-mix(in srgb, var(--palette-pop-plum) 32%, var(--miku-text)); -} - -body[data-miku-theme="MinimalMiku"] { - --miku-accent: color-mix(in srgb, var(--palette-19601-3) 40%, var(--palette-pop-cyan)); - --miku-deep-teal: var(--palette-19601-4); - --miku-bg: #12151a; - --miku-secondary-dark: color-mix(in srgb, var(--miku-plum-soft) 15%, #1f252c); - --miku-glow: color-mix(in srgb, var(--palette-pop-cyan-hot) 35%, white); - --miku-text: var(--palette-19601-2); - --miku-pop: var(--miku-hot-pink); - --miku-accent-warm: var(--palette-pop-pink-soft); - --miku-heading-3: color-mix(in srgb, var(--palette-pop-sun) 38%, var(--miku-text)); - --miku-heading-4: color-mix(in srgb, var(--palette-pop-sky) 44%, var(--miku-text)); - --miku-heading-5: color-mix(in srgb, var(--palette-19601-5) 42%, var(--miku-text)); - --miku-heading-6: color-mix(in srgb, var(--palette-pop-violet) 36%, var(--miku-muted)); - --miku-border-subtle: color-mix(in srgb, var(--palette-19601-3) 28%, var(--palette-pop-violet)); - - --miku-gradient-surface: linear-gradient( - 148deg, - color-mix(in srgb, var(--palette-pop-cyan-hot) 13%, var(--miku-bg)) 0%, - var(--miku-bg) 38%, - color-mix(in srgb, var(--palette-pop-violet) 10%, var(--miku-bg)) 62%, - color-mix(in srgb, var(--palette-19601-5) 10%, var(--miku-bg)) 88%, - color-mix(in srgb, var(--palette-pop-sun) 6%, var(--miku-bg)) 100% - ); - - --miku-tab-rail: color-mix(in srgb, var(--palette-pop-cyan-hot) 20%, transparent); -} - -body[data-miku-theme="Concert"] { - --miku-accent: color-mix(in srgb, var(--palette-pop-cyan-hot) 72%, white); - --miku-deep-teal: color-mix(in srgb, var(--palette-pop-teal-deep) 55%, var(--palette-19601-4)); - --miku-bg: #0f141c; - --miku-secondary-dark: #171f2e; - --miku-glow: #e8fff8; - --miku-text: #eef4fc; - --miku-pop: #ff62c6; - --miku-accent-warm: color-mix(in srgb, var(--palette-pop-pink-hot) 52%, white); - --miku-muted: color-mix(in srgb, var(--miku-text) 58%, var(--palette-pop-plum)); - - --miku-border-subtle: color-mix(in srgb, var(--palette-pop-violet) 38%, var(--palette-19601-1)); - - --miku-gradient-surface: linear-gradient( - 158deg, - color-mix(in srgb, var(--palette-pop-cyan-hot) 22%, var(--miku-bg)) 0%, - color-mix(in srgb, var(--palette-pop-pink-soft) 6%, var(--miku-bg)) 28%, - var(--miku-bg) 48%, - color-mix(in srgb, var(--palette-pop-sun-hot) 7%, var(--miku-bg)) 72%, - color-mix(in srgb, var(--palette-pop-violet) 14%, var(--miku-bg)) 100% - ); - - --miku-tab-rail: color-mix(in srgb, var(--palette-pop-pink-soft) 32%, transparent); - --miku-heading-1: color-mix(in srgb, var(--palette-pop-cyan-hot) 88%, white); - --miku-heading-2: color-mix(in srgb, var(--palette-pop-violet) 88%, white); - --miku-heading-3: color-mix(in srgb, var(--palette-pop-sun-hot) 42%, white); - --miku-heading-4: color-mix(in srgb, var(--palette-pop-sky) 82%, white); - --miku-heading-5: color-mix(in srgb, var(--palette-pop-pink-soft) 76%, white); - --miku-heading-6: color-mix(in srgb, var(--palette-pop-plum) 58%, white); -} - -body[data-miku-theme="NightNeon"] { - --miku-accent: color-mix(in srgb, var(--palette-pop-cyan-hot) 48%, var(--palette-pop-teal-deep)); - --miku-deep-teal: #065a61; - --miku-bg: #0b0e14; - --miku-secondary-dark: color-mix(in srgb, var(--palette-pop-plum) 22%, #141a22); - --miku-glow: color-mix(in srgb, var(--palette-pop-cyan-hot) 65%, cyan); - --miku-text: #d6e6f8; - --miku-pop: color-mix(in srgb, var(--palette-pop-pink-hot) 72%, magenta); - --miku-accent-warm: var(--palette-pop-pink-soft); - --miku-muted: color-mix(in srgb, var(--miku-text) 45%, var(--palette-pop-plum)); - - --miku-border-subtle: color-mix(in srgb, var(--palette-pop-violet) 45%, black); - - --miku-gradient-surface: radial-gradient( - 120% 80% at 50% -20%, - color-mix(in srgb, var(--palette-pop-cyan-hot) 14%, transparent), - transparent 55% - ), - linear-gradient( - 172deg, - var(--miku-bg) 0%, - color-mix(in srgb, var(--palette-pop-plum) 35%, var(--miku-bg)) 45%, - color-mix(in srgb, var(--palette-pop-pink-hot) 9%, var(--miku-bg)) 100% - ); - - --miku-tab-rail: color-mix(in srgb, var(--palette-pop-cyan-hot) 30%, transparent); - --miku-heading-1: color-mix(in srgb, var(--palette-pop-cyan-hot) 70%, white); - --miku-heading-2: color-mix(in srgb, var(--palette-pop-violet) 75%, white); - --miku-heading-3: color-mix(in srgb, var(--miku-pop) 52%, white); - --miku-heading-4: color-mix(in srgb, var(--palette-pop-cyan-hot) 64%, white); - --miku-heading-5: color-mix(in srgb, var(--palette-pop-pink-hot) 62%, white); - --miku-heading-6: color-mix(in srgb, var(--palette-pop-violet) 72%, white); -} - -body[data-miku-theme="SnowMiku"] { - --miku-accent: color-mix(in srgb, #d4fffb 58%, var(--palette-pop-teal-deep)); - --miku-deep-teal: color-mix(in srgb, var(--palette-pop-teal-deep) 30%, white); - --miku-bg: #1e2830; - --miku-secondary-dark: #2a3540; - --miku-glow: color-mix(in srgb, var(--palette-pop-cyan) 45%, white); - --miku-text: color-mix(in srgb, var(--palette-pop-sky) 8%, white); - --miku-pop: color-mix(in srgb, var(--palette-pop-pink-soft) 65%, pink); - --miku-accent-warm: color-mix(in srgb, var(--palette-pop-sun) 22%, white); - --miku-muted: color-mix(in srgb, var(--miku-text) 52%, var(--palette-pop-navy)); - - --miku-border-subtle: color-mix(in srgb, var(--palette-19601-2) 32%, var(--palette-pop-violet)); - - --miku-gradient-surface: linear-gradient( - 158deg, - color-mix(in srgb, var(--palette-pop-sky) 12%, var(--miku-bg)) 0%, - var(--miku-bg) 40%, - color-mix(in srgb, white 10%, color-mix(in srgb, var(--palette-pop-pink-soft) 12%, var(--miku-bg))) 70%, - color-mix(in srgb, var(--palette-pop-sun) 8%, var(--miku-bg)) 100% - ); - - --miku-tab-rail: color-mix(in srgb, var(--palette-pop-sky) 24%, transparent); - --miku-heading-1: color-mix(in srgb, var(--palette-pop-cyan) 88%, white); - --miku-heading-2: color-mix(in srgb, var(--palette-pop-violet) 55%, white); - --miku-heading-3: color-mix(in srgb, var(--palette-pop-sun) 35%, white); - --miku-heading-4: color-mix(in srgb, var(--palette-pop-sky) 68%, white); - --miku-heading-5: color-mix(in srgb, var(--palette-pop-pink-soft) 62%, white); - --miku-heading-6: color-mix(in srgb, var(--palette-pop-plum) 40%, white); -} - -body.miku-plugin-enabled { - --font-text-theme: "Inter", "Noto Sans", sans-serif; - --font-interface-theme: "Inter", "Noto Sans", sans-serif; - --line-height-normal: 1.7; - --background-primary: var(--miku-bg); - --background-secondary: var(--miku-secondary-dark); - --background-modifier-border: var(--miku-border-subtle); - --interactive-accent: var(--miku-accent); - --interactive-accent-hover: color-mix(in srgb, var(--miku-pop) 62%, var(--miku-accent)); - --background-modifier-active: color-mix(in srgb, var(--miku-accent) 22%, transparent); - --text-normal: var(--miku-text); - --text-muted: var(--miku-muted); - --text-faint: color-mix(in srgb, var(--miku-muted) 85%, transparent); - --icon-color: color-mix(in srgb, var(--miku-accent) 72%, var(--miku-text)); - --text-accent: var(--miku-sun); - --text-on-accent: #1a1620; - --link-color: color-mix(in srgb, var(--palette-pop-sky) 55%, white); - --link-color-hover: var(--miku-link-hover); - --miku-file-title: color-mix( - in srgb, - color-mix(in srgb, var(--miku-accent) 38%, var(--miku-pop)) 74%, - var(--miku-text) - ); - --blockquote-border-color: color-mix(in srgb, var(--miku-accent-warm) 22%, transparent); - --italic-color: color-mix( - in srgb, - var(--palette-pop-sky) 38%, - color-mix(in srgb, var(--miku-accent-warm) 42%, var(--miku-text)) - ); - --bold-color: color-mix( - in srgb, - var(--palette-pop-pink-soft) 44%, - color-mix(in srgb, var(--miku-heading-2) 52%, white) - ); - --miku-code-inline: color-mix( - in srgb, - var(--palette-pop-cyan-hot) 42%, - color-mix(in srgb, var(--miku-glow) 35%, var(--miku-text)) - ); - --miku-code-inline-bg: color-mix( - in srgb, - var(--palette-pop-violet) 22%, - var(--miku-secondary-dark) - ); - --miku-code-block-fg: color-mix( - in srgb, - var(--miku-text) 72%, - color-mix(in srgb, var(--palette-pop-cyan) 48%, white) - ); - --miku-code-block-bg: color-mix( - in srgb, - var(--palette-pop-plum) 16%, - color-mix(in srgb, var(--miku-bg) 92%, var(--miku-deep-teal)) - ); - --miku-code-block-border: color-mix( - in srgb, - var(--palette-pop-cyan-hot) 32%, - transparent - ); - --code-normal: var(--miku-code-inline); -} - -body.miku-plugin-enabled .markdown-preview-view { - color: var(--miku-text); - line-height: 1.8; -} - -body.miku-plugin-enabled .markdown-preview-view h1, -body.miku-plugin-enabled .markdown-rendered h1 { - color: var(--miku-heading-1); -} - -body.miku-plugin-enabled .markdown-preview-view h2, -body.miku-plugin-enabled .markdown-rendered h2 { - color: var(--miku-heading-2); -} - -body.miku-plugin-enabled .markdown-preview-view h3, -body.miku-plugin-enabled .markdown-rendered h3 { - color: var(--miku-heading-3); -} - -body.miku-plugin-enabled .markdown-preview-view h4, -body.miku-plugin-enabled .markdown-rendered h4 { - color: var(--miku-heading-4); -} - -body.miku-plugin-enabled .markdown-preview-view h5, -body.miku-plugin-enabled .markdown-rendered h5 { - color: var(--miku-heading-5); -} - -body.miku-plugin-enabled .markdown-preview-view h6, -body.miku-plugin-enabled .markdown-rendered h6 { - color: var(--miku-heading-6); -} - -body.miku-plugin-enabled .markdown-preview-view em, -body.miku-plugin-enabled .markdown-rendered em { - color: var(--italic-color); -} - -body.miku-plugin-enabled .markdown-preview-view strong, -body.miku-plugin-enabled .markdown-rendered strong { - color: var(--bold-color); -} - -body.miku-plugin-enabled .markdown-preview-view :not(pre) > code, -body.miku-plugin-enabled .markdown-rendered :not(pre) > code { - color: var(--miku-code-inline); - background-color: var(--miku-code-inline-bg); - padding: 0.12em 0.38em; - border-radius: 0.35rem; - border: 1px solid color-mix(in srgb, var(--palette-pop-cyan-hot) 22%, transparent); - box-shadow: 0 0 0.55rem color-mix(in srgb, var(--miku-glow) 12%, transparent); -} - -body.miku-plugin-enabled .markdown-preview-view pre, -body.miku-plugin-enabled .markdown-rendered pre { - color: var(--miku-code-block-fg); - background-color: var(--miku-code-block-bg); - border: 1px solid var(--miku-code-block-border); - border-radius: 0.55rem; - box-shadow: - 0 0 0 1px color-mix(in srgb, var(--palette-pop-pink-muted) 12%, transparent), - 0 0 1.1rem color-mix(in srgb, var(--palette-pop-violet) 10%, transparent); -} - -body.miku-plugin-enabled .markdown-preview-view pre code, -body.miku-plugin-enabled .markdown-rendered pre code { - color: inherit; - background-color: transparent; - border: none; - padding: 0; - box-shadow: none; -} - -body.miku-plugin-enabled .cm-content .cm-header-1, -body.miku-plugin-enabled .HyperMD-header-1 { - color: var(--miku-heading-1); -} - -body.miku-plugin-enabled .cm-content .cm-header-2, -body.miku-plugin-enabled .HyperMD-header-2 { - color: var(--miku-heading-2); -} - -body.miku-plugin-enabled .cm-content .cm-header-3, -body.miku-plugin-enabled .HyperMD-header-3 { - color: var(--miku-heading-3); -} - -body.miku-plugin-enabled .cm-content .cm-header-4, -body.miku-plugin-enabled .HyperMD-header-4 { - color: var(--miku-heading-4); -} - -body.miku-plugin-enabled .cm-content .cm-header-5, -body.miku-plugin-enabled .HyperMD-header-5 { - color: var(--miku-heading-5); -} - -body.miku-plugin-enabled .cm-content .cm-header-6, -body.miku-plugin-enabled .HyperMD-header-6 { - color: var(--miku-heading-6); -} - -body.miku-plugin-enabled .cm-content .cm-strong { - color: var(--bold-color); -} - -body.miku-plugin-enabled .cm-content .cm-em { - color: var(--italic-color); -} - -body.miku-plugin-enabled .cm-content .cm-inline-code { - color: var(--miku-code-inline); - background-color: var(--miku-code-inline-bg); - border-radius: 0.32rem; - padding: 0.05em 0.26em; - border: 1px solid color-mix(in srgb, var(--palette-pop-cyan-hot) 26%, transparent); - box-shadow: 0 0 0.5rem color-mix(in srgb, var(--miku-glow) 10%, transparent); -} - -body.miku-plugin-enabled .markdown-source-view.mod-cm6 .HyperMD-codeblock { - color: var(--miku-code-block-fg); - background-color: color-mix( - in srgb, - var(--miku-code-block-bg) 88%, - var(--background-primary) - ); - border-left: 3px solid color-mix(in srgb, var(--palette-pop-pink-soft) 45%, transparent); - border-radius: 0 0.4rem 0.4rem 0; -} - -body.miku-plugin-enabled .markdown-source-view.mod-cm6 .HyperMD-codeblock-begin { - border-top-left-radius: 0.4rem; - border-top-right-radius: 0.4rem; -} - -body.miku-plugin-enabled .markdown-source-view.mod-cm6 .HyperMD-codeblock-end { - border-bottom-left-radius: 0.4rem; - border-bottom-right-radius: 0.4rem; -} - -body.miku-plugin-enabled .cm-content span.cm-highlight { - background-color: color-mix(in srgb, var(--palette-pop-sun) 35%, transparent); - color: color-mix(in srgb, var(--miku-text) 92%, black); -} - -body.miku-plugin-enabled .markdown-preview-view mark { - background-color: color-mix(in srgb, var(--palette-pop-sun-hot) 22%, transparent); - color: var(--text-normal); -} - -body.miku-plugin-enabled .markdown-preview-view hr { - border-color: color-mix(in srgb, var(--palette-pop-violet) 35%, transparent); - border-top-color: color-mix(in srgb, var(--miku-accent) 40%, transparent); - opacity: 0.85; -} - -body.miku-plugin-enabled .markdown-preview-view a, -body.miku-plugin-enabled .cm-underline.cm-hmd-internal-link.cm-link, -body.miku-plugin-enabled .cm-underline.cm-link { - color: var(--link-color); -} - -body.miku-plugin-enabled .markdown-preview-view .internal-link:hover, -body.miku-plugin-enabled .cm-link:hover { - color: var(--link-color-hover); -} - -body.miku-plugin-enabled .markdown-rendered .tag { - border-width: 1px; -} - -body.miku-plugin-enabled .markdown-rendered .tag:nth-of-type(3n + 1) { - background-color: color-mix(in srgb, var(--miku-accent) 18%, transparent); - border-color: color-mix(in srgb, var(--palette-pop-cyan-hot) 50%, transparent); - color: color-mix(in srgb, var(--miku-heading-1) 55%, white); -} - -body.miku-plugin-enabled .markdown-rendered .tag:nth-of-type(3n + 2) { - background-color: color-mix(in srgb, var(--miku-pop) 22%, transparent); - border-color: color-mix(in srgb, var(--palette-pop-pink-soft) 45%, transparent); - color: color-mix(in srgb, white 82%, var(--miku-pop)); -} - -body.miku-plugin-enabled .markdown-rendered .tag:nth-of-type(3n + 3) { - background-color: color-mix(in srgb, var(--palette-pop-violet) 26%, transparent); - border-color: color-mix(in srgb, var(--miku-violet-bright) 45%, transparent); - color: color-mix(in srgb, white 88%, var(--palette-pop-violet)); -} - -body.miku-plugin-enabled .cm-content { - line-height: 1.7; -} - -@keyframes mikuGlowPulse { - 0%, - 100% { - box-shadow: 0 0 0.65rem color-mix(in srgb, var(--miku-glow) 52%, transparent), - 0 0 1.35rem color-mix(in srgb, var(--miku-pop) 22%, transparent), - 0 0 2rem color-mix(in srgb, var(--miku-glow) 12%, transparent); - } - 50% { - box-shadow: 0 0 1.05rem color-mix(in srgb, var(--miku-glow) 68%, transparent), - 0 0 1.75rem color-mix(in srgb, var(--miku-pop) 38%, transparent), - 0 0 2.6rem color-mix(in srgb, var(--miku-accent) 18%, transparent); - } -} - -body.miku-plugin-enabled[data-miku-reduced-motion="true"] * { - animation-duration: 0.001ms !important; - animation-iteration-count: 1 !important; - transition-duration: 0.001ms !important; -} - -body.miku-plugin-enabled .workspace { - background: var(--miku-gradient-surface); -} - -body.miku-plugin-enabled .workspace-split.mod-root .workspace-tab-header-container { - border-bottom: 1px solid var(--miku-tab-rail); - box-shadow: 0 1px 0 color-mix(in srgb, var(--miku-pop) 14%, transparent), - 0 8px 24px color-mix(in srgb, var(--palette-pop-violet) 6%, transparent); -} - -body.miku-plugin-enabled .workspace-tab-header.is-active .workspace-tab-header-inner-title { - color: var(--miku-file-title); - text-shadow: - 0 0 0.55rem color-mix(in srgb, var(--miku-pop) 48%, transparent), - 0 0 1rem color-mix(in srgb, var(--miku-glow) 26%, transparent); -} - -body.miku-plugin-enabled .workspace-tab-header .workspace-tab-header-inner-title, -body.miku-plugin-enabled .nav-file-title-content, -body.miku-plugin-enabled .view-header-title { - color: var(--miku-file-title); -} - -body.miku-plugin-enabled .workspace-tab-header.is-active { - box-shadow: 0 -2px 0 0 color-mix(in srgb, var(--miku-accent) 75%, transparent) inset, - 0 -4px 0 0 color-mix(in srgb, var(--miku-pop) 35%, transparent) inset, - 0 -7px 0 0 color-mix(in srgb, var(--miku-sun) 28%, transparent) inset; -} - -body.miku-plugin-enabled .nav-folder-title:hover, -body.miku-plugin-enabled .vertical-tab-nav-item:hover { - background-color: color-mix(in srgb, var(--miku-accent) 10%, transparent); -} - -body.miku-plugin-enabled .nav-folder .nav-folder-title-content { - font-weight: 600; -} - -body.miku-plugin-enabled .nav-folder:nth-of-type(6n + 1) > .nav-folder-title .nav-folder-title-content { - color: color-mix(in srgb, var(--palette-pop-cyan-hot) 74%, var(--miku-text)); -} - -body.miku-plugin-enabled .nav-folder:nth-of-type(6n + 2) > .nav-folder-title .nav-folder-title-content { - color: color-mix(in srgb, var(--palette-pop-sky) 72%, var(--miku-text)); -} - -body.miku-plugin-enabled .nav-folder:nth-of-type(6n + 3) > .nav-folder-title .nav-folder-title-content { - color: color-mix(in srgb, var(--palette-pop-violet) 66%, var(--miku-text)); -} - -body.miku-plugin-enabled .nav-folder:nth-of-type(6n + 4) > .nav-folder-title .nav-folder-title-content { - color: color-mix(in srgb, var(--palette-pop-pink-soft) 64%, var(--miku-text)); -} - -body.miku-plugin-enabled .nav-folder:nth-of-type(6n + 5) > .nav-folder-title .nav-folder-title-content { - color: color-mix(in srgb, var(--palette-pop-sun) 56%, var(--miku-text)); -} - -body.miku-plugin-enabled .nav-folder:nth-of-type(6n + 6) > .nav-folder-title .nav-folder-title-content { - color: color-mix(in srgb, var(--palette-pop-teal-deep) 64%, var(--miku-text)); -} - -body.miku-plugin-enabled textarea, -body.miku-plugin-enabled input[type="text"], -body.miku-plugin-enabled .dropdown { - border-color: var(--miku-border-subtle); -} - -body.miku-plugin-enabled .workspace-split.mod-root .workspace-tab-header-container, -body.miku-plugin-enabled .workspace-sidedock { - backdrop-filter: blur(14px); - background: linear-gradient( - 178deg, - color-mix(in srgb, var(--miku-secondary-dark) 86%, transparent) 0%, - color-mix(in srgb, var(--miku-accent) 3%, transparent) 100% - ); -} - -body.miku-plugin-enabled .workspace-tab-header.is-active, -body.miku-plugin-enabled .clickable-icon:hover, -body.miku-plugin-enabled .nav-file-title:hover { - animation: mikuGlowPulse 2.6s ease-in-out infinite; -} - -body.miku-plugin-enabled .markdown-preview-view .callout { - border-radius: 0.8rem; - border-style: solid; - border-width: 1px; - border-color: color-mix(in srgb, var(--miku-accent) 45%, var(--palette-pop-violet)); - border-left-width: 4px; - border-left-color: var(--miku-accent-warm); - background: linear-gradient( - 145deg, - color-mix(in srgb, var(--miku-secondary-dark) 88%, transparent) 0%, - color-mix(in srgb, var(--palette-pop-violet) 5%, var(--miku-secondary-dark)) 100% - ); - box-shadow: 2px 0 0 color-mix(in srgb, var(--palette-pop-cyan-hot) 18%, transparent) inset, - -1px 0 0 color-mix(in srgb, var(--miku-pop) 12%, transparent) inset; -} - -body.miku-plugin-enabled input[type="checkbox"]:checked { - background-color: var(--miku-accent); - border-color: color-mix(in srgb, var(--palette-pop-violet) 42%, transparent); -} - -body.miku-plugin-enabled .graph-view.color-fill-highlight { - color: color-mix(in srgb, var(--miku-glow) calc(var(--miku-glow-intensity, 0.55) * 100%), var(--miku-pop)); -} - -body.miku-plugin-enabled .graph-view.color-line { - color: color-mix(in srgb, var(--miku-accent) 45%, var(--palette-pop-violet)); -} - -body.miku-plugin-enabled .graph-view.graph-controls .graph-expand-button svg { - color: color-mix(in srgb, var(--miku-accent) 60%, white); -} diff --git a/obsidian-miku-theme/README.md b/obsidian-miku-theme/README.md index 1faabed..8732ad8 100644 --- a/obsidian-miku-theme/README.md +++ b/obsidian-miku-theme/README.md @@ -1,29 +1,20 @@ -# Obsidian Miku Theme Hybrid Base +# Theme CSS source (contributors) -Visual companion theme for the Miku plugin hybrid architecture. +Palette and workspace styling for **Miku Hybrid**. This folder is **not** a separate Community install for end users. -## Includes +Release builds merge [`theme.css`](theme.css) into the plugin’s `styles.css` via `obsidian-miku-plugin/scripts/merge-styles.mjs`. -- Core palette tokens: Color-Hex [19601](https://www.color-hex.com/color-palette/19601) plus extended **pop** hues (cyan `#00e5ff`/`#40e0d0`, hot pink `#ff007f`, sun `#fee440`, violet `#9b5de5`, plum `#540d6e`, sky `#00bbf9`) mapped into gradients, headings, tags, tabs, and callouts -- Typography tuning -- Glass-like sidebar and panel styling -- Subtle glow animation -- Callout and checkbox accents -- Graph view color treatment +## Editing -## Theme Modes +- Source modules: `src/*.css` +- Shipped bundle: `theme.css` (keep in sync when editing `src/`) -The theme reacts to `body[data-miku-theme]` values set by the plugin: +## Theme modes -- `MinimalMiku` -- `Concert` -- `NightNeon` -- `SnowMiku` +CSS reacts to `body[data-miku-theme]` set by the plugin: -## No-Audio Policy +- `MinimalMiku`, `Concert`, `NightNeon`, `SnowMiku` -This theme package contains no audio assets or audio-related functionality. +## No-audio policy -## Packaging - -Ship `theme.css` and `manifest.json` for releases. +No audio assets or audio-related functionality. diff --git a/package-lock.json b/package-lock.json deleted file mode 100644 index 5f2684e..0000000 --- a/package-lock.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "name": "mitsu-plugin", - "lockfileVersion": 3, - "requires": true, - "packages": {} -} diff --git a/qa/install-to-vault.sh b/qa/install-to-vault.sh index 541c8fa..6e14d07 100755 --- a/qa/install-to-vault.sh +++ b/qa/install-to-vault.sh @@ -2,22 +2,21 @@ set -euo pipefail ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" -VAULT_PATH="${1:-$ROOT_DIR/qa/vaults/clean-desktop}" +VAULT_PATH="${1:-$ROOT_DIR/qa/vaults/example-vault}" PLUGIN_SRC="$ROOT_DIR/obsidian-miku-plugin" PLUGIN_DST="$VAULT_PATH/.obsidian/plugins/miku-plugin-hybrid" THEME_DST_DIR="$VAULT_PATH/.obsidian/themes" THEME_DST_FILE="$THEME_DST_DIR/Miku Theme Hybrid Base.css" -ANCHOR_NOTE="$VAULT_PATH/README.md" INSTALL_SPLIT_THEME="${MIKU_INSTALL_SPLIT_THEME:-0}" -mkdir -p "$PLUGIN_DST" - -if [[ ! -f "$ANCHOR_NOTE" ]]; then - echo "# QA vault anchor" >>"$ANCHOR_NOTE" - echo "This file lets \`qa/launch-obsidian.sh\` open this folder via obsidian:// (see qa/test-environment.md)." >>"$ANCHOR_NOTE" +if [[ ! -f "$PLUGIN_SRC/main.js" || ! -f "$PLUGIN_SRC/styles.css" ]]; then + echo "Missing build artifacts. Run: npm run build --prefix ./obsidian-miku-plugin" >&2 + exit 1 fi +mkdir -p "$PLUGIN_DST" "$VAULT_PATH" + MANIFEST_SRC="$ROOT_DIR/manifest.json" if [[ ! -f "$MANIFEST_SRC" ]]; then MANIFEST_SRC="$PLUGIN_SRC/manifest.json" @@ -36,11 +35,8 @@ echo "Installed plugin into: $VAULT_PATH" echo "Plugin path: $PLUGIN_DST" if [[ "$INSTALL_SPLIT_THEME" == "1" ]]; then echo "Theme file (split dev mode): $THEME_DST_FILE" - echo " Settings → Appearance → Theme → \"Miku Theme Hybrid Base\"" else - echo "Bundled hybrid CSS in plugin styles.css (catalog-style install)." + echo "Bundled hybrid CSS in plugin styles.css." fi -echo "URI anchor note: $ANCHOR_NOTE" echo "" -echo "Next in Obsidian:" -echo " Settings → Community plugins → enable \"Miku Hybrid\"." +echo "Open the vault in Obsidian, then enable \"Miku Hybrid\" under Community plugins." diff --git a/qa/launch-obsidian.sh b/qa/launch-obsidian.sh deleted file mode 100755 index d5e2708..0000000 --- a/qa/launch-obsidian.sh +++ /dev/null @@ -1,80 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" - -if [[ $# -lt 1 ]]; then - echo "Usage: $0 " - exit 1 -fi - -VAULT_PATH="$(realpath "$1")" - -if [[ ! -d "$VAULT_PATH" ]]; then - echo "Vault path does not exist: $VAULT_PATH" - exit 1 -fi - -ANCHOR="$VAULT_PATH/README.md" - -EXTRA_ARGS=() -if [[ "${EUID:-$(id -u)}" -eq 0 ]]; then - EXTRA_ARGS+=("--no-sandbox") -fi - -# Obsidian's registered CLI (~/.local/bin/obsidian, often earlier on PATH) expects -# `obsidian [options]`; the distro wrapper runs Electron with the same argv. -# Newer builds treat that first positional after flags as an Obsidian CLI *command*, -# so a vault path yields: Command ".../vault" not found. -# Opening by filesystem path reliably uses the URI handler instead. -launch_uri() { - local path="$1" - local encoded uri - if ! command -v python3 >/dev/null 2>&1; then - echo "python3 is required to build obsidian://open URLs (URI percent-encoding)." >&2 - return 1 - fi - encoded="$(python3 -c 'import urllib.parse,sys; print(urllib.parse.quote(sys.argv[1], safe=""))' "$path")" - uri="obsidian://open?path=${encoded}" - case "$(uname -s)" in - Darwin) - open "$uri" - ;; - *) - if ! command -v xdg-open >/dev/null 2>&1; then - echo "xdg-open not found; cannot open URI on this OS." >&2 - return 1 - fi - xdg-open "$uri" - ;; - esac -} - -fallback_argv_gui() { - if [[ -n "${OBSIDIAN_GUI:-}" ]]; then - : - elif [[ -x "/usr/bin/obsidian" ]]; then - OBSIDIAN_GUI="/usr/bin/obsidian" - else - OBSIDIAN_GUI="obsidian" - fi - exec env -u ELECTRON_RUN_AS_NODE "${OBSIDIAN_GUI}" "${EXTRA_ARGS[@]}" "${VAULT_PATH}" -} - -# Cursor/automation shells may export ELECTRON_RUN_AS_NODE=1, which breaks Electron -# when spawning the GUI launcher directly — not consulted for xdg-open URI opens. -if [[ "${MIKU_OBSIDIAN_LEGACY_ARGV_LAUNCH:-0}" == "1" ]]; then - fallback_argv_gui -fi - -if [[ ! -f "$ANCHOR" ]]; then - echo "Missing $ANCHOR — run ./qa/install-to-vault.sh \"$VAULT_PATH\" first (needed for obsidian://open anchor)." >&2 - exit 1 -fi - -if [[ "${MIKU_SKIP_OBSIDIAN_REGISTER:-0}" != "1" ]]; then - python3 "$SCRIPT_DIR/register-obsidian-vault.py" "$VAULT_PATH" -fi - -launch_uri "$ANCHOR" -exit 0 diff --git a/qa/lifecycle-perf-check.sh b/qa/lifecycle-perf-check.sh deleted file mode 100755 index 28cd252..0000000 --- a/qa/lifecycle-perf-check.sh +++ /dev/null @@ -1,29 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" - -echo "Running lifecycle/performance guard checks..." - -# Ensure plugin lifecycle calls registry mount/unmount. -rg "mountAll\\(\\)" "$ROOT_DIR/obsidian-miku-plugin/src/main.ts" >/dev/null -rg "unmountAll\\(\\)" "$ROOT_DIR/obsidian-miku-plugin/src/main.ts" >/dev/null - -# Ensure quote interval is cleaned up. -rg "clearInterval" "$ROOT_DIR/obsidian-miku-plugin/src/widgets.ts" >/dev/null -rg "stopRotation\\(\\)" "$ROOT_DIR/obsidian-miku-plugin/src/widgets.ts" >/dev/null - -# Ensure dashboard leaves are detached on unmount path. -rg "detachLeavesOfType\\(MIKU_PANEL_VIEW\\)" "$ROOT_DIR/obsidian-miku-plugin/src/modules.ts" >/dev/null - -# Basic anti-polling guard. -if rg "setInterval\\(|setTimeout\\(" "$ROOT_DIR/obsidian-miku-plugin/src" >/dev/null; then - # Allow quote widget interval only. - COUNT="$(rg "setInterval\\(|setTimeout\\(" "$ROOT_DIR/obsidian-miku-plugin/src" | wc -l | tr -d ' ')" - if [[ "$COUNT" -gt 1 ]]; then - echo "Unexpected repeated timers detected: $COUNT" - exit 1 - fi -fi - -echo "Lifecycle/performance guards passed." diff --git a/qa/register-obsidian-vault.py b/qa/register-obsidian-vault.py deleted file mode 100644 index 13b7678..0000000 --- a/qa/register-obsidian-vault.py +++ /dev/null @@ -1,109 +0,0 @@ -#!/usr/bin/env python3 -"""Ensure a vault folder is listed in ~/.config/obsidian/obsidian.json (Linux/XDG paths). - -Obsidian rejects obsidian://open?path=... with \"Vault not found\" when the folder -was never opened via Manage Vaults, because path resolution uses the registered vault list. -""" - -from __future__ import annotations - -import json -import os -import platform -import secrets -import sys -import time -from pathlib import Path - - -def obsidian_config_file() -> Path: - home = Path.home() - system = platform.system() - if system == "Darwin": - return home / "Library" / "Application Support" / "obsidian" / "obsidian.json" - if system == "Windows": - appdata = os.environ.get("APPDATA") - if not appdata: - raise SystemExit("APPDATA not set; cannot locate Obsidian config on Windows") - return Path(appdata) / "Obsidian" / "obsidian.json" - cfg = Path(os.environ.get("XDG_CONFIG_HOME", home / ".config")) - return cfg / "obsidian" / "obsidian.json" - - -def main() -> None: - if len(sys.argv) != 2: - print(f"Usage: {sys.argv[0]} ", file=sys.stderr) - raise SystemExit(2) - vault = Path(sys.argv[1]).expanduser().resolve(strict=True) - if not vault.is_dir(): - print(f"Not a directory: {vault}", file=sys.stderr) - raise SystemExit(1) - - config_path = obsidian_config_file() - config_path.parent.mkdir(parents=True, exist_ok=True) - - if config_path.exists(): - data = json.loads(config_path.read_text(encoding="utf-8")) - else: - data = {"vaults": {}} - - vaults = data.setdefault("vaults", {}) - if not isinstance(vaults, dict): - print("'vaults' in obsidian.json is not an object", file=sys.stderr) - raise SystemExit(1) - - config_parent = config_path.parent - - def ensure_per_vault_window_state(vault_id: str) -> None: - """Obsidian reads ~/.config/obsidian/.json for window/chrome state on startup.""" - p = config_parent / f"{vault_id}.json" - if p.exists(): - return - p.write_text( - json.dumps( - {"isMaximized": True, "devTools": False, "zoom": -1}, - separators=(",", ":"), - ), - encoding="utf-8", - ) - - vault_str = str(vault) - target_id = None - for vid, entry in vaults.items(): - if not isinstance(entry, dict): - continue - p = entry.get("path") - if not p: - continue - try: - if Path(p).expanduser().resolve() == vault: - target_id = vid - break - except OSError: - continue - - if target_id is None: - target_id = secrets.token_hex(8) - vaults[target_id] = { - "path": vault_str, - "ts": int(time.time() * 1000), - "open": True, - } - else: - vaults[target_id]["path"] = vault_str - vaults[target_id]["ts"] = int(time.time() * 1000) - - for vid, entry in vaults.items(): - if isinstance(entry, dict): - entry["open"] = vid == target_id - - for vid in vaults: - if isinstance(vid, str): - ensure_per_vault_window_state(vid) - - config_path.write_text(json.dumps(data, separators=(",", ":")), encoding="utf-8") - print(f"Registered vault in {config_path}: {vault_str}") - - -if __name__ == "__main__": - main() diff --git a/qa/reports/release-test-report-desktop.md b/qa/reports/release-test-report-desktop.md deleted file mode 100644 index 52be138..0000000 --- a/qa/reports/release-test-report-desktop.md +++ /dev/null @@ -1,68 +0,0 @@ -# Release Test Report (Desktop) - -Date: 2026-05-15 - -## 1) Environment and Install Setup - -- Clean test vault: `qa/vaults/clean-desktop` -- Install helper: `qa/install-to-vault.sh` (catalog-style: plugin only, bundled `styles.css`) -- Installed artifacts: - - `.obsidian/plugins/miku-plugin-hybrid/{main.js,manifest.json,styles.css}` (manifest `1.0.0`) - - No separate Appearance theme file (bundled hybrid CSS) - -Status: PASS - -## 2) Build and Static Checks - -- `npm run typecheck --prefix ./obsidian-miku-plugin` -> PASS -- `npm run build --prefix ./obsidian-miku-plugin` -> PASS (esbuild + theme CSS merge) -- `./qa/static-guards.sh` -> PASS - - Hybrid DOM hooks in `theme-manager.ts` - - `body[data-miku-theme=` in theme source and merged `styles.css` - - Bundled theme marker in `styles.css` - - Module lifecycle and command registration - - No audio-policy violations - -Status: PASS - -## 3) Manual Matrix Execution - -Obsidian GUI launch not run in this CI/agent environment (`Cannot find module 'electron'` when attempted previously). - -Status: BLOCKED (environment-level) — run [manual matrix](#manual-matrix-checklist) on a desktop Obsidian install before marketplace sign-off. - -## 4) Lifecycle and Performance Validation - -- `./qa/lifecycle-perf-check.sh` -> PASS -- Theme manager cleanup, quote interval teardown, no audio paths (static guards) - -Status: PASS - -## 5) Release Sign-off Readiness - -| Item | Status | -|------|--------| -| Root `manifest.json`, `LICENSE`, `README.md` | Ready | -| Merged `styles.css` in plugin package | Ready | -| GitHub Actions `release.yml` | Ready | -| GitHub Release `1.0.0` tag | Pending push (see below) | -| community.obsidian.md submission | Human step — [docs/COMMUNITY_SUBMISSION.md](../../docs/COMMUNITY_SUBMISSION.md) | - -Status: CONDITIONAL PASS (automated); manual UI matrix required on host with Obsidian. - -## Command Evidence Snapshot - -- `npm run typecheck --prefix ./obsidian-miku-plugin` -> PASS -- `npm run build --prefix ./obsidian-miku-plugin` -> PASS -- `./qa/static-guards.sh` -> PASS -- `./qa/lifecycle-perf-check.sh` -> PASS -- `./qa/install-to-vault.sh ./qa/vaults/clean-desktop` -> PASS - -## Manual Matrix Checklist - -- Enable **Miku Hybrid** only (no Appearance theme); verify palette/modes. -- Cycle all theme modes; confirm `data-miku-theme` and status bar label. -- Toggle settings; persist after restart. -- Widget mount/unmount; no stale DOM after disable. -- Dashboard via ribbon and command. -- Optional: `MIKU_INSTALL_SPLIT_THEME=1 ./qa/install-to-vault.sh ` for split theme dev test. diff --git a/qa/test-environment.md b/qa/test-environment.md deleted file mode 100644 index 1fe81f4..0000000 --- a/qa/test-environment.md +++ /dev/null @@ -1,72 +0,0 @@ -# Desktop Test Environment Setup - -## Vault Targets - -- Active desktop vault (detected): `/home/cachy/Documents/bfh4` -- Clean test vault (local): `/home/cachy/IdeaProjects/cachy/mitsu-plugin/qa/vaults/clean-desktop` - -## Install Artifacts Into a Vault - -**Run from the repository root.** Paths like `./qa/install-to-vault.sh` are relative to `mitsu-plugin/`, not to `mitsu-plugin/qa/`. If your shell is already inside `qa/`, `cd` to the root first (or call `./install-to-vault.sh` from `qa/`). - -Build the plugin bundle first so `main.js` is up to date, then install from the repo root. - -Active desktop vault: - -```bash -cd /home/cachy/IdeaProjects/cachy/mitsu-plugin/obsidian-miku-plugin && npm run build -cd /home/cachy/IdeaProjects/cachy/mitsu-plugin -./qa/install-to-vault.sh /home/cachy/Documents/bfh4 -``` - -Isolated clean test vault (still from repo root): - -```bash -cd /home/cachy/IdeaProjects/cachy/mitsu-plugin/obsidian-miku-plugin && npm run build -cd /home/cachy/IdeaProjects/cachy/mitsu-plugin -./qa/install-to-vault.sh ./qa/vaults/clean-desktop -``` - -## Expected Install Paths - -- Plugin: `/.obsidian/plugins/miku-plugin-hybrid/` (`main.js`, root or plugin `manifest.json`, merged `styles.css`) - -Default install matches the **Community plugin** bundle: hybrid palette CSS is merged into plugin `styles.css`. Enable **Miku Hybrid** under Community plugins—no separate Appearance theme. - -Split theme dev install (optional): - -```bash -MIKU_INSTALL_SPLIT_THEME=1 ./qa/install-to-vault.sh /path/to/vault -``` - -That also copies `/.obsidian/themes/Miku Theme Hybrid Base.css`; pick it under **Settings → Appearance → Theme** when testing the legacy split layout. - -## Notes - -- This setup is desktop-focused and matches the release test plan scope. -- The script does not enable plugins/theme automatically; enable them from Obsidian settings. -- If `obsidian ` fails with `Cannot find module 'electron'`, launch with: - -```bash -./qa/launch-obsidian.sh /home/cachy/Documents/bfh4 -``` - -This wrapper unsets `ELECTRON_RUN_AS_NODE` (common in automation shells) and adds `--no-sandbox` automatically when running as root. - -### Opening a vault path from the terminal (URI vs argv) - -Recent Obsidian builds treat the **first positional argument** after Chromium-style flags as an **Obsidian CLI subcommand** (same style of message as **`Command "/path/to/thing" not found`**), even when spawned via distro wrappers like **`/usr/bin/obsidian`** (Arch's script forwards **`electron … app.asar`** args). Passing **`/path/to/vault`** therefore no longer opens that folder reliably. - -For QA, `./qa/launch-obsidian.sh` writes the vault folder into **`~/.config/obsidian/obsidian.json`** (unless `MIKU_SKIP_OBSIDIAN_REGISTER=1`). Obsidian otherwise answers **`Vault not found`** for `obsidian://open?path=...`, because URIs resolve only against **registered** vault paths. Then it opens **`xdg-open`** on **`obsidian://open?path=...`** using **`README.md`** at the vault root (created by `./qa/install-to-vault.sh` if missing). You need `python3` (encoding + registration), `xdg-open`, and a working `obsidian://` handler. Use `MIKU_OBSIDIAN_LEGACY_ARGV_LAUNCH=1 ./qa/launch-obsidian.sh ./path/to/vault` only to debug older installs that still honor folder-as-argv (`OBSIDIAN_GUI` selects the Electron wrapper in that fallback). Close Obsidian before first registration if you see config write conflicts. - -If the terminal briefly logs **`ENOENT … ~/.config/obsidian/.json`**, Obsidian expects a small window-state JSON next to **`obsidian.json`** for each vault id. **`register-obsidian-vault.py`** now creates missing `.json` files automatically when you launch via **`./qa/launch-obsidian.sh`**. - -### Community Plugins search versus side-loaded QA plugins - -The **browse/search** modal lists plugins from Obsidian's **Community Plugins** catalog. Plugins installed only via **`./qa/install-to-vault.sh`** (files under `.obsidian/plugins/miku-plugin-hybrid/`) are **not** in that catalog, so **`Show installed only` + query** often shows **zero** hits even though the plugin is loaded. Confirm under **Settings → Community plugins**: the toggle list should include **Miku Hybrid**. The status bar **`Miku: MinimalMiku`** (or another mode name) confirms the bundle is executing. - -After enabling **Settings → General → Command line interface**, Obsidian often adds **`~/.local/bin/obsidian`** ahead of the distro launcher on **`PATH`**; it only understands **`obsidian `** automation when the GUI is already running, not cold-start **`obsidian /folder`**. - -### Arch `user-flags.conf` trap - -On Arch (and similar), `~/.config/obsidian/user-flags.conf` is inlined into the desktop launcher (`exec electron … app.asar $OBSIDIAN_USER_FLAGS "$@"`). A wrong token there is treated as a normal CLI argument—not an Electron/Chromium flag. For example `# -disable-gpu` can expand to `-disable-gpu` (missing the second hyphen), after which Obsidian may report something like **`Command "-disable-gpu" not found`**. Prefer valid flags (e.g. `--disable-gpu` if you really need GPU disable) or comment/remove the line entirely. \ No newline at end of file