@record-evolution/widget-sidenav 1.0.12 → 1.0.14

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/README.md CHANGED
@@ -1,35 +1,145 @@
1
- # \<widget-sidenav>
1
+ # widget-sidenav
2
2
 
3
- This webcomponent follows the [open-wc](https://github.com/open-wc/open-wc) recommendation.
3
+ IronFlock widget: A Lit 3.x web component that renders a vertical side navigation menu with Material Design icons. Supports route-based highlighting and emits navigation events for integration with SPA routers.
4
+
5
+ ![screenshot](thumbnail.png)
4
6
 
5
7
  ## Installation
6
8
 
7
9
  ```bash
8
- npm i widget-sidenav
10
+ npm i @record-evolution/widget-sidenav
9
11
  ```
10
12
 
13
+ **Peer Dependencies:** This widget requires `@material/web` to be available at runtime via import map or bundled separately.
14
+
11
15
  ## Usage
12
16
 
13
17
  ```html
14
18
  <script type="module">
15
- import 'widget-sidenav/widget-sidenav.js'
19
+ import '@record-evolution/widget-sidenav'
16
20
  </script>
17
21
 
18
- <widget-sidenav></widget-sidenav>
22
+ <widget-sidenav-1.0.12></widget-sidenav-1.0.12>
23
+ ```
24
+
25
+ > **Note:** The element tag includes the version number (e.g., `widget-sidenav-1.0.12`). This is replaced at build time via `@rollup/plugin-replace`.
26
+
27
+ ## Configuration
28
+
29
+ The widget accepts an `inputData` property with the following structure:
30
+
31
+ ```typescript
32
+ interface InputData {
33
+ title?: string
34
+ route?: string // Navigation route for title click
35
+ leadingSlash?: boolean // Add leading slash to routes
36
+ trailingSlash?: boolean // Add trailing slash to routes
37
+ style?: {
38
+ fontSize?: number
39
+ fontWeight?: number
40
+ color?: string // Font color
41
+ backgroundColor?: string
42
+ }
43
+ navItems?: Array<{
44
+ label?: string
45
+ iconName?: string // Material icon name (e.g., "home", "settings")
46
+ route?: string // Navigation route on click
47
+ leadingSlash?: boolean
48
+ trailingSlash?: boolean
49
+ }>
50
+ }
51
+ ```
52
+
53
+ ## Example Configuration
54
+
55
+ ```javascript
56
+ element.inputData = {
57
+ title: 'Dashboard',
58
+ route: '/',
59
+ style: {
60
+ fontSize: 16,
61
+ fontWeight: 500
62
+ },
63
+ navItems: [
64
+ { label: 'Home', iconName: 'home', route: '/' },
65
+ { label: 'Analytics', iconName: 'analytics', route: '/analytics' },
66
+ { label: 'Settings', iconName: 'settings', route: '/settings' }
67
+ ]
68
+ }
69
+ ```
70
+
71
+ ## Navigation Events
72
+
73
+ When a navigation item is clicked, the widget dispatches a `nav-submit` custom event:
74
+
75
+ ```javascript
76
+ element.addEventListener('nav-submit', (e) => {
77
+ console.log(e.detail)
78
+ // { path: "/settings" }
79
+ })
19
80
  ```
20
81
 
21
- ## Expected data format
82
+ ## Route Matching
22
83
 
23
- ## Tooling configs
84
+ The widget highlights the active navigation item based on the current route:
24
85
 
25
- For most of the tools, the configuration is in the `package.json` to reduce the amount of files in your project.
86
+ ```javascript
87
+ element.route = '/settings/profile' // Highlights "Settings" nav item
88
+ ```
89
+
90
+ - Routes starting with `/` are matched from the beginning (absolute)
91
+ - Other routes are matched from the end (relative)
92
+
93
+ ## Material Icons
94
+
95
+ Icons are rendered using `@material/web` icon component. Use icon names from [Google Material Symbols](https://fonts.google.com/icons):
96
+
97
+ ```javascript
98
+ {
99
+ iconName: 'home'
100
+ } // Home icon
101
+ {
102
+ iconName: 'settings'
103
+ } // Settings gear icon
104
+ {
105
+ iconName: 'person'
106
+ } // User profile icon
107
+ ```
108
+
109
+ ## Theming
26
110
 
27
- If you customize the configuration a lot, you can consider moving them to individual files.
111
+ The widget supports theming via CSS custom properties or the `theme` property:
28
112
 
29
- ## Local Demo with `web-dev-server`
113
+ **CSS Custom Properties:**
114
+
115
+ ```css
116
+ widget-sidenav-1.0.12 {
117
+ --re-text-color: #333;
118
+ --re-tile-background-color: #fff;
119
+ }
120
+ ```
121
+
122
+ **Theme Object:**
123
+
124
+ ```javascript
125
+ element.theme = {
126
+ theme_name: 'dark',
127
+ theme_object: {
128
+ backgroundColor: '#1a1a1a',
129
+ title: { textStyle: { color: '#fff' } }
130
+ }
131
+ }
132
+ ```
133
+
134
+ ## Development
30
135
 
31
136
  ```bash
32
- npm start
137
+ npm run start # Dev server at localhost:8000/demo/
138
+ npm run build # Production build to dist/
139
+ npm run types # Regenerate TypeScript types from schema
140
+ npm run release # Build, bump version, push to git with tag
33
141
  ```
34
142
 
35
- To run a local development server that serves the basic demo located in `demo/index.html`
143
+ ## License
144
+
145
+ MIT
@@ -1,15 +1,15 @@
1
- import { css as d, LitElement as u, html as p } from "lit";
2
- import { property as h, state as c, customElement as g } from "lit/decorators.js";
3
- import { repeat as m } from "lit/directives/repeat.js";
1
+ import { css as u, LitElement as d, html as p } from "lit";
2
+ import { property as h, state as c, customElement as m } from "lit/decorators.js";
3
+ import { repeat as g } from "lit/directives/repeat.js";
4
4
  import "@material/web/icon/icon.js";
5
- var f = Object.defineProperty, y = Object.getOwnPropertyDescriptor, a = (t, e, o, i) => {
6
- for (var r = i > 1 ? void 0 : i ? y(e, o) : e, n = t.length - 1, l; n >= 0; n--)
7
- (l = t[n]) && (r = (i ? l(e, o, r) : l(r)) || r);
8
- return i && r && f(e, o, r), r;
5
+ var f = Object.defineProperty, v = Object.getOwnPropertyDescriptor, n = (t, e, i, o) => {
6
+ for (var r = o > 1 ? void 0 : o ? v(e, i) : e, l = t.length - 1, a; l >= 0; l--)
7
+ (a = t[l]) && (r = (o ? a(e, i, r) : a(r)) || r);
8
+ return o && r && f(e, i, r), r;
9
9
  };
10
- let s = class extends u {
10
+ let s = class extends d {
11
11
  constructor() {
12
- super(...arguments), this.version = "1.0.12";
12
+ super(...arguments), this.version = "1.0.14";
13
13
  }
14
14
  update(t) {
15
15
  t.has("theme") && this.registerTheme(this.theme), super.update(t);
@@ -18,8 +18,8 @@ let s = class extends u {
18
18
  this.registerTheme(this.theme);
19
19
  }
20
20
  registerTheme(t) {
21
- const e = getComputedStyle(this).getPropertyValue("--re-text-color").trim(), o = getComputedStyle(this).getPropertyValue("--re-tile-background-color").trim();
22
- this.themeBgColor = o || this.theme?.theme_object?.backgroundColor, this.themeTitleColor = e || this.theme?.theme_object?.title?.textStyle?.color;
21
+ const e = getComputedStyle(this).getPropertyValue("--re-text-color").trim(), i = getComputedStyle(this).getPropertyValue("--re-tile-background-color").trim();
22
+ this.themeBgColor = i || this.theme?.theme_object?.backgroundColor, this.themeTitleColor = e || this.theme?.theme_object?.title?.textStyle?.color;
23
23
  }
24
24
  handleNavItemClick(t) {
25
25
  const e = new CustomEvent("nav-submit", {
@@ -32,17 +32,27 @@ let s = class extends u {
32
32
  trimRoute(t) {
33
33
  return t = String(t), t ? "/" + t.split("/").filter(Boolean).join("/") : "";
34
34
  }
35
- addSlashes(t) {
36
- return t.route = String(t.route), !t.route?.endsWith("/") && t.trailingSlash && (t.route += "/"), !t.route?.startsWith("/") && t.leadingSlash && (t.route = "/" + t.route), t.route;
35
+ resolveRoute(t) {
36
+ let e = String(t?.route ?? "");
37
+ if (t?.variables)
38
+ for (const i of t.variables)
39
+ i.label && (e = e.split(`{{${i.label}}}`).join(encodeURIComponent(String(i.value ?? ""))));
40
+ if (e.includes("*")) {
41
+ const i = (this.route || "").split("/").filter(Boolean), o = e.split("/").filter(Boolean);
42
+ for (let r = 0; r < o.length; r++)
43
+ o[r] === "*" && (o[r] = i[r] ?? "");
44
+ e = (e.startsWith("/") ? "/" : "") + o.filter(Boolean).join("/");
45
+ }
46
+ return e;
37
47
  }
38
48
  matchesRoute(t) {
39
49
  if (t === void 0) return !1;
40
50
  t = String(t);
41
- const e = this.trimRoute(decodeURIComponent(this.route || "/")), o = this.trimRoute(t);
42
- return t.startsWith("/") ? e.startsWith(o) : e.endsWith(o) || e === o;
51
+ const e = this.trimRoute(decodeURIComponent(this.route || "/")), i = this.trimRoute(t);
52
+ return t.startsWith("/") ? e.startsWith(i) : e.endsWith(i) || e === i;
43
53
  }
44
54
  render() {
45
- const t = this.inputData?.style?.fontSize ?? 16, e = (this.inputData?.style?.fontSize ?? 16) * 1.5, o = t * 0.4;
55
+ const t = this.inputData?.style?.fontSize ?? 16, e = (this.inputData?.style?.fontSize ?? 16) * 1.5, i = t * 0.4;
46
56
  return p`
47
57
  <div
48
58
  class="wrapper"
@@ -54,27 +64,32 @@ let s = class extends u {
54
64
  class="paging"
55
65
  style=${this.inputData?.route ? "cursor: pointer" : ""}
56
66
  ?active=${this.inputData?.title}
57
- @click=${() => this.handleNavItemClick(this.addSlashes(this.inputData))}
67
+ @click=${() => this.handleNavItemClick(
68
+ this.resolveRoute({
69
+ route: this.inputData?.route,
70
+ variables: this.inputData?.variables
71
+ })
72
+ )}
58
73
  >
59
74
  ${this.inputData?.title}
60
75
  </h2>
61
76
  <div class="nav-list">
62
- ${m(
77
+ ${g(
63
78
  this.inputData?.navItems || [],
64
- (i) => i.label,
65
- (i) => p`
79
+ (o) => o.label,
80
+ (o) => p`
66
81
  <div
67
- class="nav-item ${this.matchesRoute(this.addSlashes(i)) ? "selected" : ""}"
68
- style="gap: ${o}px;"
69
- @click=${() => this.handleNavItemClick(this.addSlashes(i))}
82
+ class="nav-item ${this.matchesRoute(this.resolveRoute(o)) ? "selected" : ""}"
83
+ style="gap: ${i}px;"
84
+ @click=${() => this.handleNavItemClick(this.resolveRoute(o))}
70
85
  >
71
- ${i.iconName ? p`
86
+ ${o.iconName ? p`
72
87
  <md-icon
73
88
  style="font-size: ${e}px;width: ${e}px;"
74
- >${i.iconName}
89
+ >${o.iconName}
75
90
  </md-icon>
76
91
  ` : ""}
77
- ${i.label}
92
+ ${o.label}
78
93
  </div>
79
94
  `
80
95
  )}
@@ -83,7 +98,7 @@ let s = class extends u {
83
98
  `;
84
99
  }
85
100
  };
86
- s.styles = d`
101
+ s.styles = u`
87
102
  :host {
88
103
  display: block;
89
104
  font-family: sans-serif;
@@ -136,23 +151,23 @@ s.styles = d`
136
151
  font-size: 1.2em;
137
152
  }
138
153
  `;
139
- a([
154
+ n([
140
155
  h({ type: Object })
141
156
  ], s.prototype, "inputData", 2);
142
- a([
157
+ n([
143
158
  h({ type: Object })
144
159
  ], s.prototype, "theme", 2);
145
- a([
160
+ n([
146
161
  h({ type: String })
147
162
  ], s.prototype, "route", 2);
148
- a([
163
+ n([
149
164
  c()
150
165
  ], s.prototype, "themeBgColor", 2);
151
- a([
166
+ n([
152
167
  c()
153
168
  ], s.prototype, "themeTitleColor", 2);
154
- s = a([
155
- g("widget-sidenav-1.0.12")
169
+ s = n([
170
+ m("widget-sidenav-1.0.14")
156
171
  ], s);
157
172
  export {
158
173
  s as WidgetSidenav
@@ -1 +1 @@
1
- {"version":3,"file":"widget-sidenav.js","sources":["../src/widget-sidenav.ts"],"sourcesContent":["import { html, css, LitElement, PropertyValues } from 'lit'\nimport { customElement, property, state } from 'lit/decorators.js'\nimport { repeat } from 'lit/directives/repeat.js'\nimport { InputData } from './definition-schema'\n\nimport '@material/web/icon/icon.js'\n\ntype Theme = {\n theme_name: string\n theme_object: any\n}\n@customElement('widget-sidenav-versionplaceholder')\nexport class WidgetSidenav extends LitElement {\n @property({ type: Object }) inputData?: InputData\n @property({ type: Object }) theme?: Theme\n @property({ type: String }) route?: string\n\n @state() private themeBgColor?: string\n @state() private themeTitleColor?: string\n\n version: string = 'versionplaceholder'\n\n update(changedProperties: Map<string, any>) {\n if (changedProperties.has('theme')) {\n this.registerTheme(this.theme)\n }\n\n super.update(changedProperties)\n }\n\n protected firstUpdated(_changedProperties: PropertyValues): void {\n this.registerTheme(this.theme)\n }\n\n registerTheme(theme?: Theme) {\n const cssTextColor = getComputedStyle(this).getPropertyValue('--re-text-color').trim()\n const cssBgColor = getComputedStyle(this).getPropertyValue('--re-tile-background-color').trim()\n this.themeBgColor = cssBgColor || this.theme?.theme_object?.backgroundColor\n this.themeTitleColor = cssTextColor || this.theme?.theme_object?.title?.textStyle?.color\n }\n\n handleNavItemClick(route?: string) {\n // console.log('Navigating to:', item.route)\n const event = new CustomEvent('nav-submit', {\n detail: { path: String(route) },\n bubbles: true,\n composed: true\n })\n this.dispatchEvent(event)\n }\n\n trimRoute(route?: string) {\n route = String(route)\n if (!route) return ''\n return '/' + route.split('/').filter(Boolean).join('/')\n }\n\n addSlashes(item?: any): string | undefined {\n item.route = String(item.route)\n if (!item.route?.endsWith('/') && item.trailingSlash) {\n item.route += '/'\n }\n if (!item.route?.startsWith('/') && item.leadingSlash) {\n item.route = '/' + item.route\n }\n return item.route\n }\n\n matchesRoute(itemRoute?: string) {\n if (itemRoute === undefined) return false\n itemRoute = String(itemRoute)\n const route = this.trimRoute(decodeURIComponent(this.route || '/'))\n const subRoute = this.trimRoute(itemRoute)\n if (itemRoute.startsWith('/')) {\n return route.startsWith(subRoute)\n } else {\n return route.endsWith(subRoute) || route === subRoute\n }\n }\n\n static styles = css`\n :host {\n display: block;\n font-family: sans-serif;\n box-sizing: border-box;\n margin: auto;\n }\n\n .paging:not([active]) {\n display: none !important;\n }\n\n .wrapper {\n display: flex;\n flex-direction: column;\n height: 100%;\n width: 100%;\n padding: 12px;\n box-sizing: border-box;\n }\n\n .nav-list {\n display: flex;\n flex-direction: column;\n gap: 2px;\n margin-top: 12px;\n overflow-y: auto;\n flex-grow: 1;\n }\n\n .nav-item {\n cursor: pointer;\n display: flex;\n align-items: center;\n gap: 8px;\n padding: 8px;\n border-radius: 4px;\n }\n\n md-icon {\n font-family: 'Material Symbols Outlined';\n }\n\n .selected {\n background-color: rgba(0, 0, 0, 0.1);\n }\n\n h2 {\n margin: 0;\n padding: 0px;\n font-size: 1.2em;\n }\n `\n\n render() {\n const fontSize = this.inputData?.style?.fontSize ?? 16\n const iconFontSize = (this.inputData?.style?.fontSize ?? 16) * 1.5\n const gap = fontSize * 0.4\n return html`\n <div\n class=\"wrapper\"\n style=\"background-color: ${this.inputData?.style?.backgroundColor ||\n this.themeBgColor}; color: ${this.inputData?.style?.color || this.themeTitleColor};\n font-weight: ${this.inputData?.style?.fontWeight};\n font-size: ${fontSize}px;\"\n >\n <h2\n class=\"paging\"\n style=${this.inputData?.route ? 'cursor: pointer' : ''}\n ?active=${this.inputData?.title}\n @click=${() => this.handleNavItemClick(this.addSlashes(this.inputData))}\n >\n ${this.inputData?.title}\n </h2>\n <div class=\"nav-list\">\n ${repeat(\n this.inputData?.navItems || [],\n (item) => item.label,\n (item) => html`\n <div\n class=\"nav-item ${this.matchesRoute(this.addSlashes(item)) ? 'selected' : ''}\"\n style=\"gap: ${gap}px;\"\n @click=${() => this.handleNavItemClick(this.addSlashes(item))}\n >\n ${item.iconName\n ? html`\n <md-icon\n style=\"font-size: ${iconFontSize}px;width: ${iconFontSize}px;\"\n >${item.iconName}\n </md-icon>\n `\n : ''}\n ${item.label}\n </div>\n `\n )}\n </div>\n </div>\n `\n }\n}\n"],"names":["WidgetSidenav","LitElement","changedProperties","_changedProperties","theme","cssTextColor","cssBgColor","route","event","item","itemRoute","subRoute","fontSize","iconFontSize","gap","html","repeat","css","__decorateClass","property","state","customElement"],"mappings":"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"}
1
+ {"version":3,"file":"widget-sidenav.js","sources":["../src/widget-sidenav.ts"],"sourcesContent":["import { html, css, LitElement, PropertyValues } from 'lit'\nimport { customElement, property, state } from 'lit/decorators.js'\nimport { repeat } from 'lit/directives/repeat.js'\nimport { InputData } from './definition-schema'\n\nimport '@material/web/icon/icon.js'\n\ntype Theme = {\n theme_name: string\n theme_object: any\n}\n@customElement('widget-sidenav-versionplaceholder')\nexport class WidgetSidenav extends LitElement {\n @property({ type: Object }) inputData?: InputData\n @property({ type: Object }) theme?: Theme\n @property({ type: String }) route?: string\n\n @state() private themeBgColor?: string\n @state() private themeTitleColor?: string\n\n version: string = 'versionplaceholder'\n\n update(changedProperties: Map<string, any>) {\n if (changedProperties.has('theme')) {\n this.registerTheme(this.theme)\n }\n\n super.update(changedProperties)\n }\n\n protected firstUpdated(_changedProperties: PropertyValues): void {\n this.registerTheme(this.theme)\n }\n\n registerTheme(theme?: Theme) {\n const cssTextColor = getComputedStyle(this).getPropertyValue('--re-text-color').trim()\n const cssBgColor = getComputedStyle(this).getPropertyValue('--re-tile-background-color').trim()\n this.themeBgColor = cssBgColor || this.theme?.theme_object?.backgroundColor\n this.themeTitleColor = cssTextColor || this.theme?.theme_object?.title?.textStyle?.color\n }\n\n handleNavItemClick(route?: string) {\n // console.log('Navigating to:', item.route)\n const event = new CustomEvent('nav-submit', {\n detail: { path: String(route) },\n bubbles: true,\n composed: true\n })\n this.dispatchEvent(event)\n }\n\n trimRoute(route?: string) {\n route = String(route)\n if (!route) return ''\n return '/' + route.split('/').filter(Boolean).join('/')\n }\n\n resolveRoute(item?: any): string | undefined {\n let route = String(item?.route ?? '')\n if (item?.variables) {\n for (const variable of item.variables) {\n if (variable.label) {\n route = route\n .split(`{{${variable.label}}}`)\n .join(encodeURIComponent(String(variable.value ?? '')))\n }\n }\n }\n if (route.includes('*')) {\n const currentSegments = (this.route || '').split('/').filter(Boolean)\n const routeSegments = route.split('/').filter(Boolean)\n for (let i = 0; i < routeSegments.length; i++) {\n if (routeSegments[i] === '*') {\n routeSegments[i] = currentSegments[i] ?? ''\n }\n }\n route = (route.startsWith('/') ? '/' : '') + routeSegments.filter(Boolean).join('/')\n }\n return route\n }\n\n matchesRoute(itemRoute?: string) {\n if (itemRoute === undefined) return false\n itemRoute = String(itemRoute)\n const route = this.trimRoute(decodeURIComponent(this.route || '/'))\n const subRoute = this.trimRoute(itemRoute)\n if (itemRoute.startsWith('/')) {\n return route.startsWith(subRoute)\n } else {\n return route.endsWith(subRoute) || route === subRoute\n }\n }\n\n static styles = css`\n :host {\n display: block;\n font-family: sans-serif;\n box-sizing: border-box;\n margin: auto;\n }\n\n .paging:not([active]) {\n display: none !important;\n }\n\n .wrapper {\n display: flex;\n flex-direction: column;\n height: 100%;\n width: 100%;\n padding: 12px;\n box-sizing: border-box;\n }\n\n .nav-list {\n display: flex;\n flex-direction: column;\n gap: 2px;\n margin-top: 12px;\n overflow-y: auto;\n flex-grow: 1;\n }\n\n .nav-item {\n cursor: pointer;\n display: flex;\n align-items: center;\n gap: 8px;\n padding: 8px;\n border-radius: 4px;\n }\n\n md-icon {\n font-family: 'Material Symbols Outlined';\n }\n\n .selected {\n background-color: rgba(0, 0, 0, 0.1);\n }\n\n h2 {\n margin: 0;\n padding: 0px;\n font-size: 1.2em;\n }\n `\n\n render() {\n const fontSize = this.inputData?.style?.fontSize ?? 16\n const iconFontSize = (this.inputData?.style?.fontSize ?? 16) * 1.5\n const gap = fontSize * 0.4\n return html`\n <div\n class=\"wrapper\"\n style=\"background-color: ${this.inputData?.style?.backgroundColor ||\n this.themeBgColor}; color: ${this.inputData?.style?.color || this.themeTitleColor};\n font-weight: ${this.inputData?.style?.fontWeight};\n font-size: ${fontSize}px;\"\n >\n <h2\n class=\"paging\"\n style=${this.inputData?.route ? 'cursor: pointer' : ''}\n ?active=${this.inputData?.title}\n @click=${() =>\n this.handleNavItemClick(\n this.resolveRoute({\n route: this.inputData?.route,\n variables: this.inputData?.variables\n })\n )}\n >\n ${this.inputData?.title}\n </h2>\n <div class=\"nav-list\">\n ${repeat(\n this.inputData?.navItems || [],\n (item) => item.label,\n (item) => html`\n <div\n class=\"nav-item ${this.matchesRoute(this.resolveRoute(item))\n ? 'selected'\n : ''}\"\n style=\"gap: ${gap}px;\"\n @click=${() => this.handleNavItemClick(this.resolveRoute(item))}\n >\n ${item.iconName\n ? html`\n <md-icon\n style=\"font-size: ${iconFontSize}px;width: ${iconFontSize}px;\"\n >${item.iconName}\n </md-icon>\n `\n : ''}\n ${item.label}\n </div>\n `\n )}\n </div>\n </div>\n `\n }\n}\n"],"names":["WidgetSidenav","LitElement","changedProperties","_changedProperties","theme","cssTextColor","cssBgColor","route","event","item","variable","currentSegments","routeSegments","i","itemRoute","subRoute","fontSize","iconFontSize","gap","html","repeat","css","__decorateClass","property","state","customElement"],"mappings":"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"}
package/package.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "description": "Webcomponent widget-sidenav following open-wc recommendations",
4
4
  "license": "MIT",
5
5
  "author": "widget-sidenav",
6
- "version": "1.0.12",
6
+ "version": "1.0.14",
7
7
  "engines": {
8
8
  "node": ">=24.9.0",
9
9
  "npm": ">=10.0.2"
@@ -5,56 +5,91 @@
5
5
  * and run json-schema-to-typescript to regenerate this file.
6
6
  */
7
7
 
8
+ /**
9
+ * The header text displayed at the top of the side navigation, typically used for branding or section identification.
10
+ */
8
11
  export type TitleSettings = string;
9
12
  /**
10
- * Navigation to perform on title click. Example: If you are at route /my/route/asset then 'settings/profile' routes to '/my/route/settings/profile'.
13
+ * The destination path when the title/header is clicked. Use for navigating to a home or main page. Use relative paths or enable leadingSlash for absolute routing.
11
14
  */
12
15
  export type TitleNavigation = string;
13
16
  /**
14
- * Whether to add a leading slash to the navigation route.
17
+ * The variable name used in the route string. Reference this in your route using the {{label}} syntax. Use descriptive names like 'temperature', 'status', 'deviceName'.
18
+ */
19
+ export type Label = string;
20
+ /**
21
+ * The replacement text that will appear in place of {{label}} in the rendered output. Can be a static value or bound to a data column for dynamic updates.
15
22
  */
16
- export type AddLeadingSlash = boolean;
23
+ export type Value = string;
17
24
  /**
18
- * Whether to add a trailing slash to the navigation route.
25
+ * Array of variable definitions for dynamic route segment substitution. Each variable has a label (used in the route string as {{label}}) and a value (the replacement text). Values can be static or bound to data columns for real-time updates.
26
+ */
27
+ export type TitleNavigationVariables = {
28
+ label?: Label;
29
+ value?: Value;
30
+ [k: string]: unknown;
31
+ }[];
32
+ /**
33
+ * Font size in pixels for navigation item labels.
19
34
  */
20
- export type AddTraillingSlash = boolean;
21
35
  export type FontSize = number;
36
+ /**
37
+ * Font weight for navigation items (100=thin, 400=normal, 700=bold, 900=black).
38
+ */
22
39
  export type FontWeight = number;
23
- export type Label = string;
24
40
  /**
25
- * Name of the icon from https://fonts.google.com/icons
41
+ * The text displayed for this navigation item. Should clearly describe the destination section.
42
+ */
43
+ export type Label1 = string;
44
+ /**
45
+ * Material icon name displayed alongside the label. Find icon names at https://fonts.google.com/icons (e.g., 'dashboard', 'settings', 'analytics', 'people').
26
46
  */
27
47
  export type IconName = string;
28
48
  /**
29
- * Navigation to perform on click. Example: If you are at route /my/route/asset then 'settings/profile' routes to '/my/route/settings/profile'.
49
+ * The destination path when this item is clicked. Supports two dynamic substitution mechanisms: (1) Variable placeholders using double curly braces — define variables in the 'Variables' array below and reference them as {{variableName}} in the route string (e.g., '/device/{{deviceId}}/details' where a variable with label 'deviceId' provides the value). (2) Wildcard segments using a single asterisk '*' as a full path segment — each '*' is replaced at runtime by the segment at the same position from the current browser route (e.g., '/* /settings' navigates to the settings page while preserving the first segment of the current URL). The '*' must be the entire segment to be substituted; partial wildcards like 'test*' are treated as literal text. Routes starting with '/' are absolute; routes without a leading slash are relative to the current location.
30
50
  */
31
51
  export type NavigationRoute = string;
32
52
  /**
33
- * Whether to add a leading slash to the navigation route.
53
+ * The variable name used in the route string. Reference this in your route using the {{label}} syntax. Use descriptive names like 'temperature', 'status', 'deviceName'.
34
54
  */
35
- export type AddLeadingSlash1 = boolean;
55
+ export type Label2 = string;
36
56
  /**
37
- * Whether to add a trailing slash to the navigation route.
57
+ * The replacement text that will appear in place of {{label}} in the rendered output. Can be a static value or bound to a data column for dynamic updates.
58
+ */
59
+ export type Value1 = string;
60
+ /**
61
+ * Array of variable definitions for dynamic route segment substitution. Each variable has a label (used in the route string as {{label}}) and a value (the replacement text). Values can be static or bound to data columns for real-time updates.
62
+ */
63
+ export type NavigationVariables = {
64
+ label?: Label2;
65
+ value?: Value1;
66
+ [k: string]: unknown;
67
+ }[];
68
+ /**
69
+ * Array of navigation links displayed vertically in the sidebar. Each item can have a label, icon, and destination route.
38
70
  */
39
- export type AddTraillingSlash1 = boolean;
40
71
  export type NavigationItems = {
41
- label?: Label;
72
+ label?: Label1;
42
73
  iconName?: IconName;
43
74
  route?: NavigationRoute;
44
- leadingSlash?: AddLeadingSlash1;
45
- trailingSlash?: AddTraillingSlash1;
75
+ variables?: NavigationVariables;
46
76
  [k: string]: unknown;
47
77
  }[];
48
78
 
79
+ /**
80
+ * A vertical side navigation widget for creating sidebar menus in dashboards. Use this widget to provide hierarchical or list-based navigation along the side of the dashboard. Features a clickable title/header and multiple navigation items with icons. Supports customizable styling for fonts and colors to match your dashboard theme. Ideal for applications with multiple sections, settings pages, or when you need persistent navigation that doesn't take up header space.
81
+ */
49
82
  export interface InputData {
50
83
  title?: TitleSettings;
51
84
  route?: TitleNavigation;
52
- leadingSlash?: AddLeadingSlash;
53
- trailingSlash?: AddTraillingSlash;
85
+ variables?: TitleNavigationVariables;
54
86
  style?: ItemStyle;
55
87
  navItems?: NavigationItems;
56
88
  [k: string]: unknown;
57
89
  }
90
+ /**
91
+ * Global styling options applied to all navigation items in the sidebar.
92
+ */
58
93
  export interface ItemStyle {
59
94
  fontSize?: FontSize;
60
95
  fontWeight?: FontWeight;
@@ -62,9 +97,15 @@ export interface ItemStyle {
62
97
  backgroundColor?: BackgroundColor;
63
98
  [k: string]: unknown;
64
99
  }
100
+ /**
101
+ * Text color for navigation item labels and icons.
102
+ */
65
103
  export interface FontColor {
66
104
  [k: string]: unknown;
67
105
  }
106
+ /**
107
+ * Background color of the side navigation panel.
108
+ */
68
109
  export interface BackgroundColor {
69
110
  [k: string]: unknown;
70
111
  }
@@ -1,57 +1,74 @@
1
1
  {
2
2
  "title": "Input Data",
3
+ "description": "A vertical side navigation widget for creating sidebar menus in dashboards. Use this widget to provide hierarchical or list-based navigation along the side of the dashboard. Features a clickable title/header and multiple navigation items with icons. Supports customizable styling for fonts and colors to match your dashboard theme. Ideal for applications with multiple sections, settings pages, or when you need persistent navigation that doesn't take up header space.",
3
4
  "type": "object",
4
5
  "properties": {
5
6
  "title": {
6
7
  "title": "Title Settings",
8
+ "description": "The header text displayed at the top of the side navigation, typically used for branding or section identification.",
7
9
  "order": 1,
8
10
  "type": "string"
9
11
  },
10
12
  "route": {
11
13
  "title": "Title Navigation",
12
14
  "order": 3,
13
- "description": "Navigation to perform on title click. Example: If you are at route /my/route/asset then 'settings/profile' routes to '/my/route/settings/profile'.",
15
+ "description": "The destination path when the title/header is clicked. Use for navigating to a home or main page. Use relative paths or enable leadingSlash for absolute routing.",
14
16
  "type": "string"
15
17
  },
16
- "leadingSlash": {
17
- "title": "Add Leading Slash",
18
- "order": 4,
19
- "description": "Whether to add a leading slash to the navigation route.",
20
- "dataDrivenDisabled": true,
21
- "type": "boolean"
22
- },
23
- "trailingSlash": {
24
- "title": "Add Trailling Slash",
25
- "order": 5,
26
- "description": "Whether to add a trailing slash to the navigation route.",
18
+ "variables": {
19
+ "title": "Title Navigation Variables",
20
+ "description": "Array of variable definitions for dynamic route segment substitution. Each variable has a label (used in the route string as {{label}}) and a value (the replacement text). Values can be static or bound to data columns for real-time updates.",
21
+ "type": "array",
27
22
  "dataDrivenDisabled": true,
28
- "type": "boolean"
23
+ "order": 4,
24
+ "items": {
25
+ "type": "object",
26
+ "properties": {
27
+ "label": {
28
+ "title": "Label",
29
+ "description": "The variable name used in the route string. Reference this in your route using the {{label}} syntax. Use descriptive names like 'temperature', 'status', 'deviceName'.",
30
+ "type": "string",
31
+ "order": 1
32
+ },
33
+ "value": {
34
+ "title": "Value",
35
+ "description": "The replacement text that will appear in place of {{label}} in the rendered output. Can be a static value or bound to a data column for dynamic updates.",
36
+ "type": "string",
37
+ "order": 2
38
+ }
39
+ }
40
+ }
29
41
  },
30
42
  "style": {
31
43
  "title": "Item Style",
44
+ "description": "Global styling options applied to all navigation items in the sidebar.",
32
45
  "order": 6,
33
46
  "type": "object",
34
47
  "properties": {
35
48
  "fontSize": {
36
49
  "title": "Font Size",
50
+ "description": "Font size in pixels for navigation item labels.",
37
51
  "order": 1,
38
52
  "dataDrivenDisabled": true,
39
53
  "type": "number"
40
54
  },
41
55
  "fontWeight": {
42
56
  "title": "Font Weight",
57
+ "description": "Font weight for navigation items (100=thin, 400=normal, 700=bold, 900=black).",
43
58
  "order": 3,
44
59
  "dataDrivenDisabled": true,
45
60
  "type": "number"
46
61
  },
47
62
  "color": {
48
63
  "title": "Font Color",
64
+ "description": "Text color for navigation item labels and icons.",
49
65
  "order": 4,
50
66
  "type": "color",
51
67
  "dataDrivenDisabled": true
52
68
  },
53
69
  "backgroundColor": {
54
70
  "title": "Background Color",
71
+ "description": "Background color of the side navigation panel.",
55
72
  "order": 4,
56
73
  "type": "color",
57
74
  "dataDrivenDisabled": true
@@ -60,6 +77,7 @@
60
77
  },
61
78
  "navItems": {
62
79
  "title": "Navigation Items",
80
+ "description": "Array of navigation links displayed vertically in the sidebar. Each item can have a label, icon, and destination route.",
63
81
  "order": 7,
64
82
  "type": "array",
65
83
  "items": {
@@ -67,35 +85,46 @@
67
85
  "properties": {
68
86
  "label": {
69
87
  "title": "Label",
88
+ "description": "The text displayed for this navigation item. Should clearly describe the destination section.",
70
89
  "order": 1,
71
90
  "type": "string"
72
91
  },
73
92
  "iconName": {
74
93
  "title": "Icon Name",
75
94
  "order": 2,
76
- "description": "Name of the icon from https://fonts.google.com/icons",
95
+ "description": "Material icon name displayed alongside the label. Find icon names at https://fonts.google.com/icons (e.g., 'dashboard', 'settings', 'analytics', 'people').",
77
96
  "dataDrivenDisabled": true,
78
97
  "type": "string"
79
98
  },
80
99
  "route": {
81
100
  "title": "Navigation Route",
82
101
  "order": 3,
83
- "description": "Navigation to perform on click. Example: If you are at route /my/route/asset then 'settings/profile' routes to '/my/route/settings/profile'.",
102
+ "description": "The destination path when this item is clicked. Supports two dynamic substitution mechanisms: (1) Variable placeholders using double curly braces — define variables in the 'Variables' array below and reference them as {{variableName}} in the route string (e.g., '/device/{{deviceId}}/details' where a variable with label 'deviceId' provides the value). (2) Wildcard segments using a single asterisk '*' as a full path segment — each '*' is replaced at runtime by the segment at the same position from the current browser route (e.g., '/*/settings' navigates to the settings page while preserving the first segment of the current URL). The '*' must be the entire segment to be substituted; partial wildcards like 'test*' are treated as literal text. Routes starting with '/' are absolute; routes without a leading slash are relative to the current location.",
84
103
  "type": "string"
85
104
  },
86
- "leadingSlash": {
87
- "title": "Add Leading Slash",
88
- "order": 4,
89
- "description": "Whether to add a leading slash to the navigation route.",
90
- "dataDrivenDisabled": true,
91
- "type": "boolean"
92
- },
93
- "trailingSlash": {
94
- "title": "Add Trailling Slash",
95
- "order": 5,
96
- "description": "Whether to add a trailing slash to the navigation route.",
105
+ "variables": {
106
+ "title": "Navigation Variables",
107
+ "description": "Array of variable definitions for dynamic route segment substitution. Each variable has a label (used in the route string as {{label}}) and a value (the replacement text). Values can be static or bound to data columns for real-time updates.",
108
+ "type": "array",
97
109
  "dataDrivenDisabled": true,
98
- "type": "boolean"
110
+ "order": 4,
111
+ "items": {
112
+ "type": "object",
113
+ "properties": {
114
+ "label": {
115
+ "title": "Label",
116
+ "description": "The variable name used in the route string. Reference this in your route using the {{label}} syntax. Use descriptive names like 'temperature', 'status', 'deviceName'.",
117
+ "type": "string",
118
+ "order": 1
119
+ },
120
+ "value": {
121
+ "title": "Value",
122
+ "description": "The replacement text that will appear in place of {{label}} in the rendered output. Can be a static value or bound to a data column for dynamic updates.",
123
+ "type": "string",
124
+ "order": 2
125
+ }
126
+ }
127
+ }
99
128
  }
100
129
  }
101
130
  }
@@ -55,15 +55,28 @@ export class WidgetSidenav extends LitElement {
55
55
  return '/' + route.split('/').filter(Boolean).join('/')
56
56
  }
57
57
 
58
- addSlashes(item?: any): string | undefined {
59
- item.route = String(item.route)
60
- if (!item.route?.endsWith('/') && item.trailingSlash) {
61
- item.route += '/'
58
+ resolveRoute(item?: any): string | undefined {
59
+ let route = String(item?.route ?? '')
60
+ if (item?.variables) {
61
+ for (const variable of item.variables) {
62
+ if (variable.label) {
63
+ route = route
64
+ .split(`{{${variable.label}}}`)
65
+ .join(encodeURIComponent(String(variable.value ?? '')))
66
+ }
67
+ }
62
68
  }
63
- if (!item.route?.startsWith('/') && item.leadingSlash) {
64
- item.route = '/' + item.route
69
+ if (route.includes('*')) {
70
+ const currentSegments = (this.route || '').split('/').filter(Boolean)
71
+ const routeSegments = route.split('/').filter(Boolean)
72
+ for (let i = 0; i < routeSegments.length; i++) {
73
+ if (routeSegments[i] === '*') {
74
+ routeSegments[i] = currentSegments[i] ?? ''
75
+ }
76
+ }
77
+ route = (route.startsWith('/') ? '/' : '') + routeSegments.filter(Boolean).join('/')
65
78
  }
66
- return item.route
79
+ return route
67
80
  }
68
81
 
69
82
  matchesRoute(itemRoute?: string) {
@@ -148,7 +161,13 @@ export class WidgetSidenav extends LitElement {
148
161
  class="paging"
149
162
  style=${this.inputData?.route ? 'cursor: pointer' : ''}
150
163
  ?active=${this.inputData?.title}
151
- @click=${() => this.handleNavItemClick(this.addSlashes(this.inputData))}
164
+ @click=${() =>
165
+ this.handleNavItemClick(
166
+ this.resolveRoute({
167
+ route: this.inputData?.route,
168
+ variables: this.inputData?.variables
169
+ })
170
+ )}
152
171
  >
153
172
  ${this.inputData?.title}
154
173
  </h2>
@@ -158,9 +177,11 @@ export class WidgetSidenav extends LitElement {
158
177
  (item) => item.label,
159
178
  (item) => html`
160
179
  <div
161
- class="nav-item ${this.matchesRoute(this.addSlashes(item)) ? 'selected' : ''}"
180
+ class="nav-item ${this.matchesRoute(this.resolveRoute(item))
181
+ ? 'selected'
182
+ : ''}"
162
183
  style="gap: ${gap}px;"
163
- @click=${() => this.handleNavItemClick(this.addSlashes(item))}
184
+ @click=${() => this.handleNavItemClick(this.resolveRoute(item))}
164
185
  >
165
186
  ${item.iconName
166
187
  ? html`