@jsenv/navi 0.0.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.
Files changed (123) hide show
  1. package/index.js +51 -0
  2. package/package.json +38 -0
  3. package/src/action_private_properties.js +11 -0
  4. package/src/action_proxy_test.html +353 -0
  5. package/src/action_run_states.js +5 -0
  6. package/src/actions.js +1377 -0
  7. package/src/browser_integration/browser_integration.js +191 -0
  8. package/src/browser_integration/document_back_and_forward.js +17 -0
  9. package/src/browser_integration/document_loading_signal.js +100 -0
  10. package/src/browser_integration/document_state_signal.js +9 -0
  11. package/src/browser_integration/document_url_signal.js +9 -0
  12. package/src/browser_integration/use_is_visited.js +19 -0
  13. package/src/browser_integration/via_history.js +199 -0
  14. package/src/browser_integration/via_navigation.js +168 -0
  15. package/src/components/action_execution/form_context.js +8 -0
  16. package/src/components/action_execution/render_actionable_component.jsx +27 -0
  17. package/src/components/action_execution/use_action.js +330 -0
  18. package/src/components/action_execution/use_execute_action.js +161 -0
  19. package/src/components/action_renderer.jsx +136 -0
  20. package/src/components/collect_form_element_values.js +79 -0
  21. package/src/components/demos/0_button_demo.html +155 -0
  22. package/src/components/demos/1_checkbox_demo.html +257 -0
  23. package/src/components/demos/2_input_textual_demo.html +354 -0
  24. package/src/components/demos/3_radio_demo.html +222 -0
  25. package/src/components/demos/4_select_demo.html +104 -0
  26. package/src/components/demos/5_list_scrollable_demo.html +153 -0
  27. package/src/components/demos/action/0_button_demo.html +204 -0
  28. package/src/components/demos/action/10_shortcuts_demo.html +189 -0
  29. package/src/components/demos/action/11_nested_shortcuts_demo.html +401 -0
  30. package/src/components/demos/action/1_input_text_demo.html +461 -0
  31. package/src/components/demos/action/2_form_multiple.html +303 -0
  32. package/src/components/demos/action/3_details_demo.html +172 -0
  33. package/src/components/demos/action/4_input_checkbox_demo.html +611 -0
  34. package/src/components/demos/action/6_checkbox_list_demo.html +109 -0
  35. package/src/components/demos/action/7_radio_list_demo.html +217 -0
  36. package/src/components/demos/action/8_editable_text_demo.html +442 -0
  37. package/src/components/demos/action/9_link_demo.html +172 -0
  38. package/src/components/demos/demo.md +0 -0
  39. package/src/components/demos/route/basic/basic.html +14 -0
  40. package/src/components/demos/route/basic/basic_route_demo.jsx +224 -0
  41. package/src/components/demos/route/multi/multi.html +14 -0
  42. package/src/components/demos/route/multi/multi_route_demo.jsx +277 -0
  43. package/src/components/details/details.jsx +248 -0
  44. package/src/components/details/summary_marker.jsx +141 -0
  45. package/src/components/editable_text/editable_text.jsx +96 -0
  46. package/src/components/error_boundary_context.js +9 -0
  47. package/src/components/form.jsx +144 -0
  48. package/src/components/input/button.jsx +333 -0
  49. package/src/components/input/checkbox_list.jsx +294 -0
  50. package/src/components/input/field.jsx +61 -0
  51. package/src/components/input/field_css.js +118 -0
  52. package/src/components/input/input.jsx +15 -0
  53. package/src/components/input/input_checkbox.jsx +370 -0
  54. package/src/components/input/input_radio.jsx +299 -0
  55. package/src/components/input/input_textual.jsx +338 -0
  56. package/src/components/input/radio_list.jsx +283 -0
  57. package/src/components/input/select.jsx +273 -0
  58. package/src/components/input/use_form_event.js +20 -0
  59. package/src/components/input/use_on_change.js +12 -0
  60. package/src/components/link/link.jsx +291 -0
  61. package/src/components/loader/loader_background.jsx +324 -0
  62. package/src/components/loader/loading_spinner.jsx +68 -0
  63. package/src/components/loader/network_speed.js +83 -0
  64. package/src/components/loader/rectangle_loading.jsx +225 -0
  65. package/src/components/route.jsx +15 -0
  66. package/src/components/selection/selection.js +5 -0
  67. package/src/components/selection/selection_context.jsx +262 -0
  68. package/src/components/shortcut/os.js +9 -0
  69. package/src/components/shortcut/shortcut_context.jsx +390 -0
  70. package/src/components/use_action_events.js +37 -0
  71. package/src/components/use_auto_focus.js +43 -0
  72. package/src/components/use_debounce_true.js +31 -0
  73. package/src/components/use_focus_group.js +19 -0
  74. package/src/components/use_initial_value.js +104 -0
  75. package/src/components/use_is_visited.js +19 -0
  76. package/src/components/use_ref_array.js +38 -0
  77. package/src/components/use_signal_sync.js +50 -0
  78. package/src/components/use_state_array.js +40 -0
  79. package/src/docs/actions.md +228 -0
  80. package/src/docs/demos/resource/action_status.jsx +42 -0
  81. package/src/docs/demos/resource/demo.md +1 -0
  82. package/src/docs/demos/resource/resource_demo_0.html +84 -0
  83. package/src/docs/demos/resource/resource_demo_10_post_gc.html +364 -0
  84. package/src/docs/demos/resource/resource_demo_11_describe_many.html +362 -0
  85. package/src/docs/demos/resource/resource_demo_2.html +173 -0
  86. package/src/docs/demos/resource/resource_demo_3_filtered_users.html +415 -0
  87. package/src/docs/demos/resource/resource_demo_4_details.html +284 -0
  88. package/src/docs/demos/resource/resource_demo_5_renderer_lazy.html +115 -0
  89. package/src/docs/demos/resource/resource_demo_6_gc.html +217 -0
  90. package/src/docs/demos/resource/resource_demo_7_child_gc.html +240 -0
  91. package/src/docs/demos/resource/resource_demo_8_proxy_gc.html +319 -0
  92. package/src/docs/demos/resource/resource_demo_9_describe_one.html +472 -0
  93. package/src/docs/demos/resource/tata.jsx +3 -0
  94. package/src/docs/demos/resource/toto.jsx +3 -0
  95. package/src/docs/demos/user_nav/user_nav.html +12 -0
  96. package/src/docs/demos/user_nav/user_nav.jsx +330 -0
  97. package/src/docs/resource_dependencies.md +103 -0
  98. package/src/docs/resource_with_params.md +80 -0
  99. package/src/notes.md +13 -0
  100. package/src/route/route.js +518 -0
  101. package/src/route/route.test.html +228 -0
  102. package/src/store/array_signal_store.js +537 -0
  103. package/src/store/local_storage_signal.js +17 -0
  104. package/src/store/resource_graph.js +1303 -0
  105. package/src/store/tests/resource_graph_autoreload_demo.html +12 -0
  106. package/src/store/tests/resource_graph_autoreload_demo.jsx +964 -0
  107. package/src/store/tests/resource_graph_dependencies.test.js +95 -0
  108. package/src/store/value_in_local_storage.js +187 -0
  109. package/src/symbol_object_signal.js +1 -0
  110. package/src/use_action_data.js +10 -0
  111. package/src/use_action_status.js +47 -0
  112. package/src/utils/add_many_event_listeners.js +15 -0
  113. package/src/utils/array_add_remove.js +61 -0
  114. package/src/utils/array_signal.js +15 -0
  115. package/src/utils/compare_two_js_values.js +172 -0
  116. package/src/utils/execute_with_cleanup.js +21 -0
  117. package/src/utils/get_caller_info.js +85 -0
  118. package/src/utils/iterable_weak_set.js +62 -0
  119. package/src/utils/js_value_weak_map.js +162 -0
  120. package/src/utils/js_value_weak_map_demo.html +690 -0
  121. package/src/utils/merge_two_js_values.js +53 -0
  122. package/src/utils/stringify_for_display.js +150 -0
  123. package/src/utils/weak_effect.js +48 -0
@@ -0,0 +1,291 @@
1
+ import { closeValidationMessage, useConstraints } from "@jsenv/validation";
2
+ import { forwardRef } from "preact/compat";
3
+ import { useImperativeHandle, useLayoutEffect, useRef } from "preact/hooks";
4
+ import { useIsVisited } from "../../browser_integration/use_is_visited.js";
5
+ import { useActionStatus } from "../../use_action_status.js";
6
+ import { renderActionableComponent } from "../action_execution/render_actionable_component.jsx";
7
+ import { LoaderBackground } from "../loader/loader_background.jsx";
8
+ import {
9
+ clickToSelect,
10
+ keydownToSelect,
11
+ useRegisterSelectionValue,
12
+ useSelectionContext,
13
+ } from "../selection/selection_context.jsx";
14
+ import {
15
+ ShortcutProvider,
16
+ useShortcutContext,
17
+ } from "../shortcut/shortcut_context.jsx";
18
+ import { useAutoFocus } from "../use_auto_focus.js";
19
+
20
+ /*
21
+ * Apply opacity to child content, not the link element itself.
22
+ *
23
+ * Why not apply opacity directly to .navi_link?
24
+ * - Would make focus outlines semi-transparent too (accessibility issue)
25
+ * - We want dimmed text but full-opacity focus indicators for visibility
26
+ *
27
+ * This approach dims the content while preserving focus outline visibility.
28
+ */
29
+ import.meta.css = /* css */ `
30
+ .navi_link {
31
+ border-radius: 2px;
32
+ }
33
+
34
+ .navi_link:focus {
35
+ position: relative;
36
+ z-index: 1; /* Ensure focus outline is above other elements */
37
+ }
38
+
39
+ .navi_link[data-readonly] > *,
40
+ .navi_link[inert] > * {
41
+ opacity: 0.5;
42
+ }
43
+
44
+ .navi_link[inert] {
45
+ pointer-events: none;
46
+ }
47
+
48
+ .navi_link[data-with-selection] {
49
+ position: relative;
50
+ }
51
+
52
+ .navi_link[data-with-selection] input[type="checkbox"] {
53
+ position: absolute;
54
+ opacity: 0;
55
+ }
56
+
57
+ /* Visual feedback for selected state */
58
+ .navi_link[data-selected] {
59
+ background-color: light-dark(#bbdefb, #2563eb);
60
+ }
61
+
62
+ .navi_link[data-active] {
63
+ font-weight: bold;
64
+ }
65
+
66
+ .navi_link[data-visited] {
67
+ color: light-dark(#6a1b9a, #ab47bc);
68
+ }
69
+ `;
70
+
71
+ export const Link = forwardRef((props, ref) => {
72
+ return renderActionableComponent(props, ref, {
73
+ Basic: LinkBasic,
74
+ WithAction: LinkWithAction,
75
+ });
76
+ });
77
+
78
+ const LinkBasic = forwardRef((props, ref) => {
79
+ const selectionContext = useSelectionContext();
80
+
81
+ if (selectionContext) {
82
+ return (
83
+ <LinkWithSelection
84
+ ref={ref}
85
+ selectionContext={selectionContext}
86
+ {...props}
87
+ />
88
+ );
89
+ }
90
+ return <LinkPlain ref={ref} {...props} />;
91
+ });
92
+
93
+ const LinkPlain = forwardRef((props, ref) => {
94
+ const {
95
+ className = "",
96
+ loading,
97
+ readOnly,
98
+ disabled,
99
+ children,
100
+ autoFocus,
101
+ active,
102
+ visited,
103
+ spaceToClick = true,
104
+ constraints = [],
105
+ onClick,
106
+ onKeyDown,
107
+ href,
108
+ ...rest
109
+ } = props;
110
+
111
+ const innerRef = useRef();
112
+ useImperativeHandle(ref, () => innerRef.current);
113
+
114
+ useAutoFocus(innerRef, autoFocus);
115
+ useConstraints(innerRef, constraints);
116
+
117
+ const shouldDimColor = readOnly || disabled;
118
+ useDimColorWhen(innerRef, shouldDimColor);
119
+
120
+ const isVisited = useIsVisited(href);
121
+
122
+ return (
123
+ <LoaderBackground loading={loading} color="light-dark(#355fcc, #3b82f6)">
124
+ <a
125
+ {...rest}
126
+ href={href}
127
+ ref={innerRef}
128
+ className={["navi_link", ...className.split(" ")].join(" ")}
129
+ aria-busy={loading}
130
+ inert={disabled}
131
+ data-field=""
132
+ data-readonly={readOnly ? "" : undefined}
133
+ data-active={active ? "" : undefined}
134
+ data-visited={visited || isVisited ? "" : undefined}
135
+ onClick={(e) => {
136
+ closeValidationMessage(e.target, "click");
137
+ if (readOnly) {
138
+ e.preventDefault();
139
+ return;
140
+ }
141
+ onClick?.(e);
142
+ }}
143
+ onKeyDown={(e) => {
144
+ if (spaceToClick && e.key === " ") {
145
+ e.preventDefault(); // Prevent page scroll
146
+ if (!readOnly && !disabled) {
147
+ e.target.click();
148
+ }
149
+ }
150
+ onKeyDown?.(e);
151
+ }}
152
+ >
153
+ {children}
154
+ </a>
155
+ </LoaderBackground>
156
+ );
157
+ });
158
+
159
+ const LinkWithSelection = forwardRef((props, ref) => {
160
+ const {
161
+ selectionContext,
162
+ name,
163
+ value,
164
+ children,
165
+ onClick,
166
+ onKeyDown,
167
+ ...rest
168
+ } = props;
169
+ const isSelected = selectionContext.isSelected(value);
170
+ useRegisterSelectionValue(value);
171
+
172
+ return (
173
+ <LinkPlain
174
+ ref={ref}
175
+ {...rest}
176
+ onClick={(e) => {
177
+ clickToSelect(e, { selectionContext, value });
178
+ onClick?.(e);
179
+ }}
180
+ onKeyDown={(e) => {
181
+ keydownToSelect(e, { selectionContext, value });
182
+ onKeyDown?.(e);
183
+ }}
184
+ data-with-selection=""
185
+ data-selected={isSelected ? "" : undefined}
186
+ >
187
+ <input
188
+ className="navi_link_checkbox"
189
+ type="checkbox"
190
+ name={name}
191
+ value={value}
192
+ checked={isSelected}
193
+ // Prevent direct checkbox interaction - only via link clicks or keyboard nav (arrows)
194
+ disabled
195
+ tabIndex={-1} // Don't interfere with link tab order (might be overkill because there is already [disabled])
196
+ />
197
+ {children}
198
+ </LinkPlain>
199
+ );
200
+ });
201
+
202
+ /*
203
+ * Custom hook to apply semi-transparent color when an element should be dimmed.
204
+ *
205
+ * Why we do it this way:
206
+ * 1. **Precise timing**: Captures the element's natural color exactly when transitioning
207
+ * from normal to dimmed state (not before, not after)
208
+ * 2. **Avoids CSS inheritance issues**: CSS `currentColor` and `color-mix()` don't work
209
+ * reliably for creating true transparency that matches `opacity: 0.5`
210
+ * 3. **Performance**: Only executes when the dimmed state actually changes, not on every render
211
+ * 4. **Color accuracy**: Uses `color(from ... / 0.5)` syntax to preserve the exact visual
212
+ * appearance of `opacity: 0.5` but applied only to color
213
+ * 5. **Works with any color**: Handles default blue, visited purple, inherited colors, etc.
214
+ * 6. **Maintains focus outline**: Since we only dim the text color, focus outlines remain
215
+ * fully visible for accessibility
216
+ */
217
+ const useDimColorWhen = (elementRef, shouldDim) => {
218
+ const shouldDimPreviousRef = useRef();
219
+ useLayoutEffect(() => {
220
+ const element = elementRef.current;
221
+ const shouldDimPrevious = shouldDimPreviousRef.current;
222
+
223
+ if (shouldDim === shouldDimPrevious) {
224
+ return;
225
+ }
226
+ shouldDimPreviousRef.current = shouldDim;
227
+ if (shouldDim) {
228
+ // Capture color just before applying disabled state
229
+ const computedStyle = getComputedStyle(element);
230
+ const currentColor = computedStyle.color;
231
+ element.style.color = `color(from ${currentColor} srgb r g b / 0.5)`;
232
+ } else {
233
+ // Clear the inline style to let CSS take over
234
+ element.style.color = "";
235
+ }
236
+ });
237
+ };
238
+
239
+ const LinkWithAction = forwardRef((props, ref) => {
240
+ const {
241
+ shortcuts = [],
242
+ onActionPrevented,
243
+ onActionStart,
244
+ onActionAbort,
245
+ onActionError,
246
+ onActionEnd,
247
+ ...rest
248
+ } = props;
249
+ const innerRef = useRef();
250
+ useImperativeHandle(ref, () => innerRef.current);
251
+
252
+ return (
253
+ <ShortcutProvider
254
+ shortcuts={shortcuts}
255
+ elementRef={innerRef}
256
+ onActionPrevented={onActionPrevented}
257
+ onActionStart={onActionStart}
258
+ onActionAbort={onActionAbort}
259
+ onActionError={onActionError}
260
+ onActionEnd={onActionEnd}
261
+ >
262
+ <LinkWithShortcuts ref={innerRef} {...rest} />
263
+ </ShortcutProvider>
264
+ );
265
+ });
266
+
267
+ const LinkWithShortcuts = forwardRef((props, ref) => {
268
+ const { children, readOnly, loading, ...rest } = props;
269
+ const innerRef = useRef();
270
+ useImperativeHandle(ref, () => innerRef.current);
271
+ const { shortcutAction } = useShortcutContext();
272
+
273
+ const { loading: actionLoading } = useActionStatus(shortcutAction);
274
+ const innerLoading = Boolean(loading || actionLoading);
275
+
276
+ return (
277
+ <>
278
+ <LinkBasic
279
+ ref={innerRef}
280
+ {...rest}
281
+ loading={innerLoading}
282
+ readOnly={readOnly || actionLoading}
283
+ data-readonly-silent={actionLoading && !readOnly ? "" : undefined}
284
+ /* When we have keyboard shortcuts the link outline is visible on focus (not solely on focus-visible) */
285
+ data-focus-visible=""
286
+ >
287
+ {children}
288
+ </LinkBasic>
289
+ </>
290
+ );
291
+ });
@@ -0,0 +1,324 @@
1
+ import { resolveCSSSize } from "@jsenv/dom";
2
+ import { createPortal } from "preact/compat";
3
+ import { useLayoutEffect, useRef, useState } from "preact/hooks";
4
+ import { useDebounceTrue } from "../use_debounce_true.js";
5
+ import { RectangleLoading } from "./rectangle_loading.jsx";
6
+
7
+ import.meta.css = /* css */ `
8
+ [name="element_with_loader_wrapper"] {
9
+ display: inline-flex;
10
+ position: relative;
11
+ width: fit-content;
12
+ }
13
+
14
+ [name="loading_rectangle_wrapper"] {
15
+ pointer-events: none;
16
+ position: absolute;
17
+ z-index: 1;
18
+ }
19
+
20
+ [name="rectangle_loading"] {
21
+ position: relative;
22
+ width: 100%;
23
+ height: 100%;
24
+ }
25
+ `;
26
+
27
+ export const LoaderBackground = ({
28
+ loading,
29
+ containerRef,
30
+ targetSelector,
31
+ color,
32
+ inset = 0,
33
+ spacingTop = 0,
34
+ spacingLeft = 0,
35
+ spacingBottom = 0,
36
+ spacingRight = 0,
37
+ children,
38
+ }) => {
39
+ if (containerRef) {
40
+ const container = containerRef.current;
41
+ if (!container) {
42
+ return children;
43
+ }
44
+ return createPortal(
45
+ <LoaderBackgroundWithPortal
46
+ container={container}
47
+ loading={loading}
48
+ color={color}
49
+ inset={inset}
50
+ spacingTop={spacingTop}
51
+ spacingLeft={spacingLeft}
52
+ spacingBottom={spacingBottom}
53
+ spacingRight={spacingRight}
54
+ >
55
+ {children}
56
+ </LoaderBackgroundWithPortal>,
57
+ container,
58
+ );
59
+ }
60
+
61
+ return (
62
+ <LoaderBackgroundWithWrapper
63
+ targetSelector={targetSelector}
64
+ loading={loading}
65
+ color={color}
66
+ inset={inset}
67
+ spacingTop={spacingTop}
68
+ spacingLeft={spacingLeft}
69
+ spacingBottom={spacingBottom}
70
+ spacingRight={spacingRight}
71
+ >
72
+ {children}
73
+ </LoaderBackgroundWithWrapper>
74
+ );
75
+ };
76
+
77
+ const LoaderBackgroundWithPortal = ({
78
+ container,
79
+ loading,
80
+ color,
81
+ inset,
82
+ spacingTop,
83
+ spacingLeft,
84
+ spacingBottom,
85
+ spacingRight,
86
+ children,
87
+ }) => {
88
+ const shouldShowSpinner = useDebounceTrue(loading, 300);
89
+
90
+ if (!shouldShowSpinner) {
91
+ return children;
92
+ }
93
+
94
+ container.style.position = "relative";
95
+ let paddingTop = 0;
96
+ if (container.nodeName === "DETAILS") {
97
+ paddingTop = container.querySelector("summary").offsetHeight;
98
+ }
99
+
100
+ return (
101
+ <>
102
+ <div
103
+ style={{
104
+ position: "absolute",
105
+ top: `${inset + paddingTop + spacingTop}px`,
106
+ bottom: `${inset + spacingBottom}px`,
107
+ left: `${inset + spacingLeft}px`,
108
+ right: `${inset + spacingRight}px`,
109
+ }}
110
+ >
111
+ {shouldShowSpinner && <RectangleLoading color={color} />}
112
+ </div>
113
+ {children}
114
+ </>
115
+ );
116
+ };
117
+
118
+ const LoaderBackgroundWithWrapper = ({
119
+ loading,
120
+ targetSelector,
121
+ color,
122
+ spacingTop,
123
+ spacingLeft,
124
+ spacingBottom,
125
+ spacingRight,
126
+ inset,
127
+ children,
128
+ }) => {
129
+ const shouldShowSpinner = useDebounceTrue(loading, 300);
130
+ const containerRef = useRef(null);
131
+ const [outlineOffset, setOutlineOffset] = useState(0);
132
+ const [borderRadius, setBorderRadius] = useState(0);
133
+ const [borderTopWidth, setBorderTopWidth] = useState(0);
134
+ const [borderLeftWidth, setBorderLeftWidth] = useState(0);
135
+ const [borderRightWidth, setBorderRightWidth] = useState(0);
136
+ const [borderBottomWidth, setBorderBottomWidth] = useState(0);
137
+ const [marginTop, setMarginTop] = useState(0);
138
+ const [marginBottom, setMarginBottom] = useState(0);
139
+ const [marginLeft, setMarginLeft] = useState(0);
140
+ const [marginRight, setMarginRight] = useState(0);
141
+ const [paddingTop, setPaddingTop] = useState(0);
142
+ const [paddingLeft, setPaddingLeft] = useState(0);
143
+ const [paddingRight, setPaddingRight] = useState(0);
144
+ const [paddingBottom, setPaddingBottom] = useState(0);
145
+ const [flexGrow, setFlexGrow] = useState(0);
146
+ const [flexShrink, setFlexShrink] = useState(1);
147
+ const [flexBasis, setFlexBasis] = useState("auto");
148
+
149
+ const [currentColor, setCurrentColor] = useState(color);
150
+
151
+ useLayoutEffect(() => {
152
+ let animationFrame;
153
+ const updateStyles = () => {
154
+ const container = containerRef.current;
155
+ const containedElement = container.lastElementChild;
156
+ const target = targetSelector
157
+ ? container.querySelector(targetSelector)
158
+ : containedElement;
159
+ if (target) {
160
+ const { width, height } = target.getBoundingClientRect();
161
+
162
+ const containedComputedStyle =
163
+ window.getComputedStyle(containedElement);
164
+ const targetComputedStyle = window.getComputedStyle(target);
165
+
166
+ // Read flex properties from the contained element to mirror its behavior
167
+ const newFlexGrow = containedComputedStyle.flexGrow || "0";
168
+ const newFlexShrink = containedComputedStyle.flexShrink || "1";
169
+ const newFlexBasis = containedComputedStyle.flexBasis || "auto";
170
+
171
+ const newBorderTopWidth = resolveCSSSize(
172
+ targetComputedStyle.borderTopWidth,
173
+ );
174
+ const newBorderLeftWidth = resolveCSSSize(
175
+ targetComputedStyle.borderLeftWidth,
176
+ );
177
+ const newBorderRightWidth = resolveCSSSize(
178
+ targetComputedStyle.borderRightWidth,
179
+ );
180
+ const newBorderBottomWidth = resolveCSSSize(
181
+ targetComputedStyle.borderBottomWidth,
182
+ );
183
+ const newBorderRadius = resolveCSSSize(
184
+ targetComputedStyle.borderRadius,
185
+ {
186
+ availableSize: Math.min(width, height),
187
+ },
188
+ );
189
+ const newOutlineColor = targetComputedStyle.outlineColor;
190
+ const newBorderColor = targetComputedStyle.borderColor;
191
+ const newDetectedColor = targetComputedStyle.color;
192
+ const newOutlineOffset = resolveCSSSize(
193
+ targetComputedStyle.outlineOffset,
194
+ );
195
+ const newMarginTop = resolveCSSSize(targetComputedStyle.marginTop);
196
+ const newMarginBottom = resolveCSSSize(
197
+ targetComputedStyle.marginBottom,
198
+ );
199
+ const newMarginLeft = resolveCSSSize(targetComputedStyle.marginLeft);
200
+ const newMarginRight = resolveCSSSize(targetComputedStyle.marginRight);
201
+
202
+ const paddingTop = resolveCSSSize(containedComputedStyle.paddingTop);
203
+ const paddingLeft = resolveCSSSize(containedComputedStyle.paddingLeft);
204
+ const paddingRight = resolveCSSSize(
205
+ containedComputedStyle.paddingRight,
206
+ );
207
+ const paddingBottom = resolveCSSSize(
208
+ containedComputedStyle.paddingBottom,
209
+ );
210
+
211
+ setBorderTopWidth(newBorderTopWidth);
212
+ setBorderLeftWidth(newBorderLeftWidth);
213
+ setBorderRightWidth(newBorderRightWidth);
214
+ setBorderBottomWidth(newBorderBottomWidth);
215
+ setBorderRadius(newBorderRadius);
216
+ setOutlineOffset(newOutlineOffset);
217
+ setMarginTop(newMarginTop);
218
+ setMarginBottom(newMarginBottom);
219
+ setMarginLeft(newMarginLeft);
220
+ setMarginRight(newMarginRight);
221
+ setPaddingTop(paddingTop);
222
+ setPaddingLeft(paddingLeft);
223
+ setPaddingRight(paddingRight);
224
+ setPaddingBottom(paddingBottom);
225
+ setFlexGrow(newFlexGrow);
226
+ setFlexShrink(newFlexShrink);
227
+ setFlexBasis(newFlexBasis);
228
+
229
+ if (color) {
230
+ setCurrentColor(color);
231
+ } else if (
232
+ newOutlineColor &&
233
+ newOutlineColor !== "rgba(0, 0, 0, 0)" &&
234
+ (document.activeElement === containedElement ||
235
+ newBorderColor === "rgba(0, 0, 0, 0)")
236
+ ) {
237
+ setCurrentColor(newOutlineColor);
238
+ } else if (newBorderColor && newBorderColor !== "rgba(0, 0, 0, 0)") {
239
+ setCurrentColor(newBorderColor);
240
+ } else {
241
+ setCurrentColor(newDetectedColor);
242
+ }
243
+ }
244
+
245
+ // updateStyles is very cheap so we run it every frame
246
+ animationFrame = requestAnimationFrame(updateStyles);
247
+ };
248
+ updateStyles();
249
+
250
+ return () => {
251
+ cancelAnimationFrame(animationFrame);
252
+ };
253
+ }, [color, targetSelector]);
254
+
255
+ spacingTop += inset;
256
+ spacingTop += outlineOffset;
257
+ spacingTop -= borderTopWidth;
258
+ spacingTop += marginTop;
259
+ spacingLeft += inset;
260
+ spacingLeft += outlineOffset;
261
+ spacingLeft -= borderLeftWidth;
262
+ spacingLeft += marginLeft;
263
+ spacingRight += inset;
264
+ spacingRight += outlineOffset;
265
+ spacingRight -= borderRightWidth;
266
+ spacingRight += marginRight;
267
+ spacingBottom += inset;
268
+ spacingBottom += outlineOffset;
269
+ spacingBottom -= borderBottomWidth;
270
+ spacingBottom += marginBottom;
271
+ if (targetSelector) {
272
+ // oversimplification that actually works
273
+ // (simplified because it assumes the targeted element is a direct child of the contained element which may have padding)
274
+ spacingTop += paddingTop;
275
+ spacingLeft += paddingLeft;
276
+ spacingRight += paddingRight;
277
+ spacingBottom += paddingBottom;
278
+ }
279
+
280
+ const maxBorderWidth = Math.max(
281
+ borderTopWidth,
282
+ borderLeftWidth,
283
+ borderRightWidth,
284
+ borderBottomWidth,
285
+ );
286
+ const size = Math.max(2, maxBorderWidth / 2);
287
+
288
+ spacingTop += size / 4;
289
+ spacingLeft += size / 4;
290
+ spacingRight += size / 4;
291
+ spacingBottom += size / 4;
292
+
293
+ return (
294
+ <div
295
+ name="element_with_loader_wrapper"
296
+ ref={containerRef}
297
+ data-loader-visible={shouldShowSpinner ? "" : undefined}
298
+ style={{
299
+ flexGrow,
300
+ flexShrink,
301
+ flexBasis,
302
+ }}
303
+ >
304
+ {shouldShowSpinner && (
305
+ <div
306
+ name="loading_rectangle_wrapper"
307
+ style={{
308
+ top: `${spacingTop}px`,
309
+ left: `${spacingLeft}px`,
310
+ bottom: `${spacingBottom}px`,
311
+ right: `${spacingRight}px`,
312
+ }}
313
+ >
314
+ <RectangleLoading
315
+ color={currentColor}
316
+ radius={borderRadius}
317
+ size={size}
318
+ />
319
+ </div>
320
+ )}
321
+ {children}
322
+ </div>
323
+ );
324
+ };
@@ -0,0 +1,68 @@
1
+ export const LoadingSpinner = ({ color = "FF156D" }) => {
2
+ return (
3
+ <svg
4
+ viewBox="0 0 200 200"
5
+ width="100%"
6
+ height="100%"
7
+ xmlns="http://www.w3.org/2000/svg"
8
+ >
9
+ <rect
10
+ fill={color}
11
+ stroke={color}
12
+ stroke-width="15"
13
+ width="30"
14
+ height="30"
15
+ x="25"
16
+ y="85"
17
+ >
18
+ <animate
19
+ attributeName="opacity"
20
+ calcMode="spline"
21
+ dur="2"
22
+ values="1;0;1;"
23
+ keySplines=".5 0 .5 1;.5 0 .5 1"
24
+ repeatCount="indefinite"
25
+ begin="-.4"
26
+ ></animate>
27
+ </rect>
28
+ <rect
29
+ fill={color}
30
+ stroke={color}
31
+ stroke-width="15"
32
+ width="30"
33
+ height="30"
34
+ x="85"
35
+ y="85"
36
+ >
37
+ <animate
38
+ attributeName="opacity"
39
+ calcMode="spline"
40
+ dur="2"
41
+ values="1;0;1;"
42
+ keySplines=".5 0 .5 1;.5 0 .5 1"
43
+ repeatCount="indefinite"
44
+ begin="-.2"
45
+ ></animate>
46
+ </rect>
47
+ <rect
48
+ fill={color}
49
+ stroke={color}
50
+ stroke-width="15"
51
+ width="30"
52
+ height="30"
53
+ x="145"
54
+ y="85"
55
+ >
56
+ <animate
57
+ attributeName="opacity"
58
+ calcMode="spline"
59
+ dur="2"
60
+ values="1;0;1;"
61
+ keySplines=".5 0 .5 1;.5 0 .5 1"
62
+ repeatCount="indefinite"
63
+ begin="0"
64
+ ></animate>
65
+ </rect>
66
+ </svg>
67
+ );
68
+ };