├── hacs.json ├── .github ├── workflows │ └── validate.yaml └── ISSUE_TEMPLATE │ ├── feature_request.md │ └── bug_report.md ├── LICENSE ├── README.md └── todoist-card.js /hacs.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Todoist Card", 3 | "content_in_root": true, 4 | "filename": "todoist-card.js", 5 | "render_readme": true 6 | } -------------------------------------------------------------------------------- /.github/workflows/validate.yaml: -------------------------------------------------------------------------------- 1 | name: Validate 2 | 3 | on: 4 | push: 5 | pull_request: 6 | schedule: 7 | - cron: "0 0 * * *" 8 | 9 | jobs: 10 | validate: 11 | runs-on: "ubuntu-latest" 12 | steps: 13 | - uses: "actions/checkout@v2" 14 | - name: HACS validation 15 | uses: "hacs/action@main" 16 | with: 17 | category: "plugin" 18 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: "[FEATURE REQUEST]" 5 | labels: enhancement 6 | assignees: grinstantin 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help me improve this project 4 | title: "[BUG]" 5 | labels: bug 6 | assignees: grinstantin 7 | 8 | --- 9 | 10 | **Before submitting a bug** 11 | - [ ] I updated to the latest card version available 12 | - [ ] I cleared the cache of my browser 13 | 14 | **Describe the bug** 15 | A clear and concise description of what the bug is. 16 | 17 | **To Reproduce** 18 | Steps to reproduce the behavior: 19 | 1. Go to '...' 20 | 2. Click on '....' 21 | 3. Scroll down to '....' 22 | 4. See error 23 | 24 | **Expected behavior** 25 | A clear and concise description of what you expected to happen. 26 | 27 | **Screenshots** 28 | If applicable, add screenshots to help explain your problem. 29 | 30 | **Versions:** 31 | - Card: [e.g. 1.0.5] 32 | - HomeAssistant: [e.g. 2021.3.4] 33 | - Browser: [e.g. Google Chrome 89.0.4389.114] 34 | 35 | **Additional context** 36 | Add any other context about the problem here. 37 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Konstantin Grinkevich 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 | # Todoist Card 2 | 3 | [![hacs_badge](https://img.shields.io/badge/HACS-Default-orange.svg)](https://github.com/hacs/integration) 4 | ![hacs_badge](https://img.shields.io/github/v/release/grinstantin/todoist-card) 5 | ![hacs_badge](https://img.shields.io/github/license/grinstantin/todoist-card) 6 | 7 | Todoist card for [Home Assistant](https://www.home-assistant.io) Lovelace UI. This card displays items from selected Todoist project. 8 | 9 | ![Preview of todoist-card](https://user-images.githubusercontent.com/34913257/108243361-a8ea8500-7156-11eb-8313-a149a7cf38b8.png) 10 | 11 | ## Installing 12 | 13 | ### HACS 14 | 15 | This card is available in [HACS](https://hacs.xyz) (Home Assistant Community Store). 16 | 17 | Just search for `Todoist Card` in HACS `Frontend` tab. 18 | 19 | ### Manual 20 | 21 | 1. Download `todoist-card.js` file from the [latest release](https://github.com/grinstantin/todoist-card/releases/latest). 22 | 2. Put `todoist-card.js` file into your `config/www` folder. 23 | 3. Add a reference to `todoist-card.js` in Lovelace. There's two way to do that: 24 | 1. **Using UI:** _Configuration_ → _Lovelace Dashboards_ → _Resources_ → Click Plus button → Set _Url_ as `/local/todoist-card.js` → Set _Resource type_ as `JavaScript Module`. 25 | 2. **Using YAML:** Add the following code to `lovelace` section. 26 | ```yaml 27 | resources: 28 | - url: /local/todoist-card.js 29 | type: module 30 | ``` 31 | 4. Add `custom:todoist-card` to Lovelace UI as any other card (using either editor or YAML configuration). 32 | 33 | ## Using the card 34 | 35 | This card can be configured using Lovelace UI editor. 36 | 37 | 1. Add the following code to `configuration.yaml`: 38 | ```yaml 39 | sensor: 40 | - platform: rest 41 | name: To-do List 42 | method: GET 43 | resource: 'https://api.todoist.com/sync/v9/projects/get_data' 44 | params: 45 | project_id: TODOIST_PROJECT_ID 46 | headers: 47 | Authorization: !secret todoist_api_token 48 | value_template: '{{ value_json[''project''][''id''] }}' 49 | json_attributes: 50 | - project 51 | - items 52 | scan_interval: 30 53 | 54 | rest_command: 55 | todoist: 56 | method: post 57 | url: 'https://api.todoist.com/sync/v9/{{ url }}' 58 | payload: '{{ payload }}' 59 | headers: 60 | Authorization: !secret todoist_api_token 61 | content_type: 'application/x-www-form-urlencoded' 62 | ``` 63 | 2. ... and to `secrets.yaml`: 64 | ```yaml 65 | todoist_api_token: 'Bearer TODOIST_API_TOKEN' 66 | ``` 67 | 3. Replace `TODOIST_API_TOKEN` with your [token](https://app.todoist.com/app/settings/integrations/developer) 68 | 69 | > Important note! Replace only the `TODOIST_API_TOKEN` and keep the 'Bearer ' part unchanged. 70 | 71 | and `TODOIST_PROJECT_ID` with ID of your selected Todoist project. 72 | 73 | > `TODOIST_PROJECT_ID` contains only numbers. You can get it from project URL, which usually looks like this: 74 | `https://todoist.com/app/project/TODOIST_PROJECT_ID` 75 | 4. Reload configs or restart Home Assistant. 76 | 5. In Lovelace UI, click 3 dots in top left corner. 77 | 6. Click _Edit Dashboard_. 78 | 7. Click _Add Card_ button in the bottom right corner to add a new card. 79 | 8. Find _Custom: Todoist Card_ in the list. 80 | 9. Choose `entity`. 81 | 10. Now you should see the preview of the card! 82 | 83 | Typical example of using this card in YAML config would look like this: 84 | 85 | ```yaml 86 | type: 'custom:todoist-card' 87 | entity: sensor.to_do_list 88 | show_header: true 89 | show_completed: 5 90 | show_item_add: true 91 | use_quick_add: false 92 | show_item_close: true 93 | show_item_delete: true 94 | only_today_overdue: false 95 | ``` 96 | 97 | Here is what every option means: 98 | 99 | | Name | Type | Default | Description | 100 | | -------------------- | :-------: | :----------: | -------------------------------------------------------------------------------------------------------------------------------- | 101 | | `type` | `string` | **required** | `custom:todoist-card` | 102 | | `entity` | `string` | **required** | An entity_id within the `sensor` domain. | 103 | | `show_completed` | `integer` | `5` | Number of completed tasks shown at the end of the list (0 to disable). | 104 | | `show_header` | `boolean` | `true` | Show friendly name of the selected `sensor` in the card header. | 105 | | `show_item_add` | `boolean` | `true` | Show text input element for adding new items to the list. | 106 | | `use_quick_add` | `boolean` | `false` | Use the [Quick Add](https://todoist.com/help/articles/task-quick-add) implementation, available in the official Todoist clients. | 107 | | `show_item_close` | `boolean` | `true` | Show `close/complete` and `uncomplete` buttons. | 108 | | `show_item_delete` | `boolean` | `true` | Show `delete` buttons. | 109 | | `only_today_overdue` | `boolean` | `false` | Only show tasks that are overdue or due today. | 110 | 111 | > Note that the completed tasks list is cleared when the page is refreshed. 112 | 113 | ## Actions 114 | 115 | - _Circle_ marks selected task as completed. 116 | - _Plus_ "uncompletes" selected task, adding it back to the list. 117 | - _Trash bin_ deletes selected task (gray one deletes it only from the list of completed items, not from Todoist archive). 118 | - _Input_ adds new item to the list after pressing `Enter`. 119 | 120 | ## License 121 | 122 | MIT © [Konstantin Grinkevich](https://github.com/grinstantin) 123 | -------------------------------------------------------------------------------- /todoist-card.js: -------------------------------------------------------------------------------- 1 | import {LitElement, html, css} from 'https://unpkg.com/lit-element@2.4.0/lit-element.js?module'; 2 | 3 | class TodoistCardEditor extends LitElement { 4 | static get properties() { 5 | return { 6 | hass: Object, 7 | config: Object, 8 | }; 9 | } 10 | 11 | get _entity() { 12 | if (this.config) { 13 | return this.config.entity || ''; 14 | } 15 | 16 | return ''; 17 | } 18 | 19 | get _show_completed() { 20 | if (this.config) { 21 | return (this.config.show_completed !== undefined) ? this.config.show_completed : 5; 22 | } 23 | 24 | return 5; 25 | } 26 | 27 | get _show_header() { 28 | if (this.config) { 29 | return this.config.show_header || true; 30 | } 31 | 32 | return true; 33 | } 34 | 35 | get _show_item_add() { 36 | if (this.config) { 37 | return this.config.show_item_add || true; 38 | } 39 | 40 | return true; 41 | } 42 | 43 | get _use_quick_add() { 44 | if (this.config) { 45 | return this.config.use_quick_add || false; 46 | } 47 | 48 | return false; 49 | } 50 | 51 | get _show_item_close() { 52 | if (this.config) { 53 | return this.config.show_item_close || true; 54 | } 55 | 56 | return true; 57 | } 58 | 59 | get _show_item_delete() { 60 | if (this.config) { 61 | return this.config.show_item_delete || true; 62 | } 63 | 64 | return true; 65 | } 66 | 67 | get _only_today_overdue() { 68 | if (this.config) { 69 | return this.config.only_today_overdue || false; 70 | } 71 | 72 | return false; 73 | } 74 | 75 | setConfig(config) { 76 | this.config = config; 77 | } 78 | 79 | configChanged(config) { 80 | const e = new Event('config-changed', { 81 | bubbles: true, 82 | composed: true, 83 | }); 84 | 85 | e.detail = {config: config}; 86 | 87 | this.dispatchEvent(e); 88 | } 89 | 90 | getEntitiesByType(type) { 91 | return this.hass 92 | ? Object.keys(this.hass.states).filter(entity => entity.substr(0, entity.indexOf('.')) === type) 93 | : []; 94 | } 95 | 96 | isNumeric(v) { 97 | return !isNaN(parseFloat(v)) && isFinite(v); 98 | } 99 | 100 | valueChanged(e) { 101 | if ( 102 | !this.config 103 | || !this.hass 104 | || (this[`_${e.target.configValue}`] === e.target.value) 105 | ) { 106 | return; 107 | } 108 | 109 | if (e.target.configValue) { 110 | if (e.target.value === '') { 111 | if (!['entity', 'show_completed'].includes(e.target.configValue)) { 112 | delete this.config[e.target.configValue]; 113 | } 114 | } else { 115 | this.config = { 116 | ...this.config, 117 | [e.target.configValue]: e.target.checked !== undefined 118 | ? e.target.checked 119 | : this.isNumeric(e.target.value) ? parseFloat(e.target.value) : e.target.value, 120 | }; 121 | } 122 | } 123 | 124 | this.configChanged(this.config); 125 | } 126 | 127 | render() { 128 | if (!this.hass) { 129 | return html``; 130 | } 131 | 132 | const entities = this.getEntitiesByType('sensor'); 133 | const completedCount = [...Array(16).keys()]; 134 | 135 | return html`
136 |
137 | event.stopPropagation()} 143 | .configValue=${'entity'} 144 | .value=${this._entity} 145 | > 146 | ${entities.map(entity => { 147 | return html`${entity}`; 148 | })} 149 | 150 |
151 | 152 |
153 | event.stopPropagation()} 159 | .configValue=${'show_completed'} 160 | .value=${this._show_completed} 161 | > 162 | ${completedCount.map(count => { 163 | return html`${count}`; 164 | })} 165 | 166 |
167 | 168 |
169 | 174 | 175 | Show header 176 |
177 | 178 |
179 | 184 | 185 | Show text input element for adding new items to the list 186 |
187 | 188 |
189 | 194 | 195 | 196 | Use the Quick Add implementation, available in the official Todoist clients 197 | 198 |
199 |
200 | 201 | Check your configuration before using this option 202 | 203 |
204 | 205 |
206 | 211 | 212 | Show "close/complete" and "uncomplete" buttons 213 |
214 | 215 |
216 | 221 | 222 | Show "delete" buttons 223 |
224 | 225 |
226 | 231 | 232 | Only show today or overdue 233 |
234 |
`; 235 | } 236 | 237 | static get styles() { 238 | return css` 239 | .card-config ha-select { 240 | width: 100%; 241 | } 242 | 243 | .option { 244 | display: flex; 245 | align-items: center; 246 | padding: 5px; 247 | } 248 | 249 | .option ha-switch { 250 | margin-right: 10px; 251 | } 252 | `; 253 | } 254 | } 255 | 256 | 257 | class TodoistCard extends LitElement { 258 | constructor() { 259 | super(); 260 | 261 | this.itemsCompleted = []; 262 | } 263 | 264 | static get properties() { 265 | return { 266 | hass: Object, 267 | config: Object, 268 | }; 269 | } 270 | 271 | static getConfigElement() { 272 | return document.createElement('todoist-card-editor'); 273 | } 274 | 275 | setConfig(config) { 276 | if (!config.entity) { 277 | throw new Error('Entity is not set!'); 278 | } 279 | 280 | this.config = config; 281 | } 282 | 283 | getCardSize() { 284 | return this.hass ? (this.hass.states[this.config.entity].attributes.items.length || 1) : 1; 285 | } 286 | 287 | random(min, max) { 288 | return Math.floor(Math.random() * (max - min) + min); 289 | } 290 | 291 | getUUID() { 292 | let date = new Date(); 293 | 294 | return this.random(1, 100) + '-' + (+date) + '-' + date.getMilliseconds(); 295 | } 296 | 297 | itemAdd(e) { 298 | if (e.which === 13) { 299 | let input = this.shadowRoot.getElementById('todoist-card-item-add'); 300 | let value = input.value; 301 | 302 | if (value && value.length > 1) { 303 | let stateValue = this.hass.states[this.config.entity].state || undefined; 304 | 305 | if (stateValue) { 306 | let uuid = this.getUUID(); 307 | 308 | if (!this.config.use_quick_add) { 309 | let commands = [{ 310 | 'type': 'item_add', 311 | 'temp_id': uuid, 312 | 'uuid': uuid, 313 | 'args': { 314 | 'project_id': stateValue, 315 | 'content': value, 316 | }, 317 | }]; 318 | 319 | this.hass 320 | .callService('rest_command', 'todoist', { 321 | url: 'sync', 322 | payload: 'commands=' + JSON.stringify(commands), 323 | }) 324 | .then(response => { 325 | input.value = ''; 326 | 327 | this.hass.callService('homeassistant', 'update_entity', { 328 | entity_id: this.config.entity, 329 | }); 330 | }); 331 | } else { 332 | let state = this.hass.states[this.config.entity] || undefined; 333 | if (!state) { 334 | return; 335 | } 336 | 337 | this.hass 338 | .callService('rest_command', 'todoist', { 339 | url: 'quick/add', 340 | payload: 'text=' + value + ' #' + state.attributes.project.name.replaceAll(' ',''), 341 | }) 342 | .then(response => { 343 | input.value = ''; 344 | 345 | this.hass.callService('homeassistant', 'update_entity', { 346 | entity_id: this.config.entity, 347 | }); 348 | }); 349 | } 350 | } 351 | } 352 | } 353 | } 354 | 355 | itemClose(item) { 356 | let commands = [{ 357 | 'type': 'item_close', 358 | 'uuid': this.getUUID(), 359 | 'args': { 360 | 'id': item.id, 361 | }, 362 | }]; 363 | 364 | this.hass 365 | .callService('rest_command', 'todoist', { 366 | url: 'sync', 367 | payload: 'commands=' + JSON.stringify(commands), 368 | }) 369 | .then(response => { 370 | if (this.itemsCompleted.length >= this.config.show_completed) { 371 | this.itemsCompleted.splice(0, this.itemsCompleted.length - this.config.show_completed + 1); 372 | } 373 | this.itemsCompleted.push(item); 374 | 375 | this.hass.callService('homeassistant', 'update_entity', { 376 | entity_id: this.config.entity, 377 | }); 378 | }); 379 | } 380 | 381 | itemUncomplete(item) { 382 | let commands = [{ 383 | 'type': 'item_uncomplete', 384 | 'uuid': this.getUUID(), 385 | 'args': { 386 | 'id': item.id, 387 | }, 388 | }]; 389 | 390 | this.hass 391 | .callService('rest_command', 'todoist', { 392 | url: 'sync', 393 | payload: 'commands=' + JSON.stringify(commands), 394 | }) 395 | .then(response => { 396 | this.itemDeleteCompleted(item); 397 | 398 | // this.hass.callService('homeassistant', 'update_entity', { 399 | // entity_id: this.config.entity, 400 | // }); 401 | }); 402 | } 403 | 404 | itemDelete(item) { 405 | let commands = [{ 406 | 'type': 'item_delete', 407 | 'uuid': this.getUUID(), 408 | 'args': { 409 | 'id': item.id, 410 | }, 411 | }]; 412 | 413 | this.hass 414 | .callService('rest_command', 'todoist', { 415 | url: 'sync', 416 | payload: 'commands=' + JSON.stringify(commands), 417 | }) 418 | .then(response => { 419 | this.hass.callService('homeassistant', 'update_entity', { 420 | entity_id: this.config.entity, 421 | }); 422 | }); 423 | } 424 | 425 | itemDeleteCompleted(item) { 426 | this.itemsCompleted = this.itemsCompleted.filter(v => { 427 | return v.id != item.id; 428 | }); 429 | 430 | this.hass.callService('homeassistant', 'update_entity', { 431 | entity_id: this.config.entity, 432 | }); 433 | } 434 | 435 | render() { 436 | let state = this.hass.states[this.config.entity] || undefined; 437 | 438 | if (!state) { 439 | return html``; 440 | } 441 | 442 | let items = state.attributes.items || []; 443 | if (this.config.only_today_overdue) { 444 | items = items.filter(item => { 445 | if (item.due) { 446 | if (/^\d{4}-\d{2}-\d{2}$/.test(item.due.date)) { 447 | item.due.date += 'T00:00:00'; 448 | } 449 | 450 | return (new Date()).setHours(23, 59, 59, 999) >= (new Date(item.due.date)).getTime(); 451 | } 452 | 453 | return false; 454 | }); 455 | } 456 | 457 | return html` 458 | ${(this.config.show_header === undefined) || (this.config.show_header !== false) 459 | ? html`

460 |
${state.attributes.friendly_name}
461 |

` 462 | : html``} 463 |
464 | ${items.length 465 | ? items.map(item => { 466 | return html`
467 | ${(this.config.show_item_close === undefined) || (this.config.show_item_close !== false) 468 | ? html` this.itemClose(item)} 471 | > 472 | 473 | ` 474 | : html``} 477 |
478 | ${item.description 479 | ? html`${item.content} 480 | ${item.description}` 481 | : item.content} 482 |
483 | ${(this.config.show_item_delete === undefined) || (this.config.show_item_delete !== false) 484 | ? html` this.itemDelete(item)} 487 | > 488 | 489 | ` 490 | : html``} 491 |
`; 492 | }) 493 | : html`
No uncompleted tasks!
`} 494 | ${this.config.show_completed && this.itemsCompleted 495 | ? this.itemsCompleted.map(item => { 496 | return html`
497 | ${(this.config.show_item_close === undefined) || (this.config.show_item_close !== false) 498 | ? html` this.itemUncomplete(item)} 501 | > 502 | 503 | ` 504 | : html``} 507 |
508 | ${item.description 509 | ? html`${item.content} 510 | ${item.description}` 511 | : item.content} 512 |
513 | ${(this.config.show_item_delete === undefined) || (this.config.show_item_delete !== false) 514 | ? html` this.itemDeleteCompleted(item)} 517 | > 518 | 519 | ` 520 | : html``} 521 |
`; 522 | }) 523 | : html``} 524 |
525 | ${(this.config.show_item_add === undefined) || (this.config.show_item_add !== false) 526 | ? html`` 534 | : html``} 535 |
`; 536 | } 537 | 538 | static get styles() { 539 | return css` 540 | .card-header { 541 | padding-bottom: unset; 542 | } 543 | 544 | .todoist-list { 545 | display: flex; 546 | flex-direction: column; 547 | padding: 15px; 548 | } 549 | 550 | .todoist-list-empty { 551 | padding: 15px; 552 | text-align: center; 553 | font-size: 24px; 554 | } 555 | 556 | .todoist-item { 557 | display: flex; 558 | flex-direction: row; 559 | line-height: 48px; 560 | } 561 | 562 | .todoist-item-completed { 563 | color: #808080; 564 | } 565 | 566 | .todoist-item-text, .todoist-item-text > span { 567 | font-size: 16px; 568 | white-space: nowrap; 569 | overflow: hidden; 570 | text-overflow: ellipsis; 571 | } 572 | 573 | .todoist-item-content { 574 | display: block; 575 | margin: -12px 0 -25px; 576 | } 577 | 578 | .todoist-item-description { 579 | display: block; 580 | opacity: 0.5; 581 | font-size: 12px !important; 582 | margin: -15px 0; 583 | } 584 | 585 | .todoist-item-close { 586 | color: #008000; 587 | } 588 | 589 | .todoist-item-completed .todoist-item-close { 590 | color: #808080; 591 | } 592 | 593 | .todoist-item-delete { 594 | margin-left: auto; 595 | color: #800000; 596 | } 597 | 598 | .todoist-item-completed .todoist-item-delete { 599 | color: #808080; 600 | } 601 | 602 | .todoist-item-add { 603 | width: calc(100% - 30px); 604 | height: 32px; 605 | margin: 0 15px 15px; 606 | padding: 10px; 607 | box-sizing: border-box; 608 | border-radius: 5px; 609 | font-size: 16px; 610 | } 611 | 612 | .todoist-item ha-icon-button ha-icon { 613 | margin-top: -10px; 614 | } 615 | `; 616 | } 617 | } 618 | 619 | customElements.define('todoist-card-editor', TodoistCardEditor); 620 | customElements.define('todoist-card', TodoistCard); 621 | 622 | window.customCards = window.customCards || []; 623 | window.customCards.push({ 624 | preview: true, 625 | type: 'todoist-card', 626 | name: 'Todoist Card', 627 | description: 'Custom card for displaying lists from Todoist.', 628 | }); 629 | 630 | console.info( 631 | '%c TODOIST-CARD ', 632 | 'color: white; background: orchid; font-weight: 700', 633 | ); 634 | --------------------------------------------------------------------------------