actual-css 0.1.0 → 0.3.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 (215) hide show
  1. package/CHANGELOG.md +90 -1
  2. package/README.md +25 -8
  3. package/dist/actual-themes.min.css +1 -0
  4. package/dist/actual.css +1492 -931
  5. package/dist/actual.js +1 -3
  6. package/dist/actual.min.css +1 -1
  7. package/dist/optional.css +1048 -0
  8. package/dist/optional.min.css +1 -0
  9. package/docs/design-notes/browser-support.md +95 -6
  10. package/docs/design-notes/cascade-layer.md +1 -1
  11. package/docs/design-notes/enhancement-contract.md +101 -0
  12. package/docs/design-notes/forced-colors.md +105 -0
  13. package/docs/design-notes/naming.md +3 -1
  14. package/docs/design-notes/platform-alignment.md +492 -0
  15. package/docs/design-notes/references.md +8 -0
  16. package/docs/design-notes/widget-primitives.md +119 -0
  17. package/docs/navigation.json +135 -0
  18. package/docs/pages/components/accordion.md +80 -0
  19. package/docs/pages/components/alert.md +203 -0
  20. package/docs/pages/components/aura.md +35 -0
  21. package/docs/pages/components/avatar.md +95 -0
  22. package/docs/pages/components/badge.md +102 -0
  23. package/docs/pages/components/breadcrumb.md +19 -0
  24. package/docs/pages/components/button.md +205 -0
  25. package/docs/pages/components/card.md +183 -0
  26. package/docs/pages/components/chat.md +50 -0
  27. package/docs/pages/components/dialog.md +367 -0
  28. package/docs/pages/components/drawer.md +113 -0
  29. package/docs/pages/components/fab.md +118 -0
  30. package/docs/pages/components/key.md +33 -0
  31. package/docs/pages/components/meter.md +23 -0
  32. package/docs/pages/components/navbar.md +92 -0
  33. package/docs/pages/components/pagination.md +55 -0
  34. package/docs/pages/components/progress.md +29 -0
  35. package/docs/pages/components/skeleton.md +38 -0
  36. package/docs/pages/components/spinner.md +93 -0
  37. package/docs/pages/components/table.md +102 -0
  38. package/docs/pages/enhancements/flyout.md +391 -0
  39. package/docs/pages/enhancements/overview.md +36 -0
  40. package/docs/pages/enhancements/scrollspy.md +71 -0
  41. package/docs/pages/enhancements/status-bar.md +72 -0
  42. package/docs/pages/enhancements/tabs.md +80 -0
  43. package/docs/pages/enhancements/tooltip.md +130 -0
  44. package/docs/pages/examples/overview.md +41 -0
  45. package/docs/pages/forms/choice-cards.md +102 -0
  46. package/docs/pages/forms/controls.md +32 -0
  47. package/docs/pages/forms/detached-actions.md +59 -0
  48. package/docs/pages/forms/floating-field.md +88 -0
  49. package/docs/pages/forms/form-actions.md +56 -0
  50. package/docs/pages/forms/input-policy.md +91 -0
  51. package/docs/pages/forms/joined-controls.md +37 -0
  52. package/docs/pages/forms/labels.md +42 -0
  53. package/docs/pages/forms/otp.md +50 -0
  54. package/docs/pages/forms/overview.md +188 -0
  55. package/docs/pages/forms/password.md +40 -0
  56. package/docs/pages/forms/range.md +17 -0
  57. package/docs/pages/forms/states-and-sizes.md +321 -0
  58. package/docs/pages/forms/sticky-actions.md +187 -0
  59. package/docs/pages/forms/switch.md +31 -0
  60. package/docs/pages/forms/validation.md +241 -0
  61. package/docs/{mental-model.md → pages/foundations/philosophy.md} +33 -18
  62. package/docs/pages/foundations/theming.md +92 -0
  63. package/docs/pages/foundations/tokens.md +421 -0
  64. package/docs/{typography.md → pages/foundations/typography.md} +27 -9
  65. package/docs/pages/guides/bootstrap.md +438 -0
  66. package/docs/pages/guides/introduction.md +157 -0
  67. package/docs/pages/guides/optional-components.md +33 -0
  68. package/docs/pages/guides/progressive-enhancement.md +686 -0
  69. package/docs/pages/guides/tailwind.md +455 -0
  70. package/docs/pages/layout/app-shell.md +25 -0
  71. package/docs/pages/layout/center.md +29 -0
  72. package/docs/pages/layout/cluster.md +57 -0
  73. package/docs/pages/layout/frame.md +30 -0
  74. package/docs/pages/layout/grid.md +142 -0
  75. package/docs/pages/layout/list.md +41 -0
  76. package/docs/pages/layout/media.md +21 -0
  77. package/docs/pages/layout/overview.md +37 -0
  78. package/docs/pages/layout/scroll-snap.md +54 -0
  79. package/docs/pages/layout/sidebar-layout.md +27 -0
  80. package/docs/pages/layout/stack.md +18 -0
  81. package/docs/pages/layout/switcher.md +36 -0
  82. package/docs/pages/layout/topbar.md +23 -0
  83. package/docs/pages/patterns/actions.md +52 -0
  84. package/docs/pages/patterns/code-block.md +36 -0
  85. package/docs/pages/patterns/exclusive-accordion.md +16 -0
  86. package/docs/pages/patterns/hierarchical-nav.md +26 -0
  87. package/docs/pages/patterns/icon-input.md +25 -0
  88. package/docs/pages/patterns/intent-nesting.md +3 -0
  89. package/docs/pages/patterns/navigation-list.md +235 -0
  90. package/docs/pages/patterns/overview.md +58 -0
  91. package/docs/pages/patterns/structured-lists.md +47 -0
  92. package/docs/pages/utilities/accessibility.md +34 -0
  93. package/docs/pages/utilities/flex.md +105 -0
  94. package/docs/pages/utilities/links.md +18 -0
  95. package/docs/pages/utilities/list.md +31 -0
  96. package/docs/pages/utilities/overflow.md +43 -0
  97. package/docs/pages/utilities/overview.md +18 -0
  98. package/docs/pages/utilities/shape.md +25 -0
  99. package/docs/pages/utilities/sizing-wrapping.md +115 -0
  100. package/docs/pages/utilities/spacing.md +79 -0
  101. package/docs/pages/utilities/text.md +43 -0
  102. package/llms.txt +10 -4
  103. package/package.json +30 -16
  104. package/src/css/actual.css +12 -1
  105. package/src/css/base.css +3 -9
  106. package/src/css/components/accordion.css +18 -30
  107. package/src/css/components/actions.css +4 -1
  108. package/src/css/components/alert.css +162 -49
  109. package/src/css/components/avatar.css +16 -10
  110. package/src/css/components/badge.css +82 -39
  111. package/src/css/components/breadcrumb.css +7 -15
  112. package/src/css/components/busy.css +7 -0
  113. package/src/css/components/button.css +82 -38
  114. package/src/css/components/card.css +17 -37
  115. package/src/css/components/drawer.css +30 -13
  116. package/src/css/components/flyout.css +39 -110
  117. package/src/css/components/index.css +1 -0
  118. package/src/css/components/join.css +14 -3
  119. package/src/css/components/key.css +2 -0
  120. package/src/css/components/menu.css +125 -0
  121. package/src/css/components/meter.css +0 -23
  122. package/src/css/components/modal.css +162 -7
  123. package/src/css/components/navbar.css +17 -18
  124. package/src/css/components/overline.css +0 -7
  125. package/src/css/components/pagination.css +1 -1
  126. package/src/css/components/progress.css +8 -26
  127. package/src/css/components/scrollspy.css +2 -2
  128. package/src/css/components/skeleton.css +7 -0
  129. package/src/css/components/spinner.css +10 -11
  130. package/src/css/components/status-bar.css +18 -28
  131. package/src/css/components/surface.css +2 -8
  132. package/src/css/components/tab.css +20 -48
  133. package/src/css/components/table.css +7 -5
  134. package/src/css/components/tooltip.css +14 -7
  135. package/src/css/focus.css +16 -3
  136. package/src/css/forms/choice-card.css +13 -15
  137. package/src/css/forms/choice.css +21 -28
  138. package/src/css/forms/control.css +16 -26
  139. package/src/css/forms/custom-select.css +9 -11
  140. package/src/css/forms/form-actions.css +14 -3
  141. package/src/css/forms/form.css +7 -4
  142. package/src/css/forms/index.css +5 -4
  143. package/src/css/forms/input-icon.css +30 -12
  144. package/src/css/forms/native.css +48 -33
  145. package/src/css/forms/range.css +103 -0
  146. package/src/css/forms/select.css +8 -8
  147. package/src/css/forms/switch.css +14 -13
  148. package/src/css/forms/validation.css +32 -5
  149. package/src/css/grid.css +19 -18
  150. package/src/css/intents.css +2 -2
  151. package/src/css/layout.css +29 -1
  152. package/src/css/optional/aura.css +93 -0
  153. package/src/css/optional/chat.css +105 -0
  154. package/src/css/optional/fab.css +104 -0
  155. package/src/css/optional/floating-field.css +87 -0
  156. package/src/css/optional/index.css +5 -0
  157. package/src/css/optional/layout-extra.css +7 -4
  158. package/src/css/optional/otp.css +115 -0
  159. package/src/css/optional/scroll-snap.css +6 -0
  160. package/src/css/optional/scroller.css +19 -7
  161. package/src/css/optional/typography-fluid.css +5 -0
  162. package/src/css/optional/utilities-extra.css +17 -2
  163. package/src/css/prose.css +18 -8
  164. package/src/css/reset.css +0 -10
  165. package/src/css/theme.css +29 -13
  166. package/src/css/themes/cyberpunk.css +1 -1
  167. package/src/css/themes/dim.css +3 -3
  168. package/src/css/themes/edge.css +11 -37
  169. package/src/css/themes/index.css +3 -3
  170. package/src/css/themes/{dark.css → indigo.css} +5 -4
  171. package/src/css/themes/neon.css +5 -5
  172. package/src/css/themes/square.css +0 -3
  173. package/src/css/tokens.css +81 -21
  174. package/src/css/utilities.css +8 -10
  175. package/src/css/variants.css +37 -65
  176. package/src/js/command.js +1 -3
  177. package/src/js/context-menu.js +72 -53
  178. package/src/js/dialog.js +68 -79
  179. package/src/js/dismiss.js +15 -0
  180. package/src/js/enhance.js +76 -19
  181. package/src/js/enhancement-loader.js +301 -0
  182. package/src/js/events.js +3 -0
  183. package/src/js/filter.js +3 -3
  184. package/src/js/floating.js +101 -131
  185. package/src/js/flyout.js +106 -96
  186. package/src/js/focus-group.js +116 -0
  187. package/src/js/focus.js +6 -1
  188. package/src/js/index.js +4 -2
  189. package/src/js/input.js +1 -1
  190. package/src/js/keys.js +27 -0
  191. package/src/js/mask.js +7 -6
  192. package/src/js/menu.js +89 -35
  193. package/src/js/parse-config.js +47 -0
  194. package/src/js/password.js +9 -1
  195. package/src/js/scrollspy.js +133 -62
  196. package/src/js/selectors.js +14 -31
  197. package/src/js/status.js +16 -14
  198. package/src/js/surface.js +219 -125
  199. package/src/js/tab.js +46 -66
  200. package/src/js/tooltip.js +53 -31
  201. package/src/js/validation.js +45 -51
  202. package/dist/actual.js.map +0 -31
  203. package/docs/README.md +0 -26
  204. package/docs/components.md +0 -860
  205. package/docs/design-notes/previous-work.md +0 -5
  206. package/docs/forms.md +0 -1265
  207. package/docs/javascript.md +0 -426
  208. package/docs/layout.md +0 -480
  209. package/docs/patterns.md +0 -415
  210. package/docs/reserved.md +0 -7
  211. package/docs/tokens.md +0 -455
  212. package/docs/ui.md +0 -937
  213. package/docs/utilities.md +0 -384
  214. package/src/css/dialog-fallback.css +0 -56
  215. package/src/js/dialog-fallback.js +0 -163
@@ -1,12 +1,11 @@
1
1
  /*
2
2
  * Compact positioning engine for menus, tooltips, and popovers.
3
3
  *
4
- * Core math ported from Floating UI. Uses a single global scroll/resize
5
- * listener to reposition all registered floating elements via rAF.
4
+ * Core math ported from Floating UI. autoUpdate() batches scroll, resize,
5
+ * and element-resize callbacks per ownerDocument so each document owns its
6
+ * own listener set — no global import-time side effects.
6
7
  */
7
8
 
8
- import { EVENTS } from "./events.js";
9
-
10
9
  /* ── Placement parsing ────────────────────────────────── */
11
10
 
12
11
  function getSide(p) {
@@ -65,7 +64,7 @@ function computeCoords(reference, floating, placement, rtl) {
65
64
  return coords;
66
65
  }
67
66
 
68
- function applyOffset(coords, side, offset, rtl) {
67
+ function applyOffset(coords, side, offset) {
69
68
  switch (side) {
70
69
  case "top":
71
70
  coords.y -= offset;
@@ -74,10 +73,10 @@ function applyOffset(coords, side, offset, rtl) {
74
73
  coords.y += offset;
75
74
  break;
76
75
  case "left":
77
- coords.x += rtl ? offset : -offset;
76
+ coords.x -= offset;
78
77
  break;
79
78
  case "right":
80
- coords.x += rtl ? -offset : offset;
79
+ coords.x += offset;
81
80
  break;
82
81
  }
83
82
  }
@@ -103,7 +102,8 @@ function isRTL(el) {
103
102
  if (el.dir === "rtl") return true;
104
103
  if (el.dir === "ltr") return false;
105
104
  if (supportsDirSelector) return el.matches(":dir(rtl)");
106
- return el.ownerDocument?.dir === "rtl";
105
+ const style = el.ownerDocument?.defaultView?.getComputedStyle?.(el);
106
+ return style?.direction === "rtl";
107
107
  }
108
108
 
109
109
  function getDocEl(doc) {
@@ -134,8 +134,6 @@ function getViewportBoundary(doc) {
134
134
  doc.compatMode === "CSS1Compat"
135
135
  ? toNumber(bodyStyle?.marginLeft) + toNumber(bodyStyle?.marginRight)
136
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
137
  const stableScrollbar = Math.abs(docEl.clientWidth - body.clientWidth - bodyMargin);
140
138
  if (stableScrollbar <= STABLE_SCROLLBAR_MAX_WIDTH) {
141
139
  width -= stableScrollbar;
@@ -181,15 +179,6 @@ function isVisible(el) {
181
179
  return el.getClientRects().length > 0;
182
180
  }
183
181
 
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
182
  function getFloatingSize(float) {
194
183
  const rect = float.getBoundingClientRect();
195
184
  return {
@@ -198,138 +187,120 @@ function getFloatingSize(float) {
198
187
  };
199
188
  }
200
189
 
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
- }
190
+ /* ── Per-document batched callbacks ────────────────────── */
220
191
 
221
- for (const el of tracked) {
222
- el.dispatchEvent(
223
- new CustomEvent(EVENTS.reposition, {
224
- bubbles: false,
225
- detail: { type },
226
- }),
227
- );
228
- }
229
- }
192
+ const trackers = new WeakMap();
230
193
 
231
- function hasOpenSurface() {
232
- return topVisibleTracked() !== null;
233
- }
194
+ function createTracker(doc) {
195
+ const win = doc.defaultView;
196
+ const ResizeObserver = win?.ResizeObserver;
234
197
 
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
- }
198
+ const entries = new Map();
199
+ let resizeObserver = null;
200
+ let tick = false;
201
+ const pending = new Map();
247
202
 
248
- function getResizeObserver() {
249
- if (resizeObserver || typeof window === "undefined" || !window.ResizeObserver) {
203
+ function getResizeObserver() {
204
+ if (resizeObserver || !ResizeObserver) return resizeObserver;
205
+
206
+ resizeObserver = new ResizeObserver((entries, observer) => {
207
+ for (const entry of entries) {
208
+ observer.unobserve(entry.target);
209
+ rafNotify("element-resize", entry);
210
+ win.requestAnimationFrame(() => {
211
+ if (tracker.entries.has(entry.target)) observer.observe(entry.target);
212
+ });
213
+ }
214
+ });
250
215
  return resizeObserver;
251
216
  }
252
217
 
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);
218
+ function rafNotify(type, source) {
219
+ for (const element of entries.keys()) {
220
+ let notifications = pending.get(element);
221
+ if (!notifications) {
222
+ notifications = new Map();
223
+ pending.set(element, notifications);
224
+ }
225
+ let notification = notifications.get(type);
226
+ if (!notification) {
227
+ notification = { targets: new Set(), timeStamp: 0 };
228
+ notifications.set(type, notification);
229
+ }
230
+ if (source?.target) notification.targets.add(source.target);
231
+ notification.timeStamp = Math.max(notification.timeStamp, source?.timeStamp || 0);
232
+ }
233
+ if (!tick) {
234
+ win.requestAnimationFrame(() => {
235
+ const notifications = [...pending];
236
+ pending.clear();
237
+ for (const [element, types] of notifications) {
238
+ const cb = entries.get(element);
239
+ if (!cb || !element.isConnected) continue;
240
+ for (const [pendingType, detail] of types) {
241
+ cb({ type: pendingType, ...detail });
242
+ }
243
+ }
244
+ tick = false;
259
245
  });
260
246
  }
261
- });
262
- return resizeObserver;
263
- }
247
+ tick = true;
248
+ }
264
249
 
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
- }
250
+ doc.addEventListener("scroll", (e) => rafNotify(e.type, e), {
251
+ passive: true,
252
+ capture: true,
274
253
  });
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 });
254
+ win.addEventListener("resize", (e) => rafNotify("resize", e), { passive: true });
255
+
256
+ const tracker = {
257
+ entries,
258
+ add(element, callback) {
259
+ entries.set(element, callback);
260
+ getResizeObserver()?.observe(element);
261
+ return () => {
262
+ entries.delete(element);
263
+ pending.delete(element);
264
+ resizeObserver?.unobserve(element);
265
+ };
266
+ },
267
+ };
268
+
269
+ return tracker;
279
270
  }
280
271
 
281
- /* ── Public API ────────────────────────────────────────── */
272
+ function trackerFor(element) {
273
+ const doc = element.ownerDocument;
274
+ let tracker = trackers.get(doc);
275
+ if (!tracker) {
276
+ tracker = createTracker(doc);
277
+ trackers.set(doc, tracker);
278
+ }
279
+ return tracker;
280
+ }
282
281
 
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
- };
282
+ export function autoUpdate(element, callback) {
283
+ return trackerFor(element).add(element, callback);
294
284
  }
295
285
 
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
- */
286
+ /* ── Positioning ──────────────────────────────────────── */
287
+
309
288
  export function reposition(ref, float, opts = {}) {
310
- if (!isVisible(float)) return;
289
+ if (!isVisible(float)) return false;
311
290
 
312
291
  const placement = opts.placement || "bottom-start";
313
292
  const distance = opts.distance || 0;
314
293
  const flip = opts.flip !== false;
315
294
  const shift = opts.shift !== false;
316
- const shiftPad = opts.shiftPadding || 4;
295
+ const shiftPad = opts.shiftPadding ?? 4;
317
296
  const rtl = isRTL(ref);
318
297
 
319
298
  const rects = ref.getClientRects();
320
299
  const refRect = placement.startsWith("bottom") ? rects[rects.length - 1] : rects[0];
321
- if (!refRect) return;
300
+ if (!refRect) return false;
322
301
 
323
302
  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
- }
303
+ if (isOutsideBoundary(refRect, boundary)) return false;
333
304
 
334
305
  const floatRect = getFloatingSize(float);
335
306
  const sx = boundary.x;
@@ -342,7 +313,7 @@ export function reposition(ref, float, opts = {}) {
342
313
  let axis = getMainAxis(placement);
343
314
  let current = placement;
344
315
  let coords = computeCoords(refRect, floatRect, current, rtl);
345
- applyOffset(coords, side, distance, rtl);
316
+ applyOffset(coords, side, distance);
346
317
 
347
318
  if (flip) {
348
319
  const cx = Math.ceil(coords.x);
@@ -355,11 +326,9 @@ export function reposition(ref, float, opts = {}) {
355
326
  side = flipSide(side);
356
327
  current = align ? `${side}-${align}` : side;
357
328
  coords = computeCoords(refRect, floatRect, current, rtl);
358
- applyOffset(coords, side, distance, rtl);
329
+ applyOffset(coords, side, distance);
359
330
  }
360
331
 
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
332
  if (
364
333
  axis === "y" &&
365
334
  (coords.x < sx || coords.x + floatRect.width > cw) &&
@@ -369,7 +338,7 @@ export function reposition(ref, float, opts = {}) {
369
338
  axis = "x";
370
339
  current = align ? `${side}-${align}` : side;
371
340
  coords = computeCoords(refRect, floatRect, current, rtl);
372
- applyOffset(coords, side, distance, rtl);
341
+ applyOffset(coords, side, distance);
373
342
  }
374
343
  }
375
344
 
@@ -382,7 +351,7 @@ export function reposition(ref, float, opts = {}) {
382
351
  const nextAlign = getAlignment(current) === "end" ? "start" : "end";
383
352
  const candidatePlacement = `${side}-${nextAlign}`;
384
353
  const candidate = computeCoords(refRect, floatRect, candidatePlacement, rtl);
385
- applyOffset(candidate, side, distance, rtl);
354
+ applyOffset(candidate, side, distance);
386
355
 
387
356
  if (getInlineOverflow(candidate, floatRect, minX, maxX) < currentOverflow) {
388
357
  current = candidatePlacement;
@@ -391,7 +360,6 @@ export function reposition(ref, float, opts = {}) {
391
360
  }
392
361
  }
393
362
 
394
- // shift on x axis
395
363
  let p = 50;
396
364
  if (shift || floatRect.width > refRect.width) {
397
365
  const minX = sx + shiftPad;
@@ -433,6 +401,8 @@ export function reposition(ref, float, opts = {}) {
433
401
  left: `${coords.x}px`,
434
402
  top: `${coords.y}px`,
435
403
  });
404
+
405
+ return true;
436
406
  }
437
407
 
438
408
  export function repositionAt(x, y, float, opts = {}) {
@@ -458,5 +428,5 @@ export function repositionAt(x, y, float, opts = {}) {
458
428
  ],
459
429
  };
460
430
 
461
- reposition(ref, float, opts);
431
+ return reposition(ref, float, opts);
462
432
  }
package/src/js/flyout.js CHANGED
@@ -1,58 +1,54 @@
1
- /*
2
- * Flyout trigger — trigger adapter over the shared action-surface runtime.
3
- *
4
- * App menus keep roving focus and menuitem activation. Nav panels reuse the
5
- * same surface lifecycle without menu semantics.
6
- */
7
-
8
- import enhance from "./enhance.js";
1
+ import { registerEnhancement } from "./enhance.js";
9
2
  import { focusFirstDescendant } from "./focus.js";
10
- import { focusFirstMenuItem, focusLastMenuItem, getMenuItems } from "./menu.js";
11
- import { CLASSES } from "./selectors.js";
12
- import {
13
- closeSurface,
14
- disconnectSurface,
15
- isSurfaceOpen,
16
- openSurface,
17
- prepareSurface,
18
- } from "./surface.js";
19
-
20
- // trigger -> { flyout, controller }
3
+ import { connectMenu, focusFirstMenuItem, focusLastMenuItem } from "./menu.js";
4
+ import { closeSurface, isSurfaceOpen, openSurface, retainSurface } from "./surface.js";
5
+
6
+ const panelRefs = new WeakMap();
21
7
  const triggerMap = new WeakMap();
22
- const FLYOUT_SELECTOR = `.${CLASSES.flyout}`;
23
- const FLYOUT_TRIGGER_SELECTOR = "[aria-controls][aria-expanded]";
24
8
 
25
- function openFlyout(flyout, trigger) {
26
- openSurface(flyout, { trigger, source: trigger });
9
+ function normalizeBreakpoint(raw) {
10
+ const BREAKPOINTS = { sm: 640, md: 768, lg: 1024 };
11
+ const value = Number.parseInt(BREAKPOINTS[raw] || raw, 10);
12
+ return Number.isFinite(value) ? value : undefined;
27
13
  }
28
14
 
29
- function flyoutFor(trigger) {
30
- const flyoutId = trigger.getAttribute("aria-controls");
31
- const flyout = flyoutId && trigger.ownerDocument.getElementById(flyoutId);
32
- return flyout?.matches(FLYOUT_SELECTOR) ? flyout : null;
15
+ function readFlyoutOptions(panel) {
16
+ const opts = { autoClose: "true" };
17
+ const ds = panel.dataset;
18
+ if (ds.flyoutPlacement) opts.placement = ds.flyoutPlacement;
19
+ if (ds.flyoutDistance != null) {
20
+ const d = Number.parseFloat(ds.flyoutDistance);
21
+ if (Number.isFinite(d)) opts.distance = d;
22
+ }
23
+ if (ds.flyoutMobile) opts.mobile = ds.flyoutMobile;
24
+ if (ds.flyoutBreakpoint) {
25
+ const bp = normalizeBreakpoint(ds.flyoutBreakpoint);
26
+ if (bp != null) opts.breakpoint = bp;
27
+ }
28
+ if (ds.flyoutAutoClose) opts.autoClose = ds.flyoutAutoClose;
29
+ return opts;
33
30
  }
34
31
 
35
- function isMenuFlyout(flyout) {
36
- return flyout.matches("menu");
32
+ function panelFor(trigger) {
33
+ const id = trigger.getAttribute("aria-controls");
34
+ return id ? trigger.ownerDocument.getElementById(id) : null;
37
35
  }
38
36
 
39
- function resolveCurrentFlyout(trigger, state) {
40
- if (state.flyout?.isConnected) return state.flyout;
41
-
42
- const flyout = flyoutFor(trigger);
43
- if (!flyout) return null;
37
+ function isMenuFlyout(panel) {
38
+ return panel.matches("menu") || panel.classList.contains("menu");
39
+ }
44
40
 
45
- state.flyout = flyout;
46
- prepareSurface(flyout);
47
- if (!trigger.hasAttribute("aria-haspopup") && isMenuFlyout(flyout)) {
48
- trigger.setAttribute("aria-haspopup", "menu");
49
- }
50
- return flyout;
41
+ function openFlyout(panel, trigger) {
42
+ return openSurface(panel, {
43
+ trigger,
44
+ source: trigger,
45
+ ...readFlyoutOptions(panel),
46
+ });
51
47
  }
52
48
 
53
49
  function openAndFocusPanel(flyout, trigger) {
54
- if (!isSurfaceOpen(flyout)) openFlyout(flyout, trigger);
55
- focusFirstDescendant(flyout);
50
+ if (!isSurfaceOpen(flyout) && !openFlyout(flyout, trigger)) return false;
51
+ return focusFirstDescendant(flyout);
56
52
  }
57
53
 
58
54
  function onTriggerClick(e) {
@@ -60,35 +56,34 @@ function onTriggerClick(e) {
60
56
  const state = triggerMap.get(trigger);
61
57
  if (!state) return;
62
58
  e.stopPropagation();
63
- const flyout = resolveCurrentFlyout(trigger, state);
64
- if (!flyout) return;
65
- if (isSurfaceOpen(flyout)) closeSurface(flyout);
66
- else openFlyout(flyout, trigger);
59
+ const panel = resolvePanel(trigger, state);
60
+ if (!panel) return;
61
+ if (isSurfaceOpen(panel)) closeSurface(panel);
62
+ else openFlyout(panel, trigger);
67
63
  }
68
64
 
69
65
  function onTriggerKeydown(e) {
70
66
  const trigger = e.currentTarget;
71
67
  const state = triggerMap.get(trigger);
72
68
  if (!state) return;
73
- const flyout = resolveCurrentFlyout(trigger, state);
74
- if (!flyout) return;
75
- const items = getMenuItems(flyout);
76
- const isActionList = items.length > 0;
69
+ const panel = resolvePanel(trigger, state);
70
+ if (!panel) return;
71
+ const isActionList = isMenuFlyout(panel);
77
72
 
78
73
  if (!isActionList) {
79
74
  switch (e.key) {
80
75
  case "ArrowDown":
81
76
  e.preventDefault();
82
- openAndFocusPanel(flyout, trigger);
77
+ openAndFocusPanel(panel, trigger);
83
78
  break;
84
79
  case "Enter":
85
80
  case " ":
86
81
  e.preventDefault();
87
- if (isSurfaceOpen(flyout)) closeSurface(flyout);
88
- else openAndFocusPanel(flyout, trigger);
82
+ if (isSurfaceOpen(panel)) closeSurface(panel);
83
+ else openAndFocusPanel(panel, trigger);
89
84
  break;
90
85
  case "Tab":
91
- if (isSurfaceOpen(flyout) && !e.shiftKey && focusFirstDescendant(flyout)) {
86
+ if (isSurfaceOpen(panel) && !e.shiftKey && focusFirstDescendant(panel)) {
92
87
  e.preventDefault();
93
88
  }
94
89
  break;
@@ -99,89 +94,104 @@ function onTriggerKeydown(e) {
99
94
  switch (e.key) {
100
95
  case "ArrowDown":
101
96
  e.preventDefault();
102
- if (!isSurfaceOpen(flyout)) openFlyout(flyout, trigger);
103
- focusFirstMenuItem(flyout);
97
+ if (!isSurfaceOpen(panel) && !openFlyout(panel, trigger)) break;
98
+ focusFirstMenuItem(panel);
104
99
  break;
105
100
  case "ArrowUp":
106
101
  e.preventDefault();
107
- if (!isSurfaceOpen(flyout)) openFlyout(flyout, trigger);
108
- focusLastMenuItem(flyout);
102
+ if (!isSurfaceOpen(panel) && !openFlyout(panel, trigger)) break;
103
+ focusLastMenuItem(panel);
109
104
  break;
110
105
  case "Home":
111
106
  e.preventDefault();
112
- if (!isSurfaceOpen(flyout)) openFlyout(flyout, trigger);
113
- focusFirstMenuItem(flyout);
107
+ if (!isSurfaceOpen(panel) && !openFlyout(panel, trigger)) break;
108
+ focusFirstMenuItem(panel);
114
109
  break;
115
110
  case "End":
116
111
  e.preventDefault();
117
- if (!isSurfaceOpen(flyout)) openFlyout(flyout, trigger);
118
- focusLastMenuItem(flyout);
112
+ if (!isSurfaceOpen(panel) && !openFlyout(panel, trigger)) break;
113
+ focusLastMenuItem(panel);
119
114
  break;
120
115
  case "Enter":
121
116
  case " ":
122
117
  e.preventDefault();
123
- if (!isSurfaceOpen(flyout)) {
124
- openFlyout(flyout, trigger);
125
- focusFirstMenuItem(flyout);
118
+ if (!isSurfaceOpen(panel)) {
119
+ if (openFlyout(panel, trigger)) focusFirstMenuItem(panel);
126
120
  } else {
127
- closeSurface(flyout);
121
+ closeSurface(panel);
128
122
  }
129
123
  break;
130
124
  case "Tab":
131
- if (isSurfaceOpen(flyout)) closeSurface(flyout);
125
+ if (isSurfaceOpen(panel)) closeSurface(panel);
132
126
  break;
133
127
  }
134
128
  }
135
129
 
136
130
  function connectTrigger(trigger) {
137
131
  if (triggerMap.has(trigger)) return;
138
- const flyout = flyoutFor(trigger);
139
- if (!flyout) return;
140
132
 
141
133
  const controller = new AbortController();
142
- triggerMap.set(trigger, { flyout, controller });
134
+ const state = { panel: null, controller, releaseMenu: null };
135
+ triggerMap.set(trigger, state);
136
+
137
+ resolvePanel(trigger, state);
143
138
 
144
- prepareSurface(flyout);
145
139
  if (!trigger.hasAttribute("aria-expanded")) {
146
140
  trigger.setAttribute("aria-expanded", "false");
147
141
  }
148
- if (!trigger.hasAttribute("aria-haspopup") && isMenuFlyout(flyout)) {
149
- trigger.setAttribute("aria-haspopup", "menu");
150
- }
151
142
 
152
143
  trigger.addEventListener("click", onTriggerClick, { signal: controller.signal });
153
144
  trigger.addEventListener("keydown", onTriggerKeydown, { signal: controller.signal });
154
145
  }
155
146
 
147
+ function resolvePanel(trigger, state) {
148
+ if (state.panel?.isConnected) return state.panel;
149
+
150
+ const panel = panelFor(trigger);
151
+ if (!panel) return null;
152
+
153
+ let panelEntry = panelRefs.get(panel);
154
+ if (!panelEntry) {
155
+ const release = retainSurface(panel);
156
+ panelEntry = { triggers: new Set(), release };
157
+ panelRefs.set(panel, panelEntry);
158
+ }
159
+ panelEntry.triggers.add(trigger);
160
+ state.panel = panel;
161
+
162
+ if (isMenuFlyout(panel)) {
163
+ state.releaseMenu = connectMenu(panel, {
164
+ close: (menu) => closeSurface(menu),
165
+ });
166
+ }
167
+ if (!trigger.hasAttribute("aria-haspopup") && isMenuFlyout(panel)) {
168
+ trigger.setAttribute("aria-haspopup", "menu");
169
+ }
170
+
171
+ return panel;
172
+ }
173
+
156
174
  function disconnectTrigger(trigger) {
157
175
  const state = triggerMap.get(trigger);
158
176
  if (!state) return;
177
+ state.releaseMenu?.();
159
178
  state.controller.abort();
179
+ if (state.panel?.isConnected && isSurfaceOpen(state.panel)) {
180
+ closeSurface(state.panel, { restoreFocus: false });
181
+ }
160
182
  triggerMap.delete(trigger);
161
- }
162
183
 
163
- function connectFlyout(flyout) {
164
- if (!flyout.id) return;
165
- const triggers = flyout.ownerDocument.querySelectorAll(
166
- `${FLYOUT_TRIGGER_SELECTOR}[aria-controls="${CSS.escape(flyout.id)}"]`,
167
- );
168
- for (const trigger of triggers) {
169
- if (!triggerMap.has(trigger)) connectTrigger(trigger);
184
+ const panelEntry = state.panel ? panelRefs.get(state.panel) : undefined;
185
+ if (panelEntry) {
186
+ panelEntry.triggers.delete(trigger);
187
+ if (panelEntry.triggers.size === 0) {
188
+ panelEntry.release();
189
+ panelRefs.delete(state.panel);
190
+ }
170
191
  }
171
192
  }
172
193
 
173
- function disconnectFlyout(flyout) {
174
- if (!flyout) return;
175
- disconnectSurface(flyout);
176
- }
177
-
178
- enhance({
179
- [FLYOUT_TRIGGER_SELECTOR]: (trigger) => {
180
- connectTrigger(trigger);
181
- return () => disconnectTrigger(trigger);
182
- },
183
- [FLYOUT_SELECTOR]: (flyout) => {
184
- connectFlyout(flyout);
185
- return () => disconnectFlyout(flyout);
186
- },
194
+ registerEnhancement("flyout", (trigger) => {
195
+ connectTrigger(trigger);
196
+ return () => disconnectTrigger(trigger);
187
197
  });