@allxsmith/bestax-bulma 5.60.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 (59) hide show
  1. package/dist/bestax.css +1 -1
  2. package/dist/bestax.css.map +1 -1
  3. package/dist/esm/components/Carousel.js +3 -43
  4. package/dist/esm/components/Carousel.js.map +1 -1
  5. package/dist/esm/components/Command.js +3 -3
  6. package/dist/esm/components/Navbar.js +3 -1
  7. package/dist/esm/components/Navbar.js.map +1 -1
  8. package/dist/esm/components/Popover.js +7 -27
  9. package/dist/esm/components/Popover.js.map +1 -1
  10. package/dist/esm/components/Sidebar.js +17 -58
  11. package/dist/esm/components/Sidebar.js.map +1 -1
  12. package/dist/esm/form/ColorPicker.js +4 -4
  13. package/dist/esm/form/ColorPicker.js.map +1 -1
  14. package/dist/esm/form/Combobox.js +3 -3
  15. package/dist/esm/helpers/overlayStack.js +33 -1
  16. package/dist/esm/helpers/overlayStack.js.map +1 -1
  17. package/dist/esm/helpers/useBulmaViewport.js +82 -0
  18. package/dist/esm/helpers/useBulmaViewport.js.map +1 -0
  19. package/dist/esm/helpers/useInertSiblings.js +7 -3
  20. package/dist/esm/helpers/useInertSiblings.js.map +1 -1
  21. package/dist/esm/helpers/useOutsidePress.js +53 -0
  22. package/dist/esm/helpers/useOutsidePress.js.map +1 -0
  23. package/dist/esm/helpers/useOverlayFocus.js +74 -0
  24. package/dist/esm/helpers/useOverlayFocus.js.map +1 -0
  25. package/dist/esm/layout/AppFrame.js +438 -0
  26. package/dist/esm/layout/AppFrame.js.map +1 -0
  27. package/dist/extras.css +1 -1
  28. package/dist/extras.css.map +1 -1
  29. package/dist/index.cjs +678 -136
  30. package/dist/index.cjs.map +1 -1
  31. package/dist/index.esm.js +1 -0
  32. package/dist/index.esm.js.map +1 -1
  33. package/dist/types/components/Navbar.d.ts +4 -3
  34. package/dist/types/helpers/overlayStack.d.ts +17 -0
  35. package/dist/types/helpers/useBulmaViewport.d.ts +34 -0
  36. package/dist/types/helpers/useInertSiblings.d.ts +4 -1
  37. package/dist/types/helpers/useOutsidePress.d.ts +24 -0
  38. package/dist/types/helpers/useOverlayFocus.d.ts +44 -0
  39. package/dist/types/index.d.ts +1 -0
  40. package/dist/types/layout/AppFrame.d.ts +306 -0
  41. package/dist/types-cjs/components/Navbar.d.ts +4 -3
  42. package/dist/types-cjs/helpers/overlayStack.d.ts +17 -0
  43. package/dist/types-cjs/helpers/useBulmaViewport.d.ts +34 -0
  44. package/dist/types-cjs/helpers/useInertSiblings.d.ts +4 -1
  45. package/dist/types-cjs/helpers/useOutsidePress.d.ts +24 -0
  46. package/dist/types-cjs/helpers/useOverlayFocus.d.ts +44 -0
  47. package/dist/types-cjs/index.d.ts +1 -0
  48. package/dist/types-cjs/layout/AppFrame.d.ts +306 -0
  49. package/dist/versions/bestax-no-dark-mode.css +1 -1
  50. package/dist/versions/bestax-no-dark-mode.css.map +1 -1
  51. package/dist/versions/bestax-no-helpers-prefixed.css +1 -1
  52. package/dist/versions/bestax-no-helpers-prefixed.css.map +1 -1
  53. package/dist/versions/bestax-no-helpers.css +1 -1
  54. package/dist/versions/bestax-no-helpers.css.map +1 -1
  55. package/dist/versions/bestax-prefixed.css +1 -1
  56. package/dist/versions/bestax-prefixed.css.map +1 -1
  57. package/package.json +1 -1
  58. package/src/scss/layout/_appframe.scss +306 -0
  59. package/src/scss/layout/_index.scss +1 -0
package/dist/index.cjs CHANGED
@@ -4755,6 +4755,37 @@ function useOverlayStack(inStack, modal = true) {
4755
4755
  return !above.some(overlay => among !== 'modal' || overlay.modal);
4756
4756
  }, []);
4757
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
+ }
4758
4789
 
4759
4790
  /**
4760
4791
  * Whether `el` is editable while its parent is not, which makes it an editing
@@ -5680,7 +5711,9 @@ const NavbarBurger = React.forwardRef(function NavbarBurger({ className, active,
5680
5711
  });
5681
5712
  return (jsxRuntime.jsxs("button", { ref: ref, className: classNames(usePrefixedClassNames('navbar-burger', {
5682
5713
  'is-active': active,
5683
- }), 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] }));
5684
5717
  });
5685
5718
  NavbarBurger.displayName = 'NavbarBurger';
5686
5719
  /**
@@ -6851,16 +6884,20 @@ const above = (el) => el.parentElement ?? el.getRootNode().host;
6851
6884
  * @param active - Whether what's beside it is inert.
6852
6885
  * @param wholePage - Whether that goes for the whole page rather than only
6853
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.
6854
6890
  */
6855
- function useInertSiblings(ref, active, wholePage) {
6891
+ function useInertSiblings(ref, active, wholePage, except) {
6856
6892
  React.useEffect(() => {
6857
6893
  const el = ref.current;
6858
6894
  if (!active || !el)
6859
6895
  return undefined;
6860
6896
  const doc = el.ownerDocument;
6897
+ const keep = except?.current;
6861
6898
  const held = [];
6862
6899
  for (let node = el; node && node !== doc.body; node = wholePage ? above(node) : undefined) {
6863
- held.push(...siblingsOf(node).filter(hold));
6900
+ held.push(...siblingsOf(node).filter(sibling => sibling !== keep && hold(sibling)));
6864
6901
  }
6865
6902
  // Focus this takes away, to hand back later. It's looked up as `el`'s
6866
6903
  // tree sees it, where focus inside a nested shadow root reads as its
@@ -6875,7 +6912,7 @@ function useInertSiblings(ref, active, wholePage) {
6875
6912
  if (doc.activeElement === doc.body)
6876
6913
  taken?.focus();
6877
6914
  };
6878
- }, [ref, active, wholePage]);
6915
+ }, [ref, active, wholePage, except]);
6879
6916
  }
6880
6917
 
6881
6918
  /**
@@ -7971,6 +8008,51 @@ function useAnchoredPosition(anchorRef, panelRef, { active, position, fixed, off
7971
8008
  return resolved;
7972
8009
  }
7973
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
+
7974
8056
  const PopoverContext = React.createContext(null);
7975
8057
  /** Intrinsic elements that are controls already, and keep their own role. */
7976
8058
  const NATIVE_CONTROLS = new Set([
@@ -8021,9 +8103,6 @@ const PopoverComponent = ({ trigger, children, open: controlledOpen, defaultOpen
8021
8103
  const { bulmaHelperClasses, rest } = useBulmaClasses(props);
8022
8104
  const rootRef = React.useRef(null);
8023
8105
  const panelRef = React.useRef(null);
8024
- // The last pointerdown React saw inside the panel. React's tree includes
8025
- // content the panel portals elsewhere, which the DOM path doesn't.
8026
- const insidePointerRef = React.useRef(null);
8027
8106
  const baseId = React.useId();
8028
8107
  const panelId = `${baseId}-panel`;
8029
8108
  const titleId = `${baseId}-title`;
@@ -8079,25 +8158,10 @@ const PopoverComponent = ({ trigger, children, open: controlledOpen, defaultOpen
8079
8158
  }
8080
8159
  };
8081
8160
  }, [panelMounted, trapFocus, triggerNode]);
8082
- // Read when a press lands outside, so the listener stays put while `close`
8083
- // changes identity, which it does with `onOpenChange`, and an inline one
8084
- // changes on every render of the parent.
8085
- const closeRef = useLatestRef(close);
8086
- React.useEffect(() => {
8087
- if (!panelMounted || !closeOnClickOutside)
8088
- return undefined;
8089
- const handlePointerDown = (e) => {
8090
- if (e === insidePointerRef.current || isEventInside(e, rootRef.current)) {
8091
- return;
8092
- }
8093
- closeRef.current();
8094
- };
8095
- // The wrapper is on the page whenever the panel is. Its document, an
8096
- // iframe's when the tree renders into one, is where presses land.
8097
- const doc = rootRef.current.ownerDocument;
8098
- doc.addEventListener('pointerdown', handlePointerDown);
8099
- return () => doc.removeEventListener('pointerdown', handlePointerDown);
8100
- }, [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);
8101
8165
  const handlePanelKeyDown = (e) => {
8102
8166
  if (e.key !== 'Escape' || !closeOnEscape || e.defaultPrevented)
8103
8167
  return;
@@ -8163,9 +8227,7 @@ const PopoverComponent = ({ trigger, children, open: controlledOpen, defaultOpen
8163
8227
  top: resolved.top,
8164
8228
  left: resolved.left,
8165
8229
  }
8166
- : undefined, onKeyDown: handlePanelKeyDown, onPointerDownCapture: e => {
8167
- insidePointerRef.current = e.nativeEvent;
8168
- }, children: children }) }))] }));
8230
+ : undefined, onKeyDown: handlePanelKeyDown, onPointerDownCapture: markInsidePress, children: children }) }))] }));
8169
8231
  };
8170
8232
  /**
8171
8233
  * The open panel. It mounts with each opening, so it counts the
@@ -9293,6 +9355,69 @@ const TimelineRoot = React.forwardRef(({ className, children, active, pending, a
9293
9355
  });
9294
9356
  const Timeline = withSubComponents(TimelineRoot, { Item: TimelineItem }, 'Timeline');
9295
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
+
9296
9421
  /**
9297
9422
  * The `Sidebar` component provides a slide-out navigation panel that appears from the left or right side of the screen.
9298
9423
  *
@@ -9342,31 +9467,15 @@ const SidebarComponent = React.forwardRef(({ isOpen, onClose, position = 'left',
9342
9467
  handleClose();
9343
9468
  }
9344
9469
  }, [overlayClose, handleClose]);
9345
- // Read when Escape arrives, so the key listener stays put while
9346
- // `onClose` changes identity, as an inline one does on every render of
9347
- // the parent.
9348
- const handleCloseRef = useLatestRef(handleClose);
9349
9470
  // Joins the stack of open overlays, so the sidebar answers Escape only
9350
9471
  // while it's the topmost overlay, and keeps it from those under it. It's
9351
9472
  // modal with an overlay: without one it leaves Tab to a modal overlay
9352
9473
  // under it.
9353
9474
  const isTopmost = useOverlayStack(isOpen, overlay);
9354
9475
  const closesOnEscape = escapeClose && canCancel && Boolean(onClose);
9355
- // Handle escape key. On `window`, as in `Modal`, so everything inside the
9356
- // sidebar has had the key first, and an Escape it used is left to it. One
9357
- // the sidebar closes on is marked used, so nothing under it acts on it.
9358
- React.useEffect(() => {
9359
- if (!isOpen || !closesOnEscape)
9360
- return;
9361
- const handleKeyDown = (e) => {
9362
- if (e.key !== 'Escape' || e.defaultPrevented || !isTopmost())
9363
- return;
9364
- e.preventDefault();
9365
- handleCloseRef.current();
9366
- };
9367
- window.addEventListener('keydown', handleKeyDown);
9368
- return () => window.removeEventListener('keydown', handleKeyDown);
9369
- }, [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);
9370
9479
  // Keep Tab inside while there's an overlay, as `Modal` does:
9371
9480
  // `aria-modal` hides the rest of the page from assistive technology, so
9372
9481
  // the keyboard order has to agree. Only while it's the topmost modal
@@ -9389,41 +9498,17 @@ const SidebarComponent = React.forwardRef(({ isOpen, onClose, position = 'left',
9389
9498
  useScrollLock(isOpen && overlay);
9390
9499
  // Move focus into the sidebar on open, to the first stop Tab wraps to or
9391
9500
  // the sidebar itself, and hand it back on close, as `Modal` does. A
9392
- // different `defaultTarget` remounts the portaled sidebar: the cleanup
9393
- // hands focus back from the old nodes, which left it on <body>, and the
9394
- // re-run records the same opener and focuses the new nodes. Whether it
9395
- // takes focus is decided as it opens, so `overlay` changing while it's
9396
- // open leaves focus where the user has it.
9397
- React.useEffect(() => {
9398
- if (!isOpen)
9399
- return undefined;
9400
- const element = sidebarRef.current;
9401
- /* istanbul ignore if: the aside renders whenever the sidebar is open, so its ref is set before this runs */
9402
- if (!element)
9403
- return undefined;
9404
- // The element that had focus, wherever it sits: a portaled sidebar
9405
- // renders under `document.body` while its opener can be inside a
9406
- // shadow root.
9407
- const opener = getDeepestActiveElement();
9408
- // Without an overlay the sidebar holds no Tab, so with a modal overlay
9409
- // open under it, the first Tab would take focus back there and leave
9410
- // the sidebar's other controls out of reach. It leaves focus alone
9411
- // then, and the cleanup below finds focus outside and leaves it too.
9412
- if (overlay || !isModalOverlayOpen()) {
9413
- focusTabStop(element);
9414
- }
9415
- return () => {
9416
- // Only while the sidebar still holds focus: closing one under another
9417
- // overlay mustn't pull focus out of the one on top, and focus moved
9418
- // out to the page stays there. A removed subtree leaves focus on
9419
- // <body>, which still counts as held.
9420
- const active = getActiveElementInTree(element);
9421
- if (active === document.body || element.contains(active)) {
9422
- opener?.focus();
9423
- }
9424
- };
9425
- // eslint-disable-next-line react-hooks/exhaustive-deps -- `overlay` is read as the sidebar opens, on purpose
9426
- }, [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
+ });
9427
9512
  // Use combined ref
9428
9513
  const combinedRef = React.useCallback((node) => {
9429
9514
  sidebarRef.current =
@@ -10147,6 +10232,81 @@ const DialogContainer = () => {
10147
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) }));
10148
10233
  };
10149
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
+
10150
10310
  /**
10151
10311
  * Individual carousel item/slide.
10152
10312
  *
@@ -10174,24 +10334,10 @@ const DefaultPrevIcon = () => (jsxRuntime.jsx("svg", { xmlns: "http://www.w3.org
10174
10334
  * @returns {JSX.Element} The rendered SVG chevron-right icon.
10175
10335
  */
10176
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" }) }));
10177
- /**
10178
- * The media queries Bulma's `fullhd`, `widescreen`, `desktop` and `tablet`
10179
- * mixins open with, widest first, at Bulma's default breakpoints. The
10180
- * stylesheet picks the count in view with those mixins; these tell the
10181
- * script which count that is. Narrower than the last one is `mobile`.
10182
- */
10183
- const VIEWPORT_QUERIES = [
10184
- 'screen and (min-width: 1408px)',
10185
- 'screen and (min-width: 1216px)',
10186
- 'screen and (min-width: 1024px)',
10187
- 'screen and (min-width: 769px), print',
10188
- ];
10189
- /** `useBulmaViewport`'s answer below the tablet breakpoint. */
10190
- const MOBILE = VIEWPORT_QUERIES.length;
10191
10337
  /**
10192
10338
  * The `--bulma-carousel-items-to-show*` variable each count prop writes, in
10193
- * the order the component lists them: the viewports as above, mobile, and
10194
- * last `itemsToShow` itself.
10339
+ * the order the component lists them: `useBulmaViewport`'s viewports, widest
10340
+ * first, then mobile, and last `itemsToShow` itself.
10195
10341
  */
10196
10342
  const COUNT_SUFFIXES = [
10197
10343
  '-fullhd',
@@ -10201,33 +10347,6 @@ const COUNT_SUFFIXES = [
10201
10347
  '-mobile',
10202
10348
  '',
10203
10349
  ];
10204
- /**
10205
- * Which of Bulma's viewports the window is in: the index of the widest
10206
- * `VIEWPORT_QUERIES` entry that matches, `MOBILE` below them all, or `-1`
10207
- * when that isn't known (on the server, before the first layout effect, and
10208
- * whenever `enabled` is off, so a carousel with one count everywhere
10209
- * subscribes to nothing).
10210
- *
10211
- * @function
10212
- * @param {boolean} enabled - Whether any viewport-specific count is set.
10213
- * @returns {number} The viewport, as described above.
10214
- */
10215
- function useBulmaViewport(enabled) {
10216
- const [viewport, setViewport] = React.useState(-1);
10217
- useClientLayoutEffect(() => {
10218
- if (!enabled || typeof window.matchMedia !== 'function')
10219
- return undefined;
10220
- const lists = VIEWPORT_QUERIES.map(query => window.matchMedia(query));
10221
- const update = () => {
10222
- const index = lists.findIndex(list => list.matches);
10223
- setViewport(index < 0 ? MOBILE : index);
10224
- };
10225
- update();
10226
- lists.forEach(list => list.addEventListener('change', update));
10227
- return () => lists.forEach(list => list.removeEventListener('change', update));
10228
- }, [enabled]);
10229
- return enabled ? viewport : -1;
10230
- }
10231
10350
  /**
10232
10351
  * A count prop as the whole number the carousel works with: rounded down,
10233
10352
  * and never below 1, so a zero or a stray `NaN` can't divide by nothing.
@@ -13125,7 +13244,7 @@ const standalone$2 = {
13125
13244
  const CommandContext = React.createContext(null);
13126
13245
  const useCommand = () => React.useContext(CommandContext) ?? standalone$2;
13127
13246
  /** Hands a node to a forwarded ref and to one of the component's own. */
13128
- function setRefs$2(node, own, forwarded) {
13247
+ function setRefs$3(node, own, forwarded) {
13129
13248
  own.current = node;
13130
13249
  if (typeof forwarded === 'function')
13131
13250
  forwarded(node);
@@ -13242,7 +13361,7 @@ const CommandParts = React.forwardRef(({ children, label, query: controlledQuery
13242
13361
  registerHandler,
13243
13362
  };
13244
13363
  const { rootRef } = engine;
13245
- 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]);
13246
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 })] }));
13247
13366
  });
13248
13367
  CommandParts.displayName = 'CommandParts';
@@ -13262,7 +13381,7 @@ const CommandInput = React.forwardRef(({ isLoading, iconLeft, iconLeftName, onKe
13262
13381
  return () => reportLoading(false);
13263
13382
  }, [reportLoading, isLoading]);
13264
13383
  const { inputRef } = engine;
13265
- 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]);
13266
13385
  const { expanded, highlighted } = engine;
13267
13386
  // A name the caller gave the field wins over the Command's `label`.
13268
13387
  const callerNamed = !!props['aria-label'] || 'aria-labelledby' in props;
@@ -18791,7 +18910,7 @@ const standalone$1 = {
18791
18910
  const ComboboxContext = React.createContext(null);
18792
18911
  const useCombobox = () => React.useContext(ComboboxContext) ?? standalone$1;
18793
18912
  /** Hands a node to a forwarded ref and to one of the component's own. */
18794
- function setRefs$1(node, own, forwarded) {
18913
+ function setRefs$2(node, own, forwarded) {
18795
18914
  own.current = node;
18796
18915
  if (typeof forwarded === 'function')
18797
18916
  forwarded(node);
@@ -18923,7 +19042,7 @@ const ComboboxParts = React.forwardRef(({ children, below, onSettle, open, defau
18923
19042
  appendToBody,
18924
19043
  };
18925
19044
  const { rootRef } = engine;
18926
- 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]);
18927
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 })] }));
18928
19047
  const fieldElement = below === undefined ? (comboboxElement) : (jsxRuntime.jsxs("div", { children: [comboboxElement, jsxRuntime.jsx("div", { onPointerDownCapture: engine.markInside, onMouseDown: engine.keepFocus, children: below })] }));
18929
19048
  const messageEl = message ? jsxRuntime.jsx("p", { className: helpClass, children: message }) : null;
@@ -18956,7 +19075,7 @@ const ComboboxInput = React.forwardRef(({ id, isLoading, controlSize, onKeyDown,
18956
19075
  return () => reportLoading(false);
18957
19076
  }, [reportLoading, isLoading]);
18958
19077
  const { inputRef } = engine;
18959
- 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]);
18960
19079
  const { expanded, highlighted } = engine;
18961
19080
  // An `id` of the field's own reaches the label from a layout effect, so
18962
19081
  // in server markup, and the first render, the label's `for` still names
@@ -20253,7 +20372,7 @@ const rgbChannels = (color) => {
20253
20372
  /** Whether a press should start a drag: any touch or pen, a mouse's main button. */
20254
20373
  const startsDrag = (event) => event.pointerType !== 'mouse' || event.button === 0;
20255
20374
  /** Hands a node to a forwarded ref and to one of the part's own. */
20256
- function setRefs(node, own, forwarded) {
20375
+ function setRefs$1(node, own, forwarded) {
20257
20376
  own.current = node;
20258
20377
  if (typeof forwarded === 'function')
20259
20378
  forwarded(node);
@@ -20385,7 +20504,7 @@ const ColorPickerArea = React.forwardRef(({ className, style, ...props }, ref) =
20385
20504
  const hiddenOf = (which) => touch || keyed || axis === which ? undefined : true;
20386
20505
  if (!picker)
20387
20506
  return null;
20388
- 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: {
20389
20508
  ...style,
20390
20509
  '--color-picker-hue': Math.round(color.h),
20391
20510
  '--color-picker-rgb': rgbChannels(color),
@@ -20459,7 +20578,7 @@ const ChannelSlider = React.forwardRef(({ channel, className, style, ...props },
20459
20578
  };
20460
20579
  if (!picker)
20461
20580
  return null;
20462
- 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: {
20463
20582
  ...style,
20464
20583
  '--color-picker-hue': Math.round(color.h),
20465
20584
  '--color-picker-rgb': rgbChannels(color),
@@ -20583,7 +20702,7 @@ const ColorPickerSwatches = React.forwardRef(({ className, swatches: ownSwatches
20583
20702
  };
20584
20703
  if (!picker || list.length === 0)
20585
20704
  return null;
20586
- 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) => {
20587
20706
  const swatch = colorOf(i);
20588
20707
  const isChecked = i === checked;
20589
20708
  // The check is drawn in black or white with a ring of the other, so
@@ -27011,6 +27130,421 @@ function ClientOnly({ children, fallback = null }) {
27011
27130
  return jsxRuntime.jsx(jsxRuntime.Fragment, { children: typeof children === 'function' ? children() : children });
27012
27131
  }
27013
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
+
27014
27548
  /**
27015
27549
  * The `.stack` element behind Stack, Group, Center and Flex.
27016
27550
  *
@@ -28020,6 +28554,13 @@ const Stack = React.forwardRef(function Stack(props, ref) {
28020
28554
  Stack.displayName = 'Stack';
28021
28555
 
28022
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;
28023
28564
  exports.Autocomplete = Autocomplete;
28024
28565
  exports.Avatar = Avatar;
28025
28566
  exports.Avatars = Avatars;
@@ -28250,6 +28791,7 @@ exports.radioSizes = radioSizes;
28250
28791
  exports.switchColors = switchColors;
28251
28792
  exports.switchSizes = switchSizes;
28252
28793
  exports.toast = toast;
28794
+ exports.useAppFrame = useAppFrame;
28253
28795
  exports.useBulmaClasses = useBulmaClasses;
28254
28796
  exports.useClassPrefix = useClassPrefix;
28255
28797
  exports.useColorClasses = useColorClasses;