@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.
- package/.editorconfig +16 -0
- package/.gitmodules +3 -0
- package/.postcssrc.json +5 -0
- package/.prettierignore +14 -0
- package/.prettierrc.json +29 -0
- package/LICENSE.md +21 -0
- package/README.md +59 -0
- package/angular.json +98 -0
- package/eslint.config.js +89 -0
- package/package.json +59 -0
- package/public/demo/images/flag/flag_placeholder.png +0 -0
- package/public/demo/images/footer-image.gif +0 -0
- package/public/demo/images/galleria/galleria1.jpg +0 -0
- package/public/demo/images/galleria/galleria10.jpg +0 -0
- package/public/demo/images/galleria/galleria10s.jpg +0 -0
- package/public/demo/images/galleria/galleria11.jpg +0 -0
- package/public/demo/images/galleria/galleria11s.jpg +0 -0
- package/public/demo/images/galleria/galleria12.jpg +0 -0
- package/public/demo/images/galleria/galleria12s.jpg +0 -0
- package/public/demo/images/galleria/galleria13.jpg +0 -0
- package/public/demo/images/galleria/galleria13s.jpg +0 -0
- package/public/demo/images/galleria/galleria14.jpg +0 -0
- package/public/demo/images/galleria/galleria14s.jpg +0 -0
- package/public/demo/images/galleria/galleria15.jpg +0 -0
- package/public/demo/images/galleria/galleria15s.jpg +0 -0
- package/public/demo/images/galleria/galleria1s.jpg +0 -0
- package/public/demo/images/galleria/galleria2.jpg +0 -0
- package/public/demo/images/galleria/galleria2s.jpg +0 -0
- package/public/demo/images/galleria/galleria3.jpg +0 -0
- package/public/demo/images/galleria/galleria3s.jpg +0 -0
- package/public/demo/images/galleria/galleria4.jpg +0 -0
- package/public/demo/images/galleria/galleria4s.jpg +0 -0
- package/public/demo/images/galleria/galleria5.jpg +0 -0
- package/public/demo/images/galleria/galleria5s.jpg +0 -0
- package/public/demo/images/galleria/galleria6.jpg +0 -0
- package/public/demo/images/galleria/galleria6s.jpg +0 -0
- package/public/demo/images/galleria/galleria7.jpg +0 -0
- package/public/demo/images/galleria/galleria7s.jpg +0 -0
- package/public/demo/images/galleria/galleria8.jpg +0 -0
- package/public/demo/images/galleria/galleria8s.jpg +0 -0
- package/public/demo/images/galleria/galleria9.jpg +0 -0
- package/public/demo/images/galleria/galleria9s.jpg +0 -0
- package/public/demo/images/product/bamboo-watch.jpg +0 -0
- package/public/demo/images/product/black-watch.jpg +0 -0
- package/public/demo/images/product/blue-band.jpg +0 -0
- package/public/demo/images/product/blue-t-shirt.jpg +0 -0
- package/public/demo/images/product/bracelet.jpg +0 -0
- package/public/demo/images/product/brown-purse.jpg +0 -0
- package/public/demo/images/product/chakra-bracelet.jpg +0 -0
- package/public/demo/images/product/galaxy-earrings.jpg +0 -0
- package/public/demo/images/product/game-controller.jpg +0 -0
- package/public/demo/images/product/gaming-set.jpg +0 -0
- package/public/demo/images/product/gold-phone-case.jpg +0 -0
- package/public/demo/images/product/green-earbuds.jpg +0 -0
- package/public/demo/images/product/green-t-shirt.jpg +0 -0
- package/public/demo/images/product/grey-t-shirt.jpg +0 -0
- package/public/demo/images/product/headphones.jpg +0 -0
- package/public/demo/images/product/light-green-t-shirt.jpg +0 -0
- package/public/demo/images/product/lime-band.jpg +0 -0
- package/public/demo/images/product/mini-speakers.jpg +0 -0
- package/public/demo/images/product/painted-phone-case.jpg +0 -0
- package/public/demo/images/product/pink-band.jpg +0 -0
- package/public/demo/images/product/pink-purse.jpg +0 -0
- package/public/demo/images/product/product-placeholder.svg +10 -0
- package/public/demo/images/product/purple-band.jpg +0 -0
- package/public/demo/images/product/purple-gemstone-necklace.jpg +0 -0
- package/public/demo/images/product/purple-t-shirt.jpg +0 -0
- package/public/demo/images/product/shoes.jpg +0 -0
- package/public/demo/images/product/sneakers.jpg +0 -0
- package/public/demo/images/product/teal-t-shirt.jpg +0 -0
- package/public/demo/images/product/yellow-earbuds.jpg +0 -0
- package/public/demo/images/product/yoga-mat.jpg +0 -0
- package/public/demo/images/product/yoga-set.jpg +0 -0
- package/src/app/layout/component/configurator/app.configurator.html +48 -0
- package/src/app/layout/component/configurator/app.configurator.ts +396 -0
- package/src/app/layout/component/floatingconfigurator/app.floatingconfigurator.ts +31 -0
- package/src/app/layout/component/footer/app.footer.scss +52 -0
- package/src/app/layout/component/footer/app.footer.ts +26 -0
- package/src/app/layout/component/layout/app.layout.ts +50 -0
- package/src/app/layout/component/menu/app.menu.html +7 -0
- package/src/app/layout/component/menu/app.menu.scss +13 -0
- package/src/app/layout/component/menu/app.menu.ts +90 -0
- package/src/app/layout/component/menuitem/app.menuitem.html +56 -0
- package/src/app/layout/component/menuitem/app.menuitem.scss +218 -0
- package/src/app/layout/component/menuitem/app.menuitem.ts +126 -0
- package/src/app/layout/component/sidebar/app.sidebar.html +3 -0
- package/src/app/layout/component/sidebar/app.sidebar.scss +0 -0
- package/src/app/layout/component/sidebar/app.sidebar.ts +106 -0
- package/src/app/layout/component/topbar/app.topbar.html +190 -0
- package/src/app/layout/component/topbar/app.topbar.scss +8 -0
- package/src/app/layout/component/topbar/app.topbar.ts +68 -0
- package/src/app/layout/service/layout.service.ts +117 -0
- package/src/app/pages/auth/access.ts +32 -0
- package/src/app/pages/auth/auth.routes.ts +10 -0
- package/src/app/pages/auth/error.ts +32 -0
- package/src/app/pages/auth/login.ts +71 -0
- package/src/app/pages/crud/crud.ts +387 -0
- package/src/app/pages/dashboard/dashboard.css +778 -0
- package/src/app/pages/dashboard/dashboard.html +191 -0
- package/src/app/pages/dashboard/dashboard.ts +348 -0
- package/src/app/pages/documentation/documentation.ts +73 -0
- package/src/app/pages/empty/empty.ts +11 -0
- package/src/app/pages/landing/components/featureswidget.ts +139 -0
- package/src/app/pages/landing/components/footerwidget.ts +73 -0
- package/src/app/pages/landing/components/herowidget.ts +25 -0
- package/src/app/pages/landing/components/highlightswidget.ts +46 -0
- package/src/app/pages/landing/components/pricingwidget.ts +119 -0
- package/src/app/pages/landing/components/topbarwidget.component.ts +68 -0
- package/src/app/pages/landing/landing.ts +31 -0
- package/src/app/pages/notfound/notfound.ts +68 -0
- package/src/app/pages/pages.routes.ts +17 -0
- package/src/app/pages/profile/profile.html +57 -0
- package/src/app/pages/profile/profile.scss +145 -0
- package/src/app/pages/profile/profile.ts +19 -0
- package/src/app/pages/service/country.service.ts +255 -0
- package/src/app/pages/service/customer.service.ts +9057 -0
- package/src/app/pages/service/icon.service.ts +23 -0
- package/src/app/pages/service/node.service.ts +816 -0
- package/src/app/pages/service/photo.service.ts +103 -0
- package/src/app/pages/service/product.service.ts +1322 -0
- package/src/app/pages/tickets/tickets-create/tickets-create.html +140 -0
- package/src/app/pages/tickets/tickets-create/tickets-create.scss +617 -0
- package/src/app/pages/tickets/tickets-create/tickets-create.ts +104 -0
- package/src/app/pages/tickets/tickets-list/ticket-list.html +150 -0
- package/src/app/pages/tickets/tickets-list/ticket-list.scss +392 -0
- package/src/app/pages/tickets/tickets-list/ticket-list.ts +178 -0
- package/src/app/pages/uikit/buttondemo.ts +254 -0
- package/src/app/pages/uikit/chartdemo.ts +290 -0
- package/src/app/pages/uikit/filedemo.ts +52 -0
- package/src/app/pages/uikit/formlayoutdemo.ts +129 -0
- package/src/app/pages/uikit/inputdemo.ts +339 -0
- package/src/app/pages/uikit/listdemo.ts +217 -0
- package/src/app/pages/uikit/mediademo.ts +1021 -0
- package/src/app/pages/uikit/menudemo.ts +540 -0
- package/src/app/pages/uikit/messagesdemo.ts +101 -0
- package/src/app/pages/uikit/miscdemo.ts +192 -0
- package/src/app/pages/uikit/overlaydemo.ts +235 -0
- package/src/app/pages/uikit/panelsdemo.ts +235 -0
- package/src/app/pages/uikit/tabledemo.ts +568 -0
- package/src/app/pages/uikit/timelinedemo.ts +141 -0
- package/src/app/pages/uikit/treedemo.ts +75 -0
- package/src/app/pages/uikit/uikit.routes.ts +35 -0
- package/src/app.component.ts +22 -0
- package/src/app.config.ts +23 -0
- package/src/app.routes.ts +23 -0
- package/src/assets/demo/code.scss +17 -0
- package/src/assets/demo/demo.scss +2 -0
- package/src/assets/demo/flags/flags.css +984 -0
- package/src/assets/layout/_core.scss +24 -0
- package/src/assets/layout/_footer.scss +8 -0
- package/src/assets/layout/_main.scss +21 -0
- package/src/assets/layout/_menu.scss +159 -0
- package/src/assets/layout/_mixins.scss +15 -0
- package/src/assets/layout/_preloading.scss +47 -0
- package/src/assets/layout/_responsive.scss +111 -0
- package/src/assets/layout/_topbar.scss +201 -0
- package/src/assets/layout/_typography.scss +68 -0
- package/src/assets/layout/_utils.scss +25 -0
- package/src/assets/layout/layout.scss +13 -0
- package/src/assets/layout/variables/_common.scss +21 -0
- package/src/assets/layout/variables/_dark.scss +5 -0
- package/src/assets/layout/variables/_light.scss +5 -0
- package/src/assets/styles.scss +4 -0
- package/src/assets/tailwind.css +32 -0
- package/src/index.html +15 -0
- package/src/main.ts +5 -0
- package/tsconfig.app.json +15 -0
- package/tsconfig.json +33 -0
- package/tsconfig.spec.json +15 -0
- 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
|
+
}
|
|
File without changes
|
|
@@ -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
|
+
}
|