@r2digisolutions/components 0.14.21 → 0.15.0

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,4 +1,7 @@
1
1
  <script lang="ts">
2
+ import PanZoomViewport from '../PanZoomViewport/PanZoomViewport.svelte';
3
+ import OrgChartTree from './OrgChartTree.svelte';
4
+
2
5
  export interface OrgNode {
3
6
  id: string;
4
7
  name: string;
@@ -12,129 +15,41 @@
12
15
  root?: OrgNode | null;
13
16
  class?: string;
14
17
  onselect?: (node: OrgNode) => void;
18
+ /** Pan + wheel zoom viewport */
19
+ interactive?: boolean;
20
+ viewportHeight?: string;
21
+ showControls?: boolean;
22
+ /** Refit viewport when filter / data changes */
23
+ contentKey?: string | number | null;
15
24
  }
16
25
 
17
- let { root = null, class: className = '', onselect }: OrgChartProps = $props();
18
-
19
- let selected = $state<string | null>(null);
20
-
21
- const CARD_W = 176;
22
-
23
- function initials(name: string) {
24
- return name
25
- .split(/\s+/)
26
- .slice(0, 2)
27
- .map((p) => p[0]?.toUpperCase() ?? '')
28
- .join('');
29
- }
30
-
31
- function primaryAvatar(node: OrgNode) {
32
- return node.avatarUrl ?? node.assignees?.[0]?.avatarUrl ?? null;
33
- }
34
-
35
- function primaryLabel(node: OrgNode) {
36
- if (node.assignees?.length === 1) return node.assignees[0].name;
37
- return null;
38
- }
26
+ let {
27
+ root = null,
28
+ class: className = '',
29
+ onselect,
30
+ interactive = true,
31
+ viewportHeight = 'min(70vh, 640px)',
32
+ showControls = true,
33
+ contentKey = null
34
+ }: OrgChartProps = $props();
35
+
36
+ const viewportKey = $derived(contentKey ?? root?.id ?? 'empty');
39
37
  </script>
40
38
 
41
- {#snippet nodeCard(node: OrgNode)}
42
- <button
43
- type="button"
44
- class={[
45
- 'flex w-full flex-col items-center gap-1.5 rounded-xl border bg-surface-elevated px-3 py-2.5 text-center shadow-sm transition-all',
46
- selected === node.id
47
- ? 'border-brand-500 ring-2 ring-brand-500/30'
48
- : 'border-border hover:border-brand-400'
49
- ]}
50
- onclick={() => {
51
- selected = node.id;
52
- onselect?.(node);
53
- }}
39
+ {#if interactive && root}
40
+ <PanZoomViewport
41
+ class={className}
42
+ height={viewportHeight}
43
+ {showControls}
44
+ contentKey={viewportKey}
45
+ initialFit
54
46
  >
55
- {#if primaryAvatar(node)}
56
- <img src={primaryAvatar(node)!} alt="" class="h-9 w-9 rounded-full object-cover" />
57
- {:else}
58
- <span
59
- class="flex h-9 w-9 items-center justify-center rounded-full bg-brand-500/15 text-xs font-semibold text-brand-600 dark:text-brand-300"
60
- >
61
- {initials(node.name)}
62
- </span>
63
- {/if}
64
- <span class="line-clamp-2 text-sm leading-snug font-semibold text-primary">{node.name}</span>
65
- {#if primaryLabel(node)}
66
- <span class="truncate text-xs text-secondary">{primaryLabel(node)}</span>
67
- {/if}
68
- {#if node.role}
69
- <span class="truncate text-[11px] text-muted">{node.role}</span>
70
- {/if}
71
- {#if node.assignees && node.assignees.length > 1}
72
- <span class="text-[10px] text-muted">+{node.assignees.length - 1} más</span>
73
- {/if}
74
- </button>
75
- {/snippet}
76
-
77
- {#snippet childForest(node: OrgNode)}
78
- {#if node.children?.length}
79
- {#if node.children.length === 1}
80
- <div class="flex flex-col items-center">
81
- <div class="h-6 w-px shrink-0 bg-border" aria-hidden="true"></div>
82
- {@render branch(node.children[0])}
83
- </div>
84
- {:else}
85
- <div class="flex flex-col items-center">
86
- <div class="h-6 w-px shrink-0 bg-border" aria-hidden="true"></div>
87
- <div
88
- class="relative inline-flex items-start justify-center gap-4 pt-4"
89
- style:--siblings={node.children.length}
90
- >
91
- <div
92
- class="pointer-events-none absolute top-0 h-px bg-border"
93
- style:left="calc(50% / var(--siblings))"
94
- style:right="calc(50% / var(--siblings))"
95
- aria-hidden="true"
96
- ></div>
97
- {#each node.children as child (child.id)}
98
- <div class="flex shrink-0 flex-col items-center" style:width="{CARD_W}px">
99
- <div class="h-4 w-px shrink-0 bg-border" aria-hidden="true"></div>
100
- <div class="w-full">
101
- {@render nodeCard(child)}
102
- </div>
103
- </div>
104
- {/each}
105
- </div>
106
- <div class="inline-flex items-start justify-center gap-4">
107
- {#each node.children as child (child.id)}
108
- <div
109
- class="flex flex-col items-center pt-2"
110
- style:min-width="{CARD_W}px"
111
- >
112
- {#if child.children?.length}
113
- {@render childForest(child)}
114
- {/if}
115
- </div>
116
- {/each}
117
- </div>
118
- </div>
119
- {/if}
120
- {/if}
121
- {/snippet}
122
-
123
- {#snippet branch(node: OrgNode)}
124
- <div class="flex flex-col items-center">
125
- <div class="w-full" style:width="{CARD_W}px">
126
- {@render nodeCard(node)}
127
- </div>
128
- {@render childForest(node)}
47
+ <OrgChartTree {root} {onselect} />
48
+ </PanZoomViewport>
49
+ {:else if root}
50
+ <div class={['overflow-x-auto py-4', className]} role="tree" aria-label="Organization chart">
51
+ <OrgChartTree {root} {onselect} />
129
52
  </div>
130
- {/snippet}
131
-
132
- <div class={['overflow-x-auto py-4', className]} role="tree" aria-label="Organization chart">
133
- {#if root}
134
- <div class="flex justify-center px-4 pb-2">
135
- {@render branch(root)}
136
- </div>
137
- {:else}
138
- <p class="py-8 text-center text-sm text-muted">No organization data</p>
139
- {/if}
140
- </div>
53
+ {:else}
54
+ <p class={['py-8 text-center text-sm text-muted', className]}>No organization data</p>
55
+ {/if}
@@ -14,6 +14,12 @@ interface OrgChartProps {
14
14
  root?: OrgNode | null;
15
15
  class?: string;
16
16
  onselect?: (node: OrgNode) => void;
17
+ /** Pan + wheel zoom viewport */
18
+ interactive?: boolean;
19
+ viewportHeight?: string;
20
+ showControls?: boolean;
21
+ /** Refit viewport when filter / data changes */
22
+ contentKey?: string | number | null;
17
23
  }
18
24
  declare const OrgChart: import("svelte").Component<OrgChartProps, {}, "">;
19
25
  type OrgChart = ReturnType<typeof OrgChart>;
@@ -0,0 +1,130 @@
1
+ <script lang="ts">
2
+ import OrgChartNode from './OrgChartNode.svelte';
3
+ import { getPanZoomContext } from '../PanZoomViewport/panZoomContext.js';
4
+ import type { OrgNode } from './OrgChart.svelte';
5
+
6
+ interface OrgChartNodeProps {
7
+ node: OrgNode;
8
+ onselect?: (node: OrgNode) => void;
9
+ selectedId?: string | null;
10
+ }
11
+
12
+ let { node, onselect, selectedId = $bindable(null) }: OrgChartNodeProps = $props();
13
+
14
+ const panZoom = getPanZoomContext();
15
+ const CARD_W = 176;
16
+
17
+ let rowEl = $state<HTMLDivElement | null>(null);
18
+ let line = $state<{ left: number; width: number } | null>(null);
19
+
20
+ function initials(name: string) {
21
+ return name
22
+ .split(/\s+/)
23
+ .slice(0, 2)
24
+ .map((p) => p[0]?.toUpperCase() ?? '')
25
+ .join('');
26
+ }
27
+
28
+ function primaryAvatar(n: OrgNode) {
29
+ return n.avatarUrl ?? n.assignees?.[0]?.avatarUrl ?? null;
30
+ }
31
+
32
+ function primaryLabel(n: OrgNode) {
33
+ if (n.assignees?.length === 1) return n.assignees[0].name;
34
+ return null;
35
+ }
36
+
37
+ function selectNode(n: OrgNode) {
38
+ if (panZoom?.consumeClick()) return;
39
+ selectedId = n.id;
40
+ onselect?.(n);
41
+ }
42
+
43
+ function measureLine() {
44
+ const children = node.children ?? [];
45
+ if (!rowEl || children.length < 2) {
46
+ line = null;
47
+ return;
48
+ }
49
+ const cols = [...rowEl.querySelectorAll<HTMLElement>('[data-org-col]')];
50
+ if (cols.length < 2) {
51
+ line = null;
52
+ return;
53
+ }
54
+ const rowRect = rowEl.getBoundingClientRect();
55
+ const first = cols[0].getBoundingClientRect();
56
+ const last = cols[cols.length - 1].getBoundingClientRect();
57
+ const left = first.left + first.width / 2 - rowRect.left;
58
+ const right = last.left + last.width / 2 - rowRect.left;
59
+ line = { left, width: Math.max(0, right - left) };
60
+ }
61
+
62
+ $effect(() => {
63
+ node.children?.length;
64
+ if (!rowEl) return;
65
+ measureLine();
66
+ const ro = new ResizeObserver(() => measureLine());
67
+ ro.observe(rowEl);
68
+ for (const col of rowEl.querySelectorAll('[data-org-col]')) ro.observe(col);
69
+ return () => ro.disconnect();
70
+ });
71
+ </script>
72
+
73
+ <div class="flex flex-col items-center">
74
+ <div class="shrink-0" style:width="{CARD_W}px">
75
+ <button
76
+ type="button"
77
+ class={[
78
+ 'flex w-full flex-col items-center gap-1.5 rounded-xl border bg-surface-elevated px-3 py-2.5 text-center shadow-sm transition-all',
79
+ selectedId === node.id
80
+ ? 'border-brand-500 ring-2 ring-brand-500/30'
81
+ : 'border-border hover:border-brand-400'
82
+ ]}
83
+ onclick={() => selectNode(node)}
84
+ >
85
+ {#if primaryAvatar(node)}
86
+ <img src={primaryAvatar(node)!} alt="" class="h-9 w-9 rounded-full object-cover" />
87
+ {:else}
88
+ <span
89
+ class="flex h-9 w-9 items-center justify-center rounded-full bg-brand-500/15 text-xs font-semibold text-brand-600 dark:text-brand-300"
90
+ >
91
+ {initials(node.name)}
92
+ </span>
93
+ {/if}
94
+ <span class="line-clamp-2 text-sm leading-snug font-semibold text-primary">{node.name}</span>
95
+ {#if primaryLabel(node)}
96
+ <span class="truncate text-xs text-secondary">{primaryLabel(node)}</span>
97
+ {/if}
98
+ {#if node.role}
99
+ <span class="truncate text-[11px] text-muted">{node.role}</span>
100
+ {/if}
101
+ {#if node.assignees && node.assignees.length > 1}
102
+ <span class="text-[10px] text-muted">+{node.assignees.length - 1} más</span>
103
+ {/if}
104
+ </button>
105
+ </div>
106
+
107
+ {#if node.children?.length}
108
+ <div class="h-6 w-0.5 shrink-0 bg-border-strong" aria-hidden="true"></div>
109
+ {#if node.children.length === 1}
110
+ <OrgChartNode node={node.children[0]} {onselect} bind:selectedId />
111
+ {:else}
112
+ <div bind:this={rowEl} class="relative inline-flex items-start justify-center gap-4">
113
+ {#if line}
114
+ <div
115
+ class="pointer-events-none absolute top-0 h-0.5 bg-border-strong"
116
+ style:left="{line.left}px"
117
+ style:width="{line.width}px"
118
+ aria-hidden="true"
119
+ ></div>
120
+ {/if}
121
+ {#each node.children as child (child.id)}
122
+ <div data-org-col class="flex shrink-0 flex-col items-center" style:min-width="{CARD_W}px">
123
+ <div class="h-4 w-0.5 shrink-0 bg-border-strong" aria-hidden="true"></div>
124
+ <OrgChartNode node={child} {onselect} bind:selectedId />
125
+ </div>
126
+ {/each}
127
+ </div>
128
+ {/if}
129
+ {/if}
130
+ </div>
@@ -0,0 +1,10 @@
1
+ import OrgChartNode from './OrgChartNode.svelte';
2
+ import type { OrgNode } from './OrgChart.svelte';
3
+ interface OrgChartNodeProps {
4
+ node: OrgNode;
5
+ onselect?: (node: OrgNode) => void;
6
+ selectedId?: string | null;
7
+ }
8
+ declare const OrgChartNode: import("svelte").Component<OrgChartNodeProps, {}, "selectedId">;
9
+ type OrgChartNode = ReturnType<typeof OrgChartNode>;
10
+ export default OrgChartNode;
@@ -29,7 +29,7 @@
29
29
  };
30
30
  </script>
31
31
 
32
- <div class="rounded-2xl border border-border bg-surface-elevated p-4">
33
- <p class="mb-4 text-sm font-semibold text-primary">Organization</p>
34
- <OrgChart {root} />
32
+ <div class="rounded-2xl border border-border bg-surface-elevated p-2">
33
+ <p class="mb-2 px-2 text-sm font-semibold text-primary">Organization</p>
34
+ <OrgChart {root} viewportHeight="480px" />
35
35
  </div>
@@ -0,0 +1,16 @@
1
+ <script lang="ts">
2
+ import OrgChartNode from './OrgChartNode.svelte';
3
+ import type { OrgNode } from './OrgChart.svelte';
4
+
5
+ interface OrgChartTreeProps {
6
+ root: OrgNode;
7
+ onselect?: (node: OrgNode) => void;
8
+ }
9
+
10
+ let { root, onselect }: OrgChartTreeProps = $props();
11
+ let selectedId = $state<string | null>(null);
12
+ </script>
13
+
14
+ <div class="flex min-w-max justify-center px-4 py-4" role="tree" aria-label="Organization chart">
15
+ <OrgChartNode node={root} {onselect} bind:selectedId />
16
+ </div>
@@ -0,0 +1,8 @@
1
+ import type { OrgNode } from './OrgChart.svelte';
2
+ interface OrgChartTreeProps {
3
+ root: OrgNode;
4
+ onselect?: (node: OrgNode) => void;
5
+ }
6
+ declare const OrgChartTree: import("svelte").Component<OrgChartTreeProps, {}, "">;
7
+ type OrgChartTree = ReturnType<typeof OrgChartTree>;
8
+ export default OrgChartTree;
@@ -0,0 +1,8 @@
1
+ import PanZoomViewportStory from './PanZoomViewportStory.svelte';
2
+ const meta = {
3
+ title: 'Molecules/PanZoomViewport',
4
+ component: PanZoomViewportStory,
5
+ tags: ['autodocs']
6
+ };
7
+ export default meta;
8
+ export const Default = {};
@@ -0,0 +1,231 @@
1
+ <script lang="ts">
2
+ import type { Snippet } from 'svelte';
3
+ import { setContext } from 'svelte';
4
+ import IconButton from '../../atoms/IconButton/IconButton.svelte';
5
+ import FullscreenToggle from '../FullscreenToggle/FullscreenToggle.svelte';
6
+ import { PAN_ZOOM_CTX, type PanZoomContext } from './panZoomContext.js';
7
+
8
+ interface PanZoomViewportProps {
9
+ minZoom?: number;
10
+ maxZoom?: number;
11
+ initialFit?: boolean;
12
+ showControls?: boolean;
13
+ showFullscreen?: boolean;
14
+ height?: string;
15
+ contentKey?: string | number | null;
16
+ class?: string;
17
+ children?: Snippet;
18
+ }
19
+
20
+ let {
21
+ minZoom = 0.25,
22
+ maxZoom = 2,
23
+ initialFit = true,
24
+ showControls = true,
25
+ showFullscreen = true,
26
+ height = 'min(70vh, 640px)',
27
+ contentKey = null,
28
+ class: className = '',
29
+ children
30
+ }: PanZoomViewportProps = $props();
31
+
32
+ let scale = $state(1);
33
+ let panX = $state(0);
34
+ let panY = $state(0);
35
+ let dragging = $state(false);
36
+ let moved = $state(false);
37
+ let suppressClick = $state(false);
38
+ let pointerId = $state<number | null>(null);
39
+ let startX = 0;
40
+ let startY = 0;
41
+ let originPanX = 0;
42
+ let originPanY = 0;
43
+ let fullscreen = $state(false);
44
+
45
+ let shellEl = $state<HTMLDivElement | null>(null);
46
+ let viewportEl = $state<HTMLDivElement | null>(null);
47
+ let contentEl = $state<HTMLDivElement | null>(null);
48
+
49
+ const MOVE_THRESHOLD = 6;
50
+ const FIT_PADDING = 32;
51
+
52
+ const shellHeight = $derived(fullscreen ? '100%' : height);
53
+
54
+ function clampZoom(z: number) {
55
+ return Math.min(maxZoom, Math.max(minZoom, z));
56
+ }
57
+
58
+ export function fitToContent() {
59
+ if (!viewportEl || !contentEl) return;
60
+ const vw = viewportEl.clientWidth;
61
+ const vh = viewportEl.clientHeight;
62
+ const cw = contentEl.scrollWidth;
63
+ const ch = contentEl.scrollHeight;
64
+ if (!vw || !vh || !cw || !ch) return;
65
+
66
+ const next = clampZoom(Math.min((vw - FIT_PADDING) / cw, (vh - FIT_PADDING) / ch, 1));
67
+ scale = next;
68
+ panX = (vw - cw * next) / 2;
69
+ panY = (vh - ch * next) / 2;
70
+ }
71
+
72
+ function zoomAt(nextScale: number, clientX?: number, clientY?: number) {
73
+ if (!viewportEl) return;
74
+ const prev = scale;
75
+ const next = clampZoom(nextScale);
76
+ if (next === prev) return;
77
+
78
+ const rect = viewportEl.getBoundingClientRect();
79
+ const px = clientX != null ? clientX - rect.left : rect.width / 2;
80
+ const py = clientY != null ? clientY - rect.top : rect.height / 2;
81
+
82
+ panX = px - ((px - panX) * next) / prev;
83
+ panY = py - ((py - panY) * next) / prev;
84
+ scale = next;
85
+ }
86
+
87
+ function zoomIn() {
88
+ zoomAt(scale * 1.2);
89
+ }
90
+
91
+ function zoomOut() {
92
+ zoomAt(scale / 1.2);
93
+ }
94
+
95
+ function onWheel(e: WheelEvent) {
96
+ e.preventDefault();
97
+ const delta = e.deltaY > 0 ? -0.12 : 0.12;
98
+ zoomAt(scale + delta * scale, e.clientX, e.clientY);
99
+ }
100
+
101
+ function onPointerDown(e: PointerEvent) {
102
+ if (e.button !== 0) return;
103
+ if ((e.target as HTMLElement).closest('[data-panzoom-chrome]')) return;
104
+
105
+ dragging = true;
106
+ moved = false;
107
+ pointerId = e.pointerId;
108
+ startX = e.clientX;
109
+ startY = e.clientY;
110
+ originPanX = panX;
111
+ originPanY = panY;
112
+ (e.currentTarget as HTMLElement).setPointerCapture(e.pointerId);
113
+ }
114
+
115
+ function onPointerMove(e: PointerEvent) {
116
+ if (!dragging || pointerId !== e.pointerId) return;
117
+ const dx = e.clientX - startX;
118
+ const dy = e.clientY - startY;
119
+ if (!moved && Math.hypot(dx, dy) >= MOVE_THRESHOLD) moved = true;
120
+ panX = originPanX + dx;
121
+ panY = originPanY + dy;
122
+ }
123
+
124
+ function onPointerUp(e: PointerEvent) {
125
+ if (!dragging || pointerId !== e.pointerId) return;
126
+ dragging = false;
127
+ pointerId = null;
128
+ try {
129
+ (e.currentTarget as HTMLElement).releasePointerCapture(e.pointerId);
130
+ } catch {
131
+ /* already released */
132
+ }
133
+ if (moved) suppressClick = true;
134
+ moved = false;
135
+ }
136
+
137
+ function onDoubleClick(e: MouseEvent) {
138
+ if ((e.target as HTMLElement).closest('[data-panzoom-chrome]')) return;
139
+ fitToContent();
140
+ }
141
+
142
+ function onFullscreenChange(active: boolean) {
143
+ fullscreen = active;
144
+ requestAnimationFrame(() => fitToContent());
145
+ }
146
+
147
+ const ctx: PanZoomContext = {
148
+ consumeClick: () => {
149
+ if (!suppressClick) return false;
150
+ suppressClick = false;
151
+ return true;
152
+ }
153
+ };
154
+
155
+ setContext(PAN_ZOOM_CTX, ctx);
156
+
157
+ $effect(() => {
158
+ contentKey;
159
+ if (!initialFit) return;
160
+ const id = requestAnimationFrame(() => {
161
+ requestAnimationFrame(() => fitToContent());
162
+ });
163
+ return () => cancelAnimationFrame(id);
164
+ });
165
+ </script>
166
+
167
+ <div
168
+ bind:this={shellEl}
169
+ class={[
170
+ 'relative overflow-hidden rounded-xl border border-border bg-surface-base/40',
171
+ fullscreen && 'rounded-none border-0',
172
+ className
173
+ ]}
174
+ style:height={shellHeight}
175
+ >
176
+ {#if showControls}
177
+ <div
178
+ class="absolute top-3 right-3 z-10 flex items-center gap-1 rounded-lg border border-border bg-surface-elevated/95 p-1 shadow-sm backdrop-blur-sm"
179
+ data-panzoom-chrome
180
+ >
181
+ <IconButton label="Alejar" size="sm" variant="ghost" onclick={zoomOut}>
182
+ <span class="text-base leading-none font-medium">−</span>
183
+ </IconButton>
184
+ <span class="min-w-[3rem] px-1 text-center text-xs tabular-nums text-secondary">
185
+ {Math.round(scale * 100)}%
186
+ </span>
187
+ <IconButton label="Acercar" size="sm" variant="ghost" onclick={zoomIn}>
188
+ <span class="text-base leading-none font-medium">+</span>
189
+ </IconButton>
190
+ <IconButton label="Ajustar a vista" size="sm" variant="ghost" onclick={fitToContent}>
191
+ <span class="text-xs font-semibold">⊡</span>
192
+ </IconButton>
193
+ {#if showFullscreen}
194
+ <FullscreenToggle
195
+ target={shellEl}
196
+ size="sm"
197
+ bind:active={fullscreen}
198
+ enterLabel="Pantalla completa"
199
+ exitLabel="Salir de pantalla completa"
200
+ onchange={onFullscreenChange}
201
+ />
202
+ {/if}
203
+ </div>
204
+ {/if}
205
+
206
+ <div
207
+ bind:this={viewportEl}
208
+ class={[
209
+ 'h-full w-full touch-none select-none',
210
+ dragging ? 'cursor-grabbing' : 'cursor-grab'
211
+ ]}
212
+ onwheel={onWheel}
213
+ onpointerdown={onPointerDown}
214
+ onpointermove={onPointerMove}
215
+ onpointerup={onPointerUp}
216
+ onpointercancel={onPointerUp}
217
+ ondblclick={onDoubleClick}
218
+ role="presentation"
219
+ >
220
+ <div
221
+ bind:this={contentEl}
222
+ class="inline-block will-change-transform"
223
+ style:transform="translate({panX}px, {panY}px) scale({scale})"
224
+ style:transform-origin="0 0"
225
+ >
226
+ {#if children}
227
+ {@render children()}
228
+ {/if}
229
+ </div>
230
+ </div>
231
+ </div>
@@ -0,0 +1,17 @@
1
+ import type { Snippet } from 'svelte';
2
+ interface PanZoomViewportProps {
3
+ minZoom?: number;
4
+ maxZoom?: number;
5
+ initialFit?: boolean;
6
+ showControls?: boolean;
7
+ showFullscreen?: boolean;
8
+ height?: string;
9
+ contentKey?: string | number | null;
10
+ class?: string;
11
+ children?: Snippet;
12
+ }
13
+ declare const PanZoomViewport: import("svelte").Component<PanZoomViewportProps, {
14
+ fitToContent: () => void;
15
+ }, "">;
16
+ type PanZoomViewport = ReturnType<typeof PanZoomViewport>;
17
+ export default PanZoomViewport;
@@ -0,0 +1,20 @@
1
+ <script lang="ts">
2
+ import PanZoomViewport from './PanZoomViewport.svelte';
3
+ </script>
4
+
5
+ <PanZoomViewport height="420px">
6
+ <div class="flex min-w-max flex-col items-center gap-8 p-12">
7
+ <div class="rounded-xl border border-border bg-surface-elevated px-6 py-4 text-sm font-semibold">
8
+ Arrastra para mover · rueda para zoom · doble clic para ajustar
9
+ </div>
10
+ <div class="flex gap-16">
11
+ {#each Array.from({ length: 8 }) as _, i (i)}
12
+ <div
13
+ class="flex h-24 w-40 items-center justify-center rounded-xl border border-border bg-surface-elevated text-sm"
14
+ >
15
+ Bloque {i + 1}
16
+ </div>
17
+ {/each}
18
+ </div>
19
+ </div>
20
+ </PanZoomViewport>
@@ -0,0 +1,18 @@
1
+ interface $$__sveltets_2_IsomorphicComponent<Props extends Record<string, any> = any, Events extends Record<string, any> = any, Slots extends Record<string, any> = any, Exports = {}, Bindings = string> {
2
+ new (options: import('svelte').ComponentConstructorOptions<Props>): import('svelte').SvelteComponent<Props, Events, Slots> & {
3
+ $$bindings?: Bindings;
4
+ } & Exports;
5
+ (internal: unknown, props: {
6
+ $$events?: Events;
7
+ $$slots?: Slots;
8
+ }): Exports & {
9
+ $set?: any;
10
+ $on?: any;
11
+ };
12
+ z_$$bindings?: Bindings;
13
+ }
14
+ declare const PanZoomViewportStory: $$__sveltets_2_IsomorphicComponent<Record<string, never>, {
15
+ [evt: string]: CustomEvent<any>;
16
+ }, {}, {}, string>;
17
+ type PanZoomViewportStory = InstanceType<typeof PanZoomViewportStory>;
18
+ export default PanZoomViewportStory;
@@ -0,0 +1,5 @@
1
+ export declare const PAN_ZOOM_CTX: unique symbol;
2
+ export type PanZoomContext = {
3
+ consumeClick: () => boolean;
4
+ };
5
+ export declare function getPanZoomContext(): PanZoomContext | undefined;
@@ -0,0 +1,5 @@
1
+ import { getContext } from 'svelte';
2
+ export const PAN_ZOOM_CTX = Symbol('panZoom');
3
+ export function getPanZoomContext() {
4
+ return getContext(PAN_ZOOM_CTX);
5
+ }
package/dist/index.d.ts CHANGED
@@ -252,6 +252,8 @@ export { default as VirtualList } from './components/molecules/VirtualList/Virtu
252
252
  export { default as MasonryGrid } from './components/molecules/MasonryGrid/MasonryGrid.svelte';
253
253
  export { default as OrgChart } from './components/molecules/OrgChart/OrgChart.svelte';
254
254
  export type { OrgNode } from './components/molecules/OrgChart/OrgChart.svelte';
255
+ export { default as PanZoomViewport } from './components/molecules/PanZoomViewport/PanZoomViewport.svelte';
256
+ export { getPanZoomContext, PAN_ZOOM_CTX, type PanZoomContext } from './components/molecules/PanZoomViewport/panZoomContext.js';
255
257
  export { default as TableToolbar } from './components/molecules/TableToolbar/TableToolbar.svelte';
256
258
  export type { TableColumnOption, TableDensity } from './components/molecules/TableToolbar/TableToolbar.svelte';
257
259
  export { default as ButtonGroup } from './components/molecules/ButtonGroup/ButtonGroup.svelte';
package/dist/index.js CHANGED
@@ -194,6 +194,8 @@ export { default as MessageList } from './components/molecules/MessageList/Messa
194
194
  export { default as VirtualList } from './components/molecules/VirtualList/VirtualList.svelte';
195
195
  export { default as MasonryGrid } from './components/molecules/MasonryGrid/MasonryGrid.svelte';
196
196
  export { default as OrgChart } from './components/molecules/OrgChart/OrgChart.svelte';
197
+ export { default as PanZoomViewport } from './components/molecules/PanZoomViewport/PanZoomViewport.svelte';
198
+ export { getPanZoomContext, PAN_ZOOM_CTX } from './components/molecules/PanZoomViewport/panZoomContext.js';
197
199
  export { default as TableToolbar } from './components/molecules/TableToolbar/TableToolbar.svelte';
198
200
  export { default as ButtonGroup } from './components/molecules/ButtonGroup/ButtonGroup.svelte';
199
201
  export { default as ThemeToggle } from './components/molecules/ThemeToggle/ThemeToggle.svelte';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@r2digisolutions/components",
3
- "version": "0.14.21",
3
+ "version": "0.15.0",
4
4
  "private": false,
5
5
  "description": "R2DigiSolutions Svelte 5 component library — Atomic Design, Tailwind 4, Light/Dark mode",
6
6
  "license": "MIT",