@record-evolution/widget-switch 1.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json ADDED
@@ -0,0 +1,69 @@
1
+ {
2
+ "name": "@record-evolution/widget-switch",
3
+ "description": "IronFlock Widget widget-switch",
4
+ "license": "MIT",
5
+ "author": "widget-switch",
6
+ "version": "1.0.1",
7
+ "engines": {
8
+ "node": "18.18.2",
9
+ "npm": ">=10.0.2"
10
+ },
11
+ "type": "module",
12
+ "main": "dist/widget-switch.js",
13
+ "types": "dist/src/widget-switch.d.ts",
14
+ "files": [
15
+ "dist",
16
+ "src"
17
+ ],
18
+ "scripts": {
19
+ "analyze": "cem analyze --litelement",
20
+ "start": "concurrently -k -r \"npm run watch\" \"wds\"",
21
+ "build": "rollup -c rollup.config.js",
22
+ "watch": "rollup -w -c rollup.config.js",
23
+ "link": "npm link && cd ../RESWARM/frontend && npm link @record-evolution/widget-switch",
24
+ "unlink": "npm unlink --global && cd ../RESWARM/frontend && npm unlink @record-evolution/widget-switch && npm i @record-evolution/widget-switch",
25
+ "types": "cat src/definition-schema.json | json2ts > src/definition-schema.d.ts",
26
+ "start:build": "npm run build && es-dev-server --root-dir dist --app-index index.html --compatibility none --open",
27
+ "release": "npm run build && npm run types && npm version patch --tag-version-prefix='' && git push && git push --tag"
28
+ },
29
+ "dependencies": {
30
+ "@material/web": "^2.3.0",
31
+ "lit": "^3.3.0"
32
+ },
33
+ "devDependencies": {
34
+ "@custom-elements-manifest/analyzer": "^0.10.4",
35
+ "@rollup/plugin-babel": "^6.0.4",
36
+ "@rollup/plugin-commonjs": "^28.0.6",
37
+ "@rollup/plugin-node-resolve": "^16.0.1",
38
+ "@rollup/plugin-replace": "^6.0.2",
39
+ "@rollup/plugin-typescript": "^12.1.3",
40
+ "@typescript-eslint/eslint-plugin": "^8.34.1",
41
+ "@typescript-eslint/parser": "^8.34.1",
42
+ "@web/dev-server": "^0.4.6",
43
+ "concurrently": "^9.1.2",
44
+ "deepmerge": "^4.3.1",
45
+ "es-dev-server": "^2.1.0",
46
+ "eslint": "^9.29.0",
47
+ "eslint-config-prettier": "^10.1.5",
48
+ "husky": "^9.1.7",
49
+ "json-schema-to-typescript": "^15.0.4",
50
+ "lint-staged": "^16.1.2",
51
+ "prettier": "^3.5.3",
52
+ "rimraf": "^6.0.1",
53
+ "rollup": "^4.43.0",
54
+ "tslib": "^2.8.1",
55
+ "typescript": "^5.8.3"
56
+ },
57
+ "repository": {
58
+ "type": "git",
59
+ "url": "git+https://github.com/RecordEvolution/widget-switch.git"
60
+ },
61
+ "keywords": [
62
+ "widget",
63
+ "switch"
64
+ ],
65
+ "bugs": {
66
+ "url": "https://github.com/RecordEvolution/widget-switch/issues"
67
+ },
68
+ "homepage": "https://github.com/RecordEvolution/widget-switch#readme"
69
+ }
@@ -0,0 +1,46 @@
1
+ {
2
+ "title": "Machine Switch Panel",
3
+ "subTitle": "Main Line Controls",
4
+ "dataseries": [
5
+ {
6
+ "label": "Lamp",
7
+ "stateMap": {
8
+ "on": ">1",
9
+ "off": "<=1"
10
+ },
11
+ "styling": {
12
+ "labelColor": "#000000",
13
+ "valueColor": "#00cc66"
14
+ },
15
+ "data": [
16
+ {
17
+ "tsp": "2025-07-15T10:00:00.000Z",
18
+ "value": 1,
19
+ "actionApp": "machineControlApp",
20
+ "actionDevice": 1,
21
+ "actionTopic": "lamp"
22
+ }
23
+ ]
24
+ },
25
+ {
26
+ "label": "Cooling",
27
+ "stateMap": {
28
+ "on": "COOL_ON,1",
29
+ "off": "COOL_OFF,0"
30
+ },
31
+ "styling": {
32
+ "labelColor": "#0033cc",
33
+ "valueColor": "#0099ff"
34
+ },
35
+ "data": [
36
+ {
37
+ "tsp": "2025-07-15T10:05:00.000Z",
38
+ "value": "COOL_ON",
39
+ "actionApp": "coolingSystemApp",
40
+ "actionDevice": 1,
41
+ "actionTopic": "cooling"
42
+ }
43
+ ]
44
+ }
45
+ ]
46
+ }
@@ -0,0 +1,91 @@
1
+ /* eslint-disable */
2
+ /**
3
+ * This file was automatically generated by json-schema-to-typescript.
4
+ * DO NOT MODIFY IT BY HAND. Instead, modify the source JSONSchema file,
5
+ * and run json-schema-to-typescript to regenerate this file.
6
+ */
7
+
8
+ export type Title = string;
9
+ export type Subtitle = string;
10
+ /**
11
+ * The label for the switch.
12
+ */
13
+ export type Label = string;
14
+ /**
15
+ * The value or list of values that represent the ON state. You can also use an expression like <4 or >33 to specify a numeric range. If a value matches an ON and an OFF state, the ON state will be used.
16
+ */
17
+ export type ONValues = string;
18
+ /**
19
+ * The value or list of values that represent the OFF state. You can also use an expression like <4 or >33 to specify a numeric range. If a value matches an ON and an OFF state, the ON state will be used.
20
+ */
21
+ export type OFFValues = string;
22
+ /**
23
+ * This should be an ISO String date like 2023-11-04T22:47:52.351152+00:00. Will only be used to detect age of data.
24
+ */
25
+ export type Timestamp = string;
26
+ export type Value = string;
27
+ /**
28
+ * The action topic that is called when clicking the switch. The arguments will be True for ON and False for OFF. Refer to the app documentation for available topics to call.
29
+ */
30
+ export type ActionTopic = string;
31
+ /**
32
+ * You can specify a column in the input data to autogenerate dataseries for each distinct entry in this column. E.g. if you have a table with columns [city, timestamp, temperature] and specify 'city' as split column, then you will get a value field for each city.
33
+ */
34
+ export type SplitDataBy = string;
35
+ /**
36
+ * The data for the switch state.
37
+ */
38
+ export type SwitchStateData = {
39
+ tsp?: Timestamp;
40
+ value?: Value;
41
+ actionApp?: ActionApp;
42
+ actionDevice?: ActionDeviceTarget;
43
+ actionTopic?: ActionTopic;
44
+ pivot?: SplitDataBy;
45
+ [k: string]: unknown;
46
+ }[];
47
+ export type Switches = {
48
+ label?: Label;
49
+ stateMap?: StateMap;
50
+ styling?: Styling;
51
+ data?: SwitchStateData;
52
+ [k: string]: unknown;
53
+ }[];
54
+
55
+ export interface InputData {
56
+ title?: Title;
57
+ subTitle?: Subtitle;
58
+ dataseries?: Switches;
59
+ [k: string]: unknown;
60
+ }
61
+ /**
62
+ * Specify the values for the ON and OFF states. Any value that does not match to either an ON or OFF state will be considered as UNKNOWN.
63
+ */
64
+ export interface StateMap {
65
+ on?: ONValues;
66
+ off?: OFFValues;
67
+ [k: string]: unknown;
68
+ }
69
+ export interface Styling {
70
+ labelColor?: LabelColor;
71
+ valueColor?: ValueColor;
72
+ [k: string]: unknown;
73
+ }
74
+ export interface LabelColor {
75
+ [k: string]: unknown;
76
+ }
77
+ export interface ValueColor {
78
+ [k: string]: unknown;
79
+ }
80
+ /**
81
+ * The app that should handle the click on the switch.
82
+ */
83
+ export interface ActionApp {
84
+ [k: string]: unknown;
85
+ }
86
+ /**
87
+ * The device that should handele the click on the switch. The 'Device ID' column can be used here when using data driven mode.
88
+ */
89
+ export interface ActionDeviceTarget {
90
+ [k: string]: unknown;
91
+ }
@@ -0,0 +1,121 @@
1
+ {
2
+ "title": "InputData",
3
+ "type": "object",
4
+ "properties": {
5
+ "title": {
6
+ "title": "Title",
7
+ "type": "string",
8
+ "order": 1
9
+ },
10
+ "subTitle": {
11
+ "title": "Subtitle",
12
+ "type": "string",
13
+ "order": 2
14
+ },
15
+ "dataseries": {
16
+ "title": "Switches",
17
+ "type": "array",
18
+ "dataDrivenDisabled": true,
19
+ "order": 2,
20
+ "items": {
21
+ "type": "object",
22
+ "properties": {
23
+ "label": {
24
+ "title": "Label",
25
+ "description": "The label for the switch.",
26
+ "type": "string",
27
+ "order": 1
28
+ },
29
+ "stateMap": {
30
+ "title": "State Map",
31
+ "description": "Specify the values for the ON and OFF states. Any value that does not match to either an ON or OFF state will be considered as UNKNOWN.",
32
+ "type": "object",
33
+ "required": true,
34
+ "order": 3,
35
+ "properties": {
36
+ "on": {
37
+ "title": "ON Values",
38
+ "description": "The value or list of values that represent the ON state. You can also use an expression like <4 or >33 to specify a numeric range. If a value matches an ON and an OFF state, the ON state will be used.",
39
+ "type": "string",
40
+ "required": true
41
+ },
42
+ "off": {
43
+ "title": "OFF Values",
44
+ "description": "The value or list of values that represent the OFF state. You can also use an expression like <4 or >33 to specify a numeric range. If a value matches an ON and an OFF state, the ON state will be used.",
45
+ "type": "string",
46
+ "required": true
47
+ }
48
+ }
49
+ },
50
+ "styling": {
51
+ "title": "Styling",
52
+ "description": "",
53
+ "type": "object",
54
+ "order": 6,
55
+ "properties": {
56
+ "labelColor": {
57
+ "title": "Label Color",
58
+ "type": "color",
59
+ "color": true,
60
+ "order": 4
61
+ },
62
+ "valueColor": {
63
+ "title": "Value Color",
64
+ "type": "color",
65
+ "color": true,
66
+ "order": 5
67
+ }
68
+ }
69
+ },
70
+ "data": {
71
+ "title": "Switch State Data",
72
+ "description": "The data for the switch state.",
73
+ "type": "array",
74
+ "order": 4,
75
+ "items": {
76
+ "type": "object",
77
+ "properties": {
78
+ "tsp": {
79
+ "title": "Timestamp",
80
+ "description": "This should be an ISO String date like 2023-11-04T22:47:52.351152+00:00. Will only be used to detect age of data.",
81
+ "type": "string",
82
+ "order": 1
83
+ },
84
+ "value": {
85
+ "title": "Value",
86
+ "type": "string",
87
+ "required": true,
88
+ "order": 2
89
+ },
90
+ "actionApp": {
91
+ "title": "Action App",
92
+ "description": "The app that should handle the click on the switch.",
93
+ "type": "actionApp",
94
+ "order": 3
95
+ },
96
+ "actionDevice": {
97
+ "title": "Action Device Target",
98
+ "description": "The device that should handele the click on the switch. The 'Device ID' column can be used here when using data driven mode.",
99
+ "type": "actionDevice",
100
+ "order": 4
101
+ },
102
+ "actionTopic": {
103
+ "title": "Action Topic",
104
+ "description": "The action topic that is called when clicking the switch. The arguments will be True for ON and False for OFF. Refer to the app documentation for available topics to call.",
105
+ "type": "string",
106
+ "order": 5
107
+ },
108
+ "pivot": {
109
+ "title": "Split Data by",
110
+ "description": "You can specify a column in the input data to autogenerate dataseries for each distinct entry in this column. E.g. if you have a table with columns [city, timestamp, temperature] and specify 'city' as split column, then you will get a value field for each city.",
111
+ "type": "string",
112
+ "order": 6
113
+ }
114
+ }
115
+ }
116
+ }
117
+ }
118
+ }
119
+ }
120
+ }
121
+ }
@@ -0,0 +1,307 @@
1
+ import { html, css, LitElement, PropertyValueMap } from 'lit'
2
+ import { repeat } from 'lit/directives/repeat.js'
3
+ import { customElement, property, state } from 'lit/decorators.js'
4
+ import { InputData } from './definition-schema.js'
5
+ import '@material/web/switch/switch.js'
6
+ import { MdSwitch } from '@material/web/switch/switch.js'
7
+
8
+ type Dataseries = Exclude<InputData['dataseries'], undefined>[number] & { needleValue?: number }
9
+ type Data = Exclude<Dataseries['data'], undefined>[number]
10
+ type Theme = {
11
+ theme_name: string
12
+ theme_object: any
13
+ }
14
+ @customElement('widget-switch-versionplaceholder')
15
+ export class WidgetSwitch extends LitElement {
16
+ @property({ type: Object })
17
+ inputData?: InputData
18
+
19
+ @property({ type: Object })
20
+ theme?: Theme
21
+
22
+ @state()
23
+ private dataSets: Map<string, Dataseries> = new Map()
24
+
25
+ @state()
26
+ private textActive: boolean = true
27
+
28
+ @state() private themeBgColor?: string
29
+ @state() private themeTitleColor?: string
30
+ @state() private themeSubtitleColor?: string
31
+
32
+ version: string = 'versionplaceholder'
33
+
34
+ private resizeObserver: ResizeObserver
35
+
36
+ valueContainer?: HTMLDivElement
37
+ boxes?: HTMLDivElement[]
38
+ origWidth: number = 0
39
+ origHeight: number = 0
40
+ constructor() {
41
+ super()
42
+ this.resizeObserver = new ResizeObserver(this.applyData.bind(this))
43
+ this.resizeObserver.observe(this)
44
+ }
45
+
46
+ disconnectedCallback() {
47
+ super.disconnectedCallback()
48
+ if (this.resizeObserver) {
49
+ this.resizeObserver.disconnect()
50
+ }
51
+ }
52
+
53
+ protected firstUpdated(_changedProperties: PropertyValueMap<any> | Map<PropertyKey, unknown>) {
54
+ this.valueContainer = this?.shadowRoot?.querySelector('.value-container') as HTMLDivElement
55
+
56
+ this.registerTheme(this.theme)
57
+ this.transformData()
58
+ this.applyData()
59
+ }
60
+
61
+ update(changedProperties: Map<string, any>) {
62
+ if (changedProperties.has('inputData')) {
63
+ this.transformData()
64
+ this.applyData()
65
+ }
66
+
67
+ if (changedProperties.has('theme')) {
68
+ this.registerTheme(this.theme)
69
+ }
70
+
71
+ super.update(changedProperties)
72
+ }
73
+
74
+ registerTheme(theme?: Theme) {
75
+ const cssTextColor = getComputedStyle(this).getPropertyValue('--re-text-color').trim()
76
+ const cssBgColor = getComputedStyle(this).getPropertyValue('--re-tile-background-color').trim()
77
+ this.themeBgColor = cssBgColor || this.theme?.theme_object?.backgroundColor
78
+ this.themeTitleColor = cssTextColor || this.theme?.theme_object?.title?.textStyle?.color
79
+ this.themeSubtitleColor =
80
+ cssTextColor || this.theme?.theme_object?.title?.subtextStyle?.color || this.themeTitleColor
81
+ }
82
+
83
+ applyData() {}
84
+
85
+ async transformData() {
86
+ if (!this.inputData) return
87
+ this.dataSets.forEach((d) => {
88
+ d.label ??= ''
89
+ })
90
+ this.dataSets = new Map()
91
+ this.inputData?.dataseries
92
+ // ?.sort((a, b) => a.order - b.order)
93
+ ?.forEach((ds) => {
94
+ // pivot data
95
+ const distincts = [...new Set(ds.data?.map((d: Data) => d.pivot))].sort() as string[]
96
+ ds.selected = undefined
97
+
98
+ distincts.forEach((piv) => {
99
+ const prefix = piv ?? ''
100
+ const label = ds.label ?? ''
101
+ const value =
102
+ distincts.length === 1 ? ds.data?.[0] : ds.data?.filter((d) => d.pivot === piv)?.[0]
103
+ const pds: Dataseries = {
104
+ label: prefix + (!!prefix && !!label ? ' - ' : '') + label,
105
+ actionApp: value?.actionApp,
106
+ actionDevice: value?.actionDevice,
107
+ actionTopic: value?.actionTopic,
108
+ styling: ds.styling,
109
+ selected: this.isSelected(ds, value)
110
+ }
111
+ this.dataSets.set(pds.label ?? '', pds)
112
+ })
113
+ })
114
+ // console.log('Value Datasets', this.dataSets)
115
+ }
116
+
117
+ isSelected(ds: Dataseries, value?: Data): boolean | undefined {
118
+ if (!value) return undefined
119
+ const on = ds?.stateMap?.on
120
+ if (on?.startsWith('<=')) {
121
+ const val = parseFloat(value.value ?? '')
122
+ const comp = parseFloat(on.substring(2))
123
+ return isNaN(val) || isNaN(comp) ? undefined : val <= comp
124
+ }
125
+ if (on?.startsWith('<')) {
126
+ const val = parseFloat(value.value ?? '')
127
+ const comp = parseFloat(on.substring(1))
128
+ return isNaN(val) || isNaN(comp) ? undefined : val < comp
129
+ }
130
+ if (on?.startsWith('>=')) {
131
+ const val = parseFloat(value.value ?? '')
132
+ const comp = parseFloat(on.substring(2))
133
+ return isNaN(val) || isNaN(comp) ? undefined : val >= comp
134
+ }
135
+ if (on?.startsWith('>')) {
136
+ const val = parseFloat(value.value ?? '')
137
+ const comp = parseFloat(on.substring(1))
138
+ return isNaN(val) || isNaN(comp) ? undefined : val > comp
139
+ }
140
+
141
+ const off = ds?.stateMap?.off
142
+ if (off?.startsWith('<=')) {
143
+ const val = parseFloat(value.value ?? '')
144
+ const comp = parseFloat(off.substring(2))
145
+ return isNaN(val) || isNaN(comp) ? undefined : val > comp
146
+ }
147
+ if (off?.startsWith('<')) {
148
+ const val = parseFloat(value.value ?? '')
149
+ const comp = parseFloat(off.substring(1))
150
+ return isNaN(val) || isNaN(comp) ? undefined : val <= comp
151
+ }
152
+ if (off?.startsWith('>=')) {
153
+ const val = parseFloat(value.value ?? '')
154
+ const comp = parseFloat(off.substring(2))
155
+ return isNaN(val) || isNaN(comp) ? undefined : val < comp
156
+ }
157
+ if (off?.startsWith('>')) {
158
+ const val = parseFloat(value.value ?? '')
159
+ const comp = parseFloat(off.substring(1))
160
+ return isNaN(val) || isNaN(comp) ? undefined : val <= comp
161
+ }
162
+
163
+ const onValues = on?.split(',').map((v) => v.trim().replace('"', '').replace("'", '')) ?? []
164
+ if (onValues.includes(String(value.value) ?? '')) return true
165
+
166
+ const offValues = off?.split(',').map((v) => v.trim().replace('"', '').replace("'", '')) ?? []
167
+ if (offValues.includes(String(value.value) ?? '')) return false
168
+
169
+ return undefined
170
+ }
171
+
172
+ handleActionSubmit(e: any) {
173
+ const switchEl = e.target
174
+ const payload = {
175
+ args: switchEl?.selected,
176
+ actionApp: switchEl?.actionApp,
177
+ actionDevice: switchEl?.actionDevice,
178
+ actionTopic: switchEl?.actionTopic,
179
+ label: switchEl?.label
180
+ }
181
+ console.log('Action Submit', payload)
182
+ this.dispatchEvent(
183
+ new CustomEvent('action-submit', {
184
+ detail: payload,
185
+ bubbles: false,
186
+ composed: false
187
+ })
188
+ )
189
+ }
190
+
191
+ static styles = css`
192
+ :host {
193
+ display: block;
194
+ font-family: sans-serif;
195
+ box-sizing: border-box;
196
+ position: relative;
197
+ margin: auto;
198
+ }
199
+
200
+ .paging:not([active]) {
201
+ display: none !important;
202
+ }
203
+
204
+ .wrapper {
205
+ display: flex;
206
+ flex-direction: column;
207
+ height: 100%;
208
+ width: 100%;
209
+ padding: 16px;
210
+ box-sizing: border-box;
211
+ }
212
+
213
+ h3 {
214
+ margin: 0;
215
+ max-width: 300px;
216
+ overflow: hidden;
217
+ text-overflow: ellipsis;
218
+ white-space: nowrap;
219
+ }
220
+ p {
221
+ margin: 10px 0 0 0;
222
+ max-width: 300px;
223
+ font-size: 14px;
224
+ line-height: 17px;
225
+ overflow: hidden;
226
+ text-overflow: ellipsis;
227
+ white-space: nowrap;
228
+ }
229
+
230
+ .value-container {
231
+ display: flex;
232
+ flex-wrap: wrap;
233
+ align-items: center;
234
+ justify-content: center;
235
+ line-height: 0.9;
236
+ flex: 1;
237
+ overflow: hidden;
238
+ position: relative;
239
+ gap: 12px;
240
+ }
241
+
242
+ .switch {
243
+ overflow: hidden;
244
+ display: flex;
245
+ align-items: center;
246
+ gap: 12px;
247
+ box-sizing: border-box;
248
+ /* border-left: 4px solid #ddd; */
249
+ }
250
+
251
+ .no-data {
252
+ font-size: 20px;
253
+ display: flex;
254
+ height: 100%;
255
+ width: 100%;
256
+ text-align: center;
257
+ align-items: center;
258
+ justify-content: center;
259
+ }
260
+ `
261
+
262
+ render() {
263
+ return html`
264
+ <div
265
+ class="wrapper"
266
+ style="background-color: ${this.themeBgColor}; color: ${this.themeTitleColor}"
267
+ >
268
+ <header>
269
+ <h3 class="paging" ?active=${this.inputData?.title}>${this.inputData?.title}</h3>
270
+ <p
271
+ class="paging"
272
+ ?active=${this.inputData?.subTitle}
273
+ style="color: ${this.themeSubtitleColor}"
274
+ >
275
+ ${this.inputData?.subTitle}
276
+ </p>
277
+ </header>
278
+
279
+ <div class="paging no-data" ?active=${!this.dataSets.size}>No Data</div>
280
+ <div class="value-container">
281
+ ${repeat(
282
+ [...this.dataSets.entries()].sort(),
283
+ ([label]) => label,
284
+ ([label, ds]) => {
285
+ return html`
286
+ <div class="switch" label="${label}">
287
+ ${label}
288
+ <label
289
+ ><md-switch
290
+ aria-label="${label}"
291
+ ?selected="${!!ds.selected}"
292
+ .actionApp="${ds?.actionApp}"
293
+ .actionDevice="${ds?.actionDevice}"
294
+ .actionTopic="${ds?.actionTopic}"
295
+ .label="${label}"
296
+ @change="${this.handleActionSubmit}"
297
+ ></md-switch
298
+ ></label>
299
+ </div>
300
+ `
301
+ }
302
+ )}
303
+ </div>
304
+ </div>
305
+ `
306
+ }
307
+ }