@inkra/react 0.2.0 → 0.4.3

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.
Files changed (246) hide show
  1. package/README.md +101 -7
  2. package/dist/Button.js +3 -2
  3. package/dist/PencilIcon.d.ts +10 -0
  4. package/dist/PencilIcon.js +83 -39
  5. package/dist/PencilLine.js +9 -1
  6. package/dist/PencilText.js +15 -9
  7. package/dist/accordion/Accordion.js +65 -5
  8. package/dist/alert-dialog/index.d.ts +1 -1
  9. package/dist/alert-dialog/index.js +18 -6
  10. package/dist/audio-player/index.d.ts +2 -0
  11. package/dist/audio-player/index.js +126 -31
  12. package/dist/calendar/index.d.ts +4 -1
  13. package/dist/calendar/index.js +31 -12
  14. package/dist/carousel/index.js +13 -6
  15. package/dist/choice-grid/index.d.ts +6 -1
  16. package/dist/choice-grid/index.js +61 -33
  17. package/dist/color-picker/GradientEditor.d.ts +39 -0
  18. package/dist/color-picker/GradientEditor.js +63 -0
  19. package/dist/color-picker/gradient.d.ts +25 -0
  20. package/dist/color-picker/gradient.js +60 -0
  21. package/dist/color-picker/index.d.ts +27 -7
  22. package/dist/color-picker/index.js +134 -32
  23. package/dist/color-swatch/index.d.ts +10 -0
  24. package/dist/color-swatch/index.js +9 -0
  25. package/dist/command/index.d.ts +24 -2
  26. package/dist/command/index.js +29 -6
  27. package/dist/content/index.d.ts +5 -0
  28. package/dist/content/index.js +12 -3
  29. package/dist/context-menu/index.d.ts +2 -2
  30. package/dist/context-menu/index.js +4 -3
  31. package/dist/data-table/index.d.ts +7 -1
  32. package/dist/data-table/index.js +64 -16
  33. package/dist/data-table/model.d.ts +2 -0
  34. package/dist/data-table/model.js +7 -0
  35. package/dist/date-picker/index.d.ts +2 -0
  36. package/dist/date-picker/index.js +10 -9
  37. package/dist/day-agenda/index.d.ts +6 -1
  38. package/dist/day-agenda/index.js +46 -21
  39. package/dist/day-agenda/layout.js +65 -9
  40. package/dist/display/ProgressRing.js +7 -3
  41. package/dist/display/index.d.ts +4 -1
  42. package/dist/display/index.js +3 -2
  43. package/dist/dockable-panel/index.js +5 -5
  44. package/dist/drawer/index.d.ts +1 -1
  45. package/dist/drawer/index.js +9 -6
  46. package/dist/dropdown-menu/index.d.ts +1 -1
  47. package/dist/dropdown-menu/index.js +3 -2
  48. package/dist/editable/index.d.ts +24 -2
  49. package/dist/editable/index.js +142 -37
  50. package/dist/empty/illustrations.d.ts +8 -0
  51. package/dist/empty/illustrations.js +78 -0
  52. package/dist/empty/index.d.ts +6 -1
  53. package/dist/empty/index.js +11 -3
  54. package/dist/flip/index.js +28 -8
  55. package/dist/form/EditablePencilText.js +34 -0
  56. package/dist/form/Field.js +11 -10
  57. package/dist/form/Input.js +1 -1
  58. package/dist/form/Textarea.d.ts +3 -1
  59. package/dist/form/Textarea.js +6 -4
  60. package/dist/hotkey/index.d.ts +1 -0
  61. package/dist/hotkey/index.js +6 -4
  62. package/dist/hover-card/index.js +1 -1
  63. package/dist/image-cropper/index.js +4 -2
  64. package/dist/image-viewer/VideoResource.d.ts +21 -0
  65. package/dist/image-viewer/VideoResource.js +194 -0
  66. package/dist/image-viewer/index.d.ts +14 -0
  67. package/dist/image-viewer/index.js +118 -65
  68. package/dist/image-viewer/lucideGeometry.d.ts +3 -0
  69. package/dist/image-viewer/lucideGeometry.js +19 -0
  70. package/dist/index.d.ts +2 -0
  71. package/dist/index.js +2 -0
  72. package/dist/input-number/index.d.ts +6 -0
  73. package/dist/input-number/index.js +131 -15
  74. package/dist/input-number/model.d.ts +13 -0
  75. package/dist/input-number/model.js +50 -0
  76. package/dist/internal/cache.d.ts +4 -0
  77. package/dist/internal/cache.js +18 -1
  78. package/dist/internal/iconGeometry.d.ts +2 -0
  79. package/dist/internal/iconGeometry.js +32 -0
  80. package/dist/internal/renderTokens.d.ts +4 -0
  81. package/dist/internal/renderTokens.js +4 -0
  82. package/dist/internal/stroke.d.ts +7 -0
  83. package/dist/internal/stroke.js +63 -7
  84. package/dist/internal/surfaceBudget.d.ts +20 -0
  85. package/dist/internal/surfaceBudget.js +69 -0
  86. package/dist/internal/surfaceRenderer.js +87 -28
  87. package/dist/internal/surfaceVisibility.d.ts +3 -0
  88. package/dist/internal/surfaceVisibility.js +60 -0
  89. package/dist/internal/textRenderer.d.ts +2 -0
  90. package/dist/internal/textRenderer.js +3 -3
  91. package/dist/internal/usePencilSurface.js +17 -1
  92. package/dist/internal/wash.js +4 -3
  93. package/dist/journey-map/terrain.js +10 -5
  94. package/dist/layout/index.js +1 -1
  95. package/dist/log/index.js +29 -22
  96. package/dist/markdown/CodeBlock.d.ts +19 -0
  97. package/dist/markdown/CodeBlock.js +86 -0
  98. package/dist/markdown/Deletion.d.ts +2 -0
  99. package/dist/markdown/Deletion.js +69 -0
  100. package/dist/markdown/icons.d.ts +14 -0
  101. package/dist/markdown/icons.js +16 -0
  102. package/dist/markdown/index.d.ts +17 -0
  103. package/dist/markdown/index.js +175 -0
  104. package/dist/node-graph/index.d.ts +2 -0
  105. package/dist/node-graph/index.js +49 -25
  106. package/dist/number-ticker/index.d.ts +11 -9
  107. package/dist/number-ticker/index.js +14 -11
  108. package/dist/number-ticker/model.d.ts +38 -5
  109. package/dist/number-ticker/model.js +139 -10
  110. package/dist/number-ticker/precision.d.ts +7 -0
  111. package/dist/number-ticker/precision.js +141 -0
  112. package/dist/overflow-list/index.js +5 -2
  113. package/dist/overlay/Overlay.d.ts +1 -1
  114. package/dist/overlay/Overlay.js +5 -3
  115. package/dist/overlay/Popover.d.ts +1 -1
  116. package/dist/overlay/Popover.js +6 -5
  117. package/dist/overlay/themeBridge.d.ts +12 -1
  118. package/dist/overlay/themeBridge.js +41 -3
  119. package/dist/pagination/index.d.ts +10 -1
  120. package/dist/pagination/index.js +6 -4
  121. package/dist/pan-zoom/index.d.ts +2 -0
  122. package/dist/pan-zoom/index.js +7 -6
  123. package/dist/pencil.d.ts +1 -0
  124. package/dist/pencil.js +4 -4
  125. package/dist/radar/index.d.ts +4 -0
  126. package/dist/radar/index.js +41 -21
  127. package/dist/rating/index.d.ts +4 -0
  128. package/dist/rating/index.js +128 -24
  129. package/dist/resizable/index.js +1 -1
  130. package/dist/scroll-area/index.d.ts +1 -1
  131. package/dist/scroll-area/index.js +5 -2
  132. package/dist/select/MultipleSelect.js +5 -4
  133. package/dist/select/SearchableSelect.js +61 -6
  134. package/dist/select/Select.d.ts +1 -0
  135. package/dist/select/Select.js +3 -1
  136. package/dist/select/StepperSelect.d.ts +29 -0
  137. package/dist/select/StepperSelect.js +119 -0
  138. package/dist/select/index.d.ts +2 -0
  139. package/dist/select/index.js +1 -0
  140. package/dist/select/stepperModel.d.ts +7 -0
  141. package/dist/select/stepperModel.js +29 -0
  142. package/dist/selection/Checkbox.js +4 -3
  143. package/dist/selection/RadioGroup.js +29 -13
  144. package/dist/selection/Switch.js +4 -3
  145. package/dist/selection/useNativeChecked.d.ts +2 -1
  146. package/dist/selection/useNativeChecked.js +26 -8
  147. package/dist/slider/index.js +50 -6
  148. package/dist/sortable/index.js +15 -6
  149. package/dist/sparkline/index.js +17 -2
  150. package/dist/sparkline/model.js +11 -4
  151. package/dist/styles/accordion.css +228 -0
  152. package/dist/styles/alert-dialog.css +139 -0
  153. package/dist/styles/audio-player.css +22 -0
  154. package/dist/styles/base.css +638 -0
  155. package/dist/styles/breadcrumb.css +7 -0
  156. package/dist/styles/calendar.css +62 -0
  157. package/dist/styles/carousel.css +173 -0
  158. package/dist/styles/choice-grid.css +226 -0
  159. package/dist/styles/color-picker.css +281 -0
  160. package/dist/styles/color-swatch.css +47 -0
  161. package/dist/styles/command.css +52 -0
  162. package/dist/styles/content.css +79 -0
  163. package/dist/styles/context-menu.css +35 -0
  164. package/dist/styles/data-table.css +31 -0
  165. package/dist/styles/date-picker.css +107 -0
  166. package/dist/styles/day-agenda.css +30 -0
  167. package/dist/styles/display.css +304 -0
  168. package/dist/styles/dockable-panel.css +110 -0
  169. package/dist/styles/drawer.css +133 -0
  170. package/dist/styles/dropdown-menu.css +68 -0
  171. package/dist/styles/editable.css +145 -0
  172. package/dist/styles/empty.css +108 -0
  173. package/dist/styles/flip.css +101 -0
  174. package/dist/styles/form.css +634 -0
  175. package/dist/styles/heatmap.css +256 -0
  176. package/dist/styles/hotkey.css +189 -0
  177. package/dist/styles/hover-card.css +27 -0
  178. package/dist/styles/image-cropper.css +205 -0
  179. package/dist/styles/image-viewer.css +422 -0
  180. package/dist/styles/input-number.css +21 -0
  181. package/dist/styles/journey-map.css +19 -0
  182. package/dist/styles/keypad.css +92 -0
  183. package/dist/styles/layout.css +72 -0
  184. package/dist/styles/leaflet.css +661 -0
  185. package/dist/styles/log.css +203 -0
  186. package/dist/styles/markdown.css +379 -0
  187. package/dist/styles/node-graph.css +142 -0
  188. package/dist/styles/number-ticker.css +83 -0
  189. package/dist/styles/overflow-list.css +51 -0
  190. package/dist/styles/overlay.css +284 -0
  191. package/dist/styles/pagination.css +21 -0
  192. package/dist/styles/pan-zoom.css +136 -0
  193. package/dist/styles/radar.css +1 -0
  194. package/dist/styles/rating.css +121 -0
  195. package/dist/styles/resizable.css +108 -0
  196. package/dist/styles/scroll-area.css +122 -0
  197. package/dist/styles/select.css +557 -0
  198. package/dist/styles/selection.css +264 -0
  199. package/dist/styles/slider.css +145 -0
  200. package/dist/styles/slot.css +109 -0
  201. package/dist/styles/sortable.css +159 -0
  202. package/dist/styles/sparkline.css +24 -0
  203. package/dist/styles/steps.css +27 -0
  204. package/dist/styles/tabs.css +79 -0
  205. package/dist/styles/tag-input.css +213 -0
  206. package/dist/styles/time-picker.css +66 -0
  207. package/dist/styles/timeline.css +24 -0
  208. package/dist/styles/toast.css +160 -0
  209. package/dist/styles/toggle-group.css +107 -0
  210. package/dist/styles/tooltip.css +78 -0
  211. package/dist/styles/transfer.css +148 -0
  212. package/dist/styles/tree-select.css +131 -0
  213. package/dist/styles/tree.css +59 -0
  214. package/dist/styles/typewriter.css +62 -0
  215. package/dist/styles/upload.css +60 -0
  216. package/dist/styles/virtual-list.css +112 -0
  217. package/dist/styles.css +2241 -443
  218. package/dist/tabs/index.d.ts +9 -0
  219. package/dist/tabs/index.js +66 -16
  220. package/dist/tabs/useOverflow.d.ts +20 -0
  221. package/dist/tabs/useOverflow.js +145 -0
  222. package/dist/tag-input/index.d.ts +7 -2
  223. package/dist/tag-input/index.js +70 -26
  224. package/dist/time-picker/index.js +38 -20
  225. package/dist/toast/index.d.ts +19 -11
  226. package/dist/toast/index.js +48 -8
  227. package/dist/toast/manager.d.ts +42 -0
  228. package/dist/toast/manager.js +85 -0
  229. package/dist/toast/useToastNavigation.d.ts +3 -0
  230. package/dist/toast/useToastNavigation.js +75 -0
  231. package/dist/tokens.css +4 -0
  232. package/dist/tooltip/Tooltip.d.ts +1 -1
  233. package/dist/tooltip/Tooltip.js +6 -5
  234. package/dist/transfer/index.d.ts +5 -0
  235. package/dist/transfer/index.js +93 -29
  236. package/dist/transfer/model.d.ts +4 -0
  237. package/dist/transfer/model.js +37 -11
  238. package/dist/tree/index.d.ts +5 -1
  239. package/dist/tree/index.js +5 -5
  240. package/dist/tree-select/index.js +67 -30
  241. package/dist/tree-select/model.d.ts +11 -5
  242. package/dist/tree-select/model.js +111 -83
  243. package/dist/typewriter/index.js +10 -10
  244. package/dist/upload/index.js +30 -16
  245. package/dist/virtual-list/index.js +10 -7
  246. package/package.json +416 -5
@@ -1,18 +1,19 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { forwardRef, useEffect, useImperativeHandle, useRef, useState } from 'react';
2
+ import { forwardRef, useCallback, useEffect, useImperativeHandle, useRef, useState } from 'react';
3
3
  import { useBrowserLayoutEffect } from '../internal/useBrowserLayoutEffect.js';
4
4
  import { Button } from '../Button.js';
5
5
  import { PencilIcon } from '../PencilIcon.js';
6
6
  import { Input } from '../form/Input.js';
7
7
  import { useResolvedControlProps } from '../form/controlProps.js';
8
8
  import { Tooltip } from '../tooltip/Tooltip.js';
9
+ import { normalizeInputNumberRepeat, resolveInputNumberStep, stepInputNumber, } from './model.js';
9
10
  // Lucide Plus / Minus ISC geometry uses the shared pencil renderer.
10
11
  const plusPaths = [{ d: 'M5 12h14' }, { d: 'M12 5v14' }];
11
12
  const minusPaths = [{ d: 'M5 12h14' }];
12
13
  const finiteValue = (value) => typeof value === 'number' && Number.isFinite(value) ? value : null;
13
14
  const format = (value) => String(finiteValue(value) ?? '');
14
15
  const numberFromDraft = (draft) => draft === '' ? null : finiteValue(Number(draft));
15
- export const InputNumber = forwardRef(function InputNumber({ classNames, controls = true, decrementButtonLabel = 'Decrease value', defaultValue, disabled, incrementButtonLabel = 'Increase value', inputMode = 'decimal', max, min, onChange, onValueChange, readOnly, size = 'md', step = 1, textAlign = 'center', value, ...props }, forwardedRef) {
16
+ export const InputNumber = forwardRef(function InputNumber({ classNames, controls = true, decrementButtonLabel = 'Decrease value', defaultValue, disabled, incrementButtonLabel = 'Increase value', inputMode = 'decimal', max, min, onChange, onBlur, onKeyDown, onValueChange, pressAndHold = true, readOnly, repeatDelay = 400, repeatInterval = 80, size = 'md', step = 1, stepModifiers, textAlign = 'center', value, ...props }, forwardedRef) {
16
17
  const inputRef = useRef(null);
17
18
  const isControlled = value !== undefined;
18
19
  const [draft, setDraft] = useState(() => format(isControlled ? value : defaultValue));
@@ -21,6 +22,54 @@ export const InputNumber = forwardRef(function InputNumber({ classNames, control
21
22
  const latest = useRef({ isControlled, value, defaultValue });
22
23
  latest.current = { isControlled, value, defaultValue };
23
24
  useImperativeHandle(forwardedRef, () => inputRef.current);
25
+ const holdRef = useRef(null);
26
+ const timerRef = useRef(undefined);
27
+ const suppressClickRef = useRef(false);
28
+ const removeHoldListeners = useRef(undefined);
29
+ const stopHolding = useCallback(() => {
30
+ clearTimeout(timerRef.current);
31
+ timerRef.current = undefined;
32
+ removeHoldListeners.current?.();
33
+ removeHoldListeners.current = undefined;
34
+ const hold = holdRef.current;
35
+ holdRef.current = null;
36
+ if (hold?.button.hasPointerCapture?.(hold.pointerId)) {
37
+ try {
38
+ hold.button.releasePointerCapture(hold.pointerId);
39
+ }
40
+ catch { /* The pointer may already be inactive. */ }
41
+ }
42
+ }, []);
43
+ // Idle controls do not need global pointer, keyboard or visibility listeners.
44
+ const listenWhileHolding = () => {
45
+ const onVisibilityChange = () => { if (document.hidden)
46
+ stopHolding(); };
47
+ const onPointerEnd = (event) => {
48
+ if (event.pointerId === holdRef.current?.pointerId)
49
+ stopHolding();
50
+ };
51
+ const updateModifiers = (event) => {
52
+ if (holdRef.current)
53
+ holdRef.current.modifiers = {
54
+ shiftKey: event.shiftKey, altKey: event.altKey, ctrlKey: event.ctrlKey, metaKey: event.metaKey,
55
+ };
56
+ };
57
+ window.addEventListener('blur', stopHolding);
58
+ window.addEventListener('pointerup', onPointerEnd);
59
+ window.addEventListener('pointercancel', onPointerEnd);
60
+ window.addEventListener('keydown', updateModifiers);
61
+ window.addEventListener('keyup', updateModifiers);
62
+ document.addEventListener('visibilitychange', onVisibilityChange);
63
+ removeHoldListeners.current = () => {
64
+ window.removeEventListener('blur', stopHolding);
65
+ window.removeEventListener('pointerup', onPointerEnd);
66
+ window.removeEventListener('pointercancel', onPointerEnd);
67
+ window.removeEventListener('keydown', updateModifiers);
68
+ window.removeEventListener('keyup', updateModifiers);
69
+ document.removeEventListener('visibilitychange', onVisibilityChange);
70
+ };
71
+ };
72
+ useEffect(() => stopHolding, [stopHolding]);
24
73
  useBrowserLayoutEffect(() => {
25
74
  if (isControlled && currentValue !== finiteValue(value))
26
75
  setDraft(format(value));
@@ -32,6 +81,7 @@ export const InputNumber = forwardRef(function InputNumber({ classNames, control
32
81
  return;
33
82
  let resetTimer;
34
83
  const reset = (event) => {
84
+ stopHolding();
35
85
  clearTimeout(resetTimer);
36
86
  // Native reset runs after the event; restore the public default, not the latest draft.
37
87
  resetTimer = setTimeout(() => {
@@ -45,33 +95,99 @@ export const InputNumber = forwardRef(function InputNumber({ classNames, control
45
95
  };
46
96
  form.addEventListener('reset', reset);
47
97
  return () => { clearTimeout(resetTimer); form.removeEventListener('reset', reset); };
48
- }, [props.form]);
98
+ }, [props.form, stopHolding]);
49
99
  const cannotStep = state.disabled || state.readOnly || step === 'any' || (min !== undefined && max !== undefined && min > max);
50
100
  const decreaseDisabled = cannotStep || (currentValue !== null && min !== undefined && currentValue <= min);
51
101
  const increaseDisabled = cannotStep || (currentValue !== null && max !== undefined && currentValue >= max);
52
- function changeStep(direction) {
102
+ useBrowserLayoutEffect(() => {
103
+ const direction = holdRef.current?.direction;
104
+ if (cannotStep || !controls || !pressAndHold
105
+ || (direction === 'increment' && increaseDisabled)
106
+ || (direction === 'decrement' && decreaseDisabled))
107
+ stopHolding();
108
+ }, [cannotStep, controls, decreaseDisabled, increaseDisabled, pressAndHold, stopHolding]);
109
+ function changeStep(direction, modifiers) {
53
110
  const input = inputRef.current;
54
111
  if (!input || input.matches(':disabled') || input.readOnly || cannotStep)
55
- return;
56
- const before = input.value;
57
- // Native stepping applies the step base and decimal rounding without a custom parser.
58
- if (direction === 'increment')
59
- input.stepUp();
60
- else
61
- input.stepDown();
112
+ return false;
113
+ const changed = stepInputNumber(input, direction, resolveInputNumberStep(modifiers, stepModifiers));
62
114
  input.focus({ preventScroll: true });
63
- if (input.value === before)
64
- return;
115
+ if (!changed)
116
+ return false;
65
117
  setDraft(input.value);
66
118
  onValueChange?.(Number.isFinite(input.valueAsNumber) ? input.valueAsNumber : null, { reason: direction });
119
+ return true;
120
+ }
121
+ const stepRef = useRef(changeStep);
122
+ stepRef.current = changeStep;
123
+ const intervalRef = useRef(repeatInterval);
124
+ intervalRef.current = repeatInterval;
125
+ function startHolding(event, direction) {
126
+ if (event.button !== 0 || !event.isPrimary)
127
+ return;
128
+ event.preventDefault();
129
+ stopHolding();
130
+ suppressClickRef.current = false;
131
+ if (!pressAndHold)
132
+ return;
133
+ suppressClickRef.current = true;
134
+ const modifiers = {
135
+ shiftKey: event.shiftKey, altKey: event.altKey, ctrlKey: event.ctrlKey, metaKey: event.metaKey,
136
+ };
137
+ if (!changeStep(direction, modifiers))
138
+ return;
139
+ const button = event.currentTarget;
140
+ if (!button.isConnected || button.disabled)
141
+ return;
142
+ holdRef.current = { button, pointerId: event.pointerId, direction, modifiers };
143
+ listenWhileHolding();
144
+ try {
145
+ button.setPointerCapture(event.pointerId);
146
+ }
147
+ catch { /* Window listeners retain release handling. */ }
148
+ const repeat = () => {
149
+ const hold = holdRef.current;
150
+ if (!hold || document.hidden || !stepRef.current(hold.direction, hold.modifiers)) {
151
+ stopHolding();
152
+ return;
153
+ }
154
+ if (holdRef.current)
155
+ timerRef.current = setTimeout(repeat, normalizeInputNumberRepeat(intervalRef.current, 80, 32));
156
+ };
157
+ timerRef.current = setTimeout(repeat, normalizeInputNumberRepeat(repeatDelay, 400, 150));
158
+ }
159
+ function buttonEvents(direction) {
160
+ return {
161
+ onPointerDown: (event) => startHolding(event, direction),
162
+ onPointerUp: stopHolding,
163
+ onPointerCancel: stopHolding,
164
+ onLostPointerCapture: stopHolding,
165
+ onBlur: stopHolding,
166
+ onClick: (event) => {
167
+ if (event.detail > 0 && suppressClickRef.current) {
168
+ suppressClickRef.current = false;
169
+ return;
170
+ }
171
+ suppressClickRef.current = false;
172
+ changeStep(direction, event);
173
+ },
174
+ };
67
175
  }
68
176
  const iconSize = size === 'sm' ? 18 : size === 'lg' ? 26 : 22;
69
177
  const buttonSize = size === 'sm' ? 'icon-sm' : size === 'lg' ? 'icon-lg' : 'icon';
70
- return _jsx(Input, { ...props, ref: inputRef, type: "number", inputMode: inputMode, textAlign: textAlign, clearable: false, size: size, min: min, max: max, step: step, disabled: state.disabled, readOnly: state.readOnly, value: draft, classNames: { ...classNames, root: ['inkra-input-number', classNames?.root].filter(Boolean).join(' ') }, onChange: event => {
178
+ return _jsx(Input, { ...props, ref: inputRef, type: "number", inputMode: inputMode, textAlign: textAlign, clearable: false, size: size, min: min, max: max, step: step, disabled: state.disabled, readOnly: state.readOnly, value: draft, classNames: { ...classNames, root: ['inkra-input-number', classNames?.root].filter(Boolean).join(' ') }, onBlur: event => { stopHolding(); onBlur?.(event); }, onKeyDown: event => {
179
+ onKeyDown?.(event);
180
+ if (event.defaultPrevented || event.nativeEvent.isComposing || (event.key !== 'ArrowUp' && event.key !== 'ArrowDown'))
181
+ return;
182
+ if (resolveInputNumberStep(event, stepModifiers) === undefined)
183
+ return;
184
+ event.preventDefault();
185
+ changeStep(event.key === 'ArrowUp' ? 'increment' : 'decrement', event);
186
+ }, onChange: event => {
71
187
  const input = event.currentTarget;
72
188
  const nextValue = Number.isFinite(input.valueAsNumber) ? input.valueAsNumber : null;
73
189
  setDraft(input.value);
74
190
  onChange?.(event);
75
191
  onValueChange?.(nextValue, { reason: 'input' });
76
- }, startAction: controls ? _jsx(Tooltip.Provider, { children: _jsx("span", { className: "inkra-input-number__controls", children: _jsxs(Tooltip.Root, { children: [_jsx(Tooltip.Trigger, { asChild: true, children: _jsx(Button, { type: "button", size: buttonSize, variant: "ghost", disabled: decreaseDisabled, "aria-label": decrementButtonLabel, onPointerDown: event => event.preventDefault(), onClick: () => changeStep('decrement'), children: _jsx(PencilIcon, { paths: minusPaths, size: iconSize, sourceSize: 24 }) }) }), _jsx(Tooltip.Portal, { children: _jsx(Tooltip.Content, { children: decrementButtonLabel }) })] }) }) }) : undefined, endAction: controls ? _jsx(Tooltip.Provider, { children: _jsx("span", { className: "inkra-input-number__controls", children: _jsxs(Tooltip.Root, { children: [_jsx(Tooltip.Trigger, { asChild: true, children: _jsx(Button, { type: "button", size: buttonSize, variant: "ghost", disabled: increaseDisabled, "aria-label": incrementButtonLabel, onPointerDown: event => event.preventDefault(), onClick: () => changeStep('increment'), children: _jsx(PencilIcon, { paths: plusPaths, size: iconSize, sourceSize: 24 }) }) }), _jsx(Tooltip.Portal, { children: _jsx(Tooltip.Content, { children: incrementButtonLabel }) })] }) }) }) : undefined });
192
+ }, startAction: controls ? _jsx(Tooltip.Provider, { children: _jsx("span", { className: "inkra-input-number__controls", children: _jsxs(Tooltip.Root, { children: [_jsx(Tooltip.Trigger, { asChild: true, children: _jsx(Button, { type: "button", size: buttonSize, variant: "ghost", disabled: decreaseDisabled, "aria-label": decrementButtonLabel, ...buttonEvents('decrement'), children: _jsx(PencilIcon, { paths: minusPaths, size: iconSize, sourceSize: 24 }) }) }), _jsx(Tooltip.Portal, { children: _jsx(Tooltip.Content, { children: decrementButtonLabel }) })] }) }) }) : undefined, endAction: controls ? _jsx(Tooltip.Provider, { children: _jsx("span", { className: "inkra-input-number__controls", children: _jsxs(Tooltip.Root, { children: [_jsx(Tooltip.Trigger, { asChild: true, children: _jsx(Button, { type: "button", size: buttonSize, variant: "ghost", disabled: increaseDisabled, "aria-label": incrementButtonLabel, ...buttonEvents('increment'), children: _jsx(PencilIcon, { paths: plusPaths, size: iconSize, sourceSize: 24 }) }) }), _jsx(Tooltip.Portal, { children: _jsx(Tooltip.Content, { children: incrementButtonLabel }) })] }) }) }) : undefined });
77
193
  });
@@ -0,0 +1,13 @@
1
+ export interface InputNumberStepModifier {
2
+ shiftKey?: boolean;
3
+ altKey?: boolean;
4
+ ctrlKey?: boolean;
5
+ metaKey?: boolean;
6
+ step: number;
7
+ }
8
+ export type InputNumberModifierState = Pick<KeyboardEvent, 'shiftKey' | 'altKey' | 'ctrlKey' | 'metaKey'>;
9
+ export type InputNumberStepDirection = 'increment' | 'decrement';
10
+ export declare const defaultInputNumberStepModifiers: readonly InputNumberStepModifier[];
11
+ export declare function resolveInputNumberStep(modifiers: InputNumberModifierState, rules?: readonly InputNumberStepModifier[]): number | undefined;
12
+ export declare function normalizeInputNumberRepeat(value: number | undefined, fallback: number, minimum: number): number;
13
+ export declare function stepInputNumber(input: HTMLInputElement, direction: InputNumberStepDirection, modifierStep?: number): boolean;
@@ -0,0 +1,50 @@
1
+ export const defaultInputNumberStepModifiers = [
2
+ { shiftKey: true, step: 10 },
3
+ ];
4
+ export function resolveInputNumberStep(modifiers, rules = defaultInputNumberStepModifiers) {
5
+ return rules.find(rule => Number.isFinite(rule.step) && rule.step > 0
6
+ && Boolean(rule.shiftKey) === modifiers.shiftKey
7
+ && Boolean(rule.altKey) === modifiers.altKey
8
+ && Boolean(rule.ctrlKey) === modifiers.ctrlKey
9
+ && Boolean(rule.metaKey) === modifiers.metaKey)?.step;
10
+ }
11
+ export function normalizeInputNumberRepeat(value, fallback, minimum) {
12
+ return value === undefined || !Number.isFinite(value)
13
+ ? fallback
14
+ : Math.min(60000, Math.max(minimum, value));
15
+ }
16
+ export function stepInputNumber(input, direction, modifierStep) {
17
+ const before = input.value;
18
+ const min = input.min === '' ? -Infinity : Number(input.min);
19
+ const max = input.max === '' ? Infinity : Number(input.max);
20
+ if (input.disabled || input.readOnly || input.step === 'any' || min > max)
21
+ return false;
22
+ if ((direction === 'increment' && input.valueAsNumber >= max)
23
+ || (direction === 'decrement' && input.valueAsNumber <= min))
24
+ return false;
25
+ if (modifierStep === undefined) {
26
+ if (direction === 'increment')
27
+ input.stepUp();
28
+ else
29
+ input.stepDown();
30
+ }
31
+ else {
32
+ // A detached native input anchors the step base at the current value, so
33
+ // modifier steps are absolute deltas without floating-point arithmetic.
34
+ const probe = input.cloneNode(false);
35
+ probe.removeAttribute('min');
36
+ probe.removeAttribute('max');
37
+ probe.step = String(modifierStep);
38
+ probe.defaultValue = before || '0';
39
+ probe.value = before;
40
+ if (direction === 'increment')
41
+ probe.stepUp();
42
+ else
43
+ probe.stepDown();
44
+ const next = probe.valueAsNumber;
45
+ if (!Number.isFinite(next))
46
+ return false;
47
+ input.value = String(Math.min(Number.isFinite(max) ? max : Infinity, Math.max(Number.isFinite(min) ? min : -Infinity, next)));
48
+ }
49
+ return before !== input.value;
50
+ }
@@ -9,6 +9,10 @@ export declare class BoundedCache<Value> {
9
9
  set(key: string, value: Value, cost: number): Value;
10
10
  delete(key: string): void;
11
11
  }
12
+ export declare function boundedCanvasSize(width: number, height: number, dpr: number): {
13
+ width: number;
14
+ height: number;
15
+ };
12
16
  export declare function canvasByteCost(canvas: HTMLCanvasElement): number;
13
17
  export declare function canvasIsUsable(canvas: HTMLCanvasElement): boolean;
14
18
  export declare function effectiveDpr(limit?: number): number;
@@ -15,6 +15,12 @@ export class BoundedCache {
15
15
  return entry.value;
16
16
  }
17
17
  set(key, value, cost) {
18
+ // A one-off oversized image must not flush every useful cached control.
19
+ // Invalid accounting must never disable the byte budget (NaN) either.
20
+ if (!Number.isFinite(cost) || cost < 0 || cost > this.maxCost || this.maxEntries < 1) {
21
+ this.delete(key);
22
+ return value;
23
+ }
18
24
  const previous = this.entries.get(key);
19
25
  if (previous) {
20
26
  this.totalCost -= previous.cost;
@@ -24,7 +30,7 @@ export class BoundedCache {
24
30
  this.totalCost += cost;
25
31
  while (this.entries.size > this.maxEntries || this.totalCost > this.maxCost) {
26
32
  const oldestKey = this.entries.keys().next().value;
27
- if (!oldestKey)
33
+ if (oldestKey === undefined)
28
34
  break;
29
35
  const oldest = this.entries.get(oldestKey);
30
36
  this.entries.delete(oldestKey);
@@ -40,6 +46,17 @@ export class BoundedCache {
40
46
  this.totalCost -= entry.cost;
41
47
  }
42
48
  }
49
+ // Cache limits apply after drawing. Check allocation limits before asking the
50
+ // browser for a backing store, including transient canvases that are not cached.
51
+ export function boundedCanvasSize(width, height, dpr) {
52
+ const pixelWidth = Math.ceil(width * dpr);
53
+ const pixelHeight = Math.ceil(height * dpr);
54
+ if (![width, height, dpr].every(value => Number.isFinite(value) && value > 0)
55
+ || pixelWidth > 8192 || pixelHeight > 8192 || pixelWidth * pixelHeight > 4 * 1024 * 1024) {
56
+ throw new RangeError('Pencil canvas exceeds the bounded pixel budget');
57
+ }
58
+ return { width: pixelWidth, height: pixelHeight };
59
+ }
43
60
  export function canvasByteCost(canvas) {
44
61
  return canvas.width * canvas.height * 4;
45
62
  }
@@ -0,0 +1,2 @@
1
+ import type { PencilPoint } from './stroke.js';
2
+ export declare function cachedPathParts(data: string): readonly PencilPoint[][];
@@ -0,0 +1,32 @@
1
+ import { BoundedCache, PENCIL_RENDERER_VERSION } from './cache.js';
2
+ const geometryCache = new BoundedCache(256, 180000);
3
+ export function cachedPathParts(data) {
4
+ // Limit both parsing input and samples before allocating. Cache eviction only
5
+ // bounds retained geometry, not the work needed to construct a huge path.
6
+ if (data.length > 65536)
7
+ throw new RangeError('Pencil icon path exceeds the bounded input');
8
+ const key = `${PENCIL_RENDERER_VERSION}:${data}`;
9
+ const cached = geometryCache.get(key);
10
+ if (cached)
11
+ return cached;
12
+ const path = document.createElementNS('http://www.w3.org/2000/svg', 'path');
13
+ path.setAttribute('d', data);
14
+ const length = path.getTotalLength();
15
+ const count = Math.max(1, Math.ceil(length / 0.25));
16
+ if (!Number.isFinite(length) || length < 0 || count > 16384) {
17
+ throw new RangeError('Pencil icon path exceeds the bounded sampling budget');
18
+ }
19
+ const parts = [[]];
20
+ for (let index = 0; index <= count; index += 1) {
21
+ const point = path.getPointAtLength(length * index / count);
22
+ if (!Number.isFinite(point.x) || !Number.isFinite(point.y)) {
23
+ throw new RangeError('Pencil icon path contains invalid coordinates');
24
+ }
25
+ const activePart = parts[parts.length - 1];
26
+ const previous = activePart[activePart.length - 1];
27
+ if (previous && Math.hypot(point.x - previous[0], point.y - previous[1]) > 0.5)
28
+ parts.push([]);
29
+ parts[parts.length - 1].push([point.x, point.y]);
30
+ }
31
+ return geometryCache.set(key, parts, parts.reduce((total, part) => total + part.length, 0));
32
+ }
@@ -1,5 +1,9 @@
1
1
  export declare const pencilRenderDefaults: Readonly<{
2
2
  strokeWidth: 2;
3
+ iconStrokeWidth: 1.5;
4
+ iconBreaks: 0.18;
5
+ iconBreakCount: 1;
6
+ iconPressureVariation: 0.82;
3
7
  strokeRoughness: 1.5;
4
8
  washStrength: 0.8;
5
9
  washSpacing: 2;
@@ -1,5 +1,9 @@
1
1
  export const pencilRenderDefaults = Object.freeze({
2
2
  strokeWidth: 2,
3
+ iconStrokeWidth: 1.5,
4
+ iconBreaks: 0.18,
5
+ iconBreakCount: 1,
6
+ iconPressureVariation: 0.82,
3
7
  strokeRoughness: 1.5,
4
8
  washStrength: 0.8,
5
9
  washSpacing: 2,
@@ -2,6 +2,12 @@ export type PencilPoint = readonly [x: number, y: number];
2
2
  export interface StrokeSettings {
3
3
  variation?: number;
4
4
  breaks?: number;
5
+ continuousBreaks?: boolean;
6
+ /** Maximum gaps per path; short paths reduce the actual count. */
7
+ breakCount?: number;
8
+ /** Display pixels per path unit, excluding device pixel ratio. */
9
+ breakScale?: number;
10
+ fitPressure?: boolean;
5
11
  grain?: number;
6
12
  taperLength?: number;
7
13
  scale?: number;
@@ -20,5 +26,6 @@ export declare function strokeProfile(at: number, total: number, seed: number, s
20
26
  contact: number;
21
27
  deposit: number;
22
28
  taper: number;
29
+ gapDistance: number;
23
30
  };
24
31
  export declare function pencilStroke(context: CanvasRenderingContext2D, points: readonly PencilPoint[], color: string, width: number, seed: number, amplitude: number, passes?: number, closed?: boolean, settings?: StrokeSettings): void;
@@ -14,14 +14,52 @@ export function strokeProfile(at, total, seed, settings = {}) {
14
14
  const variation = settings.variation ?? 1;
15
15
  const breaks = settings.breaks ?? 0.8;
16
16
  const grainValue = settings.grain ?? 0.7;
17
- const force = clamp(0.12 + noise(at / 19, seed + 17) * 0.97
17
+ let force = clamp(0.12 + noise(at / 19, seed + 17) * 0.97
18
18
  + (noise(at / 5.3, seed + 39) - 0.5) * 0.3);
19
+ if (settings.fitPressure && total > 0 && total < 48) {
20
+ // Short icon paths need a full pressure gesture, not a nearly flat slice
21
+ // of the long-stroke noise. Blend back to the original profile by 48 units.
22
+ const phase = (at / total + hash(0, seed + 61)) * Math.PI * 2;
23
+ const gesture = 0.25 + (1 - Math.cos(phase)) * 0.35
24
+ + (noise(at / 5.3, seed + 39) - 0.5) * 0.08;
25
+ const blend = clamp((48 - total) / 24);
26
+ force = force * (1 - blend) + gesture * blend;
27
+ }
19
28
  const pressure = clamp(0.72 * (1 - variation) + force * variation);
20
29
  let contact = 1;
21
- const cell = Math.floor(at / 37);
22
- for (let index = cell - 1; index <= cell + 1; index += 1) {
23
- if (hash(index, seed + 277) > breaks * 0.9)
30
+ let gapDistance = Infinity;
31
+ // Reserve at least 12 CSS pixels per gap, measured over the full path.
32
+ // Display size affects density, but device pixel ratio must not affect it.
33
+ const breakScale = Number.isFinite(settings.breakScale) && settings.breakScale > 0 ? settings.breakScale : 1;
34
+ const gapCount = Number.isFinite(settings.breakCount)
35
+ ? Math.max(0, Math.min(12, Math.floor(settings.breakCount), Math.floor(total * breakScale / 12)))
36
+ : undefined;
37
+ const gapSpacing = settings.continuousBreaks
38
+ ? gapCount ? total / gapCount : Math.min(37, Math.max(1, total))
39
+ : 37;
40
+ const cell = Math.floor(at / gapSpacing);
41
+ // A fixed iteration count also terminates for out-of-range numeric input.
42
+ for (let offset = -1; offset <= 1; offset += 1) {
43
+ const index = cell + offset;
44
+ if (!settings.continuousBreaks && hash(index, seed + 277) > breaks * 0.9)
45
+ continue;
46
+ if (settings.continuousBreaks) {
47
+ if (gapCount !== undefined && (index < 0 || index >= gapCount))
48
+ continue;
49
+ const amount = clamp(breaks);
50
+ if (!amount)
51
+ continue;
52
+ // Fixed locations with linearly growing gap and feather widths avoid
53
+ // switching an entire gap on when a slider crosses a random threshold.
54
+ const middle = gapSpacing * (index + (gapCount === undefined
55
+ ? 0.2 + hash(index, seed + 283) * 0.6
56
+ : 0.3 + hash(index, seed + 283) * 0.4));
57
+ const gapScale = Math.min(1, gapSpacing / 24);
58
+ const halfGap = (1.1 + hash(index, seed + 293) * 3.6) * amount * gapScale;
59
+ gapDistance = Math.min(gapDistance, Math.abs(at - middle) - halfGap - 2.1 * amount * gapScale);
60
+ contact = Math.min(contact, clamp((Math.abs(at - middle) - halfGap) / (2.1 * amount * gapScale)));
24
61
  continue;
62
+ }
25
63
  const middle = index * 37 + 5 + hash(index, seed + 283) * 27;
26
64
  const halfGap = (1.1 + hash(index, seed + 293) * 3.6) * breaks;
27
65
  contact = Math.min(contact, clamp((Math.abs(at - middle) - halfGap) / 2.1));
@@ -29,13 +67,22 @@ export function strokeProfile(at, total, seed, settings = {}) {
29
67
  const tooth = noise(at * 1.6, seed + 401);
30
68
  const deposit = clamp((tooth - grainValue * 0.22) / Math.max(0.2, 1 - grainValue * 0.22));
31
69
  const taper = 0.2 + 0.8 * clamp(Math.min(at, total - at) / (settings.taperLength ?? 3));
32
- return { pressure, contact, deposit, taper };
70
+ return { pressure, contact, deposit, taper, gapDistance };
33
71
  }
34
72
  export function pencilStroke(context, points, color, width, seed, amplitude, passes = 2, closed = false, settings = {}) {
35
- if (points.length < 2)
73
+ const pointCount = points.length;
74
+ if (pointCount < 2 || pointCount > 65536)
36
75
  return;
76
+ // Validate the coordinates we retain, without scanning then cloning them again.
77
+ const line = [];
78
+ for (let index = 0; index < pointCount; index += 1) {
79
+ const point = points[index];
80
+ const x = point[0], y = point[1];
81
+ if (!Number.isFinite(x) || !Number.isFinite(y))
82
+ return;
83
+ line.push([x, y]);
84
+ }
37
85
  const scale = Math.max(0.4, settings.scale ?? 1);
38
- const line = points.map(point => [...point]);
39
86
  const lastPoint = line[line.length - 1];
40
87
  if (closed && Math.hypot(line[0][0] - lastPoint[0], line[0][1] - lastPoint[1]) > 0.01) {
41
88
  line.push(line[0]);
@@ -56,6 +103,13 @@ export function pencilStroke(context, points, color, width, seed, amplitude, pas
56
103
  const distanceOffset = settings.distanceOffset ?? 0;
57
104
  const totalDistance = settings.totalDistance ?? distanceOffset + total;
58
105
  const step = settings.step ?? (settings.detail === 'fill' ? 2.1 : 1.05);
106
+ // Stop invalid steps and huge geometry before entering the sampling loop.
107
+ if (!Number.isFinite(total) || !Number.isFinite(step) || step <= 0 || total / step > 65536
108
+ || !Number.isFinite(distanceOffset) || Math.abs(distanceOffset) > Number.MAX_SAFE_INTEGER
109
+ || !Number.isFinite(totalDistance) || Math.abs(totalDistance) > Number.MAX_SAFE_INTEGER
110
+ || !Number.isFinite(passes) || passes < 0 || passes > 16
111
+ || !Number.isFinite(settings.filaments ?? 3) || (settings.filaments ?? 3) > 16)
112
+ return;
59
113
  const samples = [];
60
114
  let segmentIndex = 0;
61
115
  for (let at = 0;; at = Math.min(total, at + step)) {
@@ -103,6 +157,8 @@ export function pencilStroke(context, points, color, width, seed, amplitude, pas
103
157
  if (opacity < 0.012)
104
158
  continue;
105
159
  const nib = width * (0.3 + pressure * 1.65) * Math.pow(scale, 0.32);
160
+ // Round segment caps must not paint back across intentional short gaps.
161
+ context.lineCap = Math.min(a.gapDistance, b.gapDistance) < nib / 2 + step ? 'butt' : 'round';
106
162
  const drift = pass ? (noise(a.at / 6, seed + pass * 107) - 0.5) * width * 1.1 : 0;
107
163
  context.globalAlpha = Math.min(1, inheritedAlpha * opacity * (pass ? 0.56 : 1.2)
108
164
  * (settings.gain ?? 1));
@@ -0,0 +1,20 @@
1
+ export declare const MAX_SURFACE_PHYSICAL_EDGE = 8192;
2
+ export declare const MAX_SURFACE_LAYER_PIXELS: number;
3
+ export declare const MAX_SURFACE_TOTAL_PIXELS: number;
4
+ export type SurfaceAllocationFailure = 'invalid-size' | 'edge-limit' | 'layer-budget' | 'surface-budget';
5
+ export type SurfaceAllocation = {
6
+ ok: true;
7
+ dpr: number;
8
+ width: number;
9
+ height: number;
10
+ layerPixels: number;
11
+ totalPixels: number;
12
+ } | {
13
+ ok: false;
14
+ reason: SurfaceAllocationFailure;
15
+ };
16
+ export declare function resolveSurfaceAllocation(width: number, height: number, dpr: number, bleed: number, layers: 1 | 2): SurfaceAllocation;
17
+ type CanvasBitmap = Pick<HTMLCanvasElement, 'width' | 'height'>;
18
+ export declare function resizeSurfaceBitmap(canvas: CanvasBitmap, width: number, height: number): void;
19
+ export declare function releaseSurfaceBitmaps(...canvases: Array<CanvasBitmap | null | undefined>): void;
20
+ export {};
@@ -0,0 +1,69 @@
1
+ // 1024x640 at DPR 2 is 2.62M pixels per layer and 5.24M with a clipped border layer.
2
+ export const MAX_SURFACE_PHYSICAL_EDGE = 8192;
3
+ export const MAX_SURFACE_LAYER_PIXELS = 4 * 1024 * 1024;
4
+ export const MAX_SURFACE_TOTAL_PIXELS = 6 * 1024 * 1024;
5
+ const DPR_PRECISION = 1024;
6
+ function allocationAtDpr(logicalWidth, logicalHeight, dpr, layers) {
7
+ const physicalWidth = Math.ceil(logicalWidth * dpr);
8
+ const physicalHeight = Math.ceil(logicalHeight * dpr);
9
+ if (!Number.isSafeInteger(physicalWidth) || !Number.isSafeInteger(physicalHeight)
10
+ || physicalWidth < 1 || physicalHeight < 1) {
11
+ return { ok: false, reason: 'invalid-size' };
12
+ }
13
+ if (physicalWidth > MAX_SURFACE_PHYSICAL_EDGE || physicalHeight > MAX_SURFACE_PHYSICAL_EDGE) {
14
+ return { ok: false, reason: 'edge-limit' };
15
+ }
16
+ const layerPixels = physicalWidth * physicalHeight;
17
+ if (!Number.isSafeInteger(layerPixels) || layerPixels > MAX_SURFACE_LAYER_PIXELS) {
18
+ return { ok: false, reason: 'layer-budget' };
19
+ }
20
+ const totalPixels = layerPixels * layers;
21
+ if (!Number.isSafeInteger(totalPixels) || totalPixels > MAX_SURFACE_TOTAL_PIXELS) {
22
+ return { ok: false, reason: 'surface-budget' };
23
+ }
24
+ return { ok: true, dpr, width: physicalWidth, height: physicalHeight, layerPixels, totalPixels };
25
+ }
26
+ export function resolveSurfaceAllocation(width, height, dpr, bleed, layers) {
27
+ if (![width, height, dpr, bleed].every(Number.isFinite)
28
+ || width <= 0 || height <= 0 || dpr < 1 || bleed < 0) {
29
+ return { ok: false, reason: 'invalid-size' };
30
+ }
31
+ const logicalWidth = width + bleed * 2;
32
+ const logicalHeight = height + bleed * 2;
33
+ const requested = allocationAtDpr(logicalWidth, logicalHeight, dpr, layers);
34
+ if (requested.ok)
35
+ return requested;
36
+ const minimum = allocationAtDpr(logicalWidth, logicalHeight, 1, layers);
37
+ if (!minimum.ok)
38
+ return minimum;
39
+ let lower = 1;
40
+ let upper = dpr;
41
+ for (let iteration = 0; iteration < 40; iteration += 1) {
42
+ const candidate = (lower + upper) / 2;
43
+ if (allocationAtDpr(logicalWidth, logicalHeight, candidate, layers).ok)
44
+ lower = candidate;
45
+ else
46
+ upper = candidate;
47
+ }
48
+ const resolvedDpr = Math.max(1, Math.floor(lower * DPR_PRECISION) / DPR_PRECISION);
49
+ const resolved = allocationAtDpr(logicalWidth, logicalHeight, resolvedDpr, layers);
50
+ return resolved.ok ? resolved : minimum;
51
+ }
52
+ export function resizeSurfaceBitmap(canvas, width, height) {
53
+ if (canvas.width === width && canvas.height === height) {
54
+ canvas.width = width;
55
+ return;
56
+ }
57
+ canvas.width = 0;
58
+ canvas.height = 0;
59
+ canvas.width = width;
60
+ canvas.height = height;
61
+ }
62
+ export function releaseSurfaceBitmaps(...canvases) {
63
+ for (const canvas of canvases) {
64
+ if (!canvas)
65
+ continue;
66
+ canvas.width = 0;
67
+ canvas.height = 0;
68
+ }
69
+ }