grav-svelte 0.0.30 → 0.0.32

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -87,6 +87,7 @@ Available components:
87
87
  - `FormBuilder` - Dynamic form generation
88
88
  - `Pagination` - Page navigation
89
89
  - `SearchInput` - Search with debounce
90
+ - Image columns with URL support in data tables
90
91
 
91
92
  ## Documentation
92
93
 
@@ -177,4 +177,25 @@
177
177
 
178
178
  .filter-item-bool {
179
179
  margin: auto 0;
180
+ }
181
+
182
+ .loading-spinner {
183
+ display: flex;
184
+ justify-content: center;
185
+ align-items: center;
186
+ min-height: 38px;
187
+ }
188
+
189
+ .spinner {
190
+ width: 20px;
191
+ height: 20px;
192
+ border: 2px solid #f3f3f3;
193
+ border-top: 2px solid #3498db;
194
+ border-radius: 50%;
195
+ animation: spin 1s linear infinite;
196
+ }
197
+
198
+ @keyframes spin {
199
+ 0% { transform: rotate(0deg); }
200
+ 100% { transform: rotate(360deg); }
180
201
  }
@@ -8,7 +8,7 @@
8
8
  import { slide } from "svelte/transition";
9
9
  import "./CrudFilters.css";
10
10
 
11
- import { createEventDispatcher } from "svelte";
11
+ import { createEventDispatcher, onMount } from "svelte";
12
12
  import type { FiltrosI } from "./interfaces.js";
13
13
  const dispatch = createEventDispatcher();
14
14
 
@@ -17,6 +17,7 @@
17
17
  let localPageSize = 50;
18
18
  let localPageSizeStr = "50";
19
19
  let showFilters = false;
20
+ let isLoading = false;
20
21
 
21
22
  export let Filtros: FiltrosI[];
22
23
  export let showAddButton: boolean = true;
@@ -74,6 +75,29 @@
74
75
  localPageSizeStr = input.value;
75
76
  }
76
77
  }
78
+
79
+ let dataFetched: { value: any; label: string }[][] = [];
80
+ onMount(async () => {
81
+ console.log("Filtros", Filtros);
82
+ if (Filtros.length > 0) {
83
+ isLoading = true;
84
+ try {
85
+ const promises = Filtros.map(async (filtro) => {
86
+ console.log("filtro", filtro);
87
+ if (filtro.service) {
88
+ const data = await filtro.service();
89
+ return data;
90
+ }
91
+ return [];
92
+ });
93
+
94
+ dataFetched = await Promise.all(promises);
95
+ console.log("dataFetched", dataFetched);
96
+ } finally {
97
+ isLoading = false;
98
+ }
99
+ }
100
+ });
77
101
  </script>
78
102
 
79
103
  <!-- svelte-ignore a11y-no-noninteractive-tabindex -->
@@ -94,9 +118,7 @@
94
118
  <div>
95
119
  <div class="tooltip-container">
96
120
  {#if showTooltip == "Agregar"}
97
- <div class="tooltip">
98
- Agregar
99
- </div>
121
+ <div class="tooltip">Agregar</div>
100
122
  {/if}
101
123
  </div>
102
124
  <!-- svelte-ignore a11y_consider_explicit_label -->
@@ -128,9 +150,7 @@
128
150
  <div>
129
151
  <div class="tooltip-container">
130
152
  {#if showTooltip == "Importar"}
131
- <div class="tooltip">
132
- Importar Excel
133
- </div>
153
+ <div class="tooltip">Importar Excel</div>
134
154
  {/if}
135
155
  </div>
136
156
  <!-- svelte-ignore a11y_consider_explicit_label -->
@@ -219,9 +239,7 @@
219
239
  <!-- Btn de limpiar filtros -->
220
240
  <div class="tooltip-container">
221
241
  {#if showTooltip == "Borrar"}
222
- <div class="tooltip">
223
- Borrar filtro
224
- </div>
242
+ <div class="tooltip">Borrar filtro</div>
225
243
  {/if}
226
244
  </div>
227
245
  <!-- svelte-ignore a11y_consider_explicit_label -->
@@ -266,9 +284,7 @@
266
284
  <div class="filter-group" role="group">
267
285
  <div class="tooltip-container">
268
286
  {#if showTooltip == "Aplicar"}
269
- <div class="tooltip">
270
- Aplicar filtro
271
- </div>
287
+ <div class="tooltip">Aplicar filtro</div>
272
288
  {/if}
273
289
  </div>
274
290
  <button
@@ -276,8 +292,7 @@
276
292
  on:click={() => actualizarFiltro()}
277
293
  on:mouseenter={() => (showTooltip = "Aplicar")}
278
294
  on:mouseleave={() => (showTooltip = "nada")}
279
- class="apply-filter-button"
280
- >Filtrar</button
295
+ class="apply-filter-button">Filtrar</button
281
296
  >
282
297
  </div>
283
298
  <!-- /Btn de aplicar filtro -->
@@ -287,9 +302,8 @@
287
302
  {#if showFilters}
288
303
  <div
289
304
  class="filters-grid"
290
- transition:slide|local={{ duration: 300, delay: 100 }}
291
305
  >
292
- {#each Filtros as { tipo, label, options }, i}
306
+ {#each Filtros as { tipo, label, options, service }, i}
293
307
  {#if tipo == "text"}
294
308
  <div class="filter-item">
295
309
  <InputFormText
@@ -320,11 +334,25 @@
320
334
  </div>
321
335
  {:else if tipo == "select"}
322
336
  <div class="filter-item filter-item-select">
323
- <InputFormSelect
324
- {label}
325
- res={options}
326
- bind:justValue={Filtros[i].value}
327
- />
337
+ {#if service}
338
+ {#if isLoading}
339
+ <div class="loading-spinner">
340
+ <div class="spinner"></div>
341
+ </div>
342
+ {:else}
343
+ <InputFormSelect
344
+ {label}
345
+ res={dataFetched[i]}
346
+ bind:justValue={Filtros[i].value}
347
+ />
348
+ {/if}
349
+ {:else}
350
+ <InputFormSelect
351
+ {label}
352
+ res={options}
353
+ bind:justValue={Filtros[i].value}
354
+ />
355
+ {/if}
328
356
  </div>
329
357
  {:else if tipo == "bool"}
330
358
  <div class="filter-item filter-item-bool">
@@ -1,5 +1,7 @@
1
1
  <script lang="ts">
2
- import CrudTableButtons from "./CrudTableButtons.svelte";
2
+ import CrudTableButtons from "./CrudTableButtons.svelte";
3
+ import { openModal, closeModal } from "../Modals/index.js";
4
+ import ImageModal from "./ImageModal.svelte";
3
5
 
4
6
  // COMPONENTES imports
5
7
 
@@ -36,6 +38,11 @@
36
38
  selectedAsc: selectedAscOrDesc,
37
39
  });
38
40
  }
41
+
42
+ function openImageModal(src: string) {
43
+ closeModal("crud-image-modal");
44
+ openModal("crud-image-modal", ImageModal, { src });
45
+ }
39
46
  </script>
40
47
 
41
48
  <div class="table-container">
@@ -49,6 +56,7 @@
49
56
  class="table-header-cell {index == 0
50
57
  ? 'borderleft'
51
58
  : ''} non-sortable"
59
+ style="text-align: {tableHeader.align ?? 'center'}"
52
60
  >
53
61
  {tableHeader.titulo}
54
62
  </th>
@@ -58,6 +66,7 @@
58
66
  class="table-header-cell {index == 0
59
67
  ? 'borderleft'
60
68
  : ''} sortable"
69
+ style="text-align: {tableHeader.align ?? 'left'}"
61
70
  >
62
71
  <h1>{tableHeader.titulo}</h1>
63
72
  {#if selectedSort == tableHeader.campo}
@@ -127,6 +136,7 @@
127
136
  class="cell-content {tableBodyItem.biBold
128
137
  ? 'bold'
129
138
  : ''}"
139
+ style="text-align: {tableBodyItem.align ?? 'left'}"
130
140
  >
131
141
  {item[tableBodyItem.campo] ?? ""}
132
142
  </p>
@@ -141,15 +151,49 @@
141
151
  class="cell-content {tableBodyItem.biBold
142
152
  ? 'bold'
143
153
  : ''}"
154
+ style="text-align: {tableBodyItem.align ?? 'left'}"
144
155
  >
145
156
  {item[tableBodyItem.campo] ?? ""}
146
157
  </p>
147
158
  </td>
159
+ {:else if tableBodyItem.tipo == "Bool"}
160
+ <td
161
+ class="table-cell {i == 0
162
+ ? 'sticky-cell'
163
+ : ''}"
164
+ >
165
+ <p
166
+ class="cell-content {tableBodyItem.biBold
167
+ ? 'bold'
168
+ : ''}"
169
+ style="text-align: {tableBodyItem.align ?? 'left'}"
170
+ >
171
+ {#if item[tableBodyItem.campo] === true}
172
+ <i class="fas fa-check"></i>
173
+ {:else}
174
+ -
175
+ {/if}
176
+ </p>
177
+ </td>
178
+ {:else if tableBodyItem.tipo == "Image"}
179
+ <td
180
+ class="table-cell {i == 0
181
+ ? 'sticky-cell'
182
+ : ''}"
183
+ >
184
+ <img
185
+ class="crud-image cursor-pointer"
186
+ src={item[tableBodyItem.campo] ?? ''}
187
+ alt="image"
188
+ on:click={() => openImageModal(item[tableBodyItem.campo])}
189
+ />
190
+ </td>
148
191
  {:else if tableBodyItem.tipo == "Buttons"}
149
192
  <CrudTableButtons
150
193
  id={item[tableBodyItem.campo]}
151
194
  buttonsConfig={tableBodyItem.buttonsConfig ??
152
195
  []}
196
+ align={tableBodyItem.align ?? 'center'}
153
197
  />
154
198
  {/if}
155
199
  {/each}
@@ -275,10 +319,20 @@
275
319
  background-color: #f5f5f5;
276
320
  }
277
321
 
322
+ /* ensure sticky cells follow row hover */
323
+ .table-row:hover .sticky-cell {
324
+ background-color: #f5f5f5;
325
+ }
326
+
278
327
  .table-row.selected {
279
328
  background-color: #e8e8e8;
280
329
  }
281
330
 
331
+ /* keep first column selected state consistent */
332
+ .table-row.selected .sticky-cell {
333
+ background-color: #e8e8e8;
334
+ }
335
+
282
336
  .table-cell {
283
337
  border-top: 0;
284
338
  border-left: 0;
@@ -319,6 +373,8 @@
319
373
 
320
374
  .loading-animation {
321
375
  display: flex;
376
+ flex-direction: column;
377
+ width: 100%;
322
378
  animation: pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite;
323
379
  }
324
380
 
@@ -328,8 +384,8 @@
328
384
  background-color: #e0e0e0;
329
385
  margin-bottom: 0.5rem;
330
386
  width: 100%;
331
- flex: 1;
332
- margin: 0 0.5rem;
387
+ margin: 0 0 0.5rem 0;
388
+ flex: none;
333
389
  }
334
390
 
335
391
  @keyframes pulse {
@@ -347,4 +403,11 @@
347
403
  font-size: 1rem;
348
404
  }
349
405
  }
406
+
407
+ .crud-image {
408
+ max-width: 4rem;
409
+ max-height: 4rem;
410
+ object-fit: contain;
411
+ margin: auto;
412
+ }
350
413
  </style>
@@ -3,10 +3,11 @@
3
3
 
4
4
  export let id = 1;
5
5
  export let buttonsConfig: ButtonConfig[];
6
+ export let align: 'left' | 'right' | 'center' = 'center';
6
7
  let showTooltip = "";
7
8
  </script>
8
9
 
9
- <td class="table-cell">
10
+ <td class="table-cell" style="text-align: {align}">
10
11
  <div class="button-group" role="group">
11
12
  {#each buttonsConfig as button, i}
12
13
  <div class="tooltip-container">
@@ -4,6 +4,7 @@ declare const __propDef: {
4
4
  props: {
5
5
  id?: number;
6
6
  buttonsConfig: ButtonConfig[];
7
+ align?: "left" | "right" | "center";
7
8
  };
8
9
  events: {
9
10
  [evt: string]: CustomEvent<any>;
@@ -3,7 +3,8 @@
3
3
  import CrudFilters from "./CrudFilters.svelte";
4
4
  import CrudTable from "./CrudTable.svelte";
5
5
  import type { FiltrosI, TableHeader, CrudWrapperProps } from "./interfaces.js";
6
- import PaginationCrud from "./PaginationCRUD.svelte";
6
+ import PaginationCrud from "./PaginationCRUD.svelte";
7
+ import { ModalContainer } from "../Modals/index.js";
7
8
 
8
9
  export let Filtros: FiltrosI[];
9
10
  export let todosLosObjetos: any[];
@@ -105,6 +106,7 @@
105
106
  <i class="far fa-file-pdf pr-3"> </i>PDF
106
107
  </button>
107
108
  </div>
109
+ <ModalContainer />
108
110
  </div>
109
111
 
110
112
  <style>
@@ -0,0 +1,16 @@
1
+ <script lang="ts">
2
+ import { Grav_Modal, closeModal } from '../Modals/index.js';
3
+ export let src: string;
4
+ const MODAL_ID = 'crud-image-modal';
5
+ </script>
6
+
7
+ <Grav_Modal
8
+ title="Imagen"
9
+ size="md"
10
+ isVista={true}
11
+ onClose={() => closeModal(MODAL_ID)}
12
+ >
13
+ <div class="flex items-center justify-center p-4">
14
+ <img src={src} alt="image" class="max-w-full max-h-[80vh]" />
15
+ </div>
16
+ </Grav_Modal>
@@ -0,0 +1,16 @@
1
+ import { SvelteComponentTyped } from "svelte";
2
+ declare const __propDef: {
3
+ props: {
4
+ src: string;
5
+ };
6
+ events: {
7
+ [evt: string]: CustomEvent<any>;
8
+ };
9
+ slots: {};
10
+ };
11
+ export type ImageModalProps = typeof __propDef.props;
12
+ export type ImageModalEvents = typeof __propDef.events;
13
+ export type ImageModalSlots = typeof __propDef.slots;
14
+ export default class ImageModal extends SvelteComponentTyped<ImageModalProps, ImageModalEvents, ImageModalSlots> {
15
+ }
16
+ export {};
@@ -3,4 +3,5 @@ export { default as CrudTable } from './CrudTable.svelte';
3
3
  export { default as CrudFilters } from './CrudFilters.svelte';
4
4
  export { default as PaginationCrud } from './PaginationCRUD.svelte';
5
5
  export { default as CrudTableButtons } from './CrudTableButtons.svelte';
6
+ export { default as ImageModal } from './ImageModal.svelte';
6
7
  export type { ButtonConfig, TableHeader, FiltrosI, CrudWrapperProps } from './interfaces.js';
@@ -3,3 +3,4 @@ export { default as CrudTable } from './CrudTable.svelte';
3
3
  export { default as CrudFilters } from './CrudFilters.svelte';
4
4
  export { default as PaginationCrud } from './PaginationCRUD.svelte';
5
5
  export { default as CrudTableButtons } from './CrudTableButtons.svelte';
6
+ export { default as ImageModal } from './ImageModal.svelte';
@@ -7,8 +7,13 @@ export interface ButtonConfig {
7
7
  export interface TableHeader {
8
8
  titulo: string;
9
9
  biSort: boolean;
10
- tipo: 'Text' | 'Number' | 'Buttons';
10
+ tipo: 'Text' | 'Number' | 'Buttons' | 'Bool' | 'Image';
11
11
  biBold: boolean;
12
+ /**
13
+ * Alignment for the content of the cells belonging to this header.
14
+ * Defaults to 'left' when not provided.
15
+ */
16
+ align?: 'left' | 'right' | 'center';
12
17
  campo: string;
13
18
  buttonsConfig: ButtonConfig[] | null;
14
19
  }
@@ -16,10 +21,14 @@ export interface FiltrosI {
16
21
  tipo: 'number' | 'text' | 'date' | 'datetime' | 'select' | 'bool';
17
22
  label: string;
18
23
  value: any;
19
- options: {
20
- value: string;
24
+ options?: {
25
+ value: any;
21
26
  label: string;
22
27
  }[];
28
+ service?: () => Promise<{
29
+ value: any;
30
+ label: string;
31
+ }[]>;
23
32
  }
24
33
  export interface CrudWrapperProps {
25
34
  Filtros: FiltrosI[];
@@ -2,12 +2,12 @@
2
2
  import Select from "svelte-select";
3
3
 
4
4
  interface SelectValue {
5
- value: string;
5
+ value: any;
6
6
  label: string;
7
7
  }
8
8
 
9
9
  export let value: SelectValue | null = null;
10
- export let justValue: string | null = null;
10
+ export let justValue: any | null = null;
11
11
  export let res: any[] = [];
12
12
  export let changeFunction: (
13
13
  e: CustomEvent<SelectValue | null>
@@ -2,13 +2,13 @@ import { SvelteComponentTyped } from "svelte";
2
2
  declare const __propDef: {
3
3
  props: {
4
4
  value?: {
5
- value: string;
5
+ value: any;
6
6
  label: string;
7
7
  } | null;
8
- justValue?: string | null;
8
+ justValue?: any | null;
9
9
  res?: any[];
10
10
  changeFunction?: (e: CustomEvent<{
11
- value: string;
11
+ value: any;
12
12
  label: string;
13
13
  } | null>) => void;
14
14
  onClear?: () => void;
@@ -4,23 +4,102 @@
4
4
  export let disabled = false;
5
5
  export let obligatory = false;
6
6
  export let rows: number = 3;
7
+ export let icon: string | null = null;
7
8
  </script>
8
9
 
9
- <div class="relative z-0 mt-4">
10
- <textarea
11
- {disabled}
12
- id={valueVar}
13
- bind:value={valueVar}
14
- name={valueVar}
15
- {rows}
16
- class="block pt-2.5 px-0 w-full text-base text-gray-900 bg-transparent border-0 border-b-2 border-gray-300 appearance-none focus:outline-none focus:ring-0 focus:border-black peer"
17
- />
18
- <label
19
- for={valueVar}
20
- class="absolute text-base text-gray-600 duration-300 transform -translate-y-6 scale-75 top-2.5 -z-10 origin-[0] peer-focus:left-0 peer-focus:text-gray-600 peer-placeholder-shown:scale-100 peer-placeholder-shown:translate-y-0 peer-focus:scale-75 peer-focus:-translate-y-6"
21
- >{label}
22
- {#if obligatory}
23
- <span class="text-red-600"> *</span>
24
- {/if}</label
25
- >
10
+ <div class="input-container">
11
+ {#if icon}
12
+ <div class="icon-wrapper">
13
+ <i class="{icon} icon"></i>
14
+ </div>
15
+ {/if}
16
+ <div class="input-wrapper">
17
+ <textarea
18
+ {disabled}
19
+ id={valueVar}
20
+ bind:value={valueVar}
21
+ name={valueVar}
22
+ {rows}
23
+ placeholder=""
24
+ class="input-field"
25
+ />
26
+ <label for={valueVar} class="input-label"
27
+ >{label}
28
+ {#if obligatory}
29
+ <span class="required-mark"> *</span>
30
+ {/if}</label
31
+ >
32
+ </div>
26
33
  </div>
34
+
35
+ <style>
36
+ .input-container {
37
+ display: flex;
38
+ align-items: center;
39
+ margin-top: 0.75rem;
40
+ border: 0;
41
+ border-bottom: 2px solid #9ca3af;
42
+ }
43
+
44
+ .icon-wrapper {
45
+ width: 1rem;
46
+ position: relative;
47
+ margin-right: 0.5rem;
48
+ }
49
+
50
+ .icon {
51
+ position: absolute;
52
+ top: -0.25rem;
53
+ left: 0.25rem;
54
+ }
55
+
56
+ .input-wrapper {
57
+ position: relative;
58
+ z-index: 0;
59
+ width: 100%;
60
+ }
61
+
62
+ .input-field {
63
+ display: block;
64
+ padding-top: 0.625rem;
65
+ padding-left: 0;
66
+ padding-right: 0;
67
+ width: 100%;
68
+ font-size: 1rem;
69
+ color: #111827;
70
+ background: transparent;
71
+ appearance: none;
72
+ }
73
+
74
+ .input-field:focus {
75
+ outline: none;
76
+ border-color: black;
77
+ }
78
+
79
+ .input-label {
80
+ position: absolute;
81
+ font-size: 1rem;
82
+ text-align: left;
83
+ color: black;
84
+ transition: all 0.3s;
85
+ top: 0.625rem;
86
+ z-index: -10;
87
+ transform-origin: left;
88
+ }
89
+
90
+ .input-field:focus + .input-label,
91
+ .input-field:not(:placeholder-shown) + .input-label {
92
+ left: 0;
93
+ color: #4b5563;
94
+ translate: 0rem -1.25rem;
95
+ scale: 0.75;
96
+ }
97
+
98
+ .input-field:placeholder-shown + .input-label {
99
+ transform: translateY(0) scale(1);
100
+ }
101
+
102
+ .required-mark {
103
+ color: #dc2626;
104
+ }
105
+ </style>
@@ -6,6 +6,7 @@ declare const __propDef: {
6
6
  disabled?: boolean;
7
7
  obligatory?: boolean;
8
8
  rows?: number;
9
+ icon?: string | null;
9
10
  };
10
11
  events: {
11
12
  [evt: string]: CustomEvent<any>;
@@ -2,6 +2,7 @@
2
2
  import type { SidebarSection } from "./sidebarConfig.js";
3
3
  import SidebarItem from "./SidebarItem.svelte";
4
4
  import "./SidebarWrapper.css";
5
+ import { Confirmacion_Alert } from "../index.js";
5
6
  import { onMount } from 'svelte';
6
7
  // Props
7
8
  export let sections: SidebarSection[];
@@ -26,6 +27,17 @@
26
27
  console.log("toggleCollapseShow");
27
28
  collapseShow = !collapseShow;
28
29
  }
30
+
31
+ function handleLogout(event: MouseEvent) {
32
+ event.preventDefault();
33
+ Confirmacion_Alert(
34
+ "Confirmar cierre de sesión",
35
+ "¿Desea cerrar sesión?",
36
+ () => {
37
+ window.location.href = logoutLink;
38
+ }
39
+ );
40
+ }
29
41
  </script>
30
42
 
31
43
  <nav class="sidebar {customClass}">
@@ -107,8 +119,13 @@
107
119
 
108
120
  {#if showLogout}
109
121
  <li class="logout-item">
110
- <a class="logout-link" href={logoutLink}>
111
- Log Out <i class="fas fa-sign-out-alt ml-2"></i>
122
+ <a
123
+ class="logout-link"
124
+ href={logoutLink}
125
+ on:click|preventDefault={handleLogout}
126
+ >
127
+ Log Out
128
+ <i class="fas fa-sign-out-alt ml-2"></i>
112
129
  </a>
113
130
  </li>
114
131
  {/if}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "grav-svelte",
3
- "version": "0.0.30",
3
+ "version": "0.0.32",
4
4
  "description": "A collection of Svelte components",
5
5
  "license": "MIT",
6
6
  "scripts": {