grav-svelte 0.0.97 → 0.0.99

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.
@@ -1,6 +1,7 @@
1
1
  <script lang="ts">
2
2
  import type { ButtonConfig } from "./interfaces.js";
3
3
  import Tooltip from "./Tooltip.svelte";
4
+ import { onMount, afterUpdate } from "svelte";
4
5
 
5
6
  export let id = 1;
6
7
  export let buttonsConfig: ButtonConfig[];
@@ -8,14 +9,61 @@
8
9
  export let row: any = undefined;
9
10
 
10
11
  $: visibleButtons = buttonsConfig.filter((btn) => btn.show ?? true);
12
+
13
+ function handleClick(event: MouseEvent, button: ButtonConfig) {
14
+ event.stopPropagation();
15
+ button.action(id, row);
16
+ }
17
+
18
+ // Prevenir que Font Awesome procese iconos múltiples veces
19
+ function preventIconDuplication(element: HTMLElement) {
20
+ if (!element) return;
21
+
22
+ const buttons = element.querySelectorAll('button');
23
+ buttons.forEach((button) => {
24
+ const icons = button.querySelectorAll('i[class*="fa-"]');
25
+ // Si hay más de un icono, eliminar los duplicados (mantener solo el primero)
26
+ if (icons.length > 1) {
27
+ for (let i = 1; i < icons.length; i++) {
28
+ icons[i].remove();
29
+ }
30
+ }
31
+ // Marcar como procesado para evitar que Font Awesome lo procese de nuevo
32
+ icons.forEach((icon) => {
33
+ if (!icon.hasAttribute('data-fa-processed')) {
34
+ icon.setAttribute('data-fa-processed', 'true');
35
+ }
36
+ // Prevenir que Font Awesome convierta a SVG si ya es SVG
37
+ const svg = icon.querySelector('svg');
38
+ if (svg && icon.parentElement) {
39
+ // Si ya hay un SVG, eliminar el icono original
40
+ icon.style.display = 'none';
41
+ }
42
+ });
43
+ });
44
+ }
45
+
46
+ let buttonGroupElement: HTMLDivElement;
47
+
48
+ onMount(() => {
49
+ if (buttonGroupElement) {
50
+ preventIconDuplication(buttonGroupElement);
51
+ }
52
+ });
53
+
54
+ afterUpdate(() => {
55
+ if (buttonGroupElement) {
56
+ preventIconDuplication(buttonGroupElement);
57
+ }
58
+ });
11
59
  </script>
12
60
 
13
- <div class="button-group" role="group">
14
- {#each visibleButtons as button, i}
61
+ <div class="button-group" role="group" bind:this={buttonGroupElement}>
62
+ {#each visibleButtons as button, i (button.icon + id + i)}
15
63
  <Tooltip text={button.tooltip}>
16
64
  <button
17
65
  aria-label={button.tooltip}
18
- on:click={() => button.action(id, row)}
66
+ on:click={(e) => handleClick(e, button)}
19
67
  type="button"
20
68
  class="action-buttons-group {visibleButtons.length === 1
21
69
  ? 'rounded-left rounded-right'
@@ -25,7 +73,7 @@
25
73
  ? 'rounded-right'
26
74
  : ''} {button.color}"
27
75
  >
28
- <i class={button.icon}> </i>
76
+ <i class={button.icon} data-fa-processed="true" data-fa-i2svg-processed="true"> </i>
29
77
  </button>
30
78
  </Tooltip>
31
79
  {/each}
@@ -3,6 +3,16 @@
3
3
 
4
4
  let showTooltip = false;
5
5
  export let text: string;
6
+
7
+ function handleMouseEnter(event: MouseEvent) {
8
+ event.stopPropagation();
9
+ showTooltip = true;
10
+ }
11
+
12
+ function handleMouseLeave(event: MouseEvent) {
13
+ event.stopPropagation();
14
+ showTooltip = false;
15
+ }
6
16
  </script>
7
17
 
8
18
  <div class="tooltip-container">
@@ -12,8 +22,8 @@
12
22
  </div>
13
23
  {/if}
14
24
  <div
15
- on:mouseenter={() => (showTooltip = true)}
16
- on:mouseleave={() => (showTooltip = false)}
25
+ on:mouseenter={handleMouseEnter}
26
+ on:mouseleave={handleMouseLeave}
17
27
  role="button"
18
28
  tabindex="0"
19
29
  >
@@ -7,6 +7,13 @@
7
7
 
8
8
  $: alignStyle = header.align === 'left' ? 'margin-right: auto;' :
9
9
  header.align === 'right' ? 'margin-left: auto;' : '';
10
+
11
+ function handleClick(event: MouseEvent) {
12
+ event.stopPropagation();
13
+ if (header.onButtonClick) {
14
+ header.onButtonClick(item[idField], item);
15
+ }
16
+ }
10
17
  </script>
11
18
 
12
19
  <div style="display: inline-flex; {alignStyle}">
@@ -14,15 +21,11 @@
14
21
  type="button"
15
22
  class="dynamic-button"
16
23
  style="{header.colorField && item[header.colorField] ? `background-color: ${item[header.colorField]}; color: white;` : ''} {header.styleField ? (item[header.styleField] ?? '') : ''}"
17
- on:click={() => {
18
- if (header.onButtonClick) {
19
- header.onButtonClick(item[idField], item);
20
- }
21
- }}
24
+ on:click={handleClick}
22
25
  >
23
26
  {#if header.iconField && item[header.iconField]}
24
27
  {#if !header.iconPosition || header.iconPosition === "left"}
25
- <i class="{item[header.iconField]} dynamic-button-icon-left"></i>
28
+ <i class="{item[header.iconField]} dynamic-button-icon-left" data-fa-processed="true" data-fa-i2svg-processed="true"></i>
26
29
  {/if}
27
30
  {/if}
28
31
  {#if header.textField && item[header.textField]}
@@ -30,7 +33,7 @@
30
33
  {/if}
31
34
  {#if header.iconField && item[header.iconField]}
32
35
  {#if header.iconPosition === "right"}
33
- <i class="{item[header.iconField]} dynamic-button-icon-right"></i>
36
+ <i class="{item[header.iconField]} dynamic-button-icon-right" data-fa-processed="true" data-fa-i2svg-processed="true"></i>
34
37
  {/if}
35
38
  {/if}
36
39
  </button>
@@ -0,0 +1,171 @@
1
+ <script lang="ts">
2
+ import "../typography.css";
3
+
4
+ export let valueVar: string = "";
5
+ export let label: string;
6
+ export let disabled = false;
7
+ export let obligatory = false;
8
+ export let icon: string | null = null;
9
+ export let validation: boolean = false;
10
+
11
+ let validationMessage = "";
12
+ let isValid = true;
13
+
14
+ $: {
15
+ if (valueVar) {
16
+ // Email validation using standard regex
17
+ const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
18
+
19
+ if (!emailRegex.test(valueVar)) {
20
+ validationMessage = "Invalid email format";
21
+ isValid = false;
22
+ } else {
23
+ validationMessage = "Email is valid";
24
+ isValid = true;
25
+ }
26
+ } else {
27
+ if (obligatory) {
28
+ validationMessage = "Email is required";
29
+ isValid = false;
30
+ } else {
31
+ validationMessage = "";
32
+ isValid = true;
33
+ }
34
+ }
35
+ }
36
+ </script>
37
+
38
+ <div class="input-container">
39
+ {#if icon}
40
+ <div class="icon-wrapper">
41
+ <i class="{icon} icon"></i>
42
+ </div>
43
+ {/if}
44
+ <div class="input-wrapper">
45
+ <input
46
+ {disabled}
47
+ type="email"
48
+ bind:value={valueVar}
49
+ placeholder=" "
50
+ class="input-field"
51
+ />
52
+
53
+ <label for={valueVar} class="input-label"
54
+ >{label}
55
+ {#if obligatory}
56
+ <span class="required-mark"> *</span>
57
+ {/if}</label
58
+ >
59
+ </div>
60
+ </div>
61
+ {#if validation && validationMessage}
62
+ <div
63
+ class="validation-message"
64
+ class:valid={isValid}
65
+ class:invalid={!isValid}
66
+ >
67
+ {validationMessage}
68
+ </div>
69
+ {/if}
70
+
71
+ <style>
72
+ .input-container {
73
+ display: flex;
74
+ align-items: center;
75
+ border: var(--grav-crud-input-border-width) solid
76
+ var(--grav-crud-color-neutral);
77
+ border-radius: 0.5rem;
78
+ padding-left: 0.5rem;
79
+ padding-right: 0.5rem;
80
+ padding-top: 0.2rem;
81
+ padding-bottom: 0.2rem;
82
+ margin-top: 1.95rem;
83
+ height: fit-content;
84
+ }
85
+
86
+ .icon-wrapper {
87
+ width: 1rem;
88
+ position: relative;
89
+ margin-right: 0.5rem;
90
+ }
91
+
92
+ .icon {
93
+ position: absolute;
94
+ top: -0.4rem;
95
+ left: 0.25rem;
96
+ color: var(--grav-crud-color-neutral);
97
+ }
98
+
99
+ .input-wrapper {
100
+ position: relative;
101
+ z-index: 0;
102
+ width: 100%;
103
+ }
104
+
105
+ .input-field {
106
+ display: block;
107
+ padding: 0.3rem;
108
+ width: 100%;
109
+ font-size: 1rem;
110
+ color: var(--grav-crud-color-neutral);
111
+ background: transparent;
112
+ appearance: none;
113
+ }
114
+
115
+ .input-field:focus {
116
+ outline: none;
117
+ }
118
+
119
+ .input-label {
120
+ position: absolute;
121
+ font-size: 1rem;
122
+ text-align: left;
123
+ color: var(--grav-crud-color-neutral);
124
+ transition: all 0.3s;
125
+ top: 0.25rem;
126
+ left: 0.25rem;
127
+ z-index: -10;
128
+ transform-origin: left;
129
+ }
130
+
131
+ .input-field:focus + .input-label,
132
+ .input-field:not(:placeholder-shown) + .input-label {
133
+ left: 0;
134
+ top: 0;
135
+ color: var(--grav-crud-color-neutral);
136
+ translate: -0.6rem -2.05rem;
137
+ scale: 1;
138
+ }
139
+
140
+ .input-field:placeholder-shown + .input-label {
141
+ transform: translateY(0) scale(1);
142
+ }
143
+
144
+ .required-mark {
145
+ color: #dc2626;
146
+ }
147
+
148
+ .no-margin {
149
+ margin-top: 0;
150
+ }
151
+
152
+ .no-margin .input-field:focus + .input-label,
153
+ .no-margin .input-field:not(:placeholder-shown) + .input-label {
154
+ translate: -0.6rem -1.4rem;
155
+ font-size: 0.7rem;
156
+ }
157
+
158
+ .validation-message {
159
+ font-size: 0.875rem;
160
+ margin-top: 0.25rem;
161
+ color: var(--grav-crud-color-neutral);
162
+ }
163
+
164
+ .validation-message.valid {
165
+ color: #10b981;
166
+ }
167
+
168
+ .validation-message.invalid {
169
+ color: #dc2626;
170
+ }
171
+ </style>
@@ -0,0 +1,22 @@
1
+ import { SvelteComponentTyped } from "svelte";
2
+ import "../typography.css";
3
+ declare const __propDef: {
4
+ props: {
5
+ valueVar?: string;
6
+ label: string;
7
+ disabled?: boolean;
8
+ obligatory?: boolean;
9
+ icon?: string | null;
10
+ validation?: boolean;
11
+ };
12
+ events: {
13
+ [evt: string]: CustomEvent<any>;
14
+ };
15
+ slots: {};
16
+ };
17
+ export type InputFormMailProps = typeof __propDef.props;
18
+ export type InputFormMailEvents = typeof __propDef.events;
19
+ export type InputFormMailSlots = typeof __propDef.slots;
20
+ export default class InputFormMail extends SvelteComponentTyped<InputFormMailProps, InputFormMailEvents, InputFormMailSlots> {
21
+ }
22
+ export {};
@@ -0,0 +1,229 @@
1
+ <script lang="ts">
2
+ import "../typography.css";
3
+
4
+ interface CountryOption {
5
+ value: string;
6
+ label: string;
7
+ }
8
+
9
+ export let valueVar: string = "";
10
+ export let label: string;
11
+ export let disabled = false;
12
+ export let obligatory = false;
13
+ export let defaultDialCode: string = "+52";
14
+ export let validation: boolean = false;
15
+
16
+ // Internal state
17
+ let selectedDialCode: string = defaultDialCode;
18
+ let phoneNumber: string = "";
19
+ let validationMessage = "";
20
+ let isValid = true;
21
+
22
+ // Country list with flag emojis (only flag + dial code)
23
+ const countries: CountryOption[] = [
24
+ { value: "+1", label: "🇺🇸 +1" },
25
+ { value: "+1", label: "🇨🇦 +1" },
26
+ { value: "+52", label: "🇲🇽 +52" },
27
+ { value: "+34", label: "🇪🇸 +34" },
28
+ { value: "+54", label: "🇦🇷 +54" },
29
+ { value: "+56", label: "🇨🇱 +56" },
30
+ { value: "+57", label: "🇨🇴 +57" },
31
+ { value: "+51", label: "🇵🇪 +51" },
32
+ { value: "+58", label: "🇻🇪 +58" },
33
+ { value: "+593", label: "🇪🇨 +593" },
34
+ { value: "+55", label: "🇧🇷 +55" },
35
+ { value: "+598", label: "🇺🇾 +598" },
36
+ { value: "+595", label: "🇵🇾 +595" },
37
+ { value: "+591", label: "🇧🇴 +591" },
38
+ { value: "+506", label: "🇨🇷 +506" },
39
+ { value: "+502", label: "🇬🇹 +502" },
40
+ { value: "+504", label: "🇭🇳 +504" },
41
+ { value: "+505", label: "🇳🇮 +505" },
42
+ { value: "+507", label: "🇵🇦 +507" },
43
+ { value: "+503", label: "🇸🇻 +503" },
44
+ { value: "+1", label: "🇩🇴 +1" },
45
+ { value: "+1", label: "🇵🇷 +1" },
46
+ { value: "+53", label: "🇨🇺 +53" },
47
+ { value: "+44", label: "🇬🇧 +44" },
48
+ { value: "+33", label: "🇫🇷 +33" },
49
+ { value: "+49", label: "🇩🇪 +49" },
50
+ { value: "+39", label: "🇮🇹 +39" },
51
+ { value: "+351", label: "🇵🇹 +351" },
52
+ { value: "+31", label: "🇳🇱 +31" },
53
+ { value: "+32", label: "🇧🇪 +32" },
54
+ ];
55
+
56
+ // Concatenate dial code and phone number
57
+ $: {
58
+ if (selectedDialCode && phoneNumber) {
59
+ valueVar = selectedDialCode + phoneNumber.replace(/\D/g, "");
60
+ } else if (selectedDialCode) {
61
+ valueVar = selectedDialCode;
62
+ } else {
63
+ valueVar = "";
64
+ }
65
+
66
+ // Validation logic
67
+ if (validation) {
68
+ const cleanNumber = phoneNumber.replace(/\D/g, "");
69
+
70
+ if (phoneNumber && !/^\d+$/.test(cleanNumber)) {
71
+ validationMessage = "Phone number must contain only digits";
72
+ isValid = false;
73
+ } else if (cleanNumber && cleanNumber.length < 7) {
74
+ validationMessage = "Phone number must be at least 7 digits";
75
+ isValid = false;
76
+ } else if (cleanNumber && cleanNumber.length > 15) {
77
+ validationMessage = "Phone number must not exceed 15 digits";
78
+ isValid = false;
79
+ } else if (cleanNumber) {
80
+ validationMessage = "Phone number is valid";
81
+ isValid = true;
82
+ } else {
83
+ validationMessage = "";
84
+ isValid = true;
85
+ }
86
+ }
87
+ }
88
+
89
+ // Clean phone number input (only allow digits)
90
+ function handlePhoneInput(event: Event) {
91
+ const input = event.target as HTMLInputElement;
92
+ const cleaned = input.value.replace(/\D/g, "");
93
+ phoneNumber = cleaned;
94
+ }
95
+ </script>
96
+
97
+ <div class="phone-container">
98
+ <label class="phone-label">
99
+ {label}
100
+ {#if obligatory}
101
+ <span class="required-mark"> *</span>
102
+ {/if}
103
+ </label>
104
+
105
+ <div class="phone-input-wrapper">
106
+ <div class="country-select-wrapper">
107
+ <select
108
+ bind:value={selectedDialCode}
109
+ {disabled}
110
+ class="country-select"
111
+ >
112
+ {#each countries as country}
113
+ <option value={country.value}>{country.label}</option>
114
+ {/each}
115
+ </select>
116
+ </div>
117
+
118
+ <div class="phone-number-wrapper">
119
+ <input
120
+ type="tel"
121
+ bind:value={phoneNumber}
122
+ on:input={handlePhoneInput}
123
+ {disabled}
124
+ placeholder="Phone number"
125
+ class="phone-input"
126
+ />
127
+ </div>
128
+ </div>
129
+
130
+ {#if validation && validationMessage}
131
+ <div
132
+ class="validation-message"
133
+ class:valid={isValid}
134
+ class:invalid={!isValid}
135
+ >
136
+ {validationMessage}
137
+ </div>
138
+ {/if}
139
+ </div>
140
+
141
+ <style>
142
+ .phone-container {
143
+ display: flex;
144
+ flex-direction: column;
145
+ width: 100%;
146
+ margin-top: 1.95rem;
147
+ }
148
+
149
+ .phone-label {
150
+ font-size: 1rem;
151
+ color: var(--grav-crud-color-neutral);
152
+ margin-bottom: 0.25rem;
153
+ }
154
+
155
+ .required-mark {
156
+ color: #dc2626;
157
+ }
158
+
159
+ .phone-input-wrapper {
160
+ display: grid;
161
+ grid-template-columns: 80px 1fr;
162
+ border: var(--grav-crud-input-border-width) solid
163
+ var(--grav-crud-color-neutral);
164
+ border-radius: 0.5rem;
165
+ overflow: hidden;
166
+ }
167
+
168
+ .country-select-wrapper {
169
+ border-right: var(--grav-crud-input-border-width) solid
170
+ var(--grav-crud-color-neutral);
171
+ background-color: transparent;
172
+ }
173
+
174
+ .country-select {
175
+ width: 100%;
176
+ height: 100%;
177
+ padding: 0.5rem;
178
+ padding-right: 0.25rem;
179
+ font-size: 1rem;
180
+ color: var(--grav-crud-color-neutral);
181
+ background-color: transparent;
182
+ border: none;
183
+ outline: none;
184
+ cursor: pointer;
185
+ appearance: none;
186
+ -webkit-appearance: none;
187
+ -moz-appearance: none;
188
+ }
189
+
190
+ .country-select:disabled {
191
+ cursor: not-allowed;
192
+ opacity: 0.6;
193
+ }
194
+
195
+ .phone-number-wrapper {
196
+ display: flex;
197
+ align-items: center;
198
+ padding: 0 0.5rem;
199
+ }
200
+
201
+ .phone-input {
202
+ width: 100%;
203
+ padding: 0.5rem;
204
+ font-size: 1rem;
205
+ color: var(--grav-crud-color-neutral);
206
+ background: transparent;
207
+ border: none;
208
+ outline: none;
209
+ }
210
+
211
+ .phone-input::placeholder {
212
+ color: var(--grav-crud-color-neutral);
213
+ opacity: 0.5;
214
+ }
215
+
216
+ .validation-message {
217
+ font-size: 0.875rem;
218
+ margin-top: 0.25rem;
219
+ color: var(--grav-crud-color-neutral);
220
+ }
221
+
222
+ .validation-message.valid {
223
+ color: #10b981;
224
+ }
225
+
226
+ .validation-message.invalid {
227
+ color: #dc2626;
228
+ }
229
+ </style>
@@ -0,0 +1,22 @@
1
+ import { SvelteComponentTyped } from "svelte";
2
+ import "../typography.css";
3
+ declare const __propDef: {
4
+ props: {
5
+ valueVar?: string;
6
+ label: string;
7
+ disabled?: boolean;
8
+ obligatory?: boolean;
9
+ defaultDialCode?: string;
10
+ validation?: boolean;
11
+ };
12
+ events: {
13
+ [evt: string]: CustomEvent<any>;
14
+ };
15
+ slots: {};
16
+ };
17
+ export type InputFormPhoneProps = typeof __propDef.props;
18
+ export type InputFormPhoneEvents = typeof __propDef.events;
19
+ export type InputFormPhoneSlots = typeof __propDef.slots;
20
+ export default class InputFormPhone extends SvelteComponentTyped<InputFormPhoneProps, InputFormPhoneEvents, InputFormPhoneSlots> {
21
+ }
22
+ export {};
@@ -9,4 +9,6 @@ export { default as InputFormNumber } from './InputFormNumber.svelte';
9
9
  export { default as InputFormSelect } from './InputFormSelect.svelte';
10
10
  export { default as InputFormText } from './InputFormText.svelte';
11
11
  export { default as InputFormPassword } from './InputFormPassword.svelte';
12
+ export { default as InputFormMail } from './InputFormMail.svelte';
13
+ export { default as InputFormPhone } from './InputFormPhone.svelte';
12
14
  export { default as InputFormTextArea } from './InputFormTextArea.svelte';
@@ -9,4 +9,6 @@ export { default as InputFormNumber } from './InputFormNumber.svelte';
9
9
  export { default as InputFormSelect } from './InputFormSelect.svelte';
10
10
  export { default as InputFormText } from './InputFormText.svelte';
11
11
  export { default as InputFormPassword } from './InputFormPassword.svelte';
12
+ export { default as InputFormMail } from './InputFormMail.svelte';
13
+ export { default as InputFormPhone } from './InputFormPhone.svelte';
12
14
  export { default as InputFormTextArea } from './InputFormTextArea.svelte';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "grav-svelte",
3
- "version": "0.0.97",
3
+ "version": "0.0.99",
4
4
  "description": "A collection of Svelte components",
5
5
  "license": "MIT",
6
6
  "scripts": {