├── .editorconfig ├── .eslintignore ├── .eslintrc ├── .github ├── media │ ├── crafty-navigation_1.1.gif │ ├── crafty-search.gif │ └── crafty-toltip_1.1.gif └── workflows │ └── release.yml ├── .gitignore ├── .npmrc ├── LICENSE ├── README.md ├── __tests__ ├── node.test.ts └── search.test.ts ├── dom └── handler.ts ├── esbuild.config.mjs ├── io └── fileHandler.ts ├── main.ts ├── manifest.json ├── nodes └── nodes.ts ├── observers └── observer.ts ├── package.json ├── pnpm-lock.yaml ├── specification └── index.ts ├── styles.css ├── tsconfig.json ├── version-bump.mjs └── versions.json /.editorconfig: -------------------------------------------------------------------------------- 1 | # top-most EditorConfig file 2 | root = true 3 | 4 | [*] 5 | charset = utf-8 6 | end_of_line = lf 7 | insert_final_newline = true 8 | indent_style = tab 9 | indent_size = 4 10 | tab_width = 4 11 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | 3 | main.js 4 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "parser": "@typescript-eslint/parser", 4 | "env": { "node": true }, 5 | "plugins": [ 6 | "@typescript-eslint" 7 | ], 8 | "extends": [ 9 | "eslint:recommended", 10 | "plugin:@typescript-eslint/eslint-recommended", 11 | "plugin:@typescript-eslint/recommended" 12 | ], 13 | "parserOptions": { 14 | "sourceType": "module" 15 | }, 16 | "rules": { 17 | "no-unused-vars": "off", 18 | "@typescript-eslint/no-unused-vars": ["error", { "args": "none" }], 19 | "@typescript-eslint/ban-ts-comment": "off", 20 | "no-prototype-builtins": "off", 21 | "@typescript-eslint/no-empty-function": "off" 22 | } 23 | } -------------------------------------------------------------------------------- /.github/media/crafty-navigation_1.1.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/liolle/Crafty/12952ae2775b54cb5bd1c5a89f3dd41f8431d0ca/.github/media/crafty-navigation_1.1.gif -------------------------------------------------------------------------------- /.github/media/crafty-search.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/liolle/Crafty/12952ae2775b54cb5bd1c5a89f3dd41f8431d0ca/.github/media/crafty-search.gif -------------------------------------------------------------------------------- /.github/media/crafty-toltip_1.1.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/liolle/Crafty/12952ae2775b54cb5bd1c5a89f3dd41f8431d0ca/.github/media/crafty-toltip_1.1.gif -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release Obsidian plugin 2 | 3 | on: 4 | push: 5 | tags: 6 | - "*" 7 | 8 | jobs: 9 | build: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@v3 13 | 14 | - name: Use Node.js 15 | uses: actions/setup-node@v3 16 | with: 17 | node-version: "18.18.2" 18 | 19 | - name: Cache node modules 20 | uses: actions/cache@v3 21 | with: 22 | path: node_modules 23 | key: node_modules-${{hashFiles('package-lock.json')}} 24 | restore-keys: node_modules- 25 | 26 | - uses: pnpm/action-setup@v2 27 | with: 28 | version: 8 29 | 30 | - name: Install dependencies and build plugin 31 | run: | 32 | pnpm install --no-frozen-lockfile 33 | pnpm build 34 | 35 | - name: Create release 36 | env: 37 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 38 | run: | 39 | tag="${GITHUB_REF#refs/tags/}" 40 | gh release create "$tag" \ 41 | --title="$tag" \ 42 | --draft \ 43 | main.js manifest.json styles.css 44 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # vscode 2 | .vscode 3 | 4 | # Intellij 5 | *.iml 6 | .idea 7 | 8 | # npm 9 | node_modules 10 | 11 | # Don't include the compiled main.js file in the repo. 12 | # They should be uploaded to GitHub releases instead. 13 | main.js 14 | 15 | # Exclude sourcemaps 16 | *.map 17 | 18 | # obsidian 19 | data.json 20 | 21 | # Exclude macOS Finder (System Explorer) View States 22 | .DS_Store 23 | 24 | main.css 25 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | tag-version-prefix="" -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 liolle 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## Crafty 2 | 3 | ![](https://img.shields.io/github/downloads/liolle/Crafty/total?color=0fbb3f&logo=github&style=for-the-badge) 4 | 5 | This Obsidian plugin enables users to attach tooltips to each node on their canvas. Additionally, it offers seamless navigation between nodes. 6 | 7 | #### Features 8 | 9 | - ###### Node tooltip 10 | 11 | ![](/.github/media/crafty-toltip_1.1.gif) 12 | 13 | - ###### Navigation 14 | 15 | Make sure to set the hotkeys `Setting > Hotkeys` to be able to navigate nodes 16 | ![](/.github/media/crafty-navigation_1.1.gif) 17 | 18 | - ###### Search 19 | 20 | Search for nodes title 21 | ![](/.github/media/crafty-search.gif) 22 | 23 | - ###### Sort/filter nodes 24 | 25 | #### Commands and Hotkeys 26 | 27 | | Hotkey | Action | 28 | | ---------------- | ----------------- | 29 | | Blank by default | Previous node | 30 | | Blank by default | Next node | 31 | | Blank by default | Show/Hide sidebar | 32 | 33 | #### How to Install 34 | 35 | Now available in the [Obsidian Community Plugin Directory](https://obsidian.md/plugins?search=crafty) 36 | -------------------------------------------------------------------------------- /__tests__/node.test.ts: -------------------------------------------------------------------------------- 1 | //Search tests 2 | 3 | // Sort tests 4 | import { CraftyNode } from "nodes/nodes"; 5 | import { 6 | AudioSpecification, 7 | DocumentSpecification, 8 | FileSpecification, 9 | GroupSpecification, 10 | ImageSpecification, 11 | TextSpecification, 12 | VideoSpecification, 13 | WebSpecification, 14 | } from "specification"; 15 | import { describe, expect, test } from "vitest"; 16 | 17 | const sample_nodes: CraftyNode[] = [ 18 | { 19 | id: "id1", 20 | title: "a", 21 | description: "", 22 | container: null, 23 | selected: false, 24 | type: "file", 25 | extension: "canvas", 26 | created_at: 0, 27 | last_modified: 0, 28 | }, 29 | { 30 | id: "id2", 31 | title: "b", 32 | description: "", 33 | container: null, 34 | selected: false, 35 | type: "file", 36 | extension: "md", 37 | created_at: 0, 38 | last_modified: 0, 39 | }, 40 | { 41 | id: "id3", 42 | title: "c", 43 | description: "test", 44 | container: null, 45 | selected: false, 46 | type: "file", 47 | extension: "pdf", 48 | created_at: 0, 49 | last_modified: 0, 50 | }, 51 | { 52 | id: "id4", 53 | title: "az", 54 | description: "test", 55 | container: null, 56 | selected: false, 57 | type: "file", 58 | extension: "wav", 59 | created_at: 0, 60 | last_modified: 0, 61 | }, 62 | { 63 | id: "id5", 64 | title: "bc", 65 | description: "test", 66 | container: null, 67 | selected: false, 68 | type: "file", 69 | extension: "avif", 70 | created_at: 0, 71 | last_modified: 0, 72 | }, 73 | { 74 | id: "id5", 75 | title: "bc", 76 | description: "test", 77 | container: null, 78 | selected: false, 79 | type: "file", 80 | extension: "mp4", 81 | created_at: 0, 82 | last_modified: 0, 83 | }, 84 | { 85 | id: "id5", 86 | title: "bc", 87 | description: "test", 88 | container: null, 89 | selected: false, 90 | type: "text", 91 | extension: "", 92 | created_at: 0, 93 | last_modified: 0, 94 | }, 95 | { 96 | id: "id5", 97 | title: "bc", 98 | description: "test", 99 | container: null, 100 | selected: false, 101 | type: "link", 102 | extension: "", 103 | created_at: 0, 104 | last_modified: 0, 105 | }, 106 | { 107 | id: "id5", 108 | title: "bc", 109 | description: "test", 110 | container: null, 111 | selected: false, 112 | type: "group", 113 | extension: "", 114 | created_at: 0, 115 | last_modified: 0, 116 | }, 117 | ]; 118 | 119 | describe("Specification", () => { 120 | test("Audio specification", () => { 121 | const node1: CraftyNode = { 122 | id: "id5", 123 | title: "bc", 124 | description: "test", 125 | container: null, 126 | selected: false, 127 | type: "link", 128 | extension: "", 129 | created_at: 0, 130 | last_modified: 0, 131 | }; 132 | 133 | const node2: CraftyNode = { 134 | id: "id5", 135 | title: "bc", 136 | description: "test", 137 | container: null, 138 | selected: false, 139 | type: "file", 140 | extension: "mp3", 141 | created_at: 0, 142 | last_modified: 0, 143 | }; 144 | 145 | const specification = new AudioSpecification(); 146 | 147 | expect(specification.isSatisfied(node1)).toBe(false); 148 | expect(specification.isSatisfied(node2)).toBe(true); 149 | }); 150 | test("Image specification", () => { 151 | const node1: CraftyNode = { 152 | id: "id5", 153 | title: "bc", 154 | description: "test", 155 | container: null, 156 | selected: false, 157 | type: "link", 158 | extension: "", 159 | created_at: 0, 160 | last_modified: 0, 161 | }; 162 | 163 | const node2: CraftyNode = { 164 | id: "id5", 165 | title: "bc", 166 | description: "test", 167 | container: null, 168 | selected: false, 169 | type: "file", 170 | extension: "jpg", 171 | created_at: 0, 172 | last_modified: 0, 173 | }; 174 | 175 | const specification = new ImageSpecification(); 176 | 177 | expect(specification.isSatisfied(node1)).toBe(false); 178 | expect(specification.isSatisfied(node2)).toBe(true); 179 | }); 180 | test("Video specification", () => { 181 | const node1: CraftyNode = { 182 | id: "id5", 183 | title: "bc", 184 | description: "test", 185 | container: null, 186 | selected: false, 187 | type: "link", 188 | extension: "", 189 | created_at: 0, 190 | last_modified: 0, 191 | }; 192 | 193 | const node2: CraftyNode = { 194 | id: "id5", 195 | title: "bc", 196 | description: "test", 197 | container: null, 198 | selected: false, 199 | type: "file", 200 | extension: "mp4", 201 | created_at: 0, 202 | last_modified: 0, 203 | }; 204 | 205 | const specification = new VideoSpecification(); 206 | 207 | expect(specification.isSatisfied(node1)).toBe(false); 208 | expect(specification.isSatisfied(node2)).toBe(true); 209 | }); 210 | test("Document specification", () => { 211 | const node1: CraftyNode = { 212 | id: "id5", 213 | title: "bc", 214 | description: "test", 215 | container: null, 216 | selected: false, 217 | type: "link", 218 | extension: "", 219 | created_at: 0, 220 | last_modified: 0, 221 | }; 222 | 223 | const node2: CraftyNode = { 224 | id: "id5", 225 | title: "bc", 226 | description: "test", 227 | container: null, 228 | selected: false, 229 | type: "file", 230 | extension: "pdf", 231 | created_at: 0, 232 | last_modified: 0, 233 | }; 234 | 235 | const specification = new DocumentSpecification(); 236 | 237 | expect(specification.isSatisfied(node1)).toBe(false); 238 | expect(specification.isSatisfied(node2)).toBe(true); 239 | }); 240 | 241 | test("File specification", () => { 242 | const node1: CraftyNode = { 243 | id: "id5", 244 | title: "bc", 245 | description: "test", 246 | container: null, 247 | selected: false, 248 | type: "link", 249 | extension: "", 250 | created_at: 0, 251 | last_modified: 0, 252 | }; 253 | 254 | const node2: CraftyNode = { 255 | id: "id5", 256 | title: "bc", 257 | description: "test", 258 | container: null, 259 | selected: false, 260 | type: "file", 261 | extension: "jpg", 262 | created_at: 0, 263 | last_modified: 0, 264 | }; 265 | 266 | const specification = new FileSpecification(); 267 | 268 | expect(specification.isSatisfied(node1)).toBe(false); 269 | expect(specification.isSatisfied(node2)).toBe(true); 270 | }); 271 | test("Text specification", () => { 272 | const node1: CraftyNode = { 273 | id: "id5", 274 | title: "bc", 275 | description: "test", 276 | container: null, 277 | selected: false, 278 | type: "link", 279 | extension: "", 280 | created_at: 0, 281 | last_modified: 0, 282 | }; 283 | 284 | const node2: CraftyNode = { 285 | id: "id5", 286 | title: "bc", 287 | description: "test", 288 | container: null, 289 | selected: false, 290 | type: "text", 291 | extension: "", 292 | created_at: 0, 293 | last_modified: 0, 294 | }; 295 | 296 | const specification = new TextSpecification(); 297 | 298 | expect(specification.isSatisfied(node1)).toBe(false); 299 | expect(specification.isSatisfied(node2)).toBe(true); 300 | }); 301 | test("Web specification", () => { 302 | const node1: CraftyNode = { 303 | id: "id5", 304 | title: "bc", 305 | description: "test", 306 | container: null, 307 | selected: false, 308 | type: "file", 309 | extension: "mp3", 310 | created_at: 0, 311 | last_modified: 0, 312 | }; 313 | 314 | const node2: CraftyNode = { 315 | id: "id5", 316 | title: "bc", 317 | description: "test", 318 | container: null, 319 | selected: false, 320 | type: "link", 321 | extension: "", 322 | created_at: 0, 323 | last_modified: 0, 324 | }; 325 | 326 | const specification = new WebSpecification(); 327 | 328 | expect(specification.isSatisfied(node1)).toBe(false); 329 | expect(specification.isSatisfied(node2)).toBe(true); 330 | }); 331 | test("Group specification", () => { 332 | const node1: CraftyNode = { 333 | id: "id5", 334 | title: "bc", 335 | description: "test", 336 | container: null, 337 | selected: false, 338 | type: "link", 339 | extension: "", 340 | created_at: 0, 341 | last_modified: 0, 342 | }; 343 | 344 | const node2: CraftyNode = { 345 | id: "id5", 346 | title: "bc", 347 | description: "test", 348 | container: null, 349 | selected: false, 350 | type: "group", 351 | extension: "", 352 | created_at: 0, 353 | last_modified: 0, 354 | }; 355 | 356 | const specification = new GroupSpecification(); 357 | 358 | expect(specification.isSatisfied(node1)).toBe(false); 359 | expect(specification.isSatisfied(node2)).toBe(true); 360 | }); 361 | 362 | test("Composed specification (and) ", () => { 363 | // 364 | }); 365 | test("Composed specification (or) ", () => { 366 | // Audio or Video 367 | const audio = new AudioSpecification(); 368 | const video = new VideoSpecification(); 369 | 370 | const results = sample_nodes.filter((val) => { 371 | return audio.or(video).isSatisfied(val); 372 | }); 373 | 374 | const audio_video = [ 375 | "flac", 376 | "m4a", 377 | "mp3", 378 | "ogg", 379 | "wav", 380 | "webm", 381 | "3gp", 382 | "mkv", 383 | "mov", 384 | "mp4", 385 | "ogv", 386 | "webm", 387 | ]; 388 | expect(results.length).toBe(2); 389 | for (const el of results) expect(audio_video).toContain(el.extension); 390 | 391 | //Document of Webpage 392 | const document = new DocumentSpecification(); 393 | const web = new WebSpecification(); 394 | 395 | const results2 = sample_nodes.filter((val) => { 396 | return document.or(web).isSatisfied(val); 397 | }); 398 | 399 | expect(results2.length).toBe(4); 400 | expect(results2[0].extension).toBe("canvas"); 401 | expect(results2[1].extension).toBe("md"); 402 | expect(results2[2].extension).toBe("pdf"); 403 | expect(results2[3].type).toBe("link"); 404 | }); 405 | test("Composed specification (not) ", () => { 406 | // No documents 407 | const document = new DocumentSpecification(); 408 | 409 | const results = sample_nodes.filter((val) => { 410 | return document.not().isSatisfied(val); 411 | }); 412 | 413 | expect(results.length).toBe(6); 414 | expect(results[0].extension).toBe("wav"); 415 | expect(results[2].extension).toBe("mp4"); 416 | expect(results[4].type).toBe("link"); 417 | expect(results[5].type).toBe("group"); 418 | }); 419 | }); 420 | -------------------------------------------------------------------------------- /__tests__/search.test.ts: -------------------------------------------------------------------------------- 1 | //Search tests 2 | 3 | // Sort tests 4 | import { CraftyNode, NodesExplorer } from "nodes/nodes"; 5 | import { beforeEach, describe, expect, test } from "vitest"; 6 | 7 | // const sample_titles = [ 8 | // "Cascade", 9 | // "Phoenix", 10 | // "Nebula", 11 | // "Horizon", 12 | // "Mirage", 13 | // "Dragon", 14 | // "Odyssey", 15 | // "Pinnacle", 16 | // "Comet", 17 | // "Aurora", 18 | // "Eclipse", 19 | // "Infinity", 20 | // "Zenith", 21 | // "Serenity", 22 | // "Apple", 23 | // "Banana", 24 | // "Cherry", 25 | // "Grape", 26 | // "Kiwi", 27 | // "Mango", 28 | // "Orange", 29 | // "Peach", 30 | // "Pear", 31 | // "Strawberry", 32 | // "Watermelon", 33 | // "Pineapple", 34 | // "Blueberry", 35 | // "Raspberry", 36 | // "Blackberry", 37 | // "Radiant", 38 | // "Whispering", 39 | // "Majestic", 40 | // "Vibrant", 41 | // "Enchanted", 42 | // "Luminous", 43 | // "Mystical", 44 | // "Serene", 45 | // "Wandering", 46 | // "Cosmic", 47 | // "Tranquil", 48 | // "Energetic", 49 | // "Celestial", 50 | // "Harmonious", 51 | // "Galactic", 52 | // "Lion", 53 | // "Tiger", 54 | // "Elephant", 55 | // "Zebra", 56 | // "Giraffe", 57 | // "Kangaroo", 58 | // "Panda", 59 | // "Koala", 60 | // "Hippopotamus", 61 | // "Dolphin", 62 | // "Octopus", 63 | // "Butterfly", 64 | // "Hawk", 65 | // "Owl", 66 | // "Turtle", 67 | // ]; 68 | 69 | const sample_nodes: CraftyNode[] = [ 70 | { 71 | id: "s1", 72 | title: "node1", 73 | description: "", 74 | container: null, 75 | selected: false, 76 | type: "file", 77 | extension: "canvas", 78 | created_at: 0, 79 | last_modified: 0, 80 | }, 81 | { 82 | id: "s2", 83 | title: "node2", 84 | description: "", 85 | container: null, 86 | selected: false, 87 | type: "file", 88 | extension: "canvas", 89 | created_at: 0, 90 | last_modified: 0, 91 | }, 92 | { 93 | id: "s1", 94 | title: "node1", 95 | description: "node1 bis", 96 | container: null, 97 | selected: false, 98 | type: "file", 99 | extension: "canvas", 100 | created_at: 0, 101 | last_modified: 0, 102 | }, 103 | { 104 | id: "s3", 105 | title: "node123", 106 | description: "description", 107 | container: null, 108 | selected: false, 109 | type: "file", 110 | extension: "canvas", 111 | created_at: 0, 112 | last_modified: 0, 113 | }, 114 | ]; 115 | 116 | describe("NodesExplorer", () => { 117 | const explorer = new NodesExplorer(); 118 | 119 | beforeEach(() => { 120 | explorer.clear(); 121 | }); 122 | 123 | // Add elements 124 | test("Simple add", () => { 125 | expect(explorer.size).toBe(0); 126 | 127 | explorer.add(sample_nodes[0]); 128 | 129 | expect(explorer.size).toBe(sample_nodes[0].title.length); 130 | }); 131 | 132 | test("Add multiple", () => { 133 | expect(explorer.size).toBe(0); 134 | 135 | explorer.add(sample_nodes[0]); 136 | explorer.add(sample_nodes[1]); 137 | 138 | expect(explorer.size).toBe( 139 | sample_nodes[0].title.length + sample_nodes[1].title.length 140 | ); 141 | }); 142 | 143 | test("Add multiple overlap", () => { 144 | expect(explorer.size).toBe(0); 145 | 146 | explorer.add(sample_nodes[0]); 147 | explorer.add(sample_nodes[2]); 148 | 149 | expect(explorer.size).toBe(sample_nodes[2].title.length); 150 | }); 151 | 152 | // Remove elements 153 | test("Simple remove", () => { 154 | expect(explorer.size).toBe(0); 155 | 156 | const node = sample_nodes[0]; 157 | explorer.add(node); 158 | explorer.remove(node); 159 | console.log(JSON.stringify(explorer)); 160 | expect(explorer.size).toBe(0); 161 | }); 162 | 163 | // Search elements 164 | test("Simple search", () => { 165 | expect(explorer.size).toBe(0); 166 | 167 | const node = sample_nodes[0]; 168 | explorer.add(node); 169 | 170 | const search = explorer.search(node.title); 171 | const s_node = search[0]; 172 | expect(search.length).toBe(1); 173 | expect(s_node.title).toBe(node.title); 174 | }); 175 | 176 | test("Search not exit", () => { 177 | expect(explorer.size).toBe(0); 178 | 179 | let search = explorer.search("empty"); 180 | expect(search.length).toBe(0); 181 | 182 | const node = sample_nodes[0]; 183 | explorer.add(node); 184 | 185 | search = explorer.search("empty"); 186 | expect(search.length).toBe(0); 187 | }); 188 | 189 | test("Search after remove", () => { 190 | expect(explorer.size).toBe(0); 191 | 192 | const node = sample_nodes[0]; 193 | explorer.add(node); 194 | explorer.remove(node); 195 | 196 | const search = explorer.search(node.title); 197 | expect(search.length).toBe(0); 198 | }); 199 | 200 | test("Search overlap", () => { 201 | expect(explorer.size).toBe(0); 202 | 203 | const node = sample_nodes[0]; 204 | const node2 = sample_nodes[2]; 205 | explorer.add(node); 206 | explorer.add(node2); 207 | 208 | const search = explorer.search(node.title); 209 | expect(search.length).toBe(1); 210 | 211 | const s_node = search[0]; 212 | expect(s_node.title).toBe(node2.title); 213 | expect(s_node.description).toBe(node2.description); 214 | }); 215 | 216 | test("Prefix search", () => { 217 | expect(explorer.size).toBe(0); 218 | 219 | const node = sample_nodes[0]; 220 | const node2 = sample_nodes[1]; 221 | const node3 = sample_nodes[3]; 222 | explorer.add(node); 223 | explorer.add(node2); 224 | explorer.add(node3); 225 | 226 | //@ts-ignore 227 | const search = explorer.prefixSearch("no"); 228 | expect(search.length).toBe(3); 229 | expect(search).toContain(node); 230 | expect(search).toContain(node2); 231 | expect(search).toContain(node3); 232 | }); 233 | }); 234 | -------------------------------------------------------------------------------- /dom/handler.ts: -------------------------------------------------------------------------------- 1 | import { SlDropdown, SlMenu } from "@shoelace-style/shoelace"; 2 | import { FileHandler } from "io/fileHandler"; 3 | import Crafty from "main"; 4 | import { CraftyNode, FILE_TYPE, NodeFilter } from "nodes/nodes"; 5 | import { debounce, setIcon } from "obsidian"; 6 | 7 | export class DOMHandler { 8 | private static selection_listeners_cb: (() => void)[] = []; 9 | private static nodes_click_lister_cb: (() => void)[] = []; 10 | private static title_edit_lister_cb: (() => void)[] = []; 11 | private static searchbar_lister_cb: (() => void)[] = []; 12 | private static sort_menu_lister_cb: (() => void)[] = []; 13 | private static filter_menu_badge_lister_cb: (() => void)[] = []; 14 | private static last_node_id = ""; 15 | private static titleInput: HTMLDivElement | null = null; 16 | private static titleDisplay: HTMLDivElement | null = null; 17 | private static textArea: HTMLTextAreaElement | null = null; 18 | private static save_state: HTMLSpanElement | null = null; 19 | private static search_bar: HTMLInputElement | null = null; 20 | private static nodes_container: HTMLDivElement | null = null; 21 | private static filters_container: HTMLDivElement | null = null; 22 | private static filters_display: HTMLDivElement | null = null; 23 | private static sort_button: SlDropdown | null = null; 24 | private static filter_button: SlDropdown | null = null; 25 | private static sort_menu: SlMenu | null = null; 26 | private static filter_menu: SlMenu | null = null; 27 | private static crafty: Crafty | null; 28 | 29 | static #freeSelectionListeners() { 30 | let callback = this.selection_listeners_cb.pop(); 31 | while (callback) { 32 | callback(); 33 | callback = this.selection_listeners_cb.pop(); 34 | } 35 | } 36 | 37 | static #freeNodesClickListeners() { 38 | let callback = this.nodes_click_lister_cb.pop(); 39 | while (callback) { 40 | callback(); 41 | callback = this.nodes_click_lister_cb.pop(); 42 | } 43 | } 44 | 45 | static #freeTitleEditListeners() { 46 | let callback = this.title_edit_lister_cb.pop(); 47 | while (callback) { 48 | callback(); 49 | callback = this.title_edit_lister_cb.pop(); 50 | } 51 | } 52 | 53 | static #freeSearchBarListeners() { 54 | let callback = this.searchbar_lister_cb.pop(); 55 | while (callback) { 56 | callback(); 57 | callback = this.searchbar_lister_cb.pop(); 58 | } 59 | } 60 | 61 | static #freeSortMenuListeners() { 62 | let callback = this.sort_menu_lister_cb.pop(); 63 | while (callback) { 64 | callback(); 65 | callback = this.sort_menu_lister_cb.pop(); 66 | } 67 | } 68 | 69 | static #freeFilterMenuBadgeListeners() { 70 | let callback = this.filter_menu_badge_lister_cb.pop(); 71 | while (callback) { 72 | callback(); 73 | callback = this.filter_menu_badge_lister_cb.pop(); 74 | } 75 | } 76 | 77 | static async populateNodes(nodes: CraftyNode[] | null) { 78 | const body = document.querySelector(".nodes-body"); 79 | 80 | if (nodes == null || !body) return; 81 | DOMHandler.#freeNodesClickListeners(); 82 | body.empty(); 83 | 84 | for (const node of nodes) { 85 | const cls = ["node-element"]; 86 | if (node.selected) cls.push("node-active"); 87 | const child = createEl("div", { 88 | text: node.title, 89 | attr: { class: cls.join(" ") }, 90 | }); 91 | 92 | const clickCallback = (event: Event) => { 93 | //@ts-ignore 94 | node.container.click(); 95 | }; 96 | child.addEventListener("click", clickCallback); 97 | 98 | this.nodes_click_lister_cb.push(() => { 99 | child.removeEventListener("click", clickCallback); 100 | }); 101 | 102 | body.appendChild(child); 103 | } 104 | } 105 | 106 | static showSelectedNode() { 107 | if (!this.crafty) return; 108 | const node = this.crafty.selectedNode; 109 | if (!node || this.last_node_id == node.id) return; 110 | this.#freeSelectionListeners(); 111 | const title_container = this.getTitleDisplay(); 112 | const title = title_container.querySelector("span"); 113 | 114 | const text_area = this.getTextArea(); 115 | const save_state = this.getSaveState(); 116 | 117 | text_area.classList.remove("hidden"); 118 | save_state.classList.remove("hidden"); 119 | 120 | if (!title) return; 121 | 122 | // initial state 123 | 124 | title_container.classList.remove("hidden"); 125 | title.setText(node.title); 126 | this.updateTextArea(node.description || ""); 127 | } 128 | 129 | static async showEmptyEdit() { 130 | this.#freeSelectionListeners(); 131 | 132 | const text_area = this.getTextArea(); 133 | const save_state = this.getSaveState(); 134 | 135 | text_area.classList.add("hidden"); 136 | save_state.classList.add("hidden"); 137 | 138 | DOMHandler.hideTitle(); 139 | } 140 | 141 | static async showEmptyNodes() { 142 | const search_bar = this.getSearchBar(); 143 | const nodes_container = this.getNodesContainer(); 144 | const sort_button = this.getSortButton(); 145 | const filter_button = this.getFiltersButton(); 146 | search_bar.classList.add("hidden"); 147 | nodes_container.classList.add("hidden"); 148 | sort_button.classList.add("hidden"); 149 | filter_button.classList.add("hidden"); 150 | } 151 | 152 | static async showNodes() { 153 | const search_bar = this.getSearchBar(); 154 | const nodes_container = this.getNodesContainer(); 155 | const sort_button = this.getSortButton(); 156 | const filter_button = this.getFiltersButton(); 157 | search_bar.classList.remove("hidden"); 158 | nodes_container.classList.remove("hidden"); 159 | sort_button.classList.remove("hidden"); 160 | filter_button.classList.remove("hidden"); 161 | } 162 | 163 | static hideTitle() { 164 | const title_display = this.getTitleDisplay(); 165 | const title_input = this.getTitleInput(); 166 | title_display.classList.add("hidden"); 167 | title_input.classList.add("hidden"); 168 | } 169 | 170 | static getTitleDisplay() { 171 | if (!this.titleDisplay) { 172 | const element = createEl("div", { 173 | attr: { class: "title-edit-div" }, 174 | }); 175 | 176 | element.createEl("span", { 177 | attr: { class: "title" }, 178 | }); 179 | 180 | const icon_container = element.createEl("span", { 181 | attr: { class: "edit-icon" }, 182 | }); 183 | 184 | setIcon(icon_container, "pencil"); 185 | 186 | const icon_click_cb = () => { 187 | const input = DOMHandler.getTitleInput(); 188 | element.classList.add("hidden"); 189 | input.classList.remove("hidden"); 190 | input.querySelector("input"); 191 | const inner_input = input.querySelector("input"); 192 | if (!inner_input) return; 193 | const span: HTMLSpanElement = element.querySelector( 194 | "span" 195 | ) as HTMLSpanElement; 196 | 197 | inner_input.value = span.textContent || ""; 198 | inner_input.focus(); 199 | }; 200 | icon_container.addEventListener("click", icon_click_cb); 201 | 202 | this.title_edit_lister_cb.push(() => { 203 | icon_container.removeEventListener("click", icon_click_cb); 204 | }); 205 | 206 | this.titleDisplay = element; 207 | } 208 | return this.titleDisplay; 209 | } 210 | 211 | static getSaveState() { 212 | if (!this.save_state) { 213 | const save_state = createEl("span", { 214 | text: "Saved", 215 | attr: { class: "save_state" }, 216 | }); 217 | 218 | this.save_state = save_state; 219 | } 220 | return this.save_state; 221 | } 222 | 223 | static updateTextArea(value: string) { 224 | if (!this.crafty || !this.crafty.nodeState) return; 225 | if (this.crafty.nodeState.isNodeSame) return; 226 | const text_area = this.getTextArea(); 227 | text_area.value = value; 228 | } 229 | 230 | static getTextArea() { 231 | if (!this.textArea) { 232 | const element = createEl("textarea", { 233 | attr: { class: "description-input" }, 234 | }); 235 | 236 | this.textArea = element; 237 | } 238 | this.#freeSelectionListeners(); 239 | const inputChangeCallback = debounce( 240 | async (t) => { 241 | if (!this.crafty || !this.crafty.selectedNode || !this.textArea) 242 | return; 243 | 244 | const node = this.crafty.selectedNode; 245 | const file = this.crafty.currentFile; 246 | const vault = this.crafty.vault; 247 | const save_state = DOMHandler.getSaveState(); 248 | save_state.setText("Saving..."); 249 | node.description = this.textArea.value; 250 | await FileHandler.updateCanvasNode(node, file, vault); 251 | setTimeout(() => { 252 | save_state.setText("Saved"); 253 | }, 200); 254 | }, 255 | 1000, 256 | true 257 | ); 258 | 259 | this.textArea.addEventListener("input", inputChangeCallback); 260 | this.selection_listeners_cb.push(() => { 261 | if (!this.textArea) return; 262 | this.textArea.removeEventListener("input", inputChangeCallback); 263 | }); 264 | return this.textArea; 265 | } 266 | 267 | static getSearchBar() { 268 | if (!this.search_bar) { 269 | const search_bar = createEl("input", { 270 | attr: { class: "searchBar-input" }, 271 | }); 272 | 273 | search_bar.placeholder = "Search"; 274 | const search_change_cb = debounce(() => { 275 | if (!this.crafty || !this.crafty.nodeState) return; 276 | const node_state = this.crafty.nodeState; 277 | node_state.setSearchWord(search_bar.value); 278 | }, 1000); 279 | search_bar.addEventListener("input", search_change_cb); 280 | 281 | this.searchbar_lister_cb.push(() => { 282 | search_bar.removeEventListener("input", search_change_cb); 283 | }); 284 | this.search_bar = search_bar; 285 | } 286 | 287 | return this.search_bar; 288 | } 289 | 290 | static #toggleSortMenu() { 291 | const sort_button = this.getSortButton(); 292 | const attributes_name = sort_button.getAttributeNames(); 293 | if (attributes_name.includes("open")) 294 | sort_button.removeAttribute("open"); 295 | else sort_button.setAttr("open", true); 296 | } 297 | 298 | static #sortItemTemplate( 299 | title: string, 300 | groupe: string, 301 | check_marker: string, 302 | callback: () => void 303 | ) { 304 | const container = createEl("div", { 305 | attr: { class: `sort-item ` }, 306 | }); 307 | 308 | const item = createEl("div", {}); 309 | item.setText(title); 310 | const check_logo = createEl("div", { 311 | attr: { class: `${groupe} sort-check ${check_marker}` }, 312 | }); 313 | setIcon(check_logo, "check"); 314 | 315 | container.addEventListener("click", callback); 316 | this.sort_menu_lister_cb.push(() => { 317 | container.removeEventListener("click", callback); 318 | }); 319 | 320 | container.appendChild(check_logo); 321 | container.appendChild(item); 322 | return container; 323 | } 324 | 325 | static getSortMenu() { 326 | if (!this.sort_menu) { 327 | const menu = createEl("sl-menu", { 328 | attr: { class: "sort-menu" }, 329 | }); 330 | 331 | const selectName = () => { 332 | if (!this.crafty || !this.crafty.nodeState) return; 333 | this.crafty.nodeState.sortBy("name"); 334 | this.#toggleSortMenu(); 335 | }; 336 | 337 | const selectCreated = () => { 338 | if (!this.crafty || !this.crafty.nodeState) return; 339 | this.crafty.nodeState.sortBy("created_at"); 340 | this.#toggleSortMenu(); 341 | }; 342 | 343 | const selectLastModified = () => { 344 | if (!this.crafty || !this.crafty.nodeState) return; 345 | this.crafty.nodeState.sortBy("last_modified"); 346 | this.#toggleSortMenu(); 347 | }; 348 | 349 | const selectAscending = () => { 350 | if (!this.crafty || !this.crafty.nodeState) return; 351 | this.crafty.nodeState.order("asc"); 352 | this.#toggleSortMenu(); 353 | }; 354 | 355 | const selectDescending = () => { 356 | if (!this.crafty || !this.crafty.nodeState) return; 357 | this.crafty.nodeState.order("des"); 358 | this.#toggleSortMenu(); 359 | }; 360 | 361 | const pick_name = this.#sortItemTemplate( 362 | "Name", 363 | "g1", 364 | "s-name", 365 | selectName 366 | ); 367 | const pick_created = this.#sortItemTemplate( 368 | "Created_At", 369 | "g1", 370 | "s-created", 371 | selectCreated 372 | ); 373 | 374 | const pick_last = this.#sortItemTemplate( 375 | "Last_Modified", 376 | "g1", 377 | "s-last", 378 | selectLastModified 379 | ); 380 | const pick_asc = this.#sortItemTemplate( 381 | "Ascending", 382 | "g2", 383 | "s-asc", 384 | selectAscending 385 | ); 386 | const pick_desc = this.#sortItemTemplate( 387 | "Descending", 388 | "g2", 389 | "s-desc", 390 | selectDescending 391 | ); 392 | 393 | const divider = createEl("sl-divider", {}); 394 | 395 | menu.appendChild(pick_name); 396 | menu.appendChild(pick_created); 397 | menu.appendChild(pick_last); 398 | menu.appendChild(divider); 399 | menu.appendChild(pick_asc); 400 | menu.appendChild(pick_desc); 401 | 402 | this.sort_menu = menu; 403 | } 404 | return this.sort_menu; 405 | } 406 | 407 | static getSortButton() { 408 | if (!this.sort_button) { 409 | const sort_button = createEl("sl-dropdown", { 410 | attr: { 411 | class: "sort-button-container", 412 | distance: "-40", 413 | skidding: "-10", 414 | }, 415 | }); 416 | 417 | const button = createEl("button", { 418 | attr: { class: "sort-button", slot: "trigger" }, 419 | }); 420 | 421 | const text = createEl("span", { 422 | attr: { class: "sort-button-large sb-text" }, 423 | }); 424 | 425 | const logo = createEl("div", {}); 426 | 427 | setIcon(logo, "arrow-down-up"); 428 | 429 | button.appendChild(logo); 430 | button.appendChild(text); 431 | 432 | sort_button.appendChild(button); 433 | 434 | sort_button.appendChild(this.getSortMenu()); 435 | 436 | this.sort_button = sort_button; 437 | } 438 | 439 | return this.sort_button; 440 | } 441 | 442 | static getNodesContainer() { 443 | if (!this.nodes_container) { 444 | const nodes_container = createEl("div", { 445 | attr: { class: "nodes-container" }, 446 | }); 447 | nodes_container.createEl("div", { 448 | attr: { class: "nodes-body" }, 449 | }); 450 | this.nodes_container = nodes_container; 451 | } 452 | return this.nodes_container; 453 | } 454 | 455 | static #getFilterSection( 456 | group: "Document" | "Video" | "Audio" | "Image" | "General" 457 | ) { 458 | const container = createEl("div", { 459 | attr: { 460 | class: "filter-menu-badge-container", 461 | }, 462 | }); 463 | let filters: NodeFilter[] = []; 464 | switch (group) { 465 | case "Document": 466 | if (this.crafty && this.crafty.nodeFilterState) { 467 | filters = 468 | this.crafty.nodeFilterState.getFilterByGroup( 469 | "Document" 470 | ); 471 | } 472 | break; 473 | case "Image": 474 | if (this.crafty && this.crafty.nodeFilterState) { 475 | filters = 476 | this.crafty.nodeFilterState.getFilterByGroup("Image"); 477 | } 478 | break; 479 | 480 | case "Audio": 481 | if (this.crafty && this.crafty.nodeFilterState) { 482 | filters = 483 | this.crafty.nodeFilterState.getFilterByGroup("Audio"); 484 | } 485 | break; 486 | 487 | case "Video": 488 | if (this.crafty && this.crafty.nodeFilterState) { 489 | filters = 490 | this.crafty.nodeFilterState.getFilterByGroup("Video"); 491 | } 492 | break; 493 | 494 | case "General": 495 | if (this.crafty && this.crafty.nodeFilterState) { 496 | filters = 497 | this.crafty.nodeFilterState.getFilterByGroup("General"); 498 | } 499 | break; 500 | 501 | default: 502 | break; 503 | } 504 | 505 | for (const el of filters) { 506 | const badge = createEl("div", { 507 | attr: { 508 | class: "filter-menu-badge", 509 | }, 510 | }); 511 | const badge_span = createEl("span", {}); 512 | badge_span.setText(el.title); 513 | badge.appendChild(badge_span); 514 | 515 | const badge_click_cb = () => { 516 | if (badge.classList.contains("badge-active")) { 517 | if (this.crafty && this.crafty.nodeFilterState) { 518 | this.crafty.nodeFilterState.removeFilter( 519 | el.title as FILE_TYPE 520 | ); 521 | } 522 | badge.classList.remove("badge-active"); 523 | } else { 524 | if (this.crafty && this.crafty.nodeFilterState) { 525 | this.crafty.nodeFilterState.addFilter( 526 | el.title as FILE_TYPE 527 | ); 528 | } 529 | badge.classList.add("badge-active"); 530 | } 531 | }; 532 | badge.addEventListener("click", badge_click_cb); 533 | 534 | this.filter_menu_badge_lister_cb.push(() => { 535 | badge.removeEventListener("click", badge_click_cb); 536 | }); 537 | 538 | container.appendChild(badge); 539 | } 540 | 541 | return container; 542 | } 543 | 544 | static getFilterMenu() { 545 | if (!this.filter_menu) { 546 | this.#freeFilterMenuBadgeListeners(); 547 | const filter_menu = createEl("sl-menu", { 548 | attr: { 549 | class: "filter-menu", 550 | }, 551 | }); 552 | 553 | //Document 554 | const general = createEl("div", { 555 | attr: { 556 | class: "filter-menu-section", 557 | }, 558 | }); 559 | const general_title = createEl("span", {}); 560 | general_title.setText("General"); 561 | general.appendChild(general_title); 562 | general.appendChild(this.#getFilterSection("General")); 563 | 564 | //Document 565 | const document = createEl("div", { 566 | attr: { 567 | class: "filter-menu-section", 568 | }, 569 | }); 570 | const document_title = createEl("span", {}); 571 | document_title.setText("Documents"); 572 | document.appendChild(document_title); 573 | document.appendChild(this.#getFilterSection("Document")); 574 | 575 | //Image 576 | const image = createEl("div", { 577 | attr: { 578 | class: "filter-menu-section", 579 | }, 580 | }); 581 | const image_title = createEl("span", {}); 582 | image_title.setText("Image"); 583 | image.appendChild(image_title); 584 | image.appendChild(this.#getFilterSection("Image")); 585 | 586 | //Audio 587 | const audio = createEl("div", { 588 | attr: { 589 | class: "filter-menu-section", 590 | }, 591 | }); 592 | const audio_title = createEl("span", {}); 593 | audio_title.setText("Audio"); 594 | audio.appendChild(audio_title); 595 | audio.appendChild(this.#getFilterSection("Audio")); 596 | 597 | //Video 598 | const video = createEl("div", { 599 | attr: { 600 | class: "filter-menu-section", 601 | }, 602 | }); 603 | const video_title = createEl("span", {}); 604 | video_title.setText("Video"); 605 | video.appendChild(video_title); 606 | video.appendChild(this.#getFilterSection("Image")); 607 | 608 | filter_menu.appendChild(general); 609 | filter_menu.appendChild(document); 610 | filter_menu.appendChild(image); 611 | filter_menu.appendChild(audio); 612 | filter_menu.appendChild(video); 613 | 614 | this.filter_menu = filter_menu; 615 | } 616 | return this.filter_menu; 617 | } 618 | 619 | static getFiltersButton() { 620 | if (!this.filter_button) { 621 | const filter_button = createEl("sl-dropdown", { 622 | attr: { 623 | class: "", 624 | distance: "-40", 625 | skidding: "-10", 626 | }, 627 | }); 628 | 629 | const expand_button = createEl("button", { 630 | attr: { class: "filters-button", slot: "trigger" }, 631 | }); 632 | 633 | const logo_container = createEl("div", {}); 634 | setIcon(logo_container, "plus"); 635 | const button_text = createEl("span"); 636 | button_text.setText("Filter"); 637 | 638 | expand_button.appendChild(logo_container); 639 | expand_button.appendChild(button_text); 640 | 641 | filter_button.appendChild(expand_button); 642 | 643 | filter_button.appendChild(this.getFilterMenu()); 644 | 645 | this.filter_button = filter_button; 646 | } 647 | return this.filter_button; 648 | } 649 | 650 | static getFiltersDisplay() { 651 | if (!this.filters_display) { 652 | const filters_display = createEl("div", { 653 | attr: { class: "filters_display" }, 654 | }); 655 | 656 | let filters: NodeFilter[] = []; 657 | 658 | if (this.crafty && this.crafty.nodeFilterState) { 659 | filters = this.crafty.nodeFilterState.allFilters; 660 | } 661 | 662 | for (const el of filters) { 663 | const badge = createEl("div", { 664 | attr: { 665 | class: "filter-menu-badge-display", 666 | }, 667 | }); 668 | const badge_span = createEl("span", {}); 669 | badge_span.setText(el.title); 670 | badge.appendChild(badge_span); 671 | 672 | if (el.isActive) badge.classList.add("badge-display-active"); 673 | 674 | filters_display.appendChild(badge); 675 | } 676 | 677 | this.filters_display = filters_display; 678 | } 679 | return this.filters_display; 680 | } 681 | 682 | static getFiltersContainer() { 683 | if (!this.filters_container) { 684 | const container = createEl("div", { 685 | attr: { 686 | class: "filters-container", 687 | }, 688 | }); 689 | container.appendChild(this.getFiltersDisplay()); 690 | container.appendChild(this.getFiltersButton()); 691 | 692 | this.filters_container = container; 693 | } 694 | return this.filters_container; 695 | } 696 | 697 | static setCraftyInstance(crafty: Crafty) { 698 | this.crafty = crafty; 699 | } 700 | 701 | static getTitleInput() { 702 | if (!this.titleInput) { 703 | const element = createEl("div", { 704 | attr: { class: "title-edit-div hidden" }, 705 | }); 706 | 707 | const input = element.createEl("input", { 708 | attr: { class: "title-input" }, 709 | }); 710 | 711 | const input_focus_lost_cb = async () => { 712 | this.#saveTitle(element, input); 713 | }; 714 | 715 | const input_enter_cb = async (ev: KeyboardEvent) => { 716 | if (ev.key == "Enter") { 717 | this.#saveTitle(element, input); 718 | } 719 | }; 720 | 721 | input.addEventListener("focusout", input_focus_lost_cb); 722 | 723 | input.addEventListener("keydown", input_enter_cb); 724 | 725 | this.title_edit_lister_cb.push(() => { 726 | input.removeEventListener("keydown", input_enter_cb); 727 | }); 728 | 729 | this.title_edit_lister_cb.push(() => { 730 | input.removeEventListener("focusout", input_focus_lost_cb); 731 | }); 732 | 733 | this.titleInput = element; 734 | } 735 | return this.titleInput; 736 | } 737 | static async #saveTitle(element: HTMLDivElement, input: HTMLInputElement) { 738 | const display = DOMHandler.getTitleDisplay(); 739 | element.classList.add("hidden"); 740 | display.classList.remove("hidden"); 741 | if (!this.crafty || !this.crafty.selectedNode || !this.textArea) return; 742 | const node = this.crafty.selectedNode; 743 | const file = this.crafty.currentFile; 744 | const vault = this.crafty.vault; 745 | node.title = input.value; 746 | await FileHandler.updateCanvasNode(node, file, vault); 747 | } 748 | 749 | static free() { 750 | this.#freeNodesClickListeners(); 751 | this.#freeSelectionListeners(); 752 | this.#freeTitleEditListeners(); 753 | this.#freeSearchBarListeners(); 754 | this.#freeSortMenuListeners(); 755 | this.#freeFilterMenuBadgeListeners(); 756 | } 757 | } 758 | -------------------------------------------------------------------------------- /esbuild.config.mjs: -------------------------------------------------------------------------------- 1 | import esbuild from "esbuild"; 2 | import process from "process"; 3 | import builtins from "builtin-modules"; 4 | 5 | const banner = `/* 6 | THIS IS A GENERATED/BUNDLED FILE BY ESBUILD 7 | if you want to view the source, please visit the github repository of this plugin 8 | */ 9 | `; 10 | 11 | const prod = process.argv[2] === "production"; 12 | async function setup() { 13 | const context = await esbuild.context({ 14 | banner: { 15 | js: banner, 16 | }, 17 | entryPoints: ["main.ts"], 18 | bundle: true, 19 | external: [ 20 | "obsidian", 21 | "electron", 22 | "@codemirror/autocomplete", 23 | "@codemirror/collab", 24 | "@codemirror/commands", 25 | "@codemirror/language", 26 | "@codemirror/lint", 27 | "@codemirror/search", 28 | "@codemirror/state", 29 | "@codemirror/view", 30 | "@lezer/common", 31 | "@lezer/highlight", 32 | "@lezer/lr", 33 | ...builtins, 34 | ], 35 | format: "cjs", 36 | target: "es2018", 37 | logLevel: "info", 38 | sourcemap: prod ? false : "inline", 39 | treeShaking: true, 40 | outfile: "main.js", 41 | }); 42 | 43 | if (prod) { 44 | await context.rebuild(); 45 | process.exit(0); 46 | } else { 47 | await context.watch(); 48 | } 49 | } 50 | 51 | setup(); 52 | -------------------------------------------------------------------------------- /io/fileHandler.ts: -------------------------------------------------------------------------------- 1 | import { CraftyNode } from "nodes/nodes"; 2 | import { TFile, Vault } from "obsidian"; 3 | 4 | export class FileHandler { 5 | static async updateCanvasNode(node: CraftyNode, file: TFile, vault: Vault) { 6 | const content = await vault.read(file); 7 | try { 8 | const parsed_data = JSON.parse(content); 9 | for (const elem of parsed_data.nodes) { 10 | if (elem.id == node.id) { 11 | delete elem.description; 12 | delete elem.title; 13 | if (node.description && node.description != "") { 14 | elem.description = node.description; 15 | } 16 | 17 | if (node.title && node.title != "") { 18 | elem.title = node.title; 19 | } 20 | break; 21 | } 22 | } 23 | await vault.modify(file, JSON.stringify(parsed_data)); 24 | } catch (error) { 25 | console.error(error); 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /main.ts: -------------------------------------------------------------------------------- 1 | import "@shoelace-style/shoelace/dist/components/button/button.js"; 2 | import "@shoelace-style/shoelace/dist/components/dropdown/dropdown.js"; 3 | import "@shoelace-style/shoelace/dist/components/icon/icon.js"; 4 | import "@shoelace-style/shoelace/dist/components/input/input.js"; 5 | import "@shoelace-style/shoelace/dist/components/rating/rating.js"; 6 | import "@shoelace-style/shoelace/dist/components/tab-group/tab-group.js"; 7 | import "@shoelace-style/shoelace/dist/components/tab-panel/tab-panel.js"; 8 | import "@shoelace-style/shoelace/dist/components/tab/tab.js"; 9 | import "@shoelace-style/shoelace/dist/themes/light.css"; 10 | import { ItemView, Plugin, TFile, WorkspaceLeaf, debounce } from "obsidian"; 11 | 12 | import { DOMHandler } from "dom/handler"; 13 | import { FSWatcher, watch } from "fs"; 14 | import { CraftyNode, FILE_TYPE, NODE_TYPE } from "nodes/nodes"; 15 | import { 16 | AttributeObserver, 17 | NodeFilterObserver, 18 | NodeObserver, 19 | NodesFilterState, 20 | NodesState, 21 | } from "observers/observer"; 22 | 23 | export const VIEW_TYPE = "crafty-plugin"; 24 | 25 | export class BaseView extends ItemView { 26 | constructor(leaf: WorkspaceLeaf) { 27 | super(leaf); 28 | } 29 | 30 | getViewType() { 31 | return VIEW_TYPE; 32 | } 33 | 34 | getDisplayText() { 35 | return "Crafty"; 36 | } 37 | 38 | #setBaseLayout() { 39 | const container = this.containerEl.children[1]; 40 | container.empty(); 41 | const tabGroup = container.createEl("sl-tab-group", { 42 | cls: ["side-bar-nav"], 43 | }); 44 | 45 | const tabs = []; 46 | 47 | for (const tab of ["Edit", "Nodes"]) { 48 | const tb = tabGroup.createEl("sl-tab", { 49 | text: tab, 50 | }); 51 | tb.setAttrs({ slot: "nav", panel: tab }); 52 | tabs.push(tb); 53 | } 54 | 55 | //Nodes tab 56 | const node_list = tabGroup.createEl("sl-tab-panel", { 57 | attr: { 58 | name: "Nodes", 59 | }, 60 | }); 61 | 62 | const nodes_panel = node_list.createEl("div", { 63 | attr: { class: "nodes-panel " }, 64 | }); 65 | 66 | const search_area = nodes_panel.createEl("div", { 67 | attr: { class: "search-area" }, 68 | }); 69 | 70 | const search_bar_row = createEl("div", { 71 | attr: { class: "search-bar-row" }, 72 | }); 73 | 74 | const filter_row = createEl("div", { 75 | attr: { class: "filter-row" }, 76 | }); 77 | 78 | const search_bar = DOMHandler.getSearchBar(); 79 | const sort_button = DOMHandler.getSortButton(); 80 | search_bar_row.appendChild(search_bar); 81 | search_bar_row.appendChild(sort_button); 82 | 83 | const filters_container = DOMHandler.getFiltersContainer(); 84 | filter_row.appendChild(filters_container); 85 | 86 | search_area.appendChild(search_bar_row); 87 | search_area.appendChild(filter_row); 88 | 89 | const nodes_container = DOMHandler.getNodesContainer(); 90 | nodes_panel.appendChild(nodes_container); 91 | 92 | //Edit tab 93 | const edit_panel = tabGroup.createEl("sl-tab-panel", { 94 | attr: { 95 | name: "Edit", 96 | class: "description-body", 97 | }, 98 | }); 99 | 100 | // Edit Node Title 101 | const edit_header = edit_panel.createEl("div", { 102 | attr: { class: "description-header-div" }, 103 | }); 104 | 105 | const edit_header_display = DOMHandler.getTitleDisplay(); 106 | const edit_header_input = DOMHandler.getTitleInput(); 107 | edit_header.appendChild(edit_header_display); 108 | edit_header.appendChild(edit_header_input); 109 | 110 | // Edit Node Description 111 | const text_area = DOMHandler.getTextArea(); 112 | const save_state = DOMHandler.getSaveState(); 113 | edit_panel.appendChild(text_area); 114 | edit_panel.appendChild(save_state); 115 | } 116 | 117 | async onOpen() { 118 | this.#setBaseLayout(); 119 | } 120 | 121 | async onClose() { 122 | DOMHandler.free(); 123 | } 124 | } 125 | 126 | export default class Crafty extends Plugin { 127 | private att_observer: AttributeObserver | null = null; 128 | private file_watcher: FSWatcher | null = null; 129 | 130 | private node_state: NodesState | null = null; 131 | private node_filter_state: NodesFilterState | null = null; 132 | private current_file: TFile; 133 | private current_canvas_leaf: WorkspaceLeaf | null = null; 134 | 135 | GLOBAL_CD = 100; 136 | 137 | async onload() { 138 | this.node_state = new NodesState(); 139 | this.att_observer = new AttributeObserver(); 140 | this.registerView(VIEW_TYPE, (leaf) => new BaseView(leaf)); 141 | this.node_state = new NodesState(); 142 | this.node_filter_state = new NodesFilterState(); 143 | DOMHandler.setCraftyInstance(this); 144 | //initial setup 145 | this.#updateCurrentFile(); 146 | 147 | // Filter nodes 148 | const filter_listener = new NodeFilterObserver((filters) => { 149 | const filters_display = DOMHandler.getFiltersDisplay(); 150 | const badges = filters_display.querySelectorAll( 151 | ".filter-menu-badge-display" 152 | ); 153 | 154 | //@ts-ignore 155 | for (const badge of badges) { 156 | badge.classList.remove("badge-display-active"); 157 | const span = badge.querySelector("span") as HTMLSpanElement; 158 | const value = span.getText(); 159 | const filter = filters.find((val) => val.title == value); 160 | if (filter && filter.isActive) 161 | badge.classList.add("badge-display-active"); 162 | } 163 | 164 | if (this.nodeState) { 165 | this.nodeState.setFilters( 166 | filters.filter((val) => val.isActive) 167 | ); 168 | } 169 | }); 170 | 171 | // Update tooltip 172 | const description_listener = new NodeObserver( 173 | debounce( 174 | (nodes) => { 175 | if (!this.node_state) return; 176 | const all_nodes = this.node_state.allNodes; 177 | 178 | for (const node of all_nodes) { 179 | if (!node.container) continue; 180 | if (node.description != "") { 181 | node.container.setAttribute( 182 | "aria-label", 183 | `${node.description}` 184 | ); 185 | } else { 186 | node.container?.removeAttribute("aria-label"); 187 | } 188 | } 189 | }, 190 | this.GLOBAL_CD, 191 | true 192 | ) 193 | ); 194 | 195 | // Update sidebar nodes 196 | const sidebar_node_listener = new NodeObserver( 197 | debounce( 198 | (nodes) => { 199 | DOMHandler.populateNodes(nodes); 200 | if (!this.att_observer) return; 201 | this.att_observer.observe( 202 | this.current_canvas_leaf, 203 | //@ts-ignore 204 | this.node_state 205 | ); 206 | }, 207 | this.GLOBAL_CD, 208 | true 209 | ) 210 | ); 211 | 212 | // Update sidebar description 213 | const sidebar_description_listener = new NodeObserver( 214 | debounce( 215 | (nodes) => { 216 | if (!this.node_state) return; 217 | if (!this.node_state.selectedNode) 218 | DOMHandler.showEmptyEdit(); 219 | else DOMHandler.showSelectedNode(); 220 | }, 221 | this.GLOBAL_CD, 222 | true 223 | ) 224 | ); 225 | 226 | this.node_state.registerObserver(description_listener); 227 | this.node_state.registerObserver(sidebar_node_listener); 228 | this.node_state.registerObserver(sidebar_description_listener); 229 | 230 | this.node_filter_state.registerObserver(filter_listener); 231 | 232 | this.registerEvent( 233 | this.app.workspace.on("active-leaf-change", (leaf) => { 234 | if (!leaf) return; 235 | const view_state = leaf.getViewState(); 236 | if (view_state.type != "canvas") return; 237 | if (leaf == this.current_canvas_leaf) return; 238 | this.#updateCurrentFile(); 239 | this.#updateCurrentLeaf(null); 240 | this.#trackFileChange(null); 241 | if (this.current_file.extension == "canvas") this.#syncNodes(); 242 | this.att_observer?.observe( 243 | this.current_canvas_leaf, 244 | //@ts-ignore 245 | this.node_state 246 | ); 247 | }) 248 | ); 249 | 250 | this.registerEvent( 251 | this.app.workspace.on("layout-change", () => { 252 | this.#updateCurrentFile(); 253 | this.#updateCurrentLeaf(null); 254 | this.#trackFileChange(null); 255 | 256 | if (this.current_file.extension == "canvas") { 257 | this.#syncNodes(); 258 | this.att_observer?.observe( 259 | this.current_canvas_leaf, 260 | //@ts-ignore 261 | this.node_state 262 | ); 263 | DOMHandler.showSelectedNode(); 264 | DOMHandler.showNodes(); 265 | } else { 266 | DOMHandler.showEmptyEdit(); 267 | DOMHandler.showEmptyNodes(); 268 | } 269 | }) 270 | ); 271 | 272 | this.addCommand({ 273 | id: "next-node", 274 | name: "Next node", 275 | callback: () => { 276 | if (!this.node_state) return; 277 | DOMHandler.hideTitle(); 278 | this.node_state.next(); 279 | }, 280 | }); 281 | 282 | this.addCommand({ 283 | id: "prev-node", 284 | name: "Prev node", 285 | callback: () => { 286 | if (!this.node_state) return; 287 | DOMHandler.hideTitle(); 288 | this.node_state.previous(); 289 | }, 290 | }); 291 | 292 | this.addCommand({ 293 | id: "show-panel", 294 | name: "Show Panel", 295 | callback: async () => { 296 | //@ts-ignore 297 | const rightSplit = this.app.workspace.rightSplit; 298 | const sidebar_leaf = this.sidebarLeaf; 299 | 300 | if (rightSplit.collapsed || !sidebar_leaf) { 301 | setTimeout(() => { 302 | this.activateView(); 303 | }, 50); 304 | if (rightSplit.collapsed) rightSplit.expand(); 305 | } else { 306 | this.closeView(); 307 | } 308 | }, 309 | }); 310 | } 311 | /** 312 | * Show sidebar 313 | */ 314 | async activateView() { 315 | const { workspace } = this.app; 316 | 317 | let leaf: WorkspaceLeaf | null = null; 318 | const leaves = workspace.getLeavesOfType(VIEW_TYPE); 319 | 320 | if (leaves.length > 0) { 321 | leaf = leaves[0]; 322 | } else { 323 | leaf = workspace.getRightLeaf(false); 324 | await leaf.setViewState({ type: VIEW_TYPE, active: true }); 325 | } 326 | workspace.revealLeaf(leaf); 327 | this.#updateCurrentFile(); 328 | this.#updateCurrentLeaf(null); 329 | this.#trackFileChange(null); 330 | this.#syncNodes(); 331 | } 332 | 333 | /** 334 | * Hide sidebar 335 | */ 336 | async closeView() { 337 | const { workspace } = this.app; 338 | const leaves = workspace.getLeavesOfType(VIEW_TYPE); 339 | if (leaves.length < 1) return; 340 | leaves[0].detach(); 341 | } 342 | 343 | get sidebarLeaf() { 344 | const { workspace } = this.app; 345 | const leaves = workspace.getLeavesOfType(VIEW_TYPE); 346 | if (leaves.length < 1) return; 347 | return leaves[0]; 348 | } 349 | 350 | /** 351 | * Set current_file to current activeFile 352 | * @returns void 353 | */ 354 | #updateCurrentFile() { 355 | const file = this.app.workspace.getActiveFile(); 356 | if (!file) return; 357 | this.current_file = file; 358 | } 359 | 360 | /** 361 | * Set current_canvas_leaf to leaf if leaf is a canvas 362 | * @param {WorkspaceLeaf} leaf 363 | */ 364 | #updateCurrentLeaf(leaf: WorkspaceLeaf | null) { 365 | if (!leaf) { 366 | this.app.workspace.iterateAllLeaves((leaf) => { 367 | const view_state = leaf.getViewState(); 368 | if (view_state.type != "canvas") return; 369 | //@ts-ignore 370 | const classList = leaf.containerEl.classList; 371 | if (!/mod-active/.test(classList.value)) return; 372 | this.current_canvas_leaf = leaf; 373 | }); 374 | return; 375 | } 376 | const view_state = leaf.getViewState(); 377 | if (view_state.type != "canvas") return; 378 | this.current_canvas_leaf = leaf; 379 | } 380 | 381 | /** 382 | * @param {TFile} file 383 | * Use FSWatcher to listen for change in file 384 | */ 385 | #trackFileChange(file: TFile | null) { 386 | if (!file && !this.current_file) return; 387 | if (!file) file = this.current_file; 388 | 389 | //@ts-ignore 390 | const path = `${file.vault.adapter.basePath}/${file.path}`; 391 | if (this.file_watcher) this.file_watcher.close(); 392 | this.file_watcher = watch( 393 | path, 394 | debounce(async (event) => this.#syncNodes(), this.GLOBAL_CD) 395 | ); 396 | } 397 | 398 | /** 399 | * Extract nodes content from canvas data and update the current the NodesState. 400 | * @returns 401 | */ 402 | #syncNodes() { 403 | if ( 404 | !this.current_canvas_leaf || 405 | //@ts-ignore 406 | !this.current_canvas_leaf.view.canvas 407 | ) { 408 | return; 409 | } 410 | 411 | if (!this.node_state) return; 412 | //@ts-ignore 413 | const canvas_data = this.current_canvas_leaf.view.canvas; 414 | const raw_nodes_map = this.#extractNodeData(canvas_data); 415 | 416 | const selection = Array.from( 417 | //@ts-ignore 418 | this.current_canvas_leaf.view.canvas.selection 419 | //@ts-ignore 420 | ).map((val) => val.id); 421 | 422 | if (!raw_nodes_map) { 423 | this.node_state.replace([]); 424 | this.node_state.selectNodes([]); 425 | return; 426 | } 427 | const nodes = Array.from( 428 | //@ts-ignore 429 | this.current_canvas_leaf.view.canvas.nodes, 430 | //@ts-ignore 431 | ([key, val]) => { 432 | const node = raw_nodes_map.get(key); 433 | 434 | return { 435 | id: key, 436 | title: node?.title || "Untitled", 437 | description: node?.description || "", 438 | selected: selection.includes(key), 439 | container: val.nodeEl, 440 | type: (node?.type || "") as NODE_TYPE, 441 | extension: (node?.extension || "") as FILE_TYPE, 442 | created_at: node?.created_at || 0, 443 | last_modified: node?.last_modified || 0, 444 | }; 445 | } 446 | ); 447 | 448 | this.node_state.replace(nodes); 449 | this.node_state.selectNodes(selection); 450 | } 451 | 452 | /** 453 | * Extract and format id, title and description from raw_nodes 454 | * @param raw_nodes 455 | * @returns Map 456 | */ 457 | #extractNodeData(canvas: object) { 458 | const raw_node_map: Map = new Map(); 459 | if (!canvas) return raw_node_map; 460 | 461 | //@ts-ignore 462 | const data = canvas.data.nodes; 463 | 464 | //@ts-ignore 465 | const stats = canvas.nodes; 466 | 467 | for (const el of data) { 468 | const file_stats = stats.get(el.id).file; 469 | 470 | raw_node_map.set(el.id, { 471 | id: el.id, 472 | title: 473 | el.title || 474 | this.#createTitle(el.text, el.file, el.label, el.url), 475 | description: el.description || "", 476 | type: el.type, 477 | extension: el.file ? el.file.split(".").pop() || "" : "", 478 | created_at: file_stats ? file_stats.stat.ctime : 0, 479 | last_modified: file_stats ? file_stats.stat.mtime : 0, 480 | selected: false, 481 | container: null, 482 | }); 483 | } 484 | return raw_node_map; 485 | } 486 | 487 | /** 488 | * Create default tile for nodes. 489 | * @param {string | undefined} text 490 | * @param {string | undefined} file 491 | * @param {string | undefined} label 492 | * @param {string | undefined} url 493 | * @returns 494 | */ 495 | #createTitle( 496 | text: string | undefined, 497 | file: string | undefined, 498 | label: string | undefined, 499 | url: string | undefined 500 | ) { 501 | if (file != undefined) return file?.split("/").pop() || "Untitled"; 502 | return text || label || url || "Untitled"; 503 | } 504 | 505 | getFileObserver(): FSWatcher | null { 506 | return this.file_watcher; 507 | } 508 | 509 | getAttributeObserver(): AttributeObserver | null { 510 | return this.att_observer; 511 | } 512 | 513 | onunload() { 514 | if (this.file_watcher) this.file_watcher.close(); 515 | if (this.att_observer) this.att_observer.disconnect(); 516 | DOMHandler.free(); 517 | } 518 | 519 | get vault() { 520 | return this.app.vault; 521 | } 522 | 523 | get selectedNode() { 524 | if (!this.node_state) return null; 525 | return this.node_state.selectedNode; 526 | } 527 | 528 | get currentFile() { 529 | return this.current_file; 530 | } 531 | 532 | get nodeState() { 533 | return this.node_state; 534 | } 535 | 536 | get nodeFilterState() { 537 | return this.node_filter_state; 538 | } 539 | } 540 | -------------------------------------------------------------------------------- /manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "crafty", 3 | "name": "Crafty", 4 | "version": "1.3.1", 5 | "minAppVersion": "0.15.0", 6 | "description": "Add tooltip to any canvas node and Quickly navigate between canvas nodes", 7 | "author": "liolle", 8 | "authorUrl": "https://github.com/liolle", 9 | "isDesktopOnly": true 10 | } -------------------------------------------------------------------------------- /nodes/nodes.ts: -------------------------------------------------------------------------------- 1 | // INTERFACES // 2 | export interface RawNode { 3 | description: string | undefined; 4 | file: string | undefined; 5 | text: string | undefined; 6 | label: string | undefined; 7 | url: string | undefined; 8 | title: string | undefined; 9 | height: number; 10 | id: string; 11 | type: string; 12 | width: number; 13 | x: number; 14 | y: number; 15 | } 16 | 17 | export interface CraftyNode { 18 | id: string; 19 | title: string; 20 | description: string; 21 | container: HTMLElement | null; 22 | selected: boolean; 23 | type: NODE_TYPE; 24 | extension: FILE_TYPE; 25 | created_at: number; 26 | last_modified: number; 27 | } 28 | 29 | abstract class Explorer { 30 | add: (node: CraftyNode) => void; 31 | remove: (node: CraftyNode) => CraftyNode | null; 32 | search: (word: string) => CraftyNode[]; 33 | clear: () => void; 34 | } 35 | 36 | export class NodeFilter { 37 | private _group: string; 38 | private _title: string; 39 | private _type: FILE_TYPE; 40 | private active = false; 41 | 42 | constructor(group: string, type: FILE_TYPE) { 43 | this._group = group; 44 | this._title = type == "" ? "default" : type; 45 | this._type = type; 46 | } 47 | 48 | enable() { 49 | this.active = true; 50 | } 51 | 52 | disable() { 53 | this.active = false; 54 | } 55 | 56 | get group() { 57 | return this._group; 58 | } 59 | 60 | get title() { 61 | return this._title; 62 | } 63 | 64 | get type() { 65 | return this._type; 66 | } 67 | 68 | get isActive() { 69 | return this.active; 70 | } 71 | } 72 | 73 | // INTERFACES // 74 | 75 | // TYPES // 76 | 77 | export type AUDIO_FORMAT = 78 | | "flac" 79 | | "m4a" 80 | | "mp3" 81 | | "ogg" 82 | | "wav" 83 | | "webm" 84 | | "3gp"; 85 | export type IMAGE_FORMAT = 86 | | "avif" 87 | | "bmp" 88 | | "gif" 89 | | "jpeg" 90 | | "jpg" 91 | | "png" 92 | | "svg" 93 | | "webp"; 94 | export type VIDEO_FORMAT = "mkv" | "mov" | "mp4" | "ogv" | "webm"; 95 | export type DOCUMENT_FORMAT = "canvas" | "json" | "pdf" | "md"; 96 | export type NODE_TYPE = "link" | "text" | "group" | "file" | ""; 97 | export type FILE_TYPE = 98 | | AUDIO_FORMAT 99 | | VIDEO_FORMAT 100 | | IMAGE_FORMAT 101 | | DOCUMENT_FORMAT 102 | | NODE_TYPE 103 | | "audio" 104 | | "video" 105 | | "image"; 106 | 107 | export const FILE_FORMAT = { 108 | Audio: { 109 | flac: "flac", 110 | m4a: "m4a", 111 | mp3: "mp3", 112 | ogg: "ogg", 113 | wav: "wav", 114 | webm: "webm", 115 | "3gp": "3gp", 116 | }, 117 | Image: { 118 | avif: "avif", 119 | bmp: "bmp", 120 | gif: "gif", 121 | jpeg: "jpeg", 122 | jpg: "jpg", 123 | png: "png", 124 | svg: "svg", 125 | webp: "webp", 126 | }, 127 | Video: { mkv: "mkv", mov: "mov", mp4: "mp4", ogv: "ogv", webm: "webm" }, 128 | Document: { canvas: "canvas", md: "md", pdf: "pdf", json: "json" }, 129 | General: { 130 | link: "link", 131 | text: "text", 132 | group: "group", 133 | video: "video", 134 | audio: "audio", 135 | image: "image", 136 | }, 137 | }; 138 | 139 | export type CRAFTY_NODE_SORT_TYPE = "name" | "created_at" | "last_modified"; 140 | export type NODE_ORDER = "asc" | "des"; 141 | 142 | // TYPES // 143 | 144 | export class NodesExplorer implements Explorer { 145 | #root = {}; 146 | #size = 0; 147 | 148 | #increaseSize() { 149 | this.#size++; 150 | } 151 | 152 | #decreaseSize() { 153 | if (this.#size > 0) this.#size--; 154 | } 155 | 156 | #addNode(pos: object, node: CraftyNode) { 157 | //@ts-ignore 158 | let arr: CraftyNode[] = pos["end"]; 159 | if (!arr) { 160 | arr = []; 161 | //@ts-ignore 162 | pos["end"] = arr; 163 | } 164 | 165 | for (let i = 0; i < arr.length; i++) { 166 | if (arr[i].id == node.id) { 167 | arr[i] = node; 168 | return; 169 | } 170 | } 171 | arr.push(node); 172 | this.#increaseSize(); 173 | } 174 | #splitPart(word: string): string[] { 175 | const result: string[] = []; 176 | const len = word.length; 177 | for (let idx = 0; idx < len; idx++) { 178 | result.push(word.substring(len - idx - 1, len)); 179 | } 180 | return result; 181 | } 182 | 183 | #addSingle(title: string, node: CraftyNode) { 184 | title = title.toLowerCase(); 185 | let current = this.#root; 186 | 187 | for (let idx = 0; idx < title.length; idx++) { 188 | const char = title[idx]; 189 | //@ts-ignore 190 | if (current[char]) current = current[char]; 191 | else { 192 | //@ts-ignore 193 | current[char] = {}; 194 | //@ts-ignore 195 | current = current[char]; 196 | } 197 | } 198 | this.#addNode(current, node); 199 | } 200 | 201 | add(node: CraftyNode) { 202 | for (const word of this.#splitPart(node.title)) { 203 | this.#addSingle(word, node); 204 | } 205 | } 206 | 207 | #removeNode(arr: CraftyNode[], id?: string) { 208 | const n = arr.length; 209 | if (!id) { 210 | while (arr.length > 0) { 211 | arr.pop(); 212 | this.#decreaseSize(); 213 | } 214 | return; 215 | } 216 | for (let i = 0; i < n; i++) { 217 | if (id == arr[i].id) { 218 | [arr[i]] = [arr[n - 1]]; 219 | arr.pop(); 220 | this.#decreaseSize(); 221 | } 222 | } 223 | } 224 | 225 | #removeR( 226 | word: string, 227 | idx: number, 228 | root: object, 229 | last: object, 230 | last_idx: number, 231 | id?: string 232 | ) { 233 | if (idx >= word.length) { 234 | if (!root) return; 235 | //@ts-ignore 236 | const arr = root["end"]; 237 | if (!arr) return; 238 | this.#removeNode(arr, id); 239 | if (arr.length == 0) { 240 | //@ts-ignore 241 | delete root["end"]; 242 | const keys = Object.keys(root); 243 | //@ts-ignore 244 | if (keys.length == 0) delete last[word[last_idx]]; 245 | } 246 | } 247 | 248 | if (!root) return; 249 | //@ts-ignore 250 | const next = root[word[idx]]; 251 | const keys = Object.keys(root); 252 | let len = keys.length; 253 | if (keys.includes("end")) len--; 254 | if (len > 1) { 255 | last = root; 256 | last_idx = idx; 257 | } 258 | this.#removeR(word, idx + 1, next, root, last_idx, id); 259 | } 260 | 261 | remove(node: CraftyNode) { 262 | for (const word of this.#splitPart(node.title)) { 263 | this.#removeR(word, 0, this.#root, {}, 0, node.id); 264 | } 265 | 266 | return null; 267 | } 268 | 269 | #searchR(word: string, idx: number, root: object): CraftyNode[] { 270 | word = word.toLowerCase(); 271 | if (idx >= word.length) { 272 | if (!root) return []; 273 | //@ts-ignore 274 | return root["end"] || []; 275 | } 276 | if (!root) return []; 277 | //@ts-ignore 278 | const next = root[word[idx]]; 279 | if (!next) return []; 280 | return this.#searchR(word, idx + 1, next); 281 | } 282 | 283 | search(word: string): CraftyNode[] { 284 | return this.#searchR(word, 0, this.#root); 285 | } 286 | 287 | #prefixSearchR(word: string, idx: number, root: object, acc: CraftyNode[]) { 288 | word = word.toLowerCase(); 289 | if (!root) return; 290 | const keys = Object.keys(root); 291 | if (idx >= word.length) { 292 | //@ts-ignore 293 | const nodes = root["end"]; 294 | if (nodes) for (const node of nodes) acc.push(node); 295 | for (const key of keys) { 296 | //@ts-ignore 297 | const next = root[key]; 298 | if (!next) return; 299 | if (key != "end") this.#prefixSearchR(word, idx + 1, next, acc); 300 | } 301 | } else { 302 | //@ts-ignore 303 | const [up, low] = [ 304 | word[idx].toLocaleLowerCase(), 305 | word[idx].toLocaleUpperCase(), 306 | ]; 307 | //@ts-ignore 308 | const next_lower = root[up]; 309 | //@ts-ignore 310 | const next_upper = root[low]; 311 | if (!next_lower && !next_upper) return; 312 | this.#prefixSearchR(word, idx + 1, next_lower, acc); 313 | this.#prefixSearchR(word, idx + 1, next_upper, acc); 314 | } 315 | } 316 | 317 | prefixSearch(word: string): CraftyNode[] { 318 | const res: CraftyNode[] = []; 319 | this.#prefixSearchR(word, 0, this.#root, res); 320 | return res; 321 | } 322 | 323 | #findSimilarR( 324 | root: object, 325 | letter: string, 326 | word: string, 327 | previousRow: number[], 328 | res: Array<[CraftyNode, number]>, 329 | precision: number 330 | ): void { 331 | word = word.toLowerCase(); 332 | const n = word.length; 333 | const currentRow = new Array(n + 1).fill(0); 334 | currentRow[0] = previousRow[0] + 1; 335 | let min = Infinity; 336 | for (let i = 1; i <= n; i++) { 337 | const insertionCost = currentRow[i - 1] + 1; 338 | const deleteCost = previousRow[i] + 1; 339 | let replaceCost = previousRow[i - 1]; 340 | if (word[i - 1] != letter) replaceCost++; 341 | 342 | currentRow[i] = Math.min(insertionCost, deleteCost, replaceCost); 343 | min = Math.min(min, currentRow[i]); 344 | } 345 | 346 | if (currentRow[n] <= precision) { 347 | //@ts-ignore 348 | const nodes = root["end"]; 349 | if (nodes && nodes.length > 0) { 350 | for (const node of nodes) res.push([node, currentRow[n]]); 351 | } 352 | } 353 | 354 | if (min <= precision) { 355 | const keys = Object.keys(root); 356 | for (const key of keys) { 357 | if (key == "end") continue; 358 | this.#findSimilarR( 359 | //@ts-ignore 360 | root[key], 361 | key, 362 | word, 363 | currentRow, 364 | res, 365 | precision 366 | ); 367 | } 368 | } 369 | } 370 | 371 | #lcs(s1: string, s2: string) { 372 | const n = s1.length, 373 | m = s2.length; 374 | const dp = new Array(n + 1) 375 | .fill(null) 376 | .map(() => new Array(m + 1).fill(0)); 377 | for (let i = n - 1; i >= 0; i--) { 378 | for (let j = m - 1; j >= 0; j--) { 379 | if (s1[i] == s2[j]) dp[i][j] = 1 + dp[i + 1][j + 1]; 380 | else dp[i][j] = Math.max(dp[i + 1][j], dp[i][j + 1]); 381 | } 382 | } 383 | return dp[0][0]; 384 | } 385 | 386 | findSimilar(word: string, precision: number): CraftyNode[] { 387 | word = word.toLowerCase(); 388 | const res: Array<[CraftyNode, number]> = []; 389 | const n = word.length; 390 | const currentRow = new Array(n + 1).fill(0); 391 | const keys = Object.keys(this.#root); 392 | for (const key of keys) { 393 | this.#findSimilarR( 394 | //@ts-ignore 395 | this.#root[key], 396 | key, 397 | word, 398 | currentRow, 399 | res, 400 | precision 401 | ); 402 | } 403 | res.sort((a, b) => a[1] - b[1]); 404 | return res 405 | .filter((val) => { 406 | const lcs_ration = 407 | this.#lcs(word, val[0].title.toLowerCase()) / 408 | Math.max(val[0].title.length, word.length); 409 | return lcs_ration > 0.4; 410 | }) 411 | .map((val) => val[0]); 412 | } 413 | 414 | #clearR(root: object) { 415 | for (const key of Object.keys(root)) { 416 | if (key == "end") { 417 | //@ts-ignore 418 | const arr = root["end"]; 419 | while (arr.length > 0) arr.pop(); 420 | } 421 | //@ts-ignore 422 | else this.#clearR(root[key]); 423 | //@ts-ignore 424 | delete root[key]; 425 | } 426 | } 427 | 428 | clear() { 429 | this.#clearR(this.#root); 430 | this.#size = 0; 431 | } 432 | 433 | get size() { 434 | return this.#size; 435 | } 436 | } 437 | 438 | export class NodeComparator { 439 | static SORT_BY_CREATED_AT(node1: CraftyNode, node2: CraftyNode) { 440 | if (node1.type != "file" && node2.type != "file") return 0; 441 | if (node1.type != "file") return 1; 442 | if (node2.type != "file") return -1; 443 | return node1.created_at - node2.created_at; 444 | } 445 | 446 | static SORT_BY_LAST_MODIFIED(node1: CraftyNode, node2: CraftyNode) { 447 | if (node1.type != "file" && node2.type != "file") return 0; 448 | if (node1.type != "file") return 1; 449 | if (node2.type != "file") return -1; 450 | return node2.last_modified - node1.last_modified; 451 | } 452 | 453 | static SORT_BY_NAME(node1: CraftyNode, node2: CraftyNode) { 454 | const [t1, t2] = [node1.title.toLowerCase(), node2.title.toLowerCase()]; 455 | if (t1 > t2) return 1; 456 | else if (t1 < t2) return -1; 457 | return 0; 458 | } 459 | } 460 | -------------------------------------------------------------------------------- /observers/observer.ts: -------------------------------------------------------------------------------- 1 | import { DOMHandler } from "dom/handler"; 2 | import { 3 | CRAFTY_NODE_SORT_TYPE, 4 | CraftyNode, 5 | FILE_FORMAT, 6 | FILE_TYPE, 7 | NODE_ORDER, 8 | NODE_TYPE, 9 | NodeComparator, 10 | NodeFilter, 11 | NodesExplorer, 12 | } from "nodes/nodes"; 13 | import { WorkspaceLeaf } from "obsidian"; 14 | import { 15 | AudioSpecification, 16 | ExpressionSpecification, 17 | ImageSpecification, 18 | Specification, 19 | VideoSpecification, 20 | } from "specification"; 21 | // TYPE // 22 | 23 | export abstract class Observer { 24 | update: (...args: any[]) => void; 25 | } 26 | 27 | export abstract class Navigator { 28 | current: (elem: T) => void; 29 | next: () => void; 30 | previous: () => void; 31 | } 32 | 33 | export abstract class Subject { 34 | registerObserver: (observer: Observer) => void; 35 | removeObserver: (observer: Observer) => void; 36 | notifyObserver: () => void; 37 | } 38 | 39 | export class NodeObserver implements Observer { 40 | callback: (nodes: CraftyNode[]) => void; 41 | constructor(callback: (nodes: CraftyNode[]) => void) { 42 | this.callback = callback; 43 | } 44 | update(nodes: CraftyNode[]) { 45 | this.callback(nodes); 46 | } 47 | } 48 | 49 | export class NodeFilterObserver implements Observer { 50 | callback: (filters: NodeFilter[]) => void; 51 | constructor(callback: (filters: NodeFilter[]) => void) { 52 | this.callback = callback; 53 | } 54 | update(filters: NodeFilter[]) { 55 | this.callback(filters); 56 | } 57 | } 58 | 59 | // TYPE // 60 | 61 | export class AttributeObserver { 62 | private observer: MutationObserver | null; 63 | private config = { attributes: true, attributeFilter: ["class"] }; 64 | 65 | observe(leaf: WorkspaceLeaf | null, node_state: NodesState) { 66 | if (!leaf) return; 67 | if (this.observer) this.disconnect(); 68 | this.observer = new MutationObserver((mutation) => { 69 | this.#callback(leaf, node_state); 70 | }); 71 | 72 | this.#addObservableElement(leaf); 73 | } 74 | 75 | #callback(leaf: WorkspaceLeaf, node_state: NodesState) { 76 | const view_state = leaf.getViewState(); 77 | if (view_state.type != "canvas") return; 78 | const selection = Array.from( 79 | //@ts-ignore 80 | leaf.view.canvas.selection, 81 | //@ts-ignore 82 | (val) => val.id 83 | ); 84 | 85 | node_state.selectNodes(selection); 86 | } 87 | 88 | #addObservableElement(leaf: WorkspaceLeaf) { 89 | const view_state = leaf.getViewState(); 90 | if (view_state.type != "canvas") return; 91 | const nodes = Array.from( 92 | //@ts-ignore 93 | leaf.view.canvas.nodes, 94 | //@ts-ignore 95 | ([id, value]) => ({ 96 | id, 97 | container: value.nodeEl, 98 | data: value.unknownData, 99 | }) 100 | ); 101 | 102 | if (this.observer) { 103 | for (const node of nodes) { 104 | this.observer.observe(node.container, this.config); 105 | } 106 | } 107 | } 108 | 109 | disconnect() { 110 | if (this.observer) this.observer.disconnect(); 111 | this.observer = null; 112 | } 113 | } 114 | 115 | /** 116 | * 117 | */ 118 | export class NodesState implements Subject, Navigator { 119 | private observers: NodeObserver[] = []; 120 | 121 | private node_map: Map = new Map(); 122 | private rel_node_map: Map = new Map(); 123 | private node_arr: CraftyNode[] = []; 124 | private rel_node_arr: CraftyNode[] = []; 125 | private selected: string[] = []; 126 | private firstID: string; 127 | private currentID = ""; 128 | private lastID = ""; 129 | private node_explorer = new NodesExplorer(); 130 | private currentSearch = ""; 131 | private sort_by: CRAFTY_NODE_SORT_TYPE = "name"; 132 | private node_order: NODE_ORDER = "asc"; 133 | private filters: NodeFilter[] = []; 134 | 135 | registerObserver(observer: NodeObserver) { 136 | this.observers.push(observer); 137 | } 138 | removeObserver(observer: NodeObserver) { 139 | this.observers = this.observers.filter((val) => val != observer); 140 | } 141 | notifyObserver() { 142 | for (const obs of this.observers) { 143 | obs.update(this.nodes); 144 | } 145 | } 146 | 147 | // List 148 | #swapIdx(left: number, right: number) { 149 | const node1 = this.node_arr[left]; 150 | const node2 = this.node_arr[right]; 151 | 152 | this.node_map.set(node1.id, right); 153 | this.node_map.set(node2.id, left); 154 | 155 | [this.node_arr[left], this.node_arr[right]] = [ 156 | this.node_arr[right], 157 | this.node_arr[left], 158 | ]; 159 | } 160 | 161 | #indexNodes() { 162 | this.rel_node_map.clear(); 163 | const n = this.rel_node_arr.length; 164 | for (let i = 0; i < n; i++) 165 | this.rel_node_map.set(this.rel_node_arr[i].id, i); 166 | } 167 | 168 | #clearRelNodes() { 169 | while (this.rel_node_arr.length > 0) this.rel_node_arr.pop(); 170 | } 171 | 172 | #sort() { 173 | const sort_button = DOMHandler.getSortButton(); 174 | const text = sort_button.querySelector(".sb-text") as HTMLSpanElement; 175 | const sort_menu = DOMHandler.getSortMenu(); 176 | const sort_name = sort_menu.querySelector(".s-name"); 177 | const sort_created = sort_menu.querySelector(".s-created"); 178 | const sort_last = sort_menu.querySelector(".s-last"); 179 | //@ts-ignore 180 | for (const node of [sort_name, sort_created, sort_last]) { 181 | if (!node) continue; 182 | node.classList.remove("check-active"); 183 | } 184 | switch (this.sort_by) { 185 | case "name": 186 | this.rel_node_arr.sort(NodeComparator.SORT_BY_NAME); 187 | if (text) text.setText("Name"); 188 | if (sort_name) sort_name.classList.add("check-active"); 189 | break; 190 | case "created_at": 191 | this.rel_node_arr.sort(NodeComparator.SORT_BY_CREATED_AT); 192 | if (text) text.setText("Created_at"); 193 | if (sort_created) sort_created.classList.add("check-active"); 194 | break; 195 | case "last_modified": 196 | this.rel_node_arr.sort(NodeComparator.SORT_BY_LAST_MODIFIED); 197 | if (text) text.setText("Last_modified"); 198 | if (sort_last) sort_last.classList.add("check-active"); 199 | break; 200 | default: 201 | this.rel_node_arr.sort(NodeComparator.SORT_BY_NAME); 202 | break; 203 | } 204 | } 205 | 206 | #order() { 207 | const sort_menu = DOMHandler.getSortMenu(); 208 | const sort_asc = sort_menu.querySelector(".s-asc"); 209 | const sort_desc = sort_menu.querySelector(".s-desc"); 210 | //@ts-ignore 211 | for (const node of [sort_asc, sort_desc]) { 212 | if (!node) continue; 213 | node.classList.remove("check-active"); 214 | } 215 | 216 | if (this.node_order == "des") { 217 | this.rel_node_arr.reverse(); 218 | if (sort_desc) sort_desc.classList.add("check-active"); 219 | } else { 220 | if (sort_asc) sort_asc.classList.add("check-active"); 221 | } 222 | } 223 | 224 | #getFilterSpec() { 225 | if (this.filters.length == 0) 226 | return new ExpressionSpecification(() => true); 227 | let specification: Specification = 228 | new ExpressionSpecification(() => false); 229 | 230 | for (const filter of this.filters) { 231 | if (filter.type == "audio") { 232 | specification = specification.or( 233 | new AudioSpecification() 234 | ); 235 | } else if (filter.type == "video") { 236 | specification = specification.or( 237 | new VideoSpecification() 238 | ); 239 | } else if (filter.type == "image") { 240 | specification = specification.or( 241 | new ImageSpecification() 242 | ); 243 | } else { 244 | const or_specification = 245 | new ExpressionSpecification((candidate) => { 246 | if (candidate.type == "file") 247 | return candidate.extension == filter.type; 248 | else return candidate.type == filter.type; 249 | }); 250 | specification = specification.or(or_specification); 251 | } 252 | } 253 | 254 | return specification; 255 | } 256 | 257 | #PopulateRelNodes(nodes: CraftyNode[]) { 258 | this.#clearRelNodes(); 259 | const spec = this.#getFilterSpec(); 260 | for (const node of nodes) { 261 | if (spec.isSatisfied(node)) this.rel_node_arr.push(node); 262 | } 263 | 264 | this.#sort(); 265 | this.#order(); 266 | this.#indexNodes(); 267 | } 268 | 269 | add(nodes: CraftyNode[]) { 270 | for (const node of nodes) { 271 | if (this.node_map.size == 0) this.firstID = node.id; 272 | if (this.selected.includes(node.id)) node.selected = true; 273 | this.node_map.set(node.id, this.node_arr.length); 274 | this.node_arr.push(node); 275 | this.node_explorer.add(node); 276 | } 277 | this.firstID = ""; 278 | if (nodes.length > 0) this.firstID = this.node_arr[0].id; 279 | this.notifyObserver(); 280 | } 281 | 282 | remove(id_list: string[]) { 283 | for (const id of id_list) { 284 | if (this.node_map.has(id)) { 285 | //@ts-ignore 286 | this.#swapIdx(this.node_map.get(id), this.node_arr.length - 1); 287 | const node = this.node_arr.pop(); 288 | if (node) this.node_explorer.remove(node); 289 | this.node_map.delete(id); 290 | //@ts-ignore 291 | this.#swapIdx(this.node_map.get(id), this.node_arr.length - 1); 292 | } 293 | } 294 | 295 | this.notifyObserver(); 296 | } 297 | 298 | replace(nodes: CraftyNode[]) { 299 | while (this.node_arr.length > 0) this.node_arr.pop(); 300 | this.node_map.clear(); 301 | this.node_explorer.clear(); 302 | this.add(nodes); 303 | } 304 | 305 | selectNodes(id_list: string[]) { 306 | const n = id_list.length; 307 | this.selected = id_list; 308 | if (n == 0) { 309 | this.currentID = ""; 310 | } else { 311 | this.lastID = this.currentID; 312 | this.currentID = id_list[0]; 313 | } 314 | for (const node of this.node_arr) node.selected = false; 315 | for (const id of id_list) { 316 | const node_idx = this.node_map.get(id); 317 | if (node_idx == undefined) continue; 318 | this.node_arr[node_idx].selected = true; 319 | } 320 | this.notifyObserver(); 321 | } 322 | 323 | order(order: NODE_ORDER) { 324 | if (this.node_order != order) { 325 | this.node_order = order; 326 | this.notifyObserver(); 327 | } 328 | } 329 | 330 | sortBy(sort_by: CRAFTY_NODE_SORT_TYPE) { 331 | if (this.sort_by != sort_by) { 332 | this.sort_by = sort_by; 333 | this.notifyObserver(); 334 | } 335 | } 336 | 337 | setFilters(filters: NodeFilter[]) { 338 | this.filters = filters; 339 | this.notifyObserver(); 340 | } 341 | 342 | // Search 343 | 344 | #update() { 345 | if (this.currentSearch == "") this.#PopulateRelNodes(this.node_arr); 346 | else { 347 | this.#PopulateRelNodes( 348 | // this.node_explorer.findSimilar(this.currentSearch, 4) 349 | this.node_explorer.prefixSearch(this.currentSearch) 350 | ); 351 | } 352 | } 353 | 354 | setSearchWord(word: string) { 355 | this.currentSearch = word; 356 | this.notifyObserver(); 357 | } 358 | 359 | // Navigator 360 | current(id: string) { 361 | this.currentID = id; 362 | const idx = this.node_map.get(id); 363 | if (idx === undefined) return; 364 | 365 | const next_node = this.node_arr[idx]; 366 | if (!next_node.container) return; 367 | next_node.container.click(); 368 | } 369 | 370 | #findFirstNode() { 371 | const id = this.rel_node_arr[0].id; 372 | this.currentID = id || ""; 373 | 374 | const idx = this.rel_node_map.get(id); 375 | if (idx == undefined) return null; 376 | 377 | const next_node = this.rel_node_arr[idx]; 378 | if (!next_node.container) return null; 379 | 380 | return next_node; 381 | } 382 | 383 | next() { 384 | const idx = this.rel_node_map.get(this.currentID); 385 | 386 | if (idx === undefined) { 387 | const next_node = this.#findFirstNode(); 388 | if (!next_node || !next_node.container) return; 389 | next_node.container.click(); 390 | return; 391 | } 392 | 393 | const next_idx = (idx + 1) % this.rel_node_arr.length; 394 | const next_node = this.rel_node_arr[next_idx]; 395 | if (!next_node.container) return; 396 | next_node.container.click(); 397 | } 398 | previous() { 399 | const idx = this.rel_node_map.get(this.currentID); 400 | 401 | if (idx === undefined) { 402 | const next_node = this.#findFirstNode(); 403 | if (!next_node || !next_node.container) return; 404 | next_node.container.click(); 405 | return; 406 | } 407 | 408 | const prev_idx = idx - 1 < 0 ? this.rel_node_arr.length - 1 : idx - 1; 409 | const next_node = this.rel_node_arr[prev_idx]; 410 | if (!next_node.container) return; 411 | next_node.container.click(); 412 | } 413 | 414 | get nodes() { 415 | this.#update(); 416 | return this.rel_node_arr; 417 | } 418 | 419 | get allNodes() { 420 | return this.node_arr; 421 | } 422 | 423 | get selectedNode() { 424 | const idx = this.node_map.get(this.currentID); 425 | if (idx == undefined) return null; 426 | return this.node_arr[idx]; 427 | } 428 | 429 | get isNodeSame() { 430 | if (this.currentID == "" || this.lastID == "") return false; 431 | return this.currentID == this.lastID; 432 | } 433 | } 434 | 435 | export class NodesFilterState implements Subject { 436 | private observers: NodeFilterObserver[] = []; 437 | private filter_index: Map = new Map(); 438 | private filter_list: NodeFilter[] = []; 439 | 440 | constructor() { 441 | let idx = 0; 442 | for (const group in FILE_FORMAT) { 443 | //@ts-ignore 444 | for (const type in FILE_FORMAT[group]) { 445 | const t = type as FILE_TYPE; 446 | this.filter_list.push(new NodeFilter(group, t)); 447 | this.filter_index.set(t, idx++); 448 | } 449 | } 450 | } 451 | 452 | registerObserver(observer: NodeFilterObserver) { 453 | this.observers.push(observer); 454 | } 455 | removeObserver(observer: NodeFilterObserver) { 456 | this.observers = this.observers.filter((val) => val != observer); 457 | } 458 | notifyObserver() { 459 | for (const obs of this.observers) { 460 | obs.update(this.filter_list); 461 | } 462 | } 463 | 464 | addFilter(filter: FILE_TYPE) { 465 | const idx = this.filter_index.get(filter); 466 | if (!idx) return; 467 | if (this.filter_list[idx].isActive) return; 468 | this.filter_list[idx].enable(); 469 | this.notifyObserver(); 470 | } 471 | 472 | removeFilter(filter: FILE_TYPE) { 473 | const idx = this.filter_index.get(filter); 474 | if (!idx) return; 475 | if (!this.filter_list[idx].isActive) return; 476 | this.filter_list[idx].disable(); 477 | this.notifyObserver(); 478 | } 479 | 480 | getFilterByGroup(group: string) { 481 | return this.filter_list.filter((val) => val.group == group); 482 | } 483 | 484 | get activeFilters() { 485 | return this.filter_list.filter((val) => val.isActive); 486 | } 487 | 488 | get allFilters() { 489 | return this.filter_list; 490 | } 491 | } 492 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Crafty", 3 | "version": "1.0.1", 4 | "description": "This is a sample plugin for Obsidian (https://obsidian.md)", 5 | "main": "main.js", 6 | "scripts": { 7 | "test": "vitest", 8 | "dev": "node esbuild.config.mjs", 9 | "build": "tsc -noEmit -skipLibCheck && node esbuild.config.mjs production", 10 | "version": "node version-bump.mjs && git add manifest.json versions.json" 11 | }, 12 | "keywords": [], 13 | "author": "", 14 | "license": "MIT", 15 | "devDependencies": { 16 | "@types/node": "^16.18.97", 17 | "@typescript-eslint/eslint-plugin": "5.29.0", 18 | "@typescript-eslint/parser": "5.29.0", 19 | "builtin-modules": "3.3.0", 20 | "esbuild": "0.17.3", 21 | "obsidian": "latest", 22 | "tslib": "2.4.0", 23 | "typescript": "4.7.4", 24 | "vitest": "^1.6.0" 25 | }, 26 | "dependencies": { 27 | "@shoelace-style/shoelace": "^2.15.0" 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /specification/index.ts: -------------------------------------------------------------------------------- 1 | import { FILE_FORMAT } from "nodes/nodes"; 2 | 3 | export abstract class Specification { 4 | and: (specification: Specification) => Specification; 5 | or: (specification: Specification) => Specification; 6 | not: (specification: Specification) => Specification; 7 | isSatisfied: (object: T) => boolean; 8 | } 9 | 10 | export abstract class CompositeSpecification implements Specification { 11 | and(other: Specification): Specification { 12 | return new AndSpecification(this, other); 13 | } 14 | 15 | or(other: Specification): Specification { 16 | return new OrSpecification(this, other); 17 | } 18 | 19 | not(): Specification { 20 | return new NotSpecification(this); 21 | } 22 | 23 | abstract isSatisfied(candidate: T): boolean; 24 | } 25 | 26 | class AndSpecification extends CompositeSpecification { 27 | private left: Specification; 28 | private right: Specification; 29 | 30 | constructor(left: Specification, right: Specification) { 31 | super(); 32 | this.left = left; 33 | this.right = right; 34 | } 35 | 36 | isSatisfied(candidate: T): boolean { 37 | return ( 38 | this.left.isSatisfied(candidate) && 39 | this.right.isSatisfied(candidate) 40 | ); 41 | } 42 | } 43 | 44 | class OrSpecification extends CompositeSpecification { 45 | private left: Specification; 46 | private right: Specification; 47 | 48 | constructor(left: Specification, right: Specification) { 49 | super(); 50 | this.left = left; 51 | this.right = right; 52 | } 53 | 54 | isSatisfied(candidate: T): boolean { 55 | return ( 56 | this.left.isSatisfied(candidate) || 57 | this.right.isSatisfied(candidate) 58 | ); 59 | } 60 | } 61 | 62 | class NotSpecification extends CompositeSpecification { 63 | private other: Specification; 64 | constructor(other: Specification) { 65 | super(); 66 | this.other = other; 67 | } 68 | 69 | isSatisfied(candidate: T): boolean { 70 | return !this.other.isSatisfied(candidate); 71 | } 72 | } 73 | 74 | export class ExpressionSpecification extends CompositeSpecification { 75 | private callback: (candidate: T) => boolean; 76 | constructor(callback: (candidate: T) => boolean) { 77 | super(); 78 | this.callback = callback; 79 | } 80 | 81 | isSatisfied(candidate: T): boolean { 82 | return this.callback(candidate); 83 | } 84 | } 85 | 86 | export class AudioSpecification extends CompositeSpecification { 87 | isSatisfied(candidate: T): boolean { 88 | //@ts-ignore 89 | if (!candidate.type == "file") return false; 90 | //@ts-ignore 91 | return candidate.extension in FILE_FORMAT.Audio; 92 | } 93 | } 94 | 95 | export class ImageSpecification extends CompositeSpecification { 96 | isSatisfied(candidate: T): boolean { 97 | //@ts-ignore 98 | if (!candidate.type == "file") return false; 99 | //@ts-ignore 100 | return candidate.extension in FILE_FORMAT.Image; 101 | } 102 | } 103 | 104 | export class VideoSpecification extends CompositeSpecification { 105 | isSatisfied(candidate: T): boolean { 106 | //@ts-ignore 107 | if (!candidate.type == "file") return false; 108 | //@ts-ignore 109 | return candidate.extension in FILE_FORMAT.Video; 110 | } 111 | } 112 | 113 | export class DocumentSpecification extends CompositeSpecification { 114 | isSatisfied(candidate: T): boolean { 115 | //@ts-ignore 116 | if (!candidate.type == "file") return false; 117 | //@ts-ignore 118 | return candidate.extension in FILE_FORMAT.Document; 119 | } 120 | } 121 | 122 | export class FileSpecification extends CompositeSpecification { 123 | isSatisfied(candidate: T): boolean { 124 | //@ts-ignore 125 | return candidate.type == "file"; 126 | } 127 | } 128 | 129 | export class TextSpecification extends CompositeSpecification { 130 | isSatisfied(candidate: T): boolean { 131 | //@ts-ignore 132 | return candidate.type == "text"; 133 | } 134 | } 135 | 136 | export class WebSpecification extends CompositeSpecification { 137 | isSatisfied(candidate: T): boolean { 138 | //@ts-ignore 139 | return candidate.type == "link"; 140 | } 141 | } 142 | 143 | export class GroupSpecification extends CompositeSpecification { 144 | isSatisfied(candidate: T): boolean { 145 | //@ts-ignore 146 | return candidate.type == "group"; 147 | } 148 | } 149 | -------------------------------------------------------------------------------- /styles.css: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | This CSS file will be included with your plugin, and 4 | available in the app when your plugin is enabled. 5 | 6 | If your plugin does not need CSS, delete this file. 7 | 8 | */ 9 | 10 | /* node_modules/.pnpm/@shoelace-style+shoelace@2.15.0_@types+react@18.2.74/node_modules/@shoelace-style/shoelace/dist/themes/light.css */ 11 | :root, 12 | :host, 13 | .sl-theme-light { 14 | color-scheme: light; 15 | 16 | --sl-color-gray-50: hsl(0 0% 97.5%); 17 | --sl-color-gray-100: hsl(240 4.8% 95.9%); 18 | --sl-color-gray-200: hsl(240 5.9% 90%); 19 | --sl-color-gray-300: hsl(240 4.9% 83.9%); 20 | --sl-color-gray-400: hsl(240 5% 64.9%); 21 | --sl-color-gray-500: hsl(240 3.8% 46.1%); 22 | --sl-color-gray-600: hsl(240 5.2% 33.9%); 23 | --sl-color-gray-700: hsl(240 5.3% 26.1%); 24 | --sl-color-gray-800: hsl(240 3.7% 15.9%); 25 | --sl-color-gray-900: hsl(240 5.9% 10%); 26 | --sl-color-gray-950: hsl(240 7.3% 8%); 27 | --sl-color-red-50: hsl(0 85.7% 97.3%); 28 | --sl-color-red-100: hsl(0 93.3% 94.1%); 29 | --sl-color-red-200: hsl(0 96.3% 89.4%); 30 | --sl-color-red-300: hsl(0 93.5% 81.8%); 31 | --sl-color-red-400: hsl(0 90.6% 70.8%); 32 | --sl-color-red-500: hsl(0 84.2% 60.2%); 33 | --sl-color-red-600: hsl(0 72.2% 50.6%); 34 | --sl-color-red-700: hsl(0 73.7% 41.8%); 35 | --sl-color-red-800: hsl(0 70% 35.3%); 36 | --sl-color-red-900: hsl(0 62.8% 30.6%); 37 | --sl-color-red-950: hsl(0 60% 19.6%); 38 | --sl-color-orange-50: hsl(33.3 100% 96.5%); 39 | --sl-color-orange-100: hsl(34.3 100% 91.8%); 40 | --sl-color-orange-200: hsl(32.1 97.7% 83.1%); 41 | --sl-color-orange-300: hsl(30.7 97.2% 72.4%); 42 | --sl-color-orange-400: hsl(27 96% 61%); 43 | --sl-color-orange-500: hsl(24.6 95% 53.1%); 44 | --sl-color-orange-600: hsl(20.5 90.2% 48.2%); 45 | --sl-color-orange-700: hsl(17.5 88.3% 40.4%); 46 | --sl-color-orange-800: hsl(15 79.1% 33.7%); 47 | --sl-color-orange-900: hsl(15.3 74.6% 27.8%); 48 | --sl-color-orange-950: hsl(15.2 69.1% 19%); 49 | --sl-color-amber-50: hsl(48 100% 96.1%); 50 | --sl-color-amber-100: hsl(48 96.5% 88.8%); 51 | --sl-color-amber-200: hsl(48 96.6% 76.7%); 52 | --sl-color-amber-300: hsl(45.9 96.7% 64.5%); 53 | --sl-color-amber-400: hsl(43.3 96.4% 56.3%); 54 | --sl-color-amber-500: hsl(37.7 92.1% 50.2%); 55 | --sl-color-amber-600: hsl(32.1 94.6% 43.7%); 56 | --sl-color-amber-700: hsl(26 90.5% 37.1%); 57 | --sl-color-amber-800: hsl(22.7 82.5% 31.4%); 58 | --sl-color-amber-900: hsl(21.7 77.8% 26.5%); 59 | --sl-color-amber-950: hsl(22.9 74.1% 16.7%); 60 | --sl-color-yellow-50: hsl(54.5 91.7% 95.3%); 61 | --sl-color-yellow-100: hsl(54.9 96.7% 88%); 62 | --sl-color-yellow-200: hsl(52.8 98.3% 76.9%); 63 | --sl-color-yellow-300: hsl(50.4 97.8% 63.5%); 64 | --sl-color-yellow-400: hsl(47.9 95.8% 53.1%); 65 | --sl-color-yellow-500: hsl(45.4 93.4% 47.5%); 66 | --sl-color-yellow-600: hsl(40.6 96.1% 40.4%); 67 | --sl-color-yellow-700: hsl(35.5 91.7% 32.9%); 68 | --sl-color-yellow-800: hsl(31.8 81% 28.8%); 69 | --sl-color-yellow-900: hsl(28.4 72.5% 25.7%); 70 | --sl-color-yellow-950: hsl(33.1 69% 13.9%); 71 | --sl-color-lime-50: hsl(78.3 92% 95.1%); 72 | --sl-color-lime-100: hsl(79.6 89.1% 89.2%); 73 | --sl-color-lime-200: hsl(80.9 88.5% 79.6%); 74 | --sl-color-lime-300: hsl(82 84.5% 67.1%); 75 | --sl-color-lime-400: hsl(82.7 78% 55.5%); 76 | --sl-color-lime-500: hsl(83.7 80.5% 44.3%); 77 | --sl-color-lime-600: hsl(84.8 85.2% 34.5%); 78 | --sl-color-lime-700: hsl(85.9 78.4% 27.3%); 79 | --sl-color-lime-800: hsl(86.3 69% 22.7%); 80 | --sl-color-lime-900: hsl(87.6 61.2% 20.2%); 81 | --sl-color-lime-950: hsl(86.5 60.6% 13.9%); 82 | --sl-color-green-50: hsl(138.5 76.5% 96.7%); 83 | --sl-color-green-100: hsl(140.6 84.2% 92.5%); 84 | --sl-color-green-200: hsl(141 78.9% 85.1%); 85 | --sl-color-green-300: hsl(141.7 76.6% 73.1%); 86 | --sl-color-green-400: hsl(141.9 69.2% 58%); 87 | --sl-color-green-500: hsl(142.1 70.6% 45.3%); 88 | --sl-color-green-600: hsl(142.1 76.2% 36.3%); 89 | --sl-color-green-700: hsl(142.4 71.8% 29.2%); 90 | --sl-color-green-800: hsl(142.8 64.2% 24.1%); 91 | --sl-color-green-900: hsl(143.8 61.2% 20.2%); 92 | --sl-color-green-950: hsl(144.3 60.7% 12%); 93 | --sl-color-emerald-50: hsl(151.8 81% 95.9%); 94 | --sl-color-emerald-100: hsl(149.3 80.4% 90%); 95 | --sl-color-emerald-200: hsl(152.4 76% 80.4%); 96 | --sl-color-emerald-300: hsl(156.2 71.6% 66.9%); 97 | --sl-color-emerald-400: hsl(158.1 64.4% 51.6%); 98 | --sl-color-emerald-500: hsl(160.1 84.1% 39.4%); 99 | --sl-color-emerald-600: hsl(161.4 93.5% 30.4%); 100 | --sl-color-emerald-700: hsl(162.9 93.5% 24.3%); 101 | --sl-color-emerald-800: hsl(163.1 88.1% 19.8%); 102 | --sl-color-emerald-900: hsl(164.2 85.7% 16.5%); 103 | --sl-color-emerald-950: hsl(164.3 87.5% 9.4%); 104 | --sl-color-teal-50: hsl(166.2 76.5% 96.7%); 105 | --sl-color-teal-100: hsl(167.2 85.5% 89.2%); 106 | --sl-color-teal-200: hsl(168.4 83.8% 78.2%); 107 | --sl-color-teal-300: hsl(170.6 76.9% 64.3%); 108 | --sl-color-teal-400: hsl(172.5 66% 50.4%); 109 | --sl-color-teal-500: hsl(173.4 80.4% 40%); 110 | --sl-color-teal-600: hsl(174.7 83.9% 31.6%); 111 | --sl-color-teal-700: hsl(175.3 77.4% 26.1%); 112 | --sl-color-teal-800: hsl(176.1 69.4% 21.8%); 113 | --sl-color-teal-900: hsl(175.9 60.8% 19%); 114 | --sl-color-teal-950: hsl(176.5 58.6% 11.4%); 115 | --sl-color-cyan-50: hsl(183.2 100% 96.3%); 116 | --sl-color-cyan-100: hsl(185.1 95.9% 90.4%); 117 | --sl-color-cyan-200: hsl(186.2 93.5% 81.8%); 118 | --sl-color-cyan-300: hsl(187 92.4% 69%); 119 | --sl-color-cyan-400: hsl(187.9 85.7% 53.3%); 120 | --sl-color-cyan-500: hsl(188.7 94.5% 42.7%); 121 | --sl-color-cyan-600: hsl(191.6 91.4% 36.5%); 122 | --sl-color-cyan-700: hsl(192.9 82.3% 31%); 123 | --sl-color-cyan-800: hsl(194.4 69.6% 27.1%); 124 | --sl-color-cyan-900: hsl(196.4 63.6% 23.7%); 125 | --sl-color-cyan-950: hsl(196.8 61% 16.1%); 126 | --sl-color-sky-50: hsl(204 100% 97.1%); 127 | --sl-color-sky-100: hsl(204 93.8% 93.7%); 128 | --sl-color-sky-200: hsl(200.6 94.4% 86.1%); 129 | --sl-color-sky-300: hsl(199.4 95.5% 73.9%); 130 | --sl-color-sky-400: hsl(198.4 93.2% 59.6%); 131 | --sl-color-sky-500: hsl(198.6 88.7% 48.4%); 132 | --sl-color-sky-600: hsl(200.4 98% 39.4%); 133 | --sl-color-sky-700: hsl(201.3 96.3% 32.2%); 134 | --sl-color-sky-800: hsl(201 90% 27.5%); 135 | --sl-color-sky-900: hsl(202 80.3% 23.9%); 136 | --sl-color-sky-950: hsl(202.3 73.8% 16.5%); 137 | --sl-color-blue-50: hsl(213.8 100% 96.9%); 138 | --sl-color-blue-100: hsl(214.3 94.6% 92.7%); 139 | --sl-color-blue-200: hsl(213.3 96.9% 87.3%); 140 | --sl-color-blue-300: hsl(211.7 96.4% 78.4%); 141 | --sl-color-blue-400: hsl(213.1 93.9% 67.8%); 142 | --sl-color-blue-500: hsl(217.2 91.2% 59.8%); 143 | --sl-color-blue-600: hsl(221.2 83.2% 53.3%); 144 | --sl-color-blue-700: hsl(224.3 76.3% 48%); 145 | --sl-color-blue-800: hsl(225.9 70.7% 40.2%); 146 | --sl-color-blue-900: hsl(224.4 64.3% 32.9%); 147 | --sl-color-blue-950: hsl(226.2 55.3% 18.4%); 148 | --sl-color-indigo-50: hsl(225.9 100% 96.7%); 149 | --sl-color-indigo-100: hsl(226.5 100% 93.9%); 150 | --sl-color-indigo-200: hsl(228 96.5% 88.8%); 151 | --sl-color-indigo-300: hsl(229.7 93.5% 81.8%); 152 | --sl-color-indigo-400: hsl(234.5 89.5% 73.9%); 153 | --sl-color-indigo-500: hsl(238.7 83.5% 66.7%); 154 | --sl-color-indigo-600: hsl(243.4 75.4% 58.6%); 155 | --sl-color-indigo-700: hsl(244.5 57.9% 50.6%); 156 | --sl-color-indigo-800: hsl(243.7 54.5% 41.4%); 157 | --sl-color-indigo-900: hsl(242.2 47.4% 34.3%); 158 | --sl-color-indigo-950: hsl(243.5 43.6% 22.9%); 159 | --sl-color-violet-50: hsl(250 100% 97.6%); 160 | --sl-color-violet-100: hsl(251.4 91.3% 95.5%); 161 | --sl-color-violet-200: hsl(250.5 95.2% 91.8%); 162 | --sl-color-violet-300: hsl(252.5 94.7% 85.1%); 163 | --sl-color-violet-400: hsl(255.1 91.7% 76.3%); 164 | --sl-color-violet-500: hsl(258.3 89.5% 66.3%); 165 | --sl-color-violet-600: hsl(262.1 83.3% 57.8%); 166 | --sl-color-violet-700: hsl(263.4 70% 50.4%); 167 | --sl-color-violet-800: hsl(263.4 69.3% 42.2%); 168 | --sl-color-violet-900: hsl(263.5 67.4% 34.9%); 169 | --sl-color-violet-950: hsl(265.1 61.5% 21.4%); 170 | --sl-color-purple-50: hsl(270 100% 98%); 171 | --sl-color-purple-100: hsl(268.7 100% 95.5%); 172 | --sl-color-purple-200: hsl(268.6 100% 91.8%); 173 | --sl-color-purple-300: hsl(269.2 97.4% 85.1%); 174 | --sl-color-purple-400: hsl(270 95.2% 75.3%); 175 | --sl-color-purple-500: hsl(270.7 91% 65.1%); 176 | --sl-color-purple-600: hsl(271.5 81.3% 55.9%); 177 | --sl-color-purple-700: hsl(272.1 71.7% 47.1%); 178 | --sl-color-purple-800: hsl(272.9 67.2% 39.4%); 179 | --sl-color-purple-900: hsl(273.6 65.6% 32%); 180 | --sl-color-purple-950: hsl(276 59.5% 16.5%); 181 | --sl-color-fuchsia-50: hsl(289.1 100% 97.8%); 182 | --sl-color-fuchsia-100: hsl(287 100% 95.5%); 183 | --sl-color-fuchsia-200: hsl(288.3 95.8% 90.6%); 184 | --sl-color-fuchsia-300: hsl(291.1 93.1% 82.9%); 185 | --sl-color-fuchsia-400: hsl(292 91.4% 72.5%); 186 | --sl-color-fuchsia-500: hsl(292.2 84.1% 60.6%); 187 | --sl-color-fuchsia-600: hsl(293.4 69.5% 48.8%); 188 | --sl-color-fuchsia-700: hsl(294.7 72.4% 39.8%); 189 | --sl-color-fuchsia-800: hsl(295.4 70.2% 32.9%); 190 | --sl-color-fuchsia-900: hsl(296.7 63.6% 28%); 191 | --sl-color-fuchsia-950: hsl(297.1 56.8% 14.5%); 192 | --sl-color-pink-50: hsl(327.3 73.3% 97.1%); 193 | --sl-color-pink-100: hsl(325.7 77.8% 94.7%); 194 | --sl-color-pink-200: hsl(325.9 84.6% 89.8%); 195 | --sl-color-pink-300: hsl(327.4 87.1% 81.8%); 196 | --sl-color-pink-400: hsl(328.6 85.5% 70.2%); 197 | --sl-color-pink-500: hsl(330.4 81.2% 60.4%); 198 | --sl-color-pink-600: hsl(333.3 71.4% 50.6%); 199 | --sl-color-pink-700: hsl(335.1 77.6% 42%); 200 | --sl-color-pink-800: hsl(335.8 74.4% 35.3%); 201 | --sl-color-pink-900: hsl(335.9 69% 30.4%); 202 | --sl-color-pink-950: hsl(336.2 65.4% 15.9%); 203 | --sl-color-rose-50: hsl(355.7 100% 97.3%); 204 | --sl-color-rose-100: hsl(355.6 100% 94.7%); 205 | --sl-color-rose-200: hsl(352.7 96.1% 90%); 206 | --sl-color-rose-300: hsl(352.6 95.7% 81.8%); 207 | --sl-color-rose-400: hsl(351.3 94.5% 71.4%); 208 | --sl-color-rose-500: hsl(349.7 89.2% 60.2%); 209 | --sl-color-rose-600: hsl(346.8 77.2% 49.8%); 210 | --sl-color-rose-700: hsl(345.3 82.7% 40.8%); 211 | --sl-color-rose-800: hsl(343.4 79.7% 34.7%); 212 | --sl-color-rose-900: hsl(341.5 75.5% 30.4%); 213 | --sl-color-rose-950: hsl(341.3 70.1% 17.1%); 214 | --sl-color-primary-50: var(--sl-color-sky-50); 215 | --sl-color-primary-100: var(--sl-color-sky-100); 216 | --sl-color-primary-200: var(--sl-color-sky-200); 217 | --sl-color-primary-300: var(--sl-color-sky-300); 218 | --sl-color-primary-400: var(--sl-color-sky-400); 219 | --sl-color-primary-500: var(--sl-color-sky-500); 220 | --sl-color-primary-600: var(--sl-color-sky-600); 221 | --sl-color-primary-700: var(--sl-color-sky-700); 222 | --sl-color-primary-800: var(--sl-color-sky-800); 223 | --sl-color-primary-900: var(--sl-color-sky-900); 224 | --sl-color-primary-950: var(--sl-color-sky-950); 225 | --sl-color-success-50: var(--sl-color-green-50); 226 | --sl-color-success-100: var(--sl-color-green-100); 227 | --sl-color-success-200: var(--sl-color-green-200); 228 | --sl-color-success-300: var(--sl-color-green-300); 229 | --sl-color-success-400: var(--sl-color-green-400); 230 | --sl-color-success-500: var(--sl-color-green-500); 231 | --sl-color-success-600: var(--sl-color-green-600); 232 | --sl-color-success-700: var(--sl-color-green-700); 233 | --sl-color-success-800: var(--sl-color-green-800); 234 | --sl-color-success-900: var(--sl-color-green-900); 235 | --sl-color-success-950: var(--sl-color-green-950); 236 | --sl-color-warning-50: var(--sl-color-amber-50); 237 | --sl-color-warning-100: var(--sl-color-amber-100); 238 | --sl-color-warning-200: var(--sl-color-amber-200); 239 | --sl-color-warning-300: var(--sl-color-amber-300); 240 | --sl-color-warning-400: var(--sl-color-amber-400); 241 | --sl-color-warning-500: var(--sl-color-amber-500); 242 | --sl-color-warning-600: var(--sl-color-amber-600); 243 | --sl-color-warning-700: var(--sl-color-amber-700); 244 | --sl-color-warning-800: var(--sl-color-amber-800); 245 | --sl-color-warning-900: var(--sl-color-amber-900); 246 | --sl-color-warning-950: var(--sl-color-amber-950); 247 | --sl-color-danger-50: var(--sl-color-red-50); 248 | --sl-color-danger-100: var(--sl-color-red-100); 249 | --sl-color-danger-200: var(--sl-color-red-200); 250 | --sl-color-danger-300: var(--sl-color-red-300); 251 | --sl-color-danger-400: var(--sl-color-red-400); 252 | --sl-color-danger-500: var(--sl-color-red-500); 253 | --sl-color-danger-600: var(--sl-color-red-600); 254 | --sl-color-danger-700: var(--sl-color-red-700); 255 | --sl-color-danger-800: var(--sl-color-red-800); 256 | --sl-color-danger-900: var(--sl-color-red-900); 257 | --sl-color-danger-950: var(--sl-color-red-950); 258 | --sl-color-neutral-50: var(--sl-color-gray-50); 259 | --sl-color-neutral-100: var(--sl-color-gray-100); 260 | --sl-color-neutral-200: var(--sl-color-gray-200); 261 | --sl-color-neutral-300: var(--sl-color-gray-300); 262 | --sl-color-neutral-400: var(--sl-color-gray-400); 263 | --sl-color-neutral-500: var(--sl-color-gray-500); 264 | --sl-color-neutral-600: var(--sl-color-gray-600); 265 | --sl-color-neutral-700: var(--sl-color-gray-700); 266 | --sl-color-neutral-800: var(--sl-color-gray-800); 267 | --sl-color-neutral-900: var(--sl-color-gray-900); 268 | --sl-color-neutral-950: var(--sl-color-gray-950); 269 | --sl-color-neutral-0: hsl(0, 0%, 100%); 270 | --sl-color-neutral-1000: hsl(0, 0%, 0%); 271 | --sl-border-radius-small: 0.1875rem; 272 | --sl-border-radius-medium: 0.25rem; 273 | --sl-border-radius-large: 0.5rem; 274 | --sl-border-radius-x-large: 1rem; 275 | --sl-border-radius-circle: 50%; 276 | --sl-border-radius-pill: 9999px; 277 | --sl-shadow-x-small: 0 1px 2px hsl(240 3.8% 46.1% / 6%); 278 | --sl-shadow-small: 0 1px 2px hsl(240 3.8% 46.1% / 12%); 279 | --sl-shadow-medium: 0 2px 4px hsl(240 3.8% 46.1% / 12%); 280 | --sl-shadow-large: 0 2px 8px hsl(240 3.8% 46.1% / 12%); 281 | --sl-shadow-x-large: 0 4px 16px hsl(240 3.8% 46.1% / 12%); 282 | --sl-spacing-3x-small: 0.125rem; 283 | --sl-spacing-2x-small: 0.25rem; 284 | --sl-spacing-x-small: 0.5rem; 285 | --sl-spacing-small: 0.75rem; 286 | --sl-spacing-medium: 1rem; 287 | --sl-spacing-large: 1.25rem; 288 | --sl-spacing-x-large: 1.75rem; 289 | --sl-spacing-2x-large: 2.25rem; 290 | --sl-spacing-3x-large: 3rem; 291 | --sl-spacing-4x-large: 4.5rem; 292 | --sl-transition-x-slow: 1000ms; 293 | --sl-transition-slow: 500ms; 294 | --sl-transition-medium: 250ms; 295 | --sl-transition-fast: 150ms; 296 | --sl-transition-x-fast: 50ms; 297 | --sl-font-mono: SFMono-Regular, Consolas, "Liberation Mono", Menlo, 298 | monospace; 299 | --sl-font-sans: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, 300 | Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", 301 | "Segoe UI Symbol"; 302 | --sl-font-serif: Georgia, "Times New Roman", serif; 303 | --sl-font-size-2x-small: 0.625rem; 304 | --sl-font-size-x-small: 0.75rem; 305 | --sl-font-size-small: 0.875rem; 306 | --sl-font-size-medium: 1rem; 307 | --sl-font-size-large: 1.25rem; 308 | --sl-font-size-x-large: 1.5rem; 309 | --sl-font-size-2x-large: 2.25rem; 310 | --sl-font-size-3x-large: 3rem; 311 | --sl-font-size-4x-large: 4.5rem; 312 | --sl-font-weight-light: 300; 313 | --sl-font-weight-normal: 400; 314 | --sl-font-weight-semibold: 500; 315 | --sl-font-weight-bold: 700; 316 | --sl-letter-spacing-denser: -0.03em; 317 | --sl-letter-spacing-dense: -0.015em; 318 | --sl-letter-spacing-normal: normal; 319 | --sl-letter-spacing-loose: 0.075em; 320 | --sl-letter-spacing-looser: 0.15em; 321 | --sl-line-height-denser: 1; 322 | --sl-line-height-dense: 1.4; 323 | --sl-line-height-normal: 1.8; 324 | --sl-line-height-loose: 2.2; 325 | --sl-line-height-looser: 2.6; 326 | --sl-focus-ring-color: var(--sl-color-primary-600); 327 | --sl-focus-ring-style: solid; 328 | --sl-focus-ring-width: 3px; 329 | --sl-focus-ring: var(--sl-focus-ring-style) var(--sl-focus-ring-width) 330 | var(--sl-focus-ring-color); 331 | --sl-focus-ring-offset: 1px; 332 | --sl-button-font-size-small: var(--sl-font-size-x-small); 333 | --sl-button-font-size-medium: var(--sl-font-size-small); 334 | --sl-button-font-size-large: var(--sl-font-size-medium); 335 | --sl-input-height-small: 1.875rem; 336 | --sl-input-height-medium: 2.5rem; 337 | --sl-input-height-large: 3.125rem; 338 | --sl-input-background-color: var(--sl-color-neutral-0); 339 | --sl-input-background-color-hover: var(--sl-input-background-color); 340 | --sl-input-background-color-focus: var(--sl-input-background-color); 341 | --sl-input-background-color-disabled: var(--sl-color-neutral-100); 342 | --sl-input-border-color: var(--sl-color-neutral-300); 343 | --sl-input-border-color-hover: var(--sl-color-neutral-400); 344 | --sl-input-border-color-focus: var(--sl-color-primary-500); 345 | --sl-input-border-color-disabled: var(--sl-color-neutral-300); 346 | --sl-input-border-width: 1px; 347 | --sl-input-required-content: "*"; 348 | --sl-input-required-content-offset: -2px; 349 | --sl-input-required-content-color: var(--sl-input-label-color); 350 | --sl-input-border-radius-small: var(--sl-border-radius-medium); 351 | --sl-input-border-radius-medium: var(--sl-border-radius-medium); 352 | --sl-input-border-radius-large: var(--sl-border-radius-medium); 353 | --sl-input-font-family: var(--sl-font-sans); 354 | --sl-input-font-weight: var(--sl-font-weight-normal); 355 | --sl-input-font-size-small: var(--sl-font-size-small); 356 | --sl-input-font-size-medium: var(--sl-font-size-medium); 357 | --sl-input-font-size-large: var(--sl-font-size-large); 358 | --sl-input-letter-spacing: var(--sl-letter-spacing-normal); 359 | --sl-input-color: var(--sl-color-neutral-700); 360 | --sl-input-color-hover: var(--sl-color-neutral-700); 361 | --sl-input-color-focus: var(--sl-color-neutral-700); 362 | --sl-input-color-disabled: var(--sl-color-neutral-900); 363 | --sl-input-icon-color: var(--sl-color-neutral-500); 364 | --sl-input-icon-color-hover: var(--sl-color-neutral-600); 365 | --sl-input-icon-color-focus: var(--sl-color-neutral-600); 366 | --sl-input-placeholder-color: var(--sl-color-neutral-500); 367 | --sl-input-placeholder-color-disabled: var(--sl-color-neutral-600); 368 | --sl-input-spacing-small: var(--sl-spacing-small); 369 | --sl-input-spacing-medium: var(--sl-spacing-medium); 370 | --sl-input-spacing-large: var(--sl-spacing-large); 371 | --sl-input-focus-ring-color: hsl(198.6 88.7% 48.4% / 40%); 372 | --sl-input-focus-ring-offset: 0; 373 | --sl-input-filled-background-color: var(--sl-color-neutral-100); 374 | --sl-input-filled-background-color-hover: var(--sl-color-neutral-100); 375 | --sl-input-filled-background-color-focus: var(--sl-color-neutral-100); 376 | --sl-input-filled-background-color-disabled: var(--sl-color-neutral-100); 377 | --sl-input-filled-color: var(--sl-color-neutral-800); 378 | --sl-input-filled-color-hover: var(--sl-color-neutral-800); 379 | --sl-input-filled-color-focus: var(--sl-color-neutral-700); 380 | --sl-input-filled-color-disabled: var(--sl-color-neutral-800); 381 | --sl-input-label-font-size-small: var(--sl-font-size-small); 382 | --sl-input-label-font-size-medium: var(--sl-font-size-medium); 383 | --sl-input-label-font-size-large: var(--sl-font-size-large); 384 | --sl-input-label-color: inherit; 385 | --sl-input-help-text-font-size-small: var(--sl-font-size-x-small); 386 | --sl-input-help-text-font-size-medium: var(--sl-font-size-small); 387 | --sl-input-help-text-font-size-large: var(--sl-font-size-medium); 388 | --sl-input-help-text-color: var(--sl-color-neutral-500); 389 | --sl-toggle-size-small: 0.875rem; 390 | --sl-toggle-size-medium: 1.125rem; 391 | --sl-toggle-size-large: 1.375rem; 392 | --sl-overlay-background-color: hsl(240 3.8% 46.1% / 33%); 393 | --sl-panel-background-color: var(--sl-color-neutral-0); 394 | --sl-panel-border-color: var(--sl-color-neutral-200); 395 | --sl-panel-border-width: 1px; 396 | --sl-tooltip-border-radius: var(--sl-border-radius-medium); 397 | --sl-tooltip-background-color: var(--sl-color-neutral-800); 398 | --sl-tooltip-color: var(--sl-color-neutral-0); 399 | --sl-tooltip-font-family: var(--sl-font-sans); 400 | --sl-tooltip-font-weight: var(--sl-font-weight-normal); 401 | --sl-tooltip-font-size: var(--sl-font-size-small); 402 | --sl-tooltip-line-height: var(--sl-line-height-dense); 403 | --sl-tooltip-padding: var(--sl-spacing-2x-small) var(--sl-spacing-x-small); 404 | --sl-tooltip-arrow-size: 6px; 405 | --sl-z-index-drawer: 700; 406 | --sl-z-index-dialog: 800; 407 | --sl-z-index-dropdown: 900; 408 | --sl-z-index-toast: 950; 409 | --sl-z-index-tooltip: 1000; 410 | } 411 | @supports (scrollbar-gutter: stable) { 412 | .sl-scroll-lock { 413 | scrollbar-gutter: stable !important; 414 | overflow: hidden !important; 415 | } 416 | } 417 | @supports not (scrollbar-gutter: stable) { 418 | .sl-scroll-lock body { 419 | padding-right: var(--sl-scroll-lock-size) !important; 420 | overflow: hidden !important; 421 | } 422 | } 423 | .sl-toast-stack { 424 | position: fixed; 425 | top: 0; 426 | inset-inline-end: 0; 427 | z-index: var(--sl-z-index-toast); 428 | width: 28rem; 429 | max-width: 100%; 430 | max-height: 100%; 431 | overflow: auto; 432 | } 433 | .sl-toast-stack sl-alert { 434 | margin: var(--sl-spacing-medium); 435 | } 436 | .sl-toast-stack sl-alert::part(base) { 437 | box-shadow: var(--sl-shadow-large); 438 | } 439 | /*# sourceMappingURL=data:application/json;base64,ewogICJ2ZXJzaW9uIjogMywKICAic291cmNlcyI6IFsibm9kZV9tb2R1bGVzLy5wbnBtL0BzaG9lbGFjZS1zdHlsZStzaG9lbGFjZUAyLjE1LjBfQHR5cGVzK3JlYWN0QDE4LjIuNzQvbm9kZV9tb2R1bGVzL0BzaG9lbGFjZS1zdHlsZS9zaG9lbGFjZS9kaXN0L3RoZW1lcy9saWdodC5jc3MiXSwKICAic291cmNlc0NvbnRlbnQiOiBbIjpyb290LFxuOmhvc3QsXG4uc2wtdGhlbWUtbGlnaHQge1xuICBjb2xvci1zY2hlbWU6IGxpZ2h0O1xuXG4gIC0tc2wtY29sb3ItZ3JheS01MDogaHNsKDAgMCUgOTcuNSUpO1xuICAtLXNsLWNvbG9yLWdyYXktMTAwOiBoc2woMjQwIDQuOCUgOTUuOSUpO1xuICAtLXNsLWNvbG9yLWdyYXktMjAwOiBoc2woMjQwIDUuOSUgOTAlKTtcbiAgLS1zbC1jb2xvci1ncmF5LTMwMDogaHNsKDI0MCA0LjklIDgzLjklKTtcbiAgLS1zbC1jb2xvci1ncmF5LTQwMDogaHNsKDI0MCA1JSA2NC45JSk7XG4gIC0tc2wtY29sb3ItZ3JheS01MDA6IGhzbCgyNDAgMy44JSA0Ni4xJSk7XG4gIC0tc2wtY29sb3ItZ3JheS02MDA6IGhzbCgyNDAgNS4yJSAzMy45JSk7XG4gIC0tc2wtY29sb3ItZ3JheS03MDA6IGhzbCgyNDAgNS4zJSAyNi4xJSk7XG4gIC0tc2wtY29sb3ItZ3JheS04MDA6IGhzbCgyNDAgMy43JSAxNS45JSk7XG4gIC0tc2wtY29sb3ItZ3JheS05MDA6IGhzbCgyNDAgNS45JSAxMCUpO1xuICAtLXNsLWNvbG9yLWdyYXktOTUwOiBoc2woMjQwIDcuMyUgOCUpO1xuXG4gIC0tc2wtY29sb3ItcmVkLTUwOiBoc2woMCA4NS43JSA5Ny4zJSk7XG4gIC0tc2wtY29sb3ItcmVkLTEwMDogaHNsKDAgOTMuMyUgOTQuMSUpO1xuICAtLXNsLWNvbG9yLXJlZC0yMDA6IGhzbCgwIDk2LjMlIDg5LjQlKTtcbiAgLS1zbC1jb2xvci1yZWQtMzAwOiBoc2woMCA5My41JSA4MS44JSk7XG4gIC0tc2wtY29sb3ItcmVkLTQwMDogaHNsKDAgOTAuNiUgNzAuOCUpO1xuICAtLXNsLWNvbG9yLXJlZC01MDA6IGhzbCgwIDg0LjIlIDYwLjIlKTtcbiAgLS1zbC1jb2xvci1yZWQtNjAwOiBoc2woMCA3Mi4yJSA1MC42JSk7XG4gIC0tc2wtY29sb3ItcmVkLTcwMDogaHNsKDAgNzMuNyUgNDEuOCUpO1xuICAtLXNsLWNvbG9yLXJlZC04MDA6IGhzbCgwIDcwJSAzNS4zJSk7XG4gIC0tc2wtY29sb3ItcmVkLTkwMDogaHNsKDAgNjIuOCUgMzAuNiUpO1xuICAtLXNsLWNvbG9yLXJlZC05NTA6IGhzbCgwIDYwJSAxOS42JSk7XG5cbiAgLS1zbC1jb2xvci1vcmFuZ2UtNTA6IGhzbCgzMy4zIDEwMCUgOTYuNSUpO1xuICAtLXNsLWNvbG9yLW9yYW5nZS0xMDA6IGhzbCgzNC4zIDEwMCUgOTEuOCUpO1xuICAtLXNsLWNvbG9yLW9yYW5nZS0yMDA6IGhzbCgzMi4xIDk3LjclIDgzLjElKTtcbiAgLS1zbC1jb2xvci1vcmFuZ2UtMzAwOiBoc2woMzAuNyA5Ny4yJSA3Mi40JSk7XG4gIC0tc2wtY29sb3Itb3JhbmdlLTQwMDogaHNsKDI3IDk2JSA2MSUpO1xuICAtLXNsLWNvbG9yLW9yYW5nZS01MDA6IGhzbCgyNC42IDk1JSA1My4xJSk7XG4gIC0tc2wtY29sb3Itb3JhbmdlLTYwMDogaHNsKDIwLjUgOTAuMiUgNDguMiUpO1xuICAtLXNsLWNvbG9yLW9yYW5nZS03MDA6IGhzbCgxNy41IDg4LjMlIDQwLjQlKTtcbiAgLS1zbC1jb2xvci1vcmFuZ2UtODAwOiBoc2woMTUgNzkuMSUgMzMuNyUpO1xuICAtLXNsLWNvbG9yLW9yYW5nZS05MDA6IGhzbCgxNS4zIDc0LjYlIDI3LjglKTtcbiAgLS1zbC1jb2xvci1vcmFuZ2UtOTUwOiBoc2woMTUuMiA2OS4xJSAxOSUpO1xuXG4gIC0tc2wtY29sb3ItYW1iZXItNTA6IGhzbCg0OCAxMDAlIDk2LjElKTtcbiAgLS1zbC1jb2xvci1hbWJlci0xMDA6IGhzbCg0OCA5Ni41JSA4OC44JSk7XG4gIC0tc2wtY29sb3ItYW1iZXItMjAwOiBoc2woNDggOTYuNiUgNzYuNyUpO1xuICAtLXNsLWNvbG9yLWFtYmVyLTMwMDogaHNsKDQ1LjkgOTYuNyUgNjQuNSUpO1xuICAtLXNsLWNvbG9yLWFtYmVyLTQwMDogaHNsKDQzLjMgOTYuNCUgNTYuMyUpO1xuICAtLXNsLWNvbG9yLWFtYmVyLTUwMDogaHNsKDM3LjcgOTIuMSUgNTAuMiUpO1xuICAtLXNsLWNvbG9yLWFtYmVyLTYwMDogaHNsKDMyLjEgOTQuNiUgNDMuNyUpO1xuICAtLXNsLWNvbG9yLWFtYmVyLTcwMDogaHNsKDI2IDkwLjUlIDM3LjElKTtcbiAgLS1zbC1jb2xvci1hbWJlci04MDA6IGhzbCgyMi43IDgyLjUlIDMxLjQlKTtcbiAgLS1zbC1jb2xvci1hbWJlci05MDA6IGhzbCgyMS43IDc3LjglIDI2LjUlKTtcbiAgLS1zbC1jb2xvci1hbWJlci05NTA6IGhzbCgyMi45IDc0LjElIDE2LjclKTtcblxuICAtLXNsLWNvbG9yLXllbGxvdy01MDogaHNsKDU0LjUgOTEuNyUgOTUuMyUpO1xuICAtLXNsLWNvbG9yLXllbGxvdy0xMDA6IGhzbCg1NC45IDk2LjclIDg4JSk7XG4gIC0tc2wtY29sb3IteWVsbG93LTIwMDogaHNsKDUyLjggOTguMyUgNzYuOSUpO1xuICAtLXNsLWNvbG9yLXllbGxvdy0zMDA6IGhzbCg1MC40IDk3LjglIDYzLjUlKTtcbiAgLS1zbC1jb2xvci15ZWxsb3ctNDAwOiBoc2woNDcuOSA5NS44JSA1My4xJSk7XG4gIC0tc2wtY29sb3IteWVsbG93LTUwMDogaHNsKDQ1LjQgOTMuNCUgNDcuNSUpO1xuICAtLXNsLWNvbG9yLXllbGxvdy02MDA6IGhzbCg0MC42IDk2LjElIDQwLjQlKTtcbiAgLS1zbC1jb2xvci15ZWxsb3ctNzAwOiBoc2woMzUuNSA5MS43JSAzMi45JSk7XG4gIC0tc2wtY29sb3IteWVsbG93LTgwMDogaHNsKDMxLjggODElIDI4LjglKTtcbiAgLS1zbC1jb2xvci15ZWxsb3ctOTAwOiBoc2woMjguNCA3Mi41JSAyNS43JSk7XG4gIC0tc2wtY29sb3IteWVsbG93LTk1MDogaHNsKDMzLjEgNjklIDEzLjklKTtcblxuICAtLXNsLWNvbG9yLWxpbWUtNTA6IGhzbCg3OC4zIDkyJSA5NS4xJSk7XG4gIC0tc2wtY29sb3ItbGltZS0xMDA6IGhzbCg3OS42IDg5LjElIDg5LjIlKTtcbiAgLS1zbC1jb2xvci1saW1lLTIwMDogaHNsKDgwLjkgODguNSUgNzkuNiUpO1xuICAtLXNsLWNvbG9yLWxpbWUtMzAwOiBoc2woODIgODQuNSUgNjcuMSUpO1xuICAtLXNsLWNvbG9yLWxpbWUtNDAwOiBoc2woODIuNyA3OCUgNTUuNSUpO1xuICAtLXNsLWNvbG9yLWxpbWUtNTAwOiBoc2woODMuNyA4MC41JSA0NC4zJSk7XG4gIC0tc2wtY29sb3ItbGltZS02MDA6IGhzbCg4NC44IDg1LjIlIDM0LjUlKTtcbiAgLS1zbC1jb2xvci1saW1lLTcwMDogaHNsKDg1LjkgNzguNCUgMjcuMyUpO1xuICAtLXNsLWNvbG9yLWxpbWUtODAwOiBoc2woODYuMyA2OSUgMjIuNyUpO1xuICAtLXNsLWNvbG9yLWxpbWUtOTAwOiBoc2woODcuNiA2MS4yJSAyMC4yJSk7XG4gIC0tc2wtY29sb3ItbGltZS05NTA6IGhzbCg4Ni41IDYwLjYlIDEzLjklKTtcblxuICAtLXNsLWNvbG9yLWdyZWVuLTUwOiBoc2woMTM4LjUgNzYuNSUgOTYuNyUpO1xuICAtLXNsLWNvbG9yLWdyZWVuLTEwMDogaHNsKDE0MC42IDg0LjIlIDkyLjUlKTtcbiAgLS1zbC1jb2xvci1ncmVlbi0yMDA6IGhzbCgxNDEgNzguOSUgODUuMSUpO1xuICAtLXNsLWNvbG9yLWdyZWVuLTMwMDogaHNsKDE0MS43IDc2LjYlIDczLjElKTtcbiAgLS1zbC1jb2xvci1ncmVlbi00MDA6IGhzbCgxNDEuOSA2OS4yJSA1OCUpO1xuICAtLXNsLWNvbG9yLWdyZWVuLTUwMDogaHNsKDE0Mi4xIDcwLjYlIDQ1LjMlKTtcbiAgLS1zbC1jb2xvci1ncmVlbi02MDA6IGhzbCgxNDIuMSA3Ni4yJSAzNi4zJSk7XG4gIC0tc2wtY29sb3ItZ3JlZW4tNzAwOiBoc2woMTQyLjQgNzEuOCUgMjkuMiUpO1xuICAtLXNsLWNvbG9yLWdyZWVuLTgwMDogaHNsKDE0Mi44IDY0LjIlIDI0LjElKTtcbiAgLS1zbC1jb2xvci1ncmVlbi05MDA6IGhzbCgxNDMuOCA2MS4yJSAyMC4yJSk7XG4gIC0tc2wtY29sb3ItZ3JlZW4tOTUwOiBoc2woMTQ0LjMgNjAuNyUgMTIlKTtcblxuICAtLXNsLWNvbG9yLWVtZXJhbGQtNTA6IGhzbCgxNTEuOCA4MSUgOTUuOSUpO1xuICAtLXNsLWNvbG9yLWVtZXJhbGQtMTAwOiBoc2woMTQ5LjMgODAuNCUgOTAlKTtcbiAgLS1zbC1jb2xvci1lbWVyYWxkLTIwMDogaHNsKDE1Mi40IDc2JSA4MC40JSk7XG4gIC0tc2wtY29sb3ItZW1lcmFsZC0zMDA6IGhzbCgxNTYuMiA3MS42JSA2Ni45JSk7XG4gIC0tc2wtY29sb3ItZW1lcmFsZC00MDA6IGhzbCgxNTguMSA2NC40JSA1MS42JSk7XG4gIC0tc2wtY29sb3ItZW1lcmFsZC01MDA6IGhzbCgxNjAuMSA4NC4xJSAzOS40JSk7XG4gIC0tc2wtY29sb3ItZW1lcmFsZC02MDA6IGhzbCgxNjEuNCA5My41JSAzMC40JSk7XG4gIC0tc2wtY29sb3ItZW1lcmFsZC03MDA6IGhzbCgxNjIuOSA5My41JSAyNC4zJSk7XG4gIC0tc2wtY29sb3ItZW1lcmFsZC04MDA6IGhzbCgxNjMuMSA4OC4xJSAxOS44JSk7XG4gIC0tc2wtY29sb3ItZW1lcmFsZC05MDA6IGhzbCgxNjQuMiA4NS43JSAxNi41JSk7XG4gIC0tc2wtY29sb3ItZW1lcmFsZC05NTA6IGhzbCgxNjQuMyA4Ny41JSA5LjQlKTtcblxuICAtLXNsLWNvbG9yLXRlYWwtNTA6IGhzbCgxNjYuMiA3Ni41JSA5Ni43JSk7XG4gIC0tc2wtY29sb3ItdGVhbC0xMDA6IGhzbCgxNjcuMiA4NS41JSA4OS4yJSk7XG4gIC0tc2wtY29sb3ItdGVhbC0yMDA6IGhzbCgxNjguNCA4My44JSA3OC4yJSk7XG4gIC0tc2wtY29sb3ItdGVhbC0zMDA6IGhzbCgxNzAuNiA3Ni45JSA2NC4zJSk7XG4gIC0tc2wtY29sb3ItdGVhbC00MDA6IGhzbCgxNzIuNSA2NiUgNTAuNCUpO1xuICAtLXNsLWNvbG9yLXRlYWwtNTAwOiBoc2woMTczLjQgODAuNCUgNDAlKTtcbiAgLS1zbC1jb2xvci10ZWFsLTYwMDogaHNsKDE3NC43IDgzLjklIDMxLjYlKTtcbiAgLS1zbC1jb2xvci10ZWFsLTcwMDogaHNsKDE3NS4zIDc3LjQlIDI2LjElKTtcbiAgLS1zbC1jb2xvci10ZWFsLTgwMDogaHNsKDE3Ni4xIDY5LjQlIDIxLjglKTtcbiAgLS1zbC1jb2xvci10ZWFsLTkwMDogaHNsKDE3NS45IDYwLjglIDE5JSk7XG4gIC0tc2wtY29sb3ItdGVhbC05NTA6IGhzbCgxNzYuNSA1OC42JSAxMS40JSk7XG5cbiAgLS1zbC1jb2xvci1jeWFuLTUwOiBoc2woMTgzLjIgMTAwJSA5Ni4zJSk7XG4gIC0tc2wtY29sb3ItY3lhbi0xMDA6IGhzbCgxODUuMSA5NS45JSA5MC40JSk7XG4gIC0tc2wtY29sb3ItY3lhbi0yMDA6IGhzbCgxODYuMiA5My41JSA4MS44JSk7XG4gIC0tc2wtY29sb3ItY3lhbi0zMDA6IGhzbCgxODcgOTIuNCUgNjklKTtcbiAgLS1zbC1jb2xvci1jeWFuLTQwMDogaHNsKDE4Ny45IDg1LjclIDUzLjMlKTtcbiAgLS1zbC1jb2xvci1jeWFuLTUwMDogaHNsKDE4OC43IDk0LjUlIDQyLjclKTtcbiAgLS1zbC1jb2xvci1jeWFuLTYwMDogaHNsKDE5MS42IDkxLjQlIDM2LjUlKTtcbiAgLS1zbC1jb2xvci1jeWFuLTcwMDogaHNsKDE5Mi45IDgyLjMlIDMxJSk7XG4gIC0tc2wtY29sb3ItY3lhbi04MDA6IGhzbCgxOTQuNCA2OS42JSAyNy4xJSk7XG4gIC0tc2wtY29sb3ItY3lhbi05MDA6IGhzbCgxOTYuNCA2My42JSAyMy43JSk7XG4gIC0tc2wtY29sb3ItY3lhbi05NTA6IGhzbCgxOTYuOCA2MSUgMTYuMSUpO1xuXG4gIC0tc2wtY29sb3Itc2t5LTUwOiBoc2woMjA0IDEwMCUgOTcuMSUpO1xuICAtLXNsLWNvbG9yLXNreS0xMDA6IGhzbCgyMDQgOTMuOCUgOTMuNyUpO1xuICAtLXNsLWNvbG9yLXNreS0yMDA6IGhzbCgyMDAuNiA5NC40JSA4Ni4xJSk7XG4gIC0tc2wtY29sb3Itc2t5LTMwMDogaHNsKDE5OS40IDk1LjUlIDczLjklKTtcbiAgLS1zbC1jb2xvci1za3ktNDAwOiBoc2woMTk4LjQgOTMuMiUgNTkuNiUpO1xuICAtLXNsLWNvbG9yLXNreS01MDA6IGhzbCgxOTguNiA4OC43JSA0OC40JSk7XG4gIC0tc2wtY29sb3Itc2t5LTYwMDogaHNsKDIwMC40IDk4JSAzOS40JSk7XG4gIC0tc2wtY29sb3Itc2t5LTcwMDogaHNsKDIwMS4zIDk2LjMlIDMyLjIlKTtcbiAgLS1zbC1jb2xvci1za3ktODAwOiBoc2woMjAxIDkwJSAyNy41JSk7XG4gIC0tc2wtY29sb3Itc2t5LTkwMDogaHNsKDIwMiA4MC4zJSAyMy45JSk7XG4gIC0tc2wtY29sb3Itc2t5LTk1MDogaHNsKDIwMi4zIDczLjglIDE2LjUlKTtcblxuICAtLXNsLWNvbG9yLWJsdWUtNTA6IGhzbCgyMTMuOCAxMDAlIDk2LjklKTtcbiAgLS1zbC1jb2xvci1ibHVlLTEwMDogaHNsKDIxNC4zIDk0LjYlIDkyLjclKTtcbiAgLS1zbC1jb2xvci1ibHVlLTIwMDogaHNsKDIxMy4zIDk2LjklIDg3LjMlKTtcbiAgLS1zbC1jb2xvci1ibHVlLTMwMDogaHNsKDIxMS43IDk2LjQlIDc4LjQlKTtcbiAgLS1zbC1jb2xvci1ibHVlLTQwMDogaHNsKDIxMy4xIDkzLjklIDY3LjglKTtcbiAgLS1zbC1jb2xvci1ibHVlLTUwMDogaHNsKDIxNy4yIDkxLjIlIDU5LjglKTtcbiAgLS1zbC1jb2xvci1ibHVlLTYwMDogaHNsKDIyMS4yIDgzLjIlIDUzLjMlKTtcbiAgLS1zbC1jb2xvci1ibHVlLTcwMDogaHNsKDIyNC4zIDc2LjMlIDQ4JSk7XG4gIC0tc2wtY29sb3ItYmx1ZS04MDA6IGhzbCgyMjUuOSA3MC43JSA0MC4yJSk7XG4gIC0tc2wtY29sb3ItYmx1ZS05MDA6IGhzbCgyMjQuNCA2NC4zJSAzMi45JSk7XG4gIC0tc2wtY29sb3ItYmx1ZS05NTA6IGhzbCgyMjYuMiA1NS4zJSAxOC40JSk7XG5cbiAgLS1zbC1jb2xvci1pbmRpZ28tNTA6IGhzbCgyMjUuOSAxMDAlIDk2LjclKTtcbiAgLS1zbC1jb2xvci1pbmRpZ28tMTAwOiBoc2woMjI2LjUgMTAwJSA5My45JSk7XG4gIC0tc2wtY29sb3ItaW5kaWdvLTIwMDogaHNsKDIyOCA5Ni41JSA4OC44JSk7XG4gIC0tc2wtY29sb3ItaW5kaWdvLTMwMDogaHNsKDIyOS43IDkzLjUlIDgxLjglKTtcbiAgLS1zbC1jb2xvci1pbmRpZ28tNDAwOiBoc2woMjM0LjUgODkuNSUgNzMuOSUpO1xuICAtLXNsLWNvbG9yLWluZGlnby01MDA6IGhzbCgyMzguNyA4My41JSA2Ni43JSk7XG4gIC0tc2wtY29sb3ItaW5kaWdvLTYwMDogaHNsKDI0My40IDc1LjQlIDU4LjYlKTtcbiAgLS1zbC1jb2xvci1pbmRpZ28tNzAwOiBoc2woMjQ0LjUgNTcuOSUgNTAuNiUpO1xuICAtLXNsLWNvbG9yLWluZGlnby04MDA6IGhzbCgyNDMuNyA1NC41JSA0MS40JSk7XG4gIC0tc2wtY29sb3ItaW5kaWdvLTkwMDogaHNsKDI0Mi4yIDQ3LjQlIDM0LjMlKTtcbiAgLS1zbC1jb2xvci1pbmRpZ28tOTUwOiBoc2woMjQzLjUgNDMuNiUgMjIuOSUpO1xuXG4gIC0tc2wtY29sb3ItdmlvbGV0LTUwOiBoc2woMjUwIDEwMCUgOTcuNiUpO1xuICAtLXNsLWNvbG9yLXZpb2xldC0xMDA6IGhzbCgyNTEuNCA5MS4zJSA5NS41JSk7XG4gIC0tc2wtY29sb3ItdmlvbGV0LTIwMDogaHNsKDI1MC41IDk1LjIlIDkxLjglKTtcbiAgLS1zbC1jb2xvci12aW9sZXQtMzAwOiBoc2woMjUyLjUgOTQuNyUgODUuMSUpO1xuICAtLXNsLWNvbG9yLXZpb2xldC00MDA6IGhzbCgyNTUuMSA5MS43JSA3Ni4zJSk7XG4gIC0tc2wtY29sb3ItdmlvbGV0LTUwMDogaHNsKDI1OC4zIDg5LjUlIDY2LjMlKTtcbiAgLS1zbC1jb2xvci12aW9sZXQtNjAwOiBoc2woMjYyLjEgODMuMyUgNTcuOCUpO1xuICAtLXNsLWNvbG9yLXZpb2xldC03MDA6IGhzbCgyNjMuNCA3MCUgNTAuNCUpO1xuICAtLXNsLWNvbG9yLXZpb2xldC04MDA6IGhzbCgyNjMuNCA2OS4zJSA0Mi4yJSk7XG4gIC0tc2wtY29sb3ItdmlvbGV0LTkwMDogaHNsKDI2My41IDY3LjQlIDM0LjklKTtcbiAgLS1zbC1jb2xvci12aW9sZXQtOTUwOiBoc2woMjY1LjEgNjEuNSUgMjEuNCUpO1xuXG4gIC0tc2wtY29sb3ItcHVycGxlLTUwOiBoc2woMjcwIDEwMCUgOTglKTtcbiAgLS1zbC1jb2xvci1wdXJwbGUtMTAwOiBoc2woMjY4LjcgMTAwJSA5NS41JSk7XG4gIC0tc2wtY29sb3ItcHVycGxlLTIwMDogaHNsKDI2OC42IDEwMCUgOTEuOCUpO1xuICAtLXNsLWNvbG9yLXB1cnBsZS0zMDA6IGhzbCgyNjkuMiA5Ny40JSA4NS4xJSk7XG4gIC0tc2wtY29sb3ItcHVycGxlLTQwMDogaHNsKDI3MCA5NS4yJSA3NS4zJSk7XG4gIC0tc2wtY29sb3ItcHVycGxlLTUwMDogaHNsKDI3MC43IDkxJSA2NS4xJSk7XG4gIC0tc2wtY29sb3ItcHVycGxlLTYwMDogaHNsKDI3MS41IDgxLjMlIDU1LjklKTtcbiAgLS1zbC1jb2xvci1wdXJwbGUtNzAwOiBoc2woMjcyLjEgNzEuNyUgNDcuMSUpO1xuICAtLXNsLWNvbG9yLXB1cnBsZS04MDA6IGhzbCgyNzIuOSA2Ny4yJSAzOS40JSk7XG4gIC0tc2wtY29sb3ItcHVycGxlLTkwMDogaHNsKDI3My42IDY1LjYlIDMyJSk7XG4gIC0tc2wtY29sb3ItcHVycGxlLTk1MDogaHNsKDI3NiA1OS41JSAxNi41JSk7XG5cbiAgLS1zbC1jb2xvci1mdWNoc2lhLTUwOiBoc2woMjg5LjEgMTAwJSA5Ny44JSk7XG4gIC0tc2wtY29sb3ItZnVjaHNpYS0xMDA6IGhzbCgyODcgMTAwJSA5NS41JSk7XG4gIC0tc2wtY29sb3ItZnVjaHNpYS0yMDA6IGhzbCgyODguMyA5NS44JSA5MC42JSk7XG4gIC0tc2wtY29sb3ItZnVjaHNpYS0zMDA6IGhzbCgyOTEuMSA5My4xJSA4Mi45JSk7XG4gIC0tc2wtY29sb3ItZnVjaHNpYS00MDA6IGhzbCgyOTIgOTEuNCUgNzIuNSUpO1xuICAtLXNsLWNvbG9yLWZ1Y2hzaWEtNTAwOiBoc2woMjkyLjIgODQuMSUgNjAuNiUpO1xuICAtLXNsLWNvbG9yLWZ1Y2hzaWEtNjAwOiBoc2woMjkzLjQgNjkuNSUgNDguOCUpO1xuICAtLXNsLWNvbG9yLWZ1Y2hzaWEtNzAwOiBoc2woMjk0LjcgNzIuNCUgMzkuOCUpO1xuICAtLXNsLWNvbG9yLWZ1Y2hzaWEtODAwOiBoc2woMjk1LjQgNzAuMiUgMzIuOSUpO1xuICAtLXNsLWNvbG9yLWZ1Y2hzaWEtOTAwOiBoc2woMjk2LjcgNjMuNiUgMjglKTtcbiAgLS1zbC1jb2xvci1mdWNoc2lhLTk1MDogaHNsKDI5Ny4xIDU2LjglIDE0LjUlKTtcblxuICAtLXNsLWNvbG9yLXBpbmstNTA6IGhzbCgzMjcuMyA3My4zJSA5Ny4xJSk7XG4gIC0tc2wtY29sb3ItcGluay0xMDA6IGhzbCgzMjUuNyA3Ny44JSA5NC43JSk7XG4gIC0tc2wtY29sb3ItcGluay0yMDA6IGhzbCgzMjUuOSA4NC42JSA4OS44JSk7XG4gIC0tc2wtY29sb3ItcGluay0zMDA6IGhzbCgzMjcuNCA4Ny4xJSA4MS44JSk7XG4gIC0tc2wtY29sb3ItcGluay00MDA6IGhzbCgzMjguNiA4NS41JSA3MC4yJSk7XG4gIC0tc2wtY29sb3ItcGluay01MDA6IGhzbCgzMzAuNCA4MS4yJSA2MC40JSk7XG4gIC0tc2wtY29sb3ItcGluay02MDA6IGhzbCgzMzMuMyA3MS40JSA1MC42JSk7XG4gIC0tc2wtY29sb3ItcGluay03MDA6IGhzbCgzMzUuMSA3Ny42JSA0MiUpO1xuICAtLXNsLWNvbG9yLXBpbmstODAwOiBoc2woMzM1LjggNzQuNCUgMzUuMyUpO1xuICAtLXNsLWNvbG9yLXBpbmstOTAwOiBoc2woMzM1LjkgNjklIDMwLjQlKTtcbiAgLS1zbC1jb2xvci1waW5rLTk1MDogaHNsKDMzNi4yIDY1LjQlIDE1LjklKTtcblxuICAtLXNsLWNvbG9yLXJvc2UtNTA6IGhzbCgzNTUuNyAxMDAlIDk3LjMlKTtcbiAgLS1zbC1jb2xvci1yb3NlLTEwMDogaHNsKDM1NS42IDEwMCUgOTQuNyUpO1xuICAtLXNsLWNvbG9yLXJvc2UtMjAwOiBoc2woMzUyLjcgOTYuMSUgOTAlKTtcbiAgLS1zbC1jb2xvci1yb3NlLTMwMDogaHNsKDM1Mi42IDk1LjclIDgxLjglKTtcbiAgLS1zbC1jb2xvci1yb3NlLTQwMDogaHNsKDM1MS4zIDk0LjUlIDcxLjQlKTtcbiAgLS1zbC1jb2xvci1yb3NlLTUwMDogaHNsKDM0OS43IDg5LjIlIDYwLjIlKTtcbiAgLS1zbC1jb2xvci1yb3NlLTYwMDogaHNsKDM0Ni44IDc3LjIlIDQ5LjglKTtcbiAgLS1zbC1jb2xvci1yb3NlLTcwMDogaHNsKDM0NS4zIDgyLjclIDQwLjglKTtcbiAgLS1zbC1jb2xvci1yb3NlLTgwMDogaHNsKDM0My40IDc5LjclIDM0LjclKTtcbiAgLS1zbC1jb2xvci1yb3NlLTkwMDogaHNsKDM0MS41IDc1LjUlIDMwLjQlKTtcbiAgLS1zbC1jb2xvci1yb3NlLTk1MDogaHNsKDM0MS4zIDcwLjElIDE3LjElKTtcblxuICAtLXNsLWNvbG9yLXByaW1hcnktNTA6IHZhcigtLXNsLWNvbG9yLXNreS01MCk7XG4gIC0tc2wtY29sb3ItcHJpbWFyeS0xMDA6IHZhcigtLXNsLWNvbG9yLXNreS0xMDApO1xuICAtLXNsLWNvbG9yLXByaW1hcnktMjAwOiB2YXIoLS1zbC1jb2xvci1za3ktMjAwKTtcbiAgLS1zbC1jb2xvci1wcmltYXJ5LTMwMDogdmFyKC0tc2wtY29sb3Itc2t5LTMwMCk7XG4gIC0tc2wtY29sb3ItcHJpbWFyeS00MDA6IHZhcigtLXNsLWNvbG9yLXNreS00MDApO1xuICAtLXNsLWNvbG9yLXByaW1hcnktNTAwOiB2YXIoLS1zbC1jb2xvci1za3ktNTAwKTtcbiAgLS1zbC1jb2xvci1wcmltYXJ5LTYwMDogdmFyKC0tc2wtY29sb3Itc2t5LTYwMCk7XG4gIC0tc2wtY29sb3ItcHJpbWFyeS03MDA6IHZhcigtLXNsLWNvbG9yLXNreS03MDApO1xuICAtLXNsLWNvbG9yLXByaW1hcnktODAwOiB2YXIoLS1zbC1jb2xvci1za3ktODAwKTtcbiAgLS1zbC1jb2xvci1wcmltYXJ5LTkwMDogdmFyKC0tc2wtY29sb3Itc2t5LTkwMCk7XG4gIC0tc2wtY29sb3ItcHJpbWFyeS05NTA6IHZhcigtLXNsLWNvbG9yLXNreS05NTApO1xuXG4gIC0tc2wtY29sb3Itc3VjY2Vzcy01MDogdmFyKC0tc2wtY29sb3ItZ3JlZW4tNTApO1xuICAtLXNsLWNvbG9yLXN1Y2Nlc3MtMTAwOiB2YXIoLS1zbC1jb2xvci1ncmVlbi0xMDApO1xuICAtLXNsLWNvbG9yLXN1Y2Nlc3MtMjAwOiB2YXIoLS1zbC1jb2xvci1ncmVlbi0yMDApO1xuICAtLXNsLWNvbG9yLXN1Y2Nlc3MtMzAwOiB2YXIoLS1zbC1jb2xvci1ncmVlbi0zMDApO1xuICAtLXNsLWNvbG9yLXN1Y2Nlc3MtNDAwOiB2YXIoLS1zbC1jb2xvci1ncmVlbi00MDApO1xuICAtLXNsLWNvbG9yLXN1Y2Nlc3MtNTAwOiB2YXIoLS1zbC1jb2xvci1ncmVlbi01MDApO1xuICAtLXNsLWNvbG9yLXN1Y2Nlc3MtNjAwOiB2YXIoLS1zbC1jb2xvci1ncmVlbi02MDApO1xuICAtLXNsLWNvbG9yLXN1Y2Nlc3MtNzAwOiB2YXIoLS1zbC1jb2xvci1ncmVlbi03MDApO1xuICAtLXNsLWNvbG9yLXN1Y2Nlc3MtODAwOiB2YXIoLS1zbC1jb2xvci1ncmVlbi04MDApO1xuICAtLXNsLWNvbG9yLXN1Y2Nlc3MtOTAwOiB2YXIoLS1zbC1jb2xvci1ncmVlbi05MDApO1xuICAtLXNsLWNvbG9yLXN1Y2Nlc3MtOTUwOiB2YXIoLS1zbC1jb2xvci1ncmVlbi05NTApO1xuXG4gIC0tc2wtY29sb3Itd2FybmluZy01MDogdmFyKC0tc2wtY29sb3ItYW1iZXItNTApO1xuICAtLXNsLWNvbG9yLXdhcm5pbmctMTAwOiB2YXIoLS1zbC1jb2xvci1hbWJlci0xMDApO1xuICAtLXNsLWNvbG9yLXdhcm5pbmctMjAwOiB2YXIoLS1zbC1jb2xvci1hbWJlci0yMDApO1xuICAtLXNsLWNvbG9yLXdhcm5pbmctMzAwOiB2YXIoLS1zbC1jb2xvci1hbWJlci0zMDApO1xuICAtLXNsLWNvbG9yLXdhcm5pbmctNDAwOiB2YXIoLS1zbC1jb2xvci1hbWJlci00MDApO1xuICAtLXNsLWNvbG9yLXdhcm5pbmctNTAwOiB2YXIoLS1zbC1jb2xvci1hbWJlci01MDApO1xuICAtLXNsLWNvbG9yLXdhcm5pbmctNjAwOiB2YXIoLS1zbC1jb2xvci1hbWJlci02MDApO1xuICAtLXNsLWNvbG9yLXdhcm5pbmctNzAwOiB2YXIoLS1zbC1jb2xvci1hbWJlci03MDApO1xuICAtLXNsLWNvbG9yLXdhcm5pbmctODAwOiB2YXIoLS1zbC1jb2xvci1hbWJlci04MDApO1xuICAtLXNsLWNvbG9yLXdhcm5pbmctOTAwOiB2YXIoLS1zbC1jb2xvci1hbWJlci05MDApO1xuICAtLXNsLWNvbG9yLXdhcm5pbmctOTUwOiB2YXIoLS1zbC1jb2xvci1hbWJlci05NTApO1xuXG4gIC0tc2wtY29sb3ItZGFuZ2VyLTUwOiB2YXIoLS1zbC1jb2xvci1yZWQtNTApO1xuICAtLXNsLWNvbG9yLWRhbmdlci0xMDA6IHZhcigtLXNsLWNvbG9yLXJlZC0xMDApO1xuICAtLXNsLWNvbG9yLWRhbmdlci0yMDA6IHZhcigtLXNsLWNvbG9yLXJlZC0yMDApO1xuICAtLXNsLWNvbG9yLWRhbmdlci0zMDA6IHZhcigtLXNsLWNvbG9yLXJlZC0zMDApO1xuICAtLXNsLWNvbG9yLWRhbmdlci00MDA6IHZhcigtLXNsLWNvbG9yLXJlZC00MDApO1xuICAtLXNsLWNvbG9yLWRhbmdlci01MDA6IHZhcigtLXNsLWNvbG9yLXJlZC01MDApO1xuICAtLXNsLWNvbG9yLWRhbmdlci02MDA6IHZhcigtLXNsLWNvbG9yLXJlZC02MDApO1xuICAtLXNsLWNvbG9yLWRhbmdlci03MDA6IHZhcigtLXNsLWNvbG9yLXJlZC03MDApO1xuICAtLXNsLWNvbG9yLWRhbmdlci04MDA6IHZhcigtLXNsLWNvbG9yLXJlZC04MDApO1xuICAtLXNsLWNvbG9yLWRhbmdlci05MDA6IHZhcigtLXNsLWNvbG9yLXJlZC05MDApO1xuICAtLXNsLWNvbG9yLWRhbmdlci05NTA6IHZhcigtLXNsLWNvbG9yLXJlZC05NTApO1xuXG4gIC0tc2wtY29sb3ItbmV1dHJhbC01MDogdmFyKC0tc2wtY29sb3ItZ3JheS01MCk7XG4gIC0tc2wtY29sb3ItbmV1dHJhbC0xMDA6IHZhcigtLXNsLWNvbG9yLWdyYXktMTAwKTtcbiAgLS1zbC1jb2xvci1uZXV0cmFsLTIwMDogdmFyKC0tc2wtY29sb3ItZ3JheS0yMDApO1xuICAtLXNsLWNvbG9yLW5ldXRyYWwtMzAwOiB2YXIoLS1zbC1jb2xvci1ncmF5LTMwMCk7XG4gIC0tc2wtY29sb3ItbmV1dHJhbC00MDA6IHZhcigtLXNsLWNvbG9yLWdyYXktNDAwKTtcbiAgLS1zbC1jb2xvci1uZXV0cmFsLTUwMDogdmFyKC0tc2wtY29sb3ItZ3JheS01MDApO1xuICAtLXNsLWNvbG9yLW5ldXRyYWwtNjAwOiB2YXIoLS1zbC1jb2xvci1ncmF5LTYwMCk7XG4gIC0tc2wtY29sb3ItbmV1dHJhbC03MDA6IHZhcigtLXNsLWNvbG9yLWdyYXktNzAwKTtcbiAgLS1zbC1jb2xvci1uZXV0cmFsLTgwMDogdmFyKC0tc2wtY29sb3ItZ3JheS04MDApO1xuICAtLXNsLWNvbG9yLW5ldXRyYWwtOTAwOiB2YXIoLS1zbC1jb2xvci1ncmF5LTkwMCk7XG4gIC0tc2wtY29sb3ItbmV1dHJhbC05NTA6IHZhcigtLXNsLWNvbG9yLWdyYXktOTUwKTtcblxuICAtLXNsLWNvbG9yLW5ldXRyYWwtMDogaHNsKDAsIDAlLCAxMDAlKTtcbiAgLS1zbC1jb2xvci1uZXV0cmFsLTEwMDA6IGhzbCgwLCAwJSwgMCUpO1xuXG4gIC0tc2wtYm9yZGVyLXJhZGl1cy1zbWFsbDogMC4xODc1cmVtO1xuICAtLXNsLWJvcmRlci1yYWRpdXMtbWVkaXVtOiAwLjI1cmVtO1xuICAtLXNsLWJvcmRlci1yYWRpdXMtbGFyZ2U6IDAuNXJlbTtcbiAgLS1zbC1ib3JkZXItcmFkaXVzLXgtbGFyZ2U6IDFyZW07XG5cbiAgLS1zbC1ib3JkZXItcmFkaXVzLWNpcmNsZTogNTAlO1xuICAtLXNsLWJvcmRlci1yYWRpdXMtcGlsbDogOTk5OXB4O1xuXG4gIC0tc2wtc2hhZG93LXgtc21hbGw6IDAgMXB4IDJweCBoc2woMjQwIDMuOCUgNDYuMSUgLyA2JSk7XG4gIC0tc2wtc2hhZG93LXNtYWxsOiAwIDFweCAycHggaHNsKDI0MCAzLjglIDQ2LjElIC8gMTIlKTtcbiAgLS1zbC1zaGFkb3ctbWVkaXVtOiAwIDJweCA0cHggaHNsKDI0MCAzLjglIDQ2LjElIC8gMTIlKTtcbiAgLS1zbC1zaGFkb3ctbGFyZ2U6IDAgMnB4IDhweCBoc2woMjQwIDMuOCUgNDYuMSUgLyAxMiUpO1xuICAtLXNsLXNoYWRvdy14LWxhcmdlOiAwIDRweCAxNnB4IGhzbCgyNDAgMy44JSA0Ni4xJSAvIDEyJSk7XG5cbiAgLS1zbC1zcGFjaW5nLTN4LXNtYWxsOiAwLjEyNXJlbTtcbiAgLS1zbC1zcGFjaW5nLTJ4LXNtYWxsOiAwLjI1cmVtO1xuICAtLXNsLXNwYWNpbmcteC1zbWFsbDogMC41cmVtO1xuICAtLXNsLXNwYWNpbmctc21hbGw6IDAuNzVyZW07XG4gIC0tc2wtc3BhY2luZy1tZWRpdW06IDFyZW07XG4gIC0tc2wtc3BhY2luZy1sYXJnZTogMS4yNXJlbTtcbiAgLS1zbC1zcGFjaW5nLXgtbGFyZ2U6IDEuNzVyZW07XG4gIC0tc2wtc3BhY2luZy0yeC1sYXJnZTogMi4yNXJlbTtcbiAgLS1zbC1zcGFjaW5nLTN4LWxhcmdlOiAzcmVtO1xuICAtLXNsLXNwYWNpbmctNHgtbGFyZ2U6IDQuNXJlbTtcblxuICAtLXNsLXRyYW5zaXRpb24teC1zbG93OiAxMDAwbXM7XG4gIC0tc2wtdHJhbnNpdGlvbi1zbG93OiA1MDBtcztcbiAgLS1zbC10cmFuc2l0aW9uLW1lZGl1bTogMjUwbXM7XG4gIC0tc2wtdHJhbnNpdGlvbi1mYXN0OiAxNTBtcztcbiAgLS1zbC10cmFuc2l0aW9uLXgtZmFzdDogNTBtcztcblxuICAtLXNsLWZvbnQtbW9ubzogU0ZNb25vLVJlZ3VsYXIsIENvbnNvbGFzLCBcIkxpYmVyYXRpb24gTW9ub1wiLCBNZW5sbywgbW9ub3NwYWNlO1xuICAtLXNsLWZvbnQtc2FuczogLWFwcGxlLXN5c3RlbSwgQmxpbmtNYWNTeXN0ZW1Gb250LCBcIlNlZ29lIFVJXCIsIFJvYm90byxcbiAgICBIZWx2ZXRpY2EsIEFyaWFsLCBzYW5zLXNlcmlmLCBcIkFwcGxlIENvbG9yIEVtb2ppXCIsIFwiU2Vnb2UgVUkgRW1vamlcIixcbiAgICBcIlNlZ29lIFVJIFN5bWJvbFwiO1xuICAtLXNsLWZvbnQtc2VyaWY6IEdlb3JnaWEsIFwiVGltZXMgTmV3IFJvbWFuXCIsIHNlcmlmO1xuXG4gIC0tc2wtZm9udC1zaXplLTJ4LXNtYWxsOiAwLjYyNXJlbTtcbiAgLS1zbC1mb250LXNpemUteC1zbWFsbDogMC43NXJlbTtcbiAgLS1zbC1mb250LXNpemUtc21hbGw6IDAuODc1cmVtO1xuICAtLXNsLWZvbnQtc2l6ZS1tZWRpdW06IDFyZW07XG4gIC0tc2wtZm9udC1zaXplLWxhcmdlOiAxLjI1cmVtO1xuICAtLXNsLWZvbnQtc2l6ZS14LWxhcmdlOiAxLjVyZW07XG4gIC0tc2wtZm9udC1zaXplLTJ4LWxhcmdlOiAyLjI1cmVtO1xuICAtLXNsLWZvbnQtc2l6ZS0zeC1sYXJnZTogM3JlbTtcbiAgLS1zbC1mb250LXNpemUtNHgtbGFyZ2U6IDQuNXJlbTtcblxuICAtLXNsLWZvbnQtd2VpZ2h0LWxpZ2h0OiAzMDA7XG4gIC0tc2wtZm9udC13ZWlnaHQtbm9ybWFsOiA0MDA7XG4gIC0tc2wtZm9udC13ZWlnaHQtc2VtaWJvbGQ6IDUwMDtcbiAgLS1zbC1mb250LXdlaWdodC1ib2xkOiA3MDA7XG5cbiAgLS1zbC1sZXR0ZXItc3BhY2luZy1kZW5zZXI6IC0wLjAzZW07XG4gIC0tc2wtbGV0dGVyLXNwYWNpbmctZGVuc2U6IC0wLjAxNWVtO1xuICAtLXNsLWxldHRlci1zcGFjaW5nLW5vcm1hbDogbm9ybWFsO1xuICAtLXNsLWxldHRlci1zcGFjaW5nLWxvb3NlOiAwLjA3NWVtO1xuICAtLXNsLWxldHRlci1zcGFjaW5nLWxvb3NlcjogMC4xNWVtO1xuXG4gIC0tc2wtbGluZS1oZWlnaHQtZGVuc2VyOiAxO1xuICAtLXNsLWxpbmUtaGVpZ2h0LWRlbnNlOiAxLjQ7XG4gIC0tc2wtbGluZS1oZWlnaHQtbm9ybWFsOiAxLjg7XG4gIC0tc2wtbGluZS1oZWlnaHQtbG9vc2U6IDIuMjtcbiAgLS1zbC1saW5lLWhlaWdodC1sb29zZXI6IDIuNjtcblxuICAtLXNsLWZvY3VzLXJpbmctY29sb3I6IHZhcigtLXNsLWNvbG9yLXByaW1hcnktNjAwKTtcbiAgLS1zbC1mb2N1cy1yaW5nLXN0eWxlOiBzb2xpZDtcbiAgLS1zbC1mb2N1cy1yaW5nLXdpZHRoOiAzcHg7XG4gIC0tc2wtZm9jdXMtcmluZzogdmFyKC0tc2wtZm9jdXMtcmluZy1zdHlsZSkgdmFyKC0tc2wtZm9jdXMtcmluZy13aWR0aClcbiAgICB2YXIoLS1zbC1mb2N1cy1yaW5nLWNvbG9yKTtcbiAgLS1zbC1mb2N1cy1yaW5nLW9mZnNldDogMXB4O1xuXG4gIC0tc2wtYnV0dG9uLWZvbnQtc2l6ZS1zbWFsbDogdmFyKC0tc2wtZm9udC1zaXplLXgtc21hbGwpO1xuICAtLXNsLWJ1dHRvbi1mb250LXNpemUtbWVkaXVtOiB2YXIoLS1zbC1mb250LXNpemUtc21hbGwpO1xuICAtLXNsLWJ1dHRvbi1mb250LXNpemUtbGFyZ2U6IHZhcigtLXNsLWZvbnQtc2l6ZS1tZWRpdW0pO1xuXG4gIC0tc2wtaW5wdXQtaGVpZ2h0LXNtYWxsOiAxLjg3NXJlbTtcbiAgLS1zbC1pbnB1dC1oZWlnaHQtbWVkaXVtOiAyLjVyZW07XG4gIC0tc2wtaW5wdXQtaGVpZ2h0LWxhcmdlOiAzLjEyNXJlbTtcblxuICAtLXNsLWlucHV0LWJhY2tncm91bmQtY29sb3I6IHZhcigtLXNsLWNvbG9yLW5ldXRyYWwtMCk7XG4gIC0tc2wtaW5wdXQtYmFja2dyb3VuZC1jb2xvci1ob3ZlcjogdmFyKC0tc2wtaW5wdXQtYmFja2dyb3VuZC1jb2xvcik7XG4gIC0tc2wtaW5wdXQtYmFja2dyb3VuZC1jb2xvci1mb2N1czogdmFyKC0tc2wtaW5wdXQtYmFja2dyb3VuZC1jb2xvcik7XG4gIC0tc2wtaW5wdXQtYmFja2dyb3VuZC1jb2xvci1kaXNhYmxlZDogdmFyKC0tc2wtY29sb3ItbmV1dHJhbC0xMDApO1xuICAtLXNsLWlucHV0LWJvcmRlci1jb2xvcjogdmFyKC0tc2wtY29sb3ItbmV1dHJhbC0zMDApO1xuICAtLXNsLWlucHV0LWJvcmRlci1jb2xvci1ob3ZlcjogdmFyKC0tc2wtY29sb3ItbmV1dHJhbC00MDApO1xuICAtLXNsLWlucHV0LWJvcmRlci1jb2xvci1mb2N1czogdmFyKC0tc2wtY29sb3ItcHJpbWFyeS01MDApO1xuICAtLXNsLWlucHV0LWJvcmRlci1jb2xvci1kaXNhYmxlZDogdmFyKC0tc2wtY29sb3ItbmV1dHJhbC0zMDApO1xuICAtLXNsLWlucHV0LWJvcmRlci13aWR0aDogMXB4O1xuICAtLXNsLWlucHV0LXJlcXVpcmVkLWNvbnRlbnQ6IFwiKlwiO1xuICAtLXNsLWlucHV0LXJlcXVpcmVkLWNvbnRlbnQtb2Zmc2V0OiAtMnB4O1xuICAtLXNsLWlucHV0LXJlcXVpcmVkLWNvbnRlbnQtY29sb3I6IHZhcigtLXNsLWlucHV0LWxhYmVsLWNvbG9yKTtcblxuICAtLXNsLWlucHV0LWJvcmRlci1yYWRpdXMtc21hbGw6IHZhcigtLXNsLWJvcmRlci1yYWRpdXMtbWVkaXVtKTtcbiAgLS1zbC1pbnB1dC1ib3JkZXItcmFkaXVzLW1lZGl1bTogdmFyKC0tc2wtYm9yZGVyLXJhZGl1cy1tZWRpdW0pO1xuICAtLXNsLWlucHV0LWJvcmRlci1yYWRpdXMtbGFyZ2U6IHZhcigtLXNsLWJvcmRlci1yYWRpdXMtbWVkaXVtKTtcblxuICAtLXNsLWlucHV0LWZvbnQtZmFtaWx5OiB2YXIoLS1zbC1mb250LXNhbnMpO1xuICAtLXNsLWlucHV0LWZvbnQtd2VpZ2h0OiB2YXIoLS1zbC1mb250LXdlaWdodC1ub3JtYWwpO1xuICAtLXNsLWlucHV0LWZvbnQtc2l6ZS1zbWFsbDogdmFyKC0tc2wtZm9udC1zaXplLXNtYWxsKTtcbiAgLS1zbC1pbnB1dC1mb250LXNpemUtbWVkaXVtOiB2YXIoLS1zbC1mb250LXNpemUtbWVkaXVtKTtcbiAgLS1zbC1pbnB1dC1mb250LXNpemUtbGFyZ2U6IHZhcigtLXNsLWZvbnQtc2l6ZS1sYXJnZSk7XG4gIC0tc2wtaW5wdXQtbGV0dGVyLXNwYWNpbmc6IHZhcigtLXNsLWxldHRlci1zcGFjaW5nLW5vcm1hbCk7XG5cbiAgLS1zbC1pbnB1dC1jb2xvcjogdmFyKC0tc2wtY29sb3ItbmV1dHJhbC03MDApO1xuICAtLXNsLWlucHV0LWNvbG9yLWhvdmVyOiB2YXIoLS1zbC1jb2xvci1uZXV0cmFsLTcwMCk7XG4gIC0tc2wtaW5wdXQtY29sb3ItZm9jdXM6IHZhcigtLXNsLWNvbG9yLW5ldXRyYWwtNzAwKTtcbiAgLS1zbC1pbnB1dC1jb2xvci1kaXNhYmxlZDogdmFyKC0tc2wtY29sb3ItbmV1dHJhbC05MDApO1xuICAtLXNsLWlucHV0LWljb24tY29sb3I6IHZhcigtLXNsLWNvbG9yLW5ldXRyYWwtNTAwKTtcbiAgLS1zbC1pbnB1dC1pY29uLWNvbG9yLWhvdmVyOiB2YXIoLS1zbC1jb2xvci1uZXV0cmFsLTYwMCk7XG4gIC0tc2wtaW5wdXQtaWNvbi1jb2xvci1mb2N1czogdmFyKC0tc2wtY29sb3ItbmV1dHJhbC02MDApO1xuICAtLXNsLWlucHV0LXBsYWNlaG9sZGVyLWNvbG9yOiB2YXIoLS1zbC1jb2xvci1uZXV0cmFsLTUwMCk7XG4gIC0tc2wtaW5wdXQtcGxhY2Vob2xkZXItY29sb3ItZGlzYWJsZWQ6IHZhcigtLXNsLWNvbG9yLW5ldXRyYWwtNjAwKTtcbiAgLS1zbC1pbnB1dC1zcGFjaW5nLXNtYWxsOiB2YXIoLS1zbC1zcGFjaW5nLXNtYWxsKTtcbiAgLS1zbC1pbnB1dC1zcGFjaW5nLW1lZGl1bTogdmFyKC0tc2wtc3BhY2luZy1tZWRpdW0pO1xuICAtLXNsLWlucHV0LXNwYWNpbmctbGFyZ2U6IHZhcigtLXNsLXNwYWNpbmctbGFyZ2UpO1xuXG4gIC0tc2wtaW5wdXQtZm9jdXMtcmluZy1jb2xvcjogaHNsKDE5OC42IDg4LjclIDQ4LjQlIC8gNDAlKTtcbiAgLS1zbC1pbnB1dC1mb2N1cy1yaW5nLW9mZnNldDogMDtcblxuICAtLXNsLWlucHV0LWZpbGxlZC1iYWNrZ3JvdW5kLWNvbG9yOiB2YXIoLS1zbC1jb2xvci1uZXV0cmFsLTEwMCk7XG4gIC0tc2wtaW5wdXQtZmlsbGVkLWJhY2tncm91bmQtY29sb3ItaG92ZXI6IHZhcigtLXNsLWNvbG9yLW5ldXRyYWwtMTAwKTtcbiAgLS1zbC1pbnB1dC1maWxsZWQtYmFja2dyb3VuZC1jb2xvci1mb2N1czogdmFyKC0tc2wtY29sb3ItbmV1dHJhbC0xMDApO1xuICAtLXNsLWlucHV0LWZpbGxlZC1iYWNrZ3JvdW5kLWNvbG9yLWRpc2FibGVkOiB2YXIoLS1zbC1jb2xvci1uZXV0cmFsLTEwMCk7XG4gIC0tc2wtaW5wdXQtZmlsbGVkLWNvbG9yOiB2YXIoLS1zbC1jb2xvci1uZXV0cmFsLTgwMCk7XG4gIC0tc2wtaW5wdXQtZmlsbGVkLWNvbG9yLWhvdmVyOiB2YXIoLS1zbC1jb2xvci1uZXV0cmFsLTgwMCk7XG4gIC0tc2wtaW5wdXQtZmlsbGVkLWNvbG9yLWZvY3VzOiB2YXIoLS1zbC1jb2xvci1uZXV0cmFsLTcwMCk7XG4gIC0tc2wtaW5wdXQtZmlsbGVkLWNvbG9yLWRpc2FibGVkOiB2YXIoLS1zbC1jb2xvci1uZXV0cmFsLTgwMCk7XG5cbiAgLS1zbC1pbnB1dC1sYWJlbC1mb250LXNpemUtc21hbGw6IHZhcigtLXNsLWZvbnQtc2l6ZS1zbWFsbCk7XG4gIC0tc2wtaW5wdXQtbGFiZWwtZm9udC1zaXplLW1lZGl1bTogdmFyKC0tc2wtZm9udC1zaXplLW1lZGl1bSk7XG4gIC0tc2wtaW5wdXQtbGFiZWwtZm9udC1zaXplLWxhcmdlOiB2YXIoLS1zbC1mb250LXNpemUtbGFyZ2UpO1xuICAtLXNsLWlucHV0LWxhYmVsLWNvbG9yOiBpbmhlcml0O1xuXG4gIC0tc2wtaW5wdXQtaGVscC10ZXh0LWZvbnQtc2l6ZS1zbWFsbDogdmFyKC0tc2wtZm9udC1zaXplLXgtc21hbGwpO1xuICAtLXNsLWlucHV0LWhlbHAtdGV4dC1mb250LXNpemUtbWVkaXVtOiB2YXIoLS1zbC1mb250LXNpemUtc21hbGwpO1xuICAtLXNsLWlucHV0LWhlbHAtdGV4dC1mb250LXNpemUtbGFyZ2U6IHZhcigtLXNsLWZvbnQtc2l6ZS1tZWRpdW0pO1xuICAtLXNsLWlucHV0LWhlbHAtdGV4dC1jb2xvcjogdmFyKC0tc2wtY29sb3ItbmV1dHJhbC01MDApO1xuXG4gIC0tc2wtdG9nZ2xlLXNpemUtc21hbGw6IDAuODc1cmVtO1xuICAtLXNsLXRvZ2dsZS1zaXplLW1lZGl1bTogMS4xMjVyZW07XG4gIC0tc2wtdG9nZ2xlLXNpemUtbGFyZ2U6IDEuMzc1cmVtO1xuXG4gIC0tc2wtb3ZlcmxheS1iYWNrZ3JvdW5kLWNvbG9yOiBoc2woMjQwIDMuOCUgNDYuMSUgLyAzMyUpO1xuXG4gIC0tc2wtcGFuZWwtYmFja2dyb3VuZC1jb2xvcjogdmFyKC0tc2wtY29sb3ItbmV1dHJhbC0wKTtcbiAgLS1zbC1wYW5lbC1ib3JkZXItY29sb3I6IHZhcigtLXNsLWNvbG9yLW5ldXRyYWwtMjAwKTtcbiAgLS1zbC1wYW5lbC1ib3JkZXItd2lkdGg6IDFweDtcblxuICAtLXNsLXRvb2x0aXAtYm9yZGVyLXJhZGl1czogdmFyKC0tc2wtYm9yZGVyLXJhZGl1cy1tZWRpdW0pO1xuICAtLXNsLXRvb2x0aXAtYmFja2dyb3VuZC1jb2xvcjogdmFyKC0tc2wtY29sb3ItbmV1dHJhbC04MDApO1xuICAtLXNsLXRvb2x0aXAtY29sb3I6IHZhcigtLXNsLWNvbG9yLW5ldXRyYWwtMCk7XG4gIC0tc2wtdG9vbHRpcC1mb250LWZhbWlseTogdmFyKC0tc2wtZm9udC1zYW5zKTtcbiAgLS1zbC10b29sdGlwLWZvbnQtd2VpZ2h0OiB2YXIoLS1zbC1mb250LXdlaWdodC1ub3JtYWwpO1xuICAtLXNsLXRvb2x0aXAtZm9udC1zaXplOiB2YXIoLS1zbC1mb250LXNpemUtc21hbGwpO1xuICAtLXNsLXRvb2x0aXAtbGluZS1oZWlnaHQ6IHZhcigtLXNsLWxpbmUtaGVpZ2h0LWRlbnNlKTtcbiAgLS1zbC10b29sdGlwLXBhZGRpbmc6IHZhcigtLXNsLXNwYWNpbmctMngtc21hbGwpIHZhcigtLXNsLXNwYWNpbmcteC1zbWFsbCk7XG4gIC0tc2wtdG9vbHRpcC1hcnJvdy1zaXplOiA2cHg7XG5cbiAgLS1zbC16LWluZGV4LWRyYXdlcjogNzAwO1xuICAtLXNsLXotaW5kZXgtZGlhbG9nOiA4MDA7XG4gIC0tc2wtei1pbmRleC1kcm9wZG93bjogOTAwO1xuICAtLXNsLXotaW5kZXgtdG9hc3Q6IDk1MDtcbiAgLS1zbC16LWluZGV4LXRvb2x0aXA6IDEwMDA7XG59XG5cbkBzdXBwb3J0cyAoc2Nyb2xsYmFyLWd1dHRlcjogc3RhYmxlKSB7XG4gIC5zbC1zY3JvbGwtbG9jayB7XG4gICAgc2Nyb2xsYmFyLWd1dHRlcjogc3RhYmxlICFpbXBvcnRhbnQ7XG4gICAgb3ZlcmZsb3c6IGhpZGRlbiAhaW1wb3J0YW50O1xuICB9XG59XG5cbkBzdXBwb3J0cyBub3QgKHNjcm9sbGJhci1ndXR0ZXI6IHN0YWJsZSkge1xuICAuc2wtc2Nyb2xsLWxvY2sgYm9keSB7XG4gICAgcGFkZGluZy1yaWdodDogdmFyKC0tc2wtc2Nyb2xsLWxvY2stc2l6ZSkgIWltcG9ydGFudDtcbiAgICBvdmVyZmxvdzogaGlkZGVuICFpbXBvcnRhbnQ7XG4gIH1cbn1cblxuLnNsLXRvYXN0LXN0YWNrIHtcbiAgcG9zaXRpb246IGZpeGVkO1xuICB0b3A6IDA7XG4gIGluc2V0LWlubGluZS1lbmQ6IDA7XG4gIHotaW5kZXg6IHZhcigtLXNsLXotaW5kZXgtdG9hc3QpO1xuICB3aWR0aDogMjhyZW07XG4gIG1heC13aWR0aDogMTAwJTtcbiAgbWF4LWhlaWdodDogMTAwJTtcbiAgb3ZlcmZsb3c6IGF1dG87XG59XG5cbi5zbC10b2FzdC1zdGFjayBzbC1hbGVydCB7XG4gIG1hcmdpbjogdmFyKC0tc2wtc3BhY2luZy1tZWRpdW0pO1xufVxuXG4uc2wtdG9hc3Qtc3RhY2sgc2wtYWxlcnQ6OnBhcnQoYmFzZSkge1xuICBib3gtc2hhZG93OiB2YXIoLS1zbC1zaGFkb3ctbGFyZ2UpO1xufVxuIl0sCiAgIm1hcHBpbmdzIjogIjtBQUFBO0FBQUE7QUFBQTtBQUdFO0FBRUE7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUVBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFFQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBRUE7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUVBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFFQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBRUE7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUVBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFFQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBRUE7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUVBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFFQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBRUE7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUVBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFFQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBRUE7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUVBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFFQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBRUE7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUVBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFFQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBRUE7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUVBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFFQTtBQUNBO0FBRUE7QUFDQTtBQUNBO0FBQ0E7QUFFQTtBQUNBO0FBRUE7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUVBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBRUE7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUVBO0FBQUE7QUFBQTtBQUFBO0FBQUE7QUFBQTtBQUNBO0FBQUE7QUFBQTtBQUFBO0FBQUE7QUFBQTtBQUFBO0FBQUE7QUFBQTtBQUFBO0FBQUE7QUFHQTtBQUFBO0FBQUE7QUFBQTtBQUVBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUVBO0FBQ0E7QUFDQTtBQUNBO0FBRUE7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUVBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFFQTtBQUNBO0FBQ0E7QUFDQTtBQUVBO0FBRUE7QUFDQTtBQUNBO0FBRUE7QUFDQTtBQUNBO0FBRUE7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBRUE7QUFDQTtBQUNBO0FBRUE7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBRUE7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBRUE7QUFDQTtBQUVBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFFQTtBQUNBO0FBQ0E7QUFDQTtBQUVBO0FBQ0E7QUFDQTtBQUNBO0FBRUE7QUFDQTtBQUNBO0FBRUE7QUFFQTtBQUNBO0FBQ0E7QUFFQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFFQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQUE7QUFHRjtBQUNFO0FBQ0U7QUFDQTtBQUFBO0FBQUE7QUFJSjtBQUNFO0FBQ0U7QUFDQTtBQUFBO0FBQUE7QUFJSjtBQUNFO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFBQTtBQUdGO0FBQ0U7QUFBQTtBQUdGO0FBQ0U7QUFBQTsiLAogICJuYW1lcyI6IFtdCn0K */ 440 | 441 | .side-bar-nav { 442 | display: flex; 443 | flex-direction: column; 444 | height: 100%; 445 | } 446 | 447 | .side-bar-nav::part(tabs) { 448 | border-bottom-color: var(--color-base-20); 449 | position: absolute; 450 | top: 0; 451 | z-index: 10; 452 | background-color: var(--color-base-20); 453 | width: 100%; 454 | } 455 | 456 | .side-bar-nav::part(active-tab-indicator) { 457 | border-bottom-color: var(--nav-item-weight-active); 458 | } 459 | 460 | sl-tab::part(base) { 461 | --sl-color-primary-600: var(--nav-item-weight-active); 462 | } 463 | sl-tab-group::part(base) { 464 | padding-top: 50px; 465 | height: 100%; 466 | width: 100%; 467 | } 468 | 469 | sl-tab-group::part(body) { 470 | height: inherit; 471 | padding: 5px; 472 | width: 100%; 473 | overflow-y: hidden; 474 | } 475 | 476 | sl-tab-panel::part(base) { 477 | height: 100%; 478 | padding: 0; 479 | } 480 | 481 | sl-tab-panel { 482 | height: 100%; 483 | } 484 | 485 | .workspace-leaf-content .view-content { 486 | padding: 0; 487 | } 488 | 489 | .description-body { 490 | width: 100%; 491 | height: 100%; 492 | padding: 10px; 493 | } 494 | 495 | .description-body::part(base) { 496 | display: flex; 497 | flex-direction: column; 498 | gap: 5px; 499 | } 500 | 501 | .description-header-div { 502 | width: 100%; 503 | height: fit-content; 504 | display: flex; 505 | flex-direction: row; 506 | justify-content: start; 507 | align-items: center; 508 | flex: 0 1 40px; 509 | } 510 | 511 | .description-input { 512 | max-width: 100%; 513 | min-width: 100%; 514 | flex: 1 0 0%; 515 | max-height: 100%; 516 | resize: none; 517 | } 518 | 519 | .save_state { 520 | flex: 0 1 40px; 521 | font-size: 1rem; 522 | color: var(--color-base-50); 523 | } 524 | 525 | /*Nodes tab*/ 526 | .nodes-body { 527 | border-radius: 5px; 528 | padding: 10px; 529 | } 530 | 531 | .nodes-container { 532 | overflow-y: scroll; 533 | height: 100%; 534 | } 535 | 536 | .nodes-panel { 537 | height: 100%; 538 | display: flex; 539 | flex-direction: column; 540 | gap: 10px; 541 | } 542 | 543 | .node-element { 544 | padding: 15px; 545 | border: dashed 1px var(--color-base-50); 546 | margin-bottom: 20px; 547 | border-radius: 5px; 548 | cursor: pointer; 549 | white-space: nowrap; 550 | overflow: hidden; 551 | text-overflow: ellipsis; 552 | } 553 | 554 | .node-element:hover { 555 | background-color: var(--color-base-30); 556 | } 557 | .node-active { 558 | border-color: var(--nav-item-weight-active); 559 | background-color: var(--color-base-30); 560 | } 561 | 562 | .title-edit-div { 563 | width: 100%; 564 | display: flex; 565 | justify-content: space-between; 566 | } 567 | .title-input-div { 568 | width: 100%; 569 | height: 100%; 570 | display: flex; 571 | justify-content: space-between; 572 | } 573 | 574 | .title-input { 575 | width: 100%; 576 | height: 100%; 577 | padding: 5px; 578 | border-radius: 5px; 579 | border: 0; 580 | flex: 1; 581 | } 582 | 583 | .title { 584 | padding: 5px 0; 585 | font-weight: bold; 586 | white-space: nowrap; 587 | overflow: hidden; 588 | text-overflow: ellipsis; 589 | flex: 1 0 0; 590 | } 591 | 592 | .edit-icon { 593 | width: fit-content; 594 | height: 100%; 595 | padding: 2px; 596 | display: flex; 597 | align-items: center; 598 | justify-content: center; 599 | cursor: pointer; 600 | --icon-size: var(--icon-size-m); 601 | } 602 | 603 | .edit-icon:hover { 604 | color: var(--color-accent); 605 | } 606 | 607 | .hidden { 608 | display: none; 609 | } 610 | 611 | .search-area { 612 | width: 100%; 613 | max-width: 100%; 614 | height: fit-content; 615 | display: flex; 616 | gap: 5px; 617 | flex-direction: column; 618 | padding: 5px; 619 | container-type: inline-size; 620 | } 621 | .searchBar-input { 622 | height: 30px; 623 | border-radius: 5px; 624 | min-width: 150px; 625 | padding: 5px; 626 | flex: 1; 627 | border: 2px; 628 | } 629 | 630 | .sort-button-container { 631 | height: fit-content; 632 | } 633 | 634 | .sort-button { 635 | display: flex; 636 | gap: 5px; 637 | justify-content: center; 638 | align-items: center; 639 | border-radius: 5px; 640 | cursor: pointer; 641 | flex-shrink: 0; 642 | border: 2px; 643 | } 644 | 645 | .search-bar-row { 646 | height: 30px; 647 | width: 100%; 648 | display: flex; 649 | gap: 5px; 650 | } 651 | 652 | .sort-button span { 653 | flex: 1; 654 | display: none; 655 | } 656 | .sort-button div { 657 | display: flex; 658 | justify-content: center; 659 | align-items: center; 660 | } 661 | 662 | .sort-menu { 663 | border: 2px solid var(--color-base-30); 664 | border-radius: 5px; 665 | display: flex; 666 | flex-direction: column; 667 | gap: 7px; 668 | background-color: var(--color-base-20); 669 | } 670 | 671 | .sort-menu sl-divider { 672 | width: 100%; 673 | height: 2px; 674 | background-color: var(--color-base-30); 675 | } 676 | 677 | .sort-item { 678 | display: flex; 679 | align-items: center; 680 | gap: 5px; 681 | padding: 5px 10px; 682 | cursor: pointer; 683 | } 684 | 685 | .sort-item:hover { 686 | background-color: var(--color-base-30); 687 | } 688 | 689 | .sort-check { 690 | display: flex; 691 | justify-content: center; 692 | align-items: center; 693 | color: transparent; 694 | } 695 | 696 | .check-active { 697 | color: var(--color-accent); 698 | } 699 | 700 | .filters-container { 701 | display: flex; 702 | height: 35px; 703 | width: 100%; 704 | gap: 5px; 705 | } 706 | 707 | .filters-button { 708 | cursor: pointer; 709 | display: flex; 710 | gap: 5px; 711 | justify-content: center; 712 | align-items: center; 713 | } 714 | 715 | .filters-button span { 716 | display: none; 717 | } 718 | 719 | .filters-button div { 720 | display: flex; 721 | } 722 | 723 | .filters_display { 724 | flex: 1; 725 | display: flex; 726 | gap: 7px; 727 | overflow-x: scroll; 728 | overflow-y: hidden; 729 | } 730 | 731 | .filter-menu { 732 | border: 2px solid var(--color-base-30); 733 | border-radius: 5px; 734 | display: flex; 735 | width: 190px; 736 | height: 200px; 737 | flex-direction: column; 738 | gap: 10px; 739 | background-color: var(--color-base-20); 740 | overflow-y: scroll; 741 | padding: 10px; 742 | } 743 | 744 | .filter-menu-section { 745 | display: flex; 746 | gap: 5px; 747 | flex-direction: column; 748 | width: 100%; 749 | height: fit-content; 750 | } 751 | 752 | .filter-menu-section span { 753 | font-weight: var(--sl-font-weight-bold); 754 | font-size: var(--sl-font-size-small); 755 | } 756 | 757 | .filter-menu-badge-container { 758 | display: flex; 759 | gap: 7px; 760 | flex-wrap: wrap; 761 | } 762 | 763 | .filter-menu-badge { 764 | font-size: var(--sl-font-size-small); 765 | border: 1px solid var(--color-base-50); 766 | padding: 0px 10px; 767 | border-radius: 10px; 768 | width: fit-content; 769 | height: fit-content; 770 | cursor: pointer; 771 | } 772 | 773 | .filter-menu-badge-display { 774 | display: none; 775 | } 776 | 777 | .badge-active { 778 | background-color: var(--color-accent); 779 | color: var(--color-base-20); 780 | } 781 | 782 | .badge-display-active { 783 | display: flex; 784 | } 785 | 786 | .filter-menu-badge span { 787 | font-size: var(--sl-font-size-small); 788 | font-weight: var(--sl-font-weight-normal); 789 | } 790 | 791 | .filter-menu-badge:hover { 792 | border: 1px solid var(--color-accent); 793 | } 794 | 795 | .filter-menu-badge-display { 796 | font-size: var(--sl-font-size-small); 797 | border: 1px solid var(--color-base-50); 798 | padding: 0px 10px; 799 | border-radius: 10px; 800 | width: fit-content; 801 | height: fit-content; 802 | } 803 | 804 | @container (min-width: 250px) { 805 | .filter-menu { 806 | width: 250px; 807 | } 808 | } 809 | 810 | @container (min-width: 300px) { 811 | .sort-button { 812 | width: 120px; 813 | justify-content: space-between; 814 | } 815 | 816 | .sort-button-container { 817 | width: 120px; 818 | } 819 | 820 | .sort-button span { 821 | display: flex; 822 | } 823 | 824 | .filters-button span { 825 | display: flex; 826 | } 827 | 828 | .filter-menu { 829 | width: 300px; 830 | } 831 | } 832 | 833 | @container (min-width: 400px) { 834 | .filter-menu { 835 | width: 400px; 836 | } 837 | } 838 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "baseUrl": ".", 4 | "inlineSourceMap": true, 5 | "inlineSources": true, 6 | "module": "ESNext", 7 | "target": "ES6", 8 | "allowJs": true, 9 | "noImplicitAny": true, 10 | "moduleResolution": "node", 11 | "importHelpers": true, 12 | "isolatedModules": true, 13 | "strictNullChecks": true, 14 | "lib": [ 15 | "DOM", 16 | "ES5", 17 | "ES6", 18 | "ES7" 19 | ] 20 | }, 21 | "include": [ 22 | "**/*.ts" 23 | ] 24 | } 25 | -------------------------------------------------------------------------------- /version-bump.mjs: -------------------------------------------------------------------------------- 1 | import { readFileSync, writeFileSync } from "fs"; 2 | 3 | const targetVersion = process.argv[2]; 4 | 5 | if (!targetVersion) { 6 | console.log("Missing targetVersion"); 7 | process.exit(-1); 8 | } 9 | 10 | // read minAppVersion from manifest.json and bump version to target version 11 | let manifest = JSON.parse(readFileSync("manifest.json", "utf8")); 12 | const { minAppVersion } = manifest; 13 | manifest.version = targetVersion; 14 | writeFileSync("manifest.json", JSON.stringify(manifest, null, "\t")); 15 | 16 | // update versions.json with target version and minAppVersion from manifest.json 17 | let versions = JSON.parse(readFileSync("versions.json", "utf8")); 18 | versions[targetVersion] = minAppVersion; 19 | writeFileSync("versions.json", JSON.stringify(versions, null, "\t")); 20 | -------------------------------------------------------------------------------- /versions.json: -------------------------------------------------------------------------------- 1 | { 2 | "1.0.1": "0.15.0", 3 | "1.0.2": "0.15.0", 4 | "1.1.1": "0.15.0", 5 | "1.1.2": "0.15.0", 6 | "1.2.0": "0.15.0", 7 | "1.3.0": "0.15.0", 8 | "1.3.1": "0.15.0" 9 | } --------------------------------------------------------------------------------