@duyanhdev/mvp-ifs-ui-kit 21.0.0

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.
Files changed (170) hide show
  1. package/.editorconfig +16 -0
  2. package/.gitmodules +3 -0
  3. package/.postcssrc.json +5 -0
  4. package/.prettierignore +14 -0
  5. package/.prettierrc.json +29 -0
  6. package/LICENSE.md +21 -0
  7. package/README.md +59 -0
  8. package/angular.json +98 -0
  9. package/eslint.config.js +89 -0
  10. package/package.json +59 -0
  11. package/public/demo/images/flag/flag_placeholder.png +0 -0
  12. package/public/demo/images/footer-image.gif +0 -0
  13. package/public/demo/images/galleria/galleria1.jpg +0 -0
  14. package/public/demo/images/galleria/galleria10.jpg +0 -0
  15. package/public/demo/images/galleria/galleria10s.jpg +0 -0
  16. package/public/demo/images/galleria/galleria11.jpg +0 -0
  17. package/public/demo/images/galleria/galleria11s.jpg +0 -0
  18. package/public/demo/images/galleria/galleria12.jpg +0 -0
  19. package/public/demo/images/galleria/galleria12s.jpg +0 -0
  20. package/public/demo/images/galleria/galleria13.jpg +0 -0
  21. package/public/demo/images/galleria/galleria13s.jpg +0 -0
  22. package/public/demo/images/galleria/galleria14.jpg +0 -0
  23. package/public/demo/images/galleria/galleria14s.jpg +0 -0
  24. package/public/demo/images/galleria/galleria15.jpg +0 -0
  25. package/public/demo/images/galleria/galleria15s.jpg +0 -0
  26. package/public/demo/images/galleria/galleria1s.jpg +0 -0
  27. package/public/demo/images/galleria/galleria2.jpg +0 -0
  28. package/public/demo/images/galleria/galleria2s.jpg +0 -0
  29. package/public/demo/images/galleria/galleria3.jpg +0 -0
  30. package/public/demo/images/galleria/galleria3s.jpg +0 -0
  31. package/public/demo/images/galleria/galleria4.jpg +0 -0
  32. package/public/demo/images/galleria/galleria4s.jpg +0 -0
  33. package/public/demo/images/galleria/galleria5.jpg +0 -0
  34. package/public/demo/images/galleria/galleria5s.jpg +0 -0
  35. package/public/demo/images/galleria/galleria6.jpg +0 -0
  36. package/public/demo/images/galleria/galleria6s.jpg +0 -0
  37. package/public/demo/images/galleria/galleria7.jpg +0 -0
  38. package/public/demo/images/galleria/galleria7s.jpg +0 -0
  39. package/public/demo/images/galleria/galleria8.jpg +0 -0
  40. package/public/demo/images/galleria/galleria8s.jpg +0 -0
  41. package/public/demo/images/galleria/galleria9.jpg +0 -0
  42. package/public/demo/images/galleria/galleria9s.jpg +0 -0
  43. package/public/demo/images/product/bamboo-watch.jpg +0 -0
  44. package/public/demo/images/product/black-watch.jpg +0 -0
  45. package/public/demo/images/product/blue-band.jpg +0 -0
  46. package/public/demo/images/product/blue-t-shirt.jpg +0 -0
  47. package/public/demo/images/product/bracelet.jpg +0 -0
  48. package/public/demo/images/product/brown-purse.jpg +0 -0
  49. package/public/demo/images/product/chakra-bracelet.jpg +0 -0
  50. package/public/demo/images/product/galaxy-earrings.jpg +0 -0
  51. package/public/demo/images/product/game-controller.jpg +0 -0
  52. package/public/demo/images/product/gaming-set.jpg +0 -0
  53. package/public/demo/images/product/gold-phone-case.jpg +0 -0
  54. package/public/demo/images/product/green-earbuds.jpg +0 -0
  55. package/public/demo/images/product/green-t-shirt.jpg +0 -0
  56. package/public/demo/images/product/grey-t-shirt.jpg +0 -0
  57. package/public/demo/images/product/headphones.jpg +0 -0
  58. package/public/demo/images/product/light-green-t-shirt.jpg +0 -0
  59. package/public/demo/images/product/lime-band.jpg +0 -0
  60. package/public/demo/images/product/mini-speakers.jpg +0 -0
  61. package/public/demo/images/product/painted-phone-case.jpg +0 -0
  62. package/public/demo/images/product/pink-band.jpg +0 -0
  63. package/public/demo/images/product/pink-purse.jpg +0 -0
  64. package/public/demo/images/product/product-placeholder.svg +10 -0
  65. package/public/demo/images/product/purple-band.jpg +0 -0
  66. package/public/demo/images/product/purple-gemstone-necklace.jpg +0 -0
  67. package/public/demo/images/product/purple-t-shirt.jpg +0 -0
  68. package/public/demo/images/product/shoes.jpg +0 -0
  69. package/public/demo/images/product/sneakers.jpg +0 -0
  70. package/public/demo/images/product/teal-t-shirt.jpg +0 -0
  71. package/public/demo/images/product/yellow-earbuds.jpg +0 -0
  72. package/public/demo/images/product/yoga-mat.jpg +0 -0
  73. package/public/demo/images/product/yoga-set.jpg +0 -0
  74. package/src/app/layout/component/configurator/app.configurator.html +48 -0
  75. package/src/app/layout/component/configurator/app.configurator.ts +396 -0
  76. package/src/app/layout/component/floatingconfigurator/app.floatingconfigurator.ts +31 -0
  77. package/src/app/layout/component/footer/app.footer.scss +52 -0
  78. package/src/app/layout/component/footer/app.footer.ts +26 -0
  79. package/src/app/layout/component/layout/app.layout.ts +50 -0
  80. package/src/app/layout/component/menu/app.menu.html +7 -0
  81. package/src/app/layout/component/menu/app.menu.scss +13 -0
  82. package/src/app/layout/component/menu/app.menu.ts +90 -0
  83. package/src/app/layout/component/menuitem/app.menuitem.html +56 -0
  84. package/src/app/layout/component/menuitem/app.menuitem.scss +218 -0
  85. package/src/app/layout/component/menuitem/app.menuitem.ts +126 -0
  86. package/src/app/layout/component/sidebar/app.sidebar.html +3 -0
  87. package/src/app/layout/component/sidebar/app.sidebar.scss +0 -0
  88. package/src/app/layout/component/sidebar/app.sidebar.ts +106 -0
  89. package/src/app/layout/component/topbar/app.topbar.html +190 -0
  90. package/src/app/layout/component/topbar/app.topbar.scss +8 -0
  91. package/src/app/layout/component/topbar/app.topbar.ts +68 -0
  92. package/src/app/layout/service/layout.service.ts +117 -0
  93. package/src/app/pages/auth/access.ts +32 -0
  94. package/src/app/pages/auth/auth.routes.ts +10 -0
  95. package/src/app/pages/auth/error.ts +32 -0
  96. package/src/app/pages/auth/login.ts +71 -0
  97. package/src/app/pages/crud/crud.ts +387 -0
  98. package/src/app/pages/dashboard/dashboard.css +778 -0
  99. package/src/app/pages/dashboard/dashboard.html +191 -0
  100. package/src/app/pages/dashboard/dashboard.ts +348 -0
  101. package/src/app/pages/documentation/documentation.ts +73 -0
  102. package/src/app/pages/empty/empty.ts +11 -0
  103. package/src/app/pages/landing/components/featureswidget.ts +139 -0
  104. package/src/app/pages/landing/components/footerwidget.ts +73 -0
  105. package/src/app/pages/landing/components/herowidget.ts +25 -0
  106. package/src/app/pages/landing/components/highlightswidget.ts +46 -0
  107. package/src/app/pages/landing/components/pricingwidget.ts +119 -0
  108. package/src/app/pages/landing/components/topbarwidget.component.ts +68 -0
  109. package/src/app/pages/landing/landing.ts +31 -0
  110. package/src/app/pages/notfound/notfound.ts +68 -0
  111. package/src/app/pages/pages.routes.ts +17 -0
  112. package/src/app/pages/profile/profile.html +57 -0
  113. package/src/app/pages/profile/profile.scss +145 -0
  114. package/src/app/pages/profile/profile.ts +19 -0
  115. package/src/app/pages/service/country.service.ts +255 -0
  116. package/src/app/pages/service/customer.service.ts +9057 -0
  117. package/src/app/pages/service/icon.service.ts +23 -0
  118. package/src/app/pages/service/node.service.ts +816 -0
  119. package/src/app/pages/service/photo.service.ts +103 -0
  120. package/src/app/pages/service/product.service.ts +1322 -0
  121. package/src/app/pages/tickets/tickets-create/tickets-create.html +140 -0
  122. package/src/app/pages/tickets/tickets-create/tickets-create.scss +617 -0
  123. package/src/app/pages/tickets/tickets-create/tickets-create.ts +104 -0
  124. package/src/app/pages/tickets/tickets-list/ticket-list.html +150 -0
  125. package/src/app/pages/tickets/tickets-list/ticket-list.scss +392 -0
  126. package/src/app/pages/tickets/tickets-list/ticket-list.ts +178 -0
  127. package/src/app/pages/uikit/buttondemo.ts +254 -0
  128. package/src/app/pages/uikit/chartdemo.ts +290 -0
  129. package/src/app/pages/uikit/filedemo.ts +52 -0
  130. package/src/app/pages/uikit/formlayoutdemo.ts +129 -0
  131. package/src/app/pages/uikit/inputdemo.ts +339 -0
  132. package/src/app/pages/uikit/listdemo.ts +217 -0
  133. package/src/app/pages/uikit/mediademo.ts +1021 -0
  134. package/src/app/pages/uikit/menudemo.ts +540 -0
  135. package/src/app/pages/uikit/messagesdemo.ts +101 -0
  136. package/src/app/pages/uikit/miscdemo.ts +192 -0
  137. package/src/app/pages/uikit/overlaydemo.ts +235 -0
  138. package/src/app/pages/uikit/panelsdemo.ts +235 -0
  139. package/src/app/pages/uikit/tabledemo.ts +568 -0
  140. package/src/app/pages/uikit/timelinedemo.ts +141 -0
  141. package/src/app/pages/uikit/treedemo.ts +75 -0
  142. package/src/app/pages/uikit/uikit.routes.ts +35 -0
  143. package/src/app.component.ts +22 -0
  144. package/src/app.config.ts +23 -0
  145. package/src/app.routes.ts +23 -0
  146. package/src/assets/demo/code.scss +17 -0
  147. package/src/assets/demo/demo.scss +2 -0
  148. package/src/assets/demo/flags/flags.css +984 -0
  149. package/src/assets/layout/_core.scss +24 -0
  150. package/src/assets/layout/_footer.scss +8 -0
  151. package/src/assets/layout/_main.scss +21 -0
  152. package/src/assets/layout/_menu.scss +159 -0
  153. package/src/assets/layout/_mixins.scss +15 -0
  154. package/src/assets/layout/_preloading.scss +47 -0
  155. package/src/assets/layout/_responsive.scss +111 -0
  156. package/src/assets/layout/_topbar.scss +201 -0
  157. package/src/assets/layout/_typography.scss +68 -0
  158. package/src/assets/layout/_utils.scss +25 -0
  159. package/src/assets/layout/layout.scss +13 -0
  160. package/src/assets/layout/variables/_common.scss +21 -0
  161. package/src/assets/layout/variables/_dark.scss +5 -0
  162. package/src/assets/layout/variables/_light.scss +5 -0
  163. package/src/assets/styles.scss +4 -0
  164. package/src/assets/tailwind.css +32 -0
  165. package/src/index.html +15 -0
  166. package/src/main.ts +5 -0
  167. package/tsconfig.app.json +15 -0
  168. package/tsconfig.json +33 -0
  169. package/tsconfig.spec.json +15 -0
  170. package/vercel.json +9 -0
@@ -0,0 +1,56 @@
1
+ <!-- Root section header -->
2
+ @if (root() && isVisible()) {
3
+ <div class="layout-menuitem-root-text" (click)="toggleRootSection()">
4
+ <span class="root-label">{{ item().label }}</span>
5
+ <i class="pi root-chevron" [class.pi-angle-down]="!rootCollapsed()" [class.pi-angle-right]="rootCollapsed()"> </i>
6
+ </div>
7
+ }
8
+
9
+ <!-- Non-router link item -->
10
+ @if ((!hasRouterLink() || hasChildren()) && isVisible() && showContent()) {
11
+ <a [attr.href]="item().url" (click)="itemClick($event)" [ngClass]="item().class" [attr.target]="item().target" tabindex="0" pRipple class="menu-link">
12
+ <span class="menu-icon-wrap">
13
+ <i [ngClass]="item().icon" class="layout-menuitem-icon text-center m-auto"></i>
14
+ </span>
15
+ <span class="layout-menuitem-text">{{ item().label }}</span>
16
+ @if (hasChildren()) {
17
+ <i class="pi pi-angle-down submenu-arrow"></i>
18
+ }
19
+ </a>
20
+ }
21
+
22
+ <!-- Router link item -->
23
+ @if (hasRouterLink() && !hasChildren() && isVisible() && showContent()) {
24
+ <a
25
+ (click)="itemClick($event)"
26
+ [ngClass]="item().class"
27
+ [routerLink]="item().routerLink"
28
+ routerLinkActive="active-route"
29
+ [routerLinkActiveOptions]="item().routerLinkActiveOptions || { paths: 'exact', queryParams: 'ignored', matrixParams: 'ignored', fragment: 'ignored' }"
30
+ [fragment]="item().fragment"
31
+ [queryParamsHandling]="item().queryParamsHandling"
32
+ [preserveFragment]="item().preserveFragment"
33
+ [skipLocationChange]="item().skipLocationChange"
34
+ [replaceUrl]="item().replaceUrl"
35
+ [state]="item().state"
36
+ [queryParams]="item().queryParams"
37
+ [attr.target]="item().target"
38
+ tabindex="0"
39
+ pRipple
40
+ class="menu-link"
41
+ >
42
+ <span class="menu-icon-wrap">
43
+ <i [ngClass]="item().icon" class="layout-menuitem-icon text-center m-auto"></i>
44
+ </span>
45
+ <span class="layout-menuitem-text">{{ item().label }}</span>
46
+ </a>
47
+ }
48
+
49
+ <!-- Children submenu -->
50
+ @if (hasChildren() && isVisible() && (root() || isActive()) && showContent()) {
51
+ <ul [animate.enter]="initialized() ? 'p-submenu-enter' : null" [animate.leave]="'p-submenu-leave'" [class.layout-root-submenulist]="root()" class="submenu-list">
52
+ @for (child of item().items; track child?.label) {
53
+ <li app-menuitem [item]="child" [parentPath]="fullPath()" [root]="false" [class]="child['badgeClass']"></li>
54
+ }
55
+ </ul>
56
+ }
@@ -0,0 +1,218 @@
1
+ // ─── Design Tokens ───────────────────────────────────────────────
2
+ $menu-accent: #6366f1; // indigo-500
3
+ $menu-accent-light: #818cf8; // indigo-400
4
+ $menu-accent-glow: rgba(99, 102, 241, 0.18);
5
+ $menu-active-bg: rgba(99, 102, 241, 0.12);
6
+ $menu-hover-bg: rgba(255, 255, 255, 0.055);
7
+ $menu-icon-size: 1.05rem;
8
+ $menu-radius: 10px;
9
+ $transition-smooth: all 0.22s cubic-bezier(0.4, 0, 0.2, 1);
10
+
11
+ // ─── Menu Link (shared for both router & non-router) ─────────────
12
+ .menu-link {
13
+ display: flex;
14
+ align-items: center;
15
+ gap: 0.75rem;
16
+ padding: 0.6rem 1rem 0.6rem 0.85rem;
17
+ margin: 2px 0.5rem;
18
+ border-radius: $menu-radius;
19
+ text-decoration: none;
20
+ cursor: pointer;
21
+ position: relative;
22
+ transition: $transition-smooth;
23
+ color: var(--text-color-secondary, #94a3b8);
24
+ outline: none;
25
+ overflow: hidden;
26
+
27
+ &::before {
28
+ content: '';
29
+ position: absolute;
30
+ left: 0;
31
+ top: 20%;
32
+ height: 60%;
33
+ width: 3px;
34
+ border-radius: 0 3px 3px 0;
35
+ background: $menu-accent;
36
+ opacity: 0;
37
+ transform: scaleY(0.4);
38
+ transition: $transition-smooth;
39
+ }
40
+
41
+ &:hover {
42
+ background: $menu-hover-bg;
43
+ color: var(--text-color, #e2e8f0);
44
+
45
+ .menu-icon-wrap {
46
+ background: $menu-accent-glow;
47
+
48
+ .layout-menuitem-icon {
49
+ color: $menu-accent-light;
50
+ transform: scale(1.1);
51
+ }
52
+ }
53
+ }
54
+
55
+ &:focus-visible {
56
+ box-shadow: 0 0 0 2px $menu-accent;
57
+ }
58
+
59
+ // ── Active state ──
60
+ &.active-route {
61
+ background: $menu-active-bg;
62
+ color: var(--text-color, #e2e8f0);
63
+
64
+ &::before {
65
+ opacity: 1;
66
+ transform: scaleY(1);
67
+ }
68
+
69
+ .menu-icon-wrap {
70
+ background: $menu-accent-glow;
71
+
72
+ .layout-menuitem-icon {
73
+ color: $menu-accent;
74
+ }
75
+ }
76
+
77
+ .layout-menuitem-text {
78
+ color: $menu-accent-light;
79
+ font-weight: 600;
80
+ }
81
+ }
82
+ }
83
+
84
+
85
+ .menu-icon-wrap {
86
+ display: flex;
87
+ align-items: center;
88
+ justify-content: center;
89
+ flex-shrink: 0;
90
+ width: 2rem;
91
+ height: 2rem;
92
+ border-radius: 8px;
93
+ background: rgba(255, 255, 255, 0.04);
94
+ transition: $transition-smooth;
95
+
96
+ .layout-menuitem-icon {
97
+ font-size: $menu-icon-size;
98
+ color: var(--text-color-secondary, #64748b);
99
+ transition: $transition-smooth;
100
+ margin: auto;
101
+ }
102
+ }
103
+
104
+ // ─── Menu Label ───────────────────────────────────────────────────
105
+ .layout-menuitem-text {
106
+ font-size: 0.855rem;
107
+ font-weight: 500;
108
+ letter-spacing: 0.01em;
109
+ transition: color 0.2s;
110
+ white-space: nowrap;
111
+ overflow: hidden;
112
+ text-overflow: ellipsis;
113
+ }
114
+
115
+ // ─── Submenu Arrow ────────────────────────────────────────────────
116
+ .submenu-arrow {
117
+ margin-left: auto;
118
+ font-size: 0.7rem;
119
+ opacity: 0.5;
120
+ transition: transform 0.25s ease, opacity 0.2s;
121
+
122
+ .menu-link:hover & {
123
+ opacity: 0.8;
124
+ }
125
+ }
126
+
127
+ // ─── Submenu List ─────────────────────────────────────────────────
128
+ .submenu-list {
129
+ list-style: none;
130
+ margin: 0;
131
+ padding: 0 0 0 1.25rem;
132
+ position: relative;
133
+
134
+ // Vertical guide line
135
+ &::before {
136
+ content: '';
137
+ position: absolute;
138
+ left: 1.85rem;
139
+ top: 0.25rem;
140
+ bottom: 0.25rem;
141
+ width: 1px;
142
+ background: linear-gradient(to bottom,
143
+ transparent,
144
+ rgba(99, 102, 241, 0.25) 20%,
145
+ rgba(99, 102, 241, 0.25) 80%,
146
+ transparent);
147
+ }
148
+
149
+ li {
150
+ position: relative;
151
+
152
+ // Dot connector
153
+ &::before {
154
+ content: '';
155
+ position: absolute;
156
+ left: -0.55rem;
157
+ top: 50%;
158
+ width: 5px;
159
+ height: 5px;
160
+ border-radius: 50%;
161
+ background: rgba(99, 102, 241, 0.35);
162
+ transform: translateY(-50%);
163
+ transition: background 0.2s;
164
+ }
165
+
166
+ &:has(.active-route)::before {
167
+ background: $menu-accent;
168
+ box-shadow: 0 0 6px $menu-accent-glow;
169
+ }
170
+ }
171
+
172
+ &.layout-root-submenulist {
173
+ padding-left: 0;
174
+
175
+ &::before {
176
+ display: none;
177
+ }
178
+
179
+ li::before {
180
+ display: none;
181
+ }
182
+ }
183
+ }
184
+
185
+ // ─── Submenu Animations ───────────────────────────────────────────
186
+ .p-submenu-enter {
187
+ animation: submenuSlideIn 0.22s cubic-bezier(0.4, 0, 0.2, 1) forwards;
188
+ }
189
+
190
+ .p-submenu-leave {
191
+ animation: submenuSlideOut 0.18s cubic-bezier(0.4, 0, 0.2, 1) forwards;
192
+ }
193
+
194
+ @keyframes submenuSlideIn {
195
+ from {
196
+ opacity: 0;
197
+ transform: translateY(-6px) scaleY(0.96);
198
+ transform-origin: top;
199
+ }
200
+
201
+ to {
202
+ opacity: 1;
203
+ transform: translateY(0) scaleY(1);
204
+ }
205
+ }
206
+
207
+ @keyframes submenuSlideOut {
208
+ from {
209
+ opacity: 1;
210
+ transform: translateY(0) scaleY(1);
211
+ transform-origin: top;
212
+ }
213
+
214
+ to {
215
+ opacity: 0;
216
+ transform: translateY(-4px) scaleY(0.97);
217
+ }
218
+ }
@@ -0,0 +1,126 @@
1
+ import { Component, computed, inject, input, signal } from '@angular/core';
2
+ import { NavigationEnd, Router, RouterModule } from '@angular/router';
3
+ import { CommonModule } from '@angular/common';
4
+ import { RippleModule } from 'primeng/ripple';
5
+ import { LayoutService } from '@/app/layout/service/layout.service';
6
+ import { filter } from 'rxjs/operators';
7
+
8
+ @Component({
9
+ selector: '[app-menuitem]',
10
+ imports: [CommonModule, RouterModule, RippleModule],
11
+
12
+ templateUrl: 'app.menuitem.html',
13
+ styleUrl: 'app.menuitem.scss',
14
+ host: {
15
+ '[class.active-menuitem]': 'isActive()',
16
+ '[class.layout-root-menuitem]': 'root()'
17
+ }
18
+ })
19
+ export class AppMenuitem {
20
+ layoutService = inject(LayoutService);
21
+ router = inject(Router);
22
+
23
+ item = input<any>(null);
24
+ root = input<boolean>(false);
25
+ parentPath = input<string | null>(null);
26
+
27
+ // ── Collapse state cho root section ──────────────────
28
+ rootCollapsed = signal<boolean>(false);
29
+
30
+ // Chỉ ẩn content khi là root VÀ đang collapsed
31
+ showContent = computed(() => {
32
+ if (this.root()) return !this.rootCollapsed();
33
+ return true;
34
+ });
35
+
36
+ toggleRootSection() {
37
+ this.rootCollapsed.update((v) => !v);
38
+ }
39
+
40
+ // ── Computed ──────────────────────────────────────────
41
+ isVisible = computed(() => this.item()?.visible !== false);
42
+ hasChildren = computed(() => this.item()?.items && this.item()?.items.length > 0);
43
+ hasRouterLink = computed(() => !!this.item()?.routerLink);
44
+
45
+ fullPath = computed(() => {
46
+ const itemPath = this.item()?.path;
47
+ if (!itemPath) return this.parentPath();
48
+ const parent = this.parentPath();
49
+ if (parent && !itemPath.startsWith(parent)) return parent + itemPath;
50
+ return itemPath;
51
+ });
52
+
53
+ isActive = computed(() => {
54
+ const activePath = this.layoutService.layoutState().activePath;
55
+ if (this.item()?.path) {
56
+ return activePath?.startsWith(this.fullPath() ?? '') ?? false;
57
+ }
58
+ return false;
59
+ });
60
+
61
+ initialized = signal<boolean>(false);
62
+
63
+ constructor() {
64
+ this.router.events.pipe(filter((e) => e instanceof NavigationEnd)).subscribe(() => {
65
+ if (this.item()?.routerLink) this.updateActiveStateFromRoute();
66
+ });
67
+ }
68
+
69
+ ngOnInit() {
70
+ if (this.item()?.routerLink) this.updateActiveStateFromRoute();
71
+ }
72
+
73
+ ngAfterViewInit() {
74
+ setTimeout(() => this.initialized.set(true));
75
+ }
76
+
77
+ updateActiveStateFromRoute() {
78
+ const item = this.item();
79
+ if (!item?.routerLink) return;
80
+
81
+ const isRouteActive = this.router.isActive(item.routerLink[0], {
82
+ paths: 'exact',
83
+ queryParams: 'ignored',
84
+ matrixParams: 'ignored',
85
+ fragment: 'ignored'
86
+ });
87
+
88
+ if (isRouteActive) {
89
+ const parentPath = this.parentPath();
90
+ if (parentPath) {
91
+ this.layoutService.layoutState.update((val) => ({ ...val, activePath: parentPath }));
92
+ }
93
+ }
94
+ }
95
+
96
+ itemClick(event: Event) {
97
+ const item = this.item();
98
+ if (item?.disabled) {
99
+ event.preventDefault();
100
+ return;
101
+ }
102
+ if (item?.command) {
103
+ item.command({ originalEvent: event, item });
104
+ }
105
+
106
+ if (this.hasChildren()) {
107
+ if (this.isActive()) {
108
+ this.layoutService.layoutState.update((val) => ({ ...val, activePath: this.parentPath() }));
109
+ } else {
110
+ this.layoutService.layoutState.update((val) => ({
111
+ ...val,
112
+ activePath: this.fullPath(),
113
+ menuHoverActive: true
114
+ }));
115
+ }
116
+ } else {
117
+ this.layoutService.layoutState.update((val) => ({
118
+ ...val,
119
+ overlayMenuActive: false,
120
+ staticMenuMobileActive: false,
121
+ mobileMenuActive: false,
122
+ menuHoverActive: false
123
+ }));
124
+ }
125
+ }
126
+ }
@@ -0,0 +1,3 @@
1
+ <div class="layout-sidebar">
2
+ <app-menu></app-menu>
3
+ </div>
@@ -0,0 +1,106 @@
1
+ import { Component, computed, effect, ElementRef, inject, OnDestroy, OnInit } from '@angular/core';
2
+ import { NavigationEnd, Router, RouterModule } from '@angular/router';
3
+ import { filter, Subject, takeUntil } from 'rxjs';
4
+ import { AppMenu } from './../menu/app.menu';
5
+ import { LayoutService } from '@/app/layout/service/layout.service';
6
+
7
+ @Component({
8
+ selector: 'app-sidebar',
9
+ standalone: true,
10
+ imports: [AppMenu, RouterModule],
11
+ templateUrl: 'app.sidebar.html'
12
+ })
13
+ export class AppSidebar implements OnInit, OnDestroy {
14
+ layoutService = inject(LayoutService);
15
+
16
+ router = inject(Router);
17
+
18
+ el = inject(ElementRef);
19
+
20
+ private outsideClickListener: ((event: MouseEvent) => void) | null = null;
21
+
22
+ private destroy$ = new Subject<void>();
23
+
24
+ constructor() {
25
+ effect(() => {
26
+ const state = this.layoutService.layoutState();
27
+
28
+ if (this.layoutService.isDesktop()) {
29
+ if (state.overlayMenuActive) {
30
+ this.bindOutsideClickListener();
31
+ } else {
32
+ this.unbindOutsideClickListener();
33
+ }
34
+ } else {
35
+ if (state.mobileMenuActive) {
36
+ this.bindOutsideClickListener();
37
+ } else {
38
+ this.unbindOutsideClickListener();
39
+ }
40
+ }
41
+ });
42
+ }
43
+
44
+ ngOnInit() {
45
+ this.router.events
46
+ .pipe(
47
+ filter((event) => event instanceof NavigationEnd),
48
+ takeUntil(this.destroy$)
49
+ )
50
+ .subscribe((event) => {
51
+ const navEvent = event as NavigationEnd;
52
+ this.onRouteChange(navEvent.urlAfterRedirects);
53
+ });
54
+
55
+ this.onRouteChange(this.router.url);
56
+ }
57
+
58
+ ngOnDestroy() {
59
+ this.destroy$.next();
60
+ this.destroy$.complete();
61
+ this.unbindOutsideClickListener();
62
+ }
63
+
64
+ private onRouteChange(path: string) {
65
+ this.layoutService.layoutState.update((val) => ({
66
+ ...val,
67
+ activePath: path,
68
+ overlayMenuActive: false,
69
+ staticMenuMobileActive: false,
70
+ mobileMenuActive: false,
71
+ menuHoverActive: false
72
+ }));
73
+ }
74
+
75
+ private bindOutsideClickListener() {
76
+ if (!this.outsideClickListener) {
77
+ this.outsideClickListener = (event: MouseEvent) => {
78
+ if (this.isOutsideClicked(event)) {
79
+ this.layoutService.layoutState.update((val) => ({
80
+ ...val,
81
+ overlayMenuActive: false,
82
+ staticMenuMobileActive: false,
83
+ mobileMenuActive: false,
84
+ menuHoverActive: false
85
+ }));
86
+ }
87
+ };
88
+
89
+ document.addEventListener('click', this.outsideClickListener);
90
+ }
91
+ }
92
+
93
+ private unbindOutsideClickListener() {
94
+ if (this.outsideClickListener) {
95
+ document.removeEventListener('click', this.outsideClickListener);
96
+ this.outsideClickListener = null;
97
+ }
98
+ }
99
+
100
+ private isOutsideClicked(event: MouseEvent): boolean {
101
+ const topbarButtonEl = document.querySelector('.topbar-start > button');
102
+ const sidebarEl = this.el.nativeElement;
103
+
104
+ return !(sidebarEl?.isSameNode(event.target as Node) || sidebarEl?.contains(event.target as Node) || topbarButtonEl?.isSameNode(event.target as Node) || topbarButtonEl?.contains(event.target as Node));
105
+ }
106
+ }