actual-css 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.
Files changed (129) hide show
  1. package/CHANGELOG.md +11 -0
  2. package/LICENSE +21 -0
  3. package/README.md +166 -0
  4. package/dist/actual.css +4929 -0
  5. package/dist/actual.js +3 -0
  6. package/dist/actual.js.map +31 -0
  7. package/dist/actual.min.css +1 -0
  8. package/docs/README.md +26 -0
  9. package/docs/components.md +860 -0
  10. package/docs/design-notes/accessibility-decisions.md +32 -0
  11. package/docs/design-notes/browser-support.md +93 -0
  12. package/docs/design-notes/cascade-layer.md +52 -0
  13. package/docs/design-notes/css-anchor.md +33 -0
  14. package/docs/design-notes/naming.md +37 -0
  15. package/docs/design-notes/previous-work.md +5 -0
  16. package/docs/design-notes/references.md +324 -0
  17. package/docs/forms.md +1265 -0
  18. package/docs/javascript.md +426 -0
  19. package/docs/layout.md +480 -0
  20. package/docs/mental-model.md +73 -0
  21. package/docs/patterns.md +415 -0
  22. package/docs/reserved.md +7 -0
  23. package/docs/tokens.md +455 -0
  24. package/docs/typography.md +159 -0
  25. package/docs/ui.md +937 -0
  26. package/docs/utilities.md +384 -0
  27. package/llms.txt +17 -0
  28. package/package.json +101 -0
  29. package/src/css/actual.css +17 -0
  30. package/src/css/actual.full.css +2 -0
  31. package/src/css/actual.layer.css +3 -0
  32. package/src/css/base.css +70 -0
  33. package/src/css/components/accordion.css +97 -0
  34. package/src/css/components/actions.css +21 -0
  35. package/src/css/components/alert.css +137 -0
  36. package/src/css/components/avatar.css +199 -0
  37. package/src/css/components/badge.css +143 -0
  38. package/src/css/components/breadcrumb.css +56 -0
  39. package/src/css/components/busy.css +44 -0
  40. package/src/css/components/button.css +216 -0
  41. package/src/css/components/card.css +140 -0
  42. package/src/css/components/drawer.css +122 -0
  43. package/src/css/components/flyout.css +243 -0
  44. package/src/css/components/index.css +41 -0
  45. package/src/css/components/join.css +106 -0
  46. package/src/css/components/key.css +25 -0
  47. package/src/css/components/meter.css +81 -0
  48. package/src/css/components/modal.css +162 -0
  49. package/src/css/components/navbar.css +98 -0
  50. package/src/css/components/overline.css +49 -0
  51. package/src/css/components/pagination.css +30 -0
  52. package/src/css/components/progress.css +126 -0
  53. package/src/css/components/scrollspy.css +27 -0
  54. package/src/css/components/skeleton.css +67 -0
  55. package/src/css/components/spinner.css +60 -0
  56. package/src/css/components/status-bar.css +82 -0
  57. package/src/css/components/surface.css +48 -0
  58. package/src/css/components/tab.css +128 -0
  59. package/src/css/components/table.css +64 -0
  60. package/src/css/components/tooltip.css +107 -0
  61. package/src/css/dialog-fallback.css +56 -0
  62. package/src/css/focus.css +13 -0
  63. package/src/css/forms/choice-card.css +158 -0
  64. package/src/css/forms/choice.css +196 -0
  65. package/src/css/forms/control.css +106 -0
  66. package/src/css/forms/custom-select.css +93 -0
  67. package/src/css/forms/form-actions.css +43 -0
  68. package/src/css/forms/form.css +57 -0
  69. package/src/css/forms/index.css +15 -0
  70. package/src/css/forms/input-icon.css +36 -0
  71. package/src/css/forms/native.css +68 -0
  72. package/src/css/forms/select.css +36 -0
  73. package/src/css/forms/switch.css +127 -0
  74. package/src/css/forms/validation.css +63 -0
  75. package/src/css/grid.css +95 -0
  76. package/src/css/intents.css +33 -0
  77. package/src/css/layout.css +80 -0
  78. package/src/css/optional/index.css +5 -0
  79. package/src/css/optional/layout-extra.css +55 -0
  80. package/src/css/optional/scroll-snap.css +51 -0
  81. package/src/css/optional/scroller.css +51 -0
  82. package/src/css/optional/typography-fluid.css +48 -0
  83. package/src/css/optional/utilities-extra.css +289 -0
  84. package/src/css/print.css +47 -0
  85. package/src/css/prose.css +187 -0
  86. package/src/css/reset.css +56 -0
  87. package/src/css/theme.css +186 -0
  88. package/src/css/themes/corporate.css +60 -0
  89. package/src/css/themes/cyberpunk.css +60 -0
  90. package/src/css/themes/dark.css +32 -0
  91. package/src/css/themes/dim.css +33 -0
  92. package/src/css/themes/edge.css +143 -0
  93. package/src/css/themes/forest.css +65 -0
  94. package/src/css/themes/gradient.css +97 -0
  95. package/src/css/themes/index.css +43 -0
  96. package/src/css/themes/lavender.css +66 -0
  97. package/src/css/themes/mono.css +60 -0
  98. package/src/css/themes/neon.css +66 -0
  99. package/src/css/themes/ocean.css +60 -0
  100. package/src/css/themes/petrol.css +64 -0
  101. package/src/css/themes/spruce.css +64 -0
  102. package/src/css/themes/square.css +68 -0
  103. package/src/css/themes/sunset.css +66 -0
  104. package/src/css/tokens.css +122 -0
  105. package/src/css/utilities.css +162 -0
  106. package/src/css/variants.css +118 -0
  107. package/src/js/command.js +165 -0
  108. package/src/js/context-menu.js +291 -0
  109. package/src/js/dialog-fallback.js +163 -0
  110. package/src/js/dialog.js +503 -0
  111. package/src/js/enhance.js +208 -0
  112. package/src/js/events.js +22 -0
  113. package/src/js/filter.js +122 -0
  114. package/src/js/floating.js +462 -0
  115. package/src/js/flyout.js +187 -0
  116. package/src/js/focus.js +35 -0
  117. package/src/js/index.js +26 -0
  118. package/src/js/input.js +47 -0
  119. package/src/js/keys.js +30 -0
  120. package/src/js/mask.js +184 -0
  121. package/src/js/menu.js +88 -0
  122. package/src/js/password.js +98 -0
  123. package/src/js/scrollspy.js +121 -0
  124. package/src/js/selectors.js +59 -0
  125. package/src/js/status.js +158 -0
  126. package/src/js/surface.js +358 -0
  127. package/src/js/tab.js +173 -0
  128. package/src/js/tooltip.js +374 -0
  129. package/src/js/validation.js +419 -0
@@ -0,0 +1,22 @@
1
+ export const ACTUAL_EVENT_PREFIX = "actual";
2
+
3
+ export const EVENTS = {
4
+ reposition: `${ACTUAL_EVENT_PREFIX}:reposition`,
5
+ hide: `${ACTUAL_EVENT_PREFIX}:hide`,
6
+ outOfView: `${ACTUAL_EVENT_PREFIX}:out-of-view`,
7
+ // Cancelable: dispatched by a surface before it opens.
8
+ // detail: { surface, options }
9
+ surfaceOpen: `${ACTUAL_EVENT_PREFIX}:surface-open`,
10
+ // Cancelable: dispatched by a context target before its menu opens.
11
+ // detail: { menu, context, origin, trigger }
12
+ contextMenu: `${ACTUAL_EVENT_PREFIX}:context-menu`,
13
+ // Dispatched to show or clear the singleton status bar.
14
+ // detail: { message, intent, duration } (or {} to clear)
15
+ status: `${ACTUAL_EVENT_PREFIX}:status`,
16
+ // Dispatched by a form after validation blocks submission.
17
+ // detail: { form, firstInvalid, message }
18
+ invalid: `${ACTUAL_EVENT_PREFIX}:invalid`,
19
+ // Cancelable: dispatched by dialog.js before handling native cancel close.
20
+ // detail: { dialog, sourceEvent }
21
+ dialogCancel: `${ACTUAL_EVENT_PREFIX}:dialog-cancel`,
22
+ };
@@ -0,0 +1,122 @@
1
+ /*
2
+ * Input filter — opt-in filtering for simple text policies.
3
+ *
4
+ * inputmode only hints the virtual keyboard; it never filters input on its
5
+ * own. data-filter is the explicit, separate contract for rewriting the
6
+ * value as the user types — data-filter="numeric" / "decimal" is
7
+ * intentionally destructive, unlike inputmode.
8
+ */
9
+
10
+ import enhance from "./enhance.js";
11
+ import { dispatchInput, onTextInput, selectionStart, setCaret } from "./input.js";
12
+
13
+ const SELECTOR = "input[data-filter]";
14
+ const FILTERS = new Map([
15
+ ["numeric", filterNumeric],
16
+ ["decimal", filterDecimal],
17
+ ["lower", filterLower],
18
+ ["upper", filterUpper],
19
+ ["letters", filterLetters],
20
+ ["slug", filterSlug],
21
+ ]);
22
+
23
+ function filterNumeric(value) {
24
+ return value.replace(/\D/gu, "");
25
+ }
26
+
27
+ function filterDecimal(value) {
28
+ const normalized = value.replace(/,/g, ".");
29
+ let out = "";
30
+ let hasDot = false;
31
+
32
+ for (const char of normalized) {
33
+ if (/^\d$/u.test(char)) {
34
+ out += char;
35
+ } else if (char === "." && !hasDot) {
36
+ out += ".";
37
+ hasDot = true;
38
+ }
39
+ }
40
+
41
+ return out;
42
+ }
43
+
44
+ function filterLower(value) {
45
+ return value.toLocaleLowerCase();
46
+ }
47
+
48
+ function filterUpper(value) {
49
+ return value.toLocaleUpperCase();
50
+ }
51
+
52
+ function filterLetters(value) {
53
+ return value.replace(/[^\p{L}]/gu, "");
54
+ }
55
+
56
+ function normalizeText(value) {
57
+ return value.normalize("NFKD").replace(/\p{M}/gu, "");
58
+ }
59
+
60
+ function filterSlug(value, event) {
61
+ const keepTrailingSeparator = event?.inputType === "insertText" && /[^\p{L}\p{N}]$/u.test(value);
62
+ const slug = normalizeText(value)
63
+ .toLocaleLowerCase()
64
+ .replace(/[^\p{L}\p{N}]+/gu, "-")
65
+ .replace(/-{2,}/g, "-")
66
+ .replace(/^-+/g, "");
67
+
68
+ return keepTrailingSeparator ? slug : slug.replace(/-+$/g, "");
69
+ }
70
+
71
+ function filterValue(value, modes, event) {
72
+ return modes.reduce((next, mode) => FILTERS.get(mode)(next, event), value);
73
+ }
74
+
75
+ function explicitFilterModes(value) {
76
+ return value
77
+ .split("|")
78
+ .map((mode) => mode.trim().toLowerCase())
79
+ .filter((mode) => FILTERS.has(mode));
80
+ }
81
+
82
+ function filterModes(el) {
83
+ const explicit = el.getAttribute("data-filter")?.trim().toLowerCase();
84
+ return explicit ? explicitFilterModes(explicit) : [];
85
+ }
86
+
87
+ function connectFilter(el) {
88
+ const modes = filterModes(el);
89
+ if (!modes.length) return;
90
+
91
+ const controller = new AbortController();
92
+ let filtering = false;
93
+
94
+ onTextInput(
95
+ el,
96
+ (event) => {
97
+ if (filtering) return;
98
+
99
+ const caret = selectionStart(el);
100
+ const previous = el.value;
101
+ const next = filterValue(previous, modes, event);
102
+
103
+ if (next === previous) return;
104
+
105
+ el.value = next;
106
+ setCaret(el, filterValue(previous.slice(0, caret), modes, event).length);
107
+ try {
108
+ filtering = true;
109
+ dispatchInput(el);
110
+ } finally {
111
+ filtering = false;
112
+ }
113
+ },
114
+ controller.signal,
115
+ );
116
+
117
+ return () => controller.abort();
118
+ }
119
+
120
+ enhance({
121
+ [SELECTOR]: connectFilter,
122
+ });
@@ -0,0 +1,462 @@
1
+ /*
2
+ * Compact positioning engine for menus, tooltips, and popovers.
3
+ *
4
+ * Core math ported from Floating UI. Uses a single global scroll/resize
5
+ * listener to reposition all registered floating elements via rAF.
6
+ */
7
+
8
+ import { EVENTS } from "./events.js";
9
+
10
+ /* ── Placement parsing ────────────────────────────────── */
11
+
12
+ function getSide(p) {
13
+ return p.split("-")[0];
14
+ }
15
+
16
+ function getAlignment(p) {
17
+ return p.split("-")[1] || null;
18
+ }
19
+
20
+ function getMainAxis(p) {
21
+ return ["top", "bottom"].includes(getSide(p)) ? "x" : "y";
22
+ }
23
+
24
+ function flipSide(s) {
25
+ if (s === "top") return "bottom";
26
+ if (s === "bottom") return "top";
27
+ if (s === "left") return "right";
28
+ if (s === "right") return "left";
29
+ }
30
+
31
+ /* ── Coordinate math ──────────────────────────────────── */
32
+
33
+ function computeCoords(reference, floating, placement, rtl) {
34
+ const commonX = reference.x + reference.width / 2 - floating.width / 2;
35
+ const commonY = reference.y + reference.height / 2 - floating.height / 2;
36
+ const mainAxis = getMainAxis(placement);
37
+ const commonAlign =
38
+ reference[mainAxis === "x" ? "width" : "height"] / 2 -
39
+ floating[mainAxis === "x" ? "width" : "height"] / 2;
40
+ const side = getSide(placement);
41
+
42
+ let coords;
43
+ switch (side) {
44
+ case "top":
45
+ coords = { x: commonX, y: reference.y - floating.height };
46
+ break;
47
+ case "bottom":
48
+ coords = { x: commonX, y: reference.y + reference.height };
49
+ break;
50
+ case "right":
51
+ coords = { x: reference.x + reference.width, y: commonY };
52
+ break;
53
+ case "left":
54
+ coords = { x: reference.x - floating.width, y: commonY };
55
+ break;
56
+ default:
57
+ coords = { x: reference.x, y: reference.y };
58
+ }
59
+
60
+ const align = getAlignment(placement);
61
+ const isVertical = mainAxis === "x";
62
+ if (align === "start") coords[mainAxis] -= commonAlign * (rtl && isVertical ? -1 : 1);
63
+ if (align === "end") coords[mainAxis] += commonAlign * (rtl && isVertical ? -1 : 1);
64
+
65
+ return coords;
66
+ }
67
+
68
+ function applyOffset(coords, side, offset, rtl) {
69
+ switch (side) {
70
+ case "top":
71
+ coords.y -= offset;
72
+ break;
73
+ case "bottom":
74
+ coords.y += offset;
75
+ break;
76
+ case "left":
77
+ coords.x += rtl ? offset : -offset;
78
+ break;
79
+ case "right":
80
+ coords.x += rtl ? -offset : offset;
81
+ break;
82
+ }
83
+ }
84
+
85
+ function getInlineOverflow(coords, floating, minX, maxX) {
86
+ return Math.max(minX - coords.x, 0) + Math.max(coords.x + floating.width - maxX, 0);
87
+ }
88
+
89
+ function toBoundary(rect) {
90
+ return {
91
+ x: rect.x,
92
+ y: rect.y,
93
+ width: rect.width,
94
+ height: rect.height,
95
+ right: rect.x + rect.width,
96
+ bottom: rect.y + rect.height,
97
+ };
98
+ }
99
+
100
+ const supportsDirSelector = typeof CSS !== "undefined" && CSS.supports("selector(:dir(rtl))");
101
+
102
+ function isRTL(el) {
103
+ if (el.dir === "rtl") return true;
104
+ if (el.dir === "ltr") return false;
105
+ if (supportsDirSelector) return el.matches(":dir(rtl)");
106
+ return el.ownerDocument?.dir === "rtl";
107
+ }
108
+
109
+ function getDocEl(doc) {
110
+ return doc.documentElement;
111
+ }
112
+
113
+ function toNumber(value) {
114
+ const number = Number.parseFloat(value);
115
+ return Number.isFinite(number) ? number : 0;
116
+ }
117
+
118
+ const STABLE_SCROLLBAR_MAX_WIDTH = 25;
119
+ const NARROW_INLINE_FLIP_FALLBACK = 128;
120
+
121
+ function getViewportBoundary(doc) {
122
+ const win = doc.defaultView || window;
123
+ const docEl = getDocEl(doc);
124
+ const visualViewport = win.visualViewport;
125
+ const x = visualViewport?.offsetLeft || 0;
126
+ const y = visualViewport?.offsetTop || 0;
127
+ let width = visualViewport?.width || docEl.clientWidth || win.innerWidth;
128
+ const height = visualViewport?.height || docEl.clientHeight || win.innerHeight;
129
+
130
+ const body = doc.body;
131
+ if (body?.clientWidth > 0) {
132
+ const bodyStyle = win.getComputedStyle?.(body);
133
+ const bodyMargin =
134
+ doc.compatMode === "CSS1Compat"
135
+ ? toNumber(bodyStyle?.marginLeft) + toNumber(bodyStyle?.marginRight)
136
+ : 0;
137
+ // Classic stable scrollbars reserve inline space on the body. Subtract only
138
+ // plausible scrollbar widths so unusual body sizing does not shrink the viewport.
139
+ const stableScrollbar = Math.abs(docEl.clientWidth - body.clientWidth - bodyMargin);
140
+ if (stableScrollbar <= STABLE_SCROLLBAR_MAX_WIDTH) {
141
+ width -= stableScrollbar;
142
+ }
143
+ }
144
+
145
+ return toBoundary({ x, y, width, height });
146
+ }
147
+
148
+ function getBoundary(ref, opts) {
149
+ if (!opts.scope) return getViewportBoundary(ref.ownerDocument);
150
+ const rect = opts.scope.getBoundingClientRect();
151
+ return toBoundary({
152
+ x: rect.x,
153
+ y: rect.y,
154
+ width: rect.width,
155
+ height: rect.height,
156
+ });
157
+ }
158
+
159
+ function isOutsideBoundary(rect, boundary) {
160
+ return (
161
+ rect.right < boundary.x ||
162
+ rect.left > boundary.right ||
163
+ rect.bottom < boundary.y ||
164
+ rect.top > boundary.bottom
165
+ );
166
+ }
167
+
168
+ function getAvailableHeight(refRect, side, boundary, distance, padding) {
169
+ if (side === "top") {
170
+ return Math.max(0, refRect.top - boundary.y - distance - padding);
171
+ }
172
+ if (side === "bottom") {
173
+ return Math.max(0, boundary.bottom - refRect.bottom - distance - padding);
174
+ }
175
+ return Math.max(0, boundary.height - padding * 2);
176
+ }
177
+
178
+ function isVisible(el) {
179
+ if (el.hidden) return false;
180
+ if (typeof el.checkVisibility === "function") return el.checkVisibility();
181
+ return el.getClientRects().length > 0;
182
+ }
183
+
184
+ function topVisibleTracked() {
185
+ const elements = [...tracked];
186
+ for (let i = elements.length - 1; i >= 0; i--) {
187
+ const el = elements[i];
188
+ if (el.isConnected && isVisible(el)) return el;
189
+ }
190
+ return null;
191
+ }
192
+
193
+ function getFloatingSize(float) {
194
+ const rect = float.getBoundingClientRect();
195
+ return {
196
+ width: float.offsetWidth || rect.width,
197
+ height: float.offsetHeight || rect.height,
198
+ };
199
+ }
200
+
201
+ /* ── Global scroll / resize tracking ──────────────────── */
202
+
203
+ const tracked = new Set();
204
+ let resizeObserver = null;
205
+ let tick = false;
206
+ const pendingTypes = new Set();
207
+
208
+ function notify(type) {
209
+ if (type === "escape") {
210
+ const el = topVisibleTracked();
211
+ if (!el) return;
212
+ el.dispatchEvent(
213
+ new CustomEvent(EVENTS.hide, {
214
+ bubbles: false,
215
+ detail: { type: "escape" },
216
+ }),
217
+ );
218
+ return;
219
+ }
220
+
221
+ for (const el of tracked) {
222
+ el.dispatchEvent(
223
+ new CustomEvent(EVENTS.reposition, {
224
+ bubbles: false,
225
+ detail: { type },
226
+ }),
227
+ );
228
+ }
229
+ }
230
+
231
+ function hasOpenSurface() {
232
+ return topVisibleTracked() !== null;
233
+ }
234
+
235
+ function rafNotify(e) {
236
+ if (e?.type) pendingTypes.add(e.type);
237
+ if (!tick) {
238
+ requestAnimationFrame(() => {
239
+ const types = [...pendingTypes];
240
+ pendingTypes.clear();
241
+ for (const type of types) notify(type);
242
+ tick = false;
243
+ });
244
+ }
245
+ tick = true;
246
+ }
247
+
248
+ function getResizeObserver() {
249
+ if (resizeObserver || typeof window === "undefined" || !window.ResizeObserver) {
250
+ return resizeObserver;
251
+ }
252
+
253
+ resizeObserver = new window.ResizeObserver((entries, observer) => {
254
+ for (const entry of entries) {
255
+ observer.unobserve(entry.target);
256
+ rafNotify({ type: "element-resize" });
257
+ requestAnimationFrame(() => {
258
+ if (tracked.has(entry.target)) observer.observe(entry.target);
259
+ });
260
+ }
261
+ });
262
+ return resizeObserver;
263
+ }
264
+
265
+ // Global document listeners run at import time; keep SSR imports inert.
266
+ if (typeof document !== "undefined") {
267
+ document.addEventListener("scroll", rafNotify, { passive: true, capture: true });
268
+ document.addEventListener("keydown", (e) => {
269
+ if (e.key === "Escape" && !e.ctrlKey && !e.altKey && !e.shiftKey) {
270
+ if (!hasOpenSurface()) return;
271
+ e.preventDefault();
272
+ notify("escape");
273
+ }
274
+ });
275
+ }
276
+ // Global window listener runs at import time; keep SSR imports inert.
277
+ if (typeof window !== "undefined") {
278
+ window.addEventListener("resize", rafNotify, { passive: true });
279
+ }
280
+
281
+ /* ── Public API ────────────────────────────────────────── */
282
+
283
+ /**
284
+ * Register a floating element for auto-reposition on scroll/resize.
285
+ * Returns a cleanup function that removes the element from tracking.
286
+ */
287
+ export function track(el) {
288
+ tracked.add(el);
289
+ getResizeObserver()?.observe(el);
290
+ return () => {
291
+ tracked.delete(el);
292
+ resizeObserver?.unobserve(el);
293
+ };
294
+ }
295
+
296
+ /**
297
+ * Compute and apply position for a floating element relative to a reference.
298
+ *
299
+ * @param {Element} ref
300
+ * @param {HTMLElement} float
301
+ * @param {object} [opts]
302
+ * @param {string} [opts.placement] default "bottom-start"
303
+ * @param {number} [opts.distance] px offset from reference
304
+ * @param {boolean} [opts.flip] flip if overflows viewport
305
+ * @param {boolean} [opts.shift] shift if overflows x-axis
306
+ * @param {number} [opts.shiftPadding] px padding when shifting
307
+ * @param {Element} [opts.scope] constrain to a bounding element
308
+ */
309
+ export function reposition(ref, float, opts = {}) {
310
+ if (!isVisible(float)) return;
311
+
312
+ const placement = opts.placement || "bottom-start";
313
+ const distance = opts.distance || 0;
314
+ const flip = opts.flip !== false;
315
+ const shift = opts.shift !== false;
316
+ const shiftPad = opts.shiftPadding || 4;
317
+ const rtl = isRTL(ref);
318
+
319
+ const rects = ref.getClientRects();
320
+ const refRect = placement.startsWith("bottom") ? rects[rects.length - 1] : rects[0];
321
+ if (!refRect) return;
322
+
323
+ const boundary = getBoundary(ref, opts);
324
+ if (isOutsideBoundary(refRect, boundary)) {
325
+ float.dispatchEvent(
326
+ new CustomEvent(EVENTS.outOfView, {
327
+ bubbles: false,
328
+ detail: { type: "out-of-view" },
329
+ }),
330
+ );
331
+ return;
332
+ }
333
+
334
+ const floatRect = getFloatingSize(float);
335
+ const sx = boundary.x;
336
+ const sy = boundary.y;
337
+ const cw = boundary.right;
338
+ const ch = boundary.bottom;
339
+
340
+ let side = getSide(placement);
341
+ const align = getAlignment(placement);
342
+ let axis = getMainAxis(placement);
343
+ let current = placement;
344
+ let coords = computeCoords(refRect, floatRect, current, rtl);
345
+ applyOffset(coords, side, distance, rtl);
346
+
347
+ if (flip) {
348
+ const cx = Math.ceil(coords.x);
349
+ const cy = Math.ceil(coords.y);
350
+
351
+ if (
352
+ (axis === "x" && (cy < sy || cy + floatRect.height >= ch)) ||
353
+ (axis === "y" && (cx < sx || cx + floatRect.width >= cw))
354
+ ) {
355
+ side = flipSide(side);
356
+ current = align ? `${side}-${align}` : side;
357
+ coords = computeCoords(refRect, floatRect, current, rtl);
358
+ applyOffset(coords, side, distance, rtl);
359
+ }
360
+
361
+ // Narrow side placements can fit neither left nor right. In that case,
362
+ // fall back to top so compact mobile layouts get a usable placement.
363
+ if (
364
+ axis === "y" &&
365
+ (coords.x < sx || coords.x + floatRect.width > cw) &&
366
+ boundary.width - floatRect.width < NARROW_INLINE_FLIP_FALLBACK
367
+ ) {
368
+ side = "top";
369
+ axis = "x";
370
+ current = align ? `${side}-${align}` : side;
371
+ coords = computeCoords(refRect, floatRect, current, rtl);
372
+ applyOffset(coords, side, distance, rtl);
373
+ }
374
+ }
375
+
376
+ if (axis === "x" && shift && getAlignment(current)) {
377
+ const minX = sx + shiftPad;
378
+ const maxX = cw - shiftPad;
379
+ const currentOverflow = getInlineOverflow(coords, floatRect, minX, maxX);
380
+
381
+ if (currentOverflow > 0) {
382
+ const nextAlign = getAlignment(current) === "end" ? "start" : "end";
383
+ const candidatePlacement = `${side}-${nextAlign}`;
384
+ const candidate = computeCoords(refRect, floatRect, candidatePlacement, rtl);
385
+ applyOffset(candidate, side, distance, rtl);
386
+
387
+ if (getInlineOverflow(candidate, floatRect, minX, maxX) < currentOverflow) {
388
+ current = candidatePlacement;
389
+ coords = candidate;
390
+ }
391
+ }
392
+ }
393
+
394
+ // shift on x axis
395
+ let p = 50;
396
+ if (shift || floatRect.width > refRect.width) {
397
+ const minX = sx + shiftPad;
398
+ const maxX = cw - floatRect.width - shiftPad;
399
+
400
+ if (coords.x < minX) {
401
+ const total = minX - coords.x;
402
+ coords.x = minX;
403
+ p = 50 - (total / floatRect.width) * 100;
404
+ } else if (coords.x > maxX) {
405
+ const total = maxX - coords.x;
406
+ coords.x = Math.max(sx, maxX);
407
+ p = 50 + (total / floatRect.width) * 100;
408
+ }
409
+ }
410
+
411
+ let py = 50;
412
+ if (axis === "y" && (shift || floatRect.height > refRect.height)) {
413
+ const minY = sy + shiftPad;
414
+ const maxY = ch - floatRect.height - shiftPad;
415
+
416
+ if (coords.y < minY) {
417
+ const total = minY - coords.y;
418
+ coords.y = minY;
419
+ py = 50 - (total / floatRect.height) * 100;
420
+ } else if (coords.y > maxY) {
421
+ const total = maxY - coords.y;
422
+ coords.y = Math.max(sy, maxY);
423
+ py = 50 + (total / floatRect.height) * 100;
424
+ }
425
+ }
426
+
427
+ const availableHeight = getAvailableHeight(refRect, side, boundary, distance, shiftPad);
428
+ float.style.setProperty("--arrow-x", `${p}%`);
429
+ float.style.setProperty("--arrow-y", `${py}%`);
430
+ float.style.setProperty("--available-height", `${availableHeight}px`);
431
+ float.dataset.placement = current;
432
+ Object.assign(float.style, {
433
+ left: `${coords.x}px`,
434
+ top: `${coords.y}px`,
435
+ });
436
+ }
437
+
438
+ export function repositionAt(x, y, float, opts = {}) {
439
+ const doc = float.ownerDocument;
440
+ const docEl = doc.documentElement;
441
+ const win = doc.defaultView || window;
442
+ const direction = doc.dir || docEl?.dir || win.getComputedStyle?.(docEl).direction || "";
443
+ const ref = {
444
+ ownerDocument: doc,
445
+ dir: direction,
446
+ matches: (selector) => selector === ":dir(rtl)" && direction === "rtl",
447
+ getClientRects: () => [
448
+ {
449
+ x,
450
+ y,
451
+ left: x,
452
+ top: y,
453
+ right: x,
454
+ bottom: y,
455
+ width: 0,
456
+ height: 0,
457
+ },
458
+ ],
459
+ };
460
+
461
+ reposition(ref, float, opts);
462
+ }