├── .gitignore ├── .github ├── FUNDING.yml ├── ISSUE_TEMPLATE │ ├── feature_request.md │ └── bug_report.md └── workflows │ ├── build.yml │ └── release.yml ├── simple-tabs-gif.gif ├── hacs.json ├── .prettierrc.js ├── .vscode ├── tasks.json └── extensions.json ├── tsconfig.json ├── .eslintrc.js ├── rollup.config.js ├── LICENSE ├── package.json ├── EXAMPLES.md ├── README.md └── src ├── simple-tabs-editor.ts └── simple-tabs.ts /.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules/ 2 | /.rpt2_cache/ 3 | package-lock.json 4 | /dist -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: [agoberg85] 2 | custom: ['https://buymeacoffee.com/mysmarthomeblog'] -------------------------------------------------------------------------------- /simple-tabs-gif.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/agoberg85/home-assistant-simple-tabs/HEAD/simple-tabs-gif.gif -------------------------------------------------------------------------------- /hacs.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Simple Tabs Card", 3 | "render_readme": true, 4 | "filename": "simple-tabs.js" 5 | } -------------------------------------------------------------------------------- /.prettierrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | semi: true, 3 | trailingComma: 'all', 4 | singleQuote: true, 5 | printWidth: 120, 6 | tabWidth: 2, 7 | }; -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "2.0.0", 3 | "tasks": [ 4 | { 5 | "type": "npm", 6 | "script": "start", 7 | "problemMatcher": [], 8 | "label": "npm: start", 9 | "detail": "rollup -c --watch" 10 | } 11 | ] 12 | } -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "github.vscode-pull-request-github", 4 | "eamodio.gitlens", 5 | "dbaeumer.vscode-eslint", 6 | "esbenp.prettier-vscode", 7 | "bierner.lit-html", 8 | "runem.lit-plugin", 9 | "auchenberg.vscode-browser-preview", 10 | "davidanson.vscode-markdownlint", 11 | "redhat.vscode-yaml" 12 | ] 13 | } -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2021", 4 | "module": "ESNext", 5 | "moduleResolution": "node", 6 | "lib": ["ES2021", "DOM"], 7 | "declaration": false, 8 | "sourceMap": true, 9 | "outDir": "dist", 10 | "strict": true, 11 | "esModuleInterop": true, 12 | "skipLibCheck": true, 13 | "forceConsistentCasingInFileNames": true, 14 | "experimentalDecorators": true, 15 | "useDefineForClassFields": false, 16 | "types": ["node"] 17 | }, 18 | "include": ["src/**/*.ts"], 19 | "exclude": ["node_modules", "dist"] 20 | } -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | parser: "@typescript-eslint/parser", // Specifies the ESLint parser 3 | parserOptions: { 4 | ecmaVersion: 2020, // Allows for the parsing of modern ECMAScript features 5 | sourceType: "module" // Allows for the use of imports 6 | }, 7 | extends: [ 8 | "plugin:@typescript-eslint/recommended" // Uses the recommended rules from the @typescript-eslint/eslint-plugin 9 | ], 10 | rules: { 11 | // Place to specify ESLint rules. Can be used to overwrite rules specified from the extended configs 12 | // e.g. "@typescript-eslint/explicit-function-return-type": "off", 13 | } 14 | }; -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: feature request 6 | assignees: '' 7 | --- 8 | 9 | **Is your feature request related to a problem? Please describe.** 10 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 11 | 12 | **Describe the solution you'd like** 13 | A clear and concise description of what you want to happen. 14 | 15 | **Describe alternatives you've considered** 16 | A clear and concise description of any alternative solutions or features you've considered. 17 | 18 | **Additional context** 19 | Add any other context or screenshots about the feature request here. 20 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: 'Build Check' 2 | 3 | on: 4 | push: 5 | branches: 6 | - main # Corrected from 'master' 7 | pull_request: 8 | branches: 9 | - main # Corrected from 'master' 10 | 11 | jobs: 12 | build: 13 | name: Test build 14 | runs-on: ubuntu-latest 15 | steps: 16 | - name: Checkout the repository 17 | uses: actions/checkout@v3 # Use a modern version of the checkout action 18 | 19 | - name: Set up Node.js 20 | uses: actions/setup-node@v3 21 | with: 22 | node-version: '18' # Specify a stable Node.js version 23 | 24 | - name: Install dependencies 25 | run: npm install 26 | 27 | - name: Run build 28 | run: npm run build -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | import typescript from 'rollup-plugin-typescript2'; 2 | import commonjs from '@rollup/plugin-commonjs'; 3 | import nodeResolve from '@rollup/plugin-node-resolve'; 4 | import terser from '@rollup/plugin-terser'; 5 | import serve from 'rollup-plugin-serve'; 6 | 7 | const dev = process.env.ROLLUP_WATCH; 8 | 9 | export default { 10 | input: 'src/simple-tabs.ts', 11 | output: { 12 | file: 'dist/simple-tabs.js', 13 | format: 'es', 14 | inlineDynamicImports: true, 15 | }, 16 | // --- ADD THIS LINE --- 17 | inlineDynamicImports: true, 18 | // -------------------- 19 | plugins: [ 20 | nodeResolve(), 21 | commonjs(), 22 | typescript(), 23 | dev && serve({ 24 | contentBase: '.', 25 | host: '0.0.0.0', 26 | port: 5000, 27 | headers: { 28 | 'Access-Control-Allow-Origin': '*', 29 | }, 30 | }), 31 | !dev && terser({ format: { comments: false } }), 32 | ], 33 | }; -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | release: 5 | types: [published] 6 | 7 | jobs: 8 | release: 9 | name: Create Release Asset 10 | runs-on: ubuntu-latest 11 | steps: 12 | - name: Checkout the repository 13 | uses: actions/checkout@v3 14 | 15 | - name: Set up Node.js 16 | uses: actions/setup-node@v3 17 | with: 18 | node-version: '18' 19 | 20 | - name: Install dependencies 21 | run: npm install 22 | 23 | - name: Build the production file 24 | run: npm run build 25 | 26 | - name: Upload the card to the release 27 | uses: svenstaro/upload-release-action@v2 # Use a modern version of the upload action 28 | with: 29 | repo_token: ${{ secrets.GITHUB_TOKEN }} 30 | file: dist/simple-tabs.js # Corrected path to your built file 31 | asset_name: simple-tabs.js # Corrected name for the release asset 32 | tag: ${{ github.ref }} 33 | overwrite: true -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Custom cards for Home Assistant 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 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "simple-tabs-card", 3 | "version": "1.1.7", 4 | "description": "A simple, configurable tabs card for Home Assistant.", 5 | "main": "dist/simple-tabs.js", 6 | "module": "dist/simple-tabs.js", 7 | "author": "Your Name", 8 | "license": "MIT", 9 | "type": "module", 10 | "scripts": { 11 | "start": "rollup -c --watch", 12 | "build": "rollup -c", 13 | "lint": "eslint src/*.ts" 14 | }, 15 | "dependencies": { 16 | "custom-card-helpers": "^1.9.0", 17 | "home-assistant-js-websocket": "^9.0.0", 18 | "js-yaml": "^4.1.0", 19 | "lit": "^3.0.0" 20 | }, 21 | "devDependencies": { 22 | "@rollup/plugin-commonjs": "^25.0.4", 23 | "@rollup/plugin-node-resolve": "^15.3.1", 24 | "@rollup/plugin-terser": "^0.4.3", 25 | "@types/js-yaml": "^4.0.9", 26 | "@types/node": "^20.5.9", 27 | "@typescript-eslint/eslint-plugin": "^5.62.0", 28 | "@typescript-eslint/parser": "^5.62.0", 29 | "eslint": "^8.48.0", 30 | "rollup": "^3.28.1", 31 | "rollup-plugin-serve": "^2.0.2", 32 | "rollup-plugin-typescript2": "^0.36.0", 33 | "typescript": "^5.2.2" 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: bug 6 | assignees: '' 7 | --- 8 | 9 | 14 | 15 | **Checklist:** 16 | 17 | - [ ] I updated to the latest version available 18 | - [ ] I cleared the cache of my browser 19 | 20 | **Release with the issue:** 21 | 22 | **Last working release (if known):** 23 | 24 | **Browser and Operating System:** 25 | 26 | 29 | 30 | **Description of problem:** 31 | 32 | 35 | 36 | **Javascript errors shown in the web inspector (if applicable):** 37 | 38 | ``` 39 | 40 | ``` 41 | 42 | **Additional information:** 43 | -------------------------------------------------------------------------------- /EXAMPLES.md: -------------------------------------------------------------------------------- 1 | ### Conditional Tabs 2 | 3 | You can dynamically show or hide a tab by adding a `conditions` list to its configuration. The tab will only be visible if **all** conditions in the list are met (this is an "AND" relationship). 4 | 5 | Each condition in the list must be an object of one of the following types: 6 | 7 | #### State Condition 8 | 9 | This condition checks if a specific entity has a specific state. 10 | 11 | | Key | Type | Description | 12 | | :--- | :--- | :--- | 13 | | `entity`| string | The entity ID to check. | 14 | | `state` | string | The state the entity must have for the condition to be true. | 15 | 16 | **Example:** Show a "Security" tab only if an `input_boolean` is on. 17 | ```yaml 18 | tabs: 19 | - title: Security 20 | icon: mdi:shield-lock 21 | conditions: 22 | - entity: input_boolean.show_security_tab 23 | state: 'on' 24 | card: 25 | type: alarm-panel 26 | entity: alarm_control_panel.home 27 | ``` 28 | 29 | #### Template Condition 30 | 31 | This condition evaluates a Home Assistant template in real-time. The tab will be shown if the template's result is "truthy" (e.g., `true`, a non-zero number, or a non-empty string like "show"). For clarity, it's best to have your template explicitly return `true` or `false`. 32 | 33 | | Key | Type | Description | 34 | | :--- | :--- | :--- | 35 | | `template`| string | The Home Assistant template to evaluate. | 36 | 37 | **Example:** Only show a "Guest Mode" tab if the `guest_mode` input boolean is on. 38 | ```yaml 39 | tabs: 40 | - title: Guest Mode 41 | icon: mdi:account-star 42 | conditions: 43 | - template: "{{ is_state('input_boolean.guest_mode', 'on') }}" 44 | card: 45 | # ... card config for guests 46 | ``` 47 | 48 | #### Combining Conditions 49 | 50 | You can add multiple condition objects to the list to create more specific rules. 51 | 52 | **Example:** Show a "Good Morning" tab only if a specific person is home *and* it is between 6 AM and 11 AM. 53 | ```yaml 54 | tabs: 55 | - title: Good Morning 56 | icon: mdi:weather-sunset-up 57 | conditions: 58 | # Condition 1: Person must be home 59 | - entity: person.jane_doe 60 | state: 'home' 61 | # AND Condition 2: Must be morning 62 | - template: "{{ now().hour >= 6 and now().hour < 11 }}" 63 | card: 64 | # ... card showing morning routine info 65 | ``` -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Simple Tabs Card 2 | 3 | A clean and configurable tabs card for Home Assistant Dashboards. 4 | 5 |  6 | 7 | ## Features 8 | 9 | - **Organize Your Dashboard:** Group any Dashboard cards into a clean, tabbed interface. 10 | - **Tab Icons:** Add icon to your tab titles. 11 | - **Stylable:** Customize colors for the background, border, text, and active tab. 12 | - **Alignment:** Align your tabs to the start, center, or end of the card. 13 | - **Performance:** Use the default "lazy-loading" for the best performance, or enable "pre-loading" for instantaneous tab switching. 14 | - **Conditional Tabs:** Dynamically show or hide tabs based on entity states or complex jinja templates. 15 | 16 | ## Installation 17 | 18 | ### HACS (Recommended) 19 | 20 | 1. Go to the HACS page in your Home Assistant instance. 21 | 2. Click the three-dot menu in the top right. 22 | 3. Select "Custom repositories". 23 | 4. In the "Repository" field, paste the URL of this repository (https://github.com/agoberg85/home-assistant-simple-tabs). 24 | 5. For "Category", select "Dashboard". 25 | 6. Click "Add". 26 | 7. The `simple-tabs-card` will now appear in the HACS Frontend list. Click "Install". 27 | 28 | ### Manual Installation 29 | 30 | 1. Download the `simple-tabs.js` file from the latest [release](https://github.com/agoberg85/home-assistant-simple-tabs/releases). 31 | 2. Copy the file to the `www` directory in your Home Assistant `config` folder. 32 | 3. In your Lovelace dashboard, go to "Manage Resources" and add a new resource: 33 | - URL: `/local/simple-tabs.js` 34 | - Resource Type: `JavaScript Module` 35 | 36 | ## Configuration 37 | 38 | ### Main Options 39 | 40 | | Name | Type | Required? | Description | Default | 41 | | :--- | :--- | :--- | :--- | :--- | 42 | | `type` | string | **Required** | `custom:simple-tabs` | | 43 | | `tabs` | list | **Required** | A list of tab objects to display. See below. | | 44 | | `alignment` | string | Optional | Justification for the row of tabs. (`start`, `center`, `end`) | `'center'` | 45 | | `default_tab` | number | Optional | Defines the default tab. If a tab is hidden via conditions it will fall back to the first visible tab. | `1` | 46 | | `pre-load` | boolean | Optional | If `true`, renders all tab content on load for faster switching. | `false` | 47 | | `background-color`| string | Optional | CSS color for the button background. | `none` | 48 | | `border-color` | string | Optional | CSS color for the button border. | Your theme's `divider-color` | 49 | | `text-color` | string | Optional | CSS color for the button text. | Your theme's `secondary-text-color`| 50 | | `hover-color` | string | Optional | CSS color for button text and border on hover. | Your theme's `primary-text-color`| 51 | | `active-text-color`| string | Optional | CSS color for the active tab's text. | Your theme's `text-primary-color`| 52 | | `active-background`| string | Optional | CSS color/gradient for the active tab's background. | Your theme's `primary-color` | 53 | | `container_background`| string | Optional | CSS color/gradient for the background color of the container. | none | 54 | | `container_padding`| string | Optional | Container padding | 0 | 55 | | `container_rounding`| string | Optional | Border radius of the container | 0 | 56 | 57 | ### Tab Object Options 58 | 59 | Each entry in the `tabs` list is an object with the following properties: 60 | 61 | | Name | Type | Required? | Description | 62 | | :--- | :--- | :--- | :--- | 63 | | `title` | string | Optional* | The text to display on the tab. Can be jinja template | 64 | | `icon` | string | Optional* | An MDI icon to display next to the title (e.g., `mdi:lightbulb`). Can be jinja template | 65 | | `card` | object | **Required** | A standard Lovelace card configuration. | 66 | | `conditions` | list | Optional | A list of conditions that must be met to show the tab. See [EXAMPLES.md](EXAMPLES.md) | 67 | 68 | *Either title or icon has to be defined. 69 | 70 | ## Example Usage 71 | 72 | ### Example Configuration 73 | 74 | This will create two centered tabs. 75 | 76 | ```yaml 77 | type: custom:simple-tabs 78 | pre-load: false 79 | default_tab: 2 80 | alignment: center 81 | background-color: "#2a2a2a" 82 | border-color: "#555555" 83 | text-color: "#bbbbbb" 84 | hover-color: "#ffffff" 85 | active-text-color: "#000000" 86 | active-background: linear-gradient(122deg,rgba(230, 163, 222, 1) 20%, rgba(0, 212, 255, 1) 150%) 87 | tabs: 88 | - title: Weather 89 | icon: mdi:weather-sunny 90 | conditions: 91 | - entity: input_boolean.zone_home 92 | state: "on" 93 | - template: "{{ now().hour < 17 }}" 94 | card: 95 | type: markdown 96 | content: Weather card goes here 97 | - title: "Lights on: {{ states.light | selectattr('state','eq','on') | list | count }}" 98 | icon: mdi:lightbulb 99 | card: 100 | type: markdown 101 | content: Lights goes here 102 | ``` 103 | 104 | ## Roadmap ahead 105 | 106 | - **Visual Configuration Editor:** The current version has a basic UI editor, will continue developing a more robust UI editor. 107 | - **More styling options:** Add more configuration options for for example font size, font weight, tab spacing, and button border-radius. 108 | - **Touch navigation:** Add support for touch/swipe navigation on mobile devices. 109 | - **Animations:** Add animations when switching between tabs. 110 | - **URL support:** Make tabs linkable via URLs (like #tab-2) 111 | - **Badges:** Option to add badges (text, number, color) to tab buttons. 112 | 113 | ## Support development 114 | 115 | Buy me a coffee: https://buymeacoffee.com/mysmarthomeblog 116 | 117 | Subscribe to Youtube channel: https://www.youtube.com/@My_Smart_Home 118 | -------------------------------------------------------------------------------- /src/simple-tabs-editor.ts: -------------------------------------------------------------------------------- 1 | import { LitElement, html, css, TemplateResult } from 'lit'; 2 | import { customElement, property, state } from 'lit/decorators.js'; 3 | import { fireEvent, HomeAssistant } from 'custom-card-helpers'; 4 | import { TabsCardConfig } from './simple-tabs'; 5 | import { LovelaceCardConfig } from 'custom-card-helpers/dist/types'; 6 | import * as yaml from 'js-yaml'; 7 | 8 | // Declare the types for Home Assistant's custom elements globally. 9 | declare global { 10 | interface HTMLElementTagNameMap { 11 | 'ha-yaml-editor': HaYamlEditor; 12 | 'ha-icon-picker': HaIconPicker; 13 | 'ha-textfield': HaTextField; 14 | 'ha-expansion-panel': HaExpansionPanel; 15 | } 16 | } 17 | 18 | interface HaYamlEditor extends HTMLElement { 19 | defaultValue: string; 20 | value: string; 21 | hass: HomeAssistant; 22 | isValid: boolean; 23 | name: string; 24 | } 25 | 26 | interface HaIconPicker extends HTMLElement { 27 | value: string; 28 | label: string; 29 | name: string; 30 | } 31 | 32 | interface HaTextField extends HTMLElement { 33 | value: string; 34 | label: string; 35 | name: string; 36 | } 37 | 38 | interface HaExpansionPanel extends HTMLElement { 39 | header: string; 40 | expanded: boolean; 41 | } 42 | 43 | 44 | // Helper function to safely stringify the card config into YAML 45 | function stringifyCard(card: LovelaceCardConfig | string): string { 46 | let cardObject: LovelaceCardConfig; 47 | 48 | if (typeof card === 'string') { 49 | try { 50 | // Try to parse the string as YAML. 51 | cardObject = yaml.load(card) as LovelaceCardConfig; 52 | // If the parsed result is not an object (e.g., just a string or number), 53 | // return the original string because we can't format it as a card. 54 | if (typeof cardObject !== 'object' || cardObject === null) { 55 | return card; 56 | } 57 | } catch (e) { 58 | // If it's not valid YAML, return the string as is for the user to fix. 59 | return card; 60 | } 61 | } else { 62 | cardObject = card; 63 | } 64 | 65 | // Now we are sure we have an object, dump it to a clean YAML string. 66 | try { 67 | return yaml.dump(cardObject, { skipInvalid: true, indent: 2 }).trimEnd(); 68 | } catch (e) { 69 | console.error("Error dumping YAML:", e); 70 | return JSON.stringify(cardObject, null, 2); 71 | } 72 | } 73 | 74 | @customElement('simple-tabs-editor') 75 | export class SimpleTabsEditor extends LitElement { 76 | @property({ attribute: false }) public hass!: HomeAssistant; 77 | @state() private _config!: TabsCardConfig; 78 | 79 | public setConfig(config: TabsCardConfig): void { 80 | this._config = config; 81 | } 82 | 83 | private _valueChanged(newConfig: TabsCardConfig): void { 84 | fireEvent(this, 'config-changed', { config: newConfig }); 85 | } 86 | 87 | private _handleTabChange(ev: Event, index: number): void { 88 | if (!this._config) return; 89 | 90 | const target = ev.target as (HaTextField | HaYamlEditor | HaIconPicker); 91 | const newTabs = [...this._config.tabs]; 92 | let value: string | object; 93 | 94 | // FIX: Changed the unsafe type cast from 'HTMLInputElement' to a safer generic object type. 95 | const eventValue = (ev as CustomEvent).detail?.value ?? (target as { value: string }).value; 96 | const fieldName = target.name; 97 | 98 | if (fieldName === 'card') { 99 | try { 100 | // Add indentation to each line of the input to make it valid YAML 101 | const indentedValue = eventValue 102 | .split('\n') 103 | .map((line: string) => ` ${line}`) 104 | .join('\n'); 105 | value = yaml.load(indentedValue) as object; 106 | if (value === null || typeof value !== 'object') { 107 | value = { type: '' }; 108 | } 109 | } catch (e) { 110 | value = eventValue; 111 | } 112 | } else { 113 | value = eventValue; 114 | } 115 | 116 | newTabs[index] = { ...newTabs[index], [fieldName]: value }; 117 | this._valueChanged({ ...this._config, tabs: newTabs }); 118 | } 119 | 120 | private _addTab(): void { 121 | if (!this._config) return; 122 | const newTabs = [...(this._config.tabs || []), { 123 | title: 'New Tab', 124 | icon: 'mdi:new-box', 125 | card: { type: 'markdown', content: '## New Tab Content' } 126 | }]; 127 | this._valueChanged({ ...this._config, tabs: newTabs }); 128 | } 129 | 130 | private _removeTab(index: number): void { 131 | if (!this._config) return; 132 | const newTabs = [...this._config.tabs]; 133 | newTabs.splice(index, 1); 134 | this._valueChanged({ ...this._config, tabs: newTabs }); 135 | } 136 | 137 | private _moveTab(index: number, direction: 'up' | 'down'): void { 138 | if (!this._config) return; 139 | const newTabs = [...this._config.tabs]; 140 | const [tab] = newTabs.splice(index, 1); 141 | const newIndex = direction === 'up' ? index - 1 : index + 1; 142 | newTabs.splice(newIndex, 0, tab); 143 | this._valueChanged({ ...this._config, tabs: newTabs }); 144 | } 145 | 146 | protected render(): TemplateResult { 147 | if (!this.hass || !this._config) { 148 | return html``; 149 | } 150 | 151 | return html` 152 |
Card content (Only YAML for now):
207 |