grav-svelte 0.1.240 → 0.1.246

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,7 +1,6 @@
1
1
  <script lang="ts">
2
2
  import type { ButtonConfig } from "./interfaces.js";
3
3
  import Tooltip from "./Tooltip.svelte";
4
- import { onMount, afterUpdate, onDestroy } from "svelte";
5
4
 
6
5
  export let id = 1;
7
6
  export let buttonsConfig: ButtonConfig[];
@@ -14,138 +13,9 @@
14
13
  event.stopPropagation();
15
14
  button.action(id, row);
16
15
  }
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
- // Buscar todos los iconos (tanto <i> como SVGs generados por Font Awesome)
25
- const iconElements = button.querySelectorAll(
26
- 'i[class*="fa-"], svg[data-icon]',
27
- );
28
- const iconClasses = new Set<string>();
29
-
30
- iconElements.forEach((icon, index) => {
31
- const iconClass = icon.getAttribute("class") || "";
32
- const faClasses = iconClass.split(" ").filter((c) => c.startsWith("fa-"));
33
-
34
- // Si es un elemento <i> con clase fa-
35
- if (icon.tagName === "I" && faClasses.length > 0) {
36
- const iconName = faClasses[faClasses.length - 1];
37
- const existingSvg =
38
- button.querySelector(`svg[data-icon="${iconName}"]`) ||
39
- button.querySelector(`svg[data-icon="${iconName.replace(/^fa-/, "")}"]`);
40
-
41
- if (existingSvg) {
42
- icon.remove();
43
- return;
44
- }
45
-
46
- const faKey = faClasses.join(" ");
47
- if (!iconClasses.has(faKey)) {
48
- iconClasses.add(faKey);
49
- icon.setAttribute("data-fa-processed", "true");
50
- icon.setAttribute("data-fa-i2svg-processed", "true");
51
- } else {
52
- icon.remove();
53
- }
54
- }
55
-
56
- // Si es un SVG duplicado (más de uno con el mismo data-icon)
57
- if (icon.tagName === "SVG") {
58
- const dataIcon = icon.getAttribute("data-icon");
59
- if (dataIcon) {
60
- const existingSvgs = button.querySelectorAll(
61
- `svg[data-icon="${dataIcon}"]`,
62
- );
63
- if (existingSvgs.length > 1) {
64
- // Mantener solo el primero, eliminar los demás
65
- for (let i = 1; i < existingSvgs.length; i++) {
66
- existingSvgs[i].remove();
67
- }
68
- }
69
- }
70
- }
71
- });
72
-
73
- // Limpiar iconos <i> que ya tienen SVG correspondiente
74
- const allIcons = button.querySelectorAll('i[class*="fa-"]');
75
- allIcons.forEach((icon) => {
76
- const iconClass = icon.getAttribute("class") || "";
77
- const faClasses = iconClass.split(" ").filter((c) => c.startsWith("fa-"));
78
- if (faClasses.length > 0) {
79
- const iconName = faClasses[faClasses.length - 1];
80
- const svg =
81
- button.querySelector(`svg[data-icon="${iconName}"]`) ||
82
- button.querySelector(`svg[data-icon="${iconName.replace(/^fa-/, "")}"]`);
83
- if (svg) {
84
- icon.remove();
85
- }
86
- }
87
- });
88
- });
89
- }
90
-
91
- let buttonGroupElement: HTMLDivElement;
92
- let mutationObserver: MutationObserver | null = null;
93
-
94
- onMount(() => {
95
- if (buttonGroupElement) {
96
- preventIconDuplication(buttonGroupElement);
97
-
98
- // Observar cambios en el DOM para detectar cuando Font Awesome agrega SVGs
99
- mutationObserver = new MutationObserver((mutations) => {
100
- let shouldClean = false;
101
- mutations.forEach((mutation) => {
102
- if (mutation.type === "childList" && mutation.addedNodes.length > 0) {
103
- mutation.addedNodes.forEach((node) => {
104
- if (node.nodeType === 1) {
105
- // Element node
106
- const element = node as Element;
107
- if (element.tagName === "SVG" || element.querySelector("svg")) {
108
- shouldClean = true;
109
- }
110
- }
111
- });
112
- }
113
- });
114
-
115
- if (shouldClean) {
116
- // Usar setTimeout para evitar procesar múltiples veces en el mismo ciclo
117
- setTimeout(() => {
118
- preventIconDuplication(buttonGroupElement);
119
- }, 0);
120
- }
121
- });
122
-
123
- mutationObserver.observe(buttonGroupElement, {
124
- childList: true,
125
- subtree: true,
126
- });
127
- }
128
- });
129
-
130
- afterUpdate(() => {
131
- if (buttonGroupElement) {
132
- // Usar setTimeout para evitar procesar en medio de una actualización
133
- setTimeout(() => {
134
- preventIconDuplication(buttonGroupElement);
135
- }, 0);
136
- }
137
- });
138
-
139
- // Limpiar el observer cuando el componente se destruya
140
- onDestroy(() => {
141
- if (mutationObserver) {
142
- mutationObserver.disconnect();
143
- mutationObserver = null;
144
- }
145
- });
146
16
  </script>
147
17
 
148
- <div class="button-group" role="group" bind:this={buttonGroupElement}>
18
+ <div class="button-group" role="group">
149
19
  {#each visibleButtons as button, i (button.icon + id + i)}
150
20
  <Tooltip text={button.tooltip}>
151
21
  <button
@@ -26,6 +26,7 @@
26
26
  bind:value={valueVar}
27
27
  placeholder=" "
28
28
  class="input-field"
29
+ on:wheel|preventDefault={() => {}}
29
30
  />
30
31
 
31
32
  <label for={inputId} class="input-label"
@@ -0,0 +1,195 @@
1
+ <script lang="ts">
2
+ export let src: string;
3
+ export let title: string = "Vista previa";
4
+ export let sandbox: string =
5
+ "allow-scripts allow-same-origin allow-popups allow-forms";
6
+ /** Alto del mockup en desktop (p. ej. `90vh`, `90dvh`) */
7
+ export let phoneHeight: string = "90vh";
8
+ /** Ancho del mockup en desktop (p. ej. `auto`, `50vh`, `400px`). `auto` = aspect-ratio 10/19.5 */
9
+ export let phoneWidth: string = "50vh";
10
+ /** Alto del mockup en mobile (p. ej. `70svh`, `500px`) */
11
+ export let mobileHeight: string = "90svh";
12
+ /** Ancho del mockup en mobile (p. ej. `95vw`, `100%`) */
13
+ export let mobileWidth: string = "90vw";
14
+ </script>
15
+
16
+ <!-- Desktop: solo el iframe, proporción tipo móvil, sin marco ni scroll del contenedor -->
17
+ <div class="phone-mockup-wrapper">
18
+ <div
19
+ class="phone-frame"
20
+ class:auto-width={phoneWidth === "auto"}
21
+ class:explicit-width={phoneWidth !== "auto"}
22
+ style:--phone-mockup-h={phoneHeight}
23
+ style:--phone-mockup-w={phoneWidth}
24
+ >
25
+ <div class="phone-notch"></div>
26
+ <div class="phone-mockup-screen">
27
+ <div class="phone-mockup-iframe-clip">
28
+ <iframe {src} {title} {sandbox} loading="lazy" class="phone-mockup-iframe"
29
+ ></iframe>
30
+ </div>
31
+ </div>
32
+ <div class="phone-homebar"></div>
33
+ </div>
34
+ </div>
35
+
36
+ <!-- Mobile: centrado en viewport -->
37
+ <div class="phone-mockup-mobile-outer">
38
+ <div
39
+ class="phone-frame mobile-frame"
40
+ style:--mobile-h={mobileHeight}
41
+ style:--mobile-w={mobileWidth}
42
+ >
43
+ <div class="phone-notch"></div>
44
+ <div class="phone-mockup-mobile-screen">
45
+ <div class="phone-mockup-iframe-clip">
46
+ <iframe
47
+ {src}
48
+ {title}
49
+ {sandbox}
50
+ loading="lazy"
51
+ class="phone-mockup-iframe-mobile"
52
+ ></iframe>
53
+ </div>
54
+ </div>
55
+ <div class="phone-homebar"></div>
56
+ </div>
57
+ </div>
58
+
59
+ <style>
60
+ /* ========== Wrapper ========== */
61
+ .phone-mockup-wrapper {
62
+ display: flex;
63
+ justify-content: center;
64
+ align-items: center;
65
+ box-sizing: border-box;
66
+ width: 100%;
67
+ height: 100dvh;
68
+ max-height: 100dvh;
69
+ overflow: hidden;
70
+ padding: 0;
71
+ margin: 0;
72
+ }
73
+
74
+ /* ========== Phone frame ========== */
75
+ .phone-frame {
76
+ position: relative;
77
+ box-sizing: border-box;
78
+ display: flex;
79
+ flex-direction: column;
80
+ border: 3px solid #1a1a1a;
81
+ border-radius: 32px;
82
+ background: #1a1a1a;
83
+ padding: 6px 4px;
84
+ box-shadow:
85
+ 0 0 0 1px rgba(255, 255, 255, 0.08) inset,
86
+ 0 8px 32px rgba(0, 0, 0, 0.25);
87
+ }
88
+
89
+ .phone-frame.auto-width {
90
+ aspect-ratio: 10 / 20;
91
+ width: min(100vw, calc(var(--phone-mockup-h) * 10 / 19.5));
92
+ height: min(var(--phone-mockup-h), calc(100vw * 19.5 / 10));
93
+ max-width: 100vw;
94
+ max-height: var(--phone-mockup-h);
95
+ }
96
+
97
+ .phone-frame.explicit-width {
98
+ width: var(--phone-mockup-w);
99
+ height: var(--phone-mockup-h);
100
+ max-width: 100vw;
101
+ }
102
+
103
+ /* Notch / dynamic island */
104
+ .phone-notch {
105
+ flex-shrink: 0;
106
+ width: 80px;
107
+ height: 6px;
108
+ margin: 4px auto 4px;
109
+ border-radius: 99px;
110
+ background: #2a2a2a;
111
+ }
112
+
113
+ /* Screen area */
114
+ .phone-mockup-screen {
115
+ position: relative;
116
+ flex: 1;
117
+ min-height: 0;
118
+ border-radius: 4px;
119
+ overflow: hidden;
120
+ background: #fff;
121
+ }
122
+
123
+ /* Home bar indicator */
124
+ .phone-homebar {
125
+ flex-shrink: 0;
126
+ width: 60px;
127
+ height: 4px;
128
+ margin: 5px auto 3px;
129
+ border-radius: 99px;
130
+ background: #444;
131
+ }
132
+
133
+ /*
134
+ * El documento embebido es otro origen: no se puede estilizar su scrollbar desde aquí.
135
+ * Recortamos ~20px abajo-derecha (barra clásica en Windows); en overlay puede perderse un borde fino.
136
+ */
137
+ .phone-mockup-iframe-clip {
138
+ position: absolute;
139
+ inset: 0;
140
+ overflow: hidden;
141
+ /* No robar toques: en algunos navegadores el clip queda encima del iframe */
142
+ pointer-events: none;
143
+ }
144
+
145
+ .phone-mockup-iframe,
146
+ .phone-mockup-iframe-mobile {
147
+ position: absolute;
148
+ top: 0;
149
+ left: 0;
150
+ width: calc(100% + 20px);
151
+ height: calc(100% + 20px);
152
+ margin-right: -20px;
153
+ margin-bottom: -20px;
154
+ border: none;
155
+ display: block;
156
+ pointer-events: auto;
157
+ }
158
+
159
+ /* ========== Mobile: viewport centrado, tarjeta ~70% alto ========== */
160
+ /* svh = viewport “pequeño” estable: dvh cambia al tocar y el flex recentra el iframe a saltos */
161
+ .phone-mockup-mobile-outer {
162
+ display: none;
163
+ box-sizing: border-box;
164
+ width: 100%;
165
+ height: 100svh;
166
+ padding: 0.75rem;
167
+ justify-content: center;
168
+ align-items: center;
169
+ overflow: hidden;
170
+ }
171
+
172
+ .phone-frame.mobile-frame {
173
+ width: var(--mobile-w, 95vw);
174
+ max-width: calc(100vw - 1.5rem);
175
+ height: var(--mobile-h, 70svh);
176
+ }
177
+
178
+ .phone-mockup-mobile-screen {
179
+ position: relative;
180
+ flex: 1;
181
+ min-height: 0;
182
+ border-radius: 4px;
183
+ overflow: hidden;
184
+ background: #fff;
185
+ }
186
+
187
+ @media (max-width: 768px) {
188
+ .phone-mockup-wrapper {
189
+ display: none;
190
+ }
191
+ .phone-mockup-mobile-outer {
192
+ display: flex;
193
+ }
194
+ }
195
+ </style>
@@ -0,0 +1,22 @@
1
+ import { SvelteComponentTyped } from "svelte";
2
+ declare const __propDef: {
3
+ props: {
4
+ src: string;
5
+ title?: string;
6
+ sandbox?: string;
7
+ /** Alto del mockup en desktop (p. ej. `90vh`, `90dvh`) */ phoneHeight?: string;
8
+ /** Ancho del mockup en desktop (p. ej. `auto`, `50vh`, `400px`). `auto` = aspect-ratio 10/19.5 */ phoneWidth?: string;
9
+ /** Alto del mockup en mobile (p. ej. `70svh`, `500px`) */ mobileHeight?: string;
10
+ /** Ancho del mockup en mobile (p. ej. `95vw`, `100%`) */ mobileWidth?: string;
11
+ };
12
+ events: {
13
+ [evt: string]: CustomEvent<any>;
14
+ };
15
+ slots: {};
16
+ };
17
+ export type PhoneMockupProps = typeof __propDef.props;
18
+ export type PhoneMockupEvents = typeof __propDef.events;
19
+ export type PhoneMockupSlots = typeof __propDef.slots;
20
+ export default class PhoneMockup extends SvelteComponentTyped<PhoneMockupProps, PhoneMockupEvents, PhoneMockupSlots> {
21
+ }
22
+ export {};
@@ -0,0 +1 @@
1
+ export { default as PhoneMockup } from './PhoneMockup.svelte';
@@ -0,0 +1 @@
1
+ export { default as PhoneMockup } from './PhoneMockup.svelte';
@@ -178,35 +178,19 @@
178
178
  <ul class="sidebar-list">
179
179
  {#each sections as section}
180
180
  <li class="sidebar-section-item">
181
- {#if section.biActivado}
182
- <button
183
- type="button"
184
- class="sidebar-section-btn"
185
- on:click={() => (section.biActivado = !section.biActivado)}
186
- >
187
- <i
188
- class="fas fa-caret-down sidebar-section-icon"
189
- aria-hidden="true"
190
- data-fa-processed="true"
191
- data-fa-i2svg-processed="true"
192
- ></i>
193
- {section.nombre}
194
- </button>
195
- {:else}
196
- <button
197
- type="button"
198
- class="sidebar-section-btn"
199
- on:click={() => (section.biActivado = !section.biActivado)}
200
- >
201
- <i
202
- class="fas fa-caret-right sidebar-section-icon"
203
- aria-hidden="true"
204
- data-fa-processed="true"
205
- data-fa-i2svg-processed="true"
206
- ></i>
207
- {section.nombre}
208
- </button>
209
- {/if}
181
+ <button
182
+ type="button"
183
+ class="sidebar-section-btn"
184
+ on:click={() => (section.biActivado = !section.biActivado)}
185
+ >
186
+ <i
187
+ class="fas {section.biActivado ? 'fa-caret-down' : 'fa-caret-right'} sidebar-section-icon"
188
+ aria-hidden="true"
189
+ data-fa-processed="true"
190
+ data-fa-i2svg-processed="true"
191
+ ></i>
192
+ {section.nombre}
193
+ </button>
210
194
  {#if section.biActivado}
211
195
  <ul
212
196
  class="sidebar-sublist"
package/dist/index.d.ts CHANGED
@@ -3,3 +3,4 @@ export * from './Inputs/index.js';
3
3
  export * from './CRUD/index.js';
4
4
  export * from './Sidebar/index.js';
5
5
  export * from './Alerts/index.js';
6
+ export * from './Preview/index.js';
package/dist/index.js CHANGED
@@ -3,3 +3,4 @@ export * from './Inputs/index.js';
3
3
  export * from './CRUD/index.js';
4
4
  export * from './Sidebar/index.js';
5
5
  export * from './Alerts/index.js';
6
+ export * from './Preview/index.js';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "grav-svelte",
3
- "version": "0.1.240",
3
+ "version": "0.1.246",
4
4
  "description": "A collection of Svelte components",
5
5
  "license": "MIT",
6
6
  "scripts": {
@@ -48,6 +48,10 @@
48
48
  "types": "./dist/Alerts/index.d.ts",
49
49
  "svelte": "./dist/Alerts/index.js"
50
50
  },
51
+ "./Preview": {
52
+ "types": "./dist/Preview/index.d.ts",
53
+ "svelte": "./dist/Preview/index.js"
54
+ },
51
55
  "./typography.css": "./dist/typography.css"
52
56
  },
53
57
  "dependencies": {
@@ -91,4 +95,4 @@
91
95
  "publishConfig": {
92
96
  "access": "public"
93
97
  }
94
- }
98
+ }