@eduvidu/react-autoscale 0.1.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.
- package/CHANGELOG.md +29 -0
- package/LICENSE +21 -0
- package/README.md +436 -0
- package/dist/index.cjs +414 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +313 -0
- package/dist/index.d.ts +313 -0
- package/dist/index.js +401 -0
- package/dist/index.js.map +1 -0
- package/package.json +90 -0
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,414 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var react = require('react');
|
|
4
|
+
var jsxRuntime = require('react/jsx-runtime');
|
|
5
|
+
|
|
6
|
+
// src/hooks/useAutoScale.ts
|
|
7
|
+
|
|
8
|
+
// src/core/calculateScale.ts
|
|
9
|
+
function calculateScale(container, content, mode = "contain", customCalculator) {
|
|
10
|
+
if (customCalculator) {
|
|
11
|
+
const custom = customCalculator(container, content);
|
|
12
|
+
return Number.isFinite(custom) ? custom : 1;
|
|
13
|
+
}
|
|
14
|
+
if (content.width <= 0 || content.height <= 0) return 1;
|
|
15
|
+
if (container.width <= 0 && container.height <= 0) return 1;
|
|
16
|
+
const scaleX = container.width > 0 ? container.width / content.width : 1;
|
|
17
|
+
const scaleY = container.height > 0 ? container.height / content.height : 1;
|
|
18
|
+
switch (mode) {
|
|
19
|
+
case "width":
|
|
20
|
+
return container.width > 0 ? scaleX : 1;
|
|
21
|
+
case "height":
|
|
22
|
+
return container.height > 0 ? scaleY : 1;
|
|
23
|
+
case "cover":
|
|
24
|
+
return Math.max(scaleX, scaleY);
|
|
25
|
+
case "contain":
|
|
26
|
+
default:
|
|
27
|
+
return Math.min(scaleX, scaleY);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// src/core/clamp.ts
|
|
32
|
+
function clampScale(scale, min, max) {
|
|
33
|
+
if (!Number.isFinite(scale)) return 1;
|
|
34
|
+
let lo = min;
|
|
35
|
+
let hi = max;
|
|
36
|
+
if (lo !== void 0 && hi !== void 0 && lo > hi) {
|
|
37
|
+
[lo, hi] = [hi, lo];
|
|
38
|
+
}
|
|
39
|
+
let result = scale;
|
|
40
|
+
if (lo !== void 0 && Number.isFinite(lo)) {
|
|
41
|
+
result = Math.max(result, lo);
|
|
42
|
+
}
|
|
43
|
+
if (hi !== void 0 && Number.isFinite(hi)) {
|
|
44
|
+
result = Math.min(result, hi);
|
|
45
|
+
}
|
|
46
|
+
return result;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// src/core/guards.ts
|
|
50
|
+
function isBrowser() {
|
|
51
|
+
return typeof window !== "undefined" && typeof window.document !== "undefined";
|
|
52
|
+
}
|
|
53
|
+
function isResizeObserverSupported() {
|
|
54
|
+
return isBrowser() && typeof ResizeObserver !== "undefined";
|
|
55
|
+
}
|
|
56
|
+
function canUseDom() {
|
|
57
|
+
return isBrowser() && typeof window.document.createElement === "function";
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// src/core/measure.ts
|
|
61
|
+
var ZERO = Object.freeze({ width: 0, height: 0 });
|
|
62
|
+
function measureElement(element, strategy = "scrollSize", customMeasurer) {
|
|
63
|
+
if (!element) return ZERO;
|
|
64
|
+
switch (strategy) {
|
|
65
|
+
case "custom": {
|
|
66
|
+
if (customMeasurer) {
|
|
67
|
+
const result = customMeasurer(element);
|
|
68
|
+
return {
|
|
69
|
+
width: Math.max(0, result.width),
|
|
70
|
+
height: Math.max(0, result.height)
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
return measureScrollSize(element);
|
|
74
|
+
}
|
|
75
|
+
case "boundingRect": {
|
|
76
|
+
const rect = element.getBoundingClientRect();
|
|
77
|
+
return {
|
|
78
|
+
width: Math.max(0, rect.width),
|
|
79
|
+
height: Math.max(0, rect.height)
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
case "scrollSize":
|
|
83
|
+
default:
|
|
84
|
+
return measureScrollSize(element);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
function measureScrollSize(element) {
|
|
88
|
+
const width = element.scrollWidth || element.offsetWidth || 0;
|
|
89
|
+
const height = element.scrollHeight || element.offsetHeight || 0;
|
|
90
|
+
return { width: Math.max(0, width), height: Math.max(0, height) };
|
|
91
|
+
}
|
|
92
|
+
function measureContainer(element) {
|
|
93
|
+
if (!element) return ZERO;
|
|
94
|
+
return {
|
|
95
|
+
width: Math.max(0, element.clientWidth),
|
|
96
|
+
height: Math.max(0, element.clientHeight)
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// src/core/scheduler.ts
|
|
101
|
+
function createScheduler(options = {}) {
|
|
102
|
+
let rafId = null;
|
|
103
|
+
let timeoutId = null;
|
|
104
|
+
let lastExecution = 0;
|
|
105
|
+
let destroyed = false;
|
|
106
|
+
const { throttleMs, debounceMs } = options;
|
|
107
|
+
function clearPendingRaf() {
|
|
108
|
+
if (rafId !== null) {
|
|
109
|
+
cancelAnimationFrame(rafId);
|
|
110
|
+
rafId = null;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
function clearPendingTimeout() {
|
|
114
|
+
if (timeoutId !== null) {
|
|
115
|
+
clearTimeout(timeoutId);
|
|
116
|
+
timeoutId = null;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
function enqueueRaf(callback) {
|
|
120
|
+
clearPendingRaf();
|
|
121
|
+
if (!isBrowser()) {
|
|
122
|
+
callback();
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
rafId = requestAnimationFrame(() => {
|
|
126
|
+
rafId = null;
|
|
127
|
+
if (!destroyed) callback();
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
function schedule(callback) {
|
|
131
|
+
if (destroyed) return;
|
|
132
|
+
if (debounceMs !== void 0 && debounceMs > 0) {
|
|
133
|
+
clearPendingTimeout();
|
|
134
|
+
clearPendingRaf();
|
|
135
|
+
timeoutId = setTimeout(() => {
|
|
136
|
+
timeoutId = null;
|
|
137
|
+
enqueueRaf(callback);
|
|
138
|
+
}, debounceMs);
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
if (throttleMs !== void 0 && throttleMs > 0) {
|
|
142
|
+
const now = Date.now();
|
|
143
|
+
const elapsed = now - lastExecution;
|
|
144
|
+
if (elapsed >= throttleMs) {
|
|
145
|
+
lastExecution = now;
|
|
146
|
+
enqueueRaf(callback);
|
|
147
|
+
} else {
|
|
148
|
+
clearPendingTimeout();
|
|
149
|
+
clearPendingRaf();
|
|
150
|
+
timeoutId = setTimeout(() => {
|
|
151
|
+
timeoutId = null;
|
|
152
|
+
lastExecution = Date.now();
|
|
153
|
+
enqueueRaf(callback);
|
|
154
|
+
}, throttleMs - elapsed);
|
|
155
|
+
}
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
enqueueRaf(callback);
|
|
159
|
+
}
|
|
160
|
+
function cancel() {
|
|
161
|
+
clearPendingRaf();
|
|
162
|
+
clearPendingTimeout();
|
|
163
|
+
}
|
|
164
|
+
function destroy() {
|
|
165
|
+
if (destroyed) return;
|
|
166
|
+
cancel();
|
|
167
|
+
destroyed = true;
|
|
168
|
+
}
|
|
169
|
+
return { schedule, cancel, destroy };
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// src/hooks/useAutoScale.ts
|
|
173
|
+
var SCALE_EPSILON = 1e-4;
|
|
174
|
+
var ZERO2 = { width: 0, height: 0 };
|
|
175
|
+
function useAutoScale(options = {}) {
|
|
176
|
+
const {
|
|
177
|
+
mode = "contain",
|
|
178
|
+
minScale,
|
|
179
|
+
maxScale,
|
|
180
|
+
customCalculator,
|
|
181
|
+
observeParent = false,
|
|
182
|
+
throttle: throttleMs,
|
|
183
|
+
debounce: debounceMs,
|
|
184
|
+
disabled = false,
|
|
185
|
+
debug = false,
|
|
186
|
+
measurementStrategy = "scrollSize",
|
|
187
|
+
customMeasurer,
|
|
188
|
+
onScaleChange
|
|
189
|
+
} = options;
|
|
190
|
+
const containerRef = react.useRef(null);
|
|
191
|
+
const contentRef = react.useRef(null);
|
|
192
|
+
const [scale, setScale] = react.useState(1);
|
|
193
|
+
const [containerDimensions, setContainerDimensions] = react.useState(ZERO2);
|
|
194
|
+
const [contentDimensions, setContentDimensions] = react.useState(ZERO2);
|
|
195
|
+
const [isReady, setIsReady] = react.useState(false);
|
|
196
|
+
const optionsRef = react.useRef(options);
|
|
197
|
+
optionsRef.current = options;
|
|
198
|
+
const onScaleChangeRef = react.useRef(onScaleChange);
|
|
199
|
+
onScaleChangeRef.current = onScaleChange;
|
|
200
|
+
const prevScaleRef = react.useRef(1);
|
|
201
|
+
const measure = react.useCallback(() => {
|
|
202
|
+
const container = containerRef.current;
|
|
203
|
+
const content = contentRef.current;
|
|
204
|
+
if (!container || !content) return;
|
|
205
|
+
const cDims = measureContainer(container);
|
|
206
|
+
const nDims = measureElement(content, measurementStrategy, customMeasurer);
|
|
207
|
+
if (debug) {
|
|
208
|
+
console.log("[react-autoscale]", {
|
|
209
|
+
container: cDims,
|
|
210
|
+
content: nDims,
|
|
211
|
+
mode
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
const raw = calculateScale(cDims, nDims, mode, customCalculator);
|
|
215
|
+
const clamped = clampScale(raw, minScale, maxScale);
|
|
216
|
+
setContainerDimensions(
|
|
217
|
+
(prev) => prev.width === cDims.width && prev.height === cDims.height ? prev : cDims
|
|
218
|
+
);
|
|
219
|
+
setContentDimensions(
|
|
220
|
+
(prev) => prev.width === nDims.width && prev.height === nDims.height ? prev : nDims
|
|
221
|
+
);
|
|
222
|
+
setScale((prev) => {
|
|
223
|
+
if (Math.abs(prev - clamped) < SCALE_EPSILON) return prev;
|
|
224
|
+
const cb = onScaleChangeRef.current;
|
|
225
|
+
if (cb) {
|
|
226
|
+
cb(clamped, {
|
|
227
|
+
containerDimensions: cDims,
|
|
228
|
+
contentDimensions: nDims,
|
|
229
|
+
previousScale: prevScaleRef.current
|
|
230
|
+
});
|
|
231
|
+
}
|
|
232
|
+
prevScaleRef.current = clamped;
|
|
233
|
+
return clamped;
|
|
234
|
+
});
|
|
235
|
+
setIsReady(true);
|
|
236
|
+
}, [
|
|
237
|
+
mode,
|
|
238
|
+
minScale,
|
|
239
|
+
maxScale,
|
|
240
|
+
customCalculator,
|
|
241
|
+
measurementStrategy,
|
|
242
|
+
customMeasurer,
|
|
243
|
+
debug
|
|
244
|
+
]);
|
|
245
|
+
react.useEffect(() => {
|
|
246
|
+
if (disabled || !isBrowser() || !isResizeObserverSupported()) return;
|
|
247
|
+
const scheduler = createScheduler({ throttleMs, debounceMs });
|
|
248
|
+
const observer = new ResizeObserver(() => {
|
|
249
|
+
scheduler.schedule(measure);
|
|
250
|
+
});
|
|
251
|
+
const target = observeParent ? containerRef.current?.parentElement : containerRef.current;
|
|
252
|
+
if (target) {
|
|
253
|
+
observer.observe(target);
|
|
254
|
+
}
|
|
255
|
+
if (contentRef.current) {
|
|
256
|
+
observer.observe(contentRef.current);
|
|
257
|
+
}
|
|
258
|
+
scheduler.schedule(measure);
|
|
259
|
+
return () => {
|
|
260
|
+
observer.disconnect();
|
|
261
|
+
scheduler.destroy();
|
|
262
|
+
};
|
|
263
|
+
}, [measure, disabled, observeParent, throttleMs, debounceMs]);
|
|
264
|
+
const dimensions = {
|
|
265
|
+
width: contentDimensions.width * scale,
|
|
266
|
+
height: contentDimensions.height * scale
|
|
267
|
+
};
|
|
268
|
+
return {
|
|
269
|
+
containerRef,
|
|
270
|
+
contentRef,
|
|
271
|
+
scale,
|
|
272
|
+
dimensions,
|
|
273
|
+
containerDimensions,
|
|
274
|
+
contentDimensions,
|
|
275
|
+
isReady
|
|
276
|
+
};
|
|
277
|
+
}
|
|
278
|
+
var DEFAULT_CONTEXT = {
|
|
279
|
+
scale: 1,
|
|
280
|
+
parentScale: 1,
|
|
281
|
+
depth: 0
|
|
282
|
+
};
|
|
283
|
+
var ScaleContext = react.createContext(DEFAULT_CONTEXT);
|
|
284
|
+
ScaleContext.displayName = "ScaleContext";
|
|
285
|
+
function ScaleProvider({
|
|
286
|
+
scale,
|
|
287
|
+
children
|
|
288
|
+
}) {
|
|
289
|
+
const parent = react.useContext(ScaleContext);
|
|
290
|
+
const value = react.useMemo(
|
|
291
|
+
() => ({
|
|
292
|
+
scale,
|
|
293
|
+
parentScale: parent.parentScale * parent.scale,
|
|
294
|
+
depth: parent.depth + 1
|
|
295
|
+
}),
|
|
296
|
+
[scale, parent.parentScale, parent.scale, parent.depth]
|
|
297
|
+
);
|
|
298
|
+
return /* @__PURE__ */ jsxRuntime.jsx(ScaleContext.Provider, { value, children });
|
|
299
|
+
}
|
|
300
|
+
function useScaleContext() {
|
|
301
|
+
return react.useContext(ScaleContext);
|
|
302
|
+
}
|
|
303
|
+
var AutoScale = react.forwardRef(
|
|
304
|
+
function AutoScale2(props, ref) {
|
|
305
|
+
const {
|
|
306
|
+
// AutoScaleOptions
|
|
307
|
+
mode,
|
|
308
|
+
minScale,
|
|
309
|
+
maxScale,
|
|
310
|
+
customCalculator,
|
|
311
|
+
observeParent,
|
|
312
|
+
transformOrigin = "top left",
|
|
313
|
+
throttle,
|
|
314
|
+
debounce,
|
|
315
|
+
disabled,
|
|
316
|
+
debug,
|
|
317
|
+
compensationMode = "none",
|
|
318
|
+
onScaleChange,
|
|
319
|
+
measurementStrategy,
|
|
320
|
+
customMeasurer,
|
|
321
|
+
// Component-specific
|
|
322
|
+
children,
|
|
323
|
+
contentClassName,
|
|
324
|
+
contentStyle: userContentStyle,
|
|
325
|
+
// Forwarded to outer div
|
|
326
|
+
style: userContainerStyle,
|
|
327
|
+
...rest
|
|
328
|
+
} = props;
|
|
329
|
+
const {
|
|
330
|
+
containerRef,
|
|
331
|
+
contentRef,
|
|
332
|
+
scale,
|
|
333
|
+
contentDimensions,
|
|
334
|
+
isReady
|
|
335
|
+
} = useAutoScale({
|
|
336
|
+
mode,
|
|
337
|
+
minScale,
|
|
338
|
+
maxScale,
|
|
339
|
+
customCalculator,
|
|
340
|
+
observeParent,
|
|
341
|
+
throttle,
|
|
342
|
+
debounce,
|
|
343
|
+
disabled,
|
|
344
|
+
debug,
|
|
345
|
+
compensationMode,
|
|
346
|
+
onScaleChange,
|
|
347
|
+
measurementStrategy,
|
|
348
|
+
customMeasurer
|
|
349
|
+
});
|
|
350
|
+
react.useImperativeHandle(ref, () => containerRef.current, [containerRef]);
|
|
351
|
+
const containerStyle = react.useMemo(() => {
|
|
352
|
+
const base = {
|
|
353
|
+
position: "relative",
|
|
354
|
+
overflow: "hidden",
|
|
355
|
+
width: "100%",
|
|
356
|
+
height: "100%"
|
|
357
|
+
};
|
|
358
|
+
if (isReady && compensationMode !== "none") {
|
|
359
|
+
const scaledW = contentDimensions.width * scale;
|
|
360
|
+
const scaledH = contentDimensions.height * scale;
|
|
361
|
+
if (compensationMode === "width" || compensationMode === "both") {
|
|
362
|
+
base.width = scaledW;
|
|
363
|
+
}
|
|
364
|
+
if (compensationMode === "height" || compensationMode === "both") {
|
|
365
|
+
base.height = scaledH;
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
return { ...base, ...userContainerStyle };
|
|
369
|
+
}, [
|
|
370
|
+
isReady,
|
|
371
|
+
compensationMode,
|
|
372
|
+
contentDimensions.width,
|
|
373
|
+
contentDimensions.height,
|
|
374
|
+
scale,
|
|
375
|
+
userContainerStyle
|
|
376
|
+
]);
|
|
377
|
+
const contentStyle = react.useMemo(
|
|
378
|
+
() => ({
|
|
379
|
+
position: "absolute",
|
|
380
|
+
top: 0,
|
|
381
|
+
left: 0,
|
|
382
|
+
transformOrigin,
|
|
383
|
+
transform: `scale(${scale})`,
|
|
384
|
+
willChange: "transform",
|
|
385
|
+
...userContentStyle
|
|
386
|
+
}),
|
|
387
|
+
[scale, transformOrigin, userContentStyle]
|
|
388
|
+
);
|
|
389
|
+
return /* @__PURE__ */ jsxRuntime.jsx("div", { ref: containerRef, style: containerStyle, ...rest, children: /* @__PURE__ */ jsxRuntime.jsx(
|
|
390
|
+
"div",
|
|
391
|
+
{
|
|
392
|
+
ref: contentRef,
|
|
393
|
+
className: contentClassName,
|
|
394
|
+
style: contentStyle,
|
|
395
|
+
children: /* @__PURE__ */ jsxRuntime.jsx(ScaleProvider, { scale, children })
|
|
396
|
+
}
|
|
397
|
+
) });
|
|
398
|
+
}
|
|
399
|
+
);
|
|
400
|
+
|
|
401
|
+
exports.AutoScale = AutoScale;
|
|
402
|
+
exports.ScaleProvider = ScaleProvider;
|
|
403
|
+
exports.calculateScale = calculateScale;
|
|
404
|
+
exports.canUseDom = canUseDom;
|
|
405
|
+
exports.clampScale = clampScale;
|
|
406
|
+
exports.createScheduler = createScheduler;
|
|
407
|
+
exports.isBrowser = isBrowser;
|
|
408
|
+
exports.isResizeObserverSupported = isResizeObserverSupported;
|
|
409
|
+
exports.measureContainer = measureContainer;
|
|
410
|
+
exports.measureElement = measureElement;
|
|
411
|
+
exports.useAutoScale = useAutoScale;
|
|
412
|
+
exports.useScaleContext = useScaleContext;
|
|
413
|
+
//# sourceMappingURL=index.cjs.map
|
|
414
|
+
//# sourceMappingURL=index.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/core/calculateScale.ts","../src/core/clamp.ts","../src/core/guards.ts","../src/core/measure.ts","../src/core/scheduler.ts","../src/hooks/useAutoScale.ts","../src/context/ScaleProvider.tsx","../src/components/AutoScale.tsx"],"names":["ZERO","useRef","useState","useCallback","useEffect","createContext","useContext","useMemo","jsx","forwardRef","AutoScale","useImperativeHandle"],"mappings":"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","file":"index.cjs","sourcesContent":["// ---------------------------------------------------------------------------\n// @eduvidu/react-autoscale — Scale Calculation Strategies\n// ---------------------------------------------------------------------------\n\nimport type { CustomCalculator, Dimensions, ScaleMode } from '../types/index.js';\n\n/**\n * Pure function — computes a scale factor given container & content dimensions.\n *\n * Strategies:\n * - `'width'` → container.width / content.width\n * - `'height'` → container.height / content.height\n * - `'contain'` → min(widthScale, heightScale) — fit inside\n * - `'cover'` → max(widthScale, heightScale) — fill entirely\n *\n * If a `customCalculator` is provided it takes priority over `mode`.\n *\n * Returns `1` when content has zero width or height (avoids division by zero).\n */\nexport function calculateScale(\n container: Readonly<Dimensions>,\n content: Readonly<Dimensions>,\n mode: ScaleMode = 'contain',\n customCalculator?: CustomCalculator,\n): number {\n // Custom calculator takes priority\n if (customCalculator) {\n const custom = customCalculator(container, content);\n return Number.isFinite(custom) ? custom : 1;\n }\n\n // Guard — zero-sized content cannot be scaled\n if (content.width <= 0 || content.height <= 0) return 1;\n\n // Guard — zero-sized container means nothing to scale into\n if (container.width <= 0 && container.height <= 0) return 1;\n\n const scaleX = container.width > 0 ? container.width / content.width : 1;\n const scaleY = container.height > 0 ? container.height / content.height : 1;\n\n switch (mode) {\n case 'width':\n return container.width > 0 ? scaleX : 1;\n case 'height':\n return container.height > 0 ? scaleY : 1;\n case 'cover':\n return Math.max(scaleX, scaleY);\n case 'contain':\n default:\n return Math.min(scaleX, scaleY);\n }\n}\n","// ---------------------------------------------------------------------------\n// @eduvidu/react-autoscale — Scale Clamping\n// ---------------------------------------------------------------------------\n\n/**\n * Clamp a scale value to `[min, max]`.\n *\n * • Both bounds are optional — omitting a bound disables that side.\n * • Returns `1` for NaN / non-finite input (safe fallback).\n * • If `min > max` the values are silently swapped.\n */\nexport function clampScale(\n scale: number,\n min?: number,\n max?: number,\n): number {\n // Guard against garbage input\n if (!Number.isFinite(scale)) return 1;\n\n let lo = min;\n let hi = max;\n\n // Swap if caller got them backwards\n if (lo !== undefined && hi !== undefined && lo > hi) {\n [lo, hi] = [hi, lo];\n }\n\n let result = scale;\n if (lo !== undefined && Number.isFinite(lo)) {\n result = Math.max(result, lo);\n }\n if (hi !== undefined && Number.isFinite(hi)) {\n result = Math.min(result, hi);\n }\n\n return result;\n}\n","// ---------------------------------------------------------------------------\n// @eduvidu/react-autoscale — Environment Guards\n// ---------------------------------------------------------------------------\n\n/**\n * Returns `true` when running in a browser environment.\n * Safe to call on server (Next.js SSR / RSC).\n */\nexport function isBrowser(): boolean {\n return (\n typeof window !== 'undefined' &&\n typeof window.document !== 'undefined'\n );\n}\n\n/**\n * Returns `true` when `ResizeObserver` is available.\n * Older browsers and SSR will return `false`.\n */\nexport function isResizeObserverSupported(): boolean {\n return isBrowser() && typeof ResizeObserver !== 'undefined';\n}\n\n/**\n * Composite guard — returns `true` only when both DOM and\n * `ResizeObserver` are available.\n */\nexport function canUseDom(): boolean {\n return isBrowser() && typeof window.document.createElement === 'function';\n}\n","// ---------------------------------------------------------------------------\n// @eduvidu/react-autoscale — DOM Measurement Utilities\n// ---------------------------------------------------------------------------\n\nimport type {\n CustomMeasurer,\n Dimensions,\n MeasurementStrategy,\n} from '../types/index.js';\n\n/** Zero dimensions constant — reused as a safe default. */\nconst ZERO: Dimensions = Object.freeze({ width: 0, height: 0 });\n\n/**\n * Measure an element's **natural (unscaled)** dimensions.\n *\n * Strategies:\n *\n * - `'scrollSize'` (default) — uses `scrollWidth` / `scrollHeight`.\n * These properties report the *intrinsic* content size and are NOT\n * affected by CSS `transform: scale(…)`, which is exactly what we need.\n *\n * - `'boundingRect'` — uses `getBoundingClientRect()`. Note that this\n * DOES reflect transforms, so the caller should ideally read the rect\n * *before* applying the scale transform, or compensate manually.\n *\n * - `'custom'` — delegates to a user-provided `CustomMeasurer`.\n */\nexport function measureElement(\n element: HTMLElement | null,\n strategy: MeasurementStrategy = 'scrollSize',\n customMeasurer?: CustomMeasurer,\n): Dimensions {\n if (!element) return ZERO;\n\n switch (strategy) {\n case 'custom': {\n if (customMeasurer) {\n const result = customMeasurer(element);\n return {\n width: Math.max(0, result.width),\n height: Math.max(0, result.height),\n };\n }\n // Fall through to default if no custom measurer provided\n return measureScrollSize(element);\n }\n\n case 'boundingRect': {\n const rect = element.getBoundingClientRect();\n return {\n width: Math.max(0, rect.width),\n height: Math.max(0, rect.height),\n };\n }\n\n case 'scrollSize':\n default:\n return measureScrollSize(element);\n }\n}\n\n/**\n * Internal: measure via scrollWidth/scrollHeight.\n * Falls back to offsetWidth/offsetHeight when scroll values are zero\n * (can happen on elements with `overflow: visible`).\n */\nfunction measureScrollSize(element: HTMLElement): Dimensions {\n const width = element.scrollWidth || element.offsetWidth || 0;\n const height = element.scrollHeight || element.offsetHeight || 0;\n return { width: Math.max(0, width), height: Math.max(0, height) };\n}\n\n/**\n * Measure a container element's **available inner space**.\n *\n * Uses `clientWidth` / `clientHeight` which exclude scrollbars but include\n * padding — this is the usable area for content layout.\n */\nexport function measureContainer(element: HTMLElement | null): Dimensions {\n if (!element) return ZERO;\n return {\n width: Math.max(0, element.clientWidth),\n height: Math.max(0, element.clientHeight),\n };\n}\n","// ---------------------------------------------------------------------------\n// @eduvidu/react-autoscale — rAF Scheduler\n// ---------------------------------------------------------------------------\n//\n// This module provides a micro-scheduler that batches DOM measurement and\n// scale updates into a single `requestAnimationFrame` callback. It also\n// serves as the primary defense against the well-known\n// \"ResizeObserver loop completed with undelivered notifications\" error by\n// wrapping work inside rAF.\n// ---------------------------------------------------------------------------\n\nimport { isBrowser } from './guards.js';\n\nexport interface Scheduler {\n /** Enqueue `callback` to run on the next animation frame. */\n schedule(callback: () => void): void;\n\n /** Cancel any pending animation frame. */\n cancel(): void;\n\n /**\n * Full cleanup — cancel pending frames and mark the scheduler as\n * destroyed so no further work is enqueued.\n */\n destroy(): void;\n}\n\n/**\n * Schedule options for optional throttle / debounce.\n * At most ONE of `throttle` or `debounce` should be set.\n */\nexport interface SchedulerOptions {\n /** Minimum interval (ms) between executions (throttle). */\n throttleMs?: number;\n\n /** Delay (ms) after the last call before executing (debounce). */\n debounceMs?: number;\n}\n\n/**\n * Create a scheduler instance.\n *\n * Design notes:\n * - Every `schedule()` call cancels the previous pending frame — this\n * coalesces rapid-fire `ResizeObserver` callbacks into a single frame.\n * - Throttle / debounce are implemented *outside* rAF so timing control\n * remains predictable.\n * - `destroy()` must be called on component unmount to prevent leaks.\n */\nexport function createScheduler(options: SchedulerOptions = {}): Scheduler {\n let rafId: number | null = null;\n let timeoutId: ReturnType<typeof setTimeout> | null = null;\n let lastExecution = 0;\n let destroyed = false;\n\n const { throttleMs, debounceMs } = options;\n\n function clearPendingRaf(): void {\n if (rafId !== null) {\n cancelAnimationFrame(rafId);\n rafId = null;\n }\n }\n\n function clearPendingTimeout(): void {\n if (timeoutId !== null) {\n clearTimeout(timeoutId);\n timeoutId = null;\n }\n }\n\n function enqueueRaf(callback: () => void): void {\n clearPendingRaf();\n if (!isBrowser()) {\n // SSR — synchronously invoke (should be rare, guards should prevent)\n callback();\n return;\n }\n rafId = requestAnimationFrame(() => {\n rafId = null;\n if (!destroyed) callback();\n });\n }\n\n function schedule(callback: () => void): void {\n if (destroyed) return;\n\n // --- Debounce ---\n if (debounceMs !== undefined && debounceMs > 0) {\n clearPendingTimeout();\n clearPendingRaf();\n timeoutId = setTimeout(() => {\n timeoutId = null;\n enqueueRaf(callback);\n }, debounceMs);\n return;\n }\n\n // --- Throttle ---\n if (throttleMs !== undefined && throttleMs > 0) {\n const now = Date.now();\n const elapsed = now - lastExecution;\n if (elapsed >= throttleMs) {\n lastExecution = now;\n enqueueRaf(callback);\n } else {\n // Schedule remaining time\n clearPendingTimeout();\n clearPendingRaf();\n timeoutId = setTimeout(() => {\n timeoutId = null;\n lastExecution = Date.now();\n enqueueRaf(callback);\n }, throttleMs - elapsed);\n }\n return;\n }\n\n // --- No timing control — plain rAF coalescing ---\n enqueueRaf(callback);\n }\n\n function cancel(): void {\n clearPendingRaf();\n clearPendingTimeout();\n }\n\n function destroy(): void {\n if (destroyed) return;\n cancel();\n destroyed = true;\n }\n\n return { schedule, cancel, destroy };\n}\n","// ---------------------------------------------------------------------------\n// @eduvidu/react-autoscale — useAutoScale Hook\n// ---------------------------------------------------------------------------\n\nimport { useCallback, useEffect, useRef, useState } from 'react';\n\nimport { calculateScale } from '../core/calculateScale.js';\nimport { clampScale } from '../core/clamp.js';\nimport { isBrowser, isResizeObserverSupported } from '../core/guards.js';\nimport { measureContainer, measureElement } from '../core/measure.js';\nimport { createScheduler } from '../core/scheduler.js';\nimport type {\n AutoScaleOptions,\n AutoScaleState,\n Dimensions,\n} from '../types/index.js';\n\n/** Epsilon for floating-point scale comparison. */\nconst SCALE_EPSILON = 1e-4;\n\n/** Zero dimensions constant. */\nconst ZERO: Dimensions = { width: 0, height: 0 };\n\n/**\n * Core hook for auto-scaling content to fit its container.\n *\n * Returns refs for the container and content elements, plus the computed\n * scale factor and dimension information.\n *\n * All options are optional — calling `useAutoScale()` with no arguments\n * produces a fully functional scaling setup using `'contain'` mode.\n */\nexport function useAutoScale(options: AutoScaleOptions = {}): AutoScaleState {\n const {\n mode = 'contain',\n minScale,\n maxScale,\n customCalculator,\n observeParent = false,\n throttle: throttleMs,\n debounce: debounceMs,\n disabled = false,\n debug = false,\n measurementStrategy = 'scrollSize',\n customMeasurer,\n onScaleChange,\n } = options;\n\n const containerRef = useRef<HTMLDivElement | null>(null);\n const contentRef = useRef<HTMLDivElement | null>(null);\n\n // State\n const [scale, setScale] = useState(1);\n const [containerDimensions, setContainerDimensions] =\n useState<Dimensions>(ZERO);\n const [contentDimensions, setContentDimensions] =\n useState<Dimensions>(ZERO);\n const [isReady, setIsReady] = useState(false);\n\n // Stable ref for the latest options (avoids re-creating observer on every render)\n const optionsRef = useRef(options);\n optionsRef.current = options;\n\n // Stable ref for onScaleChange to avoid effect churn\n const onScaleChangeRef = useRef(onScaleChange);\n onScaleChangeRef.current = onScaleChange;\n\n // Track previous scale for change detection\n const prevScaleRef = useRef(1);\n\n // Perform a measurement + scale calculation cycle\n const measure = useCallback(() => {\n const container = containerRef.current;\n const content = contentRef.current;\n if (!container || !content) return;\n\n const cDims = measureContainer(container);\n const nDims = measureElement(content, measurementStrategy, customMeasurer);\n\n if (debug) {\n // eslint-disable-next-line no-console\n console.log('[react-autoscale]', {\n container: cDims,\n content: nDims,\n mode,\n });\n }\n\n const raw = calculateScale(cDims, nDims, mode, customCalculator);\n const clamped = clampScale(raw, minScale, maxScale);\n\n // Only update state if dimensions or scale actually changed\n setContainerDimensions((prev) =>\n prev.width === cDims.width && prev.height === cDims.height\n ? prev\n : cDims,\n );\n setContentDimensions((prev) =>\n prev.width === nDims.width && prev.height === nDims.height\n ? prev\n : nDims,\n );\n\n setScale((prev) => {\n if (Math.abs(prev - clamped) < SCALE_EPSILON) return prev;\n\n // Fire onScaleChange callback\n const cb = onScaleChangeRef.current;\n if (cb) {\n cb(clamped, {\n containerDimensions: cDims,\n contentDimensions: nDims,\n previousScale: prevScaleRef.current,\n });\n }\n prevScaleRef.current = clamped;\n return clamped;\n });\n\n setIsReady(true);\n }, [\n mode,\n minScale,\n maxScale,\n customCalculator,\n measurementStrategy,\n customMeasurer,\n debug,\n ]);\n\n // Set up ResizeObserver + scheduler\n useEffect(() => {\n if (disabled || !isBrowser() || !isResizeObserverSupported()) return;\n\n const scheduler = createScheduler({ throttleMs, debounceMs });\n\n const observer = new ResizeObserver(() => {\n scheduler.schedule(measure);\n });\n\n const target = observeParent\n ? containerRef.current?.parentElement\n : containerRef.current;\n\n if (target) {\n observer.observe(target);\n }\n\n // Also observe the content element for intrinsic size changes\n if (contentRef.current) {\n observer.observe(contentRef.current);\n }\n\n // Initial measurement\n scheduler.schedule(measure);\n\n return () => {\n observer.disconnect();\n scheduler.destroy();\n };\n }, [measure, disabled, observeParent, throttleMs, debounceMs]);\n\n // Compute scaled dimensions\n const dimensions: Dimensions = {\n width: contentDimensions.width * scale,\n height: contentDimensions.height * scale,\n };\n\n return {\n containerRef,\n contentRef,\n scale,\n dimensions,\n containerDimensions,\n contentDimensions,\n isReady,\n };\n}\n","// ---------------------------------------------------------------------------\n// @eduvidu/react-autoscale — ScaleProvider Context\n// ---------------------------------------------------------------------------\n\nimport { createContext, useContext, useMemo } from 'react';\nimport type { ScaleContextValue } from '../types/index.js';\n\n/** Default context value — no scaling ancestor. */\nconst DEFAULT_CONTEXT: ScaleContextValue = {\n scale: 1,\n parentScale: 1,\n depth: 0,\n};\n\nconst ScaleContext = createContext<ScaleContextValue>(DEFAULT_CONTEXT);\nScaleContext.displayName = 'ScaleContext';\n\n/**\n * Provides scale context to descendants.\n *\n * When nested, each `ScaleProvider` multiplies its own `scale` with the\n * inherited `parentScale`, giving any descendant access to the full\n * accumulated scale factor.\n */\nexport function ScaleProvider({\n scale,\n children,\n}: {\n scale: number;\n children: React.ReactNode;\n}) {\n const parent = useContext(ScaleContext);\n\n const value = useMemo<ScaleContextValue>(\n () => ({\n scale,\n parentScale: parent.parentScale * parent.scale,\n depth: parent.depth + 1,\n }),\n [scale, parent.parentScale, parent.scale, parent.depth],\n );\n\n return <ScaleContext.Provider value={value}>{children}</ScaleContext.Provider>;\n}\n\n/**\n * Read the nearest `ScaleProvider` context.\n *\n * Returns `{ scale: 1, parentScale: 1, depth: 0 }` if no provider exists.\n */\nexport function useScaleContext(): ScaleContextValue {\n return useContext(ScaleContext);\n}\n","// ---------------------------------------------------------------------------\n// @eduvidu/react-autoscale — <AutoScale> Component\n// ---------------------------------------------------------------------------\n\nimport { forwardRef, useImperativeHandle, useMemo } from 'react';\n\nimport { useAutoScale } from '../hooks/useAutoScale.js';\nimport { ScaleProvider } from '../context/ScaleProvider.js';\nimport type { AutoScaleProps } from '../types/index.js';\n\n/**\n * Declarative scaling wrapper.\n *\n * Usage:\n * ```tsx\n * <AutoScale>\n * <MyDashboard />\n * </AutoScale>\n * ```\n *\n * All props are optional. The component renders two nested `<div>` elements:\n * 1. **Container** (outer) — fills available space, ref'd for size measurement\n * 2. **Content** (inner) — wraps children, `transform: scale(…)` applied here\n *\n * Any HTML `<div>` props not recognised as `AutoScaleOptions` are forwarded\n * to the outer container `<div>`.\n */\nexport const AutoScale = forwardRef<HTMLDivElement, AutoScaleProps>(\n function AutoScale(props, ref) {\n const {\n // AutoScaleOptions\n mode,\n minScale,\n maxScale,\n customCalculator,\n observeParent,\n transformOrigin = 'top left',\n throttle,\n debounce,\n disabled,\n debug,\n compensationMode = 'none',\n onScaleChange,\n measurementStrategy,\n customMeasurer,\n\n // Component-specific\n children,\n contentClassName,\n contentStyle: userContentStyle,\n\n // Forwarded to outer div\n style: userContainerStyle,\n ...rest\n } = props;\n\n const {\n containerRef,\n contentRef,\n scale,\n contentDimensions,\n isReady,\n } = useAutoScale({\n mode,\n minScale,\n maxScale,\n customCalculator,\n observeParent,\n throttle,\n debounce,\n disabled,\n debug,\n compensationMode,\n onScaleChange,\n measurementStrategy,\n customMeasurer,\n });\n\n // Forward outer ref\n useImperativeHandle(ref, () => containerRef.current!, [containerRef]);\n\n // --- Container styles ---\n const containerStyle = useMemo<React.CSSProperties>(() => {\n const base: React.CSSProperties = {\n position: 'relative',\n overflow: 'hidden',\n width: '100%',\n height: '100%',\n };\n\n // Compensation: adjust container to match scaled content\n if (isReady && compensationMode !== 'none') {\n const scaledW = contentDimensions.width * scale;\n const scaledH = contentDimensions.height * scale;\n if (compensationMode === 'width' || compensationMode === 'both') {\n base.width = scaledW;\n }\n if (compensationMode === 'height' || compensationMode === 'both') {\n base.height = scaledH;\n }\n }\n\n return { ...base, ...userContainerStyle };\n }, [\n isReady,\n compensationMode,\n contentDimensions.width,\n contentDimensions.height,\n scale,\n userContainerStyle,\n ]);\n\n // --- Content styles ---\n const contentStyle = useMemo<React.CSSProperties>(\n () => ({\n position: 'absolute',\n top: 0,\n left: 0,\n transformOrigin,\n transform: `scale(${scale})`,\n willChange: 'transform',\n ...userContentStyle,\n }),\n [scale, transformOrigin, userContentStyle],\n );\n\n return (\n <div ref={containerRef} style={containerStyle} {...rest}>\n <div\n ref={contentRef}\n className={contentClassName}\n style={contentStyle}\n >\n <ScaleProvider scale={scale}>{children}</ScaleProvider>\n </div>\n </div>\n );\n },\n);\n"]}
|