diff --git a/.github/ISSUE_TEMPLATE/bug-report.yml b/.github/ISSUE_TEMPLATE/bug-report.yml new file mode 100644 index 0000000..baae8b4 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug-report.yml @@ -0,0 +1,77 @@ +name: Bug Report +description: For reporting bugs or unexpected behavior +title: "[Bug]: " +body: + - type: textarea + id: ha_version + attributes: + label: My Home Assistant Version + description: You can view your Home Assistant Version in _Settings > About_ + placeholder: | + Core: 2025.10.3 + Frontend: 20251001.2 + Flipdown-timer-card: 1.0.1 + validations: + required: true + - type: dropdown + id: lovelace + attributes: + label: My lovelace configuration method + multiple: false + options: + - GUI + - YAML + validations: + required: true + - type: textarea + id: what_i_am_doing + attributes: + label: What I am doing + - type: textarea + id: what_i_expect_to_happen + attributes: + label: What I expect to happen + - type: textarea + id: what_happened_instead + attributes: + label: What happened instead + - type: textarea + id: minimal_steps_to_reproduce + attributes: + label: Minimal steps to reproduce + value: | + 1. + 2. + 3. + ... + - type: textarea + id: minimal_code + attributes: + label: Include any yaml code here + placeholder: Paste YAML code + render: YAML + - type: textarea + id: console_errors + attributes: + label: Error messages from the browser console + description: Select everything from the browser console and copy and paste below + placeholder: Paste console errors + render: console + - type: markdown + attributes: + value: '---' + - type: checkboxes + id: checks + attributes: + label: By checking each box below I indicate that I ... + options: + - label: Understand that this is a channel for reporting bugs, not a support forum (https://community.home-assistant.io/). + required: true + - label: Have made sure I am using the latest version of the plugin. + required: true + - label: Have followed the troubleshooting steps of the "Common Problems" section of https://github.com/thomasloven/hass-config/wiki/Lovelace-Plugins. + required: true + - label: Understand that failure to follow the template above may increase the time required to handle my bug-report, or cause it to be closed without further action. + required: true + + diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 0000000..ec4bb38 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1 @@ +blank_issues_enabled: false \ No newline at end of file diff --git a/.github/ISSUE_TEMPLATE/feature-request.md b/.github/ISSUE_TEMPLATE/feature-request.md new file mode 100644 index 0000000..510eb00 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature-request.md @@ -0,0 +1,7 @@ +--- +name: Feature request +about: For suggesting new features +title: '[FR]: ' +labels: 'feature request' +assignees: '' +--- diff --git a/CHANGELOG.md b/CHANGELOG.md index 7791b89..1ce1634 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,27 @@ +## [1.1.0-dev.2](https://github.com/dcapslock/flipdown-timer-card/compare/v1.1.0-dev.1...v1.1.0-dev.2) (2025-11-07) + +### Features + +* Add Flipdown CSS vars for theming ([#8](https://github.com/dcapslock/flipdown-timer-card/issues/8)) ([a7ed5b1](https://github.com/dcapslock/flipdown-timer-card/commit/a7ed5b15e5589c1cf9ccbeb190e27028d0023dc7)) + +### Bug Fixes + +* Apply header text shadow for light and dark thems to allow text to be shown even if theme out of place. ([#9](https://github.com/dcapslock/flipdown-timer-card/issues/9)) ([17226e4](https://github.com/dcapslock/flipdown-timer-card/commit/17226e4f3328cb8830e2b50abd5a44271bdc68e1)) + +### Documentation + +* Flipdown CSS variable added to README ([#10](https://github.com/dcapslock/flipdown-timer-card/issues/10)) ([2c1236d](https://github.com/dcapslock/flipdown-timer-card/commit/2c1236d94d399cf895cd7471fb43025debda40ec)) + +## [1.1.0-dev.1](https://github.com/dcapslock/flipdown-timer-card/compare/v1.0.1...v1.1.0-dev.1) (2025-11-06) + +### Features + +* Modern GUI Editor ([#6](https://github.com/dcapslock/flipdown-timer-card/issues/6)) ([bfe3fea](https://github.com/dcapslock/flipdown-timer-card/commit/bfe3fea202ae9eef3b904d715269fb8dd042a46c)) + +### Documentation + +* Change duration type from string to object ([7b7b9f6](https://github.com/dcapslock/flipdown-timer-card/commit/7b7b9f67e337094977a2d58587e01fcdfdf95c62)) + ## [1.0.1](https://github.com/dcapslock/flipdown-timer-card/compare/v1.0.0...v1.0.1) (2025-11-05) ### Bug Fixes diff --git a/README.md b/README.md index 0b51ae0..e22f4ba 100644 --- a/README.md +++ b/README.md @@ -19,17 +19,17 @@ Card for timer entities in the Lovelace user interface of Home Assistant ![Defau ## Configuration -| Name | Type | Requirement | Description | Default | -| ----------- | ------- | ------------ | -------------------------------------------------------- | ------- | -| type | string | **Required** | `custom:flipdown-timer-card` | | -| entity | string | **Required** | Timer, Input_datetime(with both date and time) entity | | -| duration | string | **Optional** | Timer duration indicated when idle. Should be 'hh:mm:ss' | | -| theme | string | **Optional** | Colorscheme `hass`, `dark`, `light` | `hass` | -| show_title | boolean | **Optional** | Show card title | `false` | -| show_header | boolean | **Optional** | Show rotor headings | `false` | -| show_hour | string | **Optional** | Show hour rotors `true`, `false`, `auto` | `false` | -| styles | object | **Optional** | Card style | | -| localize | object | **Optional** | Card text localization | | +| Name | Type | Requirement | Description | Default | +| --- | --- | --- | --- | --- | +| type | string | **Required** | `custom:flipdown-timer-card` | | +| entity | string | **Required** | Timer, Input_datetime(with both date and time) entity | | +| duration | object | **Optional** | Timer duration indicated when idle. Object with attributes `hours`, `minutes`, `seconds` | | +| theme | string | **Optional** | Colorscheme `hass`, `dark`, `light` | `hass` | +| show_title | boolean | **Optional** | Show card title | `false` | +| show_header | boolean | **Optional** | Show rotor headings | `false` | +| show_hour | string | **Optional** | Show hour rotors `true`, `false`, `auto` | `false` | +| styles | object | **Optional** | Card style | | +| localize | object | **Optional** | Card text localization | | ### **Duration** @@ -114,7 +114,10 @@ show_hour: false show_title: false show_header: false theme: dark -duration: '00:00:00' +duration: + - hours: 0 + - minutes: 0 + - seconds: 0 localize: button: 시작, 정지, 취소, 계속, 리셋 header: 시, 분, 초 @@ -130,6 +133,25 @@ styles: location: bottom ``` +## Flipdown CSS variables + +The following CSS variables are available to use in Home Assistant themes or to apply to `ha-card` via card-mod. These will override any Rotor style, Button style or Theme set in Flipdown Timer Card config. + +| Variable | Applies to | Valid value | Overrides | +| --- | --- | --- | --- | +| `--flipdown-primary-color` | Text color for rotor top segments, buttons, header | CSS color | Theme config | +| `--flipdown-primary-background-color` | Background color for rotor top segments, buttons, delimiters, hinge | CSS color | Theme config | +| `--flipdown-secondary-color` | Text color for rotor bottom segments | CSS color | Theme config | +| `--flipdown-secondary-background-color` | Background color for rotor bottom segments | CSS color | Theme config | +| `--flipdown-header-text-shadow` | Text shadow used for header to allow for header to be visible in most scenarios. | Valid CSS text shadow. Defaults to `1px 1px 0px var(--flipdown-primary-background-color, )` | Theme config | +| `--flipdown-rotor-space` | Space between rotors | CSS size | Rotor space config | +| `--flipdown-rotor-height` | Height of rotor | CSS size | Rotor height config | +| `--flipdown-rotor-width` | Width of rotor | CSS size | Rotor width config | +| `--flipdown-rotor-fontsize` | Rotor font size | CSS font size | Rotor fontsize config | +| `--flipdown-button-width` | Button width | CSS size | Button width config | +| `--flipdown-button-height` | Button height. Only for when buttons are at bottom location. Otherwise button height is relative to rotor height | CSS size | Button height config | +| `--flipdown-button-fontsize` | Button font size | CSS font size | Button fontsize config | + ## Notes - Timing error(<1s) may occur due to flipping effect. @@ -138,3 +160,4 @@ styles: - Original Flipdown Timer Card by [pmongloid](https://github.com/pmongloid/flipdown-timer-card) - This card is based on the work of [@PButcher/flipdown](https://github.com/PButcher/flipdown) and [@iantrich](https://github.com/iantrich)'s boilerplate card +- This repository uses Build, CI and Release configuration based on [button-card](https://github.com/custom-cards/button-card) by [@RomRider](https://github.com/RomRider) diff --git a/package.json b/package.json index da4ea85..c8a1bfe 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "flipdown-timer-card", - "version": "1.0.1", + "version": "1.1.0-dev.2", "description": "Flipdown timer card for lovelace", "main": "dist/flipdown-timer-card.js", "type": "module", diff --git a/src/editor.ts b/src/editor.ts index 4b40579..cac39d9 100755 --- a/src/editor.ts +++ b/src/editor.ts @@ -1,260 +1,279 @@ -import { LitElement, html, TemplateResult, css, CSSResultGroup } from 'lit'; -import { HomeAssistant, fireEvent, LovelaceCardEditor, ActionConfig } from 'custom-card-helpers'; +import { LitElement, html, TemplateResult, css } from 'lit'; +import { HomeAssistant, fireEvent, LovelaceCardEditor } from 'custom-card-helpers'; import { FlipdownTimerCardConfig } from './types'; import { customElement, property, state } from 'lit/decorators.js'; -const options = { - required: { - icon: 'tune', - name: 'Required', - secondary: 'Required options for this card to function', - show: true, +const configSchema = [ + { + name: 'entity', + label: 'Entity', + selector: { + entity: { + domain: ['timer', 'input_datetime', 'sensor'], + }, + }, + }, + { + name: 'name', + label: 'Name', + selector: { entity_name: {} }, + context: { entity: 'entity' }, + }, + { + name: 'duration', + label: 'Timer duration indicated when idle', + selector: { duration: {} }, }, - actions: { - icon: 'gesture-tap-hold', - name: 'Actions', - secondary: 'Perform actions based on tapping/clicking', - show: false, - options: { - tap: { - icon: 'gesture-tap', - name: 'Tap', - secondary: 'Set the action to perform on tap', - show: false, + { + type: 'grid', + schema: [ + { + name: 'show_title', + label: 'Show card title', + selector: { boolean: {} }, }, - hold: { - icon: 'gesture-tap-hold', - name: 'Hold', - secondary: 'Set the action to perform on hold', - show: false, + { + name: 'show_header', + label: 'Show rotor headings', + selector: { boolean: {} }, }, - double_tap: { - icon: 'gesture-double-tap', - name: 'Double Tap', - secondary: 'Set the action to perform on double tap', - show: false, + { + name: 'show_hour', + label: 'Show hour rotors', + selector: { + select: { + mode: 'dropdown', + options: [ + { value: 'true', label: 'Show' }, + { value: 'false', label: 'Hide' }, + { value: 'auto', label: 'Auto' }, + ], + }, + }, }, - }, + { + name: 'theme', + label: 'Theme', + selector: { + select: { + mode: 'dropdown', + options: [ + { value: 'hass', label: 'Hass' }, + { value: 'dark', label: 'Dark' }, + { value: 'light', label: 'Light' }, + ], + }, + }, + }, + ], + }, +]; + +const rotorStyleSchema = [ + { + type: 'expandable', + label: 'Rotor style', + icon: 'mdi:flip-vertical', + schema: [ + { + type: 'grid', + schema: [ + { + name: 'width', + label: 'Rotor width', + selector: { text: {} }, + }, + { + name: 'height', + label: 'Rotor height', + selector: { text: {} }, + }, + { + name: 'space', + label: 'Space between rotors', + selector: { text: {} }, + }, + { + name: 'fontsize', + label: 'Rotor font size', + selector: { text: {} }, + }, + ], + }, + ], }, - appearance: { - icon: 'palette', - name: 'Appearance', - secondary: 'Customize the name, icon, etc', - show: false, +]; + +const buttonStyleSchema = [ + { + type: 'expandable', + label: 'Button style', + icon: 'mdi:button-pointer', + schema: [ + { + type: 'grid', + schema: [ + { + name: 'width', + label: 'Button width', + selector: { text: {} }, + }, + { + name: 'height', + label: 'Button height', + selector: { text: {} }, + }, + { + name: 'fontsize', + label: 'Button font size', + selector: { text: {} }, + }, + { + name: 'location', + label: 'Location', + selector: { + select: { + mode: 'dropdown', + options: [ + { value: 'right', label: 'Right' }, + { value: 'bottom', label: 'Bottom' }, + { value: 'hide', label: 'Hide' }, + ], + }, + }, + }, + ], + }, + ], + }, +]; + +const localizeSchema = [ + { + type: 'expandable', + label: 'Localize', + icon: 'mdi:translate', + schema: [ + { + name: 'button', + selector: { text: {} }, + }, + { + name: 'header', + selector: { text: {} }, + }, + ], }, -}; +]; @customElement('flipdown-timer-card-editor') export class FlipdownTimerCardEditor extends LitElement implements LovelaceCardEditor { @property({ attribute: false }) public hass?: HomeAssistant; @state() private _config?: FlipdownTimerCardConfig; - @state() private _toggle?: boolean; - @state() private _helpers?: any; - private _initialized = false; public setConfig(config: FlipdownTimerCardConfig): void { this._config = config; + let configUpgraded: boolean = false; - this.loadCardHelpers(); - } - - protected shouldUpdate(): boolean { - if (!this._initialized) { - this._initialize(); + // Upgrade duration from string to object if needed + if (typeof this._config.duration === 'string') { + const [hours, minutes, seconds] = (this._config.duration as string).split(':').map(Number); + this._config.duration = { hours, minutes, seconds }; + configUpgraded = true; } - return true; - } - - get _name(): string { - return this._config?.name || ''; - } - - get _entity(): string { - return this._config?.entity || ''; + if (configUpgraded) { + fireEvent(this, 'config-changed', { config: this._config }); + } } - get _show_title(): boolean { - return this._config?.show_title || false; - } + private _configChanged(ev: CustomEvent): void { + ev.stopPropagation(); + const config = ev.detail.value; - get _show_error(): boolean { - return this._config?.show_error || false; + this._config = config as FlipdownTimerCardConfig; + fireEvent(this, 'config-changed', { config }); } - get _tap_action(): ActionConfig { - return this._config?.tap_action || { action: 'more-info' }; + private _rotorStyleConfigChanged(ev: CustomEvent): void { + ev.stopPropagation(); + if (!this._config) return; + const config = ev.detail.value; + const rotorStyle = { rotor: config }; + const styles = this._config.styles || {}; + this._config.styles = { ...styles, ...rotorStyle }; + fireEvent(this, 'config-changed', { config: this._config }); } - get _hold_action(): ActionConfig { - return this._config?.hold_action || { action: 'none' }; + private _buttonStyleConfigChanged(ev: CustomEvent): void { + ev.stopPropagation(); + if (!this._config) return; + const config = ev.detail.value; + const buttonStyle = { button: config }; + const styles = this._config.styles || {}; + this._config.styles = { ...styles, ...buttonStyle }; + fireEvent(this, 'config-changed', { config: this._config }); } - get _double_tap_action(): ActionConfig { - return this._config?.double_tap_action || { action: 'none' }; + private _localizeConfigChanged(ev: CustomEvent): void { + ev.stopPropagation(); + if (!this._config) return; + const config = ev.detail.value; + const localize = { localize: config }; + this._config = { ...this._config, ...localize }; + fireEvent(this, 'config-changed', { config: this._config }); } protected render(): TemplateResult | void { - if (!this.hass || !this._helpers) { + if (!this.hass || !this._config) { return html``; } - // The climate more-info has ha-switch and paper-dropdown-menu elements that are lazy loaded unless explicitly done here - this._helpers.importMoreInfoControl('climate'); - - // You can restrict on domain type - const entities = Object.keys(this.hass.states).filter( - (eid) => - eid.substr(0, eid.indexOf('.')) === 'timer' || - eid.substr(0, eid.indexOf('.')) === 'input_datetime' || - eid.substr(0, eid.indexOf('.')) === 'sensor', - ); - return html` -
-
-
- -
${options.required.name}
-
-
${options.required.secondary}
-
- ${options.required.show - ? html` -
- - - ${entities.map((entity) => { - return html` ${entity} `; - })} - - -
- ` - : ''} - -
-
- -
${options.appearance.name}
-
-
${options.appearance.secondary}
-
- ${options.appearance.show - ? html` -
- -
- - - - - - -
- ` - : ''} +
+ s.label ?? s.name} + @value-changed=${this._configChanged} + > +
+
+ s.label ?? s.name} + @value-changed=${this._rotorStyleConfigChanged} + > +
+
+ s.label ?? s.name} + @value-changed=${this._buttonStyleConfigChanged} + > +
+
+ s.label ?? s.name} + @value-changed=${this._localizeConfigChanged} + >
`; } - private _initialize(): void { - if (this.hass === undefined) return; - if (this._config === undefined) return; - if (this._helpers === undefined) return; - this._initialized = true; - } - - private async loadCardHelpers(): Promise { - this._helpers = await (window as any).loadCardHelpers(); - } - - private _toggleAction(ev): void { - this._toggleThing(ev, options.actions.options); - } - - private _toggleOption(ev): void { - this._toggleThing(ev, options); - } - - private _toggleThing(ev, optionList): void { - const show = !optionList[ev.target.option].show; - for (const [key] of Object.entries(optionList)) { - optionList[key].show = false; - } - optionList[ev.target.option].show = show; - this._toggle = !this._toggle; - } - - private _valueChanged(ev): void { - if (!this._config || !this.hass) { - return; - } - const target = ev.target; - if (this[`_${target.configValue}`] === target.value) { - return; - } - if (target.configValue) { - if (target.value === '') { - const tmpConfig = { ...this._config }; - delete tmpConfig[target.configValue]; - this._config = tmpConfig; - } else { - this._config = { - ...this._config, - [target.configValue]: target.checked !== undefined ? target.checked : target.value, - }; - } - } - fireEvent(this, 'config-changed', { config: this._config }); - } - - static get styles(): CSSResultGroup { + static get styles() { return css` - .option { - padding: 4px 0px; - cursor: pointer; - } - .row { - display: flex; - margin-bottom: -14px; - pointer-events: none; - } - .title { - padding-left: 16px; - margin-top: -6px; - pointer-events: none; - } - .secondary { - padding-left: 40px; - color: var(--secondary-text-color); - pointer-events: none; - } - .values { - padding-left: 16px; - background: var(--secondary-background-color); - display: grid; - } - ha-formfield { - padding-bottom: 8px; + .form-editor { + margin-bottom: 16px; } `; } diff --git a/src/flipdown-timer-card.ts b/src/flipdown-timer-card.ts index 6f2ef13..60e8297 100755 --- a/src/flipdown-timer-card.ts +++ b/src/flipdown-timer-card.ts @@ -1,6 +1,6 @@ import { LitElement, html, TemplateResult, PropertyValues, CSSResultGroup, unsafeCSS } from 'lit'; import { customElement, property, state } from 'lit/decorators.js'; -import { HomeAssistant, hasConfigOrEntityChanged, LovelaceCardEditor, getLovelace } from 'custom-card-helpers'; // This is a community maintained npm module with common helper functions/types +import { HomeAssistant, hasConfigOrEntityChanged, LovelaceCardEditor } from 'custom-card-helpers'; // This is a community maintained npm module with common helper functions/types import './editor'; @@ -48,19 +48,30 @@ export class FlipdownTimer extends LitElement { // https://lit-element.polymer-project.org/guide/properties#accessors-custom public setConfig(config: FlipdownTimerCardConfig): void { - // TODO Check for required fields and that they are of the proper format if (!config) { throw new Error(localize('common.invalid_configuration')); } - if (config.test_gui) { - getLovelace().setEditMode(true); - } - this.config = { ...config, }; + // Upgrade duration from string to object + if (typeof this.config.duration === 'string') { + const [hours, minutes, seconds] = (this.config.duration as string).split(':').map(Number); + this.config.duration = { hours, minutes, seconds }; + } + + // Normalise show_hour from string + if (typeof this.config.show_hour === 'string') { + this.config.show_hour = + this.config.show_hour.toLowerCase() === 'true' + ? true + : this.config.show_hour.toLowerCase() === 'auto' + ? 'auto' + : false; + } + let localizeBtn = ['start', 'stop', 'cancel', 'resume', 'reset']; let localizeHeader = ['Hours', 'Minutes', 'Seconds']; @@ -85,8 +96,8 @@ export class FlipdownTimer extends LitElement { if (!this.config.styles) { this.config.styles = { - rotor: false, - button: false, + rotor: undefined, + button: undefined, }; } } @@ -169,7 +180,13 @@ export class FlipdownTimer extends LitElement { protected _reset(): void { const state = this.hass.states[this.config.entity!]; - const duration = durationToSeconds(this.config.duration ? this.config.duration : state.attributes.duration); + const configDuration: string | boolean = + this.config.duration === undefined + ? false + : `${this.config.duration?.hours ? String(this.config.duration.hours).padStart(2, '0') : '00'} + :${this.config.duration?.minutes ? String(this.config.duration.minutes).padStart(2, '0') : '00'} + :${this.config.duration?.seconds ? String(this.config.duration.seconds).padStart(2, '0') : '00'}`; + const duration = durationToSeconds(configDuration || state.attributes.duration); this.fd.rt = duration; this.fd._tick(true); } @@ -210,15 +227,15 @@ export class FlipdownTimer extends LitElement {