@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 +122 -12
- package/dist/widget-sidenav.js +49 -34
- package/dist/widget-sidenav.js.map +1 -1
- package/package.json +1 -1
- package/src/definition-schema.d.ts +58 -17
- package/src/definition-schema.json +56 -27
- package/src/widget-sidenav.ts +31 -10
package/README.md
CHANGED
|
@@ -1,35 +1,145 @@
|
|
|
1
|
-
#
|
|
1
|
+
# widget-sidenav
|
|
2
2
|
|
|
3
|
-
|
|
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
|
+

|
|
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 '
|
|
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
|
-
##
|
|
82
|
+
## Route Matching
|
|
22
83
|
|
|
23
|
-
|
|
84
|
+
The widget highlights the active navigation item based on the current route:
|
|
24
85
|
|
|
25
|
-
|
|
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
|
-
|
|
111
|
+
The widget supports theming via CSS custom properties or the `theme` property:
|
|
28
112
|
|
|
29
|
-
|
|
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
|
-
|
|
143
|
+
## License
|
|
144
|
+
|
|
145
|
+
MIT
|
package/dist/widget-sidenav.js
CHANGED
|
@@ -1,15 +1,15 @@
|
|
|
1
|
-
import { css as
|
|
2
|
-
import { property as h, state as c, customElement as
|
|
3
|
-
import { repeat as
|
|
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,
|
|
6
|
-
for (var r =
|
|
7
|
-
(
|
|
8
|
-
return
|
|
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
|
|
10
|
+
let s = class extends d {
|
|
11
11
|
constructor() {
|
|
12
|
-
super(...arguments), this.version = "1.0.
|
|
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(),
|
|
22
|
-
this.themeBgColor =
|
|
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
|
-
|
|
36
|
-
|
|
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 || "/")),
|
|
42
|
-
return t.startsWith("/") ? e.startsWith(
|
|
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,
|
|
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(
|
|
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
|
-
${
|
|
77
|
+
${g(
|
|
63
78
|
this.inputData?.navItems || [],
|
|
64
|
-
(
|
|
65
|
-
(
|
|
79
|
+
(o) => o.label,
|
|
80
|
+
(o) => p`
|
|
66
81
|
<div
|
|
67
|
-
class="nav-item ${this.matchesRoute(this.
|
|
68
|
-
style="gap: ${
|
|
69
|
-
@click=${() => this.handleNavItemClick(this.
|
|
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
|
-
${
|
|
86
|
+
${o.iconName ? p`
|
|
72
87
|
<md-icon
|
|
73
88
|
style="font-size: ${e}px;width: ${e}px;"
|
|
74
|
-
>${
|
|
89
|
+
>${o.iconName}
|
|
75
90
|
</md-icon>
|
|
76
91
|
` : ""}
|
|
77
|
-
${
|
|
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 =
|
|
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
|
-
|
|
154
|
+
n([
|
|
140
155
|
h({ type: Object })
|
|
141
156
|
], s.prototype, "inputData", 2);
|
|
142
|
-
|
|
157
|
+
n([
|
|
143
158
|
h({ type: Object })
|
|
144
159
|
], s.prototype, "theme", 2);
|
|
145
|
-
|
|
160
|
+
n([
|
|
146
161
|
h({ type: String })
|
|
147
162
|
], s.prototype, "route", 2);
|
|
148
|
-
|
|
163
|
+
n([
|
|
149
164
|
c()
|
|
150
165
|
], s.prototype, "themeBgColor", 2);
|
|
151
|
-
|
|
166
|
+
n([
|
|
152
167
|
c()
|
|
153
168
|
], s.prototype, "themeTitleColor", 2);
|
|
154
|
-
s =
|
|
155
|
-
|
|
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
|
@@ -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
|
-
*
|
|
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
|
-
*
|
|
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
|
|
23
|
+
export type Value = string;
|
|
17
24
|
/**
|
|
18
|
-
*
|
|
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
|
-
*
|
|
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
|
-
*
|
|
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
|
-
*
|
|
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
|
|
55
|
+
export type Label2 = string;
|
|
36
56
|
/**
|
|
37
|
-
*
|
|
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?:
|
|
72
|
+
label?: Label1;
|
|
42
73
|
iconName?: IconName;
|
|
43
74
|
route?: NavigationRoute;
|
|
44
|
-
|
|
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
|
-
|
|
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": "
|
|
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
|
-
"
|
|
17
|
-
"title": "
|
|
18
|
-
"
|
|
19
|
-
"
|
|
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
|
-
"
|
|
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": "
|
|
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": "
|
|
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
|
-
"
|
|
87
|
-
"title": "
|
|
88
|
-
"
|
|
89
|
-
"
|
|
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
|
-
"
|
|
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
|
}
|
package/src/widget-sidenav.ts
CHANGED
|
@@ -55,15 +55,28 @@ export class WidgetSidenav extends LitElement {
|
|
|
55
55
|
return '/' + route.split('/').filter(Boolean).join('/')
|
|
56
56
|
}
|
|
57
57
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
if (
|
|
61
|
-
item.
|
|
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 (
|
|
64
|
-
|
|
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
|
|
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=${() =>
|
|
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.
|
|
180
|
+
class="nav-item ${this.matchesRoute(this.resolveRoute(item))
|
|
181
|
+
? 'selected'
|
|
182
|
+
: ''}"
|
|
162
183
|
style="gap: ${gap}px;"
|
|
163
|
-
@click=${() => this.handleNavItemClick(this.
|
|
184
|
+
@click=${() => this.handleNavItemClick(this.resolveRoute(item))}
|
|
164
185
|
>
|
|
165
186
|
${item.iconName
|
|
166
187
|
? html`
|