├── .github └── workflows │ ├── build.yml │ └── release.yml ├── .gitignore ├── README.md ├── grocy-chores-card.js ├── grocy-chores-card.png ├── hacs.json ├── package-lock.json ├── package.json ├── rollup.config.mjs └── style.js /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: 'Build' 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | pull_request: 8 | branches: 9 | - master 10 | 11 | jobs: 12 | build: 13 | name: Test build 14 | runs-on: ubuntu-latest 15 | steps: 16 | - uses: actions/checkout@v1 17 | - name: Build 18 | run: | 19 | npm install 20 | npm run build 21 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | release: 5 | types: [published] 6 | 7 | jobs: 8 | release: 9 | name: Prepare release 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@v1 13 | 14 | # Build 15 | - name: Build the file 16 | run: | 17 | cd /home/runner/work/lovelace-grocy-chores-card/lovelace-grocy-chores-card 18 | npm install 19 | npm run build 20 | 21 | # Upload build file to the releas as an asset. 22 | - name: Upload zip to release 23 | uses: svenstaro/upload-release-action@v1-release 24 | 25 | with: 26 | repo_token: ${{ secrets.GITHUB_TOKEN }} 27 | file: /home/runner/work/lovelace-grocy-chores-card/lovelace-grocy-chores-card/output/grocy-chores-card.js 28 | asset_name: grocy-chores-card.js 29 | tag: ${{ github.ref }} 30 | overwrite: true 31 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | output/ 3 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![hacs_badge](https://img.shields.io/badge/HACS-Custom-41BDF5.svg?style=for-the-badge)](https://github.com/hacs/integration) 2 | 3 | # grocy-chores-card 4 | 5 | A Lovelace custom card for [custom component Grocy](https://github.com/custom-components/grocy) in Home Assistant. 6 | 7 | Grocy Chores Card 8 | 9 | **This card reqires [card tools](https://github.com/thomasloven/lovelace-card-tools).** 10 | 11 | Easiest installation via [HACS](https://custom-components.github.io/hacs/). 12 | 13 | For manual installation see [this guide](https://github.com/thomasloven/hass-config/wiki/Lovelace-Plugins). 14 | 15 | 16 | 17 | ## Example configuration 18 | 19 | ```yaml 20 | title: My awesome Lovelace config 21 | resources: 22 | - url: /local/grocy-chores-card.js 23 | type: js 24 | views: 25 | title: My view 26 | cards: 27 | - type: custom:grocy-chores-card 28 | entity: 29 | - sensor.grocy_chores 30 | - sensor.grocy_tasks 31 | ``` 32 | 33 | ## Options 34 | 35 | | Name | Type | Optional | Default | Description | 36 | |------------------------------|-------------|--------------|----------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| 37 | | type | string | **Required** | | `custom:grocy-chores-card` | 38 | | entity | string/list | **Required** | | The entity id(s) of your Grocy chores and/or tasks sensor(s). | 39 | | title | string | **Optional** | `"Todo"` | The title of the card. | 40 | | show_quantity | number | **Optional** | | The number of items you want to show in the card. The rest are either hidden or show in the overflow. | 41 | | show_days | number/range | **Optional** | | E.g. `0` to only show items that are due today, overdue or have no due date. If a range is specified show only tasks with a due date in that range; e.g. `1..10` would show tasks due in the next 10 days, but not overdue tasks, or tasks due today. If not specified, shows all items. | 42 | | show_chores_without_due | bool | **Optional** | `true` | Show chores that do not have a due date. | 43 | | show_tasks_without_due | bool | **Optional** | `true` | Show tasks that do not have a due date. | 44 | | user_id | number/map | **Optional** | `1` | Id of the Grocy user performing the items. Default if not specified is `1`. A map may also be specified, see [here](#user_id)| 45 | | custom_translation | string-list | **Optional** | | List of translations of string values used in the card (see below). | 46 | | filter | string/list | **Optional** | | Only show items that contains this filter in the name. When filter is a list, filters are applied as OR. | 47 | | remove_filter | bool | **Optional** | | Use together with `filter` to remove the filter from the name when showing in card. Chore name "Yard work: Clean rain gutters" with filter "Yard work: " will then only display "Clean rain gutters". | 48 | | filter_user | number | **Optional** | | Only show items assigned to the user with this user_id. Ex: `1`. If a user_id map is defined, `current` may be used instead of a number, in which case only items assigned to the current home-assistant user are shown. | 49 | | filter_task_category | number/list | **Optional** | | Only show tasks with select category_ids. When filter is a list, filters are applied as OR. Ex: `1` | 50 | | show_assigned | bool | **Optional** | `true` | Show who's assigned to the item (does not work on tasks). | 51 | | show_last_tracked | bool | **Optional** | `true` | Show when someone last tracked this chore (does not work on tasks). | 52 | | show_last_tracked_by | bool | **Optional** | `true` | Show who last tracked this chore (`show_last_tracked` must be true to show this) (does not work on tasks). | 53 | | show_track_button | bool | **Optional** | `true` | Show track (complete) button | 54 | | show_empty | bool | **Optional** | `true` | Set to false to hide card when no items | 55 | | show_create_task | bool | **Optional** | `false` | Set to true to show ability to add a task in Grocy directly from the card. Due date must be in format yyyy-mm-dd, e.g. 2022-01-31. When due date is empty, task has no due date. | 56 | | browser_mod | bool | **Optional** | `false` | Set to true _if you have installed [browser_mod v2](https://github.com/thomasloven/hass-browser_mod)_ and want feedback when tracking or adding a task, in the form of a native toast bar. | 57 | | show_overflow | bool | **Optional** | `false` | When true, replaces the 'Look in Grocy for X more items' text with a 'Show X more' button that toggles an overflow area. | 58 | | show_divider | bool | **Optional** | `false` | When true, shows a divider between each task. Uses the CSS variable `entities-divider-color` and falls back on `divider-color` from your theme. | 59 | | use_icons | bool | **Optional** | | When null, uses icons for chores/tasks only when `chore_icon` or `task_icon` is set. When true, forces defaults if `chore_icon`/`task_icon` is not set. When false, overrides `chore_icon`/`task_icon` and always uses text buttons. | 60 | | task_icon | string | **Optional** | | Sets the icon used on Tasks. Replaces the text. Set `use_icons` to true and don't use this parameter to use default icon. | 61 | | task_icon_size | number | **Optional** | `24` | Sets the size of the icon for Tasks. Default is 24 because default is an empty checkbox. Only applies when `use_icon` or `task_icon` is set. | 62 | | chore_icon | string | **Optional** | | Sets the icon used on Chores. Replaces the text. Set `use_icons` to true and don't use this parameter to use default icon. | 63 | | chore_icon_size | number | **Optional** | `32` | Sets the size of the icon for Chores. Default is 32. Only applies when `use_icon` or `chore_icon` is set. | 64 | | expand_icon_size | number | **Optional** | `30` | Sets the size of the expand/collapse button on the Overflow area. Default is 30. Only applies when `show_overflow` is set. | 65 | | use_long_date | bool | **Optional** | `false` | Sets if the Due/Completed dates are formatted in long format (i.e. December 31, 2022) or short format (i.e. 12/31/2022). Uses localization settings for token order. | 66 | | due_in_days_threshold | number | **Optional** | `0` | Due dates are reported as 'Overdue', 'Today', 'Tomorrow', 'In X Days', and finally using the actual date. This sets how many days use the 'In X Days' format, before it switches to using date notation. | 67 | | last_tracked_days_threshold | number | **Optional** | `0` | Last tracked dates are reported as 'Today', 'Yesterday', 'x days ago' and finally the actual track date. This sets how many days use the 'x days ago' format, before it switches to using date notation. | 68 | | use_24_hours | bool | **Optional** | `true` | Sets if the times are shown in 12 hour or 24 hour formats. | 69 | | hide_text_with_no_data | bool | **Optional** | `false` | When true, if a property for an item is not set, it hides the text. For example, if a chore has never been completed, instead of showing 'Last tracked: -', it will hide the 'Last tracked' row entirely. | 70 | | haptic | string | **Optional** | `selection` | Can be set to `light`, `success`, or anything [listed here](https://companion.home-assistant.io/docs/integrations/haptics/#developers-integrating-haptics-into-custom-cards). | 71 | | show_description | bool | **Optional** | `false` | When true, show the chore/task descriptions under the name. | 72 | | description_max_length | number | **Optional** | | When set and `show_description` is set, truncate shown descriptions to the number of characters specified. | 73 | | fixed_tiling_size | number | **Optional** | | When set, provides a fixed value to the masonry tiling algorithm for card size, where 1 unit equals 50 pixels. When unset, calculate the size dynamically from the number of items in the todo list. Setting the value will give a more consistent layout of masonry elements, though they may not be well balanced. When unset, the cards will be more compactly tiled for layout, but may move around on page refresh as list length changes. 74 | | custom_sort | string/map | **Optional** | | Modifies the sort order. See [here](#custom_sort) for details. 75 | ## Advanced options 76 | It is possible to translate the following English strings in the card to whatever you like. 77 | 78 | ```yaml 79 | custom_translation: 80 | Overdue: "Försenad" 81 | Today: "Idag" 82 | Due: "Dags" 83 | 'Assigned to': "Tilldelad" 84 | 'Last tracked': "Senast" 85 | by: "av" 86 | Track: "Gör nu" 87 | 'No todos': "Tomt" 88 | 'Look in Grocy for {number} more items': "Det finns {number} fler göromål i Grocy" 89 | 'Add task': "Lägg till" 90 | 'Optional due date/time': "Valfritt datum/tid" 91 | "'Name' can't be empty": "Fyll i namn" 92 | Tracked: "Färdigställt" 93 | ``` 94 | 95 | ## Multi-User Support 96 | If your Home Assistant/Grocy installation supports multiple users, this card can handle multiple users. To enable this, specify a map in the configuration, mapping the names of each home assistant user to their grocy user-id, e.g.: 97 | 98 | ``` 99 | user_id: 100 | bob: 1 101 | alice: 2 102 | default: 1 103 | ``` 104 | In this example, home-assistant user "bob" has a grocy userid of 1, and home-assistant user "alice" has a grocy userid of 2. 105 | Then when Bob is logged into home assistant, chores/tasks will be tracked as userid 1, and when Alice is logged in, they will be tracked as userid 2. 106 | 107 | Note that this name on the left hand side is the user's home assistant display name, not the login/userid. 108 | 109 | A `default` value may also be specified, specifying the userid to use for any Home Assistant user not otherwise specified. 110 | 111 | 112 | This also affects the behavior of filter_user. Specifying `filter_user: current` in the config will only show chores/tasks for the currently logged in user. 113 | 114 | ## Custom Sorting 115 | 116 | By default, the card sorts items with the soonest due chores/tasks first. This can be modified by adding a `custom_sort` key to the configuration. The card can sort on any property available in the individual items, and it can sort on multiple indices, in ascending (default) or descending order. 117 | 118 | ### Sort Examples 119 | The current sort algorithm, sorting by due_date, ascending. 120 | `custom_sort: __due_date` 121 | 122 | Sort alphabetically by name: 123 | `custom_sort: name` 124 | 125 | Sort by user_id, then sort by due_date 126 | ``` 127 | custom_sort: 128 | - user_id 129 | - __due_date 130 | ``` 131 | Sort by category_id (descending), then by user_id, then by due date (descending) 132 | ``` 133 | custom_sort: 134 | - field: category_id 135 | direction: descending 136 | - user_id 137 | - field: __due_date 138 | direction: descending 139 | ``` 140 | 141 | The exact fields available for sort are tied into the card implementation and may change in the future without notice. To see the full list of fields that are available, review the source code or use the browser debugger to inspect what keys of the item object are available for sorting. Note tasks and chores may support different keys, so they may not sort as expected if you show both and use a sort key that is not available for both. 142 | 143 | ## Using the Collapsible Overflow 144 | Instead of the “Look in Grocy for X more items” text from older versions, this version can show all additional items in a collapsible overflow panel. 145 | 146 | ### Usage 147 | 1. Add the `show_quantity` parameter and set it to the number of items that should be shown in the main area. 148 | 2. Add the `show_overflow` parameter and set it to `true`. 149 | 3. To override the default size of the expand button icon, set `expand_icon_size` to an integer value. 150 | 151 | Once you refresh, you should see a new button with an expand button at the bottom of the card if you have additional items. Click this to expand the card and show all items. 152 | 153 | ## Icon Buttons 154 | This version adds the ability to use icons instead of the “TRACK” text buttons on each item. Tasks and Chores can have different icons and sizes. 155 | 156 | ### Usage 157 | * `use_icons` parameter 158 | - When not used or `null`, icons are controlled by the other parameters. 159 | - When `true`, both Tasks and Chores will use an icon. If the associated parameters are not set, it will use the default icons. 160 | - When `false`, all other icon options are ignored and text-based buttons are used. 161 | * `task_icon`/`chore_icon` parameters 162 | - When set to a valid icon (i.e. `mdi:check`), items of that type will use the specified icon instead of text as long as `use_icons` is not `false`. 163 | - When `use_icons` is true, you only need to use these parameters to override the default icons. 164 | * `task_icon_size`/`chore_icon_size` parameters 165 | - When icons are used, specifies the height and width of the icon for that item type. 166 | - Default for chores is `32` (as it’s a button) and tasks is `24` (because it is a checkbox). 167 | 168 | ## Date Formatting 169 | This version also introduces better date formatting. Each date is formatted based on the current user’s localization settings so that the order of tokens (day/month/year vs month/day/year) is correct. It also allows 12 or 24 hour times and long date formats (i.e. December 31, 2022). 170 | 171 | ### Usage 172 | * `use_long_date` – When `true`, uses the long date format (i.e. December 31, 2022). When `false`, uses the short format (i.e. 12/31/2022). Default is `false`. 173 | * `use_24_hours` – When `true`, the time is shown in 24 hour format (i.e. 17:59). When `false`, it uses 12 hour format (i.e. 5:59 pm). Default is to use 24 hour format (`true`). 174 | * `due_in_days_threshold` – When set to a value grater than `1`, specifies how many days from today will be shown as “Due: In X Days”. Default is `0`, which means unused. For example, if it is set to `3`, your tasks should have the following due dates: 175 | - Due: Overdue 176 | - Due: Today 177 | - Due: Tomorrow 178 | - Due: In 2 Days 179 | - Due: In 3 Days 180 | - Due: August 9, 2022 181 | - Etc. 182 | 183 | ## Miscellaneous Style Options 184 | * `show_divider` – When `true`, adds a divider between each task or chore. The color is specified in your theme using the `entities-divider-color` variable (with fallback to `divider-color`). 185 | * `hide_text_with_no_data` – When `true`, when an item’s property is blank, that property is hidden regardless of other settings. For example, if you have `show_last_tracked` set to `true`, but a chore has never been completed, instead of showing “Last tracked: -”, the “Last tracked” line simply does not appear on that item (see Clean out the Fridge in the screenshot vs Sweep the Stairs). 186 | * Some of the colors can now be specified in your theme using CSS variables. 187 | - `--red`: sets the color of Overdue due date. 188 | - `--orange`: sets the color of Today due date. 189 | - `--green`: sets the mouse-over color on icon buttons. 190 | - `--paper_font_subhead_-_font_size`: sets the size of the task title test. 191 | - `--paper_font_body1_-_font_size`: sets the size of the task’s other information text. 192 | - `--primary-text-color`: sets the title text, icon color, and item title text color. 193 | - `--secondary-text-color`: sets the color of other text. 194 | - `--accent-color`: sets the hover color of the Show More button and icon. 195 | 196 | ## Other changes in this version 197 | In general, this update aims to not change any default setups so that anyone using this card won’t notice any change unless they change it themselves. However, some small changes have been made: 198 | 199 | 1. Date is always formatted using user’s localization settings rather than always appearing YYYY/MM//DD. 200 | 2. Due Date will be shown as “Tomorrow” when it is due in 1 day. 201 | 3. Task title line will now always use the following CSS variables: 202 | - `--paper_font_subhead_-_font_size` for the size. 203 | - `--primary-text-color` for the color. 204 | 4. Other task lines will now always use the following CSS variables: 205 | - `--paper_font_body1_-_font_size` for the size. 206 | - `--secondary-text-color` for the color. 207 | 208 | ## Known Issues 209 | 1. Due/Completed dates with the time values 23:59:59 or 00:00:00 will not show the times. This is because those are the default values populated when no time is set. 210 | 211 | ## Development Instructions 212 | To do development work on this card (either for your personal use, or to contribute via pull requests), follow these steps: 213 | 214 | 1. Create a fork of this repository on GitHub 215 | 2. Download and setup the repository on your local machine, by running: 216 | ``` 217 | git clone https://github.com/YOUR_USERNAME/lovelace-grocy-chores-card 218 | cd lovelace-grocy-chores-card 219 | git remote add upstream https://github.com/isabellaalstrom/lovelace-grocy-chores-card 220 | ``` 221 | 3. Once the repository is setup, install the npm dependencies with `npm install` 222 | 4. Make local edits as needed to the grocy chores card. 223 | 5. To test changes, run `npm run build`. This will create a compiled version of the card in `output/grocy-chores-card.js`. 224 | 6. Copy the compiled card in the `output/` folder to your Home Assistant installation and update the dashboard resources to point to it. Make sure the cache is cleared each time you try updating with new changes. 225 | 7. Push the changes back to your GitHub origin, and open a pull request if you want to contribute them to the main repository. 226 | --- 227 | 228 | Like my work and want to say thanks? Do it here: 229 | 230 | Buy Me A Coffee 231 | -------------------------------------------------------------------------------- /grocy-chores-card.js: -------------------------------------------------------------------------------- 1 | import {html, LitElement, nothing} from "lit"; 2 | import {DateTime} from "luxon"; 3 | import style from './style.js'; 4 | 5 | class GrocyChoresCard extends LitElement { 6 | 7 | async loadCustomCreateTaskElements() { 8 | if(!customElements.get("ha-date-input") || !customElements.get("ha-textfield")) { 9 | const cardHelpers = await window.loadCardHelpers(); 10 | if(!customElements.get("ha-date-input")) { 11 | await cardHelpers.importMoreInfoControl("date"); 12 | } 13 | if(!customElements.get("ha-textfield")) { 14 | await cardHelpers.importMoreInfoControl("text"); 15 | } 16 | } 17 | } 18 | 19 | static get styles() { 20 | return style; 21 | } 22 | 23 | _needUpdate(hass) { 24 | const oldHass = this._hass; 25 | if(oldHass == null) { 26 | return true; 27 | } 28 | let entities = [].concat(this.config.entity); 29 | for(let i=0; ix); 129 | for(let i=0; i< sort.length; i++) { 130 | if(typeof sort[i] === "object") { 131 | let d = sort[i].direction; 132 | sort[i] = {field: sort[i].field ?? "", 133 | direction: typeof d === "string" && d.startsWith("d") ? -1 : 1}; 134 | } 135 | if(typeof sort[i] === "string") { 136 | sort[i] = {field: sort[i], direction: 1}; 137 | } 138 | } 139 | sort = sort.filter(x=>x.field !== ""); 140 | allItems.sort(function (a, b) { 141 | for(let i=0; i< sort.length; i++) { 142 | let f = sort[i].field; 143 | let dir = sort[i].direction; 144 | 145 | if (a[f] == null && b[f] == null) { 146 | continue; // Both are null, maintain current order 147 | } 148 | if (a[f] == null) { 149 | return 1 * dir; // Place null at the end 150 | } 151 | if (b[f] == null) { 152 | return -1 * dir; // Place null at the end 153 | } 154 | if (a[f] < b[f]) { 155 | return -1 * dir; 156 | } 157 | if (a[f] > b[f]) { 158 | return 1 * dir; 159 | } 160 | } 161 | return 0; 162 | }); 163 | } 164 | 165 | this.itemsNotVisible = 0; 166 | this.overflow = []; 167 | if (this.show_quantity != null) { 168 | this.items = allItems.slice(0, this.show_quantity); 169 | if (this.show_overflow) { 170 | this.overflow = allItems.slice(this.show_quantity); 171 | } else { 172 | this.itemsNotVisible = allItems.length - this.items.length; 173 | } 174 | } else { 175 | this.items = allItems; 176 | } 177 | 178 | this.requestUpdate(); 179 | } 180 | 181 | static getStubConfig() { 182 | return { 183 | entity: ["sensor.grocy_chores", "sensor.grocy_tasks"], 184 | title: "Todo", 185 | show_quantity: 5, 186 | show_assigned: true, 187 | show_overflow: true, 188 | show_chores_without_due: true, 189 | show_tasks_without_due: true, 190 | use_icons: true, 191 | use_long_date: true, 192 | due_in_days_threshold: 7, 193 | use_24_hours: true, 194 | hide_text_with_no_data: true, 195 | } 196 | } 197 | 198 | static getConfigElement() { 199 | return document.createElement("content-card-editor"); 200 | } 201 | 202 | setConfig(config) { 203 | if (!config.entity) { 204 | throw new Error('Please define entity'); 205 | } 206 | this.config = config; 207 | this._processItems(); 208 | if(this.config.show_create_task) { 209 | this.loadCustomCreateTaskElements(); 210 | } 211 | 212 | } 213 | 214 | render() { 215 | if (!this.entities) { 216 | return this._renderEntityNotFound(); 217 | } 218 | for (let i = 0; i < this.entities_not_found.length; i++) { 219 | return this._renderEntityNotFound(this.entities_not_found[i]); 220 | } 221 | 222 | if (this.items === undefined) { 223 | this.items = []; 224 | this.overflow = []; 225 | this.itemsNotVisible = 0; 226 | } 227 | 228 | if (this.items.length < 1 && !this.show_empty) { 229 | return; 230 | } 231 | 232 | return html` 233 | ${html` 234 | 235 |

236 |
237 | ${this.header} 238 |
239 | ${this.show_create_task ? this._renderAddTaskButton() : nothing} 240 |

241 | ${this.show_create_task ? this._renderAddTask() : nothing} 242 |
243 | ${this._renderItems(this.items)} 244 |
245 | ${this.itemsNotVisible > 0 ? this._renderNrItemsInGrocy() : nothing} 246 | ${this.overflow && this.overflow.length > 0 ? this._renderOverflow() : nothing} 247 |
`} 248 | `; 249 | } 250 | 251 | _renderOverflow() { 252 | return html` 253 |
254 |
255 | this._toggleOverflow(this.renderRoot)}> 257 |
258 | ${this._translate("{number} More Items", this.overflow.length)} 259 |
260 |
261 | 263 |
264 |
265 |
266 |
267 | 268 |
269 | ${this._renderItems(this.overflow)} 270 |
271 | 272 |
273 |
274 | this._toggleOverflow(this.renderRoot)}> 276 |
277 | ${this._translate("Show Less")} 278 |
279 |
280 | 282 |
283 |
284 |
285 |
286 | ` 287 | } 288 | 289 | _renderItems(cardCollection) { 290 | if (cardCollection && cardCollection.length > 0) { 291 | return html` 292 | ${cardCollection.map(item => this._renderItem(item))} 293 | ` 294 | } else { 295 | return html` 296 |
${this._translate("No todos")}!
297 | ` 298 | } 299 | } 300 | 301 | _renderItem(item) { 302 | return html` 303 |
304 |
305 | ${this._renderItemName(item)} 306 | ${this._renderItemDescription(item)} 307 | ${this._shouldRenderDueInDays(item) ? this._renderDueInDays(item) : nothing} 308 | ${this._shouldRenderAssignedToUser(item) ? this._renderAssignedToUser(item) : nothing} 309 | ${this._shouldRenderLastTracked(item) ? this._renderLastTracked(item) : nothing} 310 |
311 | ${this.show_track_button && item.__type === "chore" ? this._renderTrackChoreButton(item) : nothing} 312 | ${this.show_track_button && item.__type === "task" ? this._renderTrackTaskButton(item) : nothing} 313 |
314 | ` 315 | } 316 | 317 | _renderEntityNotFound(entity) { 318 | return html` 319 | 320 | ${this._hass.localize("ui.panel.lovelace.warning.entity_not_found", "entity", entity ?? this.config.entity)} 321 |
322 | ${this._translate(`Ensure you have the appropriate sensors enabled in your grocy integration.`)} 323 |
324 | ` 325 | } 326 | 327 | _renderNrItemsInGrocy() { 328 | return html` 329 |
330 | ${this._translate("Look in Grocy for {number} more items", this.itemsNotVisible)} 331 |
332 | ` 333 | } 334 | 335 | _shouldRenderAssignedToUser(item) { 336 | return this.show_assigned && item.assigned_to_name != null; 337 | } 338 | 339 | _renderAssignedToUser(item) { 340 | return html` 341 |
342 | ${this._translate("Assigned to")}: 343 | ${item.assigned_to_name} 344 |
345 | ` 346 | } 347 | 348 | _shouldRenderLastTracked(item) { 349 | return this.show_last_tracked && item.__type === "chore" && (!this.hide_text_with_no_data || item.__last_tracked_date != null); 350 | } 351 | 352 | _renderLastTracked(item) { 353 | return html` 354 |
355 | ${this._translate("Last tracked")}: 356 | ${this._formatLastTrackedDate(item.__last_tracked_date, item.__last_tracked_days, item.track_date_only) ?? "-"} 357 | ${this.show_last_tracked_by && item.last_done_by != null ? this._translate("by") + " " + item.last_done_by.display_name : nothing} 358 |
359 | ` 360 | } 361 | 362 | _shouldRenderDueInDays(item) { 363 | return !this.hide_text_with_no_data || item.__due_date != null; 364 | } 365 | 366 | _renderDueInDays(item) { 367 | return html` 368 |
369 | ${this._translate("Due")}: 370 | ${this._formatDueDate(item.__due_date, item.__due_in_days) ?? "-"} 371 |
372 | ` 373 | } 374 | 375 | _renderItemName(item) { 376 | return html` 377 |
378 | ${item.__filtered_name ?? item.name} 379 |
380 | ` 381 | } 382 | 383 | _renderItemDescription(item) { 384 | return item.__description ? html` 385 |
386 | ${item.__description} 387 |
388 | ` : nothing; 389 | } 390 | 391 | 392 | 393 | _renderAddTaskButton() { 394 | return html` 395 | this._toggleAddTask()}> 396 | 397 | ${this._translate("Add task")} 398 | 399 | ` 400 | } 401 | 402 | _renderAddTask() { 403 | return html` 404 |
405 | this._addTask()}> 406 | 407 | 408 | 412 | 413 |
414 |
415 | 416 | 417 | 418 | 423 | 424 |
425 | ` 426 | } 427 | 428 | _renderTrackChoreButton(item) { 429 | if (this.chore_icon != null) { 430 | return html` 431 | this._trackChore(item.id, item.name)}> 434 | 436 | 437 | ` 438 | } 439 | 440 | return html` 441 | this._trackChore(item.id, item.name)}> 443 | ${this._translate("Track")} 444 | 445 | ` 446 | } 447 | 448 | _renderTrackTaskButton(item) { 449 | if (this.task_icon != null) { 450 | return html` 451 | this._trackTask(item.id, item.name)}> 453 | 455 | 456 | ` 457 | } 458 | 459 | return html` 460 | this._trackTask(item.id, item.name)}> 462 | ${this._translate("Track")} 463 | 464 | ` 465 | } 466 | 467 | _calculateDaysTillNow(date) { 468 | const now = DateTime.now(); 469 | return date.startOf('day').diff(now.startOf('day'), 'days').days; 470 | } 471 | 472 | _dueHtmlClass(dueInDays) { 473 | if (dueInDays == null) { 474 | return null; 475 | } else if (dueInDays < 0) { 476 | return "overdue"; 477 | } else if (dueInDays < 1) { 478 | return "due-today"; 479 | } else { 480 | return "not-due"; 481 | } 482 | } 483 | 484 | _formatDueDate(dueDate, dueInDays) { 485 | if (dueInDays < 0) { 486 | return this._translate("Overdue"); 487 | } else if (dueInDays < 1) { 488 | return this._translate("Today"); 489 | } else if (dueInDays < 2) { 490 | return this._translate("Tomorrow"); 491 | } else if (dueInDays < this.due_in_days_threshold) { 492 | return this._translate("In {number} days", dueInDays); 493 | } else { 494 | return this._formatDate(dueDate, true); 495 | } 496 | } 497 | 498 | _formatLastTrackedDate(lastTrackedDate, lastTrackedDays, dateOnly) { 499 | if (lastTrackedDays < 1) { 500 | return this._translate("Today"); 501 | } else if (lastTrackedDays < 2) { 502 | return this._translate("Yesterday"); 503 | } else if (lastTrackedDays < this.last_tracked_days_threshold) { 504 | return this._translate("{number} days ago", lastTrackedDays); 505 | } else { 506 | return this._formatDate(lastTrackedDate, dateOnly); 507 | } 508 | } 509 | 510 | _translate(string, number) { 511 | let newString = string; 512 | if ((this.config.custom_translation != null) && (this.config.custom_translation[string] != null)) { 513 | newString = this.config.custom_translation[string]; 514 | } 515 | if(number != null) { 516 | newString = newString.replace("{number}", number.toString()); 517 | } 518 | return newString; 519 | } 520 | 521 | _formatDate(dateTime, isDateOnly = false) { 522 | if (dateTime == null) { 523 | return null; 524 | } 525 | 526 | let dateOptions; 527 | let timeOptions = {}; 528 | if (this.config.use_long_date) { 529 | dateOptions = {month: 'long', day: 'numeric', year: 'numeric'}; 530 | } else { 531 | dateOptions = {month: 'numeric', day: 'numeric', year: 'numeric'}; 532 | } 533 | 534 | if (!isDateOnly) { 535 | timeOptions = {hour: 'numeric', minute: 'numeric', hour12: !this.config.use_24_hours}; 536 | } 537 | 538 | const dateTimeFormatOptions = {...dateOptions, ...timeOptions}; 539 | return dateTime.toLocaleString(dateTimeFormatOptions); 540 | } 541 | 542 | _toDateTime(date) { 543 | return DateTime.fromISO(date); 544 | } 545 | 546 | _isItemVisible(item) { 547 | let visible = false; 548 | 549 | if(item.__due_in_days == null) { 550 | visible = true; 551 | visible = visible && (item.__type === "chore" ? this.show_chores_without_due : true); 552 | visible = visible && (item.__type === "task" ? this.show_tasks_without_due : true); 553 | } else { 554 | visible = visible || this.show_days == null; 555 | 556 | if(this.show_days != null) { 557 | const days_range = typeof this.show_days === "number" ? [this.show_days] : this.show_days.split("..", 2); 558 | if(days_range.length === 1) { 559 | visible = visible || (item.__due_in_days <= days_range[0]); 560 | } else { 561 | visible = visible || ((item.__due_in_days <= days_range[1]) && (item.__due_in_days >= days_range[0])); 562 | } 563 | } 564 | } 565 | 566 | visible = visible && (this.filter !== undefined ? this._checkMatchNameFilter(item) : true); 567 | visible = visible && (this.filter_user !== undefined ? this._checkMatchUserFilter(item) : true); 568 | 569 | if(item.__type === "task" && this.filter_task_category !== undefined) { 570 | visible = visible && this._checkMatchTaskCategoryFilter(item); 571 | } 572 | 573 | return visible; 574 | } 575 | 576 | _checkMatchNameFilter(item) { 577 | let filter = [].concat(this.filter); 578 | let match = filter.some(e => item.name.includes(e)); // Item name matches any filter value 579 | if(!match) { 580 | return false; 581 | } 582 | 583 | if (this.remove_filter) { 584 | item.__filtered_name = item.name; 585 | for(let i=0; i user === "current" ? this._getUserId() : user);; 595 | return userArray.some((user) => item.__user_id == user); 596 | } 597 | 598 | _checkMatchTaskCategoryFilter(item) { 599 | let filter = [].concat(this.filter_task_category); 600 | return filter.some(id => item.category_id === id); 601 | } 602 | 603 | _formatItemDescription(item) { 604 | let d = null; 605 | if(this.show_description && item.description) { 606 | d = item.description; 607 | if(this.description_max_length && (d.length > this.description_max_length)) { 608 | d = d.substring(0, this.description_max_length) + "..."; 609 | } 610 | } 611 | item.__description = d; 612 | } 613 | 614 | _getTasks(entity) { 615 | if (entity.attributes.tasks === undefined) { 616 | return null; 617 | } 618 | 619 | let items = JSON.parse(JSON.stringify(entity.attributes.tasks)); 620 | if (items === undefined) { 621 | return null; 622 | } 623 | 624 | const tasks = []; 625 | items.map(item => { 626 | item.__type = "task"; 627 | 628 | if (item.assigned_to_user) { 629 | item.__user_id = item.assigned_to_user.id; 630 | item.assigned_to_name = item.assigned_to_user.display_name; 631 | } 632 | 633 | if (item.due_date != null) { 634 | item.__due_date = this._toDateTime(item.due_date); 635 | item.__due_in_days = this._calculateDaysTillNow(item.__due_date); 636 | } 637 | 638 | this._formatItemDescription(item); 639 | 640 | if (this._isItemVisible(item)) { 641 | tasks.push(item); 642 | } 643 | 644 | }); 645 | 646 | return tasks; 647 | } 648 | 649 | _getChores(entity) { 650 | if (entity.attributes.chores === undefined) { 651 | return null; 652 | } 653 | 654 | let items = JSON.parse(JSON.stringify(entity.attributes.chores)); 655 | if (items === undefined) { 656 | return null; 657 | } 658 | 659 | const chores = []; 660 | items.map(item => { 661 | item.__type = "chore"; 662 | if (item.next_execution_assigned_user) { 663 | item.__user_id = item.next_execution_assigned_user.id; 664 | item.assigned_to_name = item.next_execution_assigned_user.display_name; 665 | } 666 | 667 | if (item.next_estimated_execution_time != null && item.next_estimated_execution_time.slice(0, 4) !== 2999) { 668 | item.__due_date = this._toDateTime(item.next_estimated_execution_time); 669 | item.__due_in_days = this._calculateDaysTillNow(item.__due_date); 670 | } 671 | 672 | if (item.last_tracked_time) { 673 | item.__last_tracked_date = this._toDateTime(item.last_tracked_time); 674 | item.__last_tracked_days = Math.abs(this._calculateDaysTillNow(item.__last_tracked_date)); 675 | } 676 | 677 | this._formatItemDescription(item); 678 | 679 | if (this._isItemVisible(item)) { 680 | chores.push(item); 681 | } 682 | 683 | }); 684 | 685 | return chores; 686 | } 687 | 688 | _getUserId() { 689 | if(typeof this.userId === "object") { 690 | return this.userId[this._hass?.user?.name] ?? this.userId.default ?? 1; 691 | } else { 692 | return this.userId ?? 1; 693 | } 694 | } 695 | 696 | _trackChore(choreId, choreName) { 697 | // Hide the chore on the next render, for better visual feedback 698 | this.local_cached_hidden_items.push(`chore${choreId}`); 699 | this.requestUpdate(); 700 | this._hass.callService("grocy", "execute_chore", { 701 | chore_id: choreId, done_by: this._getUserId() 702 | }); 703 | this._showTrackedToast(choreName); 704 | } 705 | 706 | _trackTask(taskId, taskName) { 707 | // Hide the task on the next render, for better visual feedback 708 | this.local_cached_hidden_items.push(`task${taskId}`); 709 | this.requestUpdate(); 710 | this._hass.callService("grocy", "complete_task", { 711 | task_id: taskId 712 | }); 713 | this._showTrackedToast(taskName); 714 | } 715 | 716 | _showTrackedToast(itemName) { 717 | this._showToast(itemName, this._translate("Tracked")); 718 | } 719 | 720 | _showAddedToast(itemName) { 721 | this._showToast(itemName, this._translate("Added")); 722 | } 723 | 724 | _showToast(itemName, action) { 725 | if (this.config.browser_mod) { 726 | this._hass.callService("browser_mod", "notification", { 727 | message: `${this._translate(action)} "${itemName}".`, duration: 3000 728 | }); 729 | } 730 | this._fireHaptic(); 731 | } 732 | 733 | _fireHaptic() { 734 | if (this.haptic != null) { 735 | const myevent = new Event("haptic", {bubbles: true, composed: true, cancelable: false}); 736 | myevent.detail = this.haptic; 737 | window.dispatchEvent(myevent); 738 | } 739 | } 740 | 741 | _toggleOverflow(documentFragment) { 742 | let element; 743 | const elementsHidden = documentFragment.querySelectorAll('.hidden-class.overflow-toggle'); 744 | const elementsShown = documentFragment.querySelectorAll('.show-class.overflow-toggle'); 745 | 746 | for (element in elementsHidden) { 747 | if (elementsHidden.hasOwnProperty(element)) { 748 | elementsHidden[element].classList.remove("hidden-class"); 749 | elementsHidden[element].classList.add("show-class"); 750 | } 751 | } 752 | for (element in elementsShown) { 753 | if (elementsShown.hasOwnProperty(element)) { 754 | elementsShown[element].classList.remove("show-class"); 755 | elementsShown[element].classList.add("hidden-class"); 756 | } 757 | } 758 | } 759 | 760 | _populateTaskCategory(){ 761 | let hass = this._hass; 762 | this.entities = []; 763 | this.entities_not_found = []; 764 | if(!hass) { 765 | return; 766 | } 767 | if (Array.isArray(this.config.entity)) { 768 | for (let i = 0; i < this.config.entity.length; ++i) { 769 | this.entities[i] = this.config.entity[i] in hass.states ? hass.states[this.config.entity[i]] : null; 770 | if(this.entities[i] == null) { 771 | this.entities_not_found.push(this.config.entity[i]); 772 | } 773 | } 774 | } else { 775 | this.entities[0] = this.config.entity in hass.states ? hass.states[this.config.entity] : null; 776 | if(this.entities[0] == null) { 777 | this.entities_not_found.push(this.config.entity); 778 | } 779 | } 780 | if (!Array.isArray(this.entities)) { 781 | this.requestUpdate(); 782 | return; 783 | } 784 | 785 | for (let i = 0; i < this.entities.length; i++) { 786 | let entity = this.entities[i]; 787 | let items; 788 | 789 | if(!entity) { 790 | continue; 791 | } 792 | 793 | if (entity.state === 'unknown') { 794 | console.warn("The Grocy sensor " + entity.entity_id + " is unknown."); 795 | continue; 796 | } 797 | 798 | if (entity.attributes.tasks === undefined) { 799 | return null; 800 | } 801 | 802 | let items1 = JSON.parse(JSON.stringify(entity.attributes.tasks)); 803 | if (items1 === undefined) { 804 | return null; 805 | } 806 | 807 | const tasks = []; 808 | items1.map(item => { 809 | item.__type = "task"; 810 | 811 | if (item.assigned_to_user) { 812 | item.__user_id = item.assigned_to_user.id; 813 | item.assigned_to_name = item.assigned_to_user.display_name; 814 | } 815 | 816 | if (item.due_date != null) { 817 | item.__due_date = this._toDateTime(item.due_date); 818 | item.__due_in_days = this._calculateDaysTillNow(item.__due_date); 819 | } 820 | 821 | this._formatItemDescription(item); 822 | 823 | if (this._isItemVisible(item)) { 824 | tasks.push(item); 825 | } 826 | 827 | }); 828 | const categories = {}; 829 | entity.attributes.tasks.forEach(task => { 830 | const { id, name } = task.category; 831 | categories[id] = name; // Use id as key to ensure uniqueness 832 | }); 833 | 834 | // Reference to the dropdown 835 | const dropdown = this.shadowRoot.getElementById('add-task-category'); 836 | dropdown.innerHTML=""; 837 | 838 | // Populate the dropdown 839 | Object.entries(categories).forEach(([id, name]) => { 840 | const option = this.shadowRoot.createElement('ha-list-item'); 841 | option.value = id; // Set the value to category ID 842 | option.textContent = name; // Set the text to category name 843 | dropdown.appendChild(option); 844 | }); 845 | 846 | } 847 | } 848 | 849 | _toggleAddTask() { 850 | const addTaskRow = this.shadowRoot.getElementById("add-task-row"); 851 | const addTaskRow2 = this.shadowRoot.getElementById("add-task-row2"); 852 | const addTaskIcon = this.shadowRoot.getElementById("add-task-icon"); 853 | if (addTaskRow.classList.contains('hidden-class')) { 854 | addTaskRow.classList.remove('hidden-class'); 855 | addTaskRow2.classList.remove('hidden-class'); 856 | addTaskIcon.icon = "mdi:chevron-up"; 857 | } else { 858 | addTaskRow.classList.add('hidden-class'); 859 | addTaskRow2.classList.add('hidden-class'); 860 | addTaskIcon.icon = "mdi:chevron-down"; 861 | } 862 | this._populateTaskCategory(); 863 | } 864 | 865 | _addTask() { 866 | const taskName = this.shadowRoot.getElementById('add-task').value; 867 | const taskDueDate = this.shadowRoot.getElementById('add-date').value; 868 | const taskCategory = this.shadowRoot.getElementById('add-task-category').value; 869 | let taskData = {}; 870 | if (!taskName) { 871 | alert(this._translate("'Name' can't be empty")); 872 | return; 873 | } 874 | 875 | taskData["name"] = taskName; 876 | if (taskDueDate) { 877 | let parsedDate = DateTime.fromFormat(taskDueDate, "yyyy-LL-dd"); 878 | if (!parsedDate.isValid) { 879 | alert(this._translate("Due date must be empty or a valid date in format yyyy-mm-dd")); 880 | return; 881 | } 882 | 883 | taskData["due_date"] = taskDueDate; 884 | } 885 | taskData["category_id"] = taskCategory; 886 | taskData["assigned_to_user_id"] = this._getUserId(); 887 | 888 | this._hass.callService("grocy", "add_generic", { 889 | entity_type: "tasks", data: taskData 890 | }); 891 | 892 | this.shadowRoot.getElementById('add-task').value = ""; 893 | this.shadowRoot.getElementById('add-task-category').value = ""; 894 | this.shadowRoot.getElementById('add-date').value = ""; 895 | 896 | this._showAddedToast(taskName); 897 | } 898 | 899 | _configSetup() { 900 | this.userId = this.config.user_id ?? 1; 901 | this.filter = this.config.filter; 902 | this.filter_user = this.config.filter_user; 903 | this.filter_task_category = this.config.filter_task_category; 904 | this.remove_filter = this.config.remove_filter ?? false; 905 | this.show_quantity = this.config.show_quantity || null; 906 | this.show_days = this.config.show_days ?? null; 907 | this.show_chores_without_due = this.config.show_chores_without_due ?? true; 908 | this.show_tasks_without_due = this.config.show_tasks_without_due ?? true; 909 | this.show_assigned = this.config.show_assigned ?? true; 910 | this.show_track_button = this.config.show_track_button ?? true; 911 | this.show_last_tracked = this.config.show_last_tracked ?? true; 912 | this.show_last_tracked_by = this.config.show_last_tracked_by ?? true; 913 | this.filter_category = this.config.filter_category ?? null; 914 | this.show_category = this.config.show_category ?? true; 915 | this.show_description = this.config.show_description ?? false; 916 | this.show_empty = this.config.show_empty ?? true; 917 | this.show_create_task = this.config.show_create_task ?? false; 918 | this.show_overflow = this.config.show_overflow || false; 919 | this.chore_icon_size = this.config.chore_icon_size || 32; 920 | this.task_icon_size = this.config.task_icon_size || 24; 921 | this.expand_icon_size = this.config.expand_icon_size || 30; 922 | this.show_divider = this.config.show_divider ?? false; 923 | this.due_in_days_threshold = this.config.due_in_days_threshold || 0; 924 | this.last_tracked_days_threshold = this.config.last_tracked_days_threshold || 0; 925 | this.hide_text_with_no_data = this.config.hide_text_with_no_data ?? false; 926 | this.use_long_date = this.config.use_long_date ?? false; 927 | this.use_24_hours = this.config.use_24_hours ?? true; 928 | this.haptic = this.config.haptic ?? "selection"; 929 | this.task_icon = null 930 | this.chore_icon = null 931 | this.custom_sort = this.config.custom_sort; 932 | this.fixed_tiling_size = this.config.fixed_tiling_size ?? null; 933 | this.use_icons = this.config.use_icons ?? false; 934 | if (this.use_icons) { 935 | this.task_icon = this.config.task_icon || 'mdi:checkbox-blank-outline'; 936 | this.chore_icon = this.config.chore_icon || 'mdi:check-circle-outline'; 937 | } 938 | if(this.show_description) { 939 | this.description_max_length = this.config.description_max_length ?? null; 940 | } 941 | } 942 | 943 | 944 | constructor() { 945 | super(); 946 | this.local_cached_hidden_items = [] 947 | } 948 | 949 | getCardSize() { 950 | if(this.fixed_tiling_size != null) { 951 | return this.fixed_tiling_size; 952 | } 953 | //an item seems to be about 70-80 pixels, depending on options, and a 'unit' of size is 50 pixels. 954 | if(Array.isArray(this.items)) { 955 | return Math.floor(this.items.length * 3 / 2) || 1; 956 | } else { 957 | return 3; 958 | } 959 | } 960 | } 961 | 962 | // Configure the preview in the Lovelace card picker 963 | window.customCards = window.customCards || []; 964 | window.customCards.push({ 965 | type: 'grocy-chores-card', 966 | name: 'Grocy Chores and Tasks Card', 967 | preview: false, 968 | description: 'A card used to display chores and/or tasks from the Grocy custom component.', 969 | documentationURL: 'https://github.com/isabellaalstrom/lovelace-grocy-chores-card' 970 | }); 971 | 972 | customElements.define('grocy-chores-card', GrocyChoresCard); 973 | -------------------------------------------------------------------------------- /grocy-chores-card.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/isabellaalstrom/lovelace-grocy-chores-card/d5b2a255f20e7f9e09af13f3d98cb8d4e511a6c1/grocy-chores-card.png -------------------------------------------------------------------------------- /hacs.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Lovelace Grocy Chores Card", 3 | "render_readme": true, 4 | "filename": "grocy-chores-card.js" 5 | } 6 | -------------------------------------------------------------------------------- /package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "lovelace-grocy-chores-card", 3 | "lockfileVersion": 2, 4 | "requires": true, 5 | "packages": { 6 | "": { 7 | "dependencies": { 8 | "lit": "^3.0.0", 9 | "luxon": "^3.2.1", 10 | "rollup": "^4.0.2" 11 | }, 12 | "devDependencies": { 13 | "@rollup/plugin-node-resolve": "^15.2.3" 14 | } 15 | }, 16 | "node_modules/@lit-labs/ssr-dom-shim": { 17 | "version": "1.1.2", 18 | "resolved": "https://registry.npmjs.org/@lit-labs/ssr-dom-shim/-/ssr-dom-shim-1.1.2.tgz", 19 | "integrity": "sha512-jnOD+/+dSrfTWYfSXBXlo5l5f0q1UuJo3tkbMDCYA2lKUYq79jaxqtGEvnRoh049nt1vdo1+45RinipU6FGY2g==" 20 | }, 21 | "node_modules/@lit/reactive-element": { 22 | "version": "2.0.0", 23 | "resolved": "https://registry.npmjs.org/@lit/reactive-element/-/reactive-element-2.0.0.tgz", 24 | "integrity": "sha512-wn+2+uDcs62ROBmVAwssO4x5xue/uKD3MGGZOXL2sMxReTRIT0JXKyMXeu7gh0aJ4IJNEIG/3aOnUaQvM7BMzQ==", 25 | "dependencies": { 26 | "@lit-labs/ssr-dom-shim": "^1.1.2-pre.0" 27 | } 28 | }, 29 | "node_modules/@rollup/plugin-node-resolve": { 30 | "version": "15.2.3", 31 | "resolved": "https://registry.npmjs.org/@rollup/plugin-node-resolve/-/plugin-node-resolve-15.2.3.tgz", 32 | "integrity": "sha512-j/lym8nf5E21LwBT4Df1VD6hRO2L2iwUeUmP7litikRsVp1H6NWx20NEp0Y7su+7XGc476GnXXc4kFeZNGmaSQ==", 33 | "dev": true, 34 | "dependencies": { 35 | "@rollup/pluginutils": "^5.0.1", 36 | "@types/resolve": "1.20.2", 37 | "deepmerge": "^4.2.2", 38 | "is-builtin-module": "^3.2.1", 39 | "is-module": "^1.0.0", 40 | "resolve": "^1.22.1" 41 | }, 42 | "engines": { 43 | "node": ">=14.0.0" 44 | }, 45 | "peerDependencies": { 46 | "rollup": "^2.78.0||^3.0.0||^4.0.0" 47 | }, 48 | "peerDependenciesMeta": { 49 | "rollup": { 50 | "optional": true 51 | } 52 | } 53 | }, 54 | "node_modules/@rollup/pluginutils": { 55 | "version": "5.0.5", 56 | "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-5.0.5.tgz", 57 | "integrity": "sha512-6aEYR910NyP73oHiJglti74iRyOwgFU4x3meH/H8OJx6Ry0j6cOVZ5X/wTvub7G7Ao6qaHBEaNsV3GLJkSsF+Q==", 58 | "dev": true, 59 | "dependencies": { 60 | "@types/estree": "^1.0.0", 61 | "estree-walker": "^2.0.2", 62 | "picomatch": "^2.3.1" 63 | }, 64 | "engines": { 65 | "node": ">=14.0.0" 66 | }, 67 | "peerDependencies": { 68 | "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" 69 | }, 70 | "peerDependenciesMeta": { 71 | "rollup": { 72 | "optional": true 73 | } 74 | } 75 | }, 76 | "node_modules/@rollup/rollup-android-arm-eabi": { 77 | "version": "4.0.2", 78 | "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.0.2.tgz", 79 | "integrity": "sha512-xDvk1pT4vaPU2BOLy0MqHMdYZyntqpaBf8RhBiezlqG9OjY8F50TyctHo8znigYKd+QCFhCmlmXHOL/LoaOl3w==", 80 | "cpu": [ 81 | "arm" 82 | ], 83 | "optional": true, 84 | "os": [ 85 | "android" 86 | ] 87 | }, 88 | "node_modules/@rollup/rollup-android-arm64": { 89 | "version": "4.0.2", 90 | "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.0.2.tgz", 91 | "integrity": "sha512-lqCglytY3E6raze27DD9VQJWohbwCxzqs9aSHcj5X/8hJpzZfNdbsr4Ja9Hqp6iPyF53+5PtPx0pKRlkSvlHZg==", 92 | "cpu": [ 93 | "arm64" 94 | ], 95 | "optional": true, 96 | "os": [ 97 | "android" 98 | ] 99 | }, 100 | "node_modules/@rollup/rollup-darwin-arm64": { 101 | "version": "4.0.2", 102 | "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.0.2.tgz", 103 | "integrity": "sha512-nkBKItS6E6CCzvRwgiKad+j+1ibmL7SIInj7oqMWmdkCjiSX6VeVZw2mLlRKIUL+JjsBgpATTfo7BiAXc1v0jA==", 104 | "cpu": [ 105 | "arm64" 106 | ], 107 | "optional": true, 108 | "os": [ 109 | "darwin" 110 | ] 111 | }, 112 | "node_modules/@rollup/rollup-darwin-x64": { 113 | "version": "4.0.2", 114 | "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.0.2.tgz", 115 | "integrity": "sha512-vX2C8xvWPIbpEgQht95+dY6BReKAvtDgPDGi0XN0kWJKkm4WdNmq5dnwscv/zxvi+n6jUTBhs6GtpkkWT4q8Gg==", 116 | "cpu": [ 117 | "x64" 118 | ], 119 | "optional": true, 120 | "os": [ 121 | "darwin" 122 | ] 123 | }, 124 | "node_modules/@rollup/rollup-linux-arm-gnueabihf": { 125 | "version": "4.0.2", 126 | "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.0.2.tgz", 127 | "integrity": "sha512-DVFIfcHOjgmeHOAqji4xNz2wczt1Bmzy9MwBZKBa83SjBVO/i38VHDR+9ixo8QpBOiEagmNw12DucG+v55tCrg==", 128 | "cpu": [ 129 | "arm" 130 | ], 131 | "optional": true, 132 | "os": [ 133 | "linux" 134 | ] 135 | }, 136 | "node_modules/@rollup/rollup-linux-arm64-gnu": { 137 | "version": "4.0.2", 138 | "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.0.2.tgz", 139 | "integrity": "sha512-GCK/a9ItUxPI0V5hQEJjH4JtOJO90GF2Hja7TO+EZ8rmkGvEi8/ZDMhXmcuDpQT7/PWrTT9RvnG8snMd5SrhBQ==", 140 | "cpu": [ 141 | "arm64" 142 | ], 143 | "optional": true, 144 | "os": [ 145 | "linux" 146 | ] 147 | }, 148 | "node_modules/@rollup/rollup-linux-arm64-musl": { 149 | "version": "4.0.2", 150 | "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.0.2.tgz", 151 | "integrity": "sha512-cLuBp7rOjIB1R2j/VazjCmHC7liWUur2e9mFflLJBAWCkrZ+X0+QwHLvOQakIwDymungzAKv6W9kHZnTp/Mqrg==", 152 | "cpu": [ 153 | "arm64" 154 | ], 155 | "optional": true, 156 | "os": [ 157 | "linux" 158 | ] 159 | }, 160 | "node_modules/@rollup/rollup-linux-x64-gnu": { 161 | "version": "4.0.2", 162 | "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.0.2.tgz", 163 | "integrity": "sha512-Zqw4iVnJr2naoyQus0yLy7sLtisCQcpdMKUCeXPBjkJtpiflRime/TMojbnl8O3oxUAj92mxr+t7im/RbgA20w==", 164 | "cpu": [ 165 | "x64" 166 | ], 167 | "optional": true, 168 | "os": [ 169 | "linux" 170 | ] 171 | }, 172 | "node_modules/@rollup/rollup-linux-x64-musl": { 173 | "version": "4.0.2", 174 | "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.0.2.tgz", 175 | "integrity": "sha512-jJRU9TyUD/iMqjf8aLAp7XiN3pIj5v6Qcu+cdzBfVTKDD0Fvua4oUoK8eVJ9ZuKBEQKt3WdlcwJXFkpmMLk6kg==", 176 | "cpu": [ 177 | "x64" 178 | ], 179 | "optional": true, 180 | "os": [ 181 | "linux" 182 | ] 183 | }, 184 | "node_modules/@rollup/rollup-win32-arm64-msvc": { 185 | "version": "4.0.2", 186 | "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.0.2.tgz", 187 | "integrity": "sha512-ZkS2NixCxHKC4zbOnw64ztEGGDVIYP6nKkGBfOAxEPW71Sji9v8z3yaHNuae/JHPwXA+14oDefnOuVfxl59SmQ==", 188 | "cpu": [ 189 | "arm64" 190 | ], 191 | "optional": true, 192 | "os": [ 193 | "win32" 194 | ] 195 | }, 196 | "node_modules/@rollup/rollup-win32-ia32-msvc": { 197 | "version": "4.0.2", 198 | "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.0.2.tgz", 199 | "integrity": "sha512-3SKjj+tvnZ0oZq2BKB+fI+DqYI83VrRzk7eed8tJkxeZ4zxJZcLSE8YDQLYGq1tZAnAX+H076RHHB4gTZXsQzw==", 200 | "cpu": [ 201 | "ia32" 202 | ], 203 | "optional": true, 204 | "os": [ 205 | "win32" 206 | ] 207 | }, 208 | "node_modules/@rollup/rollup-win32-x64-msvc": { 209 | "version": "4.0.2", 210 | "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.0.2.tgz", 211 | "integrity": "sha512-MBdJIOxRauKkry7t2q+rTHa3aWjVez2eioWg+etRVS3dE4tChhmt5oqZYr48R6bPmcwEhxQr96gVRfeQrLbqng==", 212 | "cpu": [ 213 | "x64" 214 | ], 215 | "optional": true, 216 | "os": [ 217 | "win32" 218 | ] 219 | }, 220 | "node_modules/@types/estree": { 221 | "version": "1.0.2", 222 | "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.2.tgz", 223 | "integrity": "sha512-VeiPZ9MMwXjO32/Xu7+OwflfmeoRwkE/qzndw42gGtgJwZopBnzy2gD//NN1+go1mADzkDcqf/KnFRSjTJ8xJA==", 224 | "dev": true 225 | }, 226 | "node_modules/@types/resolve": { 227 | "version": "1.20.2", 228 | "resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-1.20.2.tgz", 229 | "integrity": "sha512-60BCwRFOZCQhDncwQdxxeOEEkbc5dIMccYLwbxsS4TUNeVECQ/pBJ0j09mrHOl/JJvpRPGwO9SvE4nR2Nb/a4Q==", 230 | "dev": true 231 | }, 232 | "node_modules/@types/trusted-types": { 233 | "version": "2.0.4", 234 | "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.4.tgz", 235 | "integrity": "sha512-IDaobHimLQhjwsQ/NMwRVfa/yL7L/wriQPMhw1ZJall0KX6E1oxk29XMDeilW5qTIg5aoiqf5Udy8U/51aNoQQ==" 236 | }, 237 | "node_modules/builtin-modules": { 238 | "version": "3.3.0", 239 | "resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-3.3.0.tgz", 240 | "integrity": "sha512-zhaCDicdLuWN5UbN5IMnFqNMhNfo919sH85y2/ea+5Yg9TsTkeZxpL+JLbp6cgYFS4sRLp3YV4S6yDuqVWHYOw==", 241 | "dev": true, 242 | "engines": { 243 | "node": ">=6" 244 | }, 245 | "funding": { 246 | "url": "https://github.com/sponsors/sindresorhus" 247 | } 248 | }, 249 | "node_modules/deepmerge": { 250 | "version": "4.3.1", 251 | "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", 252 | "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", 253 | "dev": true, 254 | "engines": { 255 | "node": ">=0.10.0" 256 | } 257 | }, 258 | "node_modules/estree-walker": { 259 | "version": "2.0.2", 260 | "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", 261 | "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==", 262 | "dev": true 263 | }, 264 | "node_modules/fsevents": { 265 | "version": "2.3.3", 266 | "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", 267 | "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", 268 | "hasInstallScript": true, 269 | "optional": true, 270 | "os": [ 271 | "darwin" 272 | ], 273 | "engines": { 274 | "node": "^8.16.0 || ^10.6.0 || >=11.0.0" 275 | } 276 | }, 277 | "node_modules/has": { 278 | "version": "1.0.4", 279 | "resolved": "https://registry.npmjs.org/has/-/has-1.0.4.tgz", 280 | "integrity": "sha512-qdSAmqLF6209RFj4VVItywPMbm3vWylknmB3nvNiUIs72xAimcM8nVYxYr7ncvZq5qzk9MKIZR8ijqD/1QuYjQ==", 281 | "dev": true, 282 | "engines": { 283 | "node": ">= 0.4.0" 284 | } 285 | }, 286 | "node_modules/is-builtin-module": { 287 | "version": "3.2.1", 288 | "resolved": "https://registry.npmjs.org/is-builtin-module/-/is-builtin-module-3.2.1.tgz", 289 | "integrity": "sha512-BSLE3HnV2syZ0FK0iMA/yUGplUeMmNz4AW5fnTunbCIqZi4vG3WjJT9FHMy5D69xmAYBHXQhJdALdpwVxV501A==", 290 | "dev": true, 291 | "dependencies": { 292 | "builtin-modules": "^3.3.0" 293 | }, 294 | "engines": { 295 | "node": ">=6" 296 | }, 297 | "funding": { 298 | "url": "https://github.com/sponsors/sindresorhus" 299 | } 300 | }, 301 | "node_modules/is-core-module": { 302 | "version": "2.13.0", 303 | "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.13.0.tgz", 304 | "integrity": "sha512-Z7dk6Qo8pOCp3l4tsX2C5ZVas4V+UxwQodwZhLopL91TX8UyyHEXafPcyoeeWuLrwzHcr3igO78wNLwHJHsMCQ==", 305 | "dev": true, 306 | "dependencies": { 307 | "has": "^1.0.3" 308 | }, 309 | "funding": { 310 | "url": "https://github.com/sponsors/ljharb" 311 | } 312 | }, 313 | "node_modules/is-module": { 314 | "version": "1.0.0", 315 | "resolved": "https://registry.npmjs.org/is-module/-/is-module-1.0.0.tgz", 316 | "integrity": "sha512-51ypPSPCoTEIN9dy5Oy+h4pShgJmPCygKfyRCISBI+JoWT/2oJvK8QPxmwv7b/p239jXrm9M1mlQbyKJ5A152g==", 317 | "dev": true 318 | }, 319 | "node_modules/lit": { 320 | "version": "3.0.0", 321 | "resolved": "https://registry.npmjs.org/lit/-/lit-3.0.0.tgz", 322 | "integrity": "sha512-nQ0teRzU1Kdj++VdmttS2WvIen8M79wChJ6guRKIIym2M3Ansg3Adj9O6yuQh2IpjxiUXlNuS81WKlQ4iL3BmA==", 323 | "dependencies": { 324 | "@lit/reactive-element": "^2.0.0", 325 | "lit-element": "^4.0.0", 326 | "lit-html": "^3.0.0" 327 | } 328 | }, 329 | "node_modules/lit-element": { 330 | "version": "4.0.0", 331 | "resolved": "https://registry.npmjs.org/lit-element/-/lit-element-4.0.0.tgz", 332 | "integrity": "sha512-N6+f7XgusURHl69DUZU6sTBGlIN+9Ixfs3ykkNDfgfTkDYGGOWwHAYBhDqVswnFGyWgQYR2KiSpu4J76Kccs/A==", 333 | "dependencies": { 334 | "@lit-labs/ssr-dom-shim": "^1.1.2-pre.0", 335 | "@lit/reactive-element": "^2.0.0", 336 | "lit-html": "^3.0.0" 337 | } 338 | }, 339 | "node_modules/lit-html": { 340 | "version": "3.0.0", 341 | "resolved": "https://registry.npmjs.org/lit-html/-/lit-html-3.0.0.tgz", 342 | "integrity": "sha512-DNJIE8dNY0dQF2Gs0sdMNUppMQT2/CvV4OVnSdg7BXAsGqkVwsE5bqQ04POfkYH5dBIuGnJYdFz5fYYyNnOxiA==", 343 | "dependencies": { 344 | "@types/trusted-types": "^2.0.2" 345 | } 346 | }, 347 | "node_modules/luxon": { 348 | "version": "3.2.1", 349 | "resolved": "https://registry.npmjs.org/luxon/-/luxon-3.2.1.tgz", 350 | "integrity": "sha512-QrwPArQCNLAKGO/C+ZIilgIuDnEnKx5QYODdDtbFaxzsbZcc/a7WFq7MhsVYgRlwawLtvOUESTlfJ+hc/USqPg==", 351 | "engines": { 352 | "node": ">=12" 353 | } 354 | }, 355 | "node_modules/path-parse": { 356 | "version": "1.0.7", 357 | "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", 358 | "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", 359 | "dev": true 360 | }, 361 | "node_modules/picomatch": { 362 | "version": "2.3.1", 363 | "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", 364 | "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", 365 | "dev": true, 366 | "engines": { 367 | "node": ">=8.6" 368 | }, 369 | "funding": { 370 | "url": "https://github.com/sponsors/jonschlinkert" 371 | } 372 | }, 373 | "node_modules/resolve": { 374 | "version": "1.22.8", 375 | "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz", 376 | "integrity": "sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==", 377 | "dev": true, 378 | "dependencies": { 379 | "is-core-module": "^2.13.0", 380 | "path-parse": "^1.0.7", 381 | "supports-preserve-symlinks-flag": "^1.0.0" 382 | }, 383 | "bin": { 384 | "resolve": "bin/resolve" 385 | }, 386 | "funding": { 387 | "url": "https://github.com/sponsors/ljharb" 388 | } 389 | }, 390 | "node_modules/rollup": { 391 | "version": "4.0.2", 392 | "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.0.2.tgz", 393 | "integrity": "sha512-MCScu4usMPCeVFaiLcgMDaBQeYi1z6vpWxz0r0hq0Hv77Y2YuOTZldkuNJ54BdYBH3e+nkrk6j0Rre/NLDBYzg==", 394 | "bin": { 395 | "rollup": "dist/bin/rollup" 396 | }, 397 | "engines": { 398 | "node": ">=18.0.0", 399 | "npm": ">=8.0.0" 400 | }, 401 | "optionalDependencies": { 402 | "@rollup/rollup-android-arm-eabi": "4.0.2", 403 | "@rollup/rollup-android-arm64": "4.0.2", 404 | "@rollup/rollup-darwin-arm64": "4.0.2", 405 | "@rollup/rollup-darwin-x64": "4.0.2", 406 | "@rollup/rollup-linux-arm-gnueabihf": "4.0.2", 407 | "@rollup/rollup-linux-arm64-gnu": "4.0.2", 408 | "@rollup/rollup-linux-arm64-musl": "4.0.2", 409 | "@rollup/rollup-linux-x64-gnu": "4.0.2", 410 | "@rollup/rollup-linux-x64-musl": "4.0.2", 411 | "@rollup/rollup-win32-arm64-msvc": "4.0.2", 412 | "@rollup/rollup-win32-ia32-msvc": "4.0.2", 413 | "@rollup/rollup-win32-x64-msvc": "4.0.2", 414 | "fsevents": "~2.3.2" 415 | } 416 | }, 417 | "node_modules/supports-preserve-symlinks-flag": { 418 | "version": "1.0.0", 419 | "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", 420 | "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", 421 | "dev": true, 422 | "engines": { 423 | "node": ">= 0.4" 424 | }, 425 | "funding": { 426 | "url": "https://github.com/sponsors/ljharb" 427 | } 428 | } 429 | }, 430 | "dependencies": { 431 | "@lit-labs/ssr-dom-shim": { 432 | "version": "1.1.2", 433 | "resolved": "https://registry.npmjs.org/@lit-labs/ssr-dom-shim/-/ssr-dom-shim-1.1.2.tgz", 434 | "integrity": "sha512-jnOD+/+dSrfTWYfSXBXlo5l5f0q1UuJo3tkbMDCYA2lKUYq79jaxqtGEvnRoh049nt1vdo1+45RinipU6FGY2g==" 435 | }, 436 | "@lit/reactive-element": { 437 | "version": "2.0.0", 438 | "resolved": "https://registry.npmjs.org/@lit/reactive-element/-/reactive-element-2.0.0.tgz", 439 | "integrity": "sha512-wn+2+uDcs62ROBmVAwssO4x5xue/uKD3MGGZOXL2sMxReTRIT0JXKyMXeu7gh0aJ4IJNEIG/3aOnUaQvM7BMzQ==", 440 | "requires": { 441 | "@lit-labs/ssr-dom-shim": "^1.1.2-pre.0" 442 | } 443 | }, 444 | "@rollup/plugin-node-resolve": { 445 | "version": "15.2.3", 446 | "resolved": "https://registry.npmjs.org/@rollup/plugin-node-resolve/-/plugin-node-resolve-15.2.3.tgz", 447 | "integrity": "sha512-j/lym8nf5E21LwBT4Df1VD6hRO2L2iwUeUmP7litikRsVp1H6NWx20NEp0Y7su+7XGc476GnXXc4kFeZNGmaSQ==", 448 | "dev": true, 449 | "requires": { 450 | "@rollup/pluginutils": "^5.0.1", 451 | "@types/resolve": "1.20.2", 452 | "deepmerge": "^4.2.2", 453 | "is-builtin-module": "^3.2.1", 454 | "is-module": "^1.0.0", 455 | "resolve": "^1.22.1" 456 | } 457 | }, 458 | "@rollup/pluginutils": { 459 | "version": "5.0.5", 460 | "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-5.0.5.tgz", 461 | "integrity": "sha512-6aEYR910NyP73oHiJglti74iRyOwgFU4x3meH/H8OJx6Ry0j6cOVZ5X/wTvub7G7Ao6qaHBEaNsV3GLJkSsF+Q==", 462 | "dev": true, 463 | "requires": { 464 | "@types/estree": "^1.0.0", 465 | "estree-walker": "^2.0.2", 466 | "picomatch": "^2.3.1" 467 | } 468 | }, 469 | "@rollup/rollup-android-arm-eabi": { 470 | "version": "4.0.2", 471 | "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.0.2.tgz", 472 | "integrity": "sha512-xDvk1pT4vaPU2BOLy0MqHMdYZyntqpaBf8RhBiezlqG9OjY8F50TyctHo8znigYKd+QCFhCmlmXHOL/LoaOl3w==", 473 | "optional": true 474 | }, 475 | "@rollup/rollup-android-arm64": { 476 | "version": "4.0.2", 477 | "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.0.2.tgz", 478 | "integrity": "sha512-lqCglytY3E6raze27DD9VQJWohbwCxzqs9aSHcj5X/8hJpzZfNdbsr4Ja9Hqp6iPyF53+5PtPx0pKRlkSvlHZg==", 479 | "optional": true 480 | }, 481 | "@rollup/rollup-darwin-arm64": { 482 | "version": "4.0.2", 483 | "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.0.2.tgz", 484 | "integrity": "sha512-nkBKItS6E6CCzvRwgiKad+j+1ibmL7SIInj7oqMWmdkCjiSX6VeVZw2mLlRKIUL+JjsBgpATTfo7BiAXc1v0jA==", 485 | "optional": true 486 | }, 487 | "@rollup/rollup-darwin-x64": { 488 | "version": "4.0.2", 489 | "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.0.2.tgz", 490 | "integrity": "sha512-vX2C8xvWPIbpEgQht95+dY6BReKAvtDgPDGi0XN0kWJKkm4WdNmq5dnwscv/zxvi+n6jUTBhs6GtpkkWT4q8Gg==", 491 | "optional": true 492 | }, 493 | "@rollup/rollup-linux-arm-gnueabihf": { 494 | "version": "4.0.2", 495 | "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.0.2.tgz", 496 | "integrity": "sha512-DVFIfcHOjgmeHOAqji4xNz2wczt1Bmzy9MwBZKBa83SjBVO/i38VHDR+9ixo8QpBOiEagmNw12DucG+v55tCrg==", 497 | "optional": true 498 | }, 499 | "@rollup/rollup-linux-arm64-gnu": { 500 | "version": "4.0.2", 501 | "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.0.2.tgz", 502 | "integrity": "sha512-GCK/a9ItUxPI0V5hQEJjH4JtOJO90GF2Hja7TO+EZ8rmkGvEi8/ZDMhXmcuDpQT7/PWrTT9RvnG8snMd5SrhBQ==", 503 | "optional": true 504 | }, 505 | "@rollup/rollup-linux-arm64-musl": { 506 | "version": "4.0.2", 507 | "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.0.2.tgz", 508 | "integrity": "sha512-cLuBp7rOjIB1R2j/VazjCmHC7liWUur2e9mFflLJBAWCkrZ+X0+QwHLvOQakIwDymungzAKv6W9kHZnTp/Mqrg==", 509 | "optional": true 510 | }, 511 | "@rollup/rollup-linux-x64-gnu": { 512 | "version": "4.0.2", 513 | "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.0.2.tgz", 514 | "integrity": "sha512-Zqw4iVnJr2naoyQus0yLy7sLtisCQcpdMKUCeXPBjkJtpiflRime/TMojbnl8O3oxUAj92mxr+t7im/RbgA20w==", 515 | "optional": true 516 | }, 517 | "@rollup/rollup-linux-x64-musl": { 518 | "version": "4.0.2", 519 | "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.0.2.tgz", 520 | "integrity": "sha512-jJRU9TyUD/iMqjf8aLAp7XiN3pIj5v6Qcu+cdzBfVTKDD0Fvua4oUoK8eVJ9ZuKBEQKt3WdlcwJXFkpmMLk6kg==", 521 | "optional": true 522 | }, 523 | "@rollup/rollup-win32-arm64-msvc": { 524 | "version": "4.0.2", 525 | "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.0.2.tgz", 526 | "integrity": "sha512-ZkS2NixCxHKC4zbOnw64ztEGGDVIYP6nKkGBfOAxEPW71Sji9v8z3yaHNuae/JHPwXA+14oDefnOuVfxl59SmQ==", 527 | "optional": true 528 | }, 529 | "@rollup/rollup-win32-ia32-msvc": { 530 | "version": "4.0.2", 531 | "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.0.2.tgz", 532 | "integrity": "sha512-3SKjj+tvnZ0oZq2BKB+fI+DqYI83VrRzk7eed8tJkxeZ4zxJZcLSE8YDQLYGq1tZAnAX+H076RHHB4gTZXsQzw==", 533 | "optional": true 534 | }, 535 | "@rollup/rollup-win32-x64-msvc": { 536 | "version": "4.0.2", 537 | "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.0.2.tgz", 538 | "integrity": "sha512-MBdJIOxRauKkry7t2q+rTHa3aWjVez2eioWg+etRVS3dE4tChhmt5oqZYr48R6bPmcwEhxQr96gVRfeQrLbqng==", 539 | "optional": true 540 | }, 541 | "@types/estree": { 542 | "version": "1.0.2", 543 | "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.2.tgz", 544 | "integrity": "sha512-VeiPZ9MMwXjO32/Xu7+OwflfmeoRwkE/qzndw42gGtgJwZopBnzy2gD//NN1+go1mADzkDcqf/KnFRSjTJ8xJA==", 545 | "dev": true 546 | }, 547 | "@types/resolve": { 548 | "version": "1.20.2", 549 | "resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-1.20.2.tgz", 550 | "integrity": "sha512-60BCwRFOZCQhDncwQdxxeOEEkbc5dIMccYLwbxsS4TUNeVECQ/pBJ0j09mrHOl/JJvpRPGwO9SvE4nR2Nb/a4Q==", 551 | "dev": true 552 | }, 553 | "@types/trusted-types": { 554 | "version": "2.0.4", 555 | "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.4.tgz", 556 | "integrity": "sha512-IDaobHimLQhjwsQ/NMwRVfa/yL7L/wriQPMhw1ZJall0KX6E1oxk29XMDeilW5qTIg5aoiqf5Udy8U/51aNoQQ==" 557 | }, 558 | "builtin-modules": { 559 | "version": "3.3.0", 560 | "resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-3.3.0.tgz", 561 | "integrity": "sha512-zhaCDicdLuWN5UbN5IMnFqNMhNfo919sH85y2/ea+5Yg9TsTkeZxpL+JLbp6cgYFS4sRLp3YV4S6yDuqVWHYOw==", 562 | "dev": true 563 | }, 564 | "deepmerge": { 565 | "version": "4.3.1", 566 | "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", 567 | "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", 568 | "dev": true 569 | }, 570 | "estree-walker": { 571 | "version": "2.0.2", 572 | "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", 573 | "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==", 574 | "dev": true 575 | }, 576 | "fsevents": { 577 | "version": "2.3.3", 578 | "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", 579 | "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", 580 | "optional": true 581 | }, 582 | "has": { 583 | "version": "1.0.4", 584 | "resolved": "https://registry.npmjs.org/has/-/has-1.0.4.tgz", 585 | "integrity": "sha512-qdSAmqLF6209RFj4VVItywPMbm3vWylknmB3nvNiUIs72xAimcM8nVYxYr7ncvZq5qzk9MKIZR8ijqD/1QuYjQ==", 586 | "dev": true 587 | }, 588 | "is-builtin-module": { 589 | "version": "3.2.1", 590 | "resolved": "https://registry.npmjs.org/is-builtin-module/-/is-builtin-module-3.2.1.tgz", 591 | "integrity": "sha512-BSLE3HnV2syZ0FK0iMA/yUGplUeMmNz4AW5fnTunbCIqZi4vG3WjJT9FHMy5D69xmAYBHXQhJdALdpwVxV501A==", 592 | "dev": true, 593 | "requires": { 594 | "builtin-modules": "^3.3.0" 595 | } 596 | }, 597 | "is-core-module": { 598 | "version": "2.13.0", 599 | "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.13.0.tgz", 600 | "integrity": "sha512-Z7dk6Qo8pOCp3l4tsX2C5ZVas4V+UxwQodwZhLopL91TX8UyyHEXafPcyoeeWuLrwzHcr3igO78wNLwHJHsMCQ==", 601 | "dev": true, 602 | "requires": { 603 | "has": "^1.0.3" 604 | } 605 | }, 606 | "is-module": { 607 | "version": "1.0.0", 608 | "resolved": "https://registry.npmjs.org/is-module/-/is-module-1.0.0.tgz", 609 | "integrity": "sha512-51ypPSPCoTEIN9dy5Oy+h4pShgJmPCygKfyRCISBI+JoWT/2oJvK8QPxmwv7b/p239jXrm9M1mlQbyKJ5A152g==", 610 | "dev": true 611 | }, 612 | "lit": { 613 | "version": "3.0.0", 614 | "resolved": "https://registry.npmjs.org/lit/-/lit-3.0.0.tgz", 615 | "integrity": "sha512-nQ0teRzU1Kdj++VdmttS2WvIen8M79wChJ6guRKIIym2M3Ansg3Adj9O6yuQh2IpjxiUXlNuS81WKlQ4iL3BmA==", 616 | "requires": { 617 | "@lit/reactive-element": "^2.0.0", 618 | "lit-element": "^4.0.0", 619 | "lit-html": "^3.0.0" 620 | } 621 | }, 622 | "lit-element": { 623 | "version": "4.0.0", 624 | "resolved": "https://registry.npmjs.org/lit-element/-/lit-element-4.0.0.tgz", 625 | "integrity": "sha512-N6+f7XgusURHl69DUZU6sTBGlIN+9Ixfs3ykkNDfgfTkDYGGOWwHAYBhDqVswnFGyWgQYR2KiSpu4J76Kccs/A==", 626 | "requires": { 627 | "@lit-labs/ssr-dom-shim": "^1.1.2-pre.0", 628 | "@lit/reactive-element": "^2.0.0", 629 | "lit-html": "^3.0.0" 630 | } 631 | }, 632 | "lit-html": { 633 | "version": "3.0.0", 634 | "resolved": "https://registry.npmjs.org/lit-html/-/lit-html-3.0.0.tgz", 635 | "integrity": "sha512-DNJIE8dNY0dQF2Gs0sdMNUppMQT2/CvV4OVnSdg7BXAsGqkVwsE5bqQ04POfkYH5dBIuGnJYdFz5fYYyNnOxiA==", 636 | "requires": { 637 | "@types/trusted-types": "^2.0.2" 638 | } 639 | }, 640 | "luxon": { 641 | "version": "3.2.1", 642 | "resolved": "https://registry.npmjs.org/luxon/-/luxon-3.2.1.tgz", 643 | "integrity": "sha512-QrwPArQCNLAKGO/C+ZIilgIuDnEnKx5QYODdDtbFaxzsbZcc/a7WFq7MhsVYgRlwawLtvOUESTlfJ+hc/USqPg==" 644 | }, 645 | "path-parse": { 646 | "version": "1.0.7", 647 | "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", 648 | "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", 649 | "dev": true 650 | }, 651 | "picomatch": { 652 | "version": "2.3.1", 653 | "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", 654 | "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", 655 | "dev": true 656 | }, 657 | "resolve": { 658 | "version": "1.22.8", 659 | "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz", 660 | "integrity": "sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==", 661 | "dev": true, 662 | "requires": { 663 | "is-core-module": "^2.13.0", 664 | "path-parse": "^1.0.7", 665 | "supports-preserve-symlinks-flag": "^1.0.0" 666 | } 667 | }, 668 | "rollup": { 669 | "version": "4.0.2", 670 | "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.0.2.tgz", 671 | "integrity": "sha512-MCScu4usMPCeVFaiLcgMDaBQeYi1z6vpWxz0r0hq0Hv77Y2YuOTZldkuNJ54BdYBH3e+nkrk6j0Rre/NLDBYzg==", 672 | "requires": { 673 | "@rollup/rollup-android-arm-eabi": "4.0.2", 674 | "@rollup/rollup-android-arm64": "4.0.2", 675 | "@rollup/rollup-darwin-arm64": "4.0.2", 676 | "@rollup/rollup-darwin-x64": "4.0.2", 677 | "@rollup/rollup-linux-arm-gnueabihf": "4.0.2", 678 | "@rollup/rollup-linux-arm64-gnu": "4.0.2", 679 | "@rollup/rollup-linux-arm64-musl": "4.0.2", 680 | "@rollup/rollup-linux-x64-gnu": "4.0.2", 681 | "@rollup/rollup-linux-x64-musl": "4.0.2", 682 | "@rollup/rollup-win32-arm64-msvc": "4.0.2", 683 | "@rollup/rollup-win32-ia32-msvc": "4.0.2", 684 | "@rollup/rollup-win32-x64-msvc": "4.0.2", 685 | "fsevents": "~2.3.2" 686 | } 687 | }, 688 | "supports-preserve-symlinks-flag": { 689 | "version": "1.0.0", 690 | "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", 691 | "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", 692 | "dev": true 693 | } 694 | } 695 | } 696 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "dependencies": { 3 | "lit": "^3.0.0", 4 | "luxon": "^3.2.1", 5 | "rollup": "^4.0.2" 6 | }, 7 | "devDependencies": { 8 | "@rollup/plugin-node-resolve": "^15.2.3" 9 | }, 10 | "scripts": { 11 | "build": "rollup --config rollup.config.mjs" 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /rollup.config.mjs: -------------------------------------------------------------------------------- 1 | import { nodeResolve } from '@rollup/plugin-node-resolve'; 2 | 3 | export default { 4 | input: 'grocy-chores-card.js', 5 | output: { 6 | dir: 'output', 7 | format: 'es' 8 | }, 9 | plugins: [nodeResolve()] 10 | }; 11 | -------------------------------------------------------------------------------- /style.js: -------------------------------------------------------------------------------- 1 | import {css} from "lit"; 2 | 3 | const style = css` 4 | 5 | .info { 6 | padding-bottom: 1em; 7 | } 8 | 9 | .flex { 10 | display: flex; 11 | justify-content: space-between; 12 | } 13 | 14 | .card-content { 15 | padding-bottom: 0; 16 | } 17 | 18 | .overdue { 19 | color: var(--red, red) !important; 20 | } 21 | 22 | .due-today { 23 | color: var(--orange, orange) !important; 24 | } 25 | 26 | .not-showing { 27 | margin-left: 16px; 28 | padding-bottom: 16px; 29 | } 30 | 31 | .primary { 32 | display: block; 33 | color: var(--primary-text-color, #8c96a5); 34 | font-size: var(--paper-font-subhead_-_font-size); 35 | } 36 | 37 | .secondary { 38 | display: block; 39 | color: var(--secondary-text-color, #8c96a5); 40 | font-size: var(--paper-font-body1_-_font-size); 41 | } 42 | 43 | .add-row { 44 | margin-top: -16px; 45 | padding-bottom: 16px; 46 | display: flex; 47 | flex-direction: row; 48 | align-items: center; 49 | width: 100%; 50 | } 51 | 52 | .add-input { 53 | padding-right: 16px; 54 | width: 100%; 55 | } 56 | 57 | .hide-button { 58 | padding: 0 0 16px 16px; 59 | } 60 | 61 | .grocy-item-container, .grocy-item-container-no-border { 62 | border-bottom: 1px solid var(--entities-divider-color, var(--divider-color, transparent)); 63 | padding-top: 12px; 64 | align-items: center; 65 | } 66 | 67 | .grocy-item-container-no-border { 68 | border: none; 69 | } 70 | 71 | .expand-button { 72 | --mdc-ripple-color: none; 73 | } 74 | 75 | .track-button { 76 | --mdc-ripple-color: none; 77 | } 78 | 79 | .track-checkbox { 80 | --mdc-ripple-color: none; 81 | } 82 | 83 | .more-items-title { 84 | padding: 6px 16px 6px 16px; 85 | width: calc(100% - 32px); 86 | margin-top: -8px; 87 | } 88 | 89 | .show-more-button { 90 | width: 100%; 91 | display: flex; 92 | justify-content: space-between; 93 | cursor: pointer; 94 | font-size: var(--paper-font-subhead_-_font-size); 95 | line-height: calc(var(--paper-font-subhead_-_font-size) + 20px); 96 | } 97 | 98 | .show-more-button:hover { 99 | color: var(--accent-color) 100 | } 101 | 102 | .track-button-icon { 103 | color: var(--primary-text-color); 104 | } 105 | 106 | .track-button-icon:hover { 107 | color: var(--green, var(--active-color, green)); 108 | } 109 | 110 | .card-overflow-content { 111 | 112 | } 113 | 114 | .hidden-class { 115 | display: none; 116 | } 117 | 118 | .show-class { 119 | 120 | } 121 | 122 | .card-overflow-content { 123 | margin-top: 0; 124 | } 125 | 126 | `; 127 | 128 | export default style; 129 | --------------------------------------------------------------------------------