@allxsmith/bestax-bulma 5.59.0 → 5.61.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 (73) hide show
  1. package/dist/bestax.css +1 -1
  2. package/dist/bestax.css.map +1 -1
  3. package/dist/esm/components/Affix.js +46 -0
  4. package/dist/esm/components/Affix.js.map +1 -0
  5. package/dist/esm/components/Carousel.js +3 -43
  6. package/dist/esm/components/Carousel.js.map +1 -1
  7. package/dist/esm/components/Command.js +3 -3
  8. package/dist/esm/components/Dialog.js +2 -6
  9. package/dist/esm/components/Dialog.js.map +1 -1
  10. package/dist/esm/components/Modal.js +12 -24
  11. package/dist/esm/components/Modal.js.map +1 -1
  12. package/dist/esm/components/Navbar.js +3 -1
  13. package/dist/esm/components/Navbar.js.map +1 -1
  14. package/dist/esm/components/Popover.js +7 -27
  15. package/dist/esm/components/Popover.js.map +1 -1
  16. package/dist/esm/components/Sidebar.js +17 -58
  17. package/dist/esm/components/Sidebar.js.map +1 -1
  18. package/dist/esm/form/ColorPicker.js +4 -4
  19. package/dist/esm/form/ColorPicker.js.map +1 -1
  20. package/dist/esm/form/Combobox.js +3 -3
  21. package/dist/esm/helpers/overlayStack.js +33 -1
  22. package/dist/esm/helpers/overlayStack.js.map +1 -1
  23. package/dist/esm/helpers/portal.js +28 -1
  24. package/dist/esm/helpers/portal.js.map +1 -1
  25. package/dist/esm/helpers/useBulmaViewport.js +82 -0
  26. package/dist/esm/helpers/useBulmaViewport.js.map +1 -0
  27. package/dist/esm/helpers/useInertSiblings.js +7 -3
  28. package/dist/esm/helpers/useInertSiblings.js.map +1 -1
  29. package/dist/esm/helpers/useOutsidePress.js +53 -0
  30. package/dist/esm/helpers/useOutsidePress.js.map +1 -0
  31. package/dist/esm/helpers/useOverlayFocus.js +74 -0
  32. package/dist/esm/helpers/useOverlayFocus.js.map +1 -0
  33. package/dist/esm/layout/AppFrame.js +438 -0
  34. package/dist/esm/layout/AppFrame.js.map +1 -0
  35. package/dist/extras.css +1 -1
  36. package/dist/extras.css.map +1 -1
  37. package/dist/index.cjs +754 -160
  38. package/dist/index.cjs.map +1 -1
  39. package/dist/index.esm.js +2 -0
  40. package/dist/index.esm.js.map +1 -1
  41. package/dist/types/components/Affix.d.ts +67 -0
  42. package/dist/types/components/Navbar.d.ts +4 -3
  43. package/dist/types/helpers/overlayStack.d.ts +17 -0
  44. package/dist/types/helpers/portal.d.ts +42 -0
  45. package/dist/types/helpers/useBulmaViewport.d.ts +34 -0
  46. package/dist/types/helpers/useInertSiblings.d.ts +4 -1
  47. package/dist/types/helpers/useOutsidePress.d.ts +24 -0
  48. package/dist/types/helpers/useOverlayFocus.d.ts +44 -0
  49. package/dist/types/index.d.ts +2 -0
  50. package/dist/types/layout/AppFrame.d.ts +306 -0
  51. package/dist/types-cjs/components/Affix.d.ts +67 -0
  52. package/dist/types-cjs/components/Navbar.d.ts +4 -3
  53. package/dist/types-cjs/helpers/overlayStack.d.ts +17 -0
  54. package/dist/types-cjs/helpers/portal.d.ts +42 -0
  55. package/dist/types-cjs/helpers/useBulmaViewport.d.ts +34 -0
  56. package/dist/types-cjs/helpers/useInertSiblings.d.ts +4 -1
  57. package/dist/types-cjs/helpers/useOutsidePress.d.ts +24 -0
  58. package/dist/types-cjs/helpers/useOverlayFocus.d.ts +44 -0
  59. package/dist/types-cjs/index.d.ts +2 -0
  60. package/dist/types-cjs/layout/AppFrame.d.ts +306 -0
  61. package/dist/versions/bestax-no-dark-mode.css +1 -1
  62. package/dist/versions/bestax-no-dark-mode.css.map +1 -1
  63. package/dist/versions/bestax-no-helpers-prefixed.css +1 -1
  64. package/dist/versions/bestax-no-helpers-prefixed.css.map +1 -1
  65. package/dist/versions/bestax-no-helpers.css +1 -1
  66. package/dist/versions/bestax-no-helpers.css.map +1 -1
  67. package/dist/versions/bestax-prefixed.css +1 -1
  68. package/dist/versions/bestax-prefixed.css.map +1 -1
  69. package/package.json +1 -1
  70. package/src/scss/components/_affix.scss +74 -0
  71. package/src/scss/components/_index.scss +1 -0
  72. package/src/scss/layout/_appframe.scss +306 -0
  73. package/src/scss/layout/_index.scss +1 -0
package/dist/index.cjs CHANGED
@@ -4603,6 +4603,33 @@ function usePortalContainer() {
4603
4603
  const { portalContainer } = useConfig();
4604
4604
  return container => resolvePortalContainer(container || portalContainer);
4605
4605
  }
4606
+ /**
4607
+ * Reads Modal's `portal` prop (`boolean | string | HTMLElement`) for a
4608
+ * component that takes it: whether the content is in its portal yet, and
4609
+ * where it goes.
4610
+ *
4611
+ * A portal has no server-rendered counterpart, so such a component renders in
4612
+ * place on the server and while hydrating, and moves into its portal once the
4613
+ * client takes over. Render the content in `Portal` with
4614
+ * `disabled={!isPortaled}` and this `container`. Moving remounts the content,
4615
+ * so an effect that holds one of its nodes depends on `isPortaled`, `portal`
4616
+ * and `defaultTarget`.
4617
+ *
4618
+ * @function usePortalProp
4619
+ * @param portal - The component's `portal` prop.
4620
+ * @returns Whether it's portaled now, its `container`, and the provider's
4621
+ * target while that places it.
4622
+ */
4623
+ function usePortalProp(portal) {
4624
+ const hydrated = useIsHydrated();
4625
+ const { portalContainer } = useConfig();
4626
+ const isPortaled = Boolean(portal) && hydrated;
4627
+ return {
4628
+ isPortaled,
4629
+ container: typeof portal === 'boolean' ? undefined : portal,
4630
+ defaultTarget: isPortaled && portal === true ? portalContainer : undefined,
4631
+ };
4632
+ }
4606
4633
  /**
4607
4634
  * Renders its children into another part of the page, `document.body` unless `container` or a `ConfigProvider` says otherwise, so floating content escapes an ancestor's `overflow`, `transform` or stacking context.
4608
4635
  *
@@ -4728,6 +4755,37 @@ function useOverlayStack(inStack, modal = true) {
4728
4755
  return !above.some(overlay => among !== 'modal' || overlay.modal);
4729
4756
  }, []);
4730
4757
  }
4758
+ /**
4759
+ * Calls `onEscape` for an Escape while `active` and the overlay is the
4760
+ * topmost one, as `isTopmost` (from `useOverlayStack`) says, and marks the key
4761
+ * used with `preventDefault()`, so nothing under it acts on it too.
4762
+ *
4763
+ * It listens on `window`, so everything inside the overlay has had the key
4764
+ * first, and an Escape something there used, such as a popup closing, is
4765
+ * left to it. The listener stays put while `onEscape` changes identity, as an
4766
+ * inline one does on every render of the parent: it reads the latest one as
4767
+ * the key arrives.
4768
+ *
4769
+ * @function useOverlayEscape
4770
+ * @param active - Whether the overlay is open and closes on Escape.
4771
+ * @param isTopmost - The overlay's check from `useOverlayStack`.
4772
+ * @param onEscape - What closing it does.
4773
+ */
4774
+ function useOverlayEscape(active, isTopmost, onEscape) {
4775
+ const onEscapeRef = useLatestRef(onEscape);
4776
+ React.useEffect(() => {
4777
+ if (!active)
4778
+ return undefined;
4779
+ const handleKeyDown = (e) => {
4780
+ if (e.key !== 'Escape' || e.defaultPrevented || !isTopmost())
4781
+ return;
4782
+ e.preventDefault();
4783
+ onEscapeRef.current();
4784
+ };
4785
+ window.addEventListener('keydown', handleKeyDown);
4786
+ return () => window.removeEventListener('keydown', handleKeyDown);
4787
+ }, [active, isTopmost, onEscapeRef]);
4788
+ }
4731
4789
 
4732
4790
  /**
4733
4791
  * Whether `el` is editable while its parent is not, which makes it an editing
@@ -5401,7 +5459,7 @@ const ModalClose = ({ className, size = 'large', variant = 'delete', onClick, ..
5401
5459
  * @see {@link https://bulma.io/documentation/components/modal/ | Bulma Modal documentation}
5402
5460
  */
5403
5461
  const ModalRoot = React.forwardRef(function ModalRoot({ active, isActive, onClose, className, textColor, bgColor, modalCardTitle, modalCardFoot, type, children, closeOnEscape = true, lockScroll = true, portal = false, role, 'aria-modal': ariaModalProp, ...props }, ref) {
5404
- const { classPrefix, portalContainer } = useConfig();
5462
+ const { classPrefix } = useConfig();
5405
5463
  const { bulmaHelperClasses, rest } = useBulmaClasses({
5406
5464
  color: textColor,
5407
5465
  backgroundColor: bgColor,
@@ -5447,20 +5505,15 @@ const ModalRoot = React.forwardRef(function ModalRoot({ active, isActive, onClos
5447
5505
  const previouslyFocusedRef = React.useRef(null);
5448
5506
  const generatedTitleId = React.useId();
5449
5507
  // A portal has no server-rendered counterpart, so the server render and the
5450
- // hydrating render have to stay inline and only move into the portal once
5451
- // we're past hydration.
5452
- const onClient = useIsHydrated();
5453
- const resolveContainer = usePortalContainer();
5454
- // Moving into the portal, or from one portal target to another, remounts
5455
- // the modal's subtree, so any effect holding a DOM node from before the move
5456
- // is holding a detached one. Effects that touch the modal's nodes key on
5457
- // this, on `portal` and on `defaultTarget`, so they re-run against the new
5458
- // tree.
5459
- const isPortaled = Boolean(portal) && onClient;
5460
- // The configured `portalContainer` only places a modal portaled with
5461
- // `portal` set to `true`. In place, or with a target of its own, a change
5462
- // to it moves nothing, so it mustn't re-run the effects either.
5463
- const defaultTarget = isPortaled && portal === true ? portalContainer : undefined;
5508
+ // hydrating render stay inline and only move into the portal once we're
5509
+ // past hydration. Moving into the portal, or from one portal target to
5510
+ // another, remounts the modal's subtree, so any effect holding a DOM node
5511
+ // from before the move is holding a detached one. Effects that touch the
5512
+ // modal's nodes key on `isPortaled`, on `portal` and on `defaultTarget`, so
5513
+ // they re-run against the new tree. `defaultTarget` is the configured
5514
+ // `portalContainer` only while it places the modal (portaled with `portal`
5515
+ // set to `true`), so a change to it that moves nothing re-runs nothing.
5516
+ const { isPortaled, container, defaultTarget } = usePortalProp(portal);
5464
5517
  useScrollLock(isModalActive && lockScroll);
5465
5518
  // Read when Escape arrives, so the key listener stays put while `onClose`
5466
5519
  // changes identity, as an inline one does on every render of the parent.
@@ -5573,11 +5626,7 @@ const ModalRoot = React.forwardRef(function ModalRoot({ active, isActive, onClos
5573
5626
  }
5574
5627
  // In both forms, so a `Modal.Close` anywhere inside closes this modal.
5575
5628
  modalElement = (jsxRuntime.jsx(ModalCloseContext.Provider, { value: onClose, children: modalElement }));
5576
- if (isPortaled) {
5577
- const target = resolveContainer(typeof portal === 'boolean' ? undefined : portal);
5578
- return reactDom.createPortal(jsxRuntime.jsx(LocalePortalScope, { target: target, children: modalElement }), target);
5579
- }
5580
- return modalElement;
5629
+ return (jsxRuntime.jsx(Portal, { disabled: !isPortaled, container: container, children: modalElement }));
5581
5630
  });
5582
5631
  const Modal = withSubComponents(ModalRoot, {
5583
5632
  Background: ModalBackground,
@@ -5662,7 +5711,9 @@ const NavbarBurger = React.forwardRef(function NavbarBurger({ className, active,
5662
5711
  });
5663
5712
  return (jsxRuntime.jsxs("button", { ref: ref, className: classNames(usePrefixedClassNames('navbar-burger', {
5664
5713
  'is-active': active,
5665
- }), bulmaHelperClasses, className), ...rest, "aria-label": rest['aria-label'] || 'menu', "aria-expanded": rest['aria-expanded'] ?? !!active, type: buttonType(rest.type), children: [jsxRuntime.jsx("span", { "aria-hidden": "true" }), jsxRuntime.jsx("span", { "aria-hidden": "true" }), jsxRuntime.jsx("span", { "aria-hidden": "true" }), jsxRuntime.jsx("span", { "aria-hidden": "true" }), children] }));
5714
+ }), bulmaHelperClasses, className), ...rest, "aria-label": rest['aria-label'] || 'menu', "aria-expanded": rest['aria-expanded'] === null
5715
+ ? undefined
5716
+ : (rest['aria-expanded'] ?? !!active), type: buttonType(rest.type), children: [jsxRuntime.jsx("span", { "aria-hidden": "true" }), jsxRuntime.jsx("span", { "aria-hidden": "true" }), jsxRuntime.jsx("span", { "aria-hidden": "true" }), jsxRuntime.jsx("span", { "aria-hidden": "true" }), children] }));
5666
5717
  });
5667
5718
  NavbarBurger.displayName = 'NavbarBurger';
5668
5719
  /**
@@ -6833,16 +6884,20 @@ const above = (el) => el.parentElement ?? el.getRootNode().host;
6833
6884
  * @param active - Whether what's beside it is inert.
6834
6885
  * @param wholePage - Whether that goes for the whole page rather than only
6835
6886
  * the element's siblings.
6887
+ * @param except - Ref to an element beside it to leave reachable too, such
6888
+ * as the overlay's own backdrop, which has to take the presses that close
6889
+ * it rather than let them through to what's under it.
6836
6890
  */
6837
- function useInertSiblings(ref, active, wholePage) {
6891
+ function useInertSiblings(ref, active, wholePage, except) {
6838
6892
  React.useEffect(() => {
6839
6893
  const el = ref.current;
6840
6894
  if (!active || !el)
6841
6895
  return undefined;
6842
6896
  const doc = el.ownerDocument;
6897
+ const keep = except?.current;
6843
6898
  const held = [];
6844
6899
  for (let node = el; node && node !== doc.body; node = wholePage ? above(node) : undefined) {
6845
- held.push(...siblingsOf(node).filter(hold));
6900
+ held.push(...siblingsOf(node).filter(sibling => sibling !== keep && hold(sibling)));
6846
6901
  }
6847
6902
  // Focus this takes away, to hand back later. It's looked up as `el`'s
6848
6903
  // tree sees it, where focus inside a nested shadow root reads as its
@@ -6857,7 +6912,7 @@ function useInertSiblings(ref, active, wholePage) {
6857
6912
  if (doc.activeElement === doc.body)
6858
6913
  taken?.focus();
6859
6914
  };
6860
- }, [ref, active, wholePage]);
6915
+ }, [ref, active, wholePage, except]);
6861
6916
  }
6862
6917
 
6863
6918
  /**
@@ -7953,6 +8008,51 @@ function useAnchoredPosition(anchorRef, panelRef, { active, position, fixed, off
7953
8008
  return resolved;
7954
8009
  }
7955
8010
 
8011
+ // INTERNAL: deliberately not exported from src/index.ts.
8012
+ /**
8013
+ * Calls `onOutsidePress` for a `pointerdown` outside `ref`'s element while
8014
+ * `active`, as a Popover closes on one.
8015
+ *
8016
+ * A press counts as inside in two ways: on the element's own path in the DOM,
8017
+ * and anywhere in a React tree the returned handler is put on, as
8018
+ * `onPointerDownCapture`. React's tree includes content portaled out of it,
8019
+ * which the DOM path doesn't, and React has seen a press there by the time
8020
+ * the document hears it.
8021
+ *
8022
+ * The listener is on the element's own document, an iframe's when the tree
8023
+ * renders into one, and reads the latest `onOutsidePress` as a press
8024
+ * arrives, so it stays put while the callback changes identity.
8025
+ *
8026
+ * @function useOutsidePress
8027
+ * @param ref - Ref to the element a press inside of doesn't count.
8028
+ * @param active - Whether to listen.
8029
+ * @param onOutsidePress - Called with each press outside.
8030
+ * @returns The `onPointerDownCapture` handler that marks a press in its tree
8031
+ * as inside.
8032
+ */
8033
+ function useOutsidePress(ref, active, onOutsidePress) {
8034
+ // The last pointerdown React saw inside.
8035
+ const insidePressRef = React.useRef(null);
8036
+ const onOutsidePressRef = useLatestRef(onOutsidePress);
8037
+ React.useEffect(() => {
8038
+ const element = ref.current;
8039
+ if (!active || !element)
8040
+ return undefined;
8041
+ const handlePointerDown = (e) => {
8042
+ if (e === insidePressRef.current || isEventInside(e, ref.current)) {
8043
+ return;
8044
+ }
8045
+ onOutsidePressRef.current(e);
8046
+ };
8047
+ const doc = element.ownerDocument;
8048
+ doc.addEventListener('pointerdown', handlePointerDown);
8049
+ return () => doc.removeEventListener('pointerdown', handlePointerDown);
8050
+ }, [ref, active, onOutsidePressRef]);
8051
+ return React.useCallback((event) => {
8052
+ insidePressRef.current = event.nativeEvent;
8053
+ }, []);
8054
+ }
8055
+
7956
8056
  const PopoverContext = React.createContext(null);
7957
8057
  /** Intrinsic elements that are controls already, and keep their own role. */
7958
8058
  const NATIVE_CONTROLS = new Set([
@@ -8003,9 +8103,6 @@ const PopoverComponent = ({ trigger, children, open: controlledOpen, defaultOpen
8003
8103
  const { bulmaHelperClasses, rest } = useBulmaClasses(props);
8004
8104
  const rootRef = React.useRef(null);
8005
8105
  const panelRef = React.useRef(null);
8006
- // The last pointerdown React saw inside the panel. React's tree includes
8007
- // content the panel portals elsewhere, which the DOM path doesn't.
8008
- const insidePointerRef = React.useRef(null);
8009
8106
  const baseId = React.useId();
8010
8107
  const panelId = `${baseId}-panel`;
8011
8108
  const titleId = `${baseId}-title`;
@@ -8061,25 +8158,10 @@ const PopoverComponent = ({ trigger, children, open: controlledOpen, defaultOpen
8061
8158
  }
8062
8159
  };
8063
8160
  }, [panelMounted, trapFocus, triggerNode]);
8064
- // Read when a press lands outside, so the listener stays put while `close`
8065
- // changes identity, which it does with `onOpenChange`, and an inline one
8066
- // changes on every render of the parent.
8067
- const closeRef = useLatestRef(close);
8068
- React.useEffect(() => {
8069
- if (!panelMounted || !closeOnClickOutside)
8070
- return undefined;
8071
- const handlePointerDown = (e) => {
8072
- if (e === insidePointerRef.current || isEventInside(e, rootRef.current)) {
8073
- return;
8074
- }
8075
- closeRef.current();
8076
- };
8077
- // The wrapper is on the page whenever the panel is. Its document, an
8078
- // iframe's when the tree renders into one, is where presses land.
8079
- const doc = rootRef.current.ownerDocument;
8080
- doc.addEventListener('pointerdown', handlePointerDown);
8081
- return () => doc.removeEventListener('pointerdown', handlePointerDown);
8082
- }, [panelMounted, closeOnClickOutside, closeRef]);
8161
+ // A press outside the wrapper, which holds the trigger and the panel,
8162
+ // closes it. One inside the panel's React tree counts as inside, so content
8163
+ // the panel portals elsewhere does too.
8164
+ const markInsidePress = useOutsidePress(rootRef, panelMounted && closeOnClickOutside, close);
8083
8165
  const handlePanelKeyDown = (e) => {
8084
8166
  if (e.key !== 'Escape' || !closeOnEscape || e.defaultPrevented)
8085
8167
  return;
@@ -8145,9 +8227,7 @@ const PopoverComponent = ({ trigger, children, open: controlledOpen, defaultOpen
8145
8227
  top: resolved.top,
8146
8228
  left: resolved.left,
8147
8229
  }
8148
- : undefined, onKeyDown: handlePanelKeyDown, onPointerDownCapture: e => {
8149
- insidePointerRef.current = e.nativeEvent;
8150
- }, children: children }) }))] }));
8230
+ : undefined, onKeyDown: handlePanelKeyDown, onPointerDownCapture: markInsidePress, children: children }) }))] }));
8151
8231
  };
8152
8232
  /**
8153
8233
  * The open panel. It mounts with each opening, so it counts the
@@ -9275,6 +9355,69 @@ const TimelineRoot = React.forwardRef(({ className, children, active, pending, a
9275
9355
  });
9276
9356
  const Timeline = withSubComponents(TimelineRoot, { Item: TimelineItem }, 'Timeline');
9277
9357
 
9358
+ // INTERNAL: deliberately not exported from src/index.ts.
9359
+ /**
9360
+ * Moves focus into an overlay as it opens, to the first stop Tab wraps to
9361
+ * inside `ref`'s element or the element itself, and hands it back to
9362
+ * whatever had it as it closes, as `Modal` does. Focus goes back only while
9363
+ * the overlay still holds it, or it's been dropped to `<body>`: closing one
9364
+ * under another overlay mustn't pull focus out of the one on top, and focus
9365
+ * moved out to the page stays there. The opener is found wherever it sits,
9366
+ * inside a shadow root too, in the overlay's own document, an iframe's when
9367
+ * it renders into one. It's read in a layout effect as the overlay opens, so
9368
+ * before the commit's passive effects, such as one that makes the page
9369
+ * inert. Content inside the overlay with a layout effect of its own, or an
9370
+ * `autoFocus`, can still move focus first, and then there's no opener to go
9371
+ * back to: focus is never handed back into the overlay that's closing.
9372
+ *
9373
+ * @function useOverlayFocus
9374
+ * @param ref - Ref to the overlay's element.
9375
+ * @param open - Whether the overlay is open.
9376
+ * @param options - Whether it's modal, what remounts it, and whether a
9377
+ * close hands focus back.
9378
+ */
9379
+ function useOverlayFocus(ref, open, { modal, remountKey, restore }) {
9380
+ const openerRef = React.useRef(null);
9381
+ const restoreRef = useLatestRef(restore);
9382
+ // The element that had focus, wherever it sits: a portaled overlay renders
9383
+ // under `document.body` while its opener can be inside a shadow root.
9384
+ useClientLayoutEffect(() => {
9385
+ if (!open)
9386
+ return;
9387
+ const element = ref.current;
9388
+ /* istanbul ignore if: as below, the ref is set while the overlay is open */
9389
+ if (!element)
9390
+ return;
9391
+ // Focus already inside, where an `autoFocus` in the overlay puts it, has
9392
+ // no opener to go back to.
9393
+ openerRef.current = element.contains(getActiveElementInTree(element))
9394
+ ? null
9395
+ : getDeepestActiveElement(element.ownerDocument);
9396
+ }, [open, ref]);
9397
+ React.useEffect(() => {
9398
+ if (!open)
9399
+ return undefined;
9400
+ const element = ref.current;
9401
+ /* istanbul ignore if: the overlay renders whenever it's open, so its ref is set before this runs */
9402
+ if (!element)
9403
+ return undefined;
9404
+ if (modal || !isModalOverlayOpen()) {
9405
+ focusTabStop(element);
9406
+ }
9407
+ return () => {
9408
+ // A removed subtree leaves focus on <body>, which still counts as held.
9409
+ const active = getActiveElementInTree(element);
9410
+ const held = active === element.ownerDocument.body || element.contains(active);
9411
+ // Asked now rather than as it opened: it's about this close.
9412
+ // eslint-disable-next-line react-hooks/exhaustive-deps
9413
+ if (held && (restoreRef.current?.() ?? true)) {
9414
+ openerRef.current?.focus();
9415
+ }
9416
+ };
9417
+ // eslint-disable-next-line react-hooks/exhaustive-deps -- `modal` is read as the overlay opens, on purpose
9418
+ }, [open, remountKey, ref, restoreRef]);
9419
+ }
9420
+
9278
9421
  /**
9279
9422
  * The `Sidebar` component provides a slide-out navigation panel that appears from the left or right side of the screen.
9280
9423
  *
@@ -9324,31 +9467,15 @@ const SidebarComponent = React.forwardRef(({ isOpen, onClose, position = 'left',
9324
9467
  handleClose();
9325
9468
  }
9326
9469
  }, [overlayClose, handleClose]);
9327
- // Read when Escape arrives, so the key listener stays put while
9328
- // `onClose` changes identity, as an inline one does on every render of
9329
- // the parent.
9330
- const handleCloseRef = useLatestRef(handleClose);
9331
9470
  // Joins the stack of open overlays, so the sidebar answers Escape only
9332
9471
  // while it's the topmost overlay, and keeps it from those under it. It's
9333
9472
  // modal with an overlay: without one it leaves Tab to a modal overlay
9334
9473
  // under it.
9335
9474
  const isTopmost = useOverlayStack(isOpen, overlay);
9336
9475
  const closesOnEscape = escapeClose && canCancel && Boolean(onClose);
9337
- // Handle escape key. On `window`, as in `Modal`, so everything inside the
9338
- // sidebar has had the key first, and an Escape it used is left to it. One
9339
- // the sidebar closes on is marked used, so nothing under it acts on it.
9340
- React.useEffect(() => {
9341
- if (!isOpen || !closesOnEscape)
9342
- return;
9343
- const handleKeyDown = (e) => {
9344
- if (e.key !== 'Escape' || e.defaultPrevented || !isTopmost())
9345
- return;
9346
- e.preventDefault();
9347
- handleCloseRef.current();
9348
- };
9349
- window.addEventListener('keydown', handleKeyDown);
9350
- return () => window.removeEventListener('keydown', handleKeyDown);
9351
- }, [isOpen, closesOnEscape, handleCloseRef, isTopmost]);
9476
+ // Escape closes it, on `window`, as in `Modal`, so everything inside the
9477
+ // sidebar has had the key first, and an Escape it used is left to it.
9478
+ useOverlayEscape(isOpen && closesOnEscape, isTopmost, handleClose);
9352
9479
  // Keep Tab inside while there's an overlay, as `Modal` does:
9353
9480
  // `aria-modal` hides the rest of the page from assistive technology, so
9354
9481
  // the keyboard order has to agree. Only while it's the topmost modal
@@ -9371,41 +9498,17 @@ const SidebarComponent = React.forwardRef(({ isOpen, onClose, position = 'left',
9371
9498
  useScrollLock(isOpen && overlay);
9372
9499
  // Move focus into the sidebar on open, to the first stop Tab wraps to or
9373
9500
  // the sidebar itself, and hand it back on close, as `Modal` does. A
9374
- // different `defaultTarget` remounts the portaled sidebar: the cleanup
9375
- // hands focus back from the old nodes, which left it on <body>, and the
9376
- // re-run records the same opener and focuses the new nodes. Whether it
9377
- // takes focus is decided as it opens, so `overlay` changing while it's
9378
- // open leaves focus where the user has it.
9379
- React.useEffect(() => {
9380
- if (!isOpen)
9381
- return undefined;
9382
- const element = sidebarRef.current;
9383
- /* istanbul ignore if: the aside renders whenever the sidebar is open, so its ref is set before this runs */
9384
- if (!element)
9385
- return undefined;
9386
- // The element that had focus, wherever it sits: a portaled sidebar
9387
- // renders under `document.body` while its opener can be inside a
9388
- // shadow root.
9389
- const opener = getDeepestActiveElement();
9390
- // Without an overlay the sidebar holds no Tab, so with a modal overlay
9391
- // open under it, the first Tab would take focus back there and leave
9392
- // the sidebar's other controls out of reach. It leaves focus alone
9393
- // then, and the cleanup below finds focus outside and leaves it too.
9394
- if (overlay || !isModalOverlayOpen()) {
9395
- focusTabStop(element);
9396
- }
9397
- return () => {
9398
- // Only while the sidebar still holds focus: closing one under another
9399
- // overlay mustn't pull focus out of the one on top, and focus moved
9400
- // out to the page stays there. A removed subtree leaves focus on
9401
- // <body>, which still counts as held.
9402
- const active = getActiveElementInTree(element);
9403
- if (active === document.body || element.contains(active)) {
9404
- opener?.focus();
9405
- }
9406
- };
9407
- // eslint-disable-next-line react-hooks/exhaustive-deps -- `overlay` is read as the sidebar opens, on purpose
9408
- }, [isOpen, defaultTarget]);
9501
+ // different `defaultTarget` remounts the portaled sidebar: focus is handed
9502
+ // back from the old nodes and moved into the new ones. Whether it takes
9503
+ // focus is decided as it opens, so `overlay` changing while it's open
9504
+ // leaves focus where the user has it. Without an overlay the sidebar holds
9505
+ // no Tab, so with a modal overlay open under it, it leaves focus alone:
9506
+ // the first Tab would take focus back there and leave the sidebar's other
9507
+ // controls out of reach.
9508
+ useOverlayFocus(sidebarRef, isOpen, {
9509
+ modal: overlay,
9510
+ remountKey: defaultTarget,
9511
+ });
9409
9512
  // Use combined ref
9410
9513
  const combinedRef = React.useCallback((node) => {
9411
9514
  sidebarRef.current =
@@ -9967,10 +10070,7 @@ const Dialog = React.forwardRef(({ isOpen, title, message, type = 'default', con
9967
10070
  // the Modal's subtree, these buttons included, so the focus effect below
9968
10071
  // has to re-run against the new nodes, exactly as the Modal's own focus
9969
10072
  // effect does, on the same inputs.
9970
- const isHydrated = useIsHydrated();
9971
- const isPortaled = Boolean(portal) && isHydrated;
9972
- const { portalContainer } = useConfig();
9973
- const defaultTarget = isPortaled && portal === true ? portalContainer : undefined;
10073
+ const { isPortaled, defaultTarget } = usePortalProp(portal);
9974
10074
  // Focus management — Dialog picks a specific button rather than the
9975
10075
  // Modal's default (first focusable/root); this effect runs after the
9976
10076
  // inner Modal's own focus-on-open effect, so it wins.
@@ -10132,6 +10232,81 @@ const DialogContainer = () => {
10132
10232
  return (jsxRuntime.jsx(Dialog, { isOpen: true, title: options.title, message: options.message, type: options.type, confirmText: options.confirmText, cancelText: options.cancelText, icon: options.icon, showCancel: type === 'confirm', focusCancel: options.focusCancel, onConfirm: () => dialog.close(type === 'confirm' ? true : undefined), onCancel: () => dialog.close(type === 'confirm' ? false : undefined) }));
10133
10233
  };
10134
10234
 
10235
+ // INTERNAL: deliberately not exported from src/index.ts.
10236
+ /**
10237
+ * The media queries Bulma's `fullhd`, `widescreen`, `desktop` and `tablet`
10238
+ * mixins open with, widest first, at Bulma's default breakpoints. A
10239
+ * stylesheet lays a component out with those mixins; these tell its script
10240
+ * which layout that is. A theme that moves Bulma's breakpoints moves the
10241
+ * stylesheet's switch but not these. Narrower than the last one is `mobile`.
10242
+ */
10243
+ const VIEWPORT_QUERIES = [
10244
+ 'screen and (min-width: 1408px)',
10245
+ 'screen and (min-width: 1216px)',
10246
+ 'screen and (min-width: 1024px)',
10247
+ 'screen and (min-width: 769px), print',
10248
+ ];
10249
+ /** `useBulmaViewport`'s answer below the tablet breakpoint. */
10250
+ const MOBILE = VIEWPORT_QUERIES.length;
10251
+ /** Where Bulma's `widescreen`, `desktop` and `tablet` sit in `VIEWPORT_QUERIES`. */
10252
+ const VIEWPORT_INDEX = {
10253
+ widescreen: 1,
10254
+ desktop: 2,
10255
+ tablet: 3,
10256
+ };
10257
+ /**
10258
+ * Which of Bulma's viewports the window is in: the index of the widest
10259
+ * `VIEWPORT_QUERIES` entry that matches, `MOBILE` below them all, or `-1`
10260
+ * when that isn't known (on the server, before the first layout effect, and
10261
+ * whenever `enabled` is off, so a caller that needs no answer subscribes to
10262
+ * nothing). It's read in a layout effect, so the first paint after hydration
10263
+ * already has it.
10264
+ *
10265
+ * @function useBulmaViewport
10266
+ * @param enabled - Whether the caller needs the viewport at all.
10267
+ * @returns The viewport, as described above.
10268
+ */
10269
+ function useBulmaViewport(enabled) {
10270
+ const [viewport, setViewport] = React.useState(-1);
10271
+ useClientLayoutEffect(() => {
10272
+ if (!enabled || typeof window.matchMedia !== 'function')
10273
+ return undefined;
10274
+ const lists = VIEWPORT_QUERIES.map(query => window.matchMedia(query));
10275
+ const update = () => {
10276
+ const index = lists.findIndex(list => list.matches);
10277
+ setViewport(index < 0 ? MOBILE : index);
10278
+ };
10279
+ update();
10280
+ lists.forEach(list => list.addEventListener('change', update));
10281
+ return () => lists.forEach(list => list.removeEventListener('change', update));
10282
+ }, [enabled]);
10283
+ return enabled ? viewport : -1;
10284
+ }
10285
+ /**
10286
+ * Whether the window is at or past one of Bulma's breakpoints, at its default
10287
+ * width, through the one media query Bulma's mixin for it opens with, so a
10288
+ * caller re-renders only when the window crosses that breakpoint. `null`
10289
+ * until it's known: on the server, and before the first layout effect. Where
10290
+ * `matchMedia` isn't there, it's `true`, the wide layout a server renders.
10291
+ *
10292
+ * @function useBulmaBreakpoint
10293
+ * @param breakpoint - The breakpoint: `tablet`, `desktop` or `widescreen`.
10294
+ * @returns Whether the window is at or past it, or `null`.
10295
+ */
10296
+ function useBulmaBreakpoint(breakpoint) {
10297
+ const [atOrPast, setAtOrPast] = React.useState(null);
10298
+ useClientLayoutEffect(() => {
10299
+ const list = typeof window.matchMedia === 'function'
10300
+ ? window.matchMedia(VIEWPORT_QUERIES[VIEWPORT_INDEX[breakpoint]])
10301
+ : null;
10302
+ const update = () => setAtOrPast(list ? list.matches : true);
10303
+ update();
10304
+ list?.addEventListener('change', update);
10305
+ return () => list?.removeEventListener('change', update);
10306
+ }, [breakpoint]);
10307
+ return atOrPast;
10308
+ }
10309
+
10135
10310
  /**
10136
10311
  * Individual carousel item/slide.
10137
10312
  *
@@ -10159,24 +10334,10 @@ const DefaultPrevIcon = () => (jsxRuntime.jsx("svg", { xmlns: "http://www.w3.org
10159
10334
  * @returns {JSX.Element} The rendered SVG chevron-right icon.
10160
10335
  */
10161
10336
  const DefaultNextIcon = () => (jsxRuntime.jsx("svg", { xmlns: "http://www.w3.org/2000/svg", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", children: jsxRuntime.jsx("polyline", { points: "9 18 15 12 9 6" }) }));
10162
- /**
10163
- * The media queries Bulma's `fullhd`, `widescreen`, `desktop` and `tablet`
10164
- * mixins open with, widest first, at Bulma's default breakpoints. The
10165
- * stylesheet picks the count in view with those mixins; these tell the
10166
- * script which count that is. Narrower than the last one is `mobile`.
10167
- */
10168
- const VIEWPORT_QUERIES = [
10169
- 'screen and (min-width: 1408px)',
10170
- 'screen and (min-width: 1216px)',
10171
- 'screen and (min-width: 1024px)',
10172
- 'screen and (min-width: 769px), print',
10173
- ];
10174
- /** `useBulmaViewport`'s answer below the tablet breakpoint. */
10175
- const MOBILE = VIEWPORT_QUERIES.length;
10176
10337
  /**
10177
10338
  * The `--bulma-carousel-items-to-show*` variable each count prop writes, in
10178
- * the order the component lists them: the viewports as above, mobile, and
10179
- * last `itemsToShow` itself.
10339
+ * the order the component lists them: `useBulmaViewport`'s viewports, widest
10340
+ * first, then mobile, and last `itemsToShow` itself.
10180
10341
  */
10181
10342
  const COUNT_SUFFIXES = [
10182
10343
  '-fullhd',
@@ -10186,33 +10347,6 @@ const COUNT_SUFFIXES = [
10186
10347
  '-mobile',
10187
10348
  '',
10188
10349
  ];
10189
- /**
10190
- * Which of Bulma's viewports the window is in: the index of the widest
10191
- * `VIEWPORT_QUERIES` entry that matches, `MOBILE` below them all, or `-1`
10192
- * when that isn't known (on the server, before the first layout effect, and
10193
- * whenever `enabled` is off, so a carousel with one count everywhere
10194
- * subscribes to nothing).
10195
- *
10196
- * @function
10197
- * @param {boolean} enabled - Whether any viewport-specific count is set.
10198
- * @returns {number} The viewport, as described above.
10199
- */
10200
- function useBulmaViewport(enabled) {
10201
- const [viewport, setViewport] = React.useState(-1);
10202
- useClientLayoutEffect(() => {
10203
- if (!enabled || typeof window.matchMedia !== 'function')
10204
- return undefined;
10205
- const lists = VIEWPORT_QUERIES.map(query => window.matchMedia(query));
10206
- const update = () => {
10207
- const index = lists.findIndex(list => list.matches);
10208
- setViewport(index < 0 ? MOBILE : index);
10209
- };
10210
- update();
10211
- lists.forEach(list => list.addEventListener('change', update));
10212
- return () => lists.forEach(list => list.removeEventListener('change', update));
10213
- }, [enabled]);
10214
- return enabled ? viewport : -1;
10215
- }
10216
10350
  /**
10217
10351
  * A count prop as the whole number the carousel works with: rounded down,
10218
10352
  * and never below 1, so a zero or a stray `NaN` can't divide by nothing.
@@ -13110,7 +13244,7 @@ const standalone$2 = {
13110
13244
  const CommandContext = React.createContext(null);
13111
13245
  const useCommand = () => React.useContext(CommandContext) ?? standalone$2;
13112
13246
  /** Hands a node to a forwarded ref and to one of the component's own. */
13113
- function setRefs$2(node, own, forwarded) {
13247
+ function setRefs$3(node, own, forwarded) {
13114
13248
  own.current = node;
13115
13249
  if (typeof forwarded === 'function')
13116
13250
  forwarded(node);
@@ -13227,7 +13361,7 @@ const CommandParts = React.forwardRef(({ children, label, query: controlledQuery
13227
13361
  registerHandler,
13228
13362
  };
13229
13363
  const { rootRef } = engine;
13230
- const rootRefs = React.useCallback((node) => setRefs$2(node, rootRef, ref), [rootRef, ref]);
13364
+ const rootRefs = React.useCallback((node) => setRefs$3(node, rootRef, ref), [rootRef, ref]);
13231
13365
  return (jsxRuntime.jsxs("div", { className: classNames(rootClass, bulmaHelperClasses, className), ...rest, ref: rootRefs, children: [jsxRuntime.jsx(CommandContext.Provider, { value: context, children: children }), jsxRuntime.jsx(StatusRegion, { items: announced, describe: describeEmpty })] }));
13232
13366
  });
13233
13367
  CommandParts.displayName = 'CommandParts';
@@ -13247,7 +13381,7 @@ const CommandInput = React.forwardRef(({ isLoading, iconLeft, iconLeftName, onKe
13247
13381
  return () => reportLoading(false);
13248
13382
  }, [reportLoading, isLoading]);
13249
13383
  const { inputRef } = engine;
13250
- const inputRefs = React.useCallback((node) => setRefs$2(node, inputRef, ref), [inputRef, ref]);
13384
+ const inputRefs = React.useCallback((node) => setRefs$3(node, inputRef, ref), [inputRef, ref]);
13251
13385
  const { expanded, highlighted } = engine;
13252
13386
  // A name the caller gave the field wins over the Command's `label`.
13253
13387
  const callerNamed = !!props['aria-label'] || 'aria-labelledby' in props;
@@ -14624,6 +14758,42 @@ const DataTable = React.forwardRef(function DataTableRender({ columns, data, tab
14624
14758
  });
14625
14759
  DataTable.displayName = 'DataTable';
14626
14760
 
14761
+ /**
14762
+ * The `Affix` component keeps its content fixed to a corner of the viewport while the page scrolls under it.
14763
+ *
14764
+ * It's a `<div>` with no role, so what's inside keeps its own. It sits
14765
+ * `--bulma-affix-offset-vertical` and `--bulma-affix-offset-horizontal`
14766
+ * from its corner's edges, plus the device's safe-area inset, and stacks at
14767
+ * `--bulma-affix-z-index`, over the page's content and under its navbar,
14768
+ * static or fixed, its menus, popovers, the Sidebar, the modals, Tooltip and
14769
+ * Toast. It's as wide as its
14770
+ * content and never wider than the viewport less its offsets. It doesn't
14771
+ * scroll, so nothing inside it is clipped, and content that can be taller
14772
+ * than the screen scrolls itself. Its place in the reading and Tab order is
14773
+ * its place in the DOM, whatever corner it shows in.
14774
+ *
14775
+ * @function
14776
+ * @param {AffixProps} props - Props for the Affix component.
14777
+ * @param {React.Ref<HTMLDivElement>} ref - Forwarded ref to the `<div>`.
14778
+ * @returns The `<div>`, in place or in its portal.
14779
+ *
14780
+ * @example
14781
+ * <Affix position="bottom-left">
14782
+ * <Button color="primary">Feedback</Button>
14783
+ * </Affix>
14784
+ */
14785
+ const Affix = React.forwardRef(({ position = 'bottom-right', portal = false, className, children, ...props }, ref) => {
14786
+ const { bulmaHelperClasses, rest } = useBulmaClasses(props);
14787
+ const affixClasses = usePrefixedClassNames('affix', {
14788
+ [`is-${position}`]: true,
14789
+ });
14790
+ // In place on the server and while hydrating, then into the portal, as
14791
+ // Modal does. The move remounts the `<div>`.
14792
+ const { isPortaled, container } = usePortalProp(portal);
14793
+ return (jsxRuntime.jsx(Portal, { disabled: !isPortaled, container: container, children: jsxRuntime.jsx("div", { ref: ref, className: classNames(affixClasses, bulmaHelperClasses, className), ...rest, children: children }) }));
14794
+ });
14795
+ Affix.displayName = 'Affix';
14796
+
14627
14797
  /**
14628
14798
  * The `Block` component renders a simple container with Bulma's `.block` class, adding vertical margin between sections of content.
14629
14799
  *
@@ -18740,7 +18910,7 @@ const standalone$1 = {
18740
18910
  const ComboboxContext = React.createContext(null);
18741
18911
  const useCombobox = () => React.useContext(ComboboxContext) ?? standalone$1;
18742
18912
  /** Hands a node to a forwarded ref and to one of the component's own. */
18743
- function setRefs$1(node, own, forwarded) {
18913
+ function setRefs$2(node, own, forwarded) {
18744
18914
  own.current = node;
18745
18915
  if (typeof forwarded === 'function')
18746
18916
  forwarded(node);
@@ -18872,7 +19042,7 @@ const ComboboxParts = React.forwardRef(({ children, below, onSettle, open, defau
18872
19042
  appendToBody,
18873
19043
  };
18874
19044
  const { rootRef } = engine;
18875
- const rootRefs = React.useCallback((node) => setRefs$1(node, rootRef, ref), [rootRef, ref]);
19045
+ const rootRefs = React.useCallback((node) => setRefs$2(node, rootRef, ref), [rootRef, ref]);
18876
19046
  const comboboxElement = (jsxRuntime.jsxs("div", { className: classNames(rootClass, bulmaHelperClasses, className), ...rest, ref: rootRefs, children: [jsxRuntime.jsx(ComboboxContext.Provider, { value: context, children: children }), jsxRuntime.jsx(StatusRegion, { items: announced, describe: describeEmpty })] }));
18877
19047
  const fieldElement = below === undefined ? (comboboxElement) : (jsxRuntime.jsxs("div", { children: [comboboxElement, jsxRuntime.jsx("div", { onPointerDownCapture: engine.markInside, onMouseDown: engine.keepFocus, children: below })] }));
18878
19048
  const messageEl = message ? jsxRuntime.jsx("p", { className: helpClass, children: message }) : null;
@@ -18905,7 +19075,7 @@ const ComboboxInput = React.forwardRef(({ id, isLoading, controlSize, onKeyDown,
18905
19075
  return () => reportLoading(false);
18906
19076
  }, [reportLoading, isLoading]);
18907
19077
  const { inputRef } = engine;
18908
- const inputRefs = React.useCallback((node) => setRefs$1(node, inputRef, ref), [inputRef, ref]);
19078
+ const inputRefs = React.useCallback((node) => setRefs$2(node, inputRef, ref), [inputRef, ref]);
18909
19079
  const { expanded, highlighted } = engine;
18910
19080
  // An `id` of the field's own reaches the label from a layout effect, so
18911
19081
  // in server markup, and the first render, the label's `for` still names
@@ -20202,7 +20372,7 @@ const rgbChannels = (color) => {
20202
20372
  /** Whether a press should start a drag: any touch or pen, a mouse's main button. */
20203
20373
  const startsDrag = (event) => event.pointerType !== 'mouse' || event.button === 0;
20204
20374
  /** Hands a node to a forwarded ref and to one of the part's own. */
20205
- function setRefs(node, own, forwarded) {
20375
+ function setRefs$1(node, own, forwarded) {
20206
20376
  own.current = node;
20207
20377
  if (typeof forwarded === 'function')
20208
20378
  forwarded(node);
@@ -20334,7 +20504,7 @@ const ColorPickerArea = React.forwardRef(({ className, style, ...props }, ref) =
20334
20504
  const hiddenOf = (which) => touch || keyed || axis === which ? undefined : true;
20335
20505
  if (!picker)
20336
20506
  return null;
20337
- return (jsxRuntime.jsx("div", { ref: node => setRefs(node, rootRef, ref), className: classNames(areaClass, bulmaHelperClasses, className), style: {
20507
+ return (jsxRuntime.jsx("div", { ref: node => setRefs$1(node, rootRef, ref), className: classNames(areaClass, bulmaHelperClasses, className), style: {
20338
20508
  ...style,
20339
20509
  '--color-picker-hue': Math.round(color.h),
20340
20510
  '--color-picker-rgb': rgbChannels(color),
@@ -20408,7 +20578,7 @@ const ChannelSlider = React.forwardRef(({ channel, className, style, ...props },
20408
20578
  };
20409
20579
  if (!picker)
20410
20580
  return null;
20411
- return (jsxRuntime.jsxs("div", { ref: node => setRefs(node, rootRef, ref), className: classNames(sliderClass, bulmaHelperClasses, className), style: {
20581
+ return (jsxRuntime.jsxs("div", { ref: node => setRefs$1(node, rootRef, ref), className: classNames(sliderClass, bulmaHelperClasses, className), style: {
20412
20582
  ...style,
20413
20583
  '--color-picker-hue': Math.round(color.h),
20414
20584
  '--color-picker-rgb': rgbChannels(color),
@@ -20532,7 +20702,7 @@ const ColorPickerSwatches = React.forwardRef(({ className, swatches: ownSwatches
20532
20702
  };
20533
20703
  if (!picker || list.length === 0)
20534
20704
  return null;
20535
- return (jsxRuntime.jsx("div", { ref: node => setRefs(node, rootRef, ref), className: classNames(groupClass, bulmaHelperClasses, className), role: "radiogroup", "aria-label": labels.swatches, "aria-disabled": disabled || undefined, ...rest, children: list.map((entry, i) => {
20705
+ return (jsxRuntime.jsx("div", { ref: node => setRefs$1(node, rootRef, ref), className: classNames(groupClass, bulmaHelperClasses, className), role: "radiogroup", "aria-label": labels.swatches, "aria-disabled": disabled || undefined, ...rest, children: list.map((entry, i) => {
20536
20706
  const swatch = colorOf(i);
20537
20707
  const isChecked = i === checked;
20538
20708
  // The check is drawn in black or white with a ring of the other, so
@@ -26960,6 +27130,421 @@ function ClientOnly({ children, fallback = null }) {
26960
27130
  return jsxRuntime.jsx(jsxRuntime.Fragment, { children: typeof children === 'function' ? children() : children });
26961
27131
  }
26962
27132
 
27133
+ const FrameContext = React.createContext(null);
27134
+ /** A width prop as CSS: a number is pixels. */
27135
+ const cssLength = (value) => typeof value === 'number' ? `${value}px` : value;
27136
+ /**
27137
+ * Hands `node` to a part's own ref and to the one forwarded to it. A
27138
+ * forwarded callback that returns a cleanup, as React 19 allows, has it held
27139
+ * in `cleanup` and run on detach in place of a call with `null`, on React 18
27140
+ * as well.
27141
+ */
27142
+ function setRefs(node, own, forwarded, cleanup) {
27143
+ own.current = node;
27144
+ if (typeof forwarded === 'function') {
27145
+ if (node === null) {
27146
+ const detach = cleanup.current;
27147
+ cleanup.current = null;
27148
+ if (detach)
27149
+ detach();
27150
+ else
27151
+ forwarded(null);
27152
+ return;
27153
+ }
27154
+ const returned = forwarded(node);
27155
+ cleanup.current =
27156
+ typeof returned === 'function' ? returned : null;
27157
+ }
27158
+ else if (forwarded) {
27159
+ forwarded.current = node;
27160
+ }
27161
+ }
27162
+ /**
27163
+ * Warns when a part has an `id` the frame couldn't read, because it isn't a
27164
+ * direct child, so the skip link or the burger points at the wrong one.
27165
+ */
27166
+ function warnOnUnreadId(part, id, expected) {
27167
+ if (id && id !== expected) {
27168
+ warnOnce(`AppFrame:${part}-id`, `[bestax-bulma] <AppFrame.${part}> has an id <AppFrame> can't read, ` +
27169
+ `so ${part === 'Main' ? 'the skip link' : "the burger's aria-controls"} ` +
27170
+ `misses it. Put AppFrame.${part} directly inside AppFrame.`);
27171
+ }
27172
+ }
27173
+ /**
27174
+ * The top bar of an AppFrame, which sticks while the page scrolls.
27175
+ *
27176
+ * @function
27177
+ * @param {AppFrameHeaderProps} props - Props for AppFrame.Header.
27178
+ * @param {React.Ref<HTMLElement>} ref - Forwarded ref to the `<header>`.
27179
+ * @returns {JSX.Element} The rendered header.
27180
+ */
27181
+ const AppFrameHeader = React.forwardRef(({ className, children, ...props }, ref) => {
27182
+ const { bulmaHelperClasses, rest } = useBulmaClasses(props);
27183
+ const headerClass = usePrefixedClassNames('app-frame-header');
27184
+ return (jsxRuntime.jsx("header", { ref: ref, className: classNames(headerClass, bulmaHelperClasses, className), ...rest, children: children }));
27185
+ });
27186
+ AppFrameHeader.displayName = 'AppFrame.Header';
27187
+ /**
27188
+ * The navigation of an AppFrame: a column beside the page from the breakpoint up, and a drawer below it.
27189
+ *
27190
+ * @function
27191
+ * @param {AppFrameNavProps} props - Props for AppFrame.Nav.
27192
+ * @param {React.Ref<HTMLElement>} ref - Forwarded ref to the `<nav>`.
27193
+ * @returns {JSX.Element} The rendered nav, and the drawer's backdrop.
27194
+ */
27195
+ const AppFrameNav = React.forwardRef(({ className, style, width, overlay = true, closeOnEscape = true, closeOnClickOutside = true, closeOnNavigate = true, onClick, onPointerDownCapture, children, ...props }, ref) => {
27196
+ const frame = React.useContext(FrameContext);
27197
+ const { bulmaHelperClasses, rest } = useBulmaClasses(props);
27198
+ const navClass = usePrefixedClassNames('app-frame-nav');
27199
+ const closeClass = usePrefixedClassNames('app-frame-close');
27200
+ const backdropClass = usePrefixedClassNames('app-frame-backdrop');
27201
+ const activeClass = usePrefixedClassNames('is-active');
27202
+ const navRef = React.useRef(null);
27203
+ const backdropRef = React.useRef(null);
27204
+ const navCleanupRef = React.useRef(null);
27205
+ const navRefs = React.useCallback((node) => setRefs(node, navRef, ref, navCleanupRef), [ref]);
27206
+ const navId = rest.id || frame?.state.navId;
27207
+ if (frame)
27208
+ warnOnUnreadId('Nav', rest.id, frame.state.navId);
27209
+ // The drawer is open only below the breakpoint. Above it the nav is a
27210
+ // column, and none of what follows applies.
27211
+ const isDrawer = !!frame?.state.isDrawer;
27212
+ const open = isDrawer && !!frame?.state.navOpen;
27213
+ // The open drawer is Bulma's active state, on the nav as on a navbar's
27214
+ // menu, and on its backdrop.
27215
+ const shown = { [activeClass]: !!frame?.state.navOpen };
27216
+ const modal = open && overlay;
27217
+ const close = () => frame?.state.setNavOpen(false);
27218
+ // The rest of the frame goes inert: the header, the page, the aside and
27219
+ // the footer, but not the backdrop, which takes the presses outside the
27220
+ // drawer rather than let them through to what's under it. Not the whole
27221
+ // page, since a Modal or a toast renders beside the frame, often into an
27222
+ // element that's there before the drawer opens, and a Modal opened from
27223
+ // the drawer has to stay usable. First, so that as the drawer closes it
27224
+ // lets go before focus goes back to the burger: an inert element can't
27225
+ // take focus.
27226
+ useInertSiblings(navRef, modal, false, backdropRef);
27227
+ const isTopmost = useOverlayStack(open, overlay);
27228
+ useScrollLock(modal);
27229
+ // Tab stays inside while the drawer is the topmost modal overlay, as in
27230
+ // Sidebar, so a Modal opened from the drawer holds Tab until it closes.
27231
+ React.useEffect(() => {
27232
+ if (!modal)
27233
+ return undefined;
27234
+ return trapTab(() => navRef.current, window, () => isTopmost('modal'));
27235
+ }, [modal, isTopmost]);
27236
+ useOverlayEscape(open && closeOnEscape, isTopmost, close);
27237
+ // Without an overlay there's no backdrop: a press outside the drawer
27238
+ // closes it, as Popover's does, while it's the topmost overlay, so a
27239
+ // press in a Modal opened over it is the Modal's. A press on a control
27240
+ // that names the nav in its `aria-controls`, such as the burger, is left
27241
+ // to that control, which toggles it.
27242
+ const markInsidePress = useOutsidePress(navRef, open && !overlay && closeOnClickOutside, e => {
27243
+ const id = navRef.current?.id;
27244
+ const controlsNav = (node) => !!node
27245
+ .getAttribute?.('aria-controls')
27246
+ ?.split(/\s+/)
27247
+ .includes(id);
27248
+ if (!isTopmost() || e.composedPath().some(controlsNav))
27249
+ return;
27250
+ close();
27251
+ });
27252
+ // Focus moves to the drawer's first tab stop, the close button, as it
27253
+ // opens, and back to whatever had it as it closes, unless it has left
27254
+ // the drawer. After a press outside, the press's own mousedown then takes
27255
+ // it wherever the press goes. When the window widened, the drawer is the
27256
+ // column again, and focus on its links stays; the close button hides
27257
+ // there, so focus on it, or dropped from it to <body>, goes back too.
27258
+ const isDrawerRef = useLatestRef(isDrawer);
27259
+ useOverlayFocus(navRef, open, {
27260
+ modal: overlay,
27261
+ restore: () => {
27262
+ if (isDrawerRef.current)
27263
+ return true;
27264
+ const nav = navRef.current;
27265
+ const active = getActiveElementInTree(nav);
27266
+ return (!nav.contains(active) || !!active?.classList.contains(closeClass));
27267
+ },
27268
+ });
27269
+ const handleClick = (e) => {
27270
+ onClick?.(e);
27271
+ if (!open ||
27272
+ !closeOnNavigate ||
27273
+ e.button !== 0 ||
27274
+ e.metaKey ||
27275
+ e.ctrlKey ||
27276
+ e.shiftKey ||
27277
+ e.altKey) {
27278
+ return;
27279
+ }
27280
+ // Not skipped for a click whose default was prevented: a router's
27281
+ // link prevents it to navigate without a page load. Only a link the nav
27282
+ // holds: one in a Modal portaled from it is the Modal's.
27283
+ const link = e.target.closest('a[href]');
27284
+ if (!link || !navRef.current?.contains(link))
27285
+ return;
27286
+ if (link.hasAttribute('download'))
27287
+ return;
27288
+ const target = link.getAttribute('target');
27289
+ if (target && target !== '_self')
27290
+ return;
27291
+ close();
27292
+ };
27293
+ const navStyle = width === undefined
27294
+ ? style
27295
+ : {
27296
+ ...style,
27297
+ '--bulma-app-frame-nav-width': cssLength(width),
27298
+ };
27299
+ return (jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [jsxRuntime.jsxs("nav", { ref: navRefs, className: classNames(navClass, shown, bulmaHelperClasses, className), style: navStyle, onClick: handleClick, onPointerDownCapture: e => {
27300
+ onPointerDownCapture?.(e);
27301
+ markInsidePress(e);
27302
+ }, ...rest, id: navId, "aria-label": rest['aria-label'] ||
27303
+ (rest['aria-labelledby'] ? undefined : frame?.labels.nav), children: [frame && (jsxRuntime.jsx(Delete, { className: closeClass, size: "medium", ariaLabel: frame.labels.closeNav, onClick: close })), children] }), frame && overlay && (jsxRuntime.jsx("div", { ref: backdropRef, className: classNames(backdropClass, shown), "aria-hidden": "true", onClick: closeOnClickOutside
27304
+ ? () => {
27305
+ // Not under an overlay opened over the drawer.
27306
+ if (isTopmost())
27307
+ close();
27308
+ }
27309
+ : undefined }))] }));
27310
+ });
27311
+ AppFrameNav.displayName = 'AppFrame.Nav';
27312
+ /**
27313
+ * The page's main content in an AppFrame, which the skip link moves focus to.
27314
+ *
27315
+ * @function
27316
+ * @param {AppFrameMainProps} props - Props for AppFrame.Main.
27317
+ * @param {React.Ref<HTMLElement>} ref - Forwarded ref to the `<main>`.
27318
+ * @returns {JSX.Element} The rendered main.
27319
+ */
27320
+ const AppFrameMain = React.forwardRef(({ className, children, ...props }, ref) => {
27321
+ const frame = React.useContext(FrameContext);
27322
+ const { bulmaHelperClasses, rest } = useBulmaClasses(props);
27323
+ const mainClass = usePrefixedClassNames('app-frame-main');
27324
+ if (frame)
27325
+ warnOnUnreadId('Main', rest.id, frame.mainId);
27326
+ return (jsxRuntime.jsx("main", { ref: ref, tabIndex: -1, className: classNames(mainClass, bulmaHelperClasses, className), ...rest, id: rest.id || frame?.mainId, children: children }));
27327
+ });
27328
+ AppFrameMain.displayName = 'AppFrame.Main';
27329
+ /**
27330
+ * Content beside the page in an AppFrame, which moves under it below the breakpoint.
27331
+ *
27332
+ * @function
27333
+ * @param {AppFrameAsideProps} props - Props for AppFrame.Aside.
27334
+ * @param {React.Ref<HTMLElement>} ref - Forwarded ref to the `<aside>`.
27335
+ * @returns {JSX.Element} The rendered aside.
27336
+ */
27337
+ const AppFrameAside = React.forwardRef(({ className, style, width, children, ...props }, ref) => {
27338
+ const { bulmaHelperClasses, rest } = useBulmaClasses(props);
27339
+ const asideClass = usePrefixedClassNames('app-frame-aside');
27340
+ const asideStyle = width === undefined
27341
+ ? style
27342
+ : {
27343
+ ...style,
27344
+ '--bulma-app-frame-aside-width': cssLength(width),
27345
+ };
27346
+ return (jsxRuntime.jsx("aside", { ref: ref, className: classNames(asideClass, bulmaHelperClasses, className), style: asideStyle, ...rest, children: children }));
27347
+ });
27348
+ AppFrameAside.displayName = 'AppFrame.Aside';
27349
+ /**
27350
+ * The bottom of an AppFrame, under the page.
27351
+ *
27352
+ * @function
27353
+ * @param {AppFrameFooterProps} props - Props for AppFrame.Footer.
27354
+ * @param {React.Ref<HTMLElement>} ref - Forwarded ref to the `<footer>`.
27355
+ * @returns {JSX.Element} The rendered footer.
27356
+ */
27357
+ const AppFrameFooter = React.forwardRef(({ className, children, ...props }, ref) => {
27358
+ const { bulmaHelperClasses, rest } = useBulmaClasses(props);
27359
+ const footerClass = usePrefixedClassNames('app-frame-footer');
27360
+ return (jsxRuntime.jsx("footer", { ref: ref, className: classNames(footerClass, bulmaHelperClasses, className), ...rest, children: children }));
27361
+ });
27362
+ AppFrameFooter.displayName = 'AppFrame.Footer';
27363
+ /**
27364
+ * The button that opens the drawer, or collapses the column from the breakpoint up: Navbar.Burger, wired to the frame.
27365
+ *
27366
+ * @function
27367
+ * @param {AppFrameBurgerProps} props - Props for AppFrame.Burger.
27368
+ * @param {React.Ref<HTMLButtonElement>} ref - Forwarded ref to the `<button>`.
27369
+ * @returns {JSX.Element} The rendered burger.
27370
+ */
27371
+ const AppFrameBurger = React.forwardRef(({ className, onClick, ...props }, ref) => {
27372
+ // Outside a frame it's a Navbar.Burger that controls nothing.
27373
+ const frame = React.useContext(FrameContext);
27374
+ const burgerClass = usePrefixedClassNames('app-frame-burger');
27375
+ const state = frame?.state;
27376
+ // Until the frame knows whether the nav is a drawer, the burger says
27377
+ // nothing it can't stand behind: no `aria-expanded`, and its bars. It can't
27378
+ // act before then anyway, since that's before hydration. Outside a frame
27379
+ // it's a plain Navbar.Burger.
27380
+ const known = !!frame?.modeKnown;
27381
+ const expanded = known && !!state && (state.isDrawer ? state.navOpen : !state.navCollapsed);
27382
+ return (jsxRuntime.jsx(NavbarBurger, { ref: ref, ...props, className: classNames(burgerClass, className), "aria-label": props['aria-label'] || frame?.labels.burger, active: expanded, "aria-expanded": frame && !known ? null : expanded, "aria-controls": state?.navId, onClick: e => {
27383
+ onClick?.(e);
27384
+ if (e.defaultPrevented || !frame)
27385
+ return;
27386
+ // Safari, and Firefox on a Mac, don't focus a button that's pressed.
27387
+ // Focused, it's the element the drawer hands focus back to.
27388
+ e.currentTarget.focus();
27389
+ frame.state.toggleNav();
27390
+ } }));
27391
+ });
27392
+ AppFrameBurger.displayName = 'AppFrame.Burger';
27393
+ const isMain = (child) => React.isValidElement(child) && child.type === AppFrameMain;
27394
+ const isNav = (child) => React.isValidElement(child) && child.type === AppFrameNav;
27395
+ /**
27396
+ * The `AppFrame` component lays out an app's pages: a header across the top, a navigation column down the side, the page, and an aside and a footer, with the nav a drawer behind a burger on narrow screens.
27397
+ *
27398
+ * The header and the column stick while the document scrolls, and a skip
27399
+ * link comes first. The layout is CSS, so the server renders it as the
27400
+ * browser does. It holds two states, each yours to control: whether the
27401
+ * drawer is open, and whether the column is collapsed. `AppFrame.Burger`
27402
+ * and `useAppFrame()` toggle whichever applies at the current width.
27403
+ *
27404
+ * @function
27405
+ * @param {AppFrameProps} props - Props for the AppFrame component.
27406
+ * @param {React.Ref<HTMLDivElement>} ref - Forwarded ref to the root `<div>`.
27407
+ * @returns {JSX.Element} The rendered frame.
27408
+ *
27409
+ * @example
27410
+ * <AppFrame>
27411
+ * <AppFrame.Header>
27412
+ * <Navbar aria-label="Site">
27413
+ * <Navbar.Brand>
27414
+ * <Navbar.Item href="/">Acme</Navbar.Item>
27415
+ * <AppFrame.Burger />
27416
+ * </Navbar.Brand>
27417
+ * </Navbar>
27418
+ * </AppFrame.Header>
27419
+ * <AppFrame.Nav>
27420
+ * <Menu>…</Menu>
27421
+ * </AppFrame.Nav>
27422
+ * <AppFrame.Main>…</AppFrame.Main>
27423
+ * </AppFrame>
27424
+ */
27425
+ const AppFrameRoot = React.forwardRef(({ className, textColor, bgColor, breakpoint = 'desktop', navOpen: navOpenProp, defaultNavOpen = false, onNavOpenChange, navCollapsed: navCollapsedProp, defaultNavCollapsed = false, onNavCollapsedChange, skipLink = true, labels, children, ...props }, ref) => {
27426
+ const { bulmaHelperClasses, rest } = useBulmaClasses({
27427
+ color: textColor,
27428
+ backgroundColor: bgColor,
27429
+ ...props,
27430
+ });
27431
+ const rootRef = React.useRef(null);
27432
+ const rootCleanupRef = React.useRef(null);
27433
+ const rootRefs = React.useCallback((node) => setRefs(node, rootRef, ref, rootCleanupRef), [ref]);
27434
+ const [openState, setOpenState] = React.useState(defaultNavOpen);
27435
+ const [collapsedState, setCollapsedState] = React.useState(defaultNavCollapsed);
27436
+ const navOpen = navOpenProp ?? openState;
27437
+ const navCollapsed = navCollapsedProp ?? collapsedState;
27438
+ const openControlled = navOpenProp !== undefined;
27439
+ const collapsedControlled = navCollapsedProp !== undefined;
27440
+ const onNavOpenChangeRef = useLatestRef(onNavOpenChange);
27441
+ const onNavCollapsedChangeRef = useLatestRef(onNavCollapsedChange);
27442
+ const setNavOpen = React.useCallback((open) => {
27443
+ onNavOpenChangeRef.current?.(open);
27444
+ if (!openControlled)
27445
+ setOpenState(open);
27446
+ }, [openControlled, onNavOpenChangeRef]);
27447
+ const setNavCollapsed = React.useCallback((collapsed) => {
27448
+ onNavCollapsedChangeRef.current?.(collapsed);
27449
+ if (!collapsedControlled)
27450
+ setCollapsedState(collapsed);
27451
+ }, [collapsedControlled, onNavCollapsedChangeRef]);
27452
+ // The layout switches in the stylesheet. The script needs the width
27453
+ // only to know which state the burger toggles, and to close the drawer
27454
+ // once the window is at or past the breakpoint.
27455
+ const wide = useBulmaBreakpoint(breakpoint);
27456
+ const isDrawer = wide === false;
27457
+ const isWide = wide === true;
27458
+ React.useEffect(() => {
27459
+ // eslint-disable-next-line react-hooks/set-state-in-effect -- the width is known only in the browser, and the drawer closes in answer to it
27460
+ if (isWide && navOpen)
27461
+ setNavOpen(false);
27462
+ }, [isWide, navOpen, setNavOpen]);
27463
+ const toggleNav = React.useCallback(() => {
27464
+ if (isDrawer)
27465
+ setNavOpen(!navOpen);
27466
+ else
27467
+ setNavCollapsed(!navCollapsed);
27468
+ }, [isDrawer, navOpen, navCollapsed, setNavOpen, setNavCollapsed]);
27469
+ // The ids of the parts the skip link and the burger point at, read off
27470
+ // the children so the server renders them too.
27471
+ const baseId = React.useId();
27472
+ const parts = flattenChildren(children);
27473
+ const mainId = parts.find(isMain)?.props.id || `${baseId}-main`;
27474
+ const navId = parts.find(isNav)?.props.id || `${baseId}-nav`;
27475
+ const { skipLink: skipText, nav, closeNav, burger } = labels ?? {};
27476
+ const value = React.useMemo(() => ({
27477
+ state: {
27478
+ navOpen,
27479
+ setNavOpen,
27480
+ navCollapsed,
27481
+ setNavCollapsed,
27482
+ isDrawer,
27483
+ toggleNav,
27484
+ navId,
27485
+ },
27486
+ labels: {
27487
+ skipLink: skipText || 'Skip to main content',
27488
+ nav: nav || 'Sections',
27489
+ closeNav: closeNav || 'Close navigation',
27490
+ burger: burger || undefined,
27491
+ },
27492
+ mainId,
27493
+ modeKnown: wide !== null,
27494
+ }), [
27495
+ navOpen,
27496
+ setNavOpen,
27497
+ navCollapsed,
27498
+ setNavCollapsed,
27499
+ isDrawer,
27500
+ toggleNav,
27501
+ navId,
27502
+ skipText,
27503
+ nav,
27504
+ closeNav,
27505
+ burger,
27506
+ mainId,
27507
+ wide,
27508
+ ]);
27509
+ // Its header and footer are the page's banner and contentinfo only
27510
+ // outside `main` and sectioning content, as HTML-AAM maps them.
27511
+ React.useEffect(() => {
27512
+ const host = rootRef.current?.parentElement?.closest('main, article, aside, nav, section');
27513
+ if (host) {
27514
+ warnOnce('AppFrame:nested', `[bestax-bulma] <AppFrame> sits inside a <${host.localName}>, where ` +
27515
+ "its header and footer are no longer the page's banner and " +
27516
+ 'contentinfo landmarks. Put AppFrame at the top of the page.');
27517
+ }
27518
+ }, []);
27519
+ const rootClass = usePrefixedClassNames('app-frame', {
27520
+ [`is-${breakpoint}`]: true,
27521
+ 'is-nav-collapsed': navCollapsed,
27522
+ });
27523
+ const skipClass = usePrefixedClassNames('app-frame-skip-link');
27524
+ return (jsxRuntime.jsxs("div", { ref: rootRefs, className: classNames(rootClass, bulmaHelperClasses, className), ...rest, children: [skipLink && (jsxRuntime.jsx("a", { className: skipClass, href: `#${mainId}`, children: value.labels.skipLink })), jsxRuntime.jsx(FrameContext.Provider, { value: value, children: children })] }));
27525
+ });
27526
+ /**
27527
+ * The frame's state, for a control of your own inside it: whether the
27528
+ * drawer is open and the column collapsed, setters that go through the
27529
+ * frame's callbacks, and `toggleNav()`, which does what the burger does.
27530
+ * Outside an AppFrame it's `null`, as the library's other context hooks
27531
+ * are, so a control that can sit outside one renders a fallback or nothing.
27532
+ *
27533
+ * @function useAppFrame
27534
+ * @returns The frame's state, or `null` outside an AppFrame.
27535
+ */
27536
+ function useAppFrame() {
27537
+ return React.useContext(FrameContext)?.state ?? null;
27538
+ }
27539
+ const AppFrame = withSubComponents(AppFrameRoot, {
27540
+ Header: AppFrameHeader,
27541
+ Nav: AppFrameNav,
27542
+ Main: AppFrameMain,
27543
+ Aside: AppFrameAside,
27544
+ Footer: AppFrameFooter,
27545
+ Burger: AppFrameBurger,
27546
+ }, 'AppFrame');
27547
+
26963
27548
  /**
26964
27549
  * The `.stack` element behind Stack, Group, Center and Flex.
26965
27550
  *
@@ -27968,6 +28553,14 @@ const Stack = React.forwardRef(function Stack(props, ref) {
27968
28553
  });
27969
28554
  Stack.displayName = 'Stack';
27970
28555
 
28556
+ exports.Affix = Affix;
28557
+ exports.AppFrame = AppFrame;
28558
+ exports.AppFrameAside = AppFrameAside;
28559
+ exports.AppFrameBurger = AppFrameBurger;
28560
+ exports.AppFrameFooter = AppFrameFooter;
28561
+ exports.AppFrameHeader = AppFrameHeader;
28562
+ exports.AppFrameMain = AppFrameMain;
28563
+ exports.AppFrameNav = AppFrameNav;
27971
28564
  exports.Autocomplete = Autocomplete;
27972
28565
  exports.Avatar = Avatar;
27973
28566
  exports.Avatars = Avatars;
@@ -28198,6 +28791,7 @@ exports.radioSizes = radioSizes;
28198
28791
  exports.switchColors = switchColors;
28199
28792
  exports.switchSizes = switchSizes;
28200
28793
  exports.toast = toast;
28794
+ exports.useAppFrame = useAppFrame;
28201
28795
  exports.useBulmaClasses = useBulmaClasses;
28202
28796
  exports.useClassPrefix = useClassPrefix;
28203
28797
  exports.useColorClasses = useColorClasses;