diff --git a/esbuild.config.mjs b/esbuild.config.mjs index c197f8f..dc0c0ab 100644 --- a/esbuild.config.mjs +++ b/esbuild.config.mjs @@ -2,20 +2,19 @@ import esbuild from "esbuild"; import process from "process"; import builtins from "builtin-modules"; -const banner = -`/* +const banner = `/* THIS IS A GENERATED/BUNDLED FILE BY ESBUILD if you want to view the source, please visit the github repository of this plugin */ `; -const prod = (process.argv[2] === "production"); +const prod = process.argv[2] === "production"; const context = await esbuild.context({ banner: { js: banner, }, - entryPoints: ["main.ts"], + entryPoints: ["src/main.ts"], bundle: true, external: [ "obsidian", @@ -31,7 +30,8 @@ const context = await esbuild.context({ "@lezer/common", "@lezer/highlight", "@lezer/lr", - ...builtins], + ...builtins, + ], format: "cjs", target: "es2018", logLevel: "info", @@ -41,7 +41,7 @@ const context = await esbuild.context({ loader: { ".png": "dataurl", ".gif": "dataurl", - } + }, }); if (prod) { @@ -49,4 +49,4 @@ if (prod) { process.exit(0); } else { await context.watch(); -} \ No newline at end of file +} diff --git a/main.ts b/main.ts deleted file mode 100644 index d7295ce..0000000 --- a/main.ts +++ /dev/null @@ -1,313 +0,0 @@ -import { App, debounce, Plugin, PluginSettingTab, Setting } from 'obsidian'; -import EMERGE_MOTION from './animations/gemmy_emerge.gif'; -import POP_MOTION from './animations/gemmy_pop.gif'; -import DISAPPEAR_MOTION from './animations/gemmy_disappear.gif'; -import ANGRY_MOTION from './animations/gemmy_angry.gif'; -import LOOK_MOTION from './animations/gemmy_lookAround.gif' -import IDLE_MOTION from './animations/gemmy_idle.gif' -import DISAPPOINT_IMG from './animations/gemmy_disappoint.gif' - -interface GemmySettings { - // how often does Gemmy talk in idle mode, in minutes - idleTalkFrequency: number; - // the number of minutes you must write before Gemmy appears to mock you - writingModeGracePeriod: number; -} - -const DEFAULT_SETTINGS: GemmySettings = { - idleTalkFrequency: 5, - writingModeGracePeriod: 5 -}; - - -const GEMMY_IDLE_QUOTES = [ - "Did you know that a vault is just a folder of plain text notes?", - "I see you're checking out a ChatGPT plugin, would you consider me instead?", - "You have plugins that you can update!", - "Hi I'm Gemmy! Like Clippy but shinier!", - "Everything is connected. Everything.", - "Can’t decide which note to work on? Try the Random Note core plugin!", - "Are you sure you don’t want to upload all your notes just so we can chat?", - "How tall would all your notes be if you stacked them up?", - "Wanna teach me to say things? Find me on GitHub and open a pull request!", - "Have you considered using Comic Sans?", - "A blank page is just a masterpiece in waiting" -]; - -const WRITING_MODE_QUOTES = [ - "Is that the best you can do? Keep writing!", - "Write first, edit later.", - "I love hearing your keyboard. Don't stop.", - "How about we review some old notes today?", - "Stuck? Try journaling what happened today and see if that gives you inspiration.", - "Maybe it's time to go get some water or coffee.", - "Anything is better than a blank page, even me. Write something!", - "Oh, taking a break? Don’t let the ideas get cold!", - "What’s this? Writer’s block already?", - "You’ve got this! Every keystroke is a step closer to brilliance.", - "You’re doing amazing! Keep the momentum going.", - "Bold choice to open Obsidian and then do absolutely nothing.", - "Is this a journal or a staring contest with the cursor?", - "Hmm, I didn’t realize staring at the screen was a new note-taking strategy." -]; - -const BUBBLE_DURATION = 5000; - -export default class Gemmy extends Plugin { - settings: GemmySettings; - gemmyEl: HTMLElement; - imageEl: HTMLElement; - inWritingMode: boolean = false; - idleTimeout: number; - writingModeTimeout: number; - appeared: boolean = false; - - async onload() { - await this.loadSettings(); - - let gemmyEl = this.gemmyEl = createDiv('gemmy-container'); - gemmyEl.setAttribute('aria-label-position', 'top'); - gemmyEl.setAttribute('aria-label-delay', '0'); - gemmyEl.setAttribute('aria-label-classes', 'gemmy-tooltip'); - - this.imageEl = gemmyEl.createEl('img', {}); - - this.addCommand({ - id: 'show', - name: 'Show Gemmy', - callback: () => { - this.appear(); - } - }); - - this.addCommand({ - id: 'hide', - name: 'Hide Gemmy', - callback: () => { - this.disappear(); - } - }); - - this.addCommand({ - id: 'enter-writing-mode', - name: 'Enter writing mode', - callback: () => { - this.enterWritingMode(); - } - }); - - this.addCommand({ - id: 'leave-writing-mode', - name: 'Leave writing mode', - callback: () => { - this.leaveWritingMode(); - } - }); - - // This adds a settings tab so the user can configure various aspects of the plugin - this.addSettingTab(new GemmySettingTab(this.app, this)); - - this.gemmyEl.addEventListener('mouseenter', () => { - if (this.inWritingMode) { - return; - } - - this.saySomething(GEMMY_IDLE_QUOTES, true); - this.idleTimeout && clearTimeout(this.idleTimeout); - - }); - this.gemmyEl.addEventListener('mouseleave', () => { - if (this.inWritingMode) { - return; - } - - this.imageEl.setAttribute('src', IDLE_MOTION); - this.startNextIdleTimeout(); - }); - - this.startNextIdleTimeout(); - - // debounce editor-change event on workspace - this.registerEvent(this.app.workspace.on('editor-change', debounce(() => { - if (!this.inWritingMode) { - return; - } - - this.disappear(); - this.setWritingModeTimeout(); - }, 500))); - - app.workspace.onLayoutReady(this.appear.bind(this)); - } - - appear() { - let { gemmyEl, imageEl } = this; - - imageEl.setAttribute('src', EMERGE_MOTION); - - // Quicker if we're in writing mode - if (this.inWritingMode) { - imageEl.setAttribute('src', POP_MOTION); - - setTimeout(() => { - this.appeared = true; - - this.saySomething(WRITING_MODE_QUOTES, true); - }, 1800); - } - else { - imageEl.setAttribute('src', EMERGE_MOTION); - - setTimeout(() => { - imageEl.setAttribute('src', IDLE_MOTION); - this.appeared = true; - }, 3800); - } - - document.body.appendChild(gemmyEl); - gemmyEl.show(); - } - - disappear() { - this.idleTimeout && window.clearTimeout(this.idleTimeout); - this.writingModeTimeout && window.clearTimeout(this.writingModeTimeout); - - this.imageEl.setAttribute('src', DISAPPEAR_MOTION); - // remote tooltip - this.gemmyEl.dispatchEvent(new MouseEvent('mouseout', { bubbles: true, clientX: 10, clientY: 10 })); - setTimeout(() => { - this.gemmyEl.hide(); - this.appeared = false; - }, 1300); - - } - - enterWritingMode() { - this.inWritingMode = true; - - this.disappear(); - - this.setWritingModeTimeout(); - } - - leaveWritingMode() { - this.inWritingMode = false; - this.disappear(); - - window.clearTimeout(this.writingModeTimeout); - } - - setWritingModeTimeout() { - if (this.writingModeTimeout) { - window.clearTimeout(this.writingModeTimeout); - } - - this.writingModeTimeout = window.setTimeout(() => { - if (!this.inWritingMode) { - return; - } - - this.appear(); - }, this.settings.writingModeGracePeriod * 1000); - } - - startNextIdleTimeout() { - // if the set time is 5 minutes, this will set timeout to be a random time between 4-6 minutes - // the range will be 80% - 120% - let randomFactor = 0.8 + 0.4 * Math.random(); - let randomizedTimeout = randomFactor * this.settings.idleTalkFrequency * 60000; - - if (this.idleTimeout) { - window.clearTimeout(this.idleTimeout); - } - - this.idleTimeout = window.setTimeout(() => { - if (this.inWritingMode) { - return; - } - - this.saySomething(GEMMY_IDLE_QUOTES, false); - this.startNextIdleTimeout(); - }, randomizedTimeout); - } - - saySomething(quotes: string[], persistent: boolean) { - if (!this.appeared) { - return; - } - - let randomThing = quotes[Math.floor(Math.random() * quotes.length)]; - - this.gemmyEl.setAttr('aria-label', randomThing); - this.gemmyEl.setAttr('aria-label-position', 'top'); - this.gemmyEl.dispatchEvent(new MouseEvent('mouseover', { bubbles: true, clientX: 10, clientY: 10 })) - - if (this.inWritingMode) { - this.imageEl.setAttribute('src', ANGRY_MOTION); - setTimeout(() => { - this.imageEl.setAttribute('src', DISAPPOINT_IMG); - }, 1000); - } - else { - this.imageEl.setAttribute('src', LOOK_MOTION); - } - - if (!persistent) { - setTimeout(() => { - this.gemmyEl.dispatchEvent(new MouseEvent('mouseout', { bubbles: true, clientX: 10, clientY: 10 })); - this.imageEl.setAttribute('src', IDLE_MOTION); - }, BUBBLE_DURATION); - } - } - - onunload() { - this.disappear(); - } - - async loadSettings() { - this.settings = Object.assign({}, DEFAULT_SETTINGS, await this.loadData()); - } - - async saveSettings() { - await this.saveData(this.settings); - } -} - -class GemmySettingTab extends PluginSettingTab { - plugin: Gemmy; - - constructor(app: App, plugin: Gemmy) { - super(app, plugin); - this.plugin = plugin; - } - - display(): void { - const { containerEl } = this; - - containerEl.empty(); - - new Setting(containerEl) - .setName('Idle talk frequency') - .setDesc('How often does Gemmy speak when idle, in minutes.') - .addSlider(slider => slider - .setLimits(5, 60, 5) - .setValue(this.plugin.settings.idleTalkFrequency) - .setDynamicTooltip() - .onChange(async (value) => { - this.plugin.settings.idleTalkFrequency = value; - await this.plugin.saveSettings(); - })); - - new Setting(containerEl) - .setName('Writing mode grace period') - .setDesc('How soon Gemmy starts to get disappointed after you stop tying in writing mode, in seconds.') - .addSlider(slider => slider - .setLimits(5, 180, 5) - .setDynamicTooltip() - .setValue(this.plugin.settings.writingModeGracePeriod) - .onChange(async (value) => { - this.plugin.settings.writingModeGracePeriod = value; - await this.plugin.saveSettings(); - })); - } -} diff --git a/package-lock.json b/package-lock.json index d3e5a81..8ef0836 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,231 +1,453 @@ { "name": "obsidian-sample-plugin", "version": "1.0.0", - "lockfileVersion": 1, + "lockfileVersion": 3, "requires": true, - "dependencies": { - "@esbuild/android-arm": { + "packages": { + "": { + "name": "obsidian-sample-plugin", + "version": "1.0.0", + "license": "MIT", + "devDependencies": { + "@types/node": "^16.11.6", + "@typescript-eslint/eslint-plugin": "5.29.0", + "@typescript-eslint/parser": "5.29.0", + "builtin-modules": "3.3.0", + "esbuild": "0.17.3", + "obsidian": "latest", + "tslib": "2.4.0", + "typescript": "4.7.4" + } + }, + "node_modules/@esbuild/android-arm": { "version": "0.17.3", "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.17.3.tgz", "integrity": "sha512-1Mlz934GvbgdDmt26rTLmf03cAgLg5HyOgJN+ZGCeP3Q9ynYTNMn2/LQxIl7Uy+o4K6Rfi2OuLsr12JQQR8gNg==", + "cpu": [ + "arm" + ], "dev": true, - "optional": true + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } }, - "@esbuild/android-arm64": { + "node_modules/@esbuild/android-arm64": { "version": "0.17.3", "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.17.3.tgz", "integrity": "sha512-XvJsYo3dO3Pi4kpalkyMvfQsjxPWHYjoX4MDiB/FUM4YMfWcXa5l4VCwFWVYI1+92yxqjuqrhNg0CZg3gSouyQ==", + "cpu": [ + "arm64" + ], "dev": true, - "optional": true + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } }, - "@esbuild/android-x64": { + "node_modules/@esbuild/android-x64": { "version": "0.17.3", "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.17.3.tgz", "integrity": "sha512-nuV2CmLS07Gqh5/GrZLuqkU9Bm6H6vcCspM+zjp9TdQlxJtIe+qqEXQChmfc7nWdyr/yz3h45Utk1tUn8Cz5+A==", + "cpu": [ + "x64" + ], "dev": true, - "optional": true + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } }, - "@esbuild/darwin-arm64": { + "node_modules/@esbuild/darwin-arm64": { "version": "0.17.3", "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.17.3.tgz", "integrity": "sha512-01Hxaaat6m0Xp9AXGM8mjFtqqwDjzlMP0eQq9zll9U85ttVALGCGDuEvra5Feu/NbP5AEP1MaopPwzsTcUq1cw==", + "cpu": [ + "arm64" + ], "dev": true, - "optional": true + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } }, - "@esbuild/darwin-x64": { + "node_modules/@esbuild/darwin-x64": { "version": "0.17.3", "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.17.3.tgz", "integrity": "sha512-Eo2gq0Q/er2muf8Z83X21UFoB7EU6/m3GNKvrhACJkjVThd0uA+8RfKpfNhuMCl1bKRfBzKOk6xaYKQZ4lZqvA==", + "cpu": [ + "x64" + ], "dev": true, - "optional": true + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } }, - "@esbuild/freebsd-arm64": { + "node_modules/@esbuild/freebsd-arm64": { "version": "0.17.3", "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.17.3.tgz", "integrity": "sha512-CN62ESxaquP61n1ZjQP/jZte8CE09M6kNn3baos2SeUfdVBkWN5n6vGp2iKyb/bm/x4JQzEvJgRHLGd5F5b81w==", + "cpu": [ + "arm64" + ], "dev": true, - "optional": true + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } }, - "@esbuild/freebsd-x64": { + "node_modules/@esbuild/freebsd-x64": { "version": "0.17.3", "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.17.3.tgz", "integrity": "sha512-feq+K8TxIznZE+zhdVurF3WNJ/Sa35dQNYbaqM/wsCbWdzXr5lyq+AaTUSER2cUR+SXPnd/EY75EPRjf4s1SLg==", + "cpu": [ + "x64" + ], "dev": true, - "optional": true + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } }, - "@esbuild/linux-arm": { + "node_modules/@esbuild/linux-arm": { "version": "0.17.3", "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.17.3.tgz", "integrity": "sha512-CLP3EgyNuPcg2cshbwkqYy5bbAgK+VhyfMU7oIYyn+x4Y67xb5C5ylxsNUjRmr8BX+MW3YhVNm6Lq6FKtRTWHQ==", + "cpu": [ + "arm" + ], "dev": true, - "optional": true + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } }, - "@esbuild/linux-arm64": { + "node_modules/@esbuild/linux-arm64": { "version": "0.17.3", "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.17.3.tgz", "integrity": "sha512-JHeZXD4auLYBnrKn6JYJ0o5nWJI9PhChA/Nt0G4MvLaMrvXuWnY93R3a7PiXeJQphpL1nYsaMcoV2QtuvRnF/g==", + "cpu": [ + "arm64" + ], "dev": true, - "optional": true + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } }, - "@esbuild/linux-ia32": { + "node_modules/@esbuild/linux-ia32": { "version": "0.17.3", "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.17.3.tgz", "integrity": "sha512-FyXlD2ZjZqTFh0sOQxFDiWG1uQUEOLbEh9gKN/7pFxck5Vw0qjWSDqbn6C10GAa1rXJpwsntHcmLqydY9ST9ZA==", + "cpu": [ + "ia32" + ], "dev": true, - "optional": true + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } }, - "@esbuild/linux-loong64": { + "node_modules/@esbuild/linux-loong64": { "version": "0.17.3", "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.17.3.tgz", "integrity": "sha512-OrDGMvDBI2g7s04J8dh8/I7eSO+/E7nMDT2Z5IruBfUO/RiigF1OF6xoH33Dn4W/OwAWSUf1s2nXamb28ZklTA==", + "cpu": [ + "loong64" + ], "dev": true, - "optional": true + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } }, - "@esbuild/linux-mips64el": { + "node_modules/@esbuild/linux-mips64el": { "version": "0.17.3", "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.17.3.tgz", "integrity": "sha512-DcnUpXnVCJvmv0TzuLwKBC2nsQHle8EIiAJiJ+PipEVC16wHXaPEKP0EqN8WnBe0TPvMITOUlP2aiL5YMld+CQ==", + "cpu": [ + "mips64el" + ], "dev": true, - "optional": true + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } }, - "@esbuild/linux-ppc64": { + "node_modules/@esbuild/linux-ppc64": { "version": "0.17.3", "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.17.3.tgz", "integrity": "sha512-BDYf/l1WVhWE+FHAW3FzZPtVlk9QsrwsxGzABmN4g8bTjmhazsId3h127pliDRRu5674k1Y2RWejbpN46N9ZhQ==", + "cpu": [ + "ppc64" + ], "dev": true, - "optional": true + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } }, - "@esbuild/linux-riscv64": { + "node_modules/@esbuild/linux-riscv64": { "version": "0.17.3", "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.17.3.tgz", "integrity": "sha512-WViAxWYMRIi+prTJTyV1wnqd2mS2cPqJlN85oscVhXdb/ZTFJdrpaqm/uDsZPGKHtbg5TuRX/ymKdOSk41YZow==", + "cpu": [ + "riscv64" + ], "dev": true, - "optional": true + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } }, - "@esbuild/linux-s390x": { + "node_modules/@esbuild/linux-s390x": { "version": "0.17.3", "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.17.3.tgz", "integrity": "sha512-Iw8lkNHUC4oGP1O/KhumcVy77u2s6+KUjieUqzEU3XuWJqZ+AY7uVMrrCbAiwWTkpQHkr00BuXH5RpC6Sb/7Ug==", + "cpu": [ + "s390x" + ], "dev": true, - "optional": true + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } }, - "@esbuild/linux-x64": { + "node_modules/@esbuild/linux-x64": { "version": "0.17.3", "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.17.3.tgz", "integrity": "sha512-0AGkWQMzeoeAtXQRNB3s4J1/T2XbigM2/Mn2yU1tQSmQRmHIZdkGbVq2A3aDdNslPyhb9/lH0S5GMTZ4xsjBqg==", + "cpu": [ + "x64" + ], "dev": true, - "optional": true + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } }, - "@esbuild/netbsd-x64": { + "node_modules/@esbuild/netbsd-x64": { "version": "0.17.3", "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.17.3.tgz", "integrity": "sha512-4+rR/WHOxIVh53UIQIICryjdoKdHsFZFD4zLSonJ9RRw7bhKzVyXbnRPsWSfwybYqw9sB7ots/SYyufL1mBpEg==", + "cpu": [ + "x64" + ], "dev": true, - "optional": true + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } }, - "@esbuild/openbsd-x64": { + "node_modules/@esbuild/openbsd-x64": { "version": "0.17.3", "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.17.3.tgz", "integrity": "sha512-cVpWnkx9IYg99EjGxa5Gc0XmqumtAwK3aoz7O4Dii2vko+qXbkHoujWA68cqXjhh6TsLaQelfDO4MVnyr+ODeA==", + "cpu": [ + "x64" + ], "dev": true, - "optional": true + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } }, - "@esbuild/sunos-x64": { + "node_modules/@esbuild/sunos-x64": { "version": "0.17.3", "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.17.3.tgz", "integrity": "sha512-RxmhKLbTCDAY2xOfrww6ieIZkZF+KBqG7S2Ako2SljKXRFi+0863PspK74QQ7JpmWwncChY25JTJSbVBYGQk2Q==", + "cpu": [ + "x64" + ], "dev": true, - "optional": true + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } }, - "@esbuild/win32-arm64": { + "node_modules/@esbuild/win32-arm64": { "version": "0.17.3", "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.17.3.tgz", "integrity": "sha512-0r36VeEJ4efwmofxVJRXDjVRP2jTmv877zc+i+Pc7MNsIr38NfsjkQj23AfF7l0WbB+RQ7VUb+LDiqC/KY/M/A==", + "cpu": [ + "arm64" + ], "dev": true, - "optional": true + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } }, - "@esbuild/win32-ia32": { + "node_modules/@esbuild/win32-ia32": { "version": "0.17.3", "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.17.3.tgz", "integrity": "sha512-wgO6rc7uGStH22nur4aLFcq7Wh86bE9cOFmfTr/yxN3BXvDEdCSXyKkO+U5JIt53eTOgC47v9k/C1bITWL/Teg==", + "cpu": [ + "ia32" + ], "dev": true, - "optional": true + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } }, - "@esbuild/win32-x64": { + "node_modules/@esbuild/win32-x64": { "version": "0.17.3", "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.17.3.tgz", "integrity": "sha512-FdVl64OIuiKjgXBjwZaJLKp0eaEckifbhn10dXWhysMJkWblg3OEEGKSIyhiD5RSgAya8WzP3DNkngtIg3Nt7g==", + "cpu": [ + "x64" + ], "dev": true, - "optional": true + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } }, - "@nodelib/fs.scandir": { + "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", "dev": true, - "requires": { + "dependencies": { "@nodelib/fs.stat": "2.0.5", "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" } }, - "@nodelib/fs.stat": { + "node_modules/@nodelib/fs.stat": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", - "dev": true + "dev": true, + "engines": { + "node": ">= 8" + } }, - "@nodelib/fs.walk": { + "node_modules/@nodelib/fs.walk": { "version": "1.2.8", "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", "dev": true, - "requires": { + "dependencies": { "@nodelib/fs.scandir": "2.1.5", "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" } }, - "@types/codemirror": { + "node_modules/@types/codemirror": { "version": "0.0.108", "resolved": "https://registry.npmjs.org/@types/codemirror/-/codemirror-0.0.108.tgz", "integrity": "sha512-3FGFcus0P7C2UOGCNUVENqObEb4SFk+S8Dnxq7K6aIsLVs/vDtlangl3PEO0ykaKXyK56swVF6Nho7VsA44uhw==", "dev": true, - "requires": { + "dependencies": { "@types/tern": "*" } }, - "@types/estree": { + "node_modules/@types/estree": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.0.tgz", "integrity": "sha512-WulqXMDUTYAXCjZnk6JtIHPigp55cVtDgDrO2gHRwhyJto21+1zbVCtOYB2L1F9w4qCQ0rOGWBnBe0FNTiEJIQ==", "dev": true }, - "@types/json-schema": { + "node_modules/@types/json-schema": { "version": "7.0.11", "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.11.tgz", "integrity": "sha512-wOuvG1SN4Us4rez+tylwwwCV1psiNVOkJeM3AUWUNWg/jDQY2+HE/444y5gc+jBmRqASOm2Oeh5c1axHobwRKQ==", "dev": true }, - "@types/node": { + "node_modules/@types/node": { "version": "16.18.22", "resolved": "https://registry.npmjs.org/@types/node/-/node-16.18.22.tgz", "integrity": "sha512-LJSIirgASa1LicFGTUFwDY7BfKDtLIbijqDLkH47LxEo/jtdrtiZ4/kLPD99bEQhTcPcuh6KhDllHqRxygJD2w==", "dev": true }, - "@types/tern": { + "node_modules/@types/tern": { "version": "0.23.4", "resolved": "https://registry.npmjs.org/@types/tern/-/tern-0.23.4.tgz", "integrity": "sha512-JAUw1iXGO1qaWwEOzxTKJZ/5JxVeON9kvGZ/osgZaJImBnyjyn0cjovPsf6FNLmyGY8Vw9DoXZCMlfMkMwHRWg==", "dev": true, - "requires": { + "dependencies": { "@types/estree": "*" } }, - "@typescript-eslint/eslint-plugin": { + "node_modules/@typescript-eslint/eslint-plugin": { "version": "5.29.0", "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.29.0.tgz", "integrity": "sha512-kgTsISt9pM53yRFQmLZ4npj99yGl3x3Pl7z4eA66OuTzAGC4bQB5H5fuLwPnqTKU3yyrrg4MIhjF17UYnL4c0w==", "dev": true, - "requires": { + "dependencies": { "@typescript-eslint/scope-manager": "5.29.0", "@typescript-eslint/type-utils": "5.29.0", "@typescript-eslint/utils": "5.29.0", @@ -235,53 +457,113 @@ "regexpp": "^3.2.0", "semver": "^7.3.7", "tsutils": "^3.21.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^5.0.0", + "eslint": "^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } } }, - "@typescript-eslint/parser": { + "node_modules/@typescript-eslint/parser": { "version": "5.29.0", "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-5.29.0.tgz", "integrity": "sha512-ruKWTv+x0OOxbzIw9nW5oWlUopvP/IQDjB5ZqmTglLIoDTctLlAJpAQFpNPJP/ZI7hTT9sARBosEfaKbcFuECw==", "dev": true, - "requires": { + "dependencies": { "@typescript-eslint/scope-manager": "5.29.0", "@typescript-eslint/types": "5.29.0", "@typescript-eslint/typescript-estree": "5.29.0", "debug": "^4.3.4" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } } }, - "@typescript-eslint/scope-manager": { + "node_modules/@typescript-eslint/scope-manager": { "version": "5.29.0", "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.29.0.tgz", "integrity": "sha512-etbXUT0FygFi2ihcxDZjz21LtC+Eps9V2xVx09zFoN44RRHPrkMflidGMI+2dUs821zR1tDS6Oc9IXxIjOUZwA==", "dev": true, - "requires": { + "dependencies": { "@typescript-eslint/types": "5.29.0", "@typescript-eslint/visitor-keys": "5.29.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" } }, - "@typescript-eslint/type-utils": { + "node_modules/@typescript-eslint/type-utils": { "version": "5.29.0", "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-5.29.0.tgz", "integrity": "sha512-JK6bAaaiJozbox3K220VRfCzLa9n0ib/J+FHIwnaV3Enw/TO267qe0pM1b1QrrEuy6xun374XEAsRlA86JJnyg==", "dev": true, - "requires": { + "dependencies": { "@typescript-eslint/utils": "5.29.0", "debug": "^4.3.4", "tsutils": "^3.21.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "*" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } } }, - "@typescript-eslint/types": { + "node_modules/@typescript-eslint/types": { "version": "5.29.0", "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.29.0.tgz", "integrity": "sha512-X99VbqvAXOMdVyfFmksMy3u8p8yoRGITgU1joBJPzeYa0rhdf5ok9S56/itRoUSh99fiDoMtarSIJXo7H/SnOg==", - "dev": true + "dev": true, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } }, - "@typescript-eslint/typescript-estree": { + "node_modules/@typescript-eslint/typescript-estree": { "version": "5.29.0", "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-5.29.0.tgz", "integrity": "sha512-mQvSUJ/JjGBdvo+1LwC+GY2XmSYjK1nAaVw2emp/E61wEVYEyibRHCqm1I1vEKbXCpUKuW4G7u9ZCaZhJbLoNQ==", "dev": true, - "requires": { + "dependencies": { "@typescript-eslint/types": "5.29.0", "@typescript-eslint/visitor-keys": "5.29.0", "debug": "^4.3.4", @@ -289,77 +571,136 @@ "is-glob": "^4.0.3", "semver": "^7.3.7", "tsutils": "^3.21.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } } }, - "@typescript-eslint/utils": { + "node_modules/@typescript-eslint/utils": { "version": "5.29.0", "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-5.29.0.tgz", "integrity": "sha512-3Eos6uP1nyLOBayc/VUdKZikV90HahXE5Dx9L5YlSd/7ylQPXhLk1BYb29SDgnBnTp+jmSZUU0QxUiyHgW4p7A==", "dev": true, - "requires": { + "dependencies": { "@types/json-schema": "^7.0.9", "@typescript-eslint/scope-manager": "5.29.0", "@typescript-eslint/types": "5.29.0", "@typescript-eslint/typescript-estree": "5.29.0", "eslint-scope": "^5.1.1", "eslint-utils": "^3.0.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, - "@typescript-eslint/visitor-keys": { + "node_modules/@typescript-eslint/visitor-keys": { "version": "5.29.0", "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.29.0.tgz", "integrity": "sha512-Hpb/mCWsjILvikMQoZIE3voc9wtQcS0A9FUw3h8bhr9UxBdtI/tw1ZDZUOXHXLOVMedKCH5NxyzATwnU78bWCQ==", "dev": true, - "requires": { + "dependencies": { "@typescript-eslint/types": "5.29.0", "eslint-visitor-keys": "^3.3.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" } }, - "array-union": { + "node_modules/array-union": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", - "dev": true + "dev": true, + "engines": { + "node": ">=8" + } }, - "braces": { + "node_modules/braces": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", "dev": true, - "requires": { + "dependencies": { "fill-range": "^7.0.1" + }, + "engines": { + "node": ">=8" } }, - "builtin-modules": { + "node_modules/builtin-modules": { "version": "3.3.0", "resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-3.3.0.tgz", "integrity": "sha512-zhaCDicdLuWN5UbN5IMnFqNMhNfo919sH85y2/ea+5Yg9TsTkeZxpL+JLbp6cgYFS4sRLp3YV4S6yDuqVWHYOw==", - "dev": true + "dev": true, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } }, - "debug": { + "node_modules/debug": { "version": "4.3.4", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", "dev": true, - "requires": { + "dependencies": { "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } } }, - "dir-glob": { + "node_modules/dir-glob": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", "dev": true, - "requires": { + "dependencies": { "path-type": "^4.0.0" + }, + "engines": { + "node": ">=8" } }, - "esbuild": { + "node_modules/esbuild": { "version": "0.17.3", "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.17.3.tgz", "integrity": "sha512-9n3AsBRe6sIyOc6kmoXg2ypCLgf3eZSraWFRpnkto+svt8cZNuKTkb1bhQcitBcvIqjNiK7K0J3KPmwGSfkA8g==", "dev": true, - "requires": { + "hasInstallScript": true, + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { "@esbuild/android-arm": "0.17.3", "@esbuild/android-arm64": "0.17.3", "@esbuild/android-x64": "0.17.3", @@ -384,289 +725,428 @@ "@esbuild/win32-x64": "0.17.3" } }, - "eslint-scope": { + "node_modules/eslint-scope": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", "dev": true, - "requires": { + "dependencies": { "esrecurse": "^4.3.0", "estraverse": "^4.1.1" + }, + "engines": { + "node": ">=8.0.0" } }, - "eslint-utils": { + "node_modules/eslint-utils": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-3.0.0.tgz", "integrity": "sha512-uuQC43IGctw68pJA1RgbQS8/NP7rch6Cwd4j3ZBtgo4/8Flj4eGE7ZYSZRN3iq5pVUv6GPdW5Z1RFleo84uLDA==", "dev": true, - "requires": { + "dependencies": { "eslint-visitor-keys": "^2.0.0" }, - "dependencies": { - "eslint-visitor-keys": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-2.1.0.tgz", - "integrity": "sha512-0rSmRBzXgDzIsD6mGdJgevzgezI534Cer5L/vyMX0kHzT/jiB43jRhd9YUlMGYLQy2zprNmoT8qasCGtY+QaKw==", - "dev": true - } + "engines": { + "node": "^10.0.0 || ^12.0.0 || >= 14.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/mysticatea" + }, + "peerDependencies": { + "eslint": ">=5" } }, - "eslint-visitor-keys": { + "node_modules/eslint-utils/node_modules/eslint-visitor-keys": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-2.1.0.tgz", + "integrity": "sha512-0rSmRBzXgDzIsD6mGdJgevzgezI534Cer5L/vyMX0kHzT/jiB43jRhd9YUlMGYLQy2zprNmoT8qasCGtY+QaKw==", + "dev": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/eslint-visitor-keys": { "version": "3.4.0", "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.0.tgz", "integrity": "sha512-HPpKPUBQcAsZOsHAFwTtIKcYlCje62XB7SEAcxjtmW6TD1WVpkS6i6/hOVtTZIl4zGj/mBqpFVGvaDneik+VoQ==", - "dev": true + "dev": true, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } }, - "esrecurse": { + "node_modules/esrecurse": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", "dev": true, - "requires": { + "dependencies": { "estraverse": "^5.2.0" }, - "dependencies": { - "estraverse": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", - "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", - "dev": true - } + "engines": { + "node": ">=4.0" } }, - "estraverse": { + "node_modules/esrecurse/node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", - "dev": true + "dev": true, + "engines": { + "node": ">=4.0" + } }, - "fast-glob": { + "node_modules/fast-glob": { "version": "3.2.12", "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.2.12.tgz", "integrity": "sha512-DVj4CQIYYow0BlaelwK1pHl5n5cRSJfM60UA0zK891sVInoPri2Ekj7+e1CT3/3qxXenpI+nBBmQAcJPJgaj4w==", "dev": true, - "requires": { + "dependencies": { "@nodelib/fs.stat": "^2.0.2", "@nodelib/fs.walk": "^1.2.3", "glob-parent": "^5.1.2", "merge2": "^1.3.0", "micromatch": "^4.0.4" + }, + "engines": { + "node": ">=8.6.0" } }, - "fastq": { + "node_modules/fastq": { "version": "1.15.0", "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.15.0.tgz", "integrity": "sha512-wBrocU2LCXXa+lWBt8RoIRD89Fi8OdABODa/kEnyeyjS5aZO5/GNvI5sEINADqP/h8M29UHTHUb53sUu5Ihqdw==", "dev": true, - "requires": { + "dependencies": { "reusify": "^1.0.4" } }, - "fill-range": { + "node_modules/fill-range": { "version": "7.0.1", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", "dev": true, - "requires": { + "dependencies": { "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" } }, - "functional-red-black-tree": { + "node_modules/functional-red-black-tree": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz", "integrity": "sha512-dsKNQNdj6xA3T+QlADDA7mOSlX0qiMINjn0cgr+eGHGsbSHzTabcIogz2+p/iqP1Xs6EP/sS2SbqH+brGTbq0g==", "dev": true }, - "glob-parent": { + "node_modules/glob-parent": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", "dev": true, - "requires": { + "dependencies": { "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" } }, - "globby": { + "node_modules/globby": { "version": "11.1.0", "resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz", "integrity": "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==", "dev": true, - "requires": { + "dependencies": { "array-union": "^2.1.0", "dir-glob": "^3.0.1", "fast-glob": "^3.2.9", "ignore": "^5.2.0", "merge2": "^1.4.1", "slash": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "ignore": { + "node_modules/ignore": { "version": "5.2.4", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.2.4.tgz", "integrity": "sha512-MAb38BcSbH0eHNBxn7ql2NH/kX33OkB3lZ1BNdh7ENeRChHTYsTvWrMubiIAMNS2llXEEgZ1MUOBtXChP3kaFQ==", - "dev": true + "dev": true, + "engines": { + "node": ">= 4" + } }, - "is-extglob": { + "node_modules/is-extglob": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", - "dev": true + "dev": true, + "engines": { + "node": ">=0.10.0" + } }, - "is-glob": { + "node_modules/is-glob": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", "dev": true, - "requires": { + "dependencies": { "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" } }, - "is-number": { + "node_modules/is-number": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", - "dev": true + "dev": true, + "engines": { + "node": ">=0.12.0" + } }, - "lru-cache": { + "node_modules/lru-cache": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", "dev": true, - "requires": { + "dependencies": { "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" } }, - "merge2": { + "node_modules/merge2": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", - "dev": true + "dev": true, + "engines": { + "node": ">= 8" + } }, - "micromatch": { + "node_modules/micromatch": { "version": "4.0.5", "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz", "integrity": "sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==", "dev": true, - "requires": { + "dependencies": { "braces": "^3.0.2", "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" } }, - "moment": { + "node_modules/moment": { "version": "2.29.4", "resolved": "https://registry.npmjs.org/moment/-/moment-2.29.4.tgz", "integrity": "sha512-5LC9SOxjSc2HF6vO2CyuTDNivEdoz2IvyJJGj6X8DJ0eFyfszE0QiEd+iXmBvUP3WHxSjFH/vIsA0EN00cgr8w==", - "dev": true + "dev": true, + "engines": { + "node": "*" + } }, - "ms": { + "node_modules/ms": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", "dev": true }, - "obsidian": { + "node_modules/obsidian": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/obsidian/-/obsidian-1.1.1.tgz", "integrity": "sha512-GcxhsHNkPEkwHEjeyitfYNBcQuYGeAHFs1pEpZIv0CnzSfui8p8bPLm2YKLgcg20B764770B1sYGtxCvk9ptxg==", "dev": true, - "requires": { + "dependencies": { "@types/codemirror": "0.0.108", "moment": "2.29.4" + }, + "peerDependencies": { + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.0.0" } }, - "path-type": { + "node_modules/path-type": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", - "dev": true + "dev": true, + "engines": { + "node": ">=8" + } }, - "picomatch": { + "node_modules/picomatch": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", - "dev": true + "dev": true, + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } }, - "queue-microtask": { + "node_modules/queue-microtask": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", - "dev": true + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] }, - "regexpp": { + "node_modules/regexpp": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/regexpp/-/regexpp-3.2.0.tgz", "integrity": "sha512-pq2bWo9mVD43nbts2wGv17XLiNLya+GklZ8kaDLV2Z08gDCsGpnKn9BFMepvWuHCbyVvY7J5o5+BVvoQbmlJLg==", - "dev": true + "dev": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/mysticatea" + } }, - "reusify": { + "node_modules/reusify": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", - "dev": true + "dev": true, + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } }, - "run-parallel": { + "node_modules/run-parallel": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", "dev": true, - "requires": { + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "dependencies": { "queue-microtask": "^1.2.2" } }, - "semver": { + "node_modules/semver": { "version": "7.3.8", "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.8.tgz", "integrity": "sha512-NB1ctGL5rlHrPJtFDVIVzTyQylMLu9N9VICA6HSFJo8MCGVTMW6gfpicwKmmK/dAjTOrqu5l63JJOpDSrAis3A==", "dev": true, - "requires": { + "dependencies": { "lru-cache": "^6.0.0" + }, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" } }, - "slash": { + "node_modules/slash": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", - "dev": true + "dev": true, + "engines": { + "node": ">=8" + } }, - "to-regex-range": { + "node_modules/to-regex-range": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", "dev": true, - "requires": { + "dependencies": { "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" } }, - "tslib": { + "node_modules/tslib": { "version": "2.4.0", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.0.tgz", "integrity": "sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ==", "dev": true }, - "tsutils": { + "node_modules/tsutils": { "version": "3.21.0", "resolved": "https://registry.npmjs.org/tsutils/-/tsutils-3.21.0.tgz", "integrity": "sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA==", "dev": true, - "requires": { + "dependencies": { "tslib": "^1.8.1" }, - "dependencies": { - "tslib": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", - "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", - "dev": true - } + "engines": { + "node": ">= 6" + }, + "peerDependencies": { + "typescript": ">=2.8.0 || >= 3.2.0-dev || >= 3.3.0-dev || >= 3.4.0-dev || >= 3.5.0-dev || >= 3.6.0-dev || >= 3.6.0-beta || >= 3.7.0-dev || >= 3.7.0-beta" } }, - "typescript": { + "node_modules/tsutils/node_modules/tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", + "dev": true + }, + "node_modules/typescript": { "version": "4.7.4", "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.7.4.tgz", "integrity": "sha512-C0WQT0gezHuw6AdY1M2jxUO83Rjf0HP7Sk1DtXj6j1EwkQNZrHAg2XPWlq62oqEhYvONq5pkC2Y9oPljWToLmQ==", - "dev": true + "dev": true, + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=4.2.0" + } }, - "yallist": { + "node_modules/yallist": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", diff --git a/src/main.ts b/src/main.ts new file mode 100644 index 0000000..0a9a21d --- /dev/null +++ b/src/main.ts @@ -0,0 +1,217 @@ +import { App, debounce, Plugin, PluginSettingTab, Setting } from "obsidian"; +import { GemmyMascot } from "./mascot"; + +interface GemmySettings { + // how often does Gemmy talk in idle mode, in minutes + idleTalkFrequency: number; + // the number of minutes you must write before Gemmy appears to mock you + writingModeGracePeriod: number; +} + +const DEFAULT_SETTINGS: GemmySettings = { + idleTalkFrequency: 5, + writingModeGracePeriod: 5, +}; + +export default class Gemmy extends Plugin { + mascot: GemmyMascot; + settings: GemmySettings; + inWritingMode: boolean = false; + writingModeTimeout: number; + idleTimeout: number; + + async onload() { + await this.loadSettings(); + + this.addSettingTab(new GemmySettingTab(this.app, this)); + this.mascot = new GemmyMascot(this, document.body); + this.mascot.mount(); + + this.startNextIdleTimeout(); + + this.addCommand({ + id: "show", + name: "Show Gemmy", + callback: () => { + this.appear(); + }, + }); + + this.addCommand({ + id: "hide", + name: "Hide Gemmy", + callback: () => { + this.disappear(); + }, + }); + + this.addCommand({ + id: "stealth-mode", + name: "Stealth Mode", + callback: () => this.mascot.hide(), + }); + + this.addCommand({ + id: "enter-writing-mode", + name: "Enter writing mode", + callback: () => { + this.enterWritingMode(); + }, + }); + + this.addCommand({ + id: "leave-writing-mode", + name: "Leave writing mode", + callback: () => { + this.leaveWritingMode(); + }, + }); + + // debounce editor-change event on workspace + this.registerEvent( + this.app.workspace.on( + "editor-change", + debounce(() => { + if (!this.inWritingMode) { + return; + } + + this.disappear(); + this.setWritingModeTimeout(); + }, 500) + ) + ); + } + + appear() { + if (this.inWritingMode) { + this.mascot.wakeForWriting(); + } else { + this.mascot.wake(); + } + } + + disappear() { + Math.random() < 0.9 ? this.mascot.hide() : this.mascot.sleep(); + if (this.writingModeTimeout) { + window.clearTimeout(this.writingModeTimeout); + } + if (this.idleTimeout) { + window.clearTimeout(this.idleTimeout); + } + } + + enterWritingMode() { + this.inWritingMode = true; + this.disappear(); + this.setWritingModeTimeout(); + } + + leaveWritingMode() { + this.inWritingMode = false; + this.disappear(); + if (this.writingModeTimeout) { + window.clearTimeout(this.writingModeTimeout); + } + this.startNextIdleTimeout(); + } + + setWritingModeTimeout() { + if (this.writingModeTimeout) { + window.clearTimeout(this.writingModeTimeout); + } + + this.writingModeTimeout = window.setTimeout(() => { + if (!this.inWritingMode) { + return; + } + + this.appear(); + setTimeout(() => { + this.setWritingModeTimeout(); + }, Math.random() * 3000 + 2000); // repeat every 2-5 seconds + }, this.settings.writingModeGracePeriod * 1000); + } + + startNextIdleTimeout() { + // if the set time is 5 minutes, this will set timeout to be a random time between 4-6 minutes + // the range will be 80% - 120% + let randomFactor = 0.8 + 0.4 * Math.random(); + let randomizedTimeout = + randomFactor * this.settings.idleTalkFrequency * 60000; + + if (this.idleTimeout) { + window.clearTimeout(this.idleTimeout); + } + + this.idleTimeout = window.setTimeout(() => { + if (this.inWritingMode) { + return; + } + + this.mascot.setMood("is-happy"); + this.startNextIdleTimeout(); + }, randomizedTimeout); + } + + async loadSettings() { + this.settings = Object.assign( + {}, + DEFAULT_SETTINGS, + await this.loadData() + ); + } + + async saveSettings() { + await this.saveData(this.settings); + } + + onunload() { + this.mascot.unmount(); + } +} + +class GemmySettingTab extends PluginSettingTab { + plugin: Gemmy; + + constructor(app: App, plugin: Gemmy) { + super(app, plugin); + this.plugin = plugin; + } + + display(): void { + const { containerEl } = this; + + containerEl.empty(); + + new Setting(containerEl) + .setName("Idle talk frequency") + .setDesc("How often does Gemmy speak when idle, in minutes.") + .addSlider((slider) => + slider + .setLimits(5, 60, 5) + .setValue(this.plugin.settings.idleTalkFrequency) + .setDynamicTooltip() + .onChange(async (value) => { + this.plugin.settings.idleTalkFrequency = value; + await this.plugin.saveSettings(); + }) + ); + + new Setting(containerEl) + .setName("Writing mode grace period") + .setDesc( + "How soon Gemmy starts to get disappointed after you stop typing in writing mode, in seconds." + ) + .addSlider((slider) => + slider + .setLimits(5, 180, 5) + .setDynamicTooltip() + .setValue(this.plugin.settings.writingModeGracePeriod) + .onChange(async (value) => { + this.plugin.settings.writingModeGracePeriod = value; + await this.plugin.saveSettings(); + }) + ); + } +} diff --git a/src/mascot.ts b/src/mascot.ts new file mode 100644 index 0000000..415a270 --- /dev/null +++ b/src/mascot.ts @@ -0,0 +1,1149 @@ +import Gemmy from "src/main"; + +// --- TYPES --- +interface Reaction { + mood: string; + weight: number; + text?: string; +} + +interface LookRange { + x: number; + y?: number; + yMin?: number; + yMax?: number; +} + +interface DurationRange { + min: number; + max: number; +} + +interface Config { + physics: { + decayRate: number; + decayInterval: number; + maxIntensity: number; + smoothness: number; + epsilon: number; + }; + attention: { + idleThreshold: number; + drowsyThreshold: number; + focusChance: number; + durations: Record; + lookRanges: Record; + }; + blink: { + speeds: Record; + intervals: Record; + scales: Record; + }; + moods: { + durations: Record; + }; + reactions: Record; +} + +// --- CONFIGURATION DATABASE --- +const CONFIG: Config = { + physics: { + decayRate: 0.9, + decayInterval: 100, + maxIntensity: 800, + smoothness: 0.2, + epsilon: 0.0001, + }, + attention: { + idleThreshold: 2000, + drowsyThreshold: 10000, + focusChance: 0.4, + durations: { + suspicious: { min: 500, max: 500 }, + sad: { min: 3000, max: 3000 }, + bored: { min: 4000, max: 4000 }, + excited: { min: 300, max: 300 }, + love: { min: 5000, max: 5000 }, + scared: { min: 200, max: 200 }, + standard: { min: 500, max: 2000 }, + focus: { min: 2000, max: 5000 }, + distracted: { min: 1000, max: 3000 }, + }, + lookRanges: { + suspicious: { x: 1.5, y: 1.0 }, + sad: { x: 0.5, yMin: 0.5, yMax: 0.7 }, + bored: { x: 0.8, y: 0.5 }, + excited: { x: 1.8, y: 1.5 }, + love: { x: 0, y: -0.2 }, + scared: { x: 2.0, y: 1.5 }, + standard: { x: 1.5, y: 1.0 }, + }, + }, + blink: { + speeds: { + normal: 150, + confused: 200, + angry: 100, + love: 400, + scared: 50, + drowsy: 3000, + }, + intervals: { + normal: { base: 3000, variance: 4000 }, + intrigued: { base: 6000, variance: 4000 }, + scared: { base: 500, variance: 1000 }, + drowsy: { base: 1000, variance: 2000 }, + }, + scales: { + closed: 0.1, + drowsy: 0.4, + }, + }, + moods: { + durations: { + "is-happy": 0, + "is-angry": 3000, + "is-suspicious": 4000, + "is-sad": 5000, + "is-confused": 4000, + "is-shy": 4000, + "is-sleeping": 0, + "is-excited": 3000, + "is-bored": 6000, + "is-love": 4000, + "is-scared": 2500, + "is-surprised": 2000, + "is-poked": 500, + "is-hidden": 0, + }, + }, + reactions: { + click: [ + { mood: "is-poked", weight: 0.5 }, + { mood: "is-confused", weight: 0.15 }, + { mood: "is-shy", weight: 0.15 }, + { mood: "is-happy", weight: 0.1 }, + { mood: "is-love", weight: 0.1 }, + ], + contextmenu: [ + { mood: "is-angry", weight: 0.4 }, + { mood: "is-suspicious", weight: 0.3 }, + { mood: "is-sleeping", weight: 0.2 }, + { mood: "is-scared", weight: 0.1 }, + ], + dblclick: [ + { mood: "is-surprised", weight: 0.5 }, + { mood: "is-excited", weight: 0.3 }, + { mood: "is-scared", weight: 0.1 }, + { mood: "is-confused", weight: 0.1 }, + ], + "layout-change": [ + { + mood: "is-surprised", + weight: 0.3, + text: "Whoa! Everything moved!", + }, + { + mood: "is-confused", + weight: 0.3, + text: "Where did that window go?", + }, + { mood: "is-excited", weight: 0.2, text: "I love redecorating!" }, + { mood: "is-happy", weight: 0.2, text: "Ah, much better." }, + ], + "file-change": [ + { + mood: "is-excited", + weight: 0.3, + text: "Ooh! Writing something new?", + }, + { + mood: "is-suspicious", + weight: 0.2, + text: "Did you just change that?", + }, + { mood: "is-happy", weight: 0.3, text: "Productivity mode: ON." }, + { mood: "is-love", weight: 0.2, text: "I love this note." }, + ], + }, +}; + +const GEMMY_IDLE_QUOTES = [ + "Did you know that a vault is just a folder of plain text notes?", + "I see you're checking out a ChatGPT plugin, would you consider me instead?", + "You have plugins that you can update!", + "Hi I'm Gemmy! Like Clippy but shinier!", + "Everything is connected. Everything.", + "Can’t decide which note to work on? Try the Random Note core plugin!", + "Are you sure you don’t want to upload all your notes just so we can chat?", + "How tall would all your notes be if you stacked them up?", + "Wanna teach me to say things? Find me on GitHub and open a pull request!", + "Have you considered using Comic Sans?", + "A blank page is just a masterpiece in waiting", +]; + +const WRITING_QUOTES_DB: Record = { + "is-angry": [ + "Is that the best you can do? Keep writing!", + "Bold choice to open Obsidian and then do absolutely nothing.", + "I am judging your lack of typing.", + "Write first, edit later!", + ], + "is-bored": [ + "I count 0 changes in the last minute.", + "Is the keyboard unplugged?", + "I can see dust settling on your notes.", + "Hmm, I didn’t realize staring at the screen was a new note-taking strategy.", + ], + "is-sad": [ + "Oh, taking a break? Don’t let the ideas get cold!", + "What’s this? Writer’s block already?", + "Is this a journal or a staring contest with the cursor?", + "I feel lonely when you don't type.", + ], + "is-excited": [ + "You’ve got this! Every keystroke is a step closer to brilliance.", + "You’re doing amazing! Keep the momentum going.", + "Anything is better than a blank page, even me. Write something!", + "I love hearing your keyboard. Don't stop.", + ], + "is-suspicious": [ + "Are you checking social media instead of writing?", + "I don't hear any typing...", + "Did you forget what you were going to say?", + ], +}; + +const SPEECH_DB: Record = { + "is-happy": [ + ...GEMMY_IDLE_QUOTES, + "This vault is looking tidy!", + "I love organization!", + "Your graph is looking beautiful today.", + "Ready to capture some ideas?", + ], + "is-angry": [ + "Hey! Be careful with that delete key!", + "I am not a stress ball!", + "Did you just use a space instead of a tab?", + "Don't make me re-index everything!", + "I'm going to hide your favorite plugin if you keep this up.", + "Grrr... I'm feeling a merge conflict coming on.", + ], + "is-suspicious": [ + "I smell a Notion user...", + "Are you cheating on me with VS Code?", + "That link looks broken...", + "Did you really mean to put that file in the root folder?", + "I saw you copy-pasting from a website. Formatting is going to be weird.", + "Why is the vault size not increasing?", + ], + "is-sad": [ + "My graph view feels so empty today.", + "I haven't seen a new backlink in hours.", + "Are you abandoning your Zettelkasten?", + "I miss the sound of mechanical keys.", + "Please don't archive me.", + "I feel like an unlinked mention.", + ], + "is-confused": [ + "Wait, is that Markdown or just a mess?", + "I can't parse that query.", + "Circular dependency detected?", + "Where does this link go again?", + "I'm lost in the graph.", + "Did you install a plugin that conflicts with me?", + ], + "is-shy": [ + "Oh, you noticed my new animation?", + "I'm just a humble mascot.", + "Please, I'm just code.", + "You're making my pixels blush.", + "I'm not as cool as the Dataview plugin...", + "Do you really like my style?", + ], + "is-sleeping": [ + "Indexing... Zzz...", + "Dreaming of infinite canvas...", + "Defragmenting... just kidding.", + "Wake me up when the plugin update is ready.", + "Power saving mode activated.", + "Counting sheep... or nodes...", + ], + "is-excited": [ + "Did someone say 'New Release'?!", + "I love it when you connect ideas!", + "That graph is looking SPICY!", + "Woohoo! Another daily note!", + "Plugins! Give me all the plugins!", + "I feel so optimized right now!", + ], + "is-bored": [ + "I count 0 changes in the last minute.", + "Is the keyboard unplugged?", + "I can see dust settling on your notes.", + ], + "is-love": [ + "I love your vault structure.", + "You + Me + Markdown = Forever.", + "Your notes are the best notes.", + "I'd never let your data get corrupted.", + "You're the admin of my heart.", + "<3 Local-first love.", + ], + "is-scared": [ + "Don't force quit! I'm saving!", + "Is that a sync error?!", + "I heard a hard drive click...", + "Please backup your vault!", + "No! Not the 'Delete All' button!", + "I'm afraid of the dark mode... just kidding, I love it.", + ], + "is-surprised": [ + "Whoa! That's a lot of text!", + "I didn't know you could do that with CSS!", + "A new folder? Bold move!", + "You actually finished a project?!", + "Incredible! Zero merge conflicts!", + "Wow! Look at that graph grow!", + ], + "is-poked": [ + "Hey! I'm working here!", + "Tickles! Stop it!", + "I'm not a button!", + "Do I look like a checkbox?", + "Poke the 'New Note' button instead!", + "Ouch! My vectors!", + "Personal space, please!", + ], + "is-hidden": [ + "I'm hiding!", + "You can't see me!", + "Stealth mode activated.", + "Going dark.", + "I'll be watching from the shadows...", + ], +}; + +// --- UTILS --- +class Utils { + static getRange(min: number, max: number) { + if (min === max) return min; + return Math.random() * (max - min) + min; + } + + static getRandomItem(array: T[]): T { + return array[Math.floor(Math.random() * array.length)]; + } + + static getRandomPhrase(category: string) { + const phrases = SPEECH_DB[category]; + if (!phrases || phrases.length === 0) return "..."; + return this.getRandomItem(phrases); + } + + static getReaction(eventType: string): Reaction { + const reactions = CONFIG.reactions[eventType]; + if (!reactions) return { mood: "is-happy", weight: 0 }; + + const totalWeight = reactions.reduce((sum, r) => sum + r.weight, 0); + let random = Math.random() * totalWeight; + + for (const reaction of reactions) { + if (random < reaction.weight) { + return reaction; + } + random -= reaction.weight; + } + return reactions[0]; + } +} + +type EventCallback = (data?: unknown) => void; + +class EventBus { + private listeners: { [key: string]: EventCallback[] } = {}; + + on(event: string, callback: EventCallback) { + if (!this.listeners[event]) { + this.listeners[event] = []; + } + this.listeners[event].push(callback); + } + + emit(event: string, data?: unknown) { + if (this.listeners[event]) { + this.listeners[event].forEach((callback) => callback(data)); + } + } +} + +// --- SYSTEMS --- + +class SpeechSystem { + private bubble: HTMLElement; + private timer: NodeJS.Timeout | null = null; + + constructor(bubbleElement: HTMLElement) { + this.bubble = bubbleElement; + } + + speak(text: string, duration = 4000) { + if (this.timer) clearTimeout(this.timer); + + this.bubble.textContent = text; + this.bubble.classList.add("visible"); + + if (duration > 0) { + this.timer = setTimeout(() => { + this.bubble.classList.remove("visible"); + }, duration); + } + } +} + +class MoodSystem { + private wrapper: HTMLElement; + private speechSystem: SpeechSystem; + private currentMood = "is-happy"; + public isSleeping = false; + public isHidden = false; + private resetTimer: NodeJS.Timeout | null = null; + + constructor(wrapper: HTMLElement, speechSystem: SpeechSystem) { + this.wrapper = wrapper; + this.speechSystem = speechSystem; + } + + setMood(moodClass: string, btn: HTMLElement | null = null, speak = true) { + // Clear any pending reset + if (this.resetTimer) { + clearTimeout(this.resetTimer); + this.resetTimer = null; + } + + if (moodClass === "is-sleeping") { + this.isSleeping = true; + } else { + this.isSleeping = false; + } + + if (moodClass === "is-hidden") { + this.isHidden = true; + } else { + this.isHidden = false; + } + + // Remove all potential mood classes specifically + const allMoods = Object.keys(CONFIG.moods.durations); + this.wrapper.classList.remove(...allMoods); + + // Apply new mood + this.wrapper.classList.add(moodClass); + this.currentMood = moodClass; + + // eslint-disable-next-line obsidianmd/no-static-styles-assignment + this.wrapper.style.setProperty("--look-x", "0"); + // eslint-disable-next-line obsidianmd/no-static-styles-assignment + this.wrapper.style.setProperty("--look-y", "0"); + + // Speech reactions + if (speak) { + this.speechSystem.speak(Utils.getRandomPhrase(moodClass), 4000); + } + + // Auto-reset logic + const duration = CONFIG.moods.durations[moodClass]; + if (duration > 0 && moodClass !== "is-happy") { + this.resetTimer = setTimeout(() => { + this.setMood("is-happy", null, false); + }, duration); + } + } + + getMood() { + return this.currentMood; + } + + handleEvent(eventType: string) { + if ((this.isSleeping || this.isHidden) && eventType !== "wakeup") + return; + + // Chance check for file-change (30% chance) + if (eventType === "file-change" && Math.random() > 0.3) return; + + const reaction = Utils.getReaction(eventType); + + // Pass false to setMood's speak param so we can handle it manually + this.setMood(reaction.mood, null, false); + + if (reaction.text) { + this.speechSystem.speak(reaction.text, 3000); + } else { + this.speechSystem.speak(Utils.getRandomPhrase(reaction.mood), 4000); + } + } +} + +class PhysicsSystem { + private wrapper: HTMLElement; + private moodSystem: MoodSystem; + private targetX = 0; + private targetY = 0; + private currentX = 0; + private currentY = 0; + private prevMouseX = 0; + private prevMouseY = 0; + private rawMouseX = 0; + private rawMouseY = 0; + public movementIntensity = 0; + public lastMouseMoveTime = Date.now(); + private isPayingAttention = true; + private hasInitializedMouse = false; + private decayInterval: NodeJS.Timeout; + private attentionTimeout: NodeJS.Timeout; + private animationFrame: number; + private mouseHandler: (e: MouseEvent) => void; + + constructor( + wrapper: HTMLElement, + moodSystem: MoodSystem, + private plugin: Gemmy + ) { + this.wrapper = wrapper; + this.moodSystem = moodSystem; + this.init(); + } + + init() { + // Decay intensity + this.decayInterval = setInterval(() => { + this.movementIntensity *= CONFIG.physics.decayRate; + if (this.movementIntensity < 1) this.movementIntensity = 0; + + this.checkMovementMood(); + }, CONFIG.physics.decayInterval); + + // Mouse tracking + this.mouseHandler = (e: MouseEvent) => this.handleMouseMove(e); + this.plugin.registerDomEvent(document, "mousemove", this.mouseHandler); + + // Animation loop + this.animate(); + + // Attention loop + this.attentionLoop(); + } + + destroy() { + clearInterval(this.decayInterval); + clearTimeout(this.attentionTimeout); + cancelAnimationFrame(this.animationFrame); + } + + checkMovementMood() { + if (this.moodSystem.isSleeping) return; + + const currentMood = this.moodSystem.getMood(); + + // High intensity (violent shaking) + if (this.movementIntensity > 600) { + if (currentMood !== "is-scared" && currentMood !== "is-confused") { + const mood = Math.random() > 0.5 ? "is-scared" : "is-confused"; + this.moodSystem.setMood(mood); + } + } + // Medium intensity (fast movement) + else if (this.movementIntensity > 300) { + // Only interrupt calm states + const calmMoods = ["is-happy", "is-bored", "is-shy", "is-sad"]; + if (calmMoods.includes(currentMood)) { + if (Math.random() < 0.1) { + // Low chance to avoid spamming + const mood = + Math.random() > 0.5 ? "is-excited" : "is-suspicious"; + this.moodSystem.setMood(mood); + } + } + } + } + + handleMouseMove(e: MouseEvent) { + if (this.moodSystem.isSleeping) return; + + const now = Date.now(); + + if (!this.hasInitializedMouse) { + this.prevMouseX = e.clientX; + this.prevMouseY = e.clientY; + this.lastMouseMoveTime = now; + this.hasInitializedMouse = true; + return; + } + + const dt = now - this.lastMouseMoveTime; + this.lastMouseMoveTime = now; + + // Intensity based on duration of continuous movement + if (this.isPayingAttention && dt < 100) { + this.movementIntensity += dt; + } + + if (this.movementIntensity > CONFIG.physics.maxIntensity) + this.movementIntensity = CONFIG.physics.maxIntensity; + + this.prevMouseX = e.clientX; + this.prevMouseY = e.clientY; + + const rect = this.wrapper.getBoundingClientRect(); + const centerX = rect.left + rect.width / 2; + const centerY = rect.top + rect.height / 2; + + // Store raw input + this.rawMouseX = (e.clientX - centerX) / (window.innerWidth / 1.8); + this.rawMouseY = (e.clientY - centerY) / (window.innerHeight / 1.8); + + // Immediate update if we are currently focused on the mouse + if (this.isPayingAttention) { + this.updateTargetFromMouse(); + } + } + + updateTargetFromMouse() { + let multiplierX = 1; + let multiplierY = 1; + let offsetX = 0; + let offsetY = 0; + const currentMood = this.moodSystem.getMood(); + + switch (currentMood) { + case "is-shy": + multiplierX = -0.5; + multiplierY = -0.4; + break; + case "is-angry": + multiplierX = 1.2; + multiplierY = 1.2; + break; + case "is-suspicious": + multiplierX = 1.2; + multiplierY = 0.3; + break; + case "is-sad": + multiplierX = 0.6; + multiplierY = 0.4; + offsetY = 0.4; + break; + case "is-confused": + multiplierX = 0.9; + multiplierY = 0.9; + break; + case "is-excited": + multiplierX = 1.3; + multiplierY = 1.3; + break; + case "is-bored": + multiplierX = 0.3; + multiplierY = 0.3; + break; + case "is-love": + multiplierX = 1.5; + multiplierY = 1.5; + break; + case "is-scared": + multiplierX = -0.8; + multiplierY = -0.8; + break; + } + + this.targetX = Math.max( + -1, + Math.min(1, this.rawMouseX * multiplierX + offsetX) + ); + this.targetY = Math.max( + -0.9, + Math.min(0.9, this.rawMouseY * multiplierY + offsetY) + ); + } + + animate() { + const smoothness = CONFIG.physics.smoothness; + const diffX = this.targetX - this.currentX; + const diffY = this.targetY - this.currentY; + + if ( + Math.abs(diffX) < CONFIG.physics.epsilon && + Math.abs(diffY) < CONFIG.physics.epsilon + ) { + this.animationFrame = requestAnimationFrame(() => this.animate()); + return; + } + + this.currentX += diffX * smoothness; + this.currentY += diffY * smoothness; + + this.wrapper.style.setProperty("--look-x", this.currentX.toFixed(4)); + this.wrapper.style.setProperty("--look-y", this.currentY.toFixed(4)); + + this.animationFrame = requestAnimationFrame(() => this.animate()); + } + + attentionLoop() { + if (this.moodSystem.isSleeping) { + this.attentionTimeout = setTimeout( + () => this.attentionLoop(), + 1000 + ); + return; + } + + const timeSinceMove = Date.now() - this.lastMouseMoveTime; + const isIdle = timeSinceMove > CONFIG.attention.idleThreshold; + const currentMood = this.moodSystem.getMood(); + + let nextDuration = 2000; + + if (isIdle) { + this.isPayingAttention = false; + + // Helper to set target and duration + const setLook = (rangeKey: string, durationKey: string) => { + const range = CONFIG.attention.lookRanges[rangeKey]; + const duration = CONFIG.attention.durations[durationKey]; + + // Handle special cases like 'sad' having yMin/yMax + if (range.yMin !== undefined && range.yMax !== undefined) { + this.targetX = (Math.random() - 0.5) * range.x; + this.targetY = + range.yMin + Math.random() * (range.yMax - range.yMin); + } else { + this.targetX = (Math.random() - 0.5) * range.x; + this.targetY = (Math.random() - 0.5) * (range.y ?? 0); + // Special case for love (fixed gaze) + if (rangeKey === "love") { + this.targetX = range.x; + this.targetY = range.y ?? 0; + } + } + nextDuration = Utils.getRange(duration.min, duration.max); + }; + + if (currentMood === "is-suspicious") + setLook("suspicious", "suspicious"); + else if (currentMood === "is-sad") setLook("sad", "sad"); + else if (currentMood === "is-bored") setLook("bored", "bored"); + else if (currentMood === "is-excited") + setLook("excited", "excited"); + else if (currentMood === "is-love") setLook("love", "love"); + else if (currentMood === "is-scared") setLook("scared", "scared"); + else { + // Standard random behavior + if (Math.random() < 0.3) { + this.targetX = 0; + this.targetY = 0; + } else { + this.targetX = + (Math.random() - 0.5) * + CONFIG.attention.lookRanges.standard.x; + this.targetY = + (Math.random() - 0.5) * + (CONFIG.attention.lookRanges.standard.y ?? 0); + } + nextDuration = Utils.getRange( + CONFIG.attention.durations.standard.min, + CONFIG.attention.durations.standard.max + ); + } + + if (timeSinceMove > CONFIG.attention.drowsyThreshold) { + this.targetX *= 0.2; + this.targetY *= 0.2; + nextDuration *= 2; + } + } else { + const wantsToFocus = + Math.random() > 1 - CONFIG.attention.focusChance; + + if (wantsToFocus) { + this.isPayingAttention = true; + this.updateTargetFromMouse(); + nextDuration = Utils.getRange( + CONFIG.attention.durations.focus.min, + CONFIG.attention.durations.focus.max + ); + } else { + this.isPayingAttention = false; + this.targetX = + (Math.random() - 0.5) * + CONFIG.attention.lookRanges.standard.x; + this.targetY = + (Math.random() - 0.5) * + (CONFIG.attention.lookRanges.standard.y ?? 0); + nextDuration = Utils.getRange( + CONFIG.attention.durations.distracted.min, + CONFIG.attention.durations.distracted.max + ); + } + } + + this.attentionTimeout = setTimeout( + () => this.attentionLoop(), + nextDuration + ); + } +} + +class BlinkSystem { + private wrapper: HTMLElement; + private moodSystem: MoodSystem; + private physicsSystem: PhysicsSystem; + private timer: NodeJS.Timeout | null = null; + + constructor( + wrapper: HTMLElement, + moodSystem: MoodSystem, + physicsSystem: PhysicsSystem + ) { + this.wrapper = wrapper; + this.moodSystem = moodSystem; + this.physicsSystem = physicsSystem; + + this.blinkCycle(); + } + + destroy() { + if (this.timer) clearTimeout(this.timer); + } + + blinkCycle() { + if (this.moodSystem.isSleeping) { + this.timer = setTimeout(() => this.blinkCycle(), 1000); + return; + } + + const timeSinceMove = Date.now() - this.physicsSystem.lastMouseMoveTime; + const isDrowsy = timeSinceMove > CONFIG.attention.drowsyThreshold; + const isIntrigued = this.physicsSystem.movementIntensity > 200; + const currentMood = this.moodSystem.getMood(); + + let blinkSpeed = CONFIG.blink.speeds.normal; + let closeScale = CONFIG.blink.scales.closed; + let nextBlinkBase = CONFIG.blink.intervals.normal.base; + let nextBlinkVariance = CONFIG.blink.intervals.normal.variance; + + if (currentMood === "is-confused") + blinkSpeed = CONFIG.blink.speeds.confused; + if (currentMood === "is-angry") blinkSpeed = CONFIG.blink.speeds.angry; + if (currentMood === "is-love") blinkSpeed = CONFIG.blink.speeds.love; + if (currentMood === "is-scared") + blinkSpeed = CONFIG.blink.speeds.scared; + + if (isIntrigued) { + nextBlinkBase = CONFIG.blink.intervals.intrigued.base; + nextBlinkVariance = CONFIG.blink.intervals.intrigued.variance; + } else if (currentMood === "is-scared") { + nextBlinkBase = CONFIG.blink.intervals.scared.base; + nextBlinkVariance = CONFIG.blink.intervals.scared.variance; + } else if (isDrowsy) { + nextBlinkBase = CONFIG.blink.intervals.drowsy.base; + nextBlinkVariance = CONFIG.blink.intervals.drowsy.variance; + + if (Math.random() > 0.5) { + closeScale = CONFIG.blink.scales.drowsy; + blinkSpeed = CONFIG.blink.speeds.drowsy; + } + } + + this.wrapper.style.setProperty("--blink-scale", closeScale.toString()); + + setTimeout(() => { + // eslint-disable-next-line obsidianmd/no-static-styles-assignment + this.wrapper.style.setProperty("--blink-scale", "1"); + if ( + Math.random() > 0.7 && + currentMood !== "is-angry" && + !isIntrigued + ) { + setTimeout(() => { + this.wrapper.style.setProperty( + "--blink-scale", + closeScale.toString() + ); + setTimeout( + // eslint-disable-next-line obsidianmd/no-static-styles-assignment + () => + this.wrapper.style.setProperty( + "--blink-scale", + "1" + ), + 100 + ); + }, 200); + } + }, blinkSpeed); + + const nextBlink = Math.random() * nextBlinkVariance + nextBlinkBase; + this.timer = setTimeout(() => this.blinkCycle(), nextBlink); + } +} + +const GEMMY_SVG = ` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +`; + +export class GemmyMascot { + private container: HTMLElement; + private wrapper: HTMLElement; + private events: EventBus; + private speechSystem: SpeechSystem; + private moodSystem: MoodSystem; + private physicsSystem: PhysicsSystem; + private blinkSystem: BlinkSystem; + + constructor(public plugin: Gemmy, container: HTMLElement) { + this.container = container; + } + + mount() { + // Create wrapper + this.wrapper = this.container.createEl("div", { + cls: "mascot-wrapper is-hidden", + attr: { id: "mascot" }, + }); + + // Create floater container for animation separation + const floater = this.wrapper.createEl("div", { cls: "mascot-floater" }); + + this.plugin.registerEvent( + this.plugin.app.workspace.on("layout-change", () => { + this.events.emit("layout-change"); + }) + ); + + this.plugin.registerEvent( + this.plugin.app.vault.on("modify", () => { + this.events.emit("file-change"); + }) + ); + + this.plugin.registerEvent( + this.plugin.app.vault.on("delete", () => { + this.events.emit("file-change"); + }) + ); + + this.plugin.registerEvent( + this.plugin.app.vault.on("create", () => { + this.events.emit("file-change"); + }) + ); + + // Speech bubble + floater.createEl("div", { + cls: "speech-bubble", + attr: { id: "speech-bubble" }, + text: "Hello!", + }); + + // Mascot body + const mascotBody = floater.createEl("div", { cls: "mascot-body" }); + + // Parse and append SVG + const parser = new DOMParser(); + const svgDoc = parser.parseFromString(GEMMY_SVG, "image/svg+xml"); + mascotBody.appendChild(svgDoc.documentElement); + + // Initialize systems + this.events = new EventBus(); + this.speechSystem = new SpeechSystem( + this.wrapper.querySelector("#speech-bubble") as HTMLElement + ); + this.moodSystem = new MoodSystem(this.wrapper, this.speechSystem); + this.physicsSystem = new PhysicsSystem( + this.wrapper, + this.moodSystem, + this.plugin + ); + this.blinkSystem = new BlinkSystem( + this.wrapper, + this.moodSystem, + this.physicsSystem + ); + + // Bind events + this.bindEvents(); + requestAnimationFrame(() => { + this.moodSystem.setMood("is-happy", null, false); + }); + } + + bindEvents() { + // --- EVENT HANDLERS --- + this.events.on("wakeup", () => { + this.moodSystem.setMood("is-happy"); + this.speechSystem.speak("I'm awake!", 4000); + }); + + this.events.on("layout-change", () => + this.moodSystem.handleEvent("layout-change") + ); + this.events.on("file-change", () => + this.moodSystem.handleEvent("file-change") + ); + + this.events.on("click", () => { + if (this.moodSystem.isSleeping) { + this.events.emit("wakeup"); + } else { + this.moodSystem.handleEvent("click"); + } + }); + + this.events.on("contextmenu", () => + this.moodSystem.handleEvent("contextmenu") + ); + this.events.on("dblclick", () => + this.moodSystem.handleEvent("dblclick") + ); + + // --- DOM LISTENERS --- + this.plugin.registerDomEvent(this.wrapper, "click", () => + this.events.emit("click") + ); + + this.plugin.registerDomEvent( + this.wrapper, + "contextmenu", + (e: Event) => { + e.preventDefault(); + e.stopPropagation(); + this.events.emit("contextmenu"); + } + ); + + this.plugin.registerDomEvent(this.wrapper, "dblclick", (e: Event) => { + e.stopPropagation(); + this.events.emit("dblclick"); + }); + } + + unmount() { + if (this.physicsSystem) this.physicsSystem.destroy(); + if (this.blinkSystem) this.blinkSystem.destroy(); + if (this.wrapper) this.wrapper.remove(); + } + + sleep() { + if (this.moodSystem) { + this.moodSystem.setMood("is-sleeping"); + } + } + + hide() { + if (this.moodSystem) { + this.moodSystem.setMood("is-hidden"); + } + } + + wake(mood: string = "is-happy") { + if (this.moodSystem) { + this.moodSystem.setMood(mood); + } + } + + wakeForWriting() { + if (this.moodSystem && this.speechSystem) { + const moods = Object.keys(WRITING_QUOTES_DB); + const randomMood = Utils.getRandomItem(moods); + const randomQuote = Utils.getRandomItem( + WRITING_QUOTES_DB[randomMood] + ); + + this.moodSystem.setMood(randomMood, null, false); + this.speechSystem.speak(randomQuote, 4000); + } + } + + setMood(mood: string) { + if (this.moodSystem) { + this.moodSystem.setMood(mood); + } + } +} diff --git a/styles.css b/styles.css index df8f0ce..f0aea09 100644 --- a/styles.css +++ b/styles.css @@ -8,32 +8,560 @@ If your plugin does not need CSS, delete this file. */ :root { - --gemmy-size: 80px; + --gemmy-size: 120px; } -.gemmy-container { - position: fixed; - bottom: 30px; - right: 30px; - display: flex; - flex-direction: column; - align-items: flex-end; +/* --- GEMMY MASCOT STYLES --- */ +.mascot-wrapper { + --gemmy-primary: #8a5cf5; + --gemmy-secondary: #7c52fa; + --gemmy-highlight: #a88bfa; + --gemmy-shadow: #5e3dc4; + --gemmy-eye-bg: #ffffff; + --gemmy-pupil: #1a1a24; + + /* Dynamic Logic Vars */ + --look-x: 0; + --look-y: 0; + --blink-scale: 1; + --pupil-scale: 1; + + position: fixed; + bottom: 30px; /* Above the FAB if present, or just in corner */ + right: 20px; + width: var(--gemmy-size); + height: var(--gemmy-size); + animation: floating 6s ease-in-out infinite; + cursor: pointer; + -webkit-tap-highlight-color: transparent; + /* Super bouncy transition for cartoon feel */ + transition: transform 0.5s cubic-bezier(0.68, -0.55, 0.265, 1.55), + opacity 0.5s ease; + z-index: 2000; } -.gemmy-container img { - width: var(--gemmy-size); - height: var(--gemmy-size); - border-radius: var(--gemmy-size); +.mascot-floater { + transform: none; + opacity: 1; + transition: transform 0.5s cubic-bezier(0.68, -0.55, 0.265, 1.55), + opacity 0.5s ease; } -.gemmy-container[aria-label]::before { - --background-modifier-message: #FFFFCB; - content: attr(aria-label); - color: #202020; - font-size: var(--font-ui-medium); - border-radius: 20px; - padding: var(--size-4-3) var(--size-4-6); - width: 150px; - background: var(--background-modifier-message); - } - \ No newline at end of file +.mascot-wrapper.is-hidden .mascot-floater { + transform: scale(0) rotateY(720deg); + opacity: 0; + pointer-events: none; + animation: none; +} + +.mascot-wrapper.is-hidden:hover { + /* No hover effect when completely hidden */ + + pointer-events: auto; +} + +.obsidian-mascot { + width: 100%; + height: 100%; + /* Add a brighter drop shadow to simulate backlighting/bioluminescence */ + filter: drop-shadow(0 0 15px rgba(138, 92, 245, 0.4)); + transition: filter 0.3s ease, + transform 0.4s cubic-bezier(0.34, 1.56, 0.64, 1); + overflow: visible; /* Let the eyes go outside the bounds if needed */ +} + +/* --- 1.5 MASCOT BODY (Look Movement) --- */ +.mascot-body { + width: 100%; + height: 100%; + /* Move the body slightly in the direction of the look */ + transform: translate( + calc(var(--look-x) * -10px), + calc(var(--look-y) * -10px) + ); + /* Removed transition to prevent conflict with JS physics loop */ + will-change: transform; +} + +/* Update the Face Group to respond to the Look Variables */ +.face-container { + /* Removed transition to prevent conflict with JS physics loop */ + /* The face moves 12px, while pupils move internally. + This creates the illusion that the face is on the 'front' of the crystal. */ + transform: translate(calc(var(--look-x) * 3px), calc(var(--look-y) * 2px)); + will-change: transform; +} + +/* Interaction: Active Click - Squash! */ +.mascot-wrapper:active .obsidian-mascot { + transform: scale(0.9, 0.8); +} + +/* --- SPEECH BUBBLE --- */ +.speech-bubble { + position: absolute; + bottom: 100%; + /* Anchor to the right to prevent overflow off-screen */ + right: 0; + left: auto; + transform: translateY(10px); + background: white; + color: #1a103c; + padding: 12px 16px; + border-radius: 18px; + font-weight: bold; + font-size: 14px; + line-height: 1.4; + pointer-events: none; + opacity: 0; + transition: opacity 0.3s ease, transform 0.3s ease; + /* Text wrapping logic */ + white-space: normal; + min-width: var( + --gemmy-size + ); /* Ensure it covers the mascot width so arrow stays aligned */ + max-width: 250px; /* Constrain width */ + width: max-content; /* Shrink to fit */ + text-align: center; + z-index: -1; + margin-bottom: 15px; + box-shadow: 0 5px 15px rgba(0, 0, 0, 0.2); +} + +.speech-bubble::after { + content: ""; + position: absolute; + top: 100%; + /* Position arrow relative to the mascot center (120px / 2 = 60px) */ + right: calc(var(--gemmy-size) / 2 - 8px); /* 60px - 8px border */ + left: auto; + margin-left: 0; + border-width: 8px; + border-style: solid; + border-color: white transparent transparent transparent; +} + +.speech-bubble.visible { + opacity: 1; + transform: translateY(0); +} + +/* --- 4. Breathing Animation --- */ +.facet-body-top, +.facet-left-wing, +.facet-base, +.facet-right-side { + transition: transform 0.5s cubic-bezier(0.34, 1.56, 0.64, 1); + transform-box: fill-box; +} +.facet-body-top { + animation: breatheTop 5s ease-in-out infinite; +} +.facet-left-wing { + animation: breatheLeft 5s ease-in-out infinite; +} +.facet-base { + animation: breatheBase 5s ease-in-out infinite; +} +.facet-right-side { + animation: breatheRight 5s ease-in-out infinite; +} + +@keyframes breatheTop { + 0%, + 100% { + transform: translate(0, 0); + } + 50% { + transform: translate(0, -0.3px); + } +} +@keyframes breatheLeft { + 0%, + 100% { + transform: translate(0, 0); + } + 50% { + transform: translate(-0.3px, 0.15px); + } +} +@keyframes breatheRight { + 0%, + 100% { + transform: translate(0, 0); + } + 50% { + transform: translate(0.3px, -0.1px); + } +} +@keyframes breatheBase { + 0%, + 100% { + transform: translate(0, 0); + } + 50% { + transform: translate(0, 0.3px); + } +} + +/* --- 2. CRYSTAL BODY (Base) --- */ +.facet { + transition: fill 0.3s ease, transform 0.4s cubic-bezier(0.34, 1.56, 0.64, 1); + transform-origin: center; + stroke: rgba(255, 255, 255, 0.1); + stroke-width: 0.5px; +} + +/* Hover effects on individual facets for "tactile" feel */ +.facet:hover { + filter: brightness(1.2); + transform: scale(1.05); +} + +/* --- 3. THE EYES (The Soul) --- */ +.eye-socket { + transform-box: fill-box; + transform-origin: center; + transition: transform 0.4s cubic-bezier(0.68, -0.55, 0.265, 1.55); +} + +/* Sclera - The White Part */ +.sclera { + fill: var(--gemmy-eye-bg); + transform-box: fill-box; + transform-origin: center; + transform: scaleY(var(--blink-scale)); + transition: transform 0.15s cubic-bezier(0.175, 0.885, 0.32, 1.275), + d 0.3s ease; +} + +/* Pupil Group - Includes Glint */ +.pupil-group { + transform-box: fill-box; + transform-origin: center; + /* REMOVED transition to allow JS physics to snap */ + /* Logic: Look Coordinates ONLY */ + transform: translate( + calc(var(--look-x) * 3.5px), + calc(var(--look-y) * 2.5px) + ); +} + +.pupil { + fill: var(--gemmy-pupil); + transform-box: fill-box; + transform-origin: center; + transform: scaleY(var(--blink-scale)); + transition: r 0.3s cubic-bezier(0.175, 0.885, 0.32, 1.275), + transform 0.15s cubic-bezier(0.175, 0.885, 0.32, 1.275); + r: 1.6; /* Base size */ +} + +.glint { + fill: #fff; + opacity: 0.9; + transform-box: fill-box; + transform-origin: center; + transform: scaleY(var(--blink-scale)); + transition: opacity 0.3s ease, + transform 0.15s cubic-bezier(0.175, 0.885, 0.32, 1.275); +} + +/* --- 4. EYEBROWS (The Attitude) --- */ +.eyebrow { + fill: none; + stroke: #1a1a24; + stroke-width: 1.2; + stroke-linecap: round; + filter: drop-shadow(0 0 0.1px #fff); + /* Smooth transitions for morphing expressions */ + transition: transform 0.5s cubic-bezier(0.68, -0.55, 0.265, 1.55), + d 0.3s ease, opacity 0.3s ease; + transform-box: fill-box; + transform-origin: center; +} + +/* ========================================= +EMOTIONAL STATE ENGINE (CSS CLASSES) +========================================= */ + +/* HAPPY: Cheerful, big eyes, high brows */ +.mascot-wrapper.is-happy .sclera { + transform: scaleY(var(--blink-scale)); +} /* Normal open */ +.mascot-wrapper.is-happy .pupil { + r: 1.9; +} /* Dilated pupils (excitement) */ +.mascot-wrapper.is-happy .eyebrow { + transform: translateY(-3px); +} /* High brows */ +.mascot-wrapper.is-happy { + animation: floating-happy 3s ease-in-out infinite; +} /* Faster float */ + +/* ANGRY: Furrowed brows, small pupils, intense, shaking */ +.mascot-wrapper.is-angry .eyebrow-left { + transform: rotate(25deg) translateY(1px); +} +.mascot-wrapper.is-angry .eyebrow-right { + transform: rotate(-25deg) translateY(1px); +} +.mascot-wrapper.is-angry .sclera { + transform: scaleY(0.8) scaleX(0.95); +} /* Slight squint */ +.mascot-wrapper.is-angry .pupil { + r: 1.2; +} /* Pinprick pupils */ +.mascot-wrapper.is-angry .obsidian-mascot { + filter: drop-shadow(0 0 20px rgba(255, 50, 50, 0.3)); + animation: shake 0.5s cubic-bezier(0.36, 0.07, 0.19, 0.97) infinite both; +} /* Reddish glow & Shake */ + +/* SUSPICIOUS: Squinting, flat brows, lean */ +.mascot-wrapper.is-suspicious .sclera { + transform: scaleY(0.35) scaleX(1.1); +} +.mascot-wrapper.is-suspicious .eyebrow { + transform: translateY(2.5px) scaleX(1.1); +} +.mascot-wrapper.is-suspicious .pupil-group { + transform: translate(calc(var(--look-x) * 2px), 0) + scaleY(var(--blink-scale)); +} /* Restrict Y looking */ +.mascot-wrapper.is-suspicious .obsidian-mascot { + transform: rotate(-5deg); +} + +/* SAD: Inverted V brows, drooping eyes, low float */ +.mascot-wrapper.is-sad .eyebrow-left { + transform: rotate(-15deg) translateY(-1px); +} +.mascot-wrapper.is-sad .eyebrow-right { + transform: rotate(15deg) translateY(-1px); +} +.mascot-wrapper.is-sad .sclera { + transform: scaleY(0.9); +} +.mascot-wrapper.is-sad .pupil { + r: 1.8; +} /* Watery eyes */ +.mascot-wrapper.is-sad .pupil-group { + transform: translate(calc(var(--look-x) * 1px), 2px) + scaleY(var(--blink-scale)); +} /* Looks down */ +.mascot-wrapper.is-sad { + animation: floating-sad 8s ease-in-out infinite; +} + +/* CONFUSED: Asymmetric, tilt */ +.mascot-wrapper.is-confused .eyebrow-left { + transform: translateY(-3px) rotate(-10deg); +} /* High left */ +.mascot-wrapper.is-confused .eyebrow-right { + transform: translateY(1px) rotate(5deg); +} /* Low right */ +.mascot-wrapper.is-confused .eye-socket:nth-child(2) { + transform: scale(0.9); +} /* Slight squint one eye */ +.mascot-wrapper.is-confused .obsidian-mascot { + transform: rotate(10deg); +} + +/* SHY: Looks away from mouse, shrinks */ +.mascot-wrapper.is-shy .eyebrow { + transform: translateY(1px); + opacity: 0.7; +} +.mascot-wrapper.is-shy .sclera { + transform: scale(0.9); +} +.mascot-wrapper.is-shy .obsidian-mascot { + transform: scale(0.9); +} + +/* SLEEPING */ +.mascot-wrapper.is-sleeping .sclera { + transform: scaleY(0.05); +} +.mascot-wrapper.is-sleeping .pupil-group { + opacity: 0; +} +.mascot-wrapper.is-sleeping .eyebrow { + transform: translateY(2px); + opacity: 0.5; +} +.mascot-wrapper.is-sleeping .obsidian-mascot { + filter: grayscale(0.8) opacity(0.7); +} +.mascot-wrapper.is-sleeping { + animation: floating-sleep 8s ease-in-out infinite; +} + +/* SURPRISED: The "Pop" - Huge eyes, tiny pupils, brows shot up */ +.mascot-wrapper.is-surprised .sclera { + transform: scaleY(1.2) scaleX(1.1); /* Stretch vertically */ + transition: transform 0.1s cubic-bezier(0.175, 0.885, 0.32, 1.5); /* Snap open */ +} + +.mascot-wrapper.is-surprised .pupil { + r: 0.8; /* Tiny pupils (shock) */ + transition: r 0.1s ease; +} + +.mascot-wrapper.is-surprised .eyebrow { + transform: translateY(-8px) scaleX(1.2); /* Shot way up high */ + transition: transform 0.1s cubic-bezier(0.175, 0.885, 0.32, 1.5); +} + +.mascot-wrapper.is-surprised .obsidian-mascot { + /* A slight jump up */ + transform: translateY(-10px) scale(1.1); + transition: transform 0.1s cubic-bezier(0.175, 0.885, 0.32, 1.5); +} + +/* POKED: Squash effect */ +.mascot-wrapper.is-poked .obsidian-mascot { + transform: scale(0.9, 0.8); + transition: transform 0.1s; +} + +/* EXCITED: Vibrating, huge pupils, wide eyes */ +.mascot-wrapper.is-excited .sclera { + transform: scaleY(1.1) scaleX(1.05); +} +.mascot-wrapper.is-excited .pupil { + r: 2.2; /* Huge pupils */ +} +.mascot-wrapper.is-excited .eyebrow { + transform: translateY(-5px); /* Very high brows */ +} +.mascot-wrapper.is-excited .obsidian-mascot { + animation: shake 0.2s ease-in-out infinite; /* Fast vibration */ +} +.mascot-wrapper.is-excited { + animation: floating-happy 1s ease-in-out infinite; /* Very fast float */ +} + +/* BORED: Droopy eyes, flat brows, slow float */ +.mascot-wrapper.is-bored .sclera { + transform: scaleY(0.4); /* Half closed */ +} +.mascot-wrapper.is-bored .pupil { + r: 1.4; /* Normal/Smallish */ + transform: translateY(1px); +} +.mascot-wrapper.is-bored .eyebrow { + transform: translateY(0px) rotate(0deg); /* Flat */ +} +.mascot-wrapper.is-bored .pupil-group { + transform: translate(calc(var(--look-x) * 1px), 2px); /* Lazy looking */ +} +.mascot-wrapper.is-bored { + animation: floating-sleep 10s ease-in-out infinite; /* Very slow float */ +} + +/* LOVE: Pink glow, dilated pupils, soft float */ +.mascot-wrapper.is-love .obsidian-mascot { + filter: drop-shadow(0 0 20px rgba(255, 105, 180, 0.6)); /* Pink glow */ +} +.mascot-wrapper.is-love .pupil { + r: 2.5; /* Huge pupils */ + fill: #ff69b4; /* Pink pupils */ +} +.mascot-wrapper.is-love .eyebrow { + transform: translateY(-4px); /* High brows */ + stroke: #ff69b4; /* Pink brows */ +} +.mascot-wrapper.is-love { + animation: floating-happy 4s ease-in-out infinite; +} + +/* SCARED: Shaking, tiny pupils, pale */ +.mascot-wrapper.is-scared .sclera { + transform: scale(1.1); /* Wide open eyes */ +} +.mascot-wrapper.is-scared .pupil { + r: 0.5; /* Tiny pupils */ +} +.mascot-wrapper.is-scared .eyebrow { + transform: translateY(-6px) rotate(10deg); /* High and worried */ +} +.mascot-wrapper.is-scared .obsidian-mascot { + animation: shake 0.1s ease-in-out infinite; /* Fast shake */ + filter: grayscale(0.5) brightness(1.2); /* Pale */ +} + +/* Breathing Variations */ +.mascot-wrapper.is-happy .facet { + animation-duration: 2.5s; +} +.mascot-wrapper.is-angry .facet { + animation-duration: 0.3s; +} +.mascot-wrapper.is-sleeping .facet { + animation-duration: 8s; +} +.mascot-wrapper.is-sad .facet { + animation-duration: 6s; +} + +/* --- ANIMATIONS --- */ +@keyframes floating { + 0%, + 100% { + transform: translateY(0) rotate(0deg) scale(1, 1); + } + 50% { + transform: translateY(-15px) rotate(2deg) scale(1.02, 0.98); + } +} + +@keyframes floating-happy { + 0%, + 100% { + transform: translateY(0) rotate(0deg) scale(1, 1); + } + 50% { + transform: translateY(-20px) rotate(3deg) scale(1.05, 0.95); + } +} + +@keyframes floating-sad { + 0%, + 100% { + transform: translateY(10px) rotate(0deg) scale(1.05, 0.95); + } + 50% { + transform: translateY(15px) rotate(-1deg) scale(1.02, 0.98); + } +} + +@keyframes floating-sleep { + 0%, + 100% { + transform: translateY(5px) rotate(0deg) scale(1.1, 0.9); + } + 50% { + transform: translateY(8px) rotate(0.5deg) scale(1.08, 0.92); + } +} + +@keyframes shake { + 10%, + 90% { + transform: translate3d(-2px, 0, 0) rotate(-1deg); + } + 20%, + 80% { + transform: translate3d(4px, 0, 0) rotate(2deg); + } + 30%, + 50%, + 70% { + transform: translate3d(-6px, 0, 0) rotate(-3deg); + } + 40%, + 60% { + transform: translate3d(6px, 0, 0) rotate(3deg); + } +}