@allxsmith/bestax-bulma 5.33.0 → 5.34.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 (42) hide show
  1. package/dist/bestax.css +1 -1
  2. package/dist/bestax.css.map +1 -1
  3. package/dist/esm/components/EmptyState.js +139 -0
  4. package/dist/esm/components/EmptyState.js.map +1 -0
  5. package/dist/esm/elements/Buttons.js.map +1 -1
  6. package/dist/esm/elements/SubTitle.js.map +1 -1
  7. package/dist/esm/elements/Title.js.map +1 -1
  8. package/dist/esm/form/Autocomplete.js +59 -14
  9. package/dist/esm/form/Autocomplete.js.map +1 -1
  10. package/dist/esm/helpers/statusRegion.js +19 -12
  11. package/dist/esm/helpers/statusRegion.js.map +1 -1
  12. package/dist/extras.css +1 -1
  13. package/dist/extras.css.map +1 -1
  14. package/dist/index.cjs +1188 -1006
  15. package/dist/index.cjs.map +1 -1
  16. package/dist/index.esm.js +1 -0
  17. package/dist/index.esm.js.map +1 -1
  18. package/dist/types/components/EmptyState.d.ts +149 -0
  19. package/dist/types/elements/Buttons.d.ts +1 -1
  20. package/dist/types/elements/SubTitle.d.ts +13 -2
  21. package/dist/types/elements/Title.d.ts +11 -2
  22. package/dist/types/form/Autocomplete.d.ts +18 -3
  23. package/dist/types/helpers/statusRegion.d.ts +4 -4
  24. package/dist/types/index.d.ts +1 -0
  25. package/dist/types-cjs/components/EmptyState.d.ts +149 -0
  26. package/dist/types-cjs/elements/Buttons.d.ts +1 -1
  27. package/dist/types-cjs/elements/SubTitle.d.ts +13 -2
  28. package/dist/types-cjs/elements/Title.d.ts +11 -2
  29. package/dist/types-cjs/form/Autocomplete.d.ts +18 -3
  30. package/dist/types-cjs/helpers/statusRegion.d.ts +4 -4
  31. package/dist/types-cjs/index.d.ts +1 -0
  32. package/dist/versions/bestax-no-dark-mode.css +1 -1
  33. package/dist/versions/bestax-no-dark-mode.css.map +1 -1
  34. package/dist/versions/bestax-no-helpers-prefixed.css +1 -1
  35. package/dist/versions/bestax-no-helpers-prefixed.css.map +1 -1
  36. package/dist/versions/bestax-no-helpers.css +1 -1
  37. package/dist/versions/bestax-no-helpers.css.map +1 -1
  38. package/dist/versions/bestax-prefixed.css +1 -1
  39. package/dist/versions/bestax-prefixed.css.map +1 -1
  40. package/package.json +1 -1
  41. package/src/scss/components/_emptystate.scss +243 -0
  42. package/src/scss/components/_index.scss +1 -0
package/dist/index.cjs CHANGED
@@ -2841,237 +2841,921 @@ const Dropdown = withSubComponents(DropdownComponent, {
2841
2841
  Divider: DropdownDivider,
2842
2842
  }, 'Dropdown');
2843
2843
 
2844
- // Context to track MenuList nesting level
2845
- const MenuListLevelContext = React.createContext(0);
2844
+ const validButtonColors = [...validColors, 'text', 'ghost'];
2846
2845
  /**
2847
- * The `Menu` component provides Bulma's vertical navigation menu: a simple, accessible sidebar or section menu for your Bulma React UI.
2846
+ * The `Button` component provides a flexible and highly customizable button for your Bulma React UI.
2848
2847
  *
2849
2848
  * @function
2850
- * @param {MenuProps} props - Props for the Menu component.
2851
- * @returns {JSX.Element} The rendered menu.
2852
- * @see {@link https://bulma.io/documentation/components/menu/ | Bulma Menu documentation}
2849
+ * @param {ButtonProps} props - Props for the Button component.
2850
+ * @param {React.Ref} ref - Forwarded ref to the element `as` renders.
2851
+ * @returns {JSX.Element} The rendered button, anchor, or custom element.
2852
+ * @see {@link https://bulma.io/documentation/elements/button/ | Bulma Button documentation}
2853
2853
  */
2854
- const MenuComponent = ({ className, children, ...props }) => {
2855
- const { bulmaHelperClasses, rest } = useBulmaClasses(props);
2854
+ const Button = React.forwardRef(function Button(props, ref) {
2855
+ const { color, size, isLight, isRounded, isLoading, isStatic, isFullwidth, isFullWidth, isOutlined, isInverted, isFocused, isActive, isHovered, isDisabled, className, children, textColor, bgColor, as: Component = 'button', href, onClick, target, rel, ...bulmaProps } = props;
2856
+ const { bulmaHelperClasses, rest } = useBulmaClasses({
2857
+ color: textColor,
2858
+ backgroundColor: bgColor,
2859
+ ...bulmaProps,
2860
+ });
2856
2861
  // Generate Bulma classes with prefix
2857
- const bulmaClasses = usePrefixedClassNames('menu');
2858
- return (jsxRuntime.jsx("aside", { className: classNames(bulmaClasses, bulmaHelperClasses, className), ...rest, children: children }));
2859
- };
2862
+ const bulmaClasses = usePrefixedClassNames('button', {
2863
+ [`is-${color}`]: color &&
2864
+ validButtonColors.includes(color),
2865
+ [`is-${size}`]: size,
2866
+ 'is-outlined': isOutlined,
2867
+ 'is-light': isLight,
2868
+ 'is-loading': isLoading,
2869
+ 'is-static': isStatic,
2870
+ 'is-disabled': isDisabled,
2871
+ 'is-rounded': isRounded,
2872
+ 'is-hovered': isHovered,
2873
+ 'is-focused': isFocused,
2874
+ 'is-active': isActive,
2875
+ 'is-inverted': isInverted,
2876
+ 'is-fullwidth': isFullwidth ?? isFullWidth,
2877
+ });
2878
+ // Combine prefixed Bulma classes with unprefixed user className and prefixed helper classes
2879
+ const buttonClasses = classNames(bulmaClasses, bulmaHelperClasses, className);
2880
+ if (Component !== 'button') {
2881
+ // Strip the form-control attributes a link-like element cannot carry, so a
2882
+ // JavaScript consumer or a spread object doesn't put them on the DOM. The
2883
+ // types no longer allow them through; this is the runtime backstop.
2884
+ //
2885
+ // Only for INTRINSIC tags. A custom component (a router Link, ...) owns its
2886
+ // prop contract, and `ComponentPropsWithoutRef<T>` promises the caller that
2887
+ // its props reach it. Stripping names by their spelling alone broke that
2888
+ // promise silently: `<Button as={Custom} name="x" />` type-checked while
2889
+ // `Custom` never received the `name` it requires.
2890
+ //
2891
+ // `autoFocus` and `type` are deliberately NOT stripped: both are valid on
2892
+ // targets this list was filtering them from. `autoFocus` is a global
2893
+ // attribute (React decides per element whether to honour it, which is not
2894
+ // ours to pre-empt), and `type` on an anchor is the MIME hint that
2895
+ // `ButtonProps<'a'>` now types it as.
2896
+ //
2897
+ // The types already answer this for a TypeScript caller: `ButtonProps<T>`
2898
+ // derives from `ComponentPropsWithoutRef<T>`, so only what the target
2899
+ // accepts can be passed. What is left is a runtime backstop for JavaScript
2900
+ // consumers and spread objects.
2901
+ //
2902
+ // Keep it to the two cases where a stray attribute does something. The
2903
+ // submit-overrides belong to a submit control and never make sense on the
2904
+ // anchor path. `disabled` is the one that is VISIBLE: Bulma styles
2905
+ // `.button[disabled]` (background, border, shadow, opacity), so letting it
2906
+ // through to a `<span>` greys the element out — main stripped it, and not
2907
+ // stripping it would be a silent visual change on a types-only release.
2908
+ //
2909
+ // Everything else (`name`, `value`, `form`) is inert where it does not
2910
+ // belong, and enumerating which tags own it is what kept going wrong:
2911
+ // first all nine names on every non-button tag (so `<Button as="input"
2912
+ // name="query" value="Search">` submitted nothing), then a form-control
2913
+ // allowlist that still lost `formAction` on an input and `value` on an
2914
+ // option. Those now ride on the types instead.
2915
+ //
2916
+ // A custom component is exempt entirely — it owns its prop contract, and
2917
+ // `ComponentPropsWithoutRef<T>` promises the caller its props arrive.
2918
+ //
2919
+ // The elements that own `disabled`, per the HTML spec. A closed set, unlike
2920
+ // the open-ended question of which tag owns which attribute.
2921
+ const SUBMIT_OVERRIDES = [
2922
+ 'formAction',
2923
+ 'formEncType',
2924
+ 'formMethod',
2925
+ 'formNoValidate',
2926
+ 'formTarget',
2927
+ ];
2928
+ const OWNS_DISABLED = [
2929
+ 'button',
2930
+ 'fieldset',
2931
+ 'input',
2932
+ 'optgroup',
2933
+ 'option',
2934
+ 'select',
2935
+ 'textarea',
2936
+ ];
2937
+ // Two independent filters. They are not exclusive — an `<a>` owns neither
2938
+ // the submit-overrides nor `disabled`, so it takes both — and deriving them
2939
+ // from one destructure, or from one if/else, is what broke each of them in
2940
+ // turn: first the overrides came off every intrinsic (an `as="input"` lost
2941
+ // `formAction`), then scoping them to the anchor let `disabled` back onto
2942
+ // it.
2943
+ const forwardedRest = { ...rest };
2944
+ if (typeof Component === 'string' && !isCustomElement(Component)) {
2945
+ // Meaningless anywhere but a submit control, and an anchor is not one.
2946
+ if (Component === 'a') {
2947
+ for (const key of SUBMIT_OVERRIDES)
2948
+ delete forwardedRest[key];
2949
+ }
2950
+ // Visible via Bulma's `.button[disabled]`, so withheld from any element
2951
+ // that does not own it.
2952
+ if (!OWNS_DISABLED.includes(Component))
2953
+ delete forwardedRest.disabled;
2954
+ }
2955
+ return (jsxRuntime.jsx(Component, { ref: ref, className: buttonClasses, href: href, target: target, rel: rel, "aria-disabled": isDisabled, tabIndex: isDisabled ? -1 : undefined, onClick: isDisabled
2956
+ ? // Guarded, because this handler is installed into whatever `as`
2957
+ // renders and a custom component's `onClick` need not be a DOM
2958
+ // event handler at all. One declaring `onClick: (v: string) =>
2959
+ // void` calls this with a string, and an unguarded
2960
+ // `e.preventDefault()` throws `is not a function`. Blocking the
2961
+ // real event is the point; a foreign payload is simply not ours
2962
+ // to act on.
2963
+ (e) => {
2964
+ if (typeof e?.preventDefault === 'function')
2965
+ e.preventDefault();
2966
+ }
2967
+ : onClick, ...forwardedRest, children: children }));
2968
+ }
2969
+ return (jsxRuntime.jsx("button", { ref: ref, className: buttonClasses, disabled: isDisabled, onClick: onClick, ...rest, children: children }));
2970
+ });
2971
+ Button.displayName = 'Button';
2972
+
2860
2973
  /**
2861
- * Bulma Menu label component.
2974
+ * The `LinkButton` component renders a `<button>` that visually looks like text or a link.
2862
2975
  *
2863
2976
  * @function
2864
- * @param {MenuLabelProps} props - Props for the MenuLabel component.
2865
- * @returns {JSX.Element} The rendered menu label.
2977
+ * @param {LinkButtonProps} props - Props for the LinkButton component.
2978
+ * @param {React.Ref} ref - Forwarded ref to the element `as` renders.
2979
+ * @returns {JSX.Element} The rendered link-styled button element.
2980
+ *
2981
+ * @example
2982
+ * // Text variant (default)
2983
+ * <LinkButton onClick={handleClick}>Click me</LinkButton>
2984
+ *
2985
+ * @example
2986
+ * // Underline variant with color
2987
+ * <LinkButton variant="underline" color="primary">Learn more</LinkButton>
2866
2988
  */
2867
- const MenuLabel = ({ className, children, ...props }) => {
2868
- const { bulmaHelperClasses, rest } = useBulmaClasses(props);
2869
- return (jsxRuntime.jsx("p", { className: classNames(usePrefixedClassNames('menu-label'), className, bulmaHelperClasses), ...rest, children: children }));
2870
- };
2989
+ const LinkButton = React.forwardRef(function LinkButton({ variant = 'text', color, className, ...props }, ref) {
2990
+ const buttonColor = variant === 'underline' ? 'text' : variant;
2991
+ const prefixedClasses = usePrefixedClassNames('link-button', color && `link-button-${color}`, variant === 'underline' && 'link-button-underline');
2992
+ return (jsxRuntime.jsx(Button
2993
+ // LinkButton is polymorphic through to Button, which resolves the element
2994
+ // from `as`. TS cannot infer that through the spread below.
2995
+ , {
2996
+ // LinkButton is polymorphic through to Button, which resolves the element
2997
+ // from `as`. TS cannot infer that through the spread below.
2998
+ ref: ref, color: buttonColor, className: classNames(prefixedClasses, className), ...props }));
2999
+ });
3000
+ LinkButton.displayName = 'LinkButton';
3001
+
2871
3002
  /**
2872
- * MenuList applies `menu-list` class only at the top level (not for nested lists).
3003
+ * The `ToggleButton` component renders a button that stays pressed until it's pressed again, and says whether it's on through `aria-pressed`.
3004
+ *
3005
+ * It's always a `<button type="button">`, so it never submits a form and
3006
+ * carries no form value: a setting saved with a form is a `Checkbox` or a
3007
+ * `Switch`. A set of toggles that each turn on and off on their own goes in
3008
+ * `Buttons hasAddons` with `role="group"` and an `aria-label`. A set where only
3009
+ * one can be on is a radio group rather than a row of these: use
3010
+ * `SegmentedControl`, or `Radios` for a choice that's sent with a form.
2873
3011
  *
2874
3012
  * @function
2875
- * @param {MenuListProps} props - Props for the MenuList component.
2876
- * @returns {JSX.Element} The rendered menu list.
2877
- */
2878
- const MenuList = ({ className, children, ...props }) => {
2879
- const level = React.useContext(MenuListLevelContext);
2880
- const { bulmaHelperClasses, rest } = useBulmaClasses(props);
2881
- const ulClass = classNames(className, bulmaHelperClasses, {
2882
- [usePrefixedClassNames('menu-list')]: level === 0,
2883
- });
2884
- // Increment level for nested MenuLists
2885
- return (jsxRuntime.jsx(MenuListLevelContext.Provider, { value: level + 1, children: jsxRuntime.jsx("ul", { className: ulClass || undefined, ...rest, children: children }) }));
2886
- };
2887
- /**
2888
- * `href` alone — annotated so a typo is a compile error rather than a silent
2889
- * no-op, the way both siblings' sets are.
3013
+ * @param {ToggleButtonProps} props - Props for the ToggleButton component.
3014
+ * @param {React.Ref<HTMLButtonElement>} ref - Forwarded ref to the `<button>`.
3015
+ * @returns {JSX.Element} The rendered toggle button.
3016
+ * @see {@link https://www.w3.org/WAI/ARIA/apg/patterns/button/ | APG button pattern}
3017
+ *
3018
+ * @example
3019
+ * <ToggleButton onPressedChange={setMuted}>Mute</ToggleButton>
3020
+ *
3021
+ * @example
3022
+ * <Buttons hasAddons role="group" aria-label="Text style">
3023
+ * <ToggleButton aria-label="Bold" pressed={bold} onPressedChange={setBold}>
3024
+ * <Icon name="bold" />
3025
+ * </ToggleButton>
3026
+ * </Buttons>
2890
3027
  */
2891
- const STRIP_FROM_NON_LINK = { href: true };
3028
+ const ToggleButton = React.forwardRef(function ToggleButton({ pressed: controlledPressed, defaultPressed = false, onPressedChange, color = 'link', isLoading, 'aria-busy': ariaBusy, 'aria-disabled': ariaDisabled, className, onClick, ...props }, ref) {
3029
+ const isControlled = controlledPressed !== undefined;
3030
+ const [internalPressed, setInternalPressed] = React.useState(defaultPressed);
3031
+ const pressed = controlledPressed ?? internalPressed;
3032
+ // Prefixed like Button's own classes, which a hand-written
3033
+ // `className="is-selected"` could not be under ConfigProvider classPrefix.
3034
+ // Bulma lifts `is-selected` above its neighbours in `.buttons.has-addons`.
3035
+ const selectedClass = usePrefixedClassNames({ 'is-selected': pressed });
3036
+ // The caller's onClick runs first, so a `preventDefault()` there vetoes
3037
+ // the toggle, as it does in Radix, MUI and Zag. Enter and Space reach here
3038
+ // through the button's own click, and a disabled button gets no click.
3039
+ //
3040
+ // A loading one gets the click but drops it. Bulma's spinner only stops a
3041
+ // pointer, so without this a key flipped a toggle the mouse couldn't.
3042
+ // `disabled` would stop both, but it takes focus away mid-save.
3043
+ const handleClick = (event) => {
3044
+ if (isLoading)
3045
+ return;
3046
+ onClick?.(event);
3047
+ if (event.defaultPrevented)
3048
+ return;
3049
+ const next = !pressed;
3050
+ onPressedChange?.(next);
3051
+ if (!isControlled)
3052
+ setInternalPressed(next);
3053
+ };
3054
+ // The props are spread first, so what a toggle depends on wins over
3055
+ // anything a JavaScript caller or a spread object passes along: it stays
3056
+ // a button, never submits, reports the state it's in, and says it's busy
3057
+ // and won't respond while it's loading. A caller's own `aria-busy` or
3058
+ // `aria-disabled` stays when it isn't loading.
3059
+ return (jsxRuntime.jsx(Button, { ...props, ref: ref, as: "button", type: "button", isLoading: isLoading, "aria-busy": isLoading || ariaBusy, "aria-disabled": isLoading || ariaDisabled, "aria-pressed": pressed, color: pressed ? color : undefined, className: classNames(selectedClass, className), onClick: handleClick }));
3060
+ });
3061
+ ToggleButton.displayName = 'ToggleButton';
3062
+
3063
+ const validButtonsSizes = ['small', 'medium', 'large'];
2892
3064
  /**
2893
- * MenuItem supports `as` prop for custom link components, e.g., react-router-dom Link.
3065
+ * The `Buttons` component lets you group multiple `Button` elements together with Bulma's spacing, alignment, and add-on features.
2894
3066
  *
2895
3067
  * @function
2896
- * @param {MenuItemProps} props - Props for the MenuItem component.
2897
- * @param {React.Ref} ref - Forwarded ref to the inner element `as` renders.
2898
- * @returns {JSX.Element} The rendered menu item.
3068
+ * @param {ButtonsProps} props - Props for the Buttons component.
3069
+ * @returns {JSX.Element} The rendered group of buttons.
3070
+ * @see {@link https://bulma.io/documentation/elements/button/#group | Bulma Button Group documentation}
2899
3071
  */
2900
- const MenuItem = React.forwardRef(function MenuItem(itemProps, ref) {
2901
- const { className, children, active, as: Component = 'a', 'data-testid': testId, style, id, title, role, tabIndex, ...rest } = itemProps;
2902
- const { bulmaHelperClasses, rest: forwarded } = useBulmaClasses(rest);
2903
- // `href` reaches an anchor or a custom component (which owns its own prop
2904
- // contract), but not another intrinsic tag — `<span href>` is invalid HTML.
2905
- // The same rule Avatar applies through `isLinkLike` and Button through its
2906
- // intrinsic-only strip. Before #641 `href` was an own prop re-applied only
2907
- // for `as="a"`; deriving it from `as` must not quietly widen that.
2908
- const isLinkLike = Component === 'a' ||
2909
- typeof Component !== 'string' ||
2910
- isCustomElement(Component);
2911
- // `href` ALONE. `Dropdown.Item` strips every anchor-only attribute except
2912
- // `type`; `Level.Item` strips all of them, plus `rel`.
2913
- //
2914
- // Not because nothing else can arrive: `as` here is an open `React.ElementType`,
2915
- // and a non-fresh spread skips excess-property checking, so a typed caller
2916
- // CAN deliver `target` or `download` and they are forwarded. The set stays at
2917
- // `href` because widening it would be wrong for an open `as` — `referrerPolicy`
2918
- // on `as="img"`, `target` on `as="form"` and `media` on `as="source"` are all
2919
- // legal, and a flat strip would delete them. Getting that right needs a
2920
- // per-attribute-per-element rule, which `bestax-migrate` already models in
2921
- // `LINK_ATTR_ELEMENTS`.
2922
- const linkProps = isLinkLike
2923
- ? forwarded
2924
- : omitAttrs(forwarded, STRIP_FROM_NON_LINK);
2925
- const itemClass = classNames({ [usePrefixedClassNames('is-active')]: active }, bulmaHelperClasses);
2926
- // Split children into label and nested MenuList(s)
2927
- const labelChildren = [];
2928
- const nestedMenuLists = [];
2929
- React.Children.forEach(children, child => {
2930
- if (React.isValidElement(child) && child.type === MenuList) {
2931
- nestedMenuLists.push(child);
2932
- }
2933
- else {
2934
- labelChildren.push(child);
2935
- }
3072
+ const ButtonsComponent = ({ className, textColor, color, bgColor, isCentered, isRight, hasAddons, size, children, ...props }) => {
3073
+ const buttonsClasses = usePrefixedClassNames('buttons', {
3074
+ 'is-centered': isCentered,
3075
+ 'is-right': isRight,
3076
+ 'has-addons': hasAddons,
3077
+ [`are-${size}`]: size && validButtonsSizes.includes(size),
2936
3078
  });
2937
- return (jsxRuntime.jsxs("li", { className: className, "data-testid": testId, style: style, id: id, title: title, role: role, tabIndex: tabIndex, children: [jsxRuntime.jsx(Component, { ref: ref, className: itemClass || undefined, ...linkProps, ...(Component === 'button'
2938
- ? { type: buttonType(linkProps.type) }
2939
- : {}), children: labelChildren }), nestedMenuLists] }));
2940
- });
2941
- MenuItem.displayName = 'MenuItem';
2942
- // Attach static subcomponents
2943
- const Menu = withSubComponents(MenuComponent, {
2944
- Label: MenuLabel,
2945
- List: MenuList,
2946
- Item: MenuItem,
2947
- }, 'Menu');
2948
-
2949
- const validMessageSizes = ['small', 'medium', 'large'];
2950
- /**
2951
- * The `Message` component provides Bulma's flexible notice/message box for your Bulma React UI.
2952
- *
2953
- * @function
2954
- * @param {MessageProps} props - Props for the Message component.
2955
- * @returns {JSX.Element} The rendered message.
2956
- * @see {@link https://bulma.io/documentation/components/message/ | Bulma Message documentation}
2957
- */
2958
- const MessageComponent = ({ className, title, textColor, color, bgColor, size, onClose, children, ...props }) => {
2959
- const { classPrefix } = useConfig();
3079
+ /**
3080
+ * Generates Bulma helper classes and separates out remaining props.
3081
+ */
2960
3082
  const { bulmaHelperClasses, rest } = useBulmaClasses({
2961
- color: textColor,
3083
+ color: textColor ?? color,
2962
3084
  backgroundColor: bgColor,
2963
3085
  ...props,
2964
3086
  });
2965
- // Generate Bulma classes with prefix
2966
- const bulmaClasses = usePrefixedClassNames('message', {
2967
- [`is-${color}`]: color,
2968
- [`is-${size}`]: size && validMessageSizes.includes(size),
2969
- });
2970
- const deleteClass = usePrefixedClassNames('delete');
2971
- const messageClasses = classNames(bulmaClasses, bulmaHelperClasses, className);
2972
- return (jsxRuntime.jsxs("article", { className: messageClasses, ...rest, "data-testid": "message", children: [(title || onClose) && (jsxRuntime.jsxs("div", { className: prefixedClassNames(classPrefix, 'message-header'), children: [title && jsxRuntime.jsx("span", { children: title }), onClose && (jsxRuntime.jsx("button", { className: deleteClass, "aria-label": "delete", onClick: onClose, type: "button", "data-testid": "message-close" }))] })), children && (jsxRuntime.jsx("div", { className: prefixedClassNames(classPrefix, 'message-body'), "data-testid": "message-body", children: children }))] }));
3087
+ const combinedClasses = classNames(buttonsClasses, className, bulmaHelperClasses);
3088
+ return (jsxRuntime.jsx("div", { className: combinedClasses, ...rest, children: children }));
2973
3089
  };
2974
- /**
2975
- * Message header compound component. Renders a `.message-header` element.
2976
- *
2977
- * @function
2978
- * @param {MessageHeaderProps} props - Props for the MessageHeader component.
2979
- * @returns {JSX.Element} The rendered message header.
2980
- */
2981
- const MessageHeader = ({ className, children, ...props }) => (jsxRuntime.jsx("div", { className: classNames(usePrefixedClassNames('message-header'), className), ...props, children: children }));
2982
- /**
2983
- * Message body compound component. Renders a `.message-body` element.
2984
- *
2985
- * @function
2986
- * @param {MessageBodyProps} props - Props for the MessageBody component.
2987
- * @returns {JSX.Element} The rendered message body.
2988
- */
2989
- const MessageBody = ({ className, children, ...props }) => (jsxRuntime.jsx("div", { className: classNames(usePrefixedClassNames('message-body'), className), ...props, children: children }));
2990
- // Attach compound components
2991
- const Message = withSubComponents(MessageComponent, {
2992
- Header: MessageHeader,
2993
- Body: MessageBody,
2994
- }, 'Message');
3090
+ const Buttons = withSubComponents(ButtonsComponent, {
3091
+ Button,
3092
+ LinkButton,
3093
+ ToggleButton,
3094
+ }, 'Buttons');
2995
3095
 
2996
- // Ref-counted across every caller (e.g. a Dialog rendering its own Modal)
2997
- // so overlapping/nested locks don't fight over which one restores `overflow`.
2998
- // Every overlay that locks body scroll — Modal, Dialog, Sidebar, Loading —
2999
- // goes through here; a component setting `document.body.style.overflow`
3000
- // itself would clear a lock another overlay still needs.
3001
- let lockCount = 0;
3002
- let originalOverflow = '';
3003
3096
  /**
3004
- * Locks (and ref-counted-ly unlocks) `document.body` scrolling while `active`
3005
- * is true. Safe to call from multiple components at once — the body scroll
3006
- * is only restored once every active caller has released its lock.
3097
+ * Strips a redundant leading library prefix from an icon name (e.g. `fa-check` -> `check`
3098
+ * for the `fa` library, `mdi-account` -> `account` for `mdi`), so `name` behaves the same
3099
+ * whether or not the caller includes the prefix. Font Awesome and Material Design Icons docs
3100
+ * conventionally refer to icons with their class prefix (`fa-check`), which otherwise gets
3101
+ * doubled by `getIconClasses` (producing `fa-fa-check`, matching no glyph).
3007
3102
  *
3008
- * @function useScrollLock
3009
- * @param active - Whether this caller wants the body scroll locked.
3103
+ * @param {string} name - The icon name, possibly including a redundant library prefix.
3104
+ * @param {IconLibrary} library - The icon library the name will be rendered with.
3105
+ * @returns {string} The icon name with any redundant leading library prefix removed.
3010
3106
  */
3011
- function useScrollLock(active) {
3012
- React.useEffect(() => {
3013
- if (!active)
3014
- return undefined;
3015
- lockCount++;
3016
- if (lockCount === 1) {
3017
- originalOverflow = document.body.style.overflow;
3018
- document.body.style.overflow = 'hidden';
3019
- }
3020
- return () => {
3021
- lockCount--;
3022
- if (lockCount === 0) {
3023
- document.body.style.overflow = originalOverflow;
3024
- }
3025
- };
3026
- }, [active]);
3107
+ function stripRedundantLibraryPrefix(name, library) {
3108
+ if (!name) {
3109
+ return name;
3110
+ }
3111
+ if (library === 'fa' && name.startsWith('fa-')) {
3112
+ return name.substring(3);
3113
+ }
3114
+ if (library === 'mdi' && name.startsWith('mdi-')) {
3115
+ return name.substring(4);
3116
+ }
3117
+ return name;
3027
3118
  }
3028
-
3029
- // "Are we past hydration?" as a store that never changes: React reads the
3030
- // server snapshot both while server-rendering and while hydrating, and the
3031
- // client snapshot from the first post-hydration render onward.
3032
- const subscribeToNothing = () => () => { };
3033
- const getClientSnapshot = () => true;
3034
- const getServerSnapshot = () => false;
3035
3119
  /**
3036
- * Reports whether the component has passed hydration.
3037
- *
3038
- * Returns `false` during server rendering and during the hydrating client
3039
- * render, then `true` from the commit that follows. Use it to defer
3040
- * DOM-only behaviour (such as portalling) past hydration so the first client
3041
- * render matches the server markup instead of tripping hydration recovery.
3042
- *
3043
- * Only a component that hydrates ever sees `false` on the client. One that
3044
- * mounts later (after a route change, or in an app with no server rendering)
3045
- * gets `true` on its first render, so it pays for no extra render. It
3046
- * subscribes to nothing, so no later render flips it back.
3120
+ * Gets the correct classes for the icon element based on the library and features.
3047
3121
  *
3048
- * @function useIsHydrated
3049
- * @returns `true` once the hydrating render has completed, otherwise `false`.
3122
+ * @param {IconLibrary} library - The icon library.
3123
+ * @param {string} name - The icon name.
3124
+ * @param {string} [variant] - Icon style variant (e.g., 'solid', 'outlined', 'rounded').
3125
+ * @param {string | string[]} [features] - Additional library-specific modifiers.
3126
+ * @returns {string} The combined class string for the icon.
3050
3127
  */
3051
- function useIsHydrated() {
3052
- return React.useSyncExternalStore(subscribeToNothing, getClientSnapshot, getServerSnapshot);
3128
+ function getIconClasses(library, name, variant, features) {
3129
+ let baseClass;
3130
+ let iconClass;
3131
+ const featureList = Array.isArray(features)
3132
+ ? features
3133
+ : features
3134
+ ? [features]
3135
+ : [];
3136
+ switch (library) {
3137
+ case 'fa': {
3138
+ // Font Awesome: use variant as style ('solid' -> 'fas', 'regular' -> 'far', etc.)
3139
+ const styleMap = {
3140
+ solid: 'fas',
3141
+ regular: 'far',
3142
+ brands: 'fab',
3143
+ light: 'fal',
3144
+ duotone: 'fad',
3145
+ thin: 'fat',
3146
+ };
3147
+ const faStyle = variant ? styleMap[variant] || variant : 'fas';
3148
+ baseClass = faStyle;
3149
+ iconClass = `fa-${name}`;
3150
+ return [baseClass, iconClass, ...featureList].join(' ');
3151
+ }
3152
+ case 'mdi':
3153
+ // Material Design Icons: no variants, just features
3154
+ baseClass = 'mdi';
3155
+ iconClass = `mdi-${name}`;
3156
+ return [baseClass, iconClass, ...featureList].join(' ');
3157
+ case 'material-icons': {
3158
+ // Google Material Icons: map variants to full class names
3159
+ const styleVariants = {
3160
+ filled: 'material-icons',
3161
+ outlined: 'material-icons-outlined',
3162
+ round: 'material-icons-round',
3163
+ sharp: 'material-icons-sharp',
3164
+ };
3165
+ baseClass = variant
3166
+ ? styleVariants[variant] || `material-icons-${variant}`
3167
+ : 'material-icons';
3168
+ return [baseClass, ...featureList].join(' ');
3169
+ }
3170
+ case 'material-symbols': {
3171
+ // Google Material Symbols: map variants to full class names
3172
+ const styleVariants = {
3173
+ outlined: 'material-symbols-outlined',
3174
+ rounded: 'material-symbols-rounded',
3175
+ sharp: 'material-symbols-sharp',
3176
+ };
3177
+ baseClass = variant
3178
+ ? styleVariants[variant] || `material-symbols-${variant}`
3179
+ : 'material-symbols-outlined';
3180
+ return [baseClass, ...featureList].join(' ');
3181
+ }
3182
+ default:
3183
+ // fallback: just icon name and features
3184
+ return [name, ...featureList].join(' ');
3185
+ }
3053
3186
  }
3054
-
3055
3187
  /**
3056
- * Resolves a portal target: an `HTMLElement` is used directly, a non-empty
3057
- * `string` is treated as a `document.querySelector` selector (falling back to
3058
- * `document.body` when it matches nothing), and any falsy value — `undefined`
3059
- * or the empty string — resolves to `document.body`.
3060
- *
3061
- * The empty string is deliberately treated as "no target" rather than passed
3062
- * through: `document.querySelector('')` throws a `SyntaxError`, and callers
3063
- * building a selector from state can easily hand us `''`.
3188
+ * The `Icon` component is a Bulma-styled wrapper for displaying icons from various libraries (Font Awesome, Material Design Icons, Ionicons, Google Material Icons, Material Symbols, etc.).
3064
3189
  *
3065
- * @function resolvePortalContainer
3066
- * @param container - The requested portal target, if any.
3067
- * @returns The resolved DOM node to portal into.
3190
+ * @function
3191
+ * @param {IconProps} props - Props for the Icon component.
3192
+ * @returns {JSX.Element} The rendered icon element.
3193
+ * @see {@link https://bulma.io/documentation/elements/icon/ | Bulma Icon documentation}
3068
3194
  */
3069
- function resolvePortalContainer(container) {
3070
- if (!container) {
3071
- return document.body;
3072
- }
3073
- if (typeof container === 'string') {
3074
- return (document.querySelector(container) ?? document.body);
3195
+ const Icon = ({ className, textColor, bgColor, name, library, variant, features, libraryFeatures, // Deprecated but maintained for backward compatibility
3196
+ size, ariaLabel, style, icon, // Capture and exclude the deprecated 'icon' prop from DOM
3197
+ color: _color, // Exclude 'color' prop if passed directly
3198
+ containerClassName, children, ...restProps }) => {
3199
+ // Get the default icon library from context, fallback to 'fa' if not set
3200
+ const defaultLibrary = useIconLibrary();
3201
+ /**
3202
+ * Generates Bulma helper classes and separates out remaining props.
3203
+ * Note: variant, features, and libraryFeatures are excluded from props spread
3204
+ */
3205
+ const { bulmaHelperClasses, rest } = useBulmaClasses({
3206
+ color: textColor,
3207
+ backgroundColor: bgColor,
3208
+ ...restProps,
3209
+ });
3210
+ // Hoisted unconditionally to respect rules-of-hooks; the branches below
3211
+ // pick which result to consume.
3212
+ const defaultIconClasses = usePrefixedClassNames('icon', {
3213
+ [`is-${size}`]: size,
3214
+ });
3215
+ const sizeModifierClass = usePrefixedClassNames(size ? `is-${size}` : undefined);
3216
+ const bulmaClasses = containerClassName
3217
+ ? containerClassName
3218
+ : defaultIconClasses;
3219
+ const iconContainerClasses = classNames(bulmaClasses, containerClassName && size ? sizeModifierClass : undefined, bulmaHelperClasses, className);
3220
+ // A named icon is an image, whose name is its label alone, so a ligature inside
3221
+ // never leaks into it. An unnamed one is decorative and hidden outright. Spread
3222
+ // before `rest`, so a caller's own `role` or `aria-hidden` still wins.
3223
+ const named = !!ariaLabel || !!restProps['aria-label'] || !!restProps['aria-labelledby'];
3224
+ // A caller's own `role` or `tabIndex` on an unnamed icon means it isn't decoration (an
3225
+ // icon made a button, say), so it isn't hidden either: `aria-hidden` on something
3226
+ // focusable is a failure of its own.
3227
+ const a11yProps = named
3228
+ ? { role: 'img', 'aria-label': ariaLabel || undefined }
3229
+ : restProps.role !== undefined || restProps.tabIndex !== undefined
3230
+ ? {}
3231
+ : { 'aria-hidden': true };
3232
+ if (children !== undefined) {
3233
+ // `IconChildrenProps`: render the caller's node (an inline SVG, a `react-icons`
3234
+ // component, …) in place of a class-based glyph. Library/variant/features don't apply.
3235
+ return (jsxRuntime.jsx("span", { className: iconContainerClasses, ...a11yProps, style: style, ...rest, children: children }));
3236
+ }
3237
+ // `name` is guaranteed once `children` is absent UNLESS the caller is on the deprecated
3238
+ // `icon` path, which `IconDeprecatedProps` declares with `name?: undefined`.
3239
+ // (`IconChildrenProps['children']` excludes `undefined`, so `children={undefined}` can't
3240
+ // slip past into this branch.) The cast covers that third member, and the `!name && icon`
3241
+ // fallback below is what fills it in.
3242
+ let finalName = name;
3243
+ if (!name && icon) {
3244
+ // If icon prop is provided instead of name, try to parse it
3245
+ // e.g., "mdi mdi-rocket-launch" -> "rocket-launch"
3246
+ if (typeof icon === 'string') {
3247
+ const parts = icon.split(' ');
3248
+ const lastPart = parts[parts.length - 1];
3249
+ if (lastPart.startsWith('mdi-')) {
3250
+ finalName = lastPart.substring(4); // Remove "mdi-" prefix
3251
+ }
3252
+ else if (lastPart.startsWith('fa-')) {
3253
+ finalName = lastPart.substring(3); // Remove "fa-" prefix
3254
+ }
3255
+ else {
3256
+ finalName = lastPart;
3257
+ }
3258
+ }
3259
+ }
3260
+ const finalLibrary = library || defaultLibrary || 'fa';
3261
+ // Normalize a redundant leading library prefix (e.g. "fa-check" -> "check" when the
3262
+ // library is 'fa'), so `name` behaves identically with or without the prefix.
3263
+ finalName = stripRedundantLibraryPrefix(finalName, finalLibrary);
3264
+ if (!finalName) {
3265
+ // No glyph to name. Reachable through the public type only as `icon=""`, and otherwise
3266
+ // by a plain-JS caller (or one that casts). Building a class off an absent name produced
3267
+ // a bogus `fa-undefined` glyph; render the bare container instead, matching the
3268
+ // `children` branch.
3269
+ return (jsxRuntime.jsx("span", { className: iconContainerClasses, ...a11yProps, style: style, ...rest }));
3270
+ }
3271
+ // Backward compatibility: if libraryFeatures is provided, parse it for variant and features
3272
+ let finalVariant = variant;
3273
+ let finalFeatures = features;
3274
+ if (libraryFeatures && !variant && !features) {
3275
+ const legacyFeatures = Array.isArray(libraryFeatures)
3276
+ ? libraryFeatures
3277
+ : [libraryFeatures];
3278
+ // For Font Awesome, extract style from features
3279
+ if (finalLibrary === 'fa') {
3280
+ const faStyle = legacyFeatures.find(f => [
3281
+ 'fas',
3282
+ 'far',
3283
+ 'fab',
3284
+ 'fal',
3285
+ 'fad',
3286
+ 'fat',
3287
+ 'solid',
3288
+ 'regular',
3289
+ 'brands',
3290
+ 'light',
3291
+ 'duotone',
3292
+ 'thin',
3293
+ ].includes(f));
3294
+ if (faStyle) {
3295
+ finalVariant = faStyle;
3296
+ finalFeatures = legacyFeatures.filter(f => f !== faStyle);
3297
+ }
3298
+ else {
3299
+ finalFeatures = legacyFeatures;
3300
+ }
3301
+ }
3302
+ // For Material Icons/Symbols, extract style variant
3303
+ else if (finalLibrary === 'material-icons' ||
3304
+ finalLibrary === 'material-symbols') {
3305
+ const styleVariants = finalLibrary === 'material-icons'
3306
+ ? ['filled', 'outlined', 'round', 'sharp']
3307
+ : ['outlined', 'rounded', 'sharp'];
3308
+ const styleVariant = legacyFeatures.find(f => styleVariants.includes(f));
3309
+ if (styleVariant) {
3310
+ finalVariant = styleVariant;
3311
+ finalFeatures = legacyFeatures.filter(f => f !== styleVariant);
3312
+ }
3313
+ else {
3314
+ finalFeatures = legacyFeatures;
3315
+ }
3316
+ }
3317
+ // For others, all features go to finalFeatures
3318
+ else {
3319
+ finalFeatures = legacyFeatures;
3320
+ }
3321
+ }
3322
+ // Handle web components vs CSS-based icons
3323
+ if (finalLibrary === 'ion') {
3324
+ // For Ionicons, handle variant in the name
3325
+ let ionName = finalName;
3326
+ if (finalVariant === 'outline') {
3327
+ ionName = `${finalName}-outline`;
3328
+ }
3329
+ else if (finalVariant === 'sharp') {
3330
+ ionName = `${finalName}-sharp`;
3331
+ }
3332
+ return (jsxRuntime.jsx("span", { className: iconContainerClasses, ...a11yProps, style: style, ...rest, children: jsxRuntime.jsx("ion-icon", { name: ionName }) }));
3333
+ }
3334
+ // Legacy CSS-based icons
3335
+ const iClasses = getIconClasses(finalLibrary, finalName, finalVariant, finalFeatures);
3336
+ // Material Icons and Material Symbols use text content, not CSS classes for the icon name
3337
+ if (finalLibrary === 'material-icons' ||
3338
+ finalLibrary === 'material-symbols') {
3339
+ return (jsxRuntime.jsx("span", { className: iconContainerClasses, ...a11yProps, style: style, ...rest, children: jsxRuntime.jsx("i", { className: iClasses, children: finalName }) }));
3340
+ }
3341
+ return (jsxRuntime.jsx("span", { className: iconContainerClasses, ...a11yProps, style: style, ...rest, children: jsxRuntime.jsx("i", { className: iClasses }) }));
3342
+ };
3343
+
3344
+ /**
3345
+ * The `Paragraph` component renders a styled `<p>` element with Bulma helper class integration.
3346
+ *
3347
+ * @function
3348
+ * @param {ParagraphProps} props - Props for the Paragraph component.
3349
+ * @returns {JSX.Element} The rendered paragraph element.
3350
+ */
3351
+ const Paragraph = ({ className, textColor, bgColor, children, ...props }) => {
3352
+ /**
3353
+ * Generates Bulma helper classes and separates out remaining props.
3354
+ */
3355
+ const { bulmaHelperClasses, rest } = useBulmaClasses({
3356
+ color: textColor,
3357
+ backgroundColor: bgColor,
3358
+ ...props,
3359
+ });
3360
+ const bulmaClasses = usePrefixedClassNames();
3361
+ const paragraphClasses = classNames(bulmaClasses, bulmaHelperClasses, className);
3362
+ return (jsxRuntime.jsx("p", { className: paragraphClasses || undefined, ...rest, children: children }));
3363
+ };
3364
+
3365
+ const validTitleSizes = ['1', '2', '3', '4', '5', '6'];
3366
+ const validTitleElements = ['h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'p'];
3367
+ /**
3368
+ * The `Title` component renders a Bulma-styled title (heading), supporting sizes `1`-`6`, spacing, and rendering as any heading or paragraph element (`h1`-`h6`, `p`).
3369
+ *
3370
+ * @function
3371
+ * @param {TitleProps} props - Props for the Title component.
3372
+ * @returns {JSX.Element} The rendered title element.
3373
+ * @see {@link https://bulma.io/documentation/elements/title/ | Bulma Title documentation}
3374
+ */
3375
+ const Title = ({ className, size, isSpaced, as = 'h1', hasSkeleton, textColor, bgColor, children, ...props }) => {
3376
+ /**
3377
+ * Generates Bulma helper classes and separates out remaining props.
3378
+ */
3379
+ const { bulmaHelperClasses, rest } = useBulmaClasses({
3380
+ color: textColor,
3381
+ backgroundColor: bgColor,
3382
+ ...props,
3383
+ });
3384
+ // Validate 'as' prop at runtime
3385
+ const element = validTitleElements.includes(as) ? as : 'h1';
3386
+ // Validate 'size' prop at runtime (numeric sizes normalize to strings)
3387
+ const normalized = size == null ? undefined : String(size);
3388
+ const validSize = normalized && validTitleSizes.includes(normalized)
3389
+ ? normalized
3390
+ : undefined;
3391
+ const bulmaClasses = usePrefixedClassNames('title', {
3392
+ [`is-${validSize}`]: validSize,
3393
+ 'is-spaced': isSpaced,
3394
+ 'has-skeleton': hasSkeleton,
3395
+ });
3396
+ const titleClasses = classNames(bulmaClasses, bulmaHelperClasses, className);
3397
+ // Determine the tag based on 'element' and 'validSize'
3398
+ const Tag = element === 'p' ? 'p' : validSize ? `h${validSize}` : element;
3399
+ return (jsxRuntime.jsx(Tag, { className: titleClasses, ...rest, children: children }));
3400
+ };
3401
+
3402
+ const validEmptyStateColors = [
3403
+ 'primary',
3404
+ 'link',
3405
+ 'info',
3406
+ 'success',
3407
+ 'warning',
3408
+ 'danger',
3409
+ ];
3410
+ const validEmptyStateHeadings = ['h1', 'h2', 'h3', 'h4', 'h5', 'h6'];
3411
+ const validEmptyStateSizes = ['small', 'medium', 'large'];
3412
+ /**
3413
+ * The circle the picture sits in, hidden from assistive tech whatever is in it.
3414
+ *
3415
+ * @function
3416
+ * @param {EmptyStateIndicatorProps} props - Props for EmptyState.Indicator.
3417
+ * @returns {JSX.Element} The rendered indicator.
3418
+ */
3419
+ const EmptyStateIndicator = ({ className, textColor, bgColor, children, ...props }) => {
3420
+ const { bulmaHelperClasses, rest } = useBulmaClasses({
3421
+ color: textColor,
3422
+ backgroundColor: bgColor,
3423
+ ...props,
3424
+ });
3425
+ const indicatorClass = usePrefixedClassNames('empty-state-indicator');
3426
+ // Hidden after the rest of the props, so nothing passed in can expose the
3427
+ // picture: the title says what it means.
3428
+ return (jsxRuntime.jsx("div", { className: classNames(indicatorClass, bulmaHelperClasses, className), ...rest, "aria-hidden": "true", children: children }));
3429
+ };
3430
+ EmptyStateIndicator.displayName = 'EmptyState.Indicator';
3431
+ /**
3432
+ * The heading, a Bulma `Title` at the level `as` sets, `h2` by default, and at the EmptyState's size.
3433
+ *
3434
+ * @function
3435
+ * @param {EmptyStateTitleProps} props - Props for EmptyState.Title.
3436
+ * @returns {JSX.Element} The rendered heading.
3437
+ */
3438
+ const EmptyStateTitle = ({ as = 'h2', className, ...props }) => {
3439
+ // `size` isn't in the type, but on `Title` it picks the tag, so it's kept
3440
+ // off even when it's passed anyway. The level is `as` alone.
3441
+ const { size: _size, ...rest } = props;
3442
+ const titleClass = usePrefixedClassNames('empty-state-title');
3443
+ const level = validEmptyStateHeadings.includes(as) ? as : 'h2';
3444
+ return (jsxRuntime.jsx(Title, { as: level, className: classNames(titleClass, className), ...rest }));
3445
+ };
3446
+ EmptyStateTitle.displayName = 'EmptyState.Title';
3447
+ /**
3448
+ * A sentence or two under the title, in the color of the text around it.
3449
+ *
3450
+ * @function
3451
+ * @param {EmptyStateDescriptionProps} props - Props for EmptyState.Description.
3452
+ * @returns {JSX.Element} The rendered description.
3453
+ */
3454
+ const EmptyStateDescription = ({ className, ...props }) => {
3455
+ const descriptionClass = usePrefixedClassNames('empty-state-description');
3456
+ return (jsxRuntime.jsx(Paragraph, { className: classNames(descriptionClass, className), ...props }));
3457
+ };
3458
+ EmptyStateDescription.displayName = 'EmptyState.Description';
3459
+ /**
3460
+ * A row of buttons, centered under the text or at the start of the line beside the indicator.
3461
+ *
3462
+ * @function
3463
+ * @param {EmptyStateActionsProps} props - Props for EmptyState.Actions.
3464
+ * @returns {JSX.Element} The rendered row of buttons.
3465
+ */
3466
+ const EmptyStateActions = ({ className, ...props }) => {
3467
+ const actionsClass = usePrefixedClassNames('empty-state-actions');
3468
+ return jsxRuntime.jsx(Buttons, { className: classNames(actionsClass, className), ...props });
3469
+ };
3470
+ EmptyStateActions.displayName = 'EmptyState.Actions';
3471
+ /** Whether a shorthand prop was given something to render. */
3472
+ const isGiven = (node) => node !== undefined &&
3473
+ node !== null &&
3474
+ typeof node !== 'boolean' &&
3475
+ node !== '';
3476
+ /**
3477
+ * The `EmptyState` component renders the block an app shows where content would be: an empty list, a search with no results, or a first-run screen.
3478
+ *
3479
+ * It's a column of a picture in a circle, a heading, a line of text and the
3480
+ * actions, with one layout whether it's written with the `icon`, `title` and
3481
+ * `description` props or with the parts. The children always render as
3482
+ * written, after the parts the props render.
3483
+ *
3484
+ * It has no role and no live region. It arrives already filled and goes away
3485
+ * when results come back, so it can't announce a search that matched
3486
+ * nothing: keep a `role="status"` region beside the search field for that,
3487
+ * in the page the whole time. While the data loads, show a `Skeleton` or a
3488
+ * `Loader` instead, so "No projects yet" doesn't flash first, and show a
3489
+ * load that failed in a `Message` or a `Notification`.
3490
+ *
3491
+ * @function
3492
+ * @param {EmptyStateProps} props - Props for the EmptyState component.
3493
+ * @param {React.Ref<HTMLDivElement>} ref - Forwarded ref to the root `<div>`.
3494
+ * @returns {JSX.Element} The rendered empty state.
3495
+ *
3496
+ * @example
3497
+ * <EmptyState
3498
+ * icon="folder-open"
3499
+ * title="No projects yet"
3500
+ * description="Projects you create or join show up here."
3501
+ * >
3502
+ * <Button color="primary">New project</Button>
3503
+ * </EmptyState>
3504
+ */
3505
+ const EmptyStateRoot = React.forwardRef(({ className, icon, title, titleAs = 'h2', description, children, color, variant = 'light', size = 'normal', align = 'center', textColor, bgColor, ...props }, ref) => {
3506
+ const { bulmaHelperClasses, rest } = useBulmaClasses({
3507
+ color: textColor,
3508
+ backgroundColor: bgColor,
3509
+ ...props,
3510
+ });
3511
+ const rootClass = usePrefixedClassNames('empty-state', {
3512
+ [`is-${size}`]: validEmptyStateSizes.includes(size),
3513
+ 'is-left': align === 'left',
3514
+ [`is-${color}`]: !!color && validEmptyStateColors.includes(color),
3515
+ 'is-filled': variant === 'filled',
3516
+ });
3517
+ // The props' parts first, each only when it's given, then the children
3518
+ // exactly as they were written: nothing here looks at them.
3519
+ return (jsxRuntime.jsxs("div", { ref: ref, className: classNames(rootClass, bulmaHelperClasses, className), ...rest, children: [isGiven(icon) && (jsxRuntime.jsx(EmptyStateIndicator, { children: typeof icon === 'string' ? jsxRuntime.jsx(Icon, { name: icon }) : icon })), isGiven(title) && (jsxRuntime.jsx(EmptyStateTitle, { as: titleAs, children: title })), isGiven(description) && (jsxRuntime.jsx(EmptyStateDescription, { children: description })), children] }));
3520
+ });
3521
+ const EmptyState = withSubComponents(EmptyStateRoot, {
3522
+ Indicator: EmptyStateIndicator,
3523
+ Title: EmptyStateTitle,
3524
+ Description: EmptyStateDescription,
3525
+ Actions: EmptyStateActions,
3526
+ }, 'EmptyState');
3527
+
3528
+ // Context to track MenuList nesting level
3529
+ const MenuListLevelContext = React.createContext(0);
3530
+ /**
3531
+ * The `Menu` component provides Bulma's vertical navigation menu: a simple, accessible sidebar or section menu for your Bulma React UI.
3532
+ *
3533
+ * @function
3534
+ * @param {MenuProps} props - Props for the Menu component.
3535
+ * @returns {JSX.Element} The rendered menu.
3536
+ * @see {@link https://bulma.io/documentation/components/menu/ | Bulma Menu documentation}
3537
+ */
3538
+ const MenuComponent = ({ className, children, ...props }) => {
3539
+ const { bulmaHelperClasses, rest } = useBulmaClasses(props);
3540
+ // Generate Bulma classes with prefix
3541
+ const bulmaClasses = usePrefixedClassNames('menu');
3542
+ return (jsxRuntime.jsx("aside", { className: classNames(bulmaClasses, bulmaHelperClasses, className), ...rest, children: children }));
3543
+ };
3544
+ /**
3545
+ * Bulma Menu label component.
3546
+ *
3547
+ * @function
3548
+ * @param {MenuLabelProps} props - Props for the MenuLabel component.
3549
+ * @returns {JSX.Element} The rendered menu label.
3550
+ */
3551
+ const MenuLabel = ({ className, children, ...props }) => {
3552
+ const { bulmaHelperClasses, rest } = useBulmaClasses(props);
3553
+ return (jsxRuntime.jsx("p", { className: classNames(usePrefixedClassNames('menu-label'), className, bulmaHelperClasses), ...rest, children: children }));
3554
+ };
3555
+ /**
3556
+ * MenuList applies `menu-list` class only at the top level (not for nested lists).
3557
+ *
3558
+ * @function
3559
+ * @param {MenuListProps} props - Props for the MenuList component.
3560
+ * @returns {JSX.Element} The rendered menu list.
3561
+ */
3562
+ const MenuList = ({ className, children, ...props }) => {
3563
+ const level = React.useContext(MenuListLevelContext);
3564
+ const { bulmaHelperClasses, rest } = useBulmaClasses(props);
3565
+ const ulClass = classNames(className, bulmaHelperClasses, {
3566
+ [usePrefixedClassNames('menu-list')]: level === 0,
3567
+ });
3568
+ // Increment level for nested MenuLists
3569
+ return (jsxRuntime.jsx(MenuListLevelContext.Provider, { value: level + 1, children: jsxRuntime.jsx("ul", { className: ulClass || undefined, ...rest, children: children }) }));
3570
+ };
3571
+ /**
3572
+ * `href` alone — annotated so a typo is a compile error rather than a silent
3573
+ * no-op, the way both siblings' sets are.
3574
+ */
3575
+ const STRIP_FROM_NON_LINK = { href: true };
3576
+ /**
3577
+ * MenuItem supports `as` prop for custom link components, e.g., react-router-dom Link.
3578
+ *
3579
+ * @function
3580
+ * @param {MenuItemProps} props - Props for the MenuItem component.
3581
+ * @param {React.Ref} ref - Forwarded ref to the inner element `as` renders.
3582
+ * @returns {JSX.Element} The rendered menu item.
3583
+ */
3584
+ const MenuItem = React.forwardRef(function MenuItem(itemProps, ref) {
3585
+ const { className, children, active, as: Component = 'a', 'data-testid': testId, style, id, title, role, tabIndex, ...rest } = itemProps;
3586
+ const { bulmaHelperClasses, rest: forwarded } = useBulmaClasses(rest);
3587
+ // `href` reaches an anchor or a custom component (which owns its own prop
3588
+ // contract), but not another intrinsic tag — `<span href>` is invalid HTML.
3589
+ // The same rule Avatar applies through `isLinkLike` and Button through its
3590
+ // intrinsic-only strip. Before #641 `href` was an own prop re-applied only
3591
+ // for `as="a"`; deriving it from `as` must not quietly widen that.
3592
+ const isLinkLike = Component === 'a' ||
3593
+ typeof Component !== 'string' ||
3594
+ isCustomElement(Component);
3595
+ // `href` ALONE. `Dropdown.Item` strips every anchor-only attribute except
3596
+ // `type`; `Level.Item` strips all of them, plus `rel`.
3597
+ //
3598
+ // Not because nothing else can arrive: `as` here is an open `React.ElementType`,
3599
+ // and a non-fresh spread skips excess-property checking, so a typed caller
3600
+ // CAN deliver `target` or `download` and they are forwarded. The set stays at
3601
+ // `href` because widening it would be wrong for an open `as` — `referrerPolicy`
3602
+ // on `as="img"`, `target` on `as="form"` and `media` on `as="source"` are all
3603
+ // legal, and a flat strip would delete them. Getting that right needs a
3604
+ // per-attribute-per-element rule, which `bestax-migrate` already models in
3605
+ // `LINK_ATTR_ELEMENTS`.
3606
+ const linkProps = isLinkLike
3607
+ ? forwarded
3608
+ : omitAttrs(forwarded, STRIP_FROM_NON_LINK);
3609
+ const itemClass = classNames({ [usePrefixedClassNames('is-active')]: active }, bulmaHelperClasses);
3610
+ // Split children into label and nested MenuList(s)
3611
+ const labelChildren = [];
3612
+ const nestedMenuLists = [];
3613
+ React.Children.forEach(children, child => {
3614
+ if (React.isValidElement(child) && child.type === MenuList) {
3615
+ nestedMenuLists.push(child);
3616
+ }
3617
+ else {
3618
+ labelChildren.push(child);
3619
+ }
3620
+ });
3621
+ return (jsxRuntime.jsxs("li", { className: className, "data-testid": testId, style: style, id: id, title: title, role: role, tabIndex: tabIndex, children: [jsxRuntime.jsx(Component, { ref: ref, className: itemClass || undefined, ...linkProps, ...(Component === 'button'
3622
+ ? { type: buttonType(linkProps.type) }
3623
+ : {}), children: labelChildren }), nestedMenuLists] }));
3624
+ });
3625
+ MenuItem.displayName = 'MenuItem';
3626
+ // Attach static subcomponents
3627
+ const Menu = withSubComponents(MenuComponent, {
3628
+ Label: MenuLabel,
3629
+ List: MenuList,
3630
+ Item: MenuItem,
3631
+ }, 'Menu');
3632
+
3633
+ const validMessageSizes = ['small', 'medium', 'large'];
3634
+ /**
3635
+ * The `Message` component provides Bulma's flexible notice/message box for your Bulma React UI.
3636
+ *
3637
+ * @function
3638
+ * @param {MessageProps} props - Props for the Message component.
3639
+ * @returns {JSX.Element} The rendered message.
3640
+ * @see {@link https://bulma.io/documentation/components/message/ | Bulma Message documentation}
3641
+ */
3642
+ const MessageComponent = ({ className, title, textColor, color, bgColor, size, onClose, children, ...props }) => {
3643
+ const { classPrefix } = useConfig();
3644
+ const { bulmaHelperClasses, rest } = useBulmaClasses({
3645
+ color: textColor,
3646
+ backgroundColor: bgColor,
3647
+ ...props,
3648
+ });
3649
+ // Generate Bulma classes with prefix
3650
+ const bulmaClasses = usePrefixedClassNames('message', {
3651
+ [`is-${color}`]: color,
3652
+ [`is-${size}`]: size && validMessageSizes.includes(size),
3653
+ });
3654
+ const deleteClass = usePrefixedClassNames('delete');
3655
+ const messageClasses = classNames(bulmaClasses, bulmaHelperClasses, className);
3656
+ return (jsxRuntime.jsxs("article", { className: messageClasses, ...rest, "data-testid": "message", children: [(title || onClose) && (jsxRuntime.jsxs("div", { className: prefixedClassNames(classPrefix, 'message-header'), children: [title && jsxRuntime.jsx("span", { children: title }), onClose && (jsxRuntime.jsx("button", { className: deleteClass, "aria-label": "delete", onClick: onClose, type: "button", "data-testid": "message-close" }))] })), children && (jsxRuntime.jsx("div", { className: prefixedClassNames(classPrefix, 'message-body'), "data-testid": "message-body", children: children }))] }));
3657
+ };
3658
+ /**
3659
+ * Message header compound component. Renders a `.message-header` element.
3660
+ *
3661
+ * @function
3662
+ * @param {MessageHeaderProps} props - Props for the MessageHeader component.
3663
+ * @returns {JSX.Element} The rendered message header.
3664
+ */
3665
+ const MessageHeader = ({ className, children, ...props }) => (jsxRuntime.jsx("div", { className: classNames(usePrefixedClassNames('message-header'), className), ...props, children: children }));
3666
+ /**
3667
+ * Message body compound component. Renders a `.message-body` element.
3668
+ *
3669
+ * @function
3670
+ * @param {MessageBodyProps} props - Props for the MessageBody component.
3671
+ * @returns {JSX.Element} The rendered message body.
3672
+ */
3673
+ const MessageBody = ({ className, children, ...props }) => (jsxRuntime.jsx("div", { className: classNames(usePrefixedClassNames('message-body'), className), ...props, children: children }));
3674
+ // Attach compound components
3675
+ const Message = withSubComponents(MessageComponent, {
3676
+ Header: MessageHeader,
3677
+ Body: MessageBody,
3678
+ }, 'Message');
3679
+
3680
+ // Ref-counted across every caller (e.g. a Dialog rendering its own Modal)
3681
+ // so overlapping/nested locks don't fight over which one restores `overflow`.
3682
+ // Every overlay that locks body scroll — Modal, Dialog, Sidebar, Loading —
3683
+ // goes through here; a component setting `document.body.style.overflow`
3684
+ // itself would clear a lock another overlay still needs.
3685
+ let lockCount = 0;
3686
+ let originalOverflow = '';
3687
+ /**
3688
+ * Locks (and ref-counted-ly unlocks) `document.body` scrolling while `active`
3689
+ * is true. Safe to call from multiple components at once — the body scroll
3690
+ * is only restored once every active caller has released its lock.
3691
+ *
3692
+ * @function useScrollLock
3693
+ * @param active - Whether this caller wants the body scroll locked.
3694
+ */
3695
+ function useScrollLock(active) {
3696
+ React.useEffect(() => {
3697
+ if (!active)
3698
+ return undefined;
3699
+ lockCount++;
3700
+ if (lockCount === 1) {
3701
+ originalOverflow = document.body.style.overflow;
3702
+ document.body.style.overflow = 'hidden';
3703
+ }
3704
+ return () => {
3705
+ lockCount--;
3706
+ if (lockCount === 0) {
3707
+ document.body.style.overflow = originalOverflow;
3708
+ }
3709
+ };
3710
+ }, [active]);
3711
+ }
3712
+
3713
+ // "Are we past hydration?" as a store that never changes: React reads the
3714
+ // server snapshot both while server-rendering and while hydrating, and the
3715
+ // client snapshot from the first post-hydration render onward.
3716
+ const subscribeToNothing = () => () => { };
3717
+ const getClientSnapshot = () => true;
3718
+ const getServerSnapshot = () => false;
3719
+ /**
3720
+ * Reports whether the component has passed hydration.
3721
+ *
3722
+ * Returns `false` during server rendering and during the hydrating client
3723
+ * render, then `true` from the commit that follows. Use it to defer
3724
+ * DOM-only behaviour (such as portalling) past hydration so the first client
3725
+ * render matches the server markup instead of tripping hydration recovery.
3726
+ *
3727
+ * Only a component that hydrates ever sees `false` on the client. One that
3728
+ * mounts later (after a route change, or in an app with no server rendering)
3729
+ * gets `true` on its first render, so it pays for no extra render. It
3730
+ * subscribes to nothing, so no later render flips it back.
3731
+ *
3732
+ * @function useIsHydrated
3733
+ * @returns `true` once the hydrating render has completed, otherwise `false`.
3734
+ */
3735
+ function useIsHydrated() {
3736
+ return React.useSyncExternalStore(subscribeToNothing, getClientSnapshot, getServerSnapshot);
3737
+ }
3738
+
3739
+ /**
3740
+ * Resolves a portal target: an `HTMLElement` is used directly, a non-empty
3741
+ * `string` is treated as a `document.querySelector` selector (falling back to
3742
+ * `document.body` when it matches nothing), and any falsy value — `undefined`
3743
+ * or the empty string — resolves to `document.body`.
3744
+ *
3745
+ * The empty string is deliberately treated as "no target" rather than passed
3746
+ * through: `document.querySelector('')` throws a `SyntaxError`, and callers
3747
+ * building a selector from state can easily hand us `''`.
3748
+ *
3749
+ * @function resolvePortalContainer
3750
+ * @param container - The requested portal target, if any.
3751
+ * @returns The resolved DOM node to portal into.
3752
+ */
3753
+ function resolvePortalContainer(container) {
3754
+ if (!container) {
3755
+ return document.body;
3756
+ }
3757
+ if (typeof container === 'string') {
3758
+ return (document.querySelector(container) ?? document.body);
3075
3759
  }
3076
3760
  return container;
3077
3761
  }
@@ -3820,427 +4504,179 @@ function whole(count, fallback) {
3820
4504
  function range(from, to) {
3821
4505
  return Array.from({ length: Math.max(to - from + 1, 0) }, (_, i) => from + i);
3822
4506
  }
3823
- /**
3824
- * The pages to show for `current` of `total`: the first and last
3825
- * `boundaries`, `siblings` on each side of the current one, and a gap for
3826
- * each run left out. A gap of a single page shows that page instead, and the
3827
- * window around the current page slides inward near either end, so the row
3828
- * keeps its length as the page moves.
3829
- */
3830
- function pageItems(total, current, siblings, boundaries) {
3831
- const windowSize = siblings * 2 + 1;
3832
- // Every page fits in the room the boundary pages, the window and two gaps take.
3833
- if (total <= boundaries * 2 + windowSize + 2)
3834
- return range(1, total);
3835
- const lowest = boundaries + 2;
3836
- const highest = total - boundaries - 1;
3837
- const start = Math.min(Math.max(current - siblings, lowest), highest - windowSize + 1);
3838
- const end = start + windowSize - 1;
3839
- const gap = (from, to, name) => to === from ? [from] : [name];
3840
- return [
3841
- ...range(1, boundaries),
3842
- ...gap(boundaries + 1, start - 1, 'start-gap'),
3843
- ...range(start, end),
3844
- ...gap(end + 1, total - boundaries, 'end-gap'),
3845
- ...range(total - boundaries + 1, total),
3846
- ];
3847
- }
3848
- /**
3849
- * "Previous" navigation button.
3850
- *
3851
- * @function
3852
- * @param {PaginationPreviousNextProps} props - Props for the PaginationPrevious component.
3853
- * @returns {JSX.Element} The rendered previous button.
3854
- */
3855
- const PaginationPrevious = ({ className, disabled, children, ...props }) => (jsxRuntime.jsx("a", { className: classNames(usePrefixedClassNames('pagination-previous', {
3856
- 'is-disabled': disabled,
3857
- }), className), "aria-disabled": disabled, tabIndex: disabled ? -1 : 0, ...props, onClick: disabled
3858
- ? e => {
3859
- e.preventDefault();
3860
- e.stopPropagation();
3861
- }
3862
- : props.onClick, children: children }));
3863
- /**
3864
- * "Next" navigation button.
3865
- *
3866
- * @function
3867
- * @param {PaginationPreviousNextProps} props - Props for the PaginationNext component.
3868
- * @returns {JSX.Element} The rendered next button.
3869
- */
3870
- const PaginationNext = ({ className, disabled, children, ...props }) => (jsxRuntime.jsx("a", { className: classNames(usePrefixedClassNames('pagination-next', { 'is-disabled': disabled }), className), "aria-disabled": disabled, tabIndex: disabled ? -1 : 0, ...props, onClick: disabled
3871
- ? e => {
3872
- e.preventDefault();
3873
- e.stopPropagation();
3874
- }
3875
- : props.onClick, children: children }));
3876
- /**
3877
- * The `Pagination` component provides a flexible, composable Bulma pagination navigation for your Bulma React UI.
3878
- *
3879
- * @function
3880
- * @param {PaginationProps} props - Props for the Pagination component.
3881
- * @returns {JSX.Element} The rendered pagination.
3882
- * @see {@link https://bulma.io/documentation/components/pagination/ | Bulma Pagination documentation}
3883
- */
3884
- const PaginationComponent = ({ color, textColor, bgColor, size, align, rounded, total, current, onPageChange, siblingCount = 1, boundaryCount = 1, previousLabel = 'Previous', nextLabel = 'Next', disabled, getPageHref, getPageLabel, className, children, ...props }) => {
3885
- const [kept, setKept] = React.useState(1);
3886
- warnDeprecatedColorProp('Pagination', color, 'Use the textColor / bgColor helper props instead.');
3887
- const { bulmaHelperClasses, rest } = useBulmaClasses({
3888
- color: textColor,
3889
- backgroundColor: bgColor,
3890
- ...props,
3891
- });
3892
- // Generate Bulma classes with prefix
3893
- const bulmaClasses = usePrefixedClassNames('pagination', {
3894
- [`is-${color}`]: color,
3895
- [`is-${size}`]: size,
3896
- [`is-${align}`]: align,
3897
- 'is-rounded': rounded,
3898
- });
3899
- const paginationClasses = classNames(bulmaClasses, bulmaHelperClasses, className);
3900
- // A child that renders nothing (`{show && …}` when false) is no children.
3901
- const fromTotal = total !== undefined && React.Children.toArray(children).length === 0;
3902
- let content = children;
3903
- if (fromTotal) {
3904
- const pages = Number.isFinite(total) ? Math.max(Math.floor(total), 0) : 0;
3905
- // A current page that's no number reads as the first.
3906
- const page = Math.min(Math.max(whole(current ?? kept, 1), 1), Math.max(pages, 1));
3907
- const choose = (next) => {
3908
- if (next === page)
3909
- return;
3910
- if (current === undefined)
3911
- setKept(next);
3912
- onPageChange?.(next);
3913
- };
3914
- /**
3915
- * What a link to `target` is given, beside what it renders. With no
3916
- * `getPageHref` it's a button; a link that's `off` goes nowhere.
3917
- */
3918
- const linkTo = (target, off) => ({
3919
- href: off || !getPageHref ? undefined : getPageHref(target),
3920
- role: getPageHref ? undefined : 'button',
3921
- // A link that's off is disabled too, and the parts drop its clicks.
3922
- onClick: () => choose(target),
3923
- onKeyDown: getPageHref
3924
- ? undefined
3925
- : (e) => {
3926
- if (off || (e.key !== 'Enter' && e.key !== ' '))
3927
- return;
3928
- e.preventDefault();
3929
- choose(target);
3930
- },
3931
- });
3932
- const atStart = disabled || page <= 1;
3933
- const atEnd = disabled || page >= pages;
3934
- content = (jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [jsxRuntime.jsx(PaginationPrevious, { disabled: atStart, ...linkTo(page - 1, atStart), children: previousLabel }), jsxRuntime.jsx(PaginationNext, { disabled: atEnd, ...linkTo(page + 1, atEnd), children: nextLabel }), jsxRuntime.jsx(PaginationList, { children: pageItems(pages, page, whole(siblingCount, 1), whole(boundaryCount, 1)).map(item => typeof item === 'number' ? (jsxRuntime.jsx(PaginationLink, { "aria-label": getPageLabel ? getPageLabel(item) : `Page ${item}`, active: item === page, disabled: disabled, ...linkTo(item, Boolean(disabled)), children: item }, item)) : (jsxRuntime.jsx(PaginationEllipsis, {}, item))) })] }));
3935
- }
3936
- return (jsxRuntime.jsx("nav", { className: paginationClasses, role: "navigation", "aria-label": "pagination", ...rest, children: content }));
3937
- };
3938
- /**
3939
- * Container for page links and ellipsis.
3940
- *
3941
- * @function
3942
- * @param {PaginationListProps} props - Props for the PaginationList component.
3943
- * @returns {JSX.Element} The rendered pagination list.
3944
- */
3945
- const PaginationList = ({ className, textColor, bgColor, children, ...props }) => {
3946
- const { bulmaHelperClasses, rest } = useBulmaClasses({
3947
- color: textColor,
3948
- backgroundColor: bgColor,
3949
- ...props,
3950
- });
3951
- return (jsxRuntime.jsx("ul", { className: classNames(usePrefixedClassNames('pagination-list'), bulmaHelperClasses, className), ...rest, children: children }));
3952
- };
3953
- /**
3954
- * Page number or navigation link.
3955
- *
3956
- * @function
3957
- * @param {PaginationLinkProps} props - Props for the PaginationLink component.
3958
- * @returns {JSX.Element} The rendered pagination link.
3959
- */
3960
- const PaginationLink = ({ className, textColor, bgColor, active, disabled, onClick, children, ...props }) => {
3961
- const { bulmaHelperClasses, rest } = useBulmaClasses({
3962
- color: textColor,
3963
- backgroundColor: bgColor,
3964
- ...props,
3965
- });
3966
- const handleClick = (e) => {
3967
- if (disabled) {
3968
- e.preventDefault();
3969
- e.stopPropagation();
3970
- return;
3971
- }
3972
- if (onClick) {
3973
- onClick(e);
3974
- }
3975
- };
3976
- return (jsxRuntime.jsx("li", { children: jsxRuntime.jsx("a", { className: classNames(usePrefixedClassNames('pagination-link', {
3977
- 'is-current': active,
3978
- 'is-disabled': disabled,
3979
- }), bulmaHelperClasses, className), "aria-current": active ? 'page' : undefined, "aria-disabled": disabled, tabIndex: disabled ? -1 : 0, onClick: handleClick, ...rest, children: children }) }));
3980
- };
3981
- /**
3982
- * Ellipsis separator.
3983
- *
3984
- * @function
3985
- * @param {React.LiHTMLAttributes<HTMLLIElement>} props - Standard li props.
3986
- * @returns {JSX.Element} The rendered ellipsis.
3987
- */
3988
- const PaginationEllipsis = props => (jsxRuntime.jsx("li", { children: jsxRuntime.jsx("span", { className: usePrefixedClassNames('pagination-ellipsis'), ...props, children: "\u2026" }) }));
3989
- const Pagination = withSubComponents(PaginationComponent, {
3990
- Link: PaginationLink,
3991
- List: PaginationList,
3992
- Ellipsis: PaginationEllipsis,
3993
- Previous: PaginationPrevious,
3994
- Next: PaginationNext,
3995
- }, 'Pagination');
3996
-
3997
- /**
3998
- * Strips a redundant leading library prefix from an icon name (e.g. `fa-check` -> `check`
3999
- * for the `fa` library, `mdi-account` -> `account` for `mdi`), so `name` behaves the same
4000
- * whether or not the caller includes the prefix. Font Awesome and Material Design Icons docs
4001
- * conventionally refer to icons with their class prefix (`fa-check`), which otherwise gets
4002
- * doubled by `getIconClasses` (producing `fa-fa-check`, matching no glyph).
4003
- *
4004
- * @param {string} name - The icon name, possibly including a redundant library prefix.
4005
- * @param {IconLibrary} library - The icon library the name will be rendered with.
4006
- * @returns {string} The icon name with any redundant leading library prefix removed.
4007
- */
4008
- function stripRedundantLibraryPrefix(name, library) {
4009
- if (!name) {
4010
- return name;
4011
- }
4012
- if (library === 'fa' && name.startsWith('fa-')) {
4013
- return name.substring(3);
4014
- }
4015
- if (library === 'mdi' && name.startsWith('mdi-')) {
4016
- return name.substring(4);
4017
- }
4018
- return name;
4019
- }
4020
- /**
4021
- * Gets the correct classes for the icon element based on the library and features.
4022
- *
4023
- * @param {IconLibrary} library - The icon library.
4024
- * @param {string} name - The icon name.
4025
- * @param {string} [variant] - Icon style variant (e.g., 'solid', 'outlined', 'rounded').
4026
- * @param {string | string[]} [features] - Additional library-specific modifiers.
4027
- * @returns {string} The combined class string for the icon.
4028
- */
4029
- function getIconClasses(library, name, variant, features) {
4030
- let baseClass;
4031
- let iconClass;
4032
- const featureList = Array.isArray(features)
4033
- ? features
4034
- : features
4035
- ? [features]
4036
- : [];
4037
- switch (library) {
4038
- case 'fa': {
4039
- // Font Awesome: use variant as style ('solid' -> 'fas', 'regular' -> 'far', etc.)
4040
- const styleMap = {
4041
- solid: 'fas',
4042
- regular: 'far',
4043
- brands: 'fab',
4044
- light: 'fal',
4045
- duotone: 'fad',
4046
- thin: 'fat',
4047
- };
4048
- const faStyle = variant ? styleMap[variant] || variant : 'fas';
4049
- baseClass = faStyle;
4050
- iconClass = `fa-${name}`;
4051
- return [baseClass, iconClass, ...featureList].join(' ');
4052
- }
4053
- case 'mdi':
4054
- // Material Design Icons: no variants, just features
4055
- baseClass = 'mdi';
4056
- iconClass = `mdi-${name}`;
4057
- return [baseClass, iconClass, ...featureList].join(' ');
4058
- case 'material-icons': {
4059
- // Google Material Icons: map variants to full class names
4060
- const styleVariants = {
4061
- filled: 'material-icons',
4062
- outlined: 'material-icons-outlined',
4063
- round: 'material-icons-round',
4064
- sharp: 'material-icons-sharp',
4065
- };
4066
- baseClass = variant
4067
- ? styleVariants[variant] || `material-icons-${variant}`
4068
- : 'material-icons';
4069
- return [baseClass, ...featureList].join(' ');
4507
+ /**
4508
+ * The pages to show for `current` of `total`: the first and last
4509
+ * `boundaries`, `siblings` on each side of the current one, and a gap for
4510
+ * each run left out. A gap of a single page shows that page instead, and the
4511
+ * window around the current page slides inward near either end, so the row
4512
+ * keeps its length as the page moves.
4513
+ */
4514
+ function pageItems(total, current, siblings, boundaries) {
4515
+ const windowSize = siblings * 2 + 1;
4516
+ // Every page fits in the room the boundary pages, the window and two gaps take.
4517
+ if (total <= boundaries * 2 + windowSize + 2)
4518
+ return range(1, total);
4519
+ const lowest = boundaries + 2;
4520
+ const highest = total - boundaries - 1;
4521
+ const start = Math.min(Math.max(current - siblings, lowest), highest - windowSize + 1);
4522
+ const end = start + windowSize - 1;
4523
+ const gap = (from, to, name) => to === from ? [from] : [name];
4524
+ return [
4525
+ ...range(1, boundaries),
4526
+ ...gap(boundaries + 1, start - 1, 'start-gap'),
4527
+ ...range(start, end),
4528
+ ...gap(end + 1, total - boundaries, 'end-gap'),
4529
+ ...range(total - boundaries + 1, total),
4530
+ ];
4531
+ }
4532
+ /**
4533
+ * "Previous" navigation button.
4534
+ *
4535
+ * @function
4536
+ * @param {PaginationPreviousNextProps} props - Props for the PaginationPrevious component.
4537
+ * @returns {JSX.Element} The rendered previous button.
4538
+ */
4539
+ const PaginationPrevious = ({ className, disabled, children, ...props }) => (jsxRuntime.jsx("a", { className: classNames(usePrefixedClassNames('pagination-previous', {
4540
+ 'is-disabled': disabled,
4541
+ }), className), "aria-disabled": disabled, tabIndex: disabled ? -1 : 0, ...props, onClick: disabled
4542
+ ? e => {
4543
+ e.preventDefault();
4544
+ e.stopPropagation();
4070
4545
  }
4071
- case 'material-symbols': {
4072
- // Google Material Symbols: map variants to full class names
4073
- const styleVariants = {
4074
- outlined: 'material-symbols-outlined',
4075
- rounded: 'material-symbols-rounded',
4076
- sharp: 'material-symbols-sharp',
4077
- };
4078
- baseClass = variant
4079
- ? styleVariants[variant] || `material-symbols-${variant}`
4080
- : 'material-symbols-outlined';
4081
- return [baseClass, ...featureList].join(' ');
4546
+ : props.onClick, children: children }));
4547
+ /**
4548
+ * "Next" navigation button.
4549
+ *
4550
+ * @function
4551
+ * @param {PaginationPreviousNextProps} props - Props for the PaginationNext component.
4552
+ * @returns {JSX.Element} The rendered next button.
4553
+ */
4554
+ const PaginationNext = ({ className, disabled, children, ...props }) => (jsxRuntime.jsx("a", { className: classNames(usePrefixedClassNames('pagination-next', { 'is-disabled': disabled }), className), "aria-disabled": disabled, tabIndex: disabled ? -1 : 0, ...props, onClick: disabled
4555
+ ? e => {
4556
+ e.preventDefault();
4557
+ e.stopPropagation();
4082
4558
  }
4083
- default:
4084
- // fallback: just icon name and features
4085
- return [name, ...featureList].join(' ');
4086
- }
4087
- }
4559
+ : props.onClick, children: children }));
4088
4560
  /**
4089
- * The `Icon` component is a Bulma-styled wrapper for displaying icons from various libraries (Font Awesome, Material Design Icons, Ionicons, Google Material Icons, Material Symbols, etc.).
4561
+ * The `Pagination` component provides a flexible, composable Bulma pagination navigation for your Bulma React UI.
4090
4562
  *
4091
4563
  * @function
4092
- * @param {IconProps} props - Props for the Icon component.
4093
- * @returns {JSX.Element} The rendered icon element.
4094
- * @see {@link https://bulma.io/documentation/elements/icon/ | Bulma Icon documentation}
4564
+ * @param {PaginationProps} props - Props for the Pagination component.
4565
+ * @returns {JSX.Element} The rendered pagination.
4566
+ * @see {@link https://bulma.io/documentation/components/pagination/ | Bulma Pagination documentation}
4095
4567
  */
4096
- const Icon = ({ className, textColor, bgColor, name, library, variant, features, libraryFeatures, // Deprecated but maintained for backward compatibility
4097
- size, ariaLabel, style, icon, // Capture and exclude the deprecated 'icon' prop from DOM
4098
- color: _color, // Exclude 'color' prop if passed directly
4099
- containerClassName, children, ...restProps }) => {
4100
- // Get the default icon library from context, fallback to 'fa' if not set
4101
- const defaultLibrary = useIconLibrary();
4102
- /**
4103
- * Generates Bulma helper classes and separates out remaining props.
4104
- * Note: variant, features, and libraryFeatures are excluded from props spread
4105
- */
4568
+ const PaginationComponent = ({ color, textColor, bgColor, size, align, rounded, total, current, onPageChange, siblingCount = 1, boundaryCount = 1, previousLabel = 'Previous', nextLabel = 'Next', disabled, getPageHref, getPageLabel, className, children, ...props }) => {
4569
+ const [kept, setKept] = React.useState(1);
4570
+ warnDeprecatedColorProp('Pagination', color, 'Use the textColor / bgColor helper props instead.');
4106
4571
  const { bulmaHelperClasses, rest } = useBulmaClasses({
4107
4572
  color: textColor,
4108
4573
  backgroundColor: bgColor,
4109
- ...restProps,
4574
+ ...props,
4110
4575
  });
4111
- // Hoisted unconditionally to respect rules-of-hooks; the branches below
4112
- // pick which result to consume.
4113
- const defaultIconClasses = usePrefixedClassNames('icon', {
4576
+ // Generate Bulma classes with prefix
4577
+ const bulmaClasses = usePrefixedClassNames('pagination', {
4578
+ [`is-${color}`]: color,
4114
4579
  [`is-${size}`]: size,
4580
+ [`is-${align}`]: align,
4581
+ 'is-rounded': rounded,
4115
4582
  });
4116
- const sizeModifierClass = usePrefixedClassNames(size ? `is-${size}` : undefined);
4117
- const bulmaClasses = containerClassName
4118
- ? containerClassName
4119
- : defaultIconClasses;
4120
- const iconContainerClasses = classNames(bulmaClasses, containerClassName && size ? sizeModifierClass : undefined, bulmaHelperClasses, className);
4121
- // A named icon is an image, whose name is its label alone, so a ligature inside
4122
- // never leaks into it. An unnamed one is decorative and hidden outright. Spread
4123
- // before `rest`, so a caller's own `role` or `aria-hidden` still wins.
4124
- const named = !!ariaLabel || !!restProps['aria-label'] || !!restProps['aria-labelledby'];
4125
- // A caller's own `role` or `tabIndex` on an unnamed icon means it isn't decoration (an
4126
- // icon made a button, say), so it isn't hidden either: `aria-hidden` on something
4127
- // focusable is a failure of its own.
4128
- const a11yProps = named
4129
- ? { role: 'img', 'aria-label': ariaLabel || undefined }
4130
- : restProps.role !== undefined || restProps.tabIndex !== undefined
4131
- ? {}
4132
- : { 'aria-hidden': true };
4133
- if (children !== undefined) {
4134
- // `IconChildrenProps`: render the caller's node (an inline SVG, a `react-icons`
4135
- // component, …) in place of a class-based glyph. Library/variant/features don't apply.
4136
- return (jsxRuntime.jsx("span", { className: iconContainerClasses, ...a11yProps, style: style, ...rest, children: children }));
4137
- }
4138
- // `name` is guaranteed once `children` is absent UNLESS the caller is on the deprecated
4139
- // `icon` path, which `IconDeprecatedProps` declares with `name?: undefined`.
4140
- // (`IconChildrenProps['children']` excludes `undefined`, so `children={undefined}` can't
4141
- // slip past into this branch.) The cast covers that third member, and the `!name && icon`
4142
- // fallback below is what fills it in.
4143
- let finalName = name;
4144
- if (!name && icon) {
4145
- // If icon prop is provided instead of name, try to parse it
4146
- // e.g., "mdi mdi-rocket-launch" -> "rocket-launch"
4147
- if (typeof icon === 'string') {
4148
- const parts = icon.split(' ');
4149
- const lastPart = parts[parts.length - 1];
4150
- if (lastPart.startsWith('mdi-')) {
4151
- finalName = lastPart.substring(4); // Remove "mdi-" prefix
4152
- }
4153
- else if (lastPart.startsWith('fa-')) {
4154
- finalName = lastPart.substring(3); // Remove "fa-" prefix
4155
- }
4156
- else {
4157
- finalName = lastPart;
4158
- }
4159
- }
4160
- }
4161
- const finalLibrary = library || defaultLibrary || 'fa';
4162
- // Normalize a redundant leading library prefix (e.g. "fa-check" -> "check" when the
4163
- // library is 'fa'), so `name` behaves identically with or without the prefix.
4164
- finalName = stripRedundantLibraryPrefix(finalName, finalLibrary);
4165
- if (!finalName) {
4166
- // No glyph to name. Reachable through the public type only as `icon=""`, and otherwise
4167
- // by a plain-JS caller (or one that casts). Building a class off an absent name produced
4168
- // a bogus `fa-undefined` glyph; render the bare container instead, matching the
4169
- // `children` branch.
4170
- return (jsxRuntime.jsx("span", { className: iconContainerClasses, ...a11yProps, style: style, ...rest }));
4171
- }
4172
- // Backward compatibility: if libraryFeatures is provided, parse it for variant and features
4173
- let finalVariant = variant;
4174
- let finalFeatures = features;
4175
- if (libraryFeatures && !variant && !features) {
4176
- const legacyFeatures = Array.isArray(libraryFeatures)
4177
- ? libraryFeatures
4178
- : [libraryFeatures];
4179
- // For Font Awesome, extract style from features
4180
- if (finalLibrary === 'fa') {
4181
- const faStyle = legacyFeatures.find(f => [
4182
- 'fas',
4183
- 'far',
4184
- 'fab',
4185
- 'fal',
4186
- 'fad',
4187
- 'fat',
4188
- 'solid',
4189
- 'regular',
4190
- 'brands',
4191
- 'light',
4192
- 'duotone',
4193
- 'thin',
4194
- ].includes(f));
4195
- if (faStyle) {
4196
- finalVariant = faStyle;
4197
- finalFeatures = legacyFeatures.filter(f => f !== faStyle);
4198
- }
4199
- else {
4200
- finalFeatures = legacyFeatures;
4201
- }
4202
- }
4203
- // For Material Icons/Symbols, extract style variant
4204
- else if (finalLibrary === 'material-icons' ||
4205
- finalLibrary === 'material-symbols') {
4206
- const styleVariants = finalLibrary === 'material-icons'
4207
- ? ['filled', 'outlined', 'round', 'sharp']
4208
- : ['outlined', 'rounded', 'sharp'];
4209
- const styleVariant = legacyFeatures.find(f => styleVariants.includes(f));
4210
- if (styleVariant) {
4211
- finalVariant = styleVariant;
4212
- finalFeatures = legacyFeatures.filter(f => f !== styleVariant);
4213
- }
4214
- else {
4215
- finalFeatures = legacyFeatures;
4216
- }
4217
- }
4218
- // For others, all features go to finalFeatures
4219
- else {
4220
- finalFeatures = legacyFeatures;
4221
- }
4583
+ const paginationClasses = classNames(bulmaClasses, bulmaHelperClasses, className);
4584
+ // A child that renders nothing (`{show && …}` when false) is no children.
4585
+ const fromTotal = total !== undefined && React.Children.toArray(children).length === 0;
4586
+ let content = children;
4587
+ if (fromTotal) {
4588
+ const pages = Number.isFinite(total) ? Math.max(Math.floor(total), 0) : 0;
4589
+ // A current page that's no number reads as the first.
4590
+ const page = Math.min(Math.max(whole(current ?? kept, 1), 1), Math.max(pages, 1));
4591
+ const choose = (next) => {
4592
+ if (next === page)
4593
+ return;
4594
+ if (current === undefined)
4595
+ setKept(next);
4596
+ onPageChange?.(next);
4597
+ };
4598
+ /**
4599
+ * What a link to `target` is given, beside what it renders. With no
4600
+ * `getPageHref` it's a button; a link that's `off` goes nowhere.
4601
+ */
4602
+ const linkTo = (target, off) => ({
4603
+ href: off || !getPageHref ? undefined : getPageHref(target),
4604
+ role: getPageHref ? undefined : 'button',
4605
+ // A link that's off is disabled too, and the parts drop its clicks.
4606
+ onClick: () => choose(target),
4607
+ onKeyDown: getPageHref
4608
+ ? undefined
4609
+ : (e) => {
4610
+ if (off || (e.key !== 'Enter' && e.key !== ' '))
4611
+ return;
4612
+ e.preventDefault();
4613
+ choose(target);
4614
+ },
4615
+ });
4616
+ const atStart = disabled || page <= 1;
4617
+ const atEnd = disabled || page >= pages;
4618
+ content = (jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [jsxRuntime.jsx(PaginationPrevious, { disabled: atStart, ...linkTo(page - 1, atStart), children: previousLabel }), jsxRuntime.jsx(PaginationNext, { disabled: atEnd, ...linkTo(page + 1, atEnd), children: nextLabel }), jsxRuntime.jsx(PaginationList, { children: pageItems(pages, page, whole(siblingCount, 1), whole(boundaryCount, 1)).map(item => typeof item === 'number' ? (jsxRuntime.jsx(PaginationLink, { "aria-label": getPageLabel ? getPageLabel(item) : `Page ${item}`, active: item === page, disabled: disabled, ...linkTo(item, Boolean(disabled)), children: item }, item)) : (jsxRuntime.jsx(PaginationEllipsis, {}, item))) })] }));
4222
4619
  }
4223
- // Handle web components vs CSS-based icons
4224
- if (finalLibrary === 'ion') {
4225
- // For Ionicons, handle variant in the name
4226
- let ionName = finalName;
4227
- if (finalVariant === 'outline') {
4228
- ionName = `${finalName}-outline`;
4620
+ return (jsxRuntime.jsx("nav", { className: paginationClasses, role: "navigation", "aria-label": "pagination", ...rest, children: content }));
4621
+ };
4622
+ /**
4623
+ * Container for page links and ellipsis.
4624
+ *
4625
+ * @function
4626
+ * @param {PaginationListProps} props - Props for the PaginationList component.
4627
+ * @returns {JSX.Element} The rendered pagination list.
4628
+ */
4629
+ const PaginationList = ({ className, textColor, bgColor, children, ...props }) => {
4630
+ const { bulmaHelperClasses, rest } = useBulmaClasses({
4631
+ color: textColor,
4632
+ backgroundColor: bgColor,
4633
+ ...props,
4634
+ });
4635
+ return (jsxRuntime.jsx("ul", { className: classNames(usePrefixedClassNames('pagination-list'), bulmaHelperClasses, className), ...rest, children: children }));
4636
+ };
4637
+ /**
4638
+ * Page number or navigation link.
4639
+ *
4640
+ * @function
4641
+ * @param {PaginationLinkProps} props - Props for the PaginationLink component.
4642
+ * @returns {JSX.Element} The rendered pagination link.
4643
+ */
4644
+ const PaginationLink = ({ className, textColor, bgColor, active, disabled, onClick, children, ...props }) => {
4645
+ const { bulmaHelperClasses, rest } = useBulmaClasses({
4646
+ color: textColor,
4647
+ backgroundColor: bgColor,
4648
+ ...props,
4649
+ });
4650
+ const handleClick = (e) => {
4651
+ if (disabled) {
4652
+ e.preventDefault();
4653
+ e.stopPropagation();
4654
+ return;
4229
4655
  }
4230
- else if (finalVariant === 'sharp') {
4231
- ionName = `${finalName}-sharp`;
4656
+ if (onClick) {
4657
+ onClick(e);
4232
4658
  }
4233
- return (jsxRuntime.jsx("span", { className: iconContainerClasses, ...a11yProps, style: style, ...rest, children: jsxRuntime.jsx("ion-icon", { name: ionName }) }));
4234
- }
4235
- // Legacy CSS-based icons
4236
- const iClasses = getIconClasses(finalLibrary, finalName, finalVariant, finalFeatures);
4237
- // Material Icons and Material Symbols use text content, not CSS classes for the icon name
4238
- if (finalLibrary === 'material-icons' ||
4239
- finalLibrary === 'material-symbols') {
4240
- return (jsxRuntime.jsx("span", { className: iconContainerClasses, ...a11yProps, style: style, ...rest, children: jsxRuntime.jsx("i", { className: iClasses, children: finalName }) }));
4241
- }
4242
- return (jsxRuntime.jsx("span", { className: iconContainerClasses, ...a11yProps, style: style, ...rest, children: jsxRuntime.jsx("i", { className: iClasses }) }));
4659
+ };
4660
+ return (jsxRuntime.jsx("li", { children: jsxRuntime.jsx("a", { className: classNames(usePrefixedClassNames('pagination-link', {
4661
+ 'is-current': active,
4662
+ 'is-disabled': disabled,
4663
+ }), bulmaHelperClasses, className), "aria-current": active ? 'page' : undefined, "aria-disabled": disabled, tabIndex: disabled ? -1 : 0, onClick: handleClick, ...rest, children: children }) }));
4243
4664
  };
4665
+ /**
4666
+ * Ellipsis separator.
4667
+ *
4668
+ * @function
4669
+ * @param {React.LiHTMLAttributes<HTMLLIElement>} props - Standard li props.
4670
+ * @returns {JSX.Element} The rendered ellipsis.
4671
+ */
4672
+ const PaginationEllipsis = props => (jsxRuntime.jsx("li", { children: jsxRuntime.jsx("span", { className: usePrefixedClassNames('pagination-ellipsis'), ...props, children: "\u2026" }) }));
4673
+ const Pagination = withSubComponents(PaginationComponent, {
4674
+ Link: PaginationLink,
4675
+ List: PaginationList,
4676
+ Ellipsis: PaginationEllipsis,
4677
+ Previous: PaginationPrevious,
4678
+ Next: PaginationNext,
4679
+ }, 'Pagination');
4244
4680
 
4245
4681
  /**
4246
4682
  * The `Panel` component implements Bulma's versatile panel block for React.
@@ -4586,205 +5022,76 @@ const Tab = ({ index, disabled, icon, iconLibrary, iconVariant, iconSize = 'smal
4586
5022
  const target = findTargetTab(e.currentTarget, e.key, ctx?.orientation === 'vertical');
4587
5023
  if (target) {
4588
5024
  e.preventDefault();
4589
- target.focus();
4590
- }
4591
- };
4592
- const handleFocus = (e) => {
4593
- onFocus?.(e);
4594
- ctx?.setFocusedTab(index);
4595
- };
4596
- const handleBlur = (e) => {
4597
- onBlur?.(e);
4598
- if (!tabListOf(e.currentTarget).contains(e.relatedTarget)) {
4599
- ctx?.setFocusedTab(null);
4600
- }
4601
- };
4602
- return (jsxRuntime.jsx("li", { className: classNames(activeClass, className) || undefined, role: "tab", id: tabId, "aria-selected": isActive, "aria-controls": ctx?.panelId(index), "aria-disabled": disabled || undefined, tabIndex: tabIndex, onKeyDown: handleKeyDown, onFocus: handleFocus, onBlur: handleBlur, ...props, children: jsxRuntime.jsxs("a", { ref: anchorRef, onClick: handleClick, "aria-disabled": disabled || undefined, children: [icon && (jsxRuntime.jsx(Icon, { name: icon, library: iconLibrary, variant: iconVariant, size: iconSize, features: iconFeatures })), children && jsxRuntime.jsx("span", { children: children })] }) }));
4603
- };
4604
- /**
4605
- * Each tab; accepts `active`, `onClick`, etc.
4606
- *
4607
- * @function
4608
- * @param {TabItemProps} props - Props for the TabItem component.
4609
- * @returns {JSX.Element} The rendered tab item.
4610
- * @deprecated Use `Tabs.Tab` with an `index` prop instead.
4611
- */
4612
- const TabItem = ({ active, className, children, onClick, ...props }) => (jsxRuntime.jsx("li", { className: classNames({ [usePrefixedClassNames('is-active')]: active }, className) ||
4613
- undefined, onClick: onClick, ...props, children: children }));
4614
- /**
4615
- * Container for tab content panels. No custom props beyond `children` and standard `<div>` HTML attributes. Applies the `.tabs-content` class.
4616
- *
4617
- * @function
4618
- * @param {TabsContentProps} props - Props for the TabsContent component.
4619
- * @returns {JSX.Element} The rendered tabs content wrapper.
4620
- */
4621
- const TabsContentComponent = ({ className, children, ...props }) => {
4622
- const contentClass = usePrefixedClassNames('tabs-content');
4623
- return (jsxRuntime.jsx("div", { className: classNames(contentClass, className), ...props, children: children }));
4624
- };
4625
- /**
4626
- * Individual content panel. Shows/hides based on active tab from context.
4627
- *
4628
- * @function
4629
- * @param {TabContentItemProps} props - Props for the TabContentItem component.
4630
- * @returns {JSX.Element} The rendered tab content panel.
4631
- */
4632
- const TabContentItem = ({ index, className, children, id, ...props }) => {
4633
- const ctx = useTabsContext();
4634
- const isActive = ctx ? ctx.activeTab === index : false;
4635
- const itemClass = usePrefixedClassNames('tabs-content-item', {
4636
- 'is-active': isActive,
4637
- });
4638
- const panelId = id ?? (ctx ? `${ctx.baseId}-panel-${index}` : undefined);
4639
- const registerPanel = ctx?.registerPanel;
4640
- React.useEffect(() => {
4641
- if (!registerPanel || panelId === undefined)
4642
- return undefined;
4643
- return registerPanel({ index, id: panelId });
4644
- }, [registerPanel, index, panelId]);
4645
- return (jsxRuntime.jsx("div", { className: classNames(itemClass, className), role: "tabpanel", id: panelId, "aria-labelledby": ctx?.tabId(index), "aria-hidden": !isActive, ...props, children: children }));
4646
- };
4647
- // ---------------------------------------------------------------------------
4648
- // Static property attachment
4649
- // ---------------------------------------------------------------------------
4650
- const TabsContent = withSubComponents(TabsContentComponent, {
4651
- Item: TabContentItem,
4652
- });
4653
- const Tabs = withSubComponents(TabsComponent, {
4654
- List: TabList,
4655
- Tab,
4656
- Item: TabItem,
4657
- Content: TabsContent,
4658
- }, 'Tabs');
4659
-
4660
- const validButtonColors = [...validColors, 'text', 'ghost'];
4661
- /**
4662
- * The `Button` component provides a flexible and highly customizable button for your Bulma React UI.
4663
- *
4664
- * @function
4665
- * @param {ButtonProps} props - Props for the Button component.
4666
- * @param {React.Ref} ref - Forwarded ref to the element `as` renders.
4667
- * @returns {JSX.Element} The rendered button, anchor, or custom element.
4668
- * @see {@link https://bulma.io/documentation/elements/button/ | Bulma Button documentation}
4669
- */
4670
- const Button = React.forwardRef(function Button(props, ref) {
4671
- const { color, size, isLight, isRounded, isLoading, isStatic, isFullwidth, isFullWidth, isOutlined, isInverted, isFocused, isActive, isHovered, isDisabled, className, children, textColor, bgColor, as: Component = 'button', href, onClick, target, rel, ...bulmaProps } = props;
4672
- const { bulmaHelperClasses, rest } = useBulmaClasses({
4673
- color: textColor,
4674
- backgroundColor: bgColor,
4675
- ...bulmaProps,
4676
- });
4677
- // Generate Bulma classes with prefix
4678
- const bulmaClasses = usePrefixedClassNames('button', {
4679
- [`is-${color}`]: color &&
4680
- validButtonColors.includes(color),
4681
- [`is-${size}`]: size,
4682
- 'is-outlined': isOutlined,
4683
- 'is-light': isLight,
4684
- 'is-loading': isLoading,
4685
- 'is-static': isStatic,
4686
- 'is-disabled': isDisabled,
4687
- 'is-rounded': isRounded,
4688
- 'is-hovered': isHovered,
4689
- 'is-focused': isFocused,
4690
- 'is-active': isActive,
4691
- 'is-inverted': isInverted,
4692
- 'is-fullwidth': isFullwidth ?? isFullWidth,
4693
- });
4694
- // Combine prefixed Bulma classes with unprefixed user className and prefixed helper classes
4695
- const buttonClasses = classNames(bulmaClasses, bulmaHelperClasses, className);
4696
- if (Component !== 'button') {
4697
- // Strip the form-control attributes a link-like element cannot carry, so a
4698
- // JavaScript consumer or a spread object doesn't put them on the DOM. The
4699
- // types no longer allow them through; this is the runtime backstop.
4700
- //
4701
- // Only for INTRINSIC tags. A custom component (a router Link, ...) owns its
4702
- // prop contract, and `ComponentPropsWithoutRef<T>` promises the caller that
4703
- // its props reach it. Stripping names by their spelling alone broke that
4704
- // promise silently: `<Button as={Custom} name="x" />` type-checked while
4705
- // `Custom` never received the `name` it requires.
4706
- //
4707
- // `autoFocus` and `type` are deliberately NOT stripped: both are valid on
4708
- // targets this list was filtering them from. `autoFocus` is a global
4709
- // attribute (React decides per element whether to honour it, which is not
4710
- // ours to pre-empt), and `type` on an anchor is the MIME hint that
4711
- // `ButtonProps<'a'>` now types it as.
4712
- //
4713
- // The types already answer this for a TypeScript caller: `ButtonProps<T>`
4714
- // derives from `ComponentPropsWithoutRef<T>`, so only what the target
4715
- // accepts can be passed. What is left is a runtime backstop for JavaScript
4716
- // consumers and spread objects.
4717
- //
4718
- // Keep it to the two cases where a stray attribute does something. The
4719
- // submit-overrides belong to a submit control and never make sense on the
4720
- // anchor path. `disabled` is the one that is VISIBLE: Bulma styles
4721
- // `.button[disabled]` (background, border, shadow, opacity), so letting it
4722
- // through to a `<span>` greys the element out — main stripped it, and not
4723
- // stripping it would be a silent visual change on a types-only release.
4724
- //
4725
- // Everything else (`name`, `value`, `form`) is inert where it does not
4726
- // belong, and enumerating which tags own it is what kept going wrong:
4727
- // first all nine names on every non-button tag (so `<Button as="input"
4728
- // name="query" value="Search">` submitted nothing), then a form-control
4729
- // allowlist that still lost `formAction` on an input and `value` on an
4730
- // option. Those now ride on the types instead.
4731
- //
4732
- // A custom component is exempt entirely — it owns its prop contract, and
4733
- // `ComponentPropsWithoutRef<T>` promises the caller its props arrive.
4734
- //
4735
- // The elements that own `disabled`, per the HTML spec. A closed set, unlike
4736
- // the open-ended question of which tag owns which attribute.
4737
- const SUBMIT_OVERRIDES = [
4738
- 'formAction',
4739
- 'formEncType',
4740
- 'formMethod',
4741
- 'formNoValidate',
4742
- 'formTarget',
4743
- ];
4744
- const OWNS_DISABLED = [
4745
- 'button',
4746
- 'fieldset',
4747
- 'input',
4748
- 'optgroup',
4749
- 'option',
4750
- 'select',
4751
- 'textarea',
4752
- ];
4753
- // Two independent filters. They are not exclusive — an `<a>` owns neither
4754
- // the submit-overrides nor `disabled`, so it takes both — and deriving them
4755
- // from one destructure, or from one if/else, is what broke each of them in
4756
- // turn: first the overrides came off every intrinsic (an `as="input"` lost
4757
- // `formAction`), then scoping them to the anchor let `disabled` back onto
4758
- // it.
4759
- const forwardedRest = { ...rest };
4760
- if (typeof Component === 'string' && !isCustomElement(Component)) {
4761
- // Meaningless anywhere but a submit control, and an anchor is not one.
4762
- if (Component === 'a') {
4763
- for (const key of SUBMIT_OVERRIDES)
4764
- delete forwardedRest[key];
4765
- }
4766
- // Visible via Bulma's `.button[disabled]`, so withheld from any element
4767
- // that does not own it.
4768
- if (!OWNS_DISABLED.includes(Component))
4769
- delete forwardedRest.disabled;
4770
- }
4771
- return (jsxRuntime.jsx(Component, { ref: ref, className: buttonClasses, href: href, target: target, rel: rel, "aria-disabled": isDisabled, tabIndex: isDisabled ? -1 : undefined, onClick: isDisabled
4772
- ? // Guarded, because this handler is installed into whatever `as`
4773
- // renders and a custom component's `onClick` need not be a DOM
4774
- // event handler at all. One declaring `onClick: (v: string) =>
4775
- // void` calls this with a string, and an unguarded
4776
- // `e.preventDefault()` throws `is not a function`. Blocking the
4777
- // real event is the point; a foreign payload is simply not ours
4778
- // to act on.
4779
- (e) => {
4780
- if (typeof e?.preventDefault === 'function')
4781
- e.preventDefault();
4782
- }
4783
- : onClick, ...forwardedRest, children: children }));
4784
- }
4785
- return (jsxRuntime.jsx("button", { ref: ref, className: buttonClasses, disabled: isDisabled, onClick: onClick, ...rest, children: children }));
5025
+ target.focus();
5026
+ }
5027
+ };
5028
+ const handleFocus = (e) => {
5029
+ onFocus?.(e);
5030
+ ctx?.setFocusedTab(index);
5031
+ };
5032
+ const handleBlur = (e) => {
5033
+ onBlur?.(e);
5034
+ if (!tabListOf(e.currentTarget).contains(e.relatedTarget)) {
5035
+ ctx?.setFocusedTab(null);
5036
+ }
5037
+ };
5038
+ return (jsxRuntime.jsx("li", { className: classNames(activeClass, className) || undefined, role: "tab", id: tabId, "aria-selected": isActive, "aria-controls": ctx?.panelId(index), "aria-disabled": disabled || undefined, tabIndex: tabIndex, onKeyDown: handleKeyDown, onFocus: handleFocus, onBlur: handleBlur, ...props, children: jsxRuntime.jsxs("a", { ref: anchorRef, onClick: handleClick, "aria-disabled": disabled || undefined, children: [icon && (jsxRuntime.jsx(Icon, { name: icon, library: iconLibrary, variant: iconVariant, size: iconSize, features: iconFeatures })), children && jsxRuntime.jsx("span", { children: children })] }) }));
5039
+ };
5040
+ /**
5041
+ * Each tab; accepts `active`, `onClick`, etc.
5042
+ *
5043
+ * @function
5044
+ * @param {TabItemProps} props - Props for the TabItem component.
5045
+ * @returns {JSX.Element} The rendered tab item.
5046
+ * @deprecated Use `Tabs.Tab` with an `index` prop instead.
5047
+ */
5048
+ const TabItem = ({ active, className, children, onClick, ...props }) => (jsxRuntime.jsx("li", { className: classNames({ [usePrefixedClassNames('is-active')]: active }, className) ||
5049
+ undefined, onClick: onClick, ...props, children: children }));
5050
+ /**
5051
+ * Container for tab content panels. No custom props beyond `children` and standard `<div>` HTML attributes. Applies the `.tabs-content` class.
5052
+ *
5053
+ * @function
5054
+ * @param {TabsContentProps} props - Props for the TabsContent component.
5055
+ * @returns {JSX.Element} The rendered tabs content wrapper.
5056
+ */
5057
+ const TabsContentComponent = ({ className, children, ...props }) => {
5058
+ const contentClass = usePrefixedClassNames('tabs-content');
5059
+ return (jsxRuntime.jsx("div", { className: classNames(contentClass, className), ...props, children: children }));
5060
+ };
5061
+ /**
5062
+ * Individual content panel. Shows/hides based on active tab from context.
5063
+ *
5064
+ * @function
5065
+ * @param {TabContentItemProps} props - Props for the TabContentItem component.
5066
+ * @returns {JSX.Element} The rendered tab content panel.
5067
+ */
5068
+ const TabContentItem = ({ index, className, children, id, ...props }) => {
5069
+ const ctx = useTabsContext();
5070
+ const isActive = ctx ? ctx.activeTab === index : false;
5071
+ const itemClass = usePrefixedClassNames('tabs-content-item', {
5072
+ 'is-active': isActive,
5073
+ });
5074
+ const panelId = id ?? (ctx ? `${ctx.baseId}-panel-${index}` : undefined);
5075
+ const registerPanel = ctx?.registerPanel;
5076
+ React.useEffect(() => {
5077
+ if (!registerPanel || panelId === undefined)
5078
+ return undefined;
5079
+ return registerPanel({ index, id: panelId });
5080
+ }, [registerPanel, index, panelId]);
5081
+ return (jsxRuntime.jsx("div", { className: classNames(itemClass, className), role: "tabpanel", id: panelId, "aria-labelledby": ctx?.tabId(index), "aria-hidden": !isActive, ...props, children: children }));
5082
+ };
5083
+ // ---------------------------------------------------------------------------
5084
+ // Static property attachment
5085
+ // ---------------------------------------------------------------------------
5086
+ const TabsContent = withSubComponents(TabsContentComponent, {
5087
+ Item: TabContentItem,
4786
5088
  });
4787
- Button.displayName = 'Button';
5089
+ const Tabs = withSubComponents(TabsComponent, {
5090
+ List: TabList,
5091
+ Tab,
5092
+ Item: TabItem,
5093
+ Content: TabsContent,
5094
+ }, 'Tabs');
4788
5095
 
4789
5096
  // INTERNAL: deliberately not exported from src/index.ts.
4790
5097
  /**
@@ -8753,129 +9060,6 @@ const Box = ({ className, textColor, color, bgColor, hasShadow = true, children,
8753
9060
  return (jsxRuntime.jsx("div", { className: boxClasses, style: mergeBulmaStyles(bulmaHelperStyles, style), ...rest, children: children }));
8754
9061
  };
8755
9062
 
8756
- /**
8757
- * The `LinkButton` component renders a `<button>` that visually looks like text or a link.
8758
- *
8759
- * @function
8760
- * @param {LinkButtonProps} props - Props for the LinkButton component.
8761
- * @param {React.Ref} ref - Forwarded ref to the element `as` renders.
8762
- * @returns {JSX.Element} The rendered link-styled button element.
8763
- *
8764
- * @example
8765
- * // Text variant (default)
8766
- * <LinkButton onClick={handleClick}>Click me</LinkButton>
8767
- *
8768
- * @example
8769
- * // Underline variant with color
8770
- * <LinkButton variant="underline" color="primary">Learn more</LinkButton>
8771
- */
8772
- const LinkButton = React.forwardRef(function LinkButton({ variant = 'text', color, className, ...props }, ref) {
8773
- const buttonColor = variant === 'underline' ? 'text' : variant;
8774
- const prefixedClasses = usePrefixedClassNames('link-button', color && `link-button-${color}`, variant === 'underline' && 'link-button-underline');
8775
- return (jsxRuntime.jsx(Button
8776
- // LinkButton is polymorphic through to Button, which resolves the element
8777
- // from `as`. TS cannot infer that through the spread below.
8778
- , {
8779
- // LinkButton is polymorphic through to Button, which resolves the element
8780
- // from `as`. TS cannot infer that through the spread below.
8781
- ref: ref, color: buttonColor, className: classNames(prefixedClasses, className), ...props }));
8782
- });
8783
- LinkButton.displayName = 'LinkButton';
8784
-
8785
- /**
8786
- * The `ToggleButton` component renders a button that stays pressed until it's pressed again, and says whether it's on through `aria-pressed`.
8787
- *
8788
- * It's always a `<button type="button">`, so it never submits a form and
8789
- * carries no form value: a setting saved with a form is a `Checkbox` or a
8790
- * `Switch`. A set of toggles that each turn on and off on their own goes in
8791
- * `Buttons hasAddons` with `role="group"` and an `aria-label`. A set where only
8792
- * one can be on is a radio group rather than a row of these: use
8793
- * `SegmentedControl`, or `Radios` for a choice that's sent with a form.
8794
- *
8795
- * @function
8796
- * @param {ToggleButtonProps} props - Props for the ToggleButton component.
8797
- * @param {React.Ref<HTMLButtonElement>} ref - Forwarded ref to the `<button>`.
8798
- * @returns {JSX.Element} The rendered toggle button.
8799
- * @see {@link https://www.w3.org/WAI/ARIA/apg/patterns/button/ | APG button pattern}
8800
- *
8801
- * @example
8802
- * <ToggleButton onPressedChange={setMuted}>Mute</ToggleButton>
8803
- *
8804
- * @example
8805
- * <Buttons hasAddons role="group" aria-label="Text style">
8806
- * <ToggleButton aria-label="Bold" pressed={bold} onPressedChange={setBold}>
8807
- * <Icon name="bold" />
8808
- * </ToggleButton>
8809
- * </Buttons>
8810
- */
8811
- const ToggleButton = React.forwardRef(function ToggleButton({ pressed: controlledPressed, defaultPressed = false, onPressedChange, color = 'link', isLoading, 'aria-busy': ariaBusy, 'aria-disabled': ariaDisabled, className, onClick, ...props }, ref) {
8812
- const isControlled = controlledPressed !== undefined;
8813
- const [internalPressed, setInternalPressed] = React.useState(defaultPressed);
8814
- const pressed = controlledPressed ?? internalPressed;
8815
- // Prefixed like Button's own classes, which a hand-written
8816
- // `className="is-selected"` could not be under ConfigProvider classPrefix.
8817
- // Bulma lifts `is-selected` above its neighbours in `.buttons.has-addons`.
8818
- const selectedClass = usePrefixedClassNames({ 'is-selected': pressed });
8819
- // The caller's onClick runs first, so a `preventDefault()` there vetoes
8820
- // the toggle, as it does in Radix, MUI and Zag. Enter and Space reach here
8821
- // through the button's own click, and a disabled button gets no click.
8822
- //
8823
- // A loading one gets the click but drops it. Bulma's spinner only stops a
8824
- // pointer, so without this a key flipped a toggle the mouse couldn't.
8825
- // `disabled` would stop both, but it takes focus away mid-save.
8826
- const handleClick = (event) => {
8827
- if (isLoading)
8828
- return;
8829
- onClick?.(event);
8830
- if (event.defaultPrevented)
8831
- return;
8832
- const next = !pressed;
8833
- onPressedChange?.(next);
8834
- if (!isControlled)
8835
- setInternalPressed(next);
8836
- };
8837
- // The props are spread first, so what a toggle depends on wins over
8838
- // anything a JavaScript caller or a spread object passes along: it stays
8839
- // a button, never submits, reports the state it's in, and says it's busy
8840
- // and won't respond while it's loading. A caller's own `aria-busy` or
8841
- // `aria-disabled` stays when it isn't loading.
8842
- return (jsxRuntime.jsx(Button, { ...props, ref: ref, as: "button", type: "button", isLoading: isLoading, "aria-busy": isLoading || ariaBusy, "aria-disabled": isLoading || ariaDisabled, "aria-pressed": pressed, color: pressed ? color : undefined, className: classNames(selectedClass, className), onClick: handleClick }));
8843
- });
8844
- ToggleButton.displayName = 'ToggleButton';
8845
-
8846
- const validButtonsSizes = ['small', 'medium', 'large'];
8847
- /**
8848
- * The `Buttons` component lets you group multiple `Button` elements together with Bulma's spacing, alignment, and add-on features.
8849
- *
8850
- * @function
8851
- * @param {ButtonsProps} props - Props for the Buttons component.
8852
- * @returns {JSX.Element} The rendered group of buttons.
8853
- * @see {@link https://bulma.io/documentation/elements/button/#group | Bulma Button Group documentation}
8854
- */
8855
- const ButtonsComponent = ({ className, textColor, color, bgColor, isCentered, isRight, hasAddons, size, children, ...props }) => {
8856
- const buttonsClasses = usePrefixedClassNames('buttons', {
8857
- 'is-centered': isCentered,
8858
- 'is-right': isRight,
8859
- 'has-addons': hasAddons,
8860
- [`are-${size}`]: size && validButtonsSizes.includes(size),
8861
- });
8862
- /**
8863
- * Generates Bulma helper classes and separates out remaining props.
8864
- */
8865
- const { bulmaHelperClasses, rest } = useBulmaClasses({
8866
- color: textColor ?? color,
8867
- backgroundColor: bgColor,
8868
- ...props,
8869
- });
8870
- const combinedClasses = classNames(buttonsClasses, className, bulmaHelperClasses);
8871
- return (jsxRuntime.jsx("div", { className: combinedClasses, ...rest, children: children }));
8872
- };
8873
- const Buttons = withSubComponents(ButtonsComponent, {
8874
- Button,
8875
- LinkButton,
8876
- ToggleButton,
8877
- }, 'Buttons');
8878
-
8879
9063
  /**
8880
9064
  * The `Code` component renders a styled `<code>` element with Bulma helper class integration.
8881
9065
  *
@@ -9185,7 +9369,8 @@ const visuallyHidden = {
9185
9369
  border: 0,
9186
9370
  };
9187
9371
  /**
9188
- * Works out what a status region says. Each item is announced once. Its
9372
+ * Works out what a status region says. Each item is announced once each time
9373
+ * it appears, so one that goes and comes back is announced again. Its
9189
9374
  * announcement is written `announceDelay` after it appears, or after the last
9190
9375
  * item to join it when several appear in quick succession, alongside whatever
9191
9376
  * the region still says. Once written, it stays for `announcementLifetime`,
@@ -9220,8 +9405,8 @@ function useAnnouncements(items, describe) {
9220
9405
  React.useEffect(() => {
9221
9406
  const announced = onScreenIdsRef.current;
9222
9407
  const appeared = items.filter(item => !announced.has(item.id));
9223
- // Items that have gone drop out here too. Ids are never reused, so
9224
- // forgetting them can't bring an old item back.
9408
+ // Items that have gone drop out here too, so one that comes back has
9409
+ // appeared again.
9225
9410
  onScreenIdsRef.current = new Set(items.map(item => item.id));
9226
9411
  const next = [];
9227
9412
  for (const item of appeared) {
@@ -9234,9 +9419,13 @@ function useAnnouncements(items, describe) {
9234
9419
  setAnnouncements(current => ({
9235
9420
  ...current,
9236
9421
  key: current.key + 1,
9237
- // Announcements still waiting go out with the new ones. What's
9238
- // already written stays put.
9239
- waiting: [...current.waiting, ...next],
9422
+ // Announcements still waiting go out with the new ones, apart from
9423
+ // one whose item came back before it was written, which the new one
9424
+ // replaces. What's already written stays put.
9425
+ waiting: [
9426
+ ...current.waiting.filter(waiting => !next.some(({ id }) => id === waiting.id)),
9427
+ ...next,
9428
+ ],
9240
9429
  }));
9241
9430
  }
9242
9431
  }, [items, describe]);
@@ -9395,10 +9584,10 @@ function readAloud(element) {
9395
9584
  return name || blockLevelTags.has(element.tagName) ? ` ${text} ` : text;
9396
9585
  }
9397
9586
  /**
9398
- * A visually hidden, polite `status` live region that a programmatic
9399
- * container announces its items through. The container renders it from the
9400
- * moment it mounts, whether or not it has anything to show, so the region is
9401
- * already in the page when an announcement is written into it.
9587
+ * A visually hidden, polite `status` live region that a container announces
9588
+ * its items through. The container renders it from the moment it mounts,
9589
+ * whether or not it has anything to show, so the region is already in the
9590
+ * page when an announcement is written into it.
9402
9591
  *
9403
9592
  * The announcements are this component's own state, so writing and clearing
9404
9593
  * them re-renders the region and leaves the container's items alone. Each one
@@ -9415,8 +9604,10 @@ function StatusRegion({ items, describe, }) {
9415
9604
  // reads the whole region on every change. Each announcement is its own
9416
9605
  // node, and only additions are relevant by default, so with atomic off a
9417
9606
  // screen reader reads just what was written, and removing one announcement
9418
- // doesn't read out the ones still there.
9419
- return (jsxRuntime.jsx("div", { role: "status", "aria-live": "polite", "aria-atomic": "false", style: visuallyHidden, children: announcements.map(announcement => (jsxRuntime.jsx("div", { children: announcement.text }, announcement.id))) }));
9607
+ // doesn't read out the ones still there. A node is keyed by its batch as
9608
+ // well as its item, since an item that comes back can be written again
9609
+ // while what it said before is still there.
9610
+ return (jsxRuntime.jsx("div", { role: "status", "aria-live": "polite", "aria-atomic": "false", style: visuallyHidden, children: announcements.map(announcement => (jsxRuntime.jsx("div", { children: announcement.text }, `${announcement.batch}:${announcement.id}`))) }));
9420
9611
  }
9421
9612
 
9422
9613
  /**
@@ -9897,27 +10088,6 @@ const OrderedListComponent = ({ className, textColor, bgColor, children, ...prop
9897
10088
  };
9898
10089
  const OrderedList = withSubComponents(OrderedListComponent, { Item: ListItem }, 'OrderedList');
9899
10090
 
9900
- /**
9901
- * The `Paragraph` component renders a styled `<p>` element with Bulma helper class integration.
9902
- *
9903
- * @function
9904
- * @param {ParagraphProps} props - Props for the Paragraph component.
9905
- * @returns {JSX.Element} The rendered paragraph element.
9906
- */
9907
- const Paragraph = ({ className, textColor, bgColor, children, ...props }) => {
9908
- /**
9909
- * Generates Bulma helper classes and separates out remaining props.
9910
- */
9911
- const { bulmaHelperClasses, rest } = useBulmaClasses({
9912
- color: textColor,
9913
- backgroundColor: bgColor,
9914
- ...props,
9915
- });
9916
- const bulmaClasses = usePrefixedClassNames();
9917
- const paragraphClasses = classNames(bulmaClasses, bulmaHelperClasses, className);
9918
- return (jsxRuntime.jsx("p", { className: paragraphClasses || undefined, ...rest, children: children }));
9919
- };
9920
-
9921
10091
  /**
9922
10092
  * The `Pre` component renders a styled `<pre>` element with Bulma helper class integration.
9923
10093
  *
@@ -10330,43 +10500,6 @@ isMultiline, size, children, ...props }) => {
10330
10500
  };
10331
10501
  const Tags = withSubComponents(TagsComponent, { Tag }, 'Tags');
10332
10502
 
10333
- const validTitleSizes = ['1', '2', '3', '4', '5', '6'];
10334
- const validTitleElements = ['h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'p'];
10335
- /**
10336
- * The `Title` component renders a Bulma-styled title (heading), supporting sizes `1`-`6`, spacing, and rendering as any heading or paragraph element (`h1`-`h6`, `p`).
10337
- *
10338
- * @function
10339
- * @param {TitleProps} props - Props for the Title component.
10340
- * @returns {JSX.Element} The rendered title element.
10341
- * @see {@link https://bulma.io/documentation/elements/title/ | Bulma Title documentation}
10342
- */
10343
- const Title = ({ className, size, isSpaced, as = 'h1', hasSkeleton, textColor, bgColor, children, ...props }) => {
10344
- /**
10345
- * Generates Bulma helper classes and separates out remaining props.
10346
- */
10347
- const { bulmaHelperClasses, rest } = useBulmaClasses({
10348
- color: textColor,
10349
- backgroundColor: bgColor,
10350
- ...props,
10351
- });
10352
- // Validate 'as' prop at runtime
10353
- const element = validTitleElements.includes(as) ? as : 'h1';
10354
- // Validate 'size' prop at runtime (numeric sizes normalize to strings)
10355
- const normalized = size == null ? undefined : String(size);
10356
- const validSize = normalized && validTitleSizes.includes(normalized)
10357
- ? normalized
10358
- : undefined;
10359
- const bulmaClasses = usePrefixedClassNames('title', {
10360
- [`is-${validSize}`]: validSize,
10361
- 'is-spaced': isSpaced,
10362
- 'has-skeleton': hasSkeleton,
10363
- });
10364
- const titleClasses = classNames(bulmaClasses, bulmaHelperClasses, className);
10365
- // Determine the tag based on 'element' and 'validSize'
10366
- const Tag = element === 'p' ? 'p' : validSize ? `h${validSize}` : element;
10367
- return (jsxRuntime.jsx(Tag, { className: titleClasses, ...rest, children: children }));
10368
- };
10369
-
10370
10503
  /**
10371
10504
  * The `UnorderedList` component renders a styled unordered list (`<ul>`) element with Bulma helper class integration.
10372
10505
  *
@@ -12492,6 +12625,10 @@ const Rate = React.forwardRef(({ label, labelSize, labelProps, horizontal, messa
12492
12625
  });
12493
12626
  Rate.displayName = 'Rate';
12494
12627
 
12628
+ // What the status region announces: the `empty` content, as the one item
12629
+ // it's handed while that content is on screen because of the user's typing.
12630
+ const typedNoMatches = [{ id: 'empty' }];
12631
+ const nothingToAnnounce = [];
12495
12632
  /**
12496
12633
  * The `Autocomplete` component provides an input field with dropdown suggestions that filter based on user input.
12497
12634
  *
@@ -12536,6 +12673,13 @@ label, labelSize, labelProps, horizontal, message, messageColor, fieldClassName,
12536
12673
  const dropdownRef = React.useRef(null);
12537
12674
  const [internalValue, setInternalValue] = React.useState('');
12538
12675
  const [isActive, setIsActive] = React.useState(false);
12676
+ // Whether the user has typed since the list opened, since only typing
12677
+ // gets the `empty` content announced. Closing the list resets it (during
12678
+ // render, React's pattern for adjusting state when other state changes),
12679
+ // so opening the list again without typing stays quiet.
12680
+ const [typed, setTyped] = React.useState(false);
12681
+ if (typed && !isActive)
12682
+ setTyped(false);
12539
12683
  const [highlightedIndex, setHighlightedIndex] = React.useState(-1);
12540
12684
  const listboxId = `${React.useId()}-listbox`;
12541
12685
  // Use controlled or internal value
@@ -12554,6 +12698,11 @@ label, labelSize, labelProps, horizontal, message, messageColor, fieldClassName,
12554
12698
  const displayValue = getDisplayValue(item);
12555
12699
  return displayValue.toLowerCase().includes(inputValue.toLowerCase());
12556
12700
  });
12701
+ // The dropdown shows the options or, when nothing matches, the `empty`
12702
+ // content. The listbox holds only options, so it's on screen only while
12703
+ // there are some.
12704
+ const menuOpen = isActive && (filteredData.length > 0 || !!empty);
12705
+ const listOpen = isActive && filteredData.length > 0;
12557
12706
  // Handle input change
12558
12707
  const handleInputChange = React.useCallback((e) => {
12559
12708
  const newValue = e.target.value;
@@ -12561,6 +12710,7 @@ label, labelSize, labelProps, horizontal, message, messageColor, fieldClassName,
12561
12710
  setInternalValue(newValue);
12562
12711
  }
12563
12712
  onInput?.(newValue);
12713
+ setTyped(true);
12564
12714
  if (!isActive && newValue) {
12565
12715
  setIsActive(true);
12566
12716
  }
@@ -12711,7 +12861,7 @@ label, labelSize, labelProps, horizontal, message, messageColor, fieldClassName,
12711
12861
  });
12712
12862
  const controlClasses = usePrefixedClassNames('control', 'is-expanded', 'has-icons-right');
12713
12863
  const dropdownMenuClasses = usePrefixedClassNames('dropdown-menu', {
12714
- 'is-active': isActive && (filteredData.length > 0 || !!empty),
12864
+ 'is-active': menuOpen,
12715
12865
  });
12716
12866
  const dropdownContentClass = usePrefixedClassNames('dropdown-content');
12717
12867
  const dropdownHeaderClass = usePrefixedClassNames('dropdown-header');
@@ -12733,24 +12883,51 @@ label, labelSize, labelProps, horizontal, message, messageColor, fieldClassName,
12733
12883
  // The listbox takes its name from the label that names the combobox, the
12734
12884
  // one this renders or a surrounding Field's, so the two say the same
12735
12885
  // thing. When no label names the combobox, or the one that does has no
12736
- // id to point at, the list keeps a fallback name.
12737
- const listOpen = isActive && (filteredData.length > 0 || !!empty);
12886
+ // id to point at, the list keeps a fallback name. While the dropdown
12887
+ // shows only the `empty` content there's no listbox, so the combobox is
12888
+ // collapsed and points at nothing.
12738
12889
  const activeOptionId = listOpen &&
12739
12890
  highlightedIndex >= 0 &&
12740
12891
  highlightedIndex < filteredData.length
12741
12892
  ? `${listboxId}-${highlightedIndex}`
12742
12893
  : undefined;
12743
- const autocompleteElement = (jsxRuntime.jsxs("div", { ref: containerRef, className: combinedClasses, ...rest, children: [jsxRuntime.jsxs("div", { className: controlClasses, children: [jsxRuntime.jsx("input", { ref: combinedRef, type: "text", className: inputClasses, id: controlId, value: inputValue, placeholder: placeholder, disabled: disabled, name: name, form: form, required: required, onChange: handleInputChange, onFocus: handleFocus, onKeyDown: handleKeyDown, role: "combobox", "aria-expanded": listOpen, "aria-controls": listOpen ? listboxId : undefined, "aria-activedescendant": activeOptionId, "aria-haspopup": "listbox", "aria-autocomplete": "list", autoComplete: "off" }), showClear && (jsxRuntime.jsx("button", { type: "button", className: iconRightClickableClass, onClick: handleClear, "aria-label": "Clear", children: jsxRuntime.jsxs("svg", { xmlns: "http://www.w3.org/2000/svg", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", width: "16", height: "16", "aria-hidden": "true", children: [jsxRuntime.jsx("line", { x1: "18", y1: "6", x2: "6", y2: "18" }), jsxRuntime.jsx("line", { x1: "6", y1: "6", x2: "18", y2: "18" })] }) })), loading && (jsxRuntime.jsx("span", { className: iconRightClass, children: jsxRuntime.jsx("span", { className: loaderClass }) }))] }), listOpen && (jsxRuntime.jsx("div", { className: dropdownMenuClasses, children: jsxRuntime.jsxs("div", { ref: dropdownRef, className: dropdownContentClass, style: { maxHeight: `${maxHeight}px`, overflowY: 'auto' }, id: listboxId, role: "listbox", "aria-labelledby": listLabelId, "aria-label": listLabelId ? undefined : 'Suggestions', onScroll: handleDropdownScroll, children: [header && jsxRuntime.jsx("div", { className: dropdownHeaderClass, children: header }), filteredData.length > 0 ? (filteredData.map((item, index) => {
12744
- const isDisabled = typeof item !== 'string' && item.disabled;
12745
- const isHighlighted = index === highlightedIndex;
12746
- const itemClasses = prefixedClassNames(classPrefix, 'dropdown-item', {
12747
- 'is-active': isHighlighted,
12748
- 'is-disabled': isDisabled,
12749
- });
12750
- return (jsxRuntime.jsx("a", { id: `${listboxId}-${index}`, "data-index": index, className: itemClasses, onClick: () => !isDisabled && handleSelect(item), onMouseEnter: () => !isDisabled && setHighlightedIndex(index), role: "option", "aria-selected": isHighlighted, "aria-disabled": isDisabled, children: itemTemplate
12751
- ? itemTemplate(item)
12752
- : getDisplayValue(item) }, index));
12753
- })) : (jsxRuntime.jsx("div", { className: emptyItemClasses, children: empty })), footer && jsxRuntime.jsx("div", { className: dropdownFooterClass, children: footer })] }) }))] }));
12894
+ // A polite status region, in the page from the start so it's there
12895
+ // before anything is written into it, announces the `empty` content
12896
+ // when the user's typing leaves nothing that matches what's in the
12897
+ // field. It follows what's on screen, so results that come in after the
12898
+ // typing count too. It's said once each time that happens, not again on
12899
+ // each keystroke while nothing matches. An empty field isn't a search,
12900
+ // so it stays quiet then, even with no data. While `loading`, or a
12901
+ // loading Control around it, says results are on their way, it waits,
12902
+ // and it's said once loading ends with nothing matching.
12903
+ const emptyItems = typed &&
12904
+ menuOpen &&
12905
+ !listOpen &&
12906
+ !!inputValue &&
12907
+ !loading &&
12908
+ !controlLoading
12909
+ ? typedNoMatches
12910
+ : nothingToAnnounce;
12911
+ // The region reads the text on screen, whatever `empty` renders.
12912
+ const emptyRef = React.useRef(null);
12913
+ const describeEmpty = React.useCallback(() => {
12914
+ const node = emptyRef.current;
12915
+ /* istanbul ignore if: it's only announced while it's on screen */
12916
+ if (!node)
12917
+ return null;
12918
+ return spokenText(node);
12919
+ }, []);
12920
+ const autocompleteElement = (jsxRuntime.jsxs("div", { ref: containerRef, className: combinedClasses, ...rest, children: [jsxRuntime.jsxs("div", { className: controlClasses, children: [jsxRuntime.jsx("input", { ref: combinedRef, type: "text", className: inputClasses, id: controlId, value: inputValue, placeholder: placeholder, disabled: disabled, name: name, form: form, required: required, onChange: handleInputChange, onFocus: handleFocus, onKeyDown: handleKeyDown, role: "combobox", "aria-expanded": listOpen, "aria-controls": listOpen ? listboxId : undefined, "aria-activedescendant": activeOptionId, "aria-haspopup": "listbox", "aria-autocomplete": "list", autoComplete: "off" }), showClear && (jsxRuntime.jsx("button", { type: "button", className: iconRightClickableClass, onClick: handleClear, "aria-label": "Clear", children: jsxRuntime.jsxs("svg", { xmlns: "http://www.w3.org/2000/svg", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", width: "16", height: "16", "aria-hidden": "true", children: [jsxRuntime.jsx("line", { x1: "18", y1: "6", x2: "6", y2: "18" }), jsxRuntime.jsx("line", { x1: "6", y1: "6", x2: "18", y2: "18" })] }) })), loading && (jsxRuntime.jsx("span", { className: iconRightClass, children: jsxRuntime.jsx("span", { className: loaderClass }) }))] }), menuOpen && (jsxRuntime.jsx("div", { className: dropdownMenuClasses, children: jsxRuntime.jsxs("div", { ref: dropdownRef, className: dropdownContentClass, style: { maxHeight: `${maxHeight}px`, overflowY: 'auto' }, onScroll: handleDropdownScroll, children: [header && jsxRuntime.jsx("div", { className: dropdownHeaderClass, children: header }), listOpen ? (jsxRuntime.jsx("div", { id: listboxId, role: "listbox", "aria-labelledby": listLabelId, "aria-label": listLabelId ? undefined : 'Suggestions', children: filteredData.map((item, index) => {
12921
+ const isDisabled = typeof item !== 'string' && item.disabled;
12922
+ const isHighlighted = index === highlightedIndex;
12923
+ const itemClasses = prefixedClassNames(classPrefix, 'dropdown-item', {
12924
+ 'is-active': isHighlighted,
12925
+ 'is-disabled': isDisabled,
12926
+ });
12927
+ return (jsxRuntime.jsx("a", { id: `${listboxId}-${index}`, "data-index": index, className: itemClasses, onClick: () => !isDisabled && handleSelect(item), onMouseEnter: () => !isDisabled && setHighlightedIndex(index), role: "option", "aria-selected": isHighlighted, "aria-disabled": isDisabled, children: itemTemplate
12928
+ ? itemTemplate(item)
12929
+ : getDisplayValue(item) }, index));
12930
+ }) })) : (jsxRuntime.jsx("div", { ref: emptyRef, className: emptyItemClasses, children: empty })), footer && jsxRuntime.jsx("div", { className: dropdownFooterClass, children: footer })] }) })), jsxRuntime.jsx(StatusRegion, { items: emptyItems, describe: describeEmpty })] }));
12754
12931
  if (!insideField) {
12755
12932
  return (jsxRuntime.jsxs(Field, { label: label, labelSize: labelSize, labelProps: fieldLabelProps, horizontal: horizontal, className: fieldClassName, children: [autocompleteElement, messageEl] }));
12756
12933
  }
@@ -19412,6 +19589,11 @@ exports.Dropdown = Dropdown;
19412
19589
  exports.DropdownDivider = DropdownDivider;
19413
19590
  exports.DropdownItem = DropdownItem;
19414
19591
  exports.Emphasis = Emphasis;
19592
+ exports.EmptyState = EmptyState;
19593
+ exports.EmptyStateActions = EmptyStateActions;
19594
+ exports.EmptyStateDescription = EmptyStateDescription;
19595
+ exports.EmptyStateIndicator = EmptyStateIndicator;
19596
+ exports.EmptyStateTitle = EmptyStateTitle;
19415
19597
  exports.Field = Field;
19416
19598
  exports.FieldBody = FieldBody;
19417
19599
  exports.FieldLabel = FieldLabel;