tailjng 0.1.15 → 0.1.17
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/cli/file-operations.js +3 -13
- package/cli/settings/path-utils.js +20 -4
- package/cli/templates/app.generator.js +59 -50
- package/package.json +1 -1
- package/registry/components.json +12 -3
- package/src/lib/components/alert/alert-toast/toast-alert.component.css +7 -0
- package/src/lib/components/alert/alert-toast/toast-alert.component.html +1 -1
- package/src/lib/components/alert/alert-toast/toast-alert.component.ts +1 -0
- package/src/lib/components/card/card-complete/complete-card.component.html +5 -6
- package/src/lib/components/card/card-complete/complete-card.component.ts +12 -1
- package/src/lib/components/card/card-crud-complete/complete-crud-card.component.html +16 -7
- package/src/lib/components/card/card-crud-complete/complete-crud-card.component.ts +12 -1
- package/src/lib/components/card/card-skeleton/card-skeleton.component.html +47 -0
- package/src/lib/components/card/card-skeleton/card-skeleton.component.ts +8 -0
- package/src/lib/components/dialog/dialog.component.css +12 -0
- package/src/lib/components/dialog/dialog.component.html +9 -4
- package/src/lib/components/dialog/dialog.component.ts +173 -28
- package/src/lib/components/form/form-sidebar/sidebar-form.component.css +14 -11
- package/src/lib/components/table/table-crud-complete/complete-crud-table.component.html +20 -39
- package/src/lib/components/table/table-crud-complete/complete-crud-table.component.ts +21 -0
- package/src/lib/components/table/table-skeleton/table-skeleton-desktop-row.component.html +74 -0
- package/src/lib/components/table/table-skeleton/table-skeleton-desktop-row.component.ts +19 -0
- package/src/lib/components/table/table-skeleton/table-skeleton-mobile-card.component.html +43 -0
- package/src/lib/components/table/table-skeleton/table-skeleton-mobile-card.component.ts +13 -0
- package/src/styles.css +60 -53
package/cli/file-operations.js
CHANGED
|
@@ -86,7 +86,8 @@ function findTailjngPackageRoot() {
|
|
|
86
86
|
|
|
87
87
|
async function copySharedFolderIfNeeded(componentPath, isDependency = false, options = {}) {
|
|
88
88
|
const { forceSync = false } = options
|
|
89
|
-
const
|
|
89
|
+
const packageRoot = findTailjngPackageRoot()
|
|
90
|
+
const sharedInfo = getSharedFolderInfo(componentPath, packageRoot)
|
|
90
91
|
if (!sharedInfo) {
|
|
91
92
|
return true
|
|
92
93
|
}
|
|
@@ -95,15 +96,7 @@ async function copySharedFolderIfNeeded(componentPath, isDependency = false, opt
|
|
|
95
96
|
return true
|
|
96
97
|
}
|
|
97
98
|
|
|
98
|
-
const packageRoot = findTailjngPackageRoot()
|
|
99
99
|
const sharedSrc = path.join(packageRoot, ...sharedInfo.sourceRelative.split("/"))
|
|
100
|
-
if (!fs.existsSync(sharedSrc)) {
|
|
101
|
-
console.error(
|
|
102
|
-
`${COLORS.red}${COLORS.bright}[tailjng CLI]${COLORS.reset} ${COLORS.red}ERROR: Shared utilities ${COLORS.bright}"${sharedInfo.groupKey}/shared"${COLORS.reset} ${COLORS.red}not found in package.${COLORS.reset}`,
|
|
103
|
-
)
|
|
104
|
-
console.error(`${COLORS.dim} Expected: ${sharedSrc}${COLORS.reset}`)
|
|
105
|
-
return false
|
|
106
|
-
}
|
|
107
100
|
|
|
108
101
|
const projectRoot = process.cwd()
|
|
109
102
|
const sharedDest = buildSharedFolderTargetPath(projectRoot, componentPath)
|
|
@@ -189,10 +182,7 @@ async function copyComponentFiles(componentName, componentPath, isDependency = f
|
|
|
189
182
|
|
|
190
183
|
fs.mkdirSync(targetPath, { recursive: true })
|
|
191
184
|
copyDirectoryRecursive(nodeModulesPath, targetPath, projectRoot)
|
|
192
|
-
|
|
193
|
-
if (!sharedOk) {
|
|
194
|
-
process.exit(1)
|
|
195
|
-
}
|
|
185
|
+
await copySharedFolderIfNeeded(componentPath, isDependency, { forceSync: true })
|
|
196
186
|
|
|
197
187
|
return true
|
|
198
188
|
}
|
|
@@ -59,14 +59,22 @@ function isComponentInstalled(projectRoot, componentName, componentPath) {
|
|
|
59
59
|
}
|
|
60
60
|
|
|
61
61
|
/**
|
|
62
|
-
* When a registry path is nested (e.g. toggle-radio/toggle-radio), returns sibling `shared/` info
|
|
63
|
-
*
|
|
62
|
+
* When a registry path is nested (e.g. toggle-radio/toggle-radio), returns sibling `shared/` info
|
|
63
|
+
* only if that group actually ships a `shared/` folder in the package (e.g. toggle-radio).
|
|
64
|
+
* Nested paths like `.config/colors` or `select/select-dropdown` have no group shared folder.
|
|
65
|
+
*
|
|
66
|
+
* @param {string} componentPath
|
|
67
|
+
* @param {string | null} [packageRoot] When set, returns null unless shared exists in the package.
|
|
64
68
|
*/
|
|
65
|
-
function getSharedFolderInfo(componentPath) {
|
|
69
|
+
function getSharedFolderInfo(componentPath, packageRoot = null) {
|
|
66
70
|
if (!componentPath.startsWith(COMPONENTS_SOURCE_PREFIX)) {
|
|
67
71
|
return null
|
|
68
72
|
}
|
|
69
73
|
|
|
74
|
+
if (isConfigPath(componentPath)) {
|
|
75
|
+
return null
|
|
76
|
+
}
|
|
77
|
+
|
|
70
78
|
const relativePath = componentPath.slice(COMPONENTS_SOURCE_PREFIX.length)
|
|
71
79
|
const pathParts = relativePath.split("/")
|
|
72
80
|
if (pathParts.length < 2) {
|
|
@@ -74,10 +82,18 @@ function getSharedFolderInfo(componentPath) {
|
|
|
74
82
|
}
|
|
75
83
|
|
|
76
84
|
const groupKey = pathParts.slice(0, -1).join("/")
|
|
85
|
+
const sourceRelative = `${COMPONENTS_SOURCE_PREFIX}${groupKey}/shared`
|
|
86
|
+
|
|
87
|
+
if (packageRoot) {
|
|
88
|
+
const sharedSrc = path.join(packageRoot, ...sourceRelative.split("/"))
|
|
89
|
+
if (!fs.existsSync(sharedSrc)) {
|
|
90
|
+
return null
|
|
91
|
+
}
|
|
92
|
+
}
|
|
77
93
|
|
|
78
94
|
return {
|
|
79
95
|
groupKey,
|
|
80
|
-
sourceRelative
|
|
96
|
+
sourceRelative,
|
|
81
97
|
targetRelative: `${groupKey}/shared`,
|
|
82
98
|
}
|
|
83
99
|
}
|
|
@@ -115,63 +115,72 @@ ${buildDefaultThemeBlock()}
|
|
|
115
115
|
function buildScrollScss() {
|
|
116
116
|
return `.scroll-element {
|
|
117
117
|
padding-right: 5px;
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
118
|
+
-webkit-overflow-scrolling: touch;
|
|
119
|
+
|
|
120
|
+
@media (hover: hover) and (pointer: fine) {
|
|
121
|
+
&::-webkit-scrollbar {
|
|
122
|
+
width: 8px;
|
|
123
|
+
height: 8px;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
&::-webkit-scrollbar-track {
|
|
127
|
+
border-radius: 10px;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
&::-webkit-scrollbar-thumb {
|
|
131
|
+
border-radius: 10px;
|
|
132
|
+
background: var(--color-primary);
|
|
133
|
+
transition: 0.5s;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
&::-webkit-scrollbar-thumb:hover {
|
|
137
|
+
background: var(--color-dark-primary);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
&::-webkit-scrollbar-button {
|
|
141
|
+
width: 0;
|
|
142
|
+
height: 0;
|
|
143
|
+
}
|
|
141
144
|
}
|
|
142
145
|
}
|
|
143
146
|
|
|
144
147
|
.scroll-screen {
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
148
|
+
-webkit-overflow-scrolling: touch;
|
|
149
|
+
|
|
150
|
+
@media (hover: hover) and (pointer: fine) {
|
|
151
|
+
&::-webkit-scrollbar {
|
|
152
|
+
width: 10px;
|
|
153
|
+
height: 10px;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
&::-webkit-scrollbar-thumb {
|
|
157
|
+
background: var(--color-dark-primary);
|
|
158
|
+
border: 3px solid var(--color-primary);
|
|
159
|
+
transition: 0.5s;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
&::-webkit-scrollbar-thumb:hover {
|
|
163
|
+
background: var(--color-primary);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
&::-webkit-scrollbar-button {
|
|
167
|
+
width: 0;
|
|
168
|
+
height: 0;
|
|
169
|
+
}
|
|
163
170
|
}
|
|
164
171
|
}
|
|
165
172
|
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
173
|
+
@media (hover: hover) and (pointer: fine) {
|
|
174
|
+
.dark .scroll-screen {
|
|
175
|
+
&::-webkit-scrollbar-thumb {
|
|
176
|
+
background: var(--color-dark-primary);
|
|
177
|
+
border: 3px solid var(--color-primary);
|
|
178
|
+
transition: 0.5s;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
&::-webkit-scrollbar-thumb:hover {
|
|
182
|
+
background: var(--color-primary);
|
|
183
|
+
}
|
|
175
184
|
}
|
|
176
185
|
}
|
|
177
186
|
`;
|
package/package.json
CHANGED
package/registry/components.json
CHANGED
|
@@ -132,13 +132,17 @@
|
|
|
132
132
|
"path": "src/lib/components/filter/filter-complete",
|
|
133
133
|
"dependencies": ["select-dropdown", "select-multi-table", "button", "dialog", "checkbox-switch"]
|
|
134
134
|
},
|
|
135
|
+
"card-skeleton": {
|
|
136
|
+
"path": "src/lib/components/card/card-skeleton",
|
|
137
|
+
"dependencies": []
|
|
138
|
+
},
|
|
135
139
|
"card-crud-complete": {
|
|
136
140
|
"path": "src/lib/components/card/card-crud-complete",
|
|
137
|
-
"dependencies": ["paginator-complete", "filter-complete"]
|
|
141
|
+
"dependencies": ["paginator-complete", "filter-complete", "card-skeleton"]
|
|
138
142
|
},
|
|
139
143
|
"card-complete": {
|
|
140
144
|
"path": "src/lib/components/card/card-complete",
|
|
141
|
-
"dependencies": ["paginator-complete", "filter-complete"]
|
|
145
|
+
"dependencies": ["paginator-complete", "filter-complete", "card-skeleton"]
|
|
142
146
|
},
|
|
143
147
|
"menu-options-table": {
|
|
144
148
|
"path": "src/lib/components/menu/menu-options-table",
|
|
@@ -148,6 +152,10 @@
|
|
|
148
152
|
"path": "src/lib/components/menu/options-coach-menu",
|
|
149
153
|
"dependencies": ["button", "coach-mark"]
|
|
150
154
|
},
|
|
155
|
+
"table-skeleton": {
|
|
156
|
+
"path": "src/lib/components/table/table-skeleton",
|
|
157
|
+
"dependencies": []
|
|
158
|
+
},
|
|
151
159
|
"table-crud-complete": {
|
|
152
160
|
"path": "src/lib/components/table/table-crud-complete",
|
|
153
161
|
"dependencies": [
|
|
@@ -159,7 +167,8 @@
|
|
|
159
167
|
"dialog",
|
|
160
168
|
"viewer-image",
|
|
161
169
|
"select-dropdown",
|
|
162
|
-
"input"
|
|
170
|
+
"input",
|
|
171
|
+
"table-skeleton"
|
|
163
172
|
]
|
|
164
173
|
},
|
|
165
174
|
"table-complete": {
|
|
@@ -512,6 +512,13 @@
|
|
|
512
512
|
}
|
|
513
513
|
}
|
|
514
514
|
|
|
515
|
+
@media (max-width: 400px) {
|
|
516
|
+
.j-alert-toast-stack--top-left,
|
|
517
|
+
.j-alert-toast-stack--top-right {
|
|
518
|
+
top: 3.25rem;
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
|
|
515
522
|
@media (max-width: 450px) {
|
|
516
523
|
.j-alert-toast-stack {
|
|
517
524
|
max-width: calc(100vw - 2rem);
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
>
|
|
7
7
|
@if (stackable && toasts().length > 1 && !expanded) {
|
|
8
8
|
<span
|
|
9
|
-
class="j-alert-toast-stack-hint absolute -top-[0.35rem] -right-[0.35rem] z-50 inline-flex items-center justify-center min-w-5 h-5 px-[0.3rem] rounded-full bg-primary text-primary-foreground text-[0.625rem] font-bold leading-none shadow-[0_2px_8px_rgb(0_0_0/0.18)] pointer-events-none opacity-0 scale-[0.85] transition-[opacity,transform] duration-200 ease-in-out"
|
|
9
|
+
class="j-alert-toast-stack-hint absolute -top-[0.35rem] max-[400px]:-top-[1rem] -right-[0.35rem] z-50 inline-flex items-center justify-center min-w-5 h-5 px-[0.3rem] rounded-full bg-primary text-primary-foreground text-[0.625rem] font-bold leading-none shadow-[0_2px_8px_rgb(0_0_0/0.18)] pointer-events-none opacity-0 scale-[0.85] transition-[opacity,transform] duration-200 ease-in-out"
|
|
10
10
|
aria-hidden="true"
|
|
11
11
|
>
|
|
12
12
|
{{ toasts().length }}
|
|
@@ -43,12 +43,11 @@
|
|
|
43
43
|
</div>
|
|
44
44
|
}
|
|
45
45
|
|
|
46
|
-
@if (
|
|
47
|
-
<div class="
|
|
48
|
-
|
|
49
|
-
<
|
|
50
|
-
|
|
51
|
-
</div>
|
|
46
|
+
@if (showCardSkeletons()) {
|
|
47
|
+
<div class="grid grid-cols-4 max-[1200px]:grid-cols-3 max-[950px]:grid-cols-2 max-[650px]:grid-cols-1 items-start gap-4 my-4">
|
|
48
|
+
@for (slot of skeletonSlots; track slot) {
|
|
49
|
+
<JCardSkeleton />
|
|
50
|
+
}
|
|
52
51
|
</div>
|
|
53
52
|
} @else if (displayData.length > 0) {
|
|
54
53
|
|
|
@@ -8,11 +8,12 @@ import { JCompletePaginatorComponent } from '../../paginator/paginator-complete/
|
|
|
8
8
|
import { JCompleteFilterComponent } from '../../filter/filter-complete/complete-filter.component';
|
|
9
9
|
import { Icons } from '../../.config/icons/icons.lucide';
|
|
10
10
|
import { JIconComponent } from '../../icon/icon.component';
|
|
11
|
+
import { JCardSkeletonComponent } from '../card-skeleton/card-skeleton.component';
|
|
11
12
|
|
|
12
13
|
@Component({
|
|
13
14
|
selector: 'JCompleteCard',
|
|
14
15
|
standalone: true,
|
|
15
|
-
imports: [CommonModule, FormsModule, JCompletePaginatorComponent, JCompleteFilterComponent, JIconComponent],
|
|
16
|
+
imports: [CommonModule, FormsModule, JCompletePaginatorComponent, JCompleteFilterComponent, JIconComponent, JCardSkeletonComponent],
|
|
16
17
|
templateUrl: './complete-card.component.html',
|
|
17
18
|
styleUrl: './complete-card.component.scss',
|
|
18
19
|
animations: [
|
|
@@ -566,6 +567,16 @@ export class JCompleteCardComponent implements OnInit {
|
|
|
566
567
|
// Loading parameters
|
|
567
568
|
// =====================================================
|
|
568
569
|
|
|
570
|
+
/** Cantidad de placeholders skeleton durante la carga inicial. */
|
|
571
|
+
get skeletonSlots(): number[] {
|
|
572
|
+
const count = Math.min(this.itemsPerPage, 8);
|
|
573
|
+
return Array.from({ length: count }, (_, index) => index);
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
showCardSkeletons(): boolean {
|
|
577
|
+
return this.isLoading('initialLoad') && this.displayData.length === 0;
|
|
578
|
+
}
|
|
579
|
+
|
|
569
580
|
// MMethod to check if a specific state is loading
|
|
570
581
|
isLoading(state: keyof LoadingStates): boolean {
|
|
571
582
|
return this.loadingStates[state] === 'loading';
|
|
@@ -43,12 +43,21 @@
|
|
|
43
43
|
</div>
|
|
44
44
|
}
|
|
45
45
|
|
|
46
|
-
@if (
|
|
47
|
-
<div
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
46
|
+
@if (showCardSkeletons()) {
|
|
47
|
+
<div
|
|
48
|
+
class="grid grid-cols-4 max-[1200px]:grid-cols-3 max-[950px]:grid-cols-2 max-[650px]:grid-cols-1 gap-4 my-4"
|
|
49
|
+
[class.items-stretch]="useStretchCards"
|
|
50
|
+
[class.items-start]="!useStretchCards"
|
|
51
|
+
>
|
|
52
|
+
@for (slot of skeletonSlots; track slot) {
|
|
53
|
+
<div
|
|
54
|
+
class="flex w-full flex-col"
|
|
55
|
+
[class.h-full]="useStretchCards"
|
|
56
|
+
[class.self-start]="!useStretchCards"
|
|
57
|
+
>
|
|
58
|
+
<JCardSkeleton />
|
|
59
|
+
</div>
|
|
60
|
+
}
|
|
52
61
|
</div>
|
|
53
62
|
} @else if (displayData.length > 0) {
|
|
54
63
|
|
|
@@ -94,7 +103,7 @@
|
|
|
94
103
|
|
|
95
104
|
} @else if (!isLoading('pagination')) {
|
|
96
105
|
<div class="j-crud-card w-full flex flex-col gap-3 justify-center items-center h-100 bg-white dark:bg-foreground rounded-[20px] border border-border dark:border-dark-border hover:border-primary/50 hover:dark:border-primary">
|
|
97
|
-
<JIcon [icon]="Icons.Info" size="35" iconClass="text-primary" [ariaHidden]="true" />
|
|
106
|
+
<JIcon [icon]="Icons.Info" size="35" iconClass="text-primary" [ariaHidden]="true" [inheritParentColor]="true" />
|
|
98
107
|
<p class="text-black dark:text-white">No hay datos disponibles</p>
|
|
99
108
|
</div>
|
|
100
109
|
}
|
|
@@ -8,11 +8,12 @@ import { JCompletePaginatorComponent } from '../../paginator/paginator-complete/
|
|
|
8
8
|
import { JCompleteFilterComponent } from '../../filter/filter-complete/complete-filter.component';
|
|
9
9
|
import { Icons } from '../../.config/icons/icons.lucide';
|
|
10
10
|
import { JIconComponent } from '../../icon/icon.component';
|
|
11
|
+
import { JCardSkeletonComponent } from '../card-skeleton/card-skeleton.component';
|
|
11
12
|
|
|
12
13
|
@Component({
|
|
13
14
|
selector: 'JCompleteCrudCard',
|
|
14
15
|
standalone: true,
|
|
15
|
-
imports: [CommonModule, FormsModule, JCompletePaginatorComponent, JCompleteFilterComponent, JIconComponent],
|
|
16
|
+
imports: [CommonModule, FormsModule, JCompletePaginatorComponent, JCompleteFilterComponent, JIconComponent, JCardSkeletonComponent],
|
|
16
17
|
templateUrl: './complete-crud-card.component.html',
|
|
17
18
|
styleUrl: './complete-crud-card.component.scss',
|
|
18
19
|
animations: [
|
|
@@ -572,6 +573,16 @@ export class JCompleteCrudCardComponent implements OnInit {
|
|
|
572
573
|
// Loading parameters
|
|
573
574
|
// =====================================================
|
|
574
575
|
|
|
576
|
+
/** Cantidad de placeholders skeleton durante la carga inicial. */
|
|
577
|
+
get skeletonSlots(): number[] {
|
|
578
|
+
const count = Math.min(this.itemsPerPage, 8);
|
|
579
|
+
return Array.from({ length: count }, (_, index) => index);
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
showCardSkeletons(): boolean {
|
|
583
|
+
return this.isLoading('initialLoad') && this.displayData.length === 0;
|
|
584
|
+
}
|
|
585
|
+
|
|
575
586
|
// MMethod to check if a specific state is loading
|
|
576
587
|
isLoading(state: keyof LoadingStates): boolean {
|
|
577
588
|
return this.loadingStates[state] === 'loading';
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
<article
|
|
2
|
+
class="flex min-h-[10.5rem] w-full overflow-hidden rounded-xl border border-border dark:border-dark-border bg-white dark:bg-foreground animate-pulse"
|
|
3
|
+
aria-hidden="true"
|
|
4
|
+
>
|
|
5
|
+
<div class="w-1 shrink-0 rounded-l-xl bg-muted/50 dark:bg-dark-muted/40"></div>
|
|
6
|
+
|
|
7
|
+
<div class="flex min-w-0 flex-1 flex-col gap-2.5 p-3.5">
|
|
8
|
+
<header class="flex min-w-0 items-start gap-2">
|
|
9
|
+
<div
|
|
10
|
+
class="h-8 w-8 shrink-0 rounded-lg border border-dashed border-border/60 bg-muted/30 dark:border-dark-border/60 dark:bg-dark-muted/20"
|
|
11
|
+
></div>
|
|
12
|
+
<div class="min-w-0 flex-1 space-y-2">
|
|
13
|
+
<div class="flex flex-wrap gap-1">
|
|
14
|
+
<div class="h-4 w-14 rounded bg-muted/40 dark:bg-dark-muted/30"></div>
|
|
15
|
+
<div class="h-4 w-16 rounded bg-muted/40 dark:bg-dark-muted/30"></div>
|
|
16
|
+
</div>
|
|
17
|
+
<div class="h-4 w-[80%] max-w-[12rem] rounded bg-muted/40 dark:bg-dark-muted/30"></div>
|
|
18
|
+
</div>
|
|
19
|
+
</header>
|
|
20
|
+
|
|
21
|
+
<div class="space-y-2">
|
|
22
|
+
<div class="h-3 w-full rounded bg-muted/30 dark:bg-dark-muted/25"></div>
|
|
23
|
+
<div class="h-3 w-[92%] rounded bg-muted/30 dark:bg-dark-muted/25"></div>
|
|
24
|
+
<div class="h-3 w-[66%] rounded bg-muted/30 dark:bg-dark-muted/25"></div>
|
|
25
|
+
</div>
|
|
26
|
+
|
|
27
|
+
<div
|
|
28
|
+
class="mt-auto flex items-center justify-between gap-2 border-t border-border/50 pt-2.5 dark:border-dark-border/50"
|
|
29
|
+
>
|
|
30
|
+
<div class="flex items-center gap-1.5">
|
|
31
|
+
<div class="h-3 w-3 rounded-full bg-muted/40 dark:bg-dark-muted/30"></div>
|
|
32
|
+
<div class="h-3 w-20 rounded bg-muted/40 dark:bg-dark-muted/30"></div>
|
|
33
|
+
</div>
|
|
34
|
+
<div class="flex shrink-0 gap-1">
|
|
35
|
+
<div
|
|
36
|
+
class="h-7 w-7 rounded-full border border-dashed border-border/60 bg-muted/30 dark:border-dark-border/60 dark:bg-dark-muted/20"
|
|
37
|
+
></div>
|
|
38
|
+
<div
|
|
39
|
+
class="h-7 w-7 rounded-full border border-dashed border-border/60 bg-muted/30 dark:border-dark-border/60 dark:bg-dark-muted/20"
|
|
40
|
+
></div>
|
|
41
|
+
<div
|
|
42
|
+
class="h-7 w-7 rounded-full border border-dashed border-border/60 bg-muted/30 dark:border-dark-border/60 dark:bg-dark-muted/20"
|
|
43
|
+
></div>
|
|
44
|
+
</div>
|
|
45
|
+
</div>
|
|
46
|
+
</div>
|
|
47
|
+
</article>
|
|
@@ -24,20 +24,25 @@
|
|
|
24
24
|
[style.min-height]="!isFullScreen ? '40px' : null"
|
|
25
25
|
>
|
|
26
26
|
<div
|
|
27
|
-
class="jdialog-header flex min-h-[
|
|
27
|
+
class="jdialog-header flex min-h-[3rem] select-none items-center gap-3 border-b border-border bg-primary py-1.5 pr-3 pl-4 dark:border-dark-border dark:bg-[linear-gradient(180deg,color-mix(in_srgb,var(--color-dark-muted)_35%,var(--color-dark-background))_0%,var(--color-dark-background)_100%)]"
|
|
28
28
|
[class.jdialog-header--draggable]="draggable"
|
|
29
|
-
[class.cursor-move]="draggable"
|
|
30
|
-
(mousedown)="draggable && startDrag($event)"
|
|
31
29
|
>
|
|
32
30
|
<h3
|
|
33
31
|
class="jdialog-title m-0 min-w-0 flex-1 text-[0.9375rem] font-semibold leading-[1.3] tracking-[-0.015em] text-white dark:text-dark-foreground"
|
|
32
|
+
[class.jdialog-header-drag]="draggable"
|
|
33
|
+
[class.cursor-move]="draggable"
|
|
34
|
+
(mousedown)="onDragMouseDown($event)"
|
|
35
|
+
(touchstart)="onDragTouchStart($event)"
|
|
34
36
|
>
|
|
35
37
|
{{ title }}
|
|
36
38
|
</h3>
|
|
37
39
|
|
|
38
40
|
<button
|
|
39
41
|
type="button"
|
|
40
|
-
class="inline-flex h-8 w-8 shrink-0 cursor-pointer items-center justify-center rounded-md border-0 bg-dark-primary/30 dark:bg-primary/30 p-0 leading-none text-white/70 transition-[color,background-color,transform] duration-150 hover:bg-dark-primary/80 hover:dark:bg-primary/80 hover:text-white active:scale-[0.94] focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ring dark:hover:text-dark-foreground"
|
|
42
|
+
class="jdialog-close inline-flex h-8 w-8 shrink-0 cursor-pointer items-center justify-center rounded-md border-0 bg-dark-primary/30 dark:bg-primary/30 p-0 leading-none text-white/70 transition-[color,background-color,transform] duration-150 hover:bg-dark-primary/80 hover:dark:bg-primary/80 hover:text-white active:scale-[0.94] focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ring dark:hover:text-dark-foreground"
|
|
43
|
+
(mousedown)="$event.stopPropagation()"
|
|
44
|
+
(touchstart)="$event.stopPropagation()"
|
|
45
|
+
(pointerdown)="$event.stopPropagation()"
|
|
41
46
|
(click)="$event.stopPropagation(); onClose()"
|
|
42
47
|
aria-label="Cerrar"
|
|
43
48
|
>
|
|
@@ -5,6 +5,7 @@ import {
|
|
|
5
5
|
HostListener,
|
|
6
6
|
Input,
|
|
7
7
|
OnChanges,
|
|
8
|
+
OnDestroy,
|
|
8
9
|
Output,
|
|
9
10
|
SimpleChanges,
|
|
10
11
|
TemplateRef,
|
|
@@ -50,7 +51,10 @@ const POSITION_CLASS_MAP: Record<DialogPosition, string> = {
|
|
|
50
51
|
]),
|
|
51
52
|
],
|
|
52
53
|
})
|
|
53
|
-
export class JDialogComponent implements OnChanges {
|
|
54
|
+
export class JDialogComponent implements OnChanges, OnDestroy {
|
|
55
|
+
private static readonly TOUCH_DRAG_HOLD_MS = 400;
|
|
56
|
+
private static readonly TOUCH_DRAG_CANCEL_PX = 12;
|
|
57
|
+
|
|
54
58
|
readonly Icons = Icons;
|
|
55
59
|
|
|
56
60
|
@Input() position: DialogPosition = 'center';
|
|
@@ -75,11 +79,26 @@ export class JDialogComponent implements OnChanges {
|
|
|
75
79
|
private isDragging = false;
|
|
76
80
|
private hasMoved = false;
|
|
77
81
|
private dragOffset = { x: 0, y: 0 };
|
|
82
|
+
private touchHoldTimer: ReturnType<typeof setTimeout> | null = null;
|
|
83
|
+
private touchStartPoint = { x: 0, y: 0 };
|
|
84
|
+
private releaseDocumentListeners: (() => void) | null = null;
|
|
85
|
+
private touchHoldCleanup: (() => void) | null = null;
|
|
78
86
|
|
|
79
87
|
ngOnChanges(changes: SimpleChanges): void {
|
|
80
88
|
if (changes['openModal']?.currentValue === true) {
|
|
81
89
|
this.hasMoved = false;
|
|
82
90
|
}
|
|
91
|
+
|
|
92
|
+
if (changes['openModal']?.currentValue === false) {
|
|
93
|
+
this.clearTouchHold();
|
|
94
|
+
this.clearDocumentListeners();
|
|
95
|
+
this.isDragging = false;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
ngOnDestroy(): void {
|
|
100
|
+
this.clearTouchHold();
|
|
101
|
+
this.clearDocumentListeners();
|
|
83
102
|
}
|
|
84
103
|
|
|
85
104
|
/**
|
|
@@ -169,6 +188,7 @@ export class JDialogComponent implements OnChanges {
|
|
|
169
188
|
'border': true,
|
|
170
189
|
'border-border': true,
|
|
171
190
|
'dark:border-dark-border': true,
|
|
191
|
+
'jdialog-panel--dragging': this.isDragging,
|
|
172
192
|
...this.ngClasses,
|
|
173
193
|
};
|
|
174
194
|
}
|
|
@@ -190,27 +210,141 @@ export class JDialogComponent implements OnChanges {
|
|
|
190
210
|
}
|
|
191
211
|
|
|
192
212
|
/**
|
|
193
|
-
* Starts dragging
|
|
213
|
+
* Starts dragging from the title handle (mouse).
|
|
194
214
|
*/
|
|
195
|
-
|
|
196
|
-
if (!this.draggable) {
|
|
215
|
+
onDragMouseDown(event: MouseEvent): void {
|
|
216
|
+
if (!this.draggable || event.button !== 0) {
|
|
217
|
+
return;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
event.preventDefault();
|
|
221
|
+
event.stopPropagation();
|
|
222
|
+
|
|
223
|
+
const dialogElement = this.getDialogElement(event.currentTarget as HTMLElement);
|
|
224
|
+
if (!dialogElement) {
|
|
225
|
+
return;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
this.beginDragSession(dialogElement, event.clientX, event.clientY);
|
|
229
|
+
|
|
230
|
+
const mouseMoveHandler = (moveEvent: MouseEvent) => {
|
|
231
|
+
if (!this.isDragging) {
|
|
232
|
+
return;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
this.applyDragPosition(dialogElement, moveEvent.clientX, moveEvent.clientY);
|
|
236
|
+
};
|
|
237
|
+
|
|
238
|
+
const mouseUpHandler = () => {
|
|
239
|
+
this.isDragging = false;
|
|
240
|
+
this.clearDocumentListeners();
|
|
241
|
+
};
|
|
242
|
+
|
|
243
|
+
this.setDocumentListeners(() => {
|
|
244
|
+
document.removeEventListener('mousemove', mouseMoveHandler);
|
|
245
|
+
document.removeEventListener('mouseup', mouseUpHandler);
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
document.addEventListener('mousemove', mouseMoveHandler);
|
|
249
|
+
document.addEventListener('mouseup', mouseUpHandler);
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/**
|
|
253
|
+
* Long-press on the title handle, then drag (touch / tablet).
|
|
254
|
+
*/
|
|
255
|
+
onDragTouchStart(event: TouchEvent): void {
|
|
256
|
+
if (!this.draggable || event.touches.length !== 1) {
|
|
197
257
|
return;
|
|
198
258
|
}
|
|
199
259
|
|
|
200
260
|
event.stopPropagation();
|
|
201
|
-
this.isDragging = true;
|
|
202
261
|
|
|
203
|
-
const dialogElement = (event.currentTarget as HTMLElement)
|
|
204
|
-
'[data-draggable-dialog]',
|
|
205
|
-
) as HTMLElement | null;
|
|
262
|
+
const dialogElement = this.getDialogElement(event.currentTarget as HTMLElement);
|
|
206
263
|
if (!dialogElement) {
|
|
207
264
|
return;
|
|
208
265
|
}
|
|
209
266
|
|
|
267
|
+
const touch = event.touches[0];
|
|
268
|
+
this.touchStartPoint = { x: touch.clientX, y: touch.clientY };
|
|
269
|
+
this.clearTouchHold();
|
|
270
|
+
|
|
271
|
+
const cancelHoldIfMoved = (moveEvent: TouchEvent) => {
|
|
272
|
+
const activeTouch = moveEvent.touches[0];
|
|
273
|
+
if (!activeTouch) {
|
|
274
|
+
return;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
const dx = Math.abs(activeTouch.clientX - this.touchStartPoint.x);
|
|
278
|
+
const dy = Math.abs(activeTouch.clientY - this.touchStartPoint.y);
|
|
279
|
+
|
|
280
|
+
if (dx > JDialogComponent.TOUCH_DRAG_CANCEL_PX || dy > JDialogComponent.TOUCH_DRAG_CANCEL_PX) {
|
|
281
|
+
this.clearTouchHold();
|
|
282
|
+
}
|
|
283
|
+
};
|
|
284
|
+
|
|
285
|
+
const cancelHold = () => {
|
|
286
|
+
this.clearTouchHold();
|
|
287
|
+
};
|
|
288
|
+
|
|
289
|
+
document.addEventListener('touchmove', cancelHoldIfMoved, { passive: true });
|
|
290
|
+
document.addEventListener('touchend', cancelHold);
|
|
291
|
+
document.addEventListener('touchcancel', cancelHold);
|
|
292
|
+
|
|
293
|
+
this.touchHoldCleanup = () => {
|
|
294
|
+
document.removeEventListener('touchmove', cancelHoldIfMoved);
|
|
295
|
+
document.removeEventListener('touchend', cancelHold);
|
|
296
|
+
document.removeEventListener('touchcancel', cancelHold);
|
|
297
|
+
};
|
|
298
|
+
|
|
299
|
+
this.touchHoldTimer = setTimeout(() => {
|
|
300
|
+
this.touchHoldCleanup?.();
|
|
301
|
+
this.touchHoldCleanup = null;
|
|
302
|
+
this.touchHoldTimer = null;
|
|
303
|
+
|
|
304
|
+
this.beginDragSession(dialogElement, touch.clientX, touch.clientY);
|
|
305
|
+
|
|
306
|
+
const touchMoveHandler = (moveEvent: TouchEvent) => {
|
|
307
|
+
if (!this.isDragging || moveEvent.touches.length !== 1) {
|
|
308
|
+
return;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
moveEvent.preventDefault();
|
|
312
|
+
const activeTouch = moveEvent.touches[0];
|
|
313
|
+
this.applyDragPosition(dialogElement, activeTouch.clientX, activeTouch.clientY);
|
|
314
|
+
};
|
|
315
|
+
|
|
316
|
+
const touchEndHandler = () => {
|
|
317
|
+
this.isDragging = false;
|
|
318
|
+
this.clearDocumentListeners();
|
|
319
|
+
};
|
|
320
|
+
|
|
321
|
+
this.setDocumentListeners(() => {
|
|
322
|
+
document.removeEventListener('touchmove', touchMoveHandler);
|
|
323
|
+
document.removeEventListener('touchend', touchEndHandler);
|
|
324
|
+
document.removeEventListener('touchcancel', touchEndHandler);
|
|
325
|
+
});
|
|
326
|
+
|
|
327
|
+
document.addEventListener('touchmove', touchMoveHandler, { passive: false });
|
|
328
|
+
document.addEventListener('touchend', touchEndHandler);
|
|
329
|
+
document.addEventListener('touchcancel', touchEndHandler);
|
|
330
|
+
}, JDialogComponent.TOUCH_DRAG_HOLD_MS);
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
private getDialogElement(handle: HTMLElement): HTMLElement | null {
|
|
334
|
+
return handle.closest('[data-draggable-dialog]') as HTMLElement | null;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
private beginDragSession(
|
|
338
|
+
dialogElement: HTMLElement,
|
|
339
|
+
clientX: number,
|
|
340
|
+
clientY: number,
|
|
341
|
+
): void {
|
|
342
|
+
this.isDragging = true;
|
|
343
|
+
|
|
210
344
|
const rect = dialogElement.getBoundingClientRect();
|
|
211
345
|
this.dragOffset = {
|
|
212
|
-
x:
|
|
213
|
-
y:
|
|
346
|
+
x: clientX - rect.left,
|
|
347
|
+
y: clientY - rect.top,
|
|
214
348
|
};
|
|
215
349
|
|
|
216
350
|
if (!this.hasMoved) {
|
|
@@ -226,29 +360,40 @@ export class JDialogComponent implements OnChanges {
|
|
|
226
360
|
|
|
227
361
|
this.hasMoved = true;
|
|
228
362
|
}
|
|
363
|
+
}
|
|
229
364
|
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
365
|
+
private applyDragPosition(
|
|
366
|
+
dialogElement: HTMLElement,
|
|
367
|
+
clientX: number,
|
|
368
|
+
clientY: number,
|
|
369
|
+
): void {
|
|
370
|
+
const newLeft = clientX - this.dragOffset.x;
|
|
371
|
+
const newTop = clientY - this.dragOffset.y;
|
|
234
372
|
|
|
235
|
-
|
|
236
|
-
|
|
373
|
+
const maxLeft = window.innerWidth - dialogElement.offsetWidth;
|
|
374
|
+
const maxTop = window.innerHeight - dialogElement.offsetHeight;
|
|
237
375
|
|
|
238
|
-
|
|
239
|
-
|
|
376
|
+
dialogElement.style.left = `${Math.min(Math.max(newLeft, 0), maxLeft)}px`;
|
|
377
|
+
dialogElement.style.top = `${Math.min(Math.max(newTop, 0), maxTop)}px`;
|
|
378
|
+
}
|
|
240
379
|
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
380
|
+
private setDocumentListeners(release: () => void): void {
|
|
381
|
+
this.clearDocumentListeners();
|
|
382
|
+
this.releaseDocumentListeners = release;
|
|
383
|
+
}
|
|
244
384
|
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
};
|
|
385
|
+
private clearDocumentListeners(): void {
|
|
386
|
+
this.releaseDocumentListeners?.();
|
|
387
|
+
this.releaseDocumentListeners = null;
|
|
388
|
+
}
|
|
250
389
|
|
|
251
|
-
|
|
252
|
-
|
|
390
|
+
private clearTouchHold(): void {
|
|
391
|
+
if (this.touchHoldTimer) {
|
|
392
|
+
clearTimeout(this.touchHoldTimer);
|
|
393
|
+
this.touchHoldTimer = null;
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
this.touchHoldCleanup?.();
|
|
397
|
+
this.touchHoldCleanup = null;
|
|
253
398
|
}
|
|
254
399
|
}
|
|
@@ -18,28 +18,31 @@
|
|
|
18
18
|
|
|
19
19
|
.scroll-element {
|
|
20
20
|
margin-right: 2px;
|
|
21
|
+
-webkit-overflow-scrolling: touch;
|
|
22
|
+
}
|
|
21
23
|
|
|
22
|
-
|
|
24
|
+
@media (hover: hover) and (pointer: fine) {
|
|
25
|
+
.scroll-element::-webkit-scrollbar {
|
|
23
26
|
width: 8px !important;
|
|
24
27
|
}
|
|
25
28
|
|
|
26
|
-
|
|
29
|
+
.scroll-element::-webkit-scrollbar-thumb {
|
|
27
30
|
border-radius: 10px;
|
|
28
31
|
background: color-mix(in srgb, var(--color-primary) 65%, transparent);
|
|
29
32
|
transition: background-color 0.5s ease;
|
|
30
33
|
}
|
|
31
34
|
|
|
32
|
-
|
|
35
|
+
.scroll-element::-webkit-scrollbar-thumb:hover {
|
|
33
36
|
background: var(--color-primary);
|
|
34
37
|
}
|
|
35
|
-
}
|
|
36
38
|
|
|
37
|
-
:host-context(.dark) .scroll-element::-webkit-scrollbar-thumb,
|
|
38
|
-
:host-context(html.dark) .scroll-element::-webkit-scrollbar-thumb {
|
|
39
|
-
|
|
40
|
-
}
|
|
39
|
+
:host-context(.dark) .scroll-element::-webkit-scrollbar-thumb,
|
|
40
|
+
:host-context(html.dark) .scroll-element::-webkit-scrollbar-thumb {
|
|
41
|
+
background: color-mix(in srgb, var(--color-dark-primary) 45%, transparent);
|
|
42
|
+
}
|
|
41
43
|
|
|
42
|
-
:host-context(.dark) .scroll-element::-webkit-scrollbar-thumb:hover,
|
|
43
|
-
:host-context(html.dark) .scroll-element::-webkit-scrollbar-thumb:hover {
|
|
44
|
-
|
|
44
|
+
:host-context(.dark) .scroll-element::-webkit-scrollbar-thumb:hover,
|
|
45
|
+
:host-context(html.dark) .scroll-element::-webkit-scrollbar-thumb:hover {
|
|
46
|
+
background: color-mix(in srgb, var(--color-dark-primary) 70%, transparent);
|
|
47
|
+
}
|
|
45
48
|
}
|
|
@@ -182,23 +182,18 @@
|
|
|
182
182
|
</thead>
|
|
183
183
|
|
|
184
184
|
<tbody class="bg-white dark:bg-foreground text-black dark:text-white">
|
|
185
|
-
@if (
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
[
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
iconClass="text-primary animate-spin" [ariaHidden]="true" />
|
|
198
|
-
<p>Cargando datos...</p>
|
|
199
|
-
</div>
|
|
200
|
-
</td>
|
|
201
|
-
</tr>
|
|
185
|
+
@if (showTableSkeletons()) {
|
|
186
|
+
@for (slot of skeletonSlots; track slot) {
|
|
187
|
+
<tr
|
|
188
|
+
jTableSkeletonDesktopRow
|
|
189
|
+
[columnCount]="getVisibleColumnsCount()"
|
|
190
|
+
[columnSlots]="skeletonColumnSlots"
|
|
191
|
+
[isNumbering]="isNumbering"
|
|
192
|
+
[isOptions]="isOptions"
|
|
193
|
+
[hasExpandable]="hasExpandable()"
|
|
194
|
+
[variant]="skeletonDesktopVariant"
|
|
195
|
+
></tr>
|
|
196
|
+
}
|
|
202
197
|
} @else if (displayData.length > 0) {
|
|
203
198
|
<!-- Data rows -->
|
|
204
199
|
@for (item of displayData; track $index) {
|
|
@@ -944,20 +939,8 @@
|
|
|
944
939
|
</table>
|
|
945
940
|
</div>
|
|
946
941
|
|
|
947
|
-
<!-- Add pause of charge with avoid flickering -->
|
|
948
|
-
@if (isLoading("initialLoad") && displayData.length === 0) {
|
|
949
|
-
<div
|
|
950
|
-
class="absolute inset-0 flex flex-col gap-3 items-center justify-center py-4 bg-white/80 dark:bg-foreground/80 backdrop-blur-sm z-501 select-none"
|
|
951
|
-
>
|
|
952
|
-
<JIcon [icon]="Icons.Loader2"
|
|
953
|
-
size="35"
|
|
954
|
-
iconClass="text-primary animate-spin" [ariaHidden]="true" />
|
|
955
|
-
<p class="text-black dark:text-white">Cargando datos...</p>
|
|
956
|
-
</div>
|
|
957
|
-
}
|
|
958
|
-
|
|
959
942
|
<!-- Not found data message -->
|
|
960
|
-
@
|
|
943
|
+
@if (!showTableSkeletons() && !displayData.length && !isLoading("pagination")) {
|
|
961
944
|
<div
|
|
962
945
|
class="absolute inset-0 flex flex-col gap-3 items-center justify-center py-4 bg-white/80 dark:bg-foreground/80 backdrop-blur-sm z-501 select-none rounded"
|
|
963
946
|
>
|
|
@@ -972,15 +955,13 @@
|
|
|
972
955
|
<!-- TABLE MOVILE -->
|
|
973
956
|
<div class="hidden max-[768px]:block">
|
|
974
957
|
<div class="flex flex-col gap-3">
|
|
975
|
-
@if (
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
<p class="text-black dark:text-white">Cargando datos...</p>
|
|
983
|
-
</div>
|
|
958
|
+
@if (showTableSkeletons()) {
|
|
959
|
+
@for (slot of skeletonSlots; track slot) {
|
|
960
|
+
<JTableSkeletonMobileCard
|
|
961
|
+
[isNumbering]="isNumbering"
|
|
962
|
+
[variant]="skeletonDesktopVariant"
|
|
963
|
+
/>
|
|
964
|
+
}
|
|
984
965
|
} @else if (displayData.length > 0) {
|
|
985
966
|
@if (isGroupTable) {
|
|
986
967
|
@for (item of displayData; track $index) {
|
|
@@ -39,6 +39,8 @@ import type {
|
|
|
39
39
|
CrudTableGroupTableConfig} from './complete-crud-table.types';
|
|
40
40
|
import { Icons } from '../../.config/icons/icons.lucide';
|
|
41
41
|
import { JIconComponent } from '../../icon/icon.component';
|
|
42
|
+
import { JTableSkeletonDesktopRowComponent } from '../table-skeleton/table-skeleton-desktop-row.component';
|
|
43
|
+
import { JTableSkeletonMobileCardComponent } from '../table-skeleton/table-skeleton-mobile-card.component';
|
|
42
44
|
import {
|
|
43
45
|
applyColumnDefaults,
|
|
44
46
|
applyNestedBooleanToggle,
|
|
@@ -161,6 +163,8 @@ export { CRUD_TABLE_ANIMATIONS } from './complete-crud-table.animations';
|
|
|
161
163
|
JDropdownSelectComponent,
|
|
162
164
|
JInputComponent,
|
|
163
165
|
JOptionsCoachMenuComponent,
|
|
166
|
+
JTableSkeletonDesktopRowComponent,
|
|
167
|
+
JTableSkeletonMobileCardComponent,
|
|
164
168
|
],
|
|
165
169
|
templateUrl: './complete-crud-table.component.html',
|
|
166
170
|
styleUrl: './complete-crud-table.component.scss',
|
|
@@ -798,6 +802,23 @@ export class JCompleteCrudTableComponent implements OnInit {
|
|
|
798
802
|
// Loading state
|
|
799
803
|
// =====================================================
|
|
800
804
|
|
|
805
|
+
get skeletonSlots(): number[] {
|
|
806
|
+
const count = Math.min(this.itemsPerPage, 8);
|
|
807
|
+
return Array.from({ length: count }, (_, index) => index);
|
|
808
|
+
}
|
|
809
|
+
|
|
810
|
+
get skeletonColumnSlots(): number[] {
|
|
811
|
+
return Array.from({ length: this.getVisibleColumnsCount() }, (_, index) => index);
|
|
812
|
+
}
|
|
813
|
+
|
|
814
|
+
showTableSkeletons(): boolean {
|
|
815
|
+
return this.isLoading('initialLoad') && this.displayData.length === 0;
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
get skeletonDesktopVariant(): 'default' | 'group' {
|
|
819
|
+
return this.isGroupTable ? 'group' : 'default';
|
|
820
|
+
}
|
|
821
|
+
|
|
801
822
|
isLoading(state: keyof LoadingStates): boolean {
|
|
802
823
|
return this.loadingStates[state] === 'loading';
|
|
803
824
|
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
@if (variant === 'group') {
|
|
2
|
+
<td
|
|
3
|
+
class="j-group-number-col box-border w-11 min-w-11 max-w-11 !px-[0.35rem] h-[50px] border-b border-border px-2 py-2 text-center dark:border-dark-border bg-primary/5 dark:bg-primary/10"
|
|
4
|
+
>
|
|
5
|
+
<div class="mx-auto h-4 w-4 rounded bg-muted/40 dark:bg-dark-muted/30"></div>
|
|
6
|
+
</td>
|
|
7
|
+
<td
|
|
8
|
+
[attr.colspan]="columnCount"
|
|
9
|
+
class="border-b border-border px-4 py-3 dark:border-dark-border bg-primary/5 dark:bg-primary/10"
|
|
10
|
+
>
|
|
11
|
+
<div class="flex items-center gap-5">
|
|
12
|
+
<div
|
|
13
|
+
class="h-9 w-9 shrink-0 rounded-full border border-dashed border-border/60 bg-muted/30 dark:border-dark-border/60 dark:bg-dark-muted/20"
|
|
14
|
+
></div>
|
|
15
|
+
<div class="min-w-0 flex-1 space-y-2">
|
|
16
|
+
<div class="h-4 w-40 max-w-[70%] rounded bg-muted/40 dark:bg-dark-muted/30"></div>
|
|
17
|
+
<div class="h-3 w-28 max-w-[55%] rounded bg-muted/30 dark:bg-dark-muted/25"></div>
|
|
18
|
+
</div>
|
|
19
|
+
</div>
|
|
20
|
+
</td>
|
|
21
|
+
|
|
22
|
+
@if (isOptions) {
|
|
23
|
+
<td
|
|
24
|
+
class="j-table-options-col j-group-pagination-cell box-border h-[50px] w-[var(--j-table-options-col-width)] min-w-[var(--j-table-options-col-width)] max-w-[var(--j-table-options-col-width)] border-b border-border bg-primary/5 px-4 py-2 text-center dark:border-dark-border dark:bg-primary/10"
|
|
25
|
+
>
|
|
26
|
+
<div class="mx-auto flex flex-col items-center gap-1">
|
|
27
|
+
<div class="h-4 w-16 rounded-full bg-muted/30 dark:bg-dark-muted/25"></div>
|
|
28
|
+
<div class="flex gap-1">
|
|
29
|
+
<div class="h-5 w-7 rounded bg-muted/40 dark:bg-dark-muted/30"></div>
|
|
30
|
+
<div class="h-5 w-7 rounded bg-muted/40 dark:bg-dark-muted/30"></div>
|
|
31
|
+
</div>
|
|
32
|
+
</div>
|
|
33
|
+
</td>
|
|
34
|
+
}
|
|
35
|
+
} @else {
|
|
36
|
+
@if (hasExpandable) {
|
|
37
|
+
<td class="h-[50px] border-b border-border px-4 py-2 text-center dark:border-dark-border">
|
|
38
|
+
<div class="mx-auto h-4 w-4 rounded bg-muted/40 dark:bg-dark-muted/30"></div>
|
|
39
|
+
</td>
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
@if (isNumbering) {
|
|
43
|
+
<td
|
|
44
|
+
class="j-group-number-col box-border h-[50px] w-11 min-w-11 max-w-11 border-b border-border !px-[0.35rem] px-4 py-2 text-center dark:border-dark-border"
|
|
45
|
+
>
|
|
46
|
+
<div class="mx-auto h-3 w-5 rounded bg-muted/40 dark:bg-dark-muted/30"></div>
|
|
47
|
+
</td>
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
@for (col of columnSlots; track col) {
|
|
51
|
+
<td class="h-[50px] border-b border-border px-4 py-2 dark:border-dark-border">
|
|
52
|
+
<div
|
|
53
|
+
class="h-3 rounded bg-muted/40 dark:bg-dark-muted/30"
|
|
54
|
+
[class.w-full]="col % 3 !== 0"
|
|
55
|
+
[class.w-[72%]]="col % 3 === 0"
|
|
56
|
+
></div>
|
|
57
|
+
</td>
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
@if (isOptions) {
|
|
61
|
+
<td
|
|
62
|
+
class="j-table-options-col box-border h-[50px] w-[var(--j-table-options-col-width)] min-w-[var(--j-table-options-col-width)] max-w-[var(--j-table-options-col-width)] border-b border-border px-4 py-2 text-center dark:border-dark-border"
|
|
63
|
+
>
|
|
64
|
+
<div class="flex items-center justify-center gap-1">
|
|
65
|
+
<div
|
|
66
|
+
class="h-7 w-7 rounded-full border border-dashed border-border/60 bg-muted/30 dark:border-dark-border/60 dark:bg-dark-muted/20"
|
|
67
|
+
></div>
|
|
68
|
+
<div
|
|
69
|
+
class="h-7 w-7 rounded-full border border-dashed border-border/60 bg-muted/30 dark:border-dark-border/60 dark:bg-dark-muted/20"
|
|
70
|
+
></div>
|
|
71
|
+
</div>
|
|
72
|
+
</td>
|
|
73
|
+
}
|
|
74
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { Component, Input } from '@angular/core';
|
|
2
|
+
|
|
3
|
+
@Component({
|
|
4
|
+
selector: 'tr[jTableSkeletonDesktopRow]',
|
|
5
|
+
standalone: true,
|
|
6
|
+
templateUrl: './table-skeleton-desktop-row.component.html',
|
|
7
|
+
host: {
|
|
8
|
+
class: 'animate-pulse',
|
|
9
|
+
'aria-hidden': 'true',
|
|
10
|
+
},
|
|
11
|
+
})
|
|
12
|
+
export class JTableSkeletonDesktopRowComponent {
|
|
13
|
+
@Input({ required: true }) columnCount = 1;
|
|
14
|
+
@Input() columnSlots: number[] = [];
|
|
15
|
+
@Input() isNumbering = true;
|
|
16
|
+
@Input() isOptions = true;
|
|
17
|
+
@Input() hasExpandable = false;
|
|
18
|
+
@Input() variant: 'default' | 'group' = 'default';
|
|
19
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
<div
|
|
2
|
+
class="max-w-full overflow-hidden rounded-[10px] border border-border bg-white animate-pulse dark:border-dark-border dark:bg-foreground"
|
|
3
|
+
aria-hidden="true"
|
|
4
|
+
>
|
|
5
|
+
<div
|
|
6
|
+
class="flex items-center gap-2 p-3"
|
|
7
|
+
[ngClass]="{
|
|
8
|
+
'bg-primary/10 dark:bg-primary/20': variant === 'group',
|
|
9
|
+
}"
|
|
10
|
+
>
|
|
11
|
+
@if (isNumbering) {
|
|
12
|
+
<div
|
|
13
|
+
class="flex h-6 w-6 shrink-0 items-center justify-center rounded bg-muted/40 dark:bg-dark-muted/30"
|
|
14
|
+
[ngClass]="{
|
|
15
|
+
'h-[35px] w-[35px] rounded-full border border-border': variant === 'group',
|
|
16
|
+
}"
|
|
17
|
+
></div>
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
<div class="min-w-0 flex-1 space-y-2">
|
|
21
|
+
<div class="h-3 w-[78%] rounded bg-muted/40 dark:bg-dark-muted/30"></div>
|
|
22
|
+
<div class="h-3 w-[52%] rounded bg-muted/30 dark:bg-dark-muted/25"></div>
|
|
23
|
+
<div class="flex flex-wrap gap-1">
|
|
24
|
+
<div class="h-4 w-14 rounded-full bg-muted/30 dark:bg-dark-muted/25"></div>
|
|
25
|
+
<div class="h-4 w-16 rounded-full bg-muted/30 dark:bg-dark-muted/25"></div>
|
|
26
|
+
</div>
|
|
27
|
+
</div>
|
|
28
|
+
|
|
29
|
+
<div class="h-4 w-4 shrink-0 rounded bg-muted/40 dark:bg-dark-muted/30"></div>
|
|
30
|
+
</div>
|
|
31
|
+
|
|
32
|
+
@if (variant === 'group') {
|
|
33
|
+
<div
|
|
34
|
+
class="flex items-center justify-between border-t border-border bg-primary/5 px-3 py-2 dark:border-dark-border dark:bg-dark-primary/10"
|
|
35
|
+
>
|
|
36
|
+
<div class="h-5 w-24 rounded-full bg-muted/30 dark:bg-dark-muted/25"></div>
|
|
37
|
+
<div class="flex gap-1">
|
|
38
|
+
<div class="h-6 w-8 rounded bg-muted/40 dark:bg-dark-muted/30"></div>
|
|
39
|
+
<div class="h-6 w-8 rounded bg-muted/40 dark:bg-dark-muted/30"></div>
|
|
40
|
+
</div>
|
|
41
|
+
</div>
|
|
42
|
+
}
|
|
43
|
+
</div>
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { Component, Input } from '@angular/core';
|
|
2
|
+
import { CommonModule } from '@angular/common';
|
|
3
|
+
|
|
4
|
+
@Component({
|
|
5
|
+
selector: 'JTableSkeletonMobileCard',
|
|
6
|
+
standalone: true,
|
|
7
|
+
imports: [CommonModule],
|
|
8
|
+
templateUrl: './table-skeleton-mobile-card.component.html',
|
|
9
|
+
})
|
|
10
|
+
export class JTableSkeletonMobileCardComponent {
|
|
11
|
+
@Input() isNumbering = true;
|
|
12
|
+
@Input() variant: 'default' | 'group' = 'default';
|
|
13
|
+
}
|
package/src/styles.css
CHANGED
|
@@ -1,68 +1,75 @@
|
|
|
1
1
|
/* Scrolls */
|
|
2
2
|
|
|
3
3
|
.scroll-element {
|
|
4
|
-
padding-right: 5px
|
|
4
|
+
padding-right: 5px;
|
|
5
|
+
-webkit-overflow-scrolling: touch;
|
|
5
6
|
}
|
|
6
7
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
8
|
+
/* Scroll personalizado solo en desktop; en móvil/touch queda el scroll nativo del sistema */
|
|
9
|
+
@media (hover: hover) and (pointer: fine) {
|
|
10
|
+
.scroll-element::-webkit-scrollbar {
|
|
11
|
+
width: 8px;
|
|
12
|
+
height: 8px;
|
|
13
|
+
}
|
|
11
14
|
|
|
12
|
-
.scroll-element::-webkit-scrollbar-track {
|
|
13
|
-
|
|
14
|
-
}
|
|
15
|
+
.scroll-element::-webkit-scrollbar-track {
|
|
16
|
+
border-radius: 10px;
|
|
17
|
+
}
|
|
15
18
|
|
|
16
|
-
.scroll-element::-webkit-scrollbar-thumb {
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
}
|
|
19
|
+
.scroll-element::-webkit-scrollbar-thumb {
|
|
20
|
+
border-radius: 10px;
|
|
21
|
+
background: var(--color-primary);
|
|
22
|
+
-webkit-transition: .5s;
|
|
23
|
+
transition: .5s;
|
|
24
|
+
}
|
|
22
25
|
|
|
23
|
-
.scroll-element::-webkit-scrollbar-thumb:hover {
|
|
24
|
-
|
|
25
|
-
}
|
|
26
|
+
.scroll-element::-webkit-scrollbar-thumb:hover {
|
|
27
|
+
background: var(--color-dark-primary);
|
|
28
|
+
}
|
|
26
29
|
|
|
27
|
-
.scroll-element::-webkit-scrollbar-button {
|
|
28
|
-
|
|
29
|
-
|
|
30
|
+
.scroll-element::-webkit-scrollbar-button {
|
|
31
|
+
width: 0px;
|
|
32
|
+
height: 0px;
|
|
33
|
+
}
|
|
30
34
|
}
|
|
31
35
|
|
|
32
36
|
.scroll-screen {
|
|
33
|
-
padding-right: 5px
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
37
|
+
padding-right: 5px;
|
|
38
|
+
-webkit-overflow-scrolling: touch;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
@media (hover: hover) and (pointer: fine) {
|
|
42
|
+
.scroll-screen::-webkit-scrollbar {
|
|
43
|
+
width: 10px;
|
|
44
|
+
height: 10px;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
.scroll-screen::-webkit-scrollbar-thumb {
|
|
48
|
+
background: var(--color-dark-primary);
|
|
49
|
+
border: 3px solid var(--color-primary);
|
|
50
|
+
-webkit-transition: .5s;
|
|
51
|
+
transition: .5s;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
.scroll-screen::-webkit-scrollbar-thumb:hover {
|
|
55
|
+
background: var(--color-primary);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
.scroll-screen::-webkit-scrollbar-button {
|
|
59
|
+
width: 0px;
|
|
60
|
+
height: 0px;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
.dark .scroll-screen::-webkit-scrollbar-thumb {
|
|
64
|
+
background: var(--color-dark-primary);
|
|
65
|
+
border: 3px solid var(--color-primary);
|
|
66
|
+
-webkit-transition: .5s;
|
|
67
|
+
transition: .5s;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
.dark .scroll-screen::-webkit-scrollbar-thumb:hover {
|
|
71
|
+
background: var(--color-primary);
|
|
72
|
+
}
|
|
66
73
|
}
|
|
67
74
|
|
|
68
75
|
|