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.
Files changed (25) hide show
  1. package/cli/file-operations.js +3 -13
  2. package/cli/settings/path-utils.js +20 -4
  3. package/cli/templates/app.generator.js +59 -50
  4. package/package.json +1 -1
  5. package/registry/components.json +12 -3
  6. package/src/lib/components/alert/alert-toast/toast-alert.component.css +7 -0
  7. package/src/lib/components/alert/alert-toast/toast-alert.component.html +1 -1
  8. package/src/lib/components/alert/alert-toast/toast-alert.component.ts +1 -0
  9. package/src/lib/components/card/card-complete/complete-card.component.html +5 -6
  10. package/src/lib/components/card/card-complete/complete-card.component.ts +12 -1
  11. package/src/lib/components/card/card-crud-complete/complete-crud-card.component.html +16 -7
  12. package/src/lib/components/card/card-crud-complete/complete-crud-card.component.ts +12 -1
  13. package/src/lib/components/card/card-skeleton/card-skeleton.component.html +47 -0
  14. package/src/lib/components/card/card-skeleton/card-skeleton.component.ts +8 -0
  15. package/src/lib/components/dialog/dialog.component.css +12 -0
  16. package/src/lib/components/dialog/dialog.component.html +9 -4
  17. package/src/lib/components/dialog/dialog.component.ts +173 -28
  18. package/src/lib/components/form/form-sidebar/sidebar-form.component.css +14 -11
  19. package/src/lib/components/table/table-crud-complete/complete-crud-table.component.html +20 -39
  20. package/src/lib/components/table/table-crud-complete/complete-crud-table.component.ts +21 -0
  21. package/src/lib/components/table/table-skeleton/table-skeleton-desktop-row.component.html +74 -0
  22. package/src/lib/components/table/table-skeleton/table-skeleton-desktop-row.component.ts +19 -0
  23. package/src/lib/components/table/table-skeleton/table-skeleton-mobile-card.component.html +43 -0
  24. package/src/lib/components/table/table-skeleton/table-skeleton-mobile-card.component.ts +13 -0
  25. package/src/styles.css +60 -53
@@ -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 sharedInfo = getSharedFolderInfo(componentPath)
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
- const sharedOk = await copySharedFolderIfNeeded(componentPath, isDependency, { forceSync: true })
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
- * Used by the CLI to copy group-level utilities alongside the component folder.
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: `${COMPONENTS_SOURCE_PREFIX}${groupKey}/shared`,
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
- &::-webkit-scrollbar {
120
- width: 8px;
121
- height: 8px;
122
- }
123
-
124
- &::-webkit-scrollbar-track {
125
- border-radius: 10px;
126
- }
127
-
128
- &::-webkit-scrollbar-thumb {
129
- border-radius: 10px;
130
- background: var(--color-primary);
131
- transition: 0.5s;
132
- }
133
-
134
- &::-webkit-scrollbar-thumb:hover {
135
- background: var(--color-dark-primary);
136
- }
137
-
138
- &::-webkit-scrollbar-button {
139
- width: 0;
140
- height: 0;
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
- &::-webkit-scrollbar {
146
- width: 10px;
147
- height: 10px;
148
- }
149
-
150
- &::-webkit-scrollbar-thumb {
151
- background: var(--color-dark-primary);
152
- border: 3px solid var(--color-primary);
153
- transition: 0.5s;
154
- }
155
-
156
- &::-webkit-scrollbar-thumb:hover {
157
- background: var(--color-primary);
158
- }
159
-
160
- &::-webkit-scrollbar-button {
161
- width: 0;
162
- height: 0;
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
- .dark .scroll-screen {
167
- &::-webkit-scrollbar-thumb {
168
- background: var(--color-dark-primary);
169
- border: 3px solid var(--color-primary);
170
- transition: 0.5s;
171
- }
172
-
173
- &::-webkit-scrollbar-thumb:hover {
174
- background: var(--color-primary);
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tailjng",
3
- "version": "0.1.15",
3
+ "version": "0.1.17",
4
4
  "peerDependencies": {
5
5
  "@angular/common": "^19.2.0",
6
6
  "@angular/core": "^19.2.0",
@@ -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 }}
@@ -1,3 +1,4 @@
1
+
1
2
  import {
2
3
  Component,
3
4
  computed,
@@ -43,12 +43,11 @@
43
43
  </div>
44
44
  }
45
45
 
46
- @if (isLoading('initialLoad') && displayData.length === 0) {
47
- <div class="w-full flex justify-center items-center h-100 bg-white dark:bg-foreground rounded-[20px] border border-border dark:border-dark-border">
48
- <div class="flex flex-col gap-3 items-center justify-center py-4">
49
- <JIcon [icon]="Icons.Loader2" size="30" iconClass="text-primary animate-spin" [ariaHidden]="true" />
50
- <p>Cargando datos...</p>
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 (isLoading('initialLoad') && displayData.length === 0) {
47
- <div class="w-full flex justify-center items-center h-100 bg-white dark:bg-foreground rounded-[20px] border border-border dark:border-dark-border">
48
- <div class="flex flex-col gap-3 items-center justify-center py-4">
49
- <JIcon [icon]="Icons.Loader2" size="30" iconClass="text-primary animate-spin" [ariaHidden]="true" />
50
- <p>Cargando datos...</p>
51
- </div>
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>
@@ -0,0 +1,8 @@
1
+ import { Component } from '@angular/core';
2
+
3
+ @Component({
4
+ selector: 'JCardSkeleton',
5
+ standalone: true,
6
+ templateUrl: './card-skeleton.component.html',
7
+ })
8
+ export class JCardSkeletonComponent {}
@@ -6,3 +6,15 @@
6
6
  margin: 0 !important;
7
7
  border-radius: 0;
8
8
  }
9
+
10
+ .jdialog-header-drag {
11
+ touch-action: none;
12
+ }
13
+
14
+ .jdialog-close {
15
+ touch-action: manipulation;
16
+ }
17
+
18
+ .jdialog-panel--dragging {
19
+ transition: none !important;
20
+ }
@@ -24,20 +24,25 @@
24
24
  [style.min-height]="!isFullScreen ? '40px' : null"
25
25
  >
26
26
  <div
27
- class="jdialog-header flex min-h-[3.25rem] select-none items-center gap-3 border-b border-border bg-primary py-2.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%)]"
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 when `draggable` is enabled.
213
+ * Starts dragging from the title handle (mouse).
194
214
  */
195
- startDrag(event: MouseEvent): void {
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).closest(
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: event.clientX - rect.left,
213
- y: event.clientY - rect.top,
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
- const mouseMoveHandler = (moveEvent: MouseEvent) => {
231
- if (!this.isDragging) {
232
- return;
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
- const newLeft = moveEvent.clientX - this.dragOffset.x;
236
- const newTop = moveEvent.clientY - this.dragOffset.y;
373
+ const maxLeft = window.innerWidth - dialogElement.offsetWidth;
374
+ const maxTop = window.innerHeight - dialogElement.offsetHeight;
237
375
 
238
- const maxLeft = window.innerWidth - dialogElement.offsetWidth;
239
- const maxTop = window.innerHeight - dialogElement.offsetHeight;
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
- dialogElement.style.left = `${Math.min(Math.max(newLeft, 0), maxLeft)}px`;
242
- dialogElement.style.top = `${Math.min(Math.max(newTop, 0), maxTop)}px`;
243
- };
380
+ private setDocumentListeners(release: () => void): void {
381
+ this.clearDocumentListeners();
382
+ this.releaseDocumentListeners = release;
383
+ }
244
384
 
245
- const mouseUpHandler = () => {
246
- this.isDragging = false;
247
- document.removeEventListener('mousemove', mouseMoveHandler);
248
- document.removeEventListener('mouseup', mouseUpHandler);
249
- };
385
+ private clearDocumentListeners(): void {
386
+ this.releaseDocumentListeners?.();
387
+ this.releaseDocumentListeners = null;
388
+ }
250
389
 
251
- document.addEventListener('mousemove', mouseMoveHandler);
252
- document.addEventListener('mouseup', mouseUpHandler);
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
- &::-webkit-scrollbar {
24
+ @media (hover: hover) and (pointer: fine) {
25
+ .scroll-element::-webkit-scrollbar {
23
26
  width: 8px !important;
24
27
  }
25
28
 
26
- &::-webkit-scrollbar-thumb {
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
- &::-webkit-scrollbar-thumb:hover {
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
- background: color-mix(in srgb, var(--color-dark-primary) 45%, transparent);
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
- background: color-mix(in srgb, var(--color-dark-primary) 70%, transparent);
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 (isLoading("initialLoad") && displayData.length === 0) {
186
- <!-- Loading state -->
187
- <tr>
188
- <td
189
- [attr.colspan]="getVisibleColumnsCount() + 3"
190
- class="px-4 py-8 text-center text-sm text-black dark:text-white"
191
- >
192
- <div
193
- class="flex flex-col gap-3 items-center justify-center py-4"
194
- >
195
- <JIcon [icon]="Icons.Loader2"
196
- size="30"
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
- @else if (!displayData.length && !isLoading("pagination")) {
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 (isLoading("initialLoad") && displayData.length === 0) {
976
- <div
977
- class="flex flex-col gap-3 items-center justify-center py-10 bg-white dark:bg-foreground rounded border border-border dark:border-dark-border"
978
- >
979
- <JIcon [icon]="Icons.Loader2"
980
- size="30"
981
- iconClass="text-primary animate-spin" [ariaHidden]="true" />
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
- .scroll-element::-webkit-scrollbar {
8
- width: 8px;
9
- height: 8px
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
- border-radius: 10px
14
- }
15
+ .scroll-element::-webkit-scrollbar-track {
16
+ border-radius: 10px;
17
+ }
15
18
 
16
- .scroll-element::-webkit-scrollbar-thumb {
17
- border-radius: 10px;
18
- background: var(--color-primary);
19
- -webkit-transition: .5s;
20
- transition: .5s
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
- background: var(--color-dark-primary)
25
- }
26
+ .scroll-element::-webkit-scrollbar-thumb:hover {
27
+ background: var(--color-dark-primary);
28
+ }
26
29
 
27
- .scroll-element::-webkit-scrollbar-button {
28
- width: 0px;
29
- height: 0px
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
- .scroll-screen::-webkit-scrollbar {
37
- width: 10px;
38
- height: 10px
39
- }
40
-
41
- .scroll-screen::-webkit-scrollbar-thumb {
42
- background: var(--color-dark-primary);
43
- border: 3px solid var(--color-primary);
44
- -webkit-transition: .5s;
45
- transition: .5s
46
- }
47
-
48
- .scroll-screen::-webkit-scrollbar-thumb:hover {
49
- background: var(--color-primary)
50
- }
51
-
52
- .scroll-screen::-webkit-scrollbar-button {
53
- width: 0px;
54
- height: 0px
55
- }
56
-
57
- .dark .scroll-screen::-webkit-scrollbar-thumb {
58
- background: var(--color-dark-primary);
59
- border: 3px solid var(--color-primary);
60
- -webkit-transition: .5s;
61
- transition: .5s
62
- }
63
-
64
- .dark .scroll-screen::-webkit-scrollbar-thumb:hover {
65
- background: var(--color-primary)
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