svelte-p5-components 0.4.0 → 0.4.1

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.
@@ -72,36 +72,59 @@
72
72
  let size = $state(untrack(() => ({ w: typeof width === 'number' ? width : 480, h: 40 })));
73
73
  let rootEl: HTMLDivElement | null = $state(null);
74
74
 
75
- function clampToViewport() {
76
- if (typeof window === 'undefined') return;
77
- // When constrained to a parent, neodrag's `bounds(BoundsFrom.parent())`
78
- // already keeps the window inside; viewport math here would be wrong
79
- // because positions are relative to the parent, not the viewport.
80
- if (constrained === 'parent') return;
81
- const vw = window.innerWidth;
82
- const vh = window.innerHeight;
83
- // Never allow the titlebar above the viewport top - that's the
84
- // "unreachable" case. Allow partial horizontal overflow so narrow
85
- // screens don't trap wide windows, but always keep `minVisible`
86
- // pixels of the window on-screen for a grab handle.
87
- const minX = -(size.w - minVisible);
88
- const maxX = vw - minVisible;
89
- const minY = 0;
90
- const maxY = vh - minVisible;
91
- pos = {
92
- x: Math.max(minX, Math.min(maxX, pos.x)),
93
- y: Math.max(minY, Math.min(maxY, pos.y))
94
- };
75
+ // Clamp `pos` back inside whatever sandbox `constrained` points at.
76
+ // Runs on drag end (drag might overshoot during pointer-move) AND on
77
+ // any resize of the sandbox (window → viewport mode, parent element →
78
+ // parent mode) so the window snaps back into view when its container
79
+ // shrinks underneath it.
80
+ function clamp() {
81
+ if (typeof window === 'undefined' || !rootEl) return;
82
+
83
+ if (constrained === 'parent') {
84
+ // Position is relative to the parent's padding-box, so bounds
85
+ // are [0, parentSize - windowSize]. Falls through to no-op if
86
+ // the window is somehow larger than the parent (Math.max(0, ...)).
87
+ const parent = rootEl.parentElement;
88
+ if (!parent) return;
89
+ const r = parent.getBoundingClientRect();
90
+ const maxX = Math.max(0, r.width - size.w);
91
+ const maxY = Math.max(0, r.height - size.h);
92
+ pos = {
93
+ x: Math.max(0, Math.min(maxX, pos.x)),
94
+ y: Math.max(0, Math.min(maxY, pos.y))
95
+ };
96
+ return;
97
+ }
98
+
99
+ if (constrained === 'viewport') {
100
+ // Never allow the titlebar above the viewport top - that's the
101
+ // "unreachable" case. Allow partial horizontal overflow so narrow
102
+ // screens don't trap wide windows, but always keep `minVisible`
103
+ // pixels of the window on-screen for a grab handle.
104
+ const vw = window.innerWidth;
105
+ const vh = window.innerHeight;
106
+ const minX = -(size.w - minVisible);
107
+ const maxX = vw - minVisible;
108
+ const minY = 0;
109
+ const maxY = vh - minVisible;
110
+ pos = {
111
+ x: Math.max(minX, Math.min(maxX, pos.x)),
112
+ y: Math.max(minY, Math.min(maxY, pos.y))
113
+ };
114
+ }
115
+ // 'none': no clamping.
95
116
  }
96
117
 
97
118
  // Keep size fresh (used for clamp math) - ResizeObserver on our own
98
- // root element catches CSS resize-from-corner too.
119
+ // root element catches CSS resize-from-corner too. We also re-clamp
120
+ // after each size change so corner-resizing past a bound snaps back.
99
121
  $effect(() => {
100
122
  if (!rootEl) return;
101
123
  const el = rootEl;
102
124
  const ro = new ResizeObserver(() => {
103
125
  const r = el.getBoundingClientRect();
104
126
  size = { w: r.width, h: r.height };
127
+ clamp();
105
128
  });
106
129
  ro.observe(el);
107
130
  // seed
@@ -110,15 +133,30 @@
110
133
  return () => ro.disconnect();
111
134
  });
112
135
 
113
- // Re-clamp on window resize - this is the "snap back in when the
114
- // viewport shrinks" case.
136
+ // Re-clamp on window resize - the "snap back when the viewport shrinks"
137
+ // case. Harmless in parent mode (clamp() handles parent bounds too), so
138
+ // we keep a single global listener instead of branching.
115
139
  $effect(() => {
116
140
  if (typeof window === 'undefined') return;
117
- const onResize = () => clampToViewport();
141
+ const onResize = () => clamp();
118
142
  window.addEventListener('resize', onResize);
119
143
  return () => window.removeEventListener('resize', onResize);
120
144
  });
121
145
 
146
+ // Parent mode: watch the parent element's size too. A flex/grid row
147
+ // resizing around us wouldn't trigger `window.resize`, so without this
148
+ // the window can end up stranded outside a shrunken container until the
149
+ // user nudges it.
150
+ $effect(() => {
151
+ if (typeof window === 'undefined') return;
152
+ if (constrained !== 'parent' || !rootEl) return;
153
+ const parent = rootEl.parentElement;
154
+ if (!parent) return;
155
+ const ro = new ResizeObserver(() => clamp());
156
+ ro.observe(parent);
157
+ return () => ro.disconnect();
158
+ });
159
+
122
160
  // The neodrag `position` plugin takes a `current: {x, y}` and forces
123
161
  // the element to that transform. Wrapped in a Compartment so updates
124
162
  // to `pos` re-run just this plugin (not the whole plugin list).
@@ -132,7 +170,7 @@
132
170
  onDrag: ({ offset }) => {
133
171
  pos = { x: offset.x, y: offset.y };
134
172
  },
135
- onDragEnd: () => clampToViewport()
173
+ onDragEnd: () => clamp()
136
174
  }),
137
175
  ...(constrained === 'viewport' ? [bounds(BoundsFrom.viewport())] : []),
138
176
  ...(constrained === 'parent' ? [bounds(BoundsFrom.parent())] : []),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "svelte-p5-components",
3
- "version": "0.4.0",
3
+ "version": "0.4.1",
4
4
  "description": "Higher-level Svelte 5 components built on svelte-p5: responsive canvas, FPS monitor, sketch debug overlay, draggable windows",
5
5
  "keywords": [
6
6
  "svelte",