@allxsmith/bestax-bulma 5.54.0 → 5.55.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 (78) hide show
  1. package/dist/bestax.css +1 -1
  2. package/dist/bestax.css.map +1 -1
  3. package/dist/esm/components/Command.js +22 -22
  4. package/dist/esm/components/Command.js.map +1 -1
  5. package/dist/esm/components/DataTable.js +215 -0
  6. package/dist/esm/components/DataTable.js.map +1 -0
  7. package/dist/esm/components/useDataTable.js +482 -0
  8. package/dist/esm/components/useDataTable.js.map +1 -0
  9. package/dist/esm/elements/ColorSwatch.js +49 -0
  10. package/dist/esm/elements/ColorSwatch.js.map +1 -0
  11. package/dist/esm/elements/RingProgress.js +3 -3
  12. package/dist/esm/elements/Td.js +4 -2
  13. package/dist/esm/elements/Td.js.map +1 -1
  14. package/dist/esm/elements/Th.js +13 -5
  15. package/dist/esm/elements/Th.js.map +1 -1
  16. package/dist/esm/elements/Tr.js.map +1 -1
  17. package/dist/esm/form/ColorPicker.js +540 -0
  18. package/dist/esm/form/ColorPicker.js.map +1 -0
  19. package/dist/esm/form/Combobox.js +18 -18
  20. package/dist/esm/form/Combobox.js.map +1 -1
  21. package/dist/esm/form/Dropzone.js +524 -0
  22. package/dist/esm/form/Dropzone.js.map +1 -0
  23. package/dist/esm/form/File.js.map +1 -1
  24. package/dist/esm/form/_colorInternals/colorMath.js +292 -0
  25. package/dist/esm/form/_colorInternals/colorMath.js.map +1 -0
  26. package/dist/esm/form/_colorInternals/pickerState.js +160 -0
  27. package/dist/esm/form/_colorInternals/pickerState.js.map +1 -0
  28. package/dist/esm/helpers/statusRegion.js +3 -3
  29. package/dist/esm/helpers/statusRegion.js.map +1 -1
  30. package/dist/esm/helpers/useFocusTrap.js +2 -2
  31. package/dist/extras.css +1 -1
  32. package/dist/extras.css.map +1 -1
  33. package/dist/index.cjs +2986 -796
  34. package/dist/index.cjs.map +1 -1
  35. package/dist/index.esm.js +5 -0
  36. package/dist/index.esm.js.map +1 -1
  37. package/dist/types/components/DataTable.d.ts +120 -0
  38. package/dist/types/components/useDataTable.d.ts +445 -0
  39. package/dist/types/elements/ColorSwatch.d.ts +54 -0
  40. package/dist/types/elements/Td.d.ts +9 -1
  41. package/dist/types/elements/Th.d.ts +14 -2
  42. package/dist/types/elements/Tr.d.ts +10 -1
  43. package/dist/types/form/ColorPicker.d.ts +266 -0
  44. package/dist/types/form/Dropzone.d.ts +138 -0
  45. package/dist/types/form/File.d.ts +2 -0
  46. package/dist/types/form/_colorInternals/colorMath.d.ts +63 -0
  47. package/dist/types/form/_colorInternals/pickerState.d.ts +62 -0
  48. package/dist/types/index.d.ts +4 -0
  49. package/dist/types-cjs/components/DataTable.d.ts +120 -0
  50. package/dist/types-cjs/components/useDataTable.d.ts +445 -0
  51. package/dist/types-cjs/elements/ColorSwatch.d.ts +54 -0
  52. package/dist/types-cjs/elements/Td.d.ts +9 -1
  53. package/dist/types-cjs/elements/Th.d.ts +14 -2
  54. package/dist/types-cjs/elements/Tr.d.ts +10 -1
  55. package/dist/types-cjs/form/ColorPicker.d.ts +266 -0
  56. package/dist/types-cjs/form/Dropzone.d.ts +138 -0
  57. package/dist/types-cjs/form/File.d.ts +2 -0
  58. package/dist/types-cjs/form/_colorInternals/colorMath.d.ts +63 -0
  59. package/dist/types-cjs/form/_colorInternals/pickerState.d.ts +62 -0
  60. package/dist/types-cjs/index.d.ts +4 -0
  61. package/dist/versions/bestax-no-dark-mode.css +1 -1
  62. package/dist/versions/bestax-no-dark-mode.css.map +1 -1
  63. package/dist/versions/bestax-no-helpers-prefixed.css +1 -1
  64. package/dist/versions/bestax-no-helpers-prefixed.css.map +1 -1
  65. package/dist/versions/bestax-no-helpers.css +1 -1
  66. package/dist/versions/bestax-no-helpers.css.map +1 -1
  67. package/dist/versions/bestax-prefixed.css +1 -1
  68. package/dist/versions/bestax-prefixed.css.map +1 -1
  69. package/package.json +1 -1
  70. package/src/scss/components/_datatable.scss +152 -0
  71. package/src/scss/components/_index.scss +1 -0
  72. package/src/scss/elements/_colorswatch.scss +100 -0
  73. package/src/scss/elements/_index.scss +1 -0
  74. package/src/scss/form/_checkbox.scss +53 -0
  75. package/src/scss/form/_colorpicker.scss +354 -0
  76. package/src/scss/form/_dropzone.scss +233 -0
  77. package/src/scss/form/_file.scss +29 -1
  78. package/src/scss/form/_index.scss +2 -0
package/dist/index.cjs CHANGED
@@ -4530,7 +4530,7 @@ function flatChildren(el) {
4530
4530
  }
4531
4531
  return Array.from(el.children);
4532
4532
  }
4533
- const isShown$1 = (el) => !el.hasAttribute('inert') && getComputedStyle(el).display !== 'none';
4533
+ const isShown$2 = (el) => !el.hasAttribute('inert') && getComputedStyle(el).display !== 'none';
4534
4534
  const isVisible = (el) => {
4535
4535
  const { visibility } = getComputedStyle(el);
4536
4536
  return visibility !== 'hidden' && visibility !== 'collapse';
@@ -4544,7 +4544,7 @@ const isVisible = (el) => {
4544
4544
  function isShownUpTo(el, top) {
4545
4545
  let below = null;
4546
4546
  for (let node = el; node && node !== top; node = flatParent(node)) {
4547
- if (!isShown$1(node))
4547
+ if (!isShown$2(node))
4548
4548
  return false;
4549
4549
  if (below &&
4550
4550
  node.localName === 'details' &&
@@ -8578,11 +8578,11 @@ const announceDelay = 100;
8578
8578
  * again.
8579
8579
  */
8580
8580
  const announcementLifetime = 1000;
8581
- const noAnnouncements = [];
8581
+ const noAnnouncements$1 = [];
8582
8582
  const nothingWritten = [];
8583
8583
  const noAnnouncementsYet = {
8584
8584
  key: 0,
8585
- waiting: noAnnouncements,
8585
+ waiting: noAnnouncements$1,
8586
8586
  written: nothingWritten,
8587
8587
  };
8588
8588
  // Hides the region with inline styles rather than a class, so it needs no
@@ -8684,7 +8684,7 @@ function useAnnouncements(items, describe) {
8684
8684
  setAnnouncements(current => current.key === key
8685
8685
  ? {
8686
8686
  ...current,
8687
- waiting: noAnnouncements,
8687
+ waiting: noAnnouncements$1,
8688
8688
  written: [
8689
8689
  ...current.written,
8690
8690
  ...current.waiting
@@ -12697,12 +12697,12 @@ const defaultEmpty = 'No results';
12697
12697
  const typedNoMatches$2 = [{ id: 'empty' }];
12698
12698
  const nothingToAnnounce$2 = [];
12699
12699
  /** Whether a slot was given something to show. */
12700
- const isShown = (node) => node !== undefined && node !== null && node !== false && node !== '';
12701
- const noop$1 = () => { };
12702
- const registerNothing$1 = () => noop$1;
12700
+ const isShown$1 = (node) => node !== undefined && node !== null && node !== false && node !== '';
12701
+ const noop$2 = () => { };
12702
+ const registerNothing$1 = () => noop$2;
12703
12703
  // What a part rendered outside a Command reads: nothing to report to, so it
12704
12704
  // renders its markup and does nothing.
12705
- const standalone$1 = {
12705
+ const standalone$2 = {
12706
12706
  engine: {
12707
12707
  open: false,
12708
12708
  expanded: false,
@@ -12715,26 +12715,26 @@ const standalone$1 = {
12715
12715
  listboxRef: { current: null },
12716
12716
  registerPopup: registerNothing$1,
12717
12717
  registerItem: registerNothing$1,
12718
- onKeyDown: noop$1,
12719
- onChange: noop$1,
12720
- onClick: noop$1,
12721
- onBlur: noop$1,
12722
- hover: noop$1,
12723
- pick: noop$1,
12724
- keepFocus: noop$1,
12725
- markInside: noop$1,
12718
+ onKeyDown: noop$2,
12719
+ onChange: noop$2,
12720
+ onClick: noop$2,
12721
+ onBlur: noop$2,
12722
+ hover: noop$2,
12723
+ pick: noop$2,
12724
+ keepFocus: noop$2,
12725
+ markInside: noop$2,
12726
12726
  },
12727
12727
  label: defaultLabel,
12728
12728
  query: '',
12729
- onFieldChange: noop$1,
12730
- reportLoading: noop$1,
12729
+ onFieldChange: noop$2,
12730
+ reportLoading: noop$2,
12731
12731
  reportEmpty: registerNothing$1,
12732
12732
  registerHandler: registerNothing$1,
12733
12733
  };
12734
12734
  const CommandContext = React.createContext(null);
12735
- const useCommand = () => React.useContext(CommandContext) ?? standalone$1;
12735
+ const useCommand = () => React.useContext(CommandContext) ?? standalone$2;
12736
12736
  /** Hands a node to a forwarded ref and to one of the component's own. */
12737
- function setRefs$1(node, own, forwarded) {
12737
+ function setRefs$2(node, own, forwarded) {
12738
12738
  own.current = node;
12739
12739
  if (typeof forwarded === 'function')
12740
12740
  forwarded(node);
@@ -12851,7 +12851,7 @@ const CommandParts = React.forwardRef(({ children, label, query: controlledQuery
12851
12851
  registerHandler,
12852
12852
  };
12853
12853
  const { rootRef } = engine;
12854
- const rootRefs = React.useCallback((node) => setRefs$1(node, rootRef, ref), [rootRef, ref]);
12854
+ const rootRefs = React.useCallback((node) => setRefs$2(node, rootRef, ref), [rootRef, ref]);
12855
12855
  return (jsxRuntime.jsxs("div", { className: classNames(rootClass, bulmaHelperClasses, className), ...rest, ref: rootRefs, children: [jsxRuntime.jsx(CommandContext.Provider, { value: context, children: children }), jsxRuntime.jsx(StatusRegion, { items: announced, describe: describeEmpty })] }));
12856
12856
  });
12857
12857
  CommandParts.displayName = 'CommandParts';
@@ -12871,7 +12871,7 @@ const CommandInput = React.forwardRef(({ isLoading, iconLeft, iconLeftName, onKe
12871
12871
  return () => reportLoading(false);
12872
12872
  }, [reportLoading, isLoading]);
12873
12873
  const { inputRef } = engine;
12874
- const inputRefs = React.useCallback((node) => setRefs$1(node, inputRef, ref), [inputRef, ref]);
12874
+ const inputRefs = React.useCallback((node) => setRefs$2(node, inputRef, ref), [inputRef, ref]);
12875
12875
  const { expanded, highlighted } = engine;
12876
12876
  // A name the caller gave the field wins over the Command's `label`.
12877
12877
  const callerNamed = !!props['aria-label'] || 'aria-labelledby' in props;
@@ -12960,10 +12960,10 @@ const CommandItem = ({ value, disabled = false, onSelect, icon, description, hin
12960
12960
  // the rest, and described by the description and the hint.
12961
12961
  const baseId = React.useId();
12962
12962
  const labelId = `${baseId}label`;
12963
- const descriptionId = isShown(description)
12963
+ const descriptionId = isShown$1(description)
12964
12964
  ? `${baseId}description`
12965
12965
  : undefined;
12966
- const hintId = isShown(hint) ? `${baseId}hint` : undefined;
12966
+ const hintId = isShown$1(hint) ? `${baseId}hint` : undefined;
12967
12967
  const describedBy = [descriptionId, hintId].filter(Boolean).join(' ') || undefined;
12968
12968
  const callerNamed = !!props['aria-label'] || 'aria-labelledby' in props;
12969
12969
  return (jsxRuntime.jsxs("div", { className: classNames(itemClass, className), "aria-describedby": describedBy, ...props, ref: itemRef, id: engine.itemId(value) || undefined, role: "option", "aria-selected": disabled ? undefined : highlighted || undefined, "aria-disabled": disabled || undefined, "aria-labelledby": callerNamed ? props['aria-labelledby'] : labelId, onClick: event => {
@@ -12974,7 +12974,7 @@ const CommandItem = ({ value, disabled = false, onSelect, icon, description, hin
12974
12974
  onPointerMove?.(event);
12975
12975
  if (!disabled && !highlighted)
12976
12976
  engine.hover(value);
12977
- }, children: [isShown(icon) && (jsxRuntime.jsx("span", { className: iconClass, "aria-hidden": "true", children: icon })), jsxRuntime.jsxs("span", { className: bodyClass, children: [jsxRuntime.jsx("span", { className: labelClass, id: labelId, children: children }), descriptionId && (jsxRuntime.jsx("span", { className: descriptionClass, id: descriptionId, children: description }))] }), hintId && (jsxRuntime.jsx("span", { className: hintClass, id: hintId, children: hint }))] }));
12977
+ }, children: [isShown$1(icon) && (jsxRuntime.jsx("span", { className: iconClass, "aria-hidden": "true", children: icon })), jsxRuntime.jsxs("span", { className: bodyClass, children: [jsxRuntime.jsx("span", { className: labelClass, id: labelId, children: children }), descriptionId && (jsxRuntime.jsx("span", { className: descriptionClass, id: descriptionId, children: description }))] }), hintId && (jsxRuntime.jsx("span", { className: hintClass, id: hintId, children: hint }))] }));
12978
12978
  };
12979
12979
  CommandItem.displayName = 'CommandItem';
12980
12980
  /**
@@ -13013,7 +13013,7 @@ const CommandActionsContent = ({ actions, filter, placeholder, empty, }) => {
13013
13013
  sections.set(group, [...(sections.get(group) ?? []), action]);
13014
13014
  }
13015
13015
  const renderItem = (action) => (jsxRuntime.jsx(CommandItem, { value: action.id, disabled: action.disabled, icon: action.icon, description: action.description, hint: action.hint, onSelect: action.onSelect, children: action.label }, action.id));
13016
- return (jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [jsxRuntime.jsx(CommandInput, { placeholder: placeholder }), shown.length > 0 ? (jsxRuntime.jsx(CommandList, { children: Array.from(sections, ([group, members]) => group ? (jsxRuntime.jsx(CommandGroup, { label: group, children: members.map(renderItem) }, group)) : (members.map(renderItem))) })) : isShown(empty) ? (jsxRuntime.jsx(CommandEmpty, { children: empty })) : null] }));
13016
+ return (jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [jsxRuntime.jsx(CommandInput, { placeholder: placeholder }), shown.length > 0 ? (jsxRuntime.jsx(CommandList, { children: Array.from(sections, ([group, members]) => group ? (jsxRuntime.jsx(CommandGroup, { label: group, children: members.map(renderItem) }, group)) : (members.map(renderItem))) })) : isShown$1(empty) ? (jsxRuntime.jsx(CommandEmpty, { children: empty })) : null] }));
13017
13017
  };
13018
13018
  /**
13019
13019
  * The Command given `actions`. It renders the parts itself and filters the
@@ -13316,343 +13316,1026 @@ const Command = withSubComponents(CommandRoot, {
13316
13316
  }, 'Command');
13317
13317
 
13318
13318
  /**
13319
- * The `Block` component renders a simple container with Bulma's `.block` class, adding vertical margin between sections of content.
13319
+ * Thead component for rendering a styled Bulma table header.
13320
+ *
13321
+ * Supports Bulma helper classes for additional styling.
13320
13322
  *
13321
13323
  * @function
13322
- * @param {BlockProps} props - Props for the Block component.
13323
- * @returns {JSX.Element} The rendered block element.
13324
- * @see {@link https://bulma.io/documentation/elements/block/ | Bulma Block documentation}
13324
+ * @param {TheadProps} props - Props for the Thead component.
13325
+ * @returns {JSX.Element} The rendered table header element.
13326
+ * @see {@link https://bulma.io/documentation/elements/table/#table-head | Bulma Table documentation}
13325
13327
  */
13326
- const Block = ({ className, textColor, color, bgColor, children, ...props }) => {
13328
+ const Thead = ({ className, children, ...props }) => {
13327
13329
  /**
13328
13330
  * Generates Bulma helper classes and separates out remaining props.
13329
13331
  */
13330
- const { bulmaHelperClasses, rest } = useBulmaClasses({
13331
- color: textColor ?? color,
13332
- backgroundColor: bgColor,
13333
- ...props,
13334
- });
13335
- const bulmaClasses = usePrefixedClassNames('block');
13336
- const blockClasses = classNames(bulmaClasses, bulmaHelperClasses, className);
13337
- return (jsxRuntime.jsx("div", { className: blockClasses, ...rest, children: children }));
13332
+ const { bulmaHelperClasses, rest } = useBulmaClasses({ ...props });
13333
+ const theadClasses = classNames(className, bulmaHelperClasses);
13334
+ return (jsxRuntime.jsx("thead", { className: theadClasses || undefined, ...rest, children: children }));
13338
13335
  };
13339
13336
 
13340
13337
  /**
13341
- * The `Box` component renders a bordered, padded container with an optional shadow using Bulma's `.box` class.
13338
+ * Tbody component for rendering a styled Bulma table body.
13339
+ *
13340
+ * Supports Bulma helper classes for additional styling.
13342
13341
  *
13343
13342
  * @function
13344
- * @param {BoxProps} props - Props for the Box component.
13345
- * @returns {JSX.Element} The rendered box element.
13346
- * @see {@link https://bulma.io/documentation/elements/box/ | Bulma Box documentation}
13343
+ * @param {TbodyProps} props - Props for the Tbody component.
13344
+ * @returns {JSX.Element} The rendered table body element.
13345
+ * @see {@link https://bulma.io/documentation/elements/table/#table-body | Bulma Table documentation}
13347
13346
  */
13348
- const Box = ({ className, textColor, color, bgColor, hasShadow = true, children, style, ...props }) => {
13347
+ const Tbody = ({ className, children, ...props }) => {
13349
13348
  /**
13350
13349
  * Generates Bulma helper classes and separates out remaining props.
13351
13350
  */
13352
- const { bulmaHelperClasses, bulmaHelperStyles, rest } = useBulmaClasses({
13353
- color: textColor ?? color,
13354
- backgroundColor: bgColor,
13355
- ...props,
13356
- });
13357
- const bulmaClasses = usePrefixedClassNames('box', {
13358
- 'is-shadowless': !hasShadow,
13359
- });
13360
- const boxClasses = classNames(bulmaClasses, bulmaHelperClasses, className);
13361
- return (jsxRuntime.jsx("div", { className: boxClasses, style: mergeBulmaStyles(bulmaHelperStyles, style), ...rest, children: children }));
13351
+ const { bulmaHelperClasses, rest } = useBulmaClasses({ ...props });
13352
+ const tbodyClasses = classNames(className, bulmaHelperClasses);
13353
+ return (jsxRuntime.jsx("tbody", { className: tbodyClasses || undefined, ...rest, children: children }));
13362
13354
  };
13363
13355
 
13364
13356
  /**
13365
- * The `Code` component renders a styled `<code>` element with Bulma helper class integration.
13357
+ * Tfoot component for rendering a styled Bulma table footer.
13358
+ *
13359
+ * Supports Bulma helper classes for additional styling.
13366
13360
  *
13367
13361
  * @function
13368
- * @param {CodeProps} props - Props for the Code component.
13369
- * @returns {JSX.Element} The rendered code element.
13362
+ * @param {TfootProps} props - Props for the Tfoot component.
13363
+ * @returns {JSX.Element} The rendered table footer element.
13364
+ * @see {@link https://bulma.io/documentation/elements/table/#table-footer | Bulma Table documentation}
13370
13365
  */
13371
- const Code = ({ className, textColor, bgColor, children, ...props }) => {
13372
- const { bulmaHelperClasses, rest } = useBulmaClasses({
13373
- color: textColor,
13374
- backgroundColor: bgColor,
13375
- ...props,
13376
- });
13377
- const bulmaClasses = usePrefixedClassNames();
13378
- const codeClasses = classNames(bulmaClasses, bulmaHelperClasses, className);
13379
- return (jsxRuntime.jsx("code", { className: codeClasses || undefined, ...rest, children: children }));
13366
+ const Tfoot = ({ className, children, ...props }) => {
13367
+ /**
13368
+ * Generates Bulma helper classes and separates out remaining props.
13369
+ */
13370
+ const { bulmaHelperClasses, rest } = useBulmaClasses({ ...props });
13371
+ const tfootClasses = classNames(className, bulmaHelperClasses);
13372
+ return (jsxRuntime.jsx("tfoot", { className: tfootClasses || undefined, ...rest, children: children }));
13380
13373
  };
13381
13374
 
13382
- // Valid size modifiers for the content class
13383
- const validSizes = ['small', 'medium', 'large'];
13384
13375
  /**
13385
- * The `Content` component applies Bulma’s typographic styles to its children, enhancing the appearance of HTML elements like paragraphs, headings, lists, and tables.
13376
+ * The `isAligned` values `Th` and `Td` accept. Each one writes
13377
+ * `has-text-<value>` on the cell, and a value outside the tuple adds no class.
13378
+ *
13379
+ * One copy, imported by both cells, so a header and its body cells always
13380
+ * take the same values.
13381
+ *
13382
+ * Its own module rather than `Th.tsx` or `Td.tsx` because `src/index.ts`
13383
+ * wildcard-exports those files, and everything published there is public
13384
+ * forever. `iconProps.ts` makes the same call.
13385
+ */
13386
+ const validTableAlignments = ['left', 'right', 'centered'];
13387
+
13388
+ /**
13389
+ * The values the table `color` prop accepts, as a readonly tuple.
13390
+ *
13391
+ * `TableColor` is typed from it, and `Tr`, `Th` and `Td` all take a
13392
+ * `TableColor`, so the tuple and those props list the same values. Map over
13393
+ * it to build a color picker, or check a value that arrives at runtime before
13394
+ * passing it in: the components add no color class for a value outside the
13395
+ * tuple.
13396
+ *
13397
+ * @example
13398
+ * import { Tr, Td, validTableColors } from '@allxsmith/bestax-bulma';
13399
+ *
13400
+ * <Tr>
13401
+ * {validTableColors.map(color => (
13402
+ * <Td key={color} color={color}>
13403
+ * {color}
13404
+ * </Td>
13405
+ * ))}
13406
+ * </Tr>;
13407
+ */
13408
+ const validTableColors = [
13409
+ 'primary',
13410
+ 'link',
13411
+ 'info',
13412
+ 'success',
13413
+ 'warning',
13414
+ 'danger',
13415
+ 'black',
13416
+ 'dark',
13417
+ 'light',
13418
+ 'white',
13419
+ ];
13420
+ /**
13421
+ * Td component for rendering a styled Bulma table cell.
13422
+ *
13423
+ * Supports alignment, narrow and vertically centered cells, Bulma color modifiers, and helper classes for additional styling.
13386
13424
  *
13387
13425
  * @function
13388
- * @param {ContentProps} props - Props for the Content component.
13389
- * @returns {JSX.Element} The rendered content block.
13390
- * @see {@link https://bulma.io/documentation/elements/content/ | Bulma Content documentation}
13426
+ * @param {TdProps} props - Props for the Td component.
13427
+ * @returns {JSX.Element} The rendered table cell element.
13428
+ * @see {@link https://bulma.io/documentation/elements/table/#table-body | Bulma Table documentation}
13391
13429
  */
13392
- const Content = ({ className, textColor, color, bgColor, size, children, ...props }) => {
13430
+ const Td = ({ className, isAligned, isNarrow, isVcentered, color, children, ...props }) => {
13431
+ const bulmaClasses = usePrefixedClassNames('', {
13432
+ [`has-text-${isAligned}`]: isAligned && validTableAlignments.includes(isAligned),
13433
+ [`is-${color}`]: color && validTableColors.includes(color),
13434
+ 'is-narrow': isNarrow,
13435
+ 'is-vcentered': isVcentered,
13436
+ });
13393
13437
  /**
13394
13438
  * Generates Bulma helper classes and separates out remaining props.
13395
13439
  */
13396
- const { bulmaHelperClasses, rest } = useBulmaClasses({
13397
- color: textColor ?? color,
13398
- backgroundColor: bgColor,
13399
- ...props,
13400
- });
13401
- const bulmaClasses = usePrefixedClassNames('content', {
13402
- [`is-${size}`]: size && size !== 'normal' && validSizes.includes(size),
13403
- });
13404
- const contentClasses = classNames(bulmaClasses, bulmaHelperClasses, className);
13405
- return (jsxRuntime.jsx("div", { className: contentClasses, ...rest, children: children }));
13440
+ const { bulmaHelperClasses, rest } = useBulmaClasses({ ...props });
13441
+ const tdClasses = classNames(bulmaClasses, className, bulmaHelperClasses);
13442
+ return (jsxRuntime.jsx("td", { className: tdClasses || undefined, ...rest, children: children }));
13406
13443
  };
13407
13444
 
13408
13445
  /**
13409
- * The `Delete` component provides a Bulma-styled close/delete button for dismissing modals, notifications, tags, messages, and more.
13446
+ * Tr component for rendering a styled Bulma table row.
13410
13447
  *
13411
- * It renders `type="button"`, so a delete button inside a form does not submit it.
13448
+ * Supports the is-selected modifier and color modifiers.
13412
13449
  *
13413
13450
  * @function
13414
- * @param {DeleteProps} props - Props for the Delete component.
13415
- * @returns {JSX.Element} The rendered delete button.
13416
- * @see {@link https://bulma.io/documentation/elements/delete/ | Bulma Delete documentation}
13451
+ * @param {TrProps} props - Props for the Tr component.
13452
+ * @returns {JSX.Element} The rendered table row element.
13453
+ * @see {@link https://bulma.io/documentation/elements/table/#table-row | Bulma Table documentation}
13417
13454
  */
13418
- const Delete = ({ className, textColor, color, bgColor, onClick, size, ariaLabel, disabled = false, ...props }) => {
13455
+ const Tr = ({ className, isSelected, color, children, ...props }) => {
13456
+ const bulmaClasses = usePrefixedClassNames('', {
13457
+ 'is-selected': isSelected,
13458
+ [`is-${color}`]: color && validTableColors.includes(color),
13459
+ });
13419
13460
  /**
13420
13461
  * Generates Bulma helper classes and separates out remaining props.
13421
13462
  */
13422
- const { bulmaHelperClasses, rest } = useBulmaClasses({
13423
- color: textColor ?? color,
13424
- backgroundColor: bgColor,
13425
- ...props,
13426
- });
13427
- const bulmaClasses = usePrefixedClassNames('delete', {
13428
- [`is-${size}`]: size,
13429
- 'is-disabled': disabled,
13430
- });
13431
- const classes = classNames(bulmaClasses, bulmaHelperClasses, className);
13432
- return (jsxRuntime.jsx("button", { className: classes, onClick: onClick, disabled: disabled, ...rest, "aria-label": rest['aria-label'] || ariaLabel || 'Close', type: buttonType(rest.type) }));
13463
+ const { bulmaHelperClasses, rest } = useBulmaClasses({ ...props });
13464
+ const trClasses = classNames(bulmaClasses, className, bulmaHelperClasses);
13465
+ return (jsxRuntime.jsx("tr", { className: trClasses || undefined, ...rest, children: children }));
13433
13466
  };
13434
13467
 
13435
- const validDescriptionListOrientations = ['vertical', 'horizontal'];
13436
- const isOrientation = (orientation) => !!orientation &&
13437
- validDescriptionListOrientations.includes(orientation);
13438
- /** Whether a prop was given something to render. */
13439
- const isGiven = (node) => node !== undefined &&
13440
- node !== null &&
13441
- typeof node !== 'boolean' &&
13442
- node !== '';
13443
13468
  /**
13444
- * A label, the name of a fact: a `<dt>`, in the weaker text color.
13469
+ * Th component for rendering a styled Bulma table header cell.
13445
13470
  *
13446
- * @function
13447
- * @param {DescriptionListLabelProps} props - Props for DescriptionList.Label.
13448
- * @param {React.Ref<HTMLElement>} ref - Forwarded ref to the `<dt>`.
13449
- * @returns {JSX.Element} The rendered label.
13471
+ * Supports alignment, width, narrow and vertically centered cells, and color modifiers.
13450
13472
  *
13451
- * @example
13452
- * <DescriptionList.Label>Placed on</DescriptionList.Label>
13473
+ * @function
13474
+ * @param {ThProps} props - Props for the Th component.
13475
+ * @returns {JSX.Element} The rendered table header cell element.
13476
+ * @see {@link https://bulma.io/documentation/elements/table/#table-head | Bulma Table documentation}
13453
13477
  */
13454
- const DescriptionListLabel = React.forwardRef(({ className, textColor, bgColor, children, ...props }, ref) => {
13455
- const { bulmaHelperClasses, rest } = useBulmaClasses({
13456
- color: textColor,
13457
- backgroundColor: bgColor,
13458
- ...props,
13478
+ const Th = ({ className, isAligned, isNarrow, isVcentered, width, color, style, children, ...props }) => {
13479
+ const bulmaClasses = usePrefixedClassNames('', {
13480
+ [`has-text-${isAligned}`]: isAligned && validTableAlignments.includes(isAligned),
13481
+ [`is-${color}`]: color && validTableColors.includes(color),
13482
+ 'is-narrow': isNarrow,
13483
+ 'is-vcentered': isVcentered,
13459
13484
  });
13460
- const labelClass = usePrefixedClassNames('description-list-label');
13461
- return (jsxRuntime.jsx("dt", { ref: ref, className: classNames(labelClass, bulmaHelperClasses, className), ...rest, children: children }));
13462
- });
13463
- DescriptionListLabel.displayName = 'DescriptionList.Label';
13485
+ /**
13486
+ * Generates Bulma helper classes and separates out remaining props.
13487
+ */
13488
+ const { bulmaHelperClasses, rest } = useBulmaClasses({ ...props });
13489
+ const thClasses = classNames(bulmaClasses, className, bulmaHelperClasses);
13490
+ return (jsxRuntime.jsx("th", { className: thClasses || undefined, style:
13491
+ // A `width` key the style leaves undefined isn't a width, so the
13492
+ // prop's stays.
13493
+ width && style?.width === undefined
13494
+ ? {
13495
+ ...style,
13496
+ width: typeof width === 'number' ? `${width}px` : width,
13497
+ }
13498
+ : style, ...rest, children: children }));
13499
+ };
13500
+
13464
13501
  /**
13465
- * A value, what the label says about the thing: a `<dd>`. An item may have several.
13502
+ * The `Table` component and its subcomponents provide a highly composable, Bulma-styled table system for React.
13466
13503
  *
13467
13504
  * @function
13468
- * @param {DescriptionListValueProps} props - Props for DescriptionList.Value.
13469
- * @param {React.Ref<HTMLElement>} ref - Forwarded ref to the `<dd>`.
13470
- * @returns {JSX.Element} The rendered value.
13471
- *
13472
- * @example
13473
- * <DescriptionList.Value>14 October 2026</DescriptionList.Value>
13505
+ * @param {TableProps} props - Props for the Table component.
13506
+ * @returns {JSX.Element} The rendered table element.
13507
+ * @see {@link https://bulma.io/documentation/elements/table/ | Bulma Table documentation}
13474
13508
  */
13475
- const DescriptionListValue = React.forwardRef(({ className, textColor, bgColor, children, ...props }, ref) => {
13476
- const { bulmaHelperClasses, rest } = useBulmaClasses({
13477
- color: textColor,
13478
- backgroundColor: bgColor,
13479
- ...props,
13480
- });
13481
- const valueClass = usePrefixedClassNames('description-list-value');
13482
- return (jsxRuntime.jsx("dd", { ref: ref, className: classNames(valueClass, bulmaHelperClasses, className), ...rest, children: children }));
13483
- });
13484
- DescriptionListValue.displayName = 'DescriptionList.Value';
13509
+ const TableComponent = ({ className, isBordered, isStriped, isNarrow, isHoverable, isFullwidth, isFullWidth, isResponsive, children, ...props }) => {
13510
+ /**
13511
+ * Generates Bulma helper classes and separates out remaining props.
13512
+ */
13513
+ const { bulmaHelperClasses, rest } = useBulmaClasses({ ...props });
13514
+ const bulmaClasses = usePrefixedClassNames('table', {
13515
+ 'is-bordered': isBordered,
13516
+ 'is-striped': isStriped,
13517
+ 'is-narrow': isNarrow,
13518
+ 'is-hoverable': isHoverable,
13519
+ 'is-fullwidth': isFullwidth ?? isFullWidth,
13520
+ });
13521
+ const containerClass = usePrefixedClassNames('table-container');
13522
+ const tableClasses = classNames(bulmaClasses, bulmaHelperClasses, className);
13523
+ const tableElement = (jsxRuntime.jsx("table", { className: tableClasses, ...rest, children: children }));
13524
+ if (isResponsive) {
13525
+ return jsxRuntime.jsx("div", { className: containerClass, children: tableElement });
13526
+ }
13527
+ return tableElement;
13528
+ };
13529
+ const Table = withSubComponents(TableComponent, {
13530
+ Thead,
13531
+ Tbody,
13532
+ Tfoot,
13533
+ Tr,
13534
+ Th,
13535
+ Td,
13536
+ }, 'Table');
13537
+
13485
13538
  /**
13486
- * One fact: its label and its value, in a `<div>`, from the `label` and `value` props or from `Label` and `Value` children.
13539
+ * The `Skeleton` component provides a Bulma-styled skeleton loader for React applications, useful for indicating that content is loading.
13487
13540
  *
13488
13541
  * @function
13489
- * @param {DescriptionListItemProps} props - Props for DescriptionList.Item.
13490
- * @param {React.Ref<HTMLDivElement>} ref - Forwarded ref to the `<div>`.
13491
- * @returns {JSX.Element} The rendered item.
13542
+ * @param {SkeletonProps} props - Props for the Skeleton component.
13543
+ * @returns {JSX.Element} The rendered skeleton element.
13492
13544
  *
13493
13545
  * @example
13494
- * <DescriptionList.Item label="Order" value="#1234" />
13546
+ * // Block skeleton
13547
+ * <Skeleton />
13495
13548
  *
13496
13549
  * @example
13497
- * <DescriptionList.Item label="Tags">
13498
- * <DescriptionList.Value>Gift</DescriptionList.Value>
13499
- * <DescriptionList.Value>Priority</DescriptionList.Value>
13500
- * </DescriptionList.Item>
13550
+ * // Multi-line skeleton
13551
+ * <Skeleton variant="lines" lines={5} />
13501
13552
  */
13502
- const DescriptionListItem = React.forwardRef(({ className, label, value, textColor, bgColor, children, ...props }, ref) => {
13503
- const { bulmaHelperClasses, rest } = useBulmaClasses({
13504
- color: textColor,
13505
- backgroundColor: bgColor,
13506
- ...props,
13553
+ const Skeleton = ({ className, variant = 'block', lines = 3, children, ...props }) => {
13554
+ const linesClass = usePrefixedClassNames('skeleton-lines');
13555
+ const blockClass = usePrefixedClassNames('skeleton-block');
13556
+ if (variant === 'lines') {
13557
+ return (jsxRuntime.jsx("div", { className: classNames(linesClass, className), ...props, children: Array.from({ length: lines }).map((_, i) => (jsxRuntime.jsx("div", {}, i))) }));
13558
+ }
13559
+ return (jsxRuntime.jsx("div", { className: classNames(blockClass, className), ...props, children: children }));
13560
+ };
13561
+
13562
+ /** A column's raw value for a row. */
13563
+ function readValue(column, row) {
13564
+ const { accessor } = column;
13565
+ if (typeof accessor === 'function')
13566
+ return accessor(row);
13567
+ if (row === null || typeof row !== 'object')
13568
+ return undefined;
13569
+ return row[(accessor ?? column.id)];
13570
+ }
13571
+ /** A column's text for a row. */
13572
+ function readText(column, row) {
13573
+ const value = readValue(column, row);
13574
+ if (column.format)
13575
+ return column.format(value, row);
13576
+ return value === null || value === undefined ? '' : String(value);
13577
+ }
13578
+ /** Whether a value sorts as empty: nothing, no text, or no number or date. */
13579
+ function isEmptyValue(value) {
13580
+ return (value === null ||
13581
+ value === undefined ||
13582
+ value === '' ||
13583
+ (typeof value === 'number' && Number.isNaN(value)) ||
13584
+ (value instanceof Date && Number.isNaN(value.getTime())));
13585
+ }
13586
+ /** Compares two values that aren't empty, for an ascending sort. */
13587
+ function compareValues(a, b, collator) {
13588
+ if (typeof a === 'number' && typeof b === 'number')
13589
+ return a - b;
13590
+ if (a instanceof Date && b instanceof Date)
13591
+ return a.getTime() - b.getTime();
13592
+ if (typeof a === 'boolean' && typeof b === 'boolean') {
13593
+ return Number(a) - Number(b);
13594
+ }
13595
+ return collator.compare(String(a), String(b));
13596
+ }
13597
+ /** Whether a filter value filters anything. */
13598
+ function isActiveFilter(value) {
13599
+ return (value !== undefined &&
13600
+ value !== null &&
13601
+ value !== '' &&
13602
+ !(Array.isArray(value) && value.length === 0));
13603
+ }
13604
+ /**
13605
+ * Whether a search or a filter value is narrowing the rows. Used by
13606
+ * DataTable to tell an empty table from one with no matches.
13607
+ */
13608
+ function isFiltering(search, filters) {
13609
+ return search.trim() !== '' || Object.values(filters).some(isActiveFilter);
13610
+ }
13611
+ /** A row's id from its `id` property, or nothing when it has none. */
13612
+ function idOf(row) {
13613
+ if (row === null || typeof row !== 'object')
13614
+ return undefined;
13615
+ const id = row.id;
13616
+ return id === undefined || id === null || id === '' ? undefined : String(id);
13617
+ }
13618
+ /**
13619
+ * A piece of state that's the caller's when they pass its value, and the
13620
+ * table's own otherwise. Setting it calls the change handler either way.
13621
+ *
13622
+ * It also returns `latest`, what a change should build on. While the table
13623
+ * holds the state, that's the value last set, even before React renders it,
13624
+ * so two changes in one handler build on each other. While the caller holds
13625
+ * it, it's the value as rendered, since only the caller knows what it made
13626
+ * of a change.
13627
+ */
13628
+ function useTableState(value, initial, onChange) {
13629
+ const [own, setOwn] = React.useState(initial);
13630
+ const held = value !== undefined;
13631
+ const current = held ? value : own;
13632
+ const rendered = useLatestRef(current);
13633
+ // A value set since the last commit, cleared once a commit shows it.
13634
+ const pendingRef = React.useRef(null);
13635
+ useClientLayoutEffect(() => {
13636
+ pendingRef.current = null;
13507
13637
  });
13508
- const itemClass = usePrefixedClassNames('description-list-item');
13509
- return (jsxRuntime.jsxs("div", { ref: ref, className: classNames(itemClass, bulmaHelperClasses, className), ...rest, children: [isGiven(label) && jsxRuntime.jsx(DescriptionListLabel, { children: label }), children, isGiven(value) && jsxRuntime.jsx(DescriptionListValue, { children: value })] }));
13510
- });
13511
- DescriptionListItem.displayName = 'DescriptionList.Item';
13638
+ const set = React.useCallback((next) => {
13639
+ if (!held) {
13640
+ pendingRef.current = { value: next };
13641
+ setOwn(next);
13642
+ }
13643
+ onChange?.(next);
13644
+ }, [held, onChange]);
13645
+ const latest = React.useCallback(() => (pendingRef.current ? pendingRef.current.value : rendered.current), [rendered]);
13646
+ return [current, set, latest];
13647
+ }
13648
+ const noSort = [];
13649
+ const noIds = [];
13650
+ const noFilters = {};
13512
13651
  /**
13513
- * The `DescriptionList` component lists facts about one thing, each a label and its value, such as an order's details or a profile.
13652
+ * `useDataTable` holds a table's sort, selection, page, search and filters,
13653
+ * works out its rows, and hands out props for bestax's own table parts, so
13654
+ * custom markup behaves as `DataTable` does. Pass its result to
13655
+ * `<DataTable table={table} />` to render it with DataTable's own markup,
13656
+ * with a toolbar or an export button elsewhere on the page.
13514
13657
  *
13515
- * It's a `<dl>`, with a `<div>` for each item, a `<dt>` for each label and
13516
- * a `<dd>` for each value, so assistive tech gets HTML's own list of terms
13517
- * and definitions and no roles are added. Labels sit above their values on
13518
- * a phone and beside them from Bulma's tablet breakpoint up, where every
13519
- * label shares one column so the values line up. For data with two
13520
- * dimensions, such as several records with the same fields, use a `Table`.
13658
+ * @example
13659
+ * const table = useDataTable({ columns, data: users, pageSize: 10 });
13660
+ *
13661
+ * <DataTable table={table} caption="Users" hasSearch />
13662
+ */
13663
+ function useDataTable(options) {
13664
+ const { columns, data, getRowId: rowIdOf, sort: sortProp, defaultSort = noSort, onSortChange, selectionMode = 'none', selected: selectedProp, defaultSelected = noIds, onSelectedChange, pageSize: pageSizeProp, page: pageProp, defaultPage = 1, onPageChange, search: searchProp, defaultSearch = '', onSearchChange, filters: filtersProp, defaultFilters = noFilters, onFiltersChange, manual = false, rowCount: rowCountProp, labels, } = options;
13665
+ const [sort, setSort] = useTableState(sortProp, defaultSort, onSortChange);
13666
+ const [selected, setSelected, latestSelected] = useTableState(selectedProp, defaultSelected, onSelectedChange);
13667
+ const [pageState, setPageState, latestPage] = useTableState(pageProp, defaultPage, onPageChange);
13668
+ const [search, setSearchState] = useTableState(searchProp, defaultSearch, onSearchChange);
13669
+ const [filters, setFiltersState, latestFilters] = useTableState(filtersProp, defaultFilters, onFiltersChange);
13670
+ const prefix = useClassPrefix();
13671
+ const { locale } = useLocale();
13672
+ const matchText = useLocaleMatcher();
13673
+ const formatNumber = useNumberFormat();
13674
+ const collator = React.useMemo(() => new Intl.Collator(locale, { numeric: true }), [locale]);
13675
+ const pageSize = pageSizeProp !== undefined &&
13676
+ Number.isFinite(pageSizeProp) &&
13677
+ pageSizeProp >= 1
13678
+ ? Math.floor(pageSizeProp)
13679
+ : undefined;
13680
+ const columnsById = React.useMemo(() => {
13681
+ const byId = new Map();
13682
+ for (const column of columns) {
13683
+ if (byId.has(column.id)) {
13684
+ warnOnce('DataTable:duplicate-column', `[bestax-bulma] DataTable has two columns with the id "${column.id}". ` +
13685
+ 'Give each column its own id: the sort, the filters and an export ' +
13686
+ 'find a column by it.');
13687
+ }
13688
+ else {
13689
+ byId.set(column.id, column);
13690
+ }
13691
+ }
13692
+ return byId;
13693
+ }, [columns]);
13694
+ // Each row's id, by its index in `data`.
13695
+ const ids = React.useMemo(() => {
13696
+ const out = data.map((row, index) => {
13697
+ const id = rowIdOf ? rowIdOf(row, index) : idOf(row);
13698
+ if (id === undefined || id === '') {
13699
+ warnOnce('DataTable:missing-row-id', '[bestax-bulma] A DataTable row has no id, so it falls back to its ' +
13700
+ "index, which doesn't survive the data changing. Give each row " +
13701
+ 'an `id`, or pass `getRowId`.');
13702
+ return String(index);
13703
+ }
13704
+ return id;
13705
+ });
13706
+ if (isDev() && new Set(out).size < out.length) {
13707
+ warnOnce('DataTable:duplicate-row-id', '[bestax-bulma] Two DataTable rows have the same id, so selecting ' +
13708
+ 'one selects both. Make the ids unique, or pass a `getRowId` ' +
13709
+ 'that does.');
13710
+ }
13711
+ return out;
13712
+ }, [data, rowIdOf]);
13713
+ const indexOf = React.useMemo(() => {
13714
+ const map = new Map();
13715
+ data.forEach((row, index) => {
13716
+ if (!map.has(row))
13717
+ map.set(row, index);
13718
+ });
13719
+ return map;
13720
+ }, [data]);
13721
+ /** A row's id, from its index when the row is one of `data`. */
13722
+ const getRowId = React.useCallback((row) => {
13723
+ const index = indexOf.get(row);
13724
+ if (index !== undefined)
13725
+ return ids[index];
13726
+ const id = rowIdOf ? rowIdOf(row, -1) : idOf(row);
13727
+ return id ?? '';
13728
+ }, [indexOf, ids, rowIdOf]);
13729
+ // Every column's text for every row, worked out once per data and
13730
+ // columns, since the search reads them on every keystroke.
13731
+ const texts = React.useMemo(() => data.map(row => columns.map(column => readText(column, row))), [data, columns]);
13732
+ const sortedIndexes = React.useMemo(() => {
13733
+ const all = data.map((_, index) => index);
13734
+ if (manual)
13735
+ return all;
13736
+ const keys = sort
13737
+ .map(entry => ({
13738
+ column: columnsById.get(entry.id),
13739
+ descending: entry.direction === 'descending',
13740
+ }))
13741
+ .filter((key) => key.column !== undefined);
13742
+ if (keys.length === 0)
13743
+ return all;
13744
+ const values = keys.map(({ column }) => column.sortFn ? null : data.map(row => readValue(column, row)));
13745
+ return all.sort((x, y) => {
13746
+ for (let k = 0; k < keys.length; k++) {
13747
+ const { column, descending } = keys[k];
13748
+ let result;
13749
+ if (column.sortFn) {
13750
+ result = column.sortFn(data[x], data[y]);
13751
+ if (descending)
13752
+ result = -result;
13753
+ }
13754
+ else {
13755
+ const a = values[k][x];
13756
+ const b = values[k][y];
13757
+ const aEmpty = isEmptyValue(a);
13758
+ const bEmpty = isEmptyValue(b);
13759
+ if (aEmpty || bEmpty) {
13760
+ // Empty values go last whichever way the column sorts.
13761
+ result = Number(aEmpty) - Number(bEmpty);
13762
+ }
13763
+ else {
13764
+ result = compareValues(a, b, collator);
13765
+ if (descending)
13766
+ result = -result;
13767
+ }
13768
+ }
13769
+ if (result)
13770
+ return result;
13771
+ }
13772
+ // Ties keep the order of `data`.
13773
+ return x - y;
13774
+ });
13775
+ }, [data, sort, columnsById, collator, manual]);
13776
+ const filteredIndexes = React.useMemo(() => {
13777
+ if (manual)
13778
+ return sortedIndexes;
13779
+ const query = search.trim();
13780
+ const holds = matchText
13781
+ ? (text, needle) => matchText(text, needle)
13782
+ : (text, needle) => text.toLowerCase().includes(needle.toLowerCase());
13783
+ const searched = columns
13784
+ .map((column, index) => ({ column, index }))
13785
+ .filter(({ column }) => column.filterable !== false);
13786
+ const filtered = Object.entries(filters)
13787
+ .filter(([, value]) => isActiveFilter(value))
13788
+ .map(([id, value]) => ({
13789
+ column: columnsById.get(id),
13790
+ index: columns.findIndex(column => column.id === id),
13791
+ value,
13792
+ }))
13793
+ .filter(({ column }) => column !== undefined && column.filterable !== false);
13794
+ if (query === '' && filtered.length === 0)
13795
+ return sortedIndexes;
13796
+ return sortedIndexes.filter(rowIndex => {
13797
+ const row = data[rowIndex];
13798
+ const rowTexts = texts[rowIndex];
13799
+ for (const { column, index, value } of filtered) {
13800
+ // An array is any of its entries, as a multiple-choice filter gives.
13801
+ const passes = column.filterFn
13802
+ ? column.filterFn(row, value)
13803
+ : Array.isArray(value)
13804
+ ? value.some(entry => holds(rowTexts[index], String(entry)))
13805
+ : holds(rowTexts[index], String(value));
13806
+ if (!passes)
13807
+ return false;
13808
+ }
13809
+ return (query === '' ||
13810
+ searched.some(({ index }) => holds(rowTexts[index], query)));
13811
+ });
13812
+ }, [
13813
+ manual,
13814
+ sortedIndexes,
13815
+ search,
13816
+ filters,
13817
+ matchText,
13818
+ columns,
13819
+ columnsById,
13820
+ data,
13821
+ texts,
13822
+ ]);
13823
+ const knowsCount = rowCountProp !== undefined && Number.isFinite(rowCountProp);
13824
+ if (manual && pageSize !== undefined && !knowsCount) {
13825
+ warnOnce('DataTable:manual-row-count', '[bestax-bulma] A DataTable with `manual` and a `pageSize` has no ' +
13826
+ '`rowCount`, so it counts only the rows in `data`, makes one page ' +
13827
+ 'of them and shows no pager. Pass `rowCount`, how many rows match ' +
13828
+ 'on the server.');
13829
+ }
13830
+ const rowCount = manual
13831
+ ? Math.max(Math.floor(knowsCount ? rowCountProp : data.length), 0)
13832
+ : filteredIndexes.length;
13833
+ const pageCount = pageSize ? Math.max(Math.ceil(rowCount / pageSize), 1) : 1;
13834
+ const page = Math.min(Math.max(Number.isFinite(pageState) ? Math.floor(pageState) : 1, 1), pageCount);
13835
+ const pageIndexes = React.useMemo(() => {
13836
+ if (manual || !pageSize)
13837
+ return filteredIndexes;
13838
+ return filteredIndexes.slice((page - 1) * pageSize, page * pageSize);
13839
+ }, [manual, pageSize, filteredIndexes, page]);
13840
+ const selectedSet = React.useMemo(() => new Set(selected), [selected]);
13841
+ const selectable = selectionMode === 'multiple';
13842
+ const rowsOf = React.useCallback((indexes) => indexes.map(index => data[index]), [data]);
13843
+ const getRows = React.useCallback((scope = 'filtered') => {
13844
+ switch (scope) {
13845
+ case 'all':
13846
+ return rowsOf(sortedIndexes);
13847
+ case 'page':
13848
+ return rowsOf(pageIndexes);
13849
+ case 'selected':
13850
+ return rowsOf(filteredIndexes.filter(index => selectedSet.has(ids[index])));
13851
+ default:
13852
+ return rowsOf(filteredIndexes);
13853
+ }
13854
+ }, [rowsOf, sortedIndexes, pageIndexes, filteredIndexes, selectedSet, ids]);
13855
+ const getValue = React.useCallback((row, columnId) => {
13856
+ const column = columnsById.get(columnId);
13857
+ return column ? readValue(column, row) : undefined;
13858
+ }, [columnsById]);
13859
+ const getText = React.useCallback((row, columnId) => {
13860
+ const column = columnsById.get(columnId);
13861
+ if (!column)
13862
+ return '';
13863
+ const index = indexOf.get(row);
13864
+ return index !== undefined
13865
+ ? texts[index][columns.indexOf(column)]
13866
+ : readText(column, row);
13867
+ }, [columnsById, indexOf, columns, texts]);
13868
+ const labelColumn = columns.find(column => column.isRowHeader) ?? columns[0];
13869
+ const getRowLabel = React.useCallback((row) => (labelColumn && getText(row, labelColumn.id)) || getRowId(row), [labelColumn, getText, getRowId]);
13870
+ const isSelected = React.useCallback((row) => selectable && selectedSet.has(getRowId(row)), [selectable, selectedSet, getRowId]);
13871
+ const setPage = React.useCallback((next) => {
13872
+ if (next !== page)
13873
+ setPageState(next);
13874
+ }, [page, setPageState]);
13875
+ // A new search or filter starts from the first page. It's measured against
13876
+ // the page as stored, not as shown: a page past the end shows the last,
13877
+ // and left stored it would come back once the rows grew again.
13878
+ const resetPage = React.useCallback(() => {
13879
+ if (latestPage() !== 1)
13880
+ setPageState(1);
13881
+ }, [latestPage, setPageState]);
13882
+ const setSearch = React.useCallback((next) => {
13883
+ setSearchState(next);
13884
+ resetPage();
13885
+ }, [setSearchState, resetPage]);
13886
+ const setFilters = React.useCallback((next) => {
13887
+ setFiltersState(next);
13888
+ resetPage();
13889
+ }, [setFiltersState, resetPage]);
13890
+ const setFilter = React.useCallback((columnId, value) => {
13891
+ const next = { ...latestFilters() };
13892
+ if (isActiveFilter(value))
13893
+ next[columnId] = value;
13894
+ else
13895
+ delete next[columnId];
13896
+ setFilters(next);
13897
+ }, [latestFilters, setFilters]);
13898
+ const toggleSort = React.useCallback((columnId) => {
13899
+ const column = columnsById.get(columnId);
13900
+ if (!column)
13901
+ return;
13902
+ const first = column.sortDescFirst
13903
+ ? 'descending'
13904
+ : 'ascending';
13905
+ const current = sort[0]?.id === columnId ? sort[0].direction : undefined;
13906
+ if (current === undefined) {
13907
+ setSort([{ id: columnId, direction: first }]);
13908
+ }
13909
+ else if (current === first) {
13910
+ setSort([
13911
+ {
13912
+ id: columnId,
13913
+ direction: first === 'ascending' ? 'descending' : 'ascending',
13914
+ },
13915
+ ]);
13916
+ }
13917
+ else {
13918
+ setSort([]);
13919
+ }
13920
+ }, [columnsById, sort, setSort]);
13921
+ const toggleRowSelected = React.useCallback((row, value) => {
13922
+ const id = getRowId(row);
13923
+ const current = latestSelected();
13924
+ const has = current.includes(id);
13925
+ const on = value ?? !has;
13926
+ if (on === has)
13927
+ return;
13928
+ setSelected(on ? [...current, id] : current.filter(s => s !== id));
13929
+ }, [getRowId, latestSelected, setSelected]);
13930
+ const pageIds = React.useMemo(() => pageIndexes.map(index => ids[index]), [pageIndexes, ids]);
13931
+ const pageSelectedCount = pageIds.filter(id => selectedSet.has(id)).length;
13932
+ const allPageSelected = pageIds.length > 0 && pageSelectedCount === pageIds.length;
13933
+ const togglePageSelected = React.useCallback((value) => {
13934
+ const current = latestSelected();
13935
+ const has = new Set(current);
13936
+ const on = value ?? !(pageIds.length > 0 && pageIds.every(id => has.has(id)));
13937
+ if (on) {
13938
+ const adding = pageIds.filter(id => !has.has(id));
13939
+ if (adding.length > 0)
13940
+ setSelected([...current, ...adding]);
13941
+ }
13942
+ else {
13943
+ const onPage = new Set(pageIds);
13944
+ const kept = current.filter(id => !onPage.has(id));
13945
+ if (kept.length !== current.length)
13946
+ setSelected(kept);
13947
+ }
13948
+ }, [latestSelected, pageIds, setSelected]);
13949
+ const count = React.useCallback((value) => (formatNumber ? formatNumber(value) : String(value)), [formatNumber]);
13950
+ const resolvedLabels = {
13951
+ selectAll: labels?.selectAll ||
13952
+ (pageCount > 1 ? 'Select all rows on this page' : 'Select all rows'),
13953
+ selectRow: labels?.selectRow ?? (label => `Select ${label}`),
13954
+ sortHint: labels?.sortHint || 'column headers with buttons are sortable',
13955
+ search: labels?.search || 'Search',
13956
+ results: labels?.results ??
13957
+ (total => total === 0
13958
+ ? 'No rows match'
13959
+ : `${count(total)} ${total === 1 ? 'row matches' : 'rows match'}`),
13960
+ pageRange: labels?.pageRange ??
13961
+ ((first, last, total) => `Showing ${count(first)} to ${count(last)} of ${count(total)}`),
13962
+ loading: labels?.loading || 'Loading',
13963
+ pagination: labels?.pagination || undefined,
13964
+ empty: labels?.empty || 'No rows to show.',
13965
+ noResults: labels?.noResults || 'No rows match.',
13966
+ };
13967
+ const sortButtonClass = prefixedClassNames(prefix, 'data-table-sort');
13968
+ return {
13969
+ columns,
13970
+ state: { sort, selected, page, pageSize, search, filters },
13971
+ selectionMode,
13972
+ manual,
13973
+ rowCount,
13974
+ pageCount,
13975
+ labels: resolvedLabels,
13976
+ getRows,
13977
+ getRowId,
13978
+ getValue,
13979
+ getText,
13980
+ getRowLabel,
13981
+ isSelected,
13982
+ setSort,
13983
+ toggleSort,
13984
+ setSelected,
13985
+ toggleRowSelected,
13986
+ togglePageSelected,
13987
+ setPage,
13988
+ setSearch,
13989
+ setFilter,
13990
+ setFilters,
13991
+ getHeaderCellProps: column => ({
13992
+ scope: 'col',
13993
+ 'aria-sort': sort[0]?.id === column.id ? sort[0].direction : undefined,
13994
+ isAligned: column.align,
13995
+ width: column.width,
13996
+ }),
13997
+ getSortButtonProps: column => ({
13998
+ type: 'button',
13999
+ className: sortButtonClass,
14000
+ onClick: () => toggleSort(column.id),
14001
+ }),
14002
+ getRowProps: row => ({ isSelected: isSelected(row) }),
14003
+ getCellProps: (_row, column) => ({
14004
+ scope: column.isRowHeader ? 'row' : undefined,
14005
+ isAligned: column.align,
14006
+ }),
14007
+ getSelectAllProps: () => ({
14008
+ 'aria-label': resolvedLabels.selectAll,
14009
+ checked: allPageSelected,
14010
+ indeterminate: pageSelectedCount > 0 && !allPageSelected,
14011
+ disabled: pageIds.length === 0,
14012
+ onChange: () => togglePageSelected(),
14013
+ }),
14014
+ getSelectRowProps: row => ({
14015
+ 'aria-label': resolvedLabels.selectRow(getRowLabel(row)),
14016
+ checked: selectedSet.has(getRowId(row)),
14017
+ onChange: () => toggleRowSelected(row),
14018
+ }),
14019
+ getPaginationProps: () => ({
14020
+ total: pageCount,
14021
+ current: page,
14022
+ onPageChange: setPage,
14023
+ }),
14024
+ getSearchProps: () => ({
14025
+ type: 'search',
14026
+ value: search,
14027
+ onChange: event => setSearch(event.target.value),
14028
+ }),
14029
+ };
14030
+ }
14031
+
14032
+ /** How long the status region waits after a change before it speaks, in ms. */
14033
+ const statusDelay = 500;
14034
+ const noColumns = [];
14035
+ const noRows = [];
14036
+ /** A text signature of the search and the filters, to tell when they change. */
14037
+ function filterSignature(search, filters) {
14038
+ try {
14039
+ return JSON.stringify([search, Object.entries(filters).sort()]);
14040
+ }
14041
+ catch {
14042
+ return search;
14043
+ }
14044
+ }
14045
+ /** Whether a node renders something. */
14046
+ const isShown = (node) => node !== undefined && node !== null && node !== false && node !== '';
14047
+ /**
14048
+ * The `DataTable` component is a table of records that people can sort,
14049
+ * search, page through and select rows from, rendered with bestax's own
14050
+ * `Table` parts, `Checkbox` and `Pagination`.
13521
14051
  *
13522
- * @function
13523
- * @param {DescriptionListProps} props - Props for the DescriptionList component.
13524
- * @param {React.Ref<HTMLDListElement>} ref - Forwarded ref to the `<dl>`.
13525
- * @returns {JSX.Element} The rendered list.
14052
+ * It's a native table, as in the WAI-ARIA Authoring Practices' sortable
14053
+ * table: a sortable header holds a button, the sorted one carries
14054
+ * `aria-sort`, and each row's checkbox is named for the row. It runs on
14055
+ * `useDataTable`, which custom markup can use too.
13526
14056
  *
13527
14057
  * @example
13528
- * <DescriptionList aria-labelledby="order-heading">
13529
- * <DescriptionList.Item label="Order" value="#1234" />
13530
- * <DescriptionList.Item label="Placed on" value="14 October 2026" />
13531
- * </DescriptionList>
14058
+ * <DataTable
14059
+ * caption="Users"
14060
+ * columns={[
14061
+ * { id: 'name', header: 'Name', sortable: true, isRowHeader: true },
14062
+ * { id: 'role', header: 'Role', sortable: true },
14063
+ * ]}
14064
+ * data={users}
14065
+ * selectionMode="multiple"
14066
+ * pageSize={10}
14067
+ * hasSearch
14068
+ * />
13532
14069
  */
13533
- const DescriptionListRoot = React.forwardRef(({ className, orientation, orientationMobile, orientationTablet, orientationDesktop, orientationWidescreen, orientationFullhd, hasDividers, textColor, bgColor, children, ...props }, ref) => {
13534
- const { bulmaHelperClasses, rest } = useBulmaClasses({
13535
- color: textColor,
13536
- backgroundColor: bgColor,
13537
- ...props,
13538
- });
13539
- // A class for each orientation given, which the stylesheet applies from
13540
- // that viewport's breakpoint, so the layout is right on the first paint
13541
- // and on the server. With none, the stylesheet's default is responsive.
13542
- const rootClass = usePrefixedClassNames('description-list', {
13543
- [`is-${orientation}`]: isOrientation(orientation),
13544
- [`is-${orientationMobile}-mobile`]: isOrientation(orientationMobile),
13545
- [`is-${orientationTablet}-tablet`]: isOrientation(orientationTablet),
13546
- [`is-${orientationDesktop}-desktop`]: isOrientation(orientationDesktop),
13547
- [`is-${orientationWidescreen}-widescreen`]: isOrientation(orientationWidescreen),
13548
- [`is-${orientationFullhd}-fullhd`]: isOrientation(orientationFullhd),
13549
- 'has-dividers': !!hasDividers,
13550
- });
13551
- return (jsxRuntime.jsx("dl", { ref: ref, className: classNames(rootClass, bulmaHelperClasses, className), ...rest, children: children }));
14070
+ const DataTable = React.forwardRef(function DataTableRender({ columns, data, table: given, getRowId, sort, defaultSort, onSortChange, selectionMode, selected, defaultSelected, onSelectedChange, pageSize, page, defaultPage, onPageChange, search, defaultSearch, onSearchChange, filters, defaultFilters, onFiltersChange, manual, rowCount, labels, caption, isBordered, isStriped, isNarrow, isHoverable, isFullwidth, isResponsive, hasSearch, toolbar, loading, empty, noResults, rowProps, className, 'aria-label': ariaLabel, 'aria-labelledby': ariaLabelledby, ...props }, ref) {
14071
+ const own = useDataTable({
14072
+ columns: columns ?? noColumns,
14073
+ data: data ?? noRows,
14074
+ getRowId,
14075
+ sort,
14076
+ defaultSort,
14077
+ onSortChange,
14078
+ selectionMode,
14079
+ selected,
14080
+ defaultSelected,
14081
+ onSelectedChange,
14082
+ pageSize,
14083
+ page,
14084
+ defaultPage,
14085
+ onPageChange,
14086
+ search,
14087
+ defaultSearch,
14088
+ onSearchChange,
14089
+ filters,
14090
+ defaultFilters,
14091
+ onFiltersChange,
14092
+ manual,
14093
+ rowCount,
14094
+ labels,
14095
+ });
14096
+ const table = given ?? own;
14097
+ const { state } = table;
14098
+ const { bulmaHelperClasses, rest } = useBulmaClasses(props);
14099
+ const rootClass = classNames(usePrefixedClassNames('data-table'), bulmaHelperClasses, className);
14100
+ const toolbarClass = usePrefixedClassNames('data-table-toolbar');
14101
+ const actionsClass = usePrefixedClassNames('data-table-actions');
14102
+ const emptyClass = usePrefixedClassNames('data-table-empty');
14103
+ const nameId = React.useId();
14104
+ const hasCaption = isShown(caption);
14105
+ const named = hasCaption || Boolean(ariaLabel || ariaLabelledby);
14106
+ React.useEffect(() => {
14107
+ if (!named) {
14108
+ warnOnce('DataTable:unnamed', '[bestax-bulma] <DataTable> has no accessible name. Pass a caption, ' +
14109
+ 'aria-label or aria-labelledby, so a screen reader can say which ' +
14110
+ 'table it is in.');
14111
+ }
14112
+ }, [named]);
14113
+ const mixed = Boolean(given) && Boolean(columns || data);
14114
+ const bare = !given && (!columns || !data);
14115
+ React.useEffect(() => {
14116
+ if (mixed) {
14117
+ warnOnce('DataTable:table-and-columns', '[bestax-bulma] <DataTable> was given a `table` and `columns` or ' +
14118
+ '`data` too. It renders the `table` and leaves the rest unread, so ' +
14119
+ 'pass those to useDataTable instead.');
14120
+ }
14121
+ if (bare) {
14122
+ warnOnce('DataTable:no-data', '[bestax-bulma] <DataTable> needs `columns` and `data`, or a `table` ' +
14123
+ 'from useDataTable.');
14124
+ }
14125
+ }, [mixed, bare]);
14126
+ // The status region speaks only after the search, a filter or the page
14127
+ // changes, so nothing is read out as the page loads. `kind` is what last
14128
+ // changed, kept beside the values it changed from.
14129
+ const signature = filterSignature(state.search, state.filters);
14130
+ const [last, setLast] = React.useState({
14131
+ signature,
14132
+ page: state.page,
14133
+ kind: null,
14134
+ });
14135
+ if (last.signature !== signature) {
14136
+ setLast({ signature, page: state.page, kind: 'results' });
14137
+ }
14138
+ else if (last.page !== state.page) {
14139
+ setLast({ signature, page: state.page, kind: 'page' });
14140
+ }
14141
+ const { labels: words, rowCount: total, pageCount } = table;
14142
+ const size = state.pageSize;
14143
+ let message = '';
14144
+ if (last.kind && loading) {
14145
+ message = words.loading;
14146
+ }
14147
+ else if (last.kind === 'results' || (last.kind === 'page' && !total)) {
14148
+ message = words.results(total);
14149
+ }
14150
+ else if (last.kind === 'page') {
14151
+ // Without a page size, as when it's just been taken away, there's one
14152
+ // page, holding every row.
14153
+ const per = size ?? total;
14154
+ message = words.pageRange((state.page - 1) * per + 1, Math.min(state.page * per, total), total);
14155
+ }
14156
+ // Written a moment after it changes, so typing in the search is read out
14157
+ // once it pauses rather than at every keystroke.
14158
+ const [said, setSaid] = React.useState('');
14159
+ React.useEffect(() => {
14160
+ const timer = setTimeout(() => setSaid(message), statusDelay);
14161
+ return () => clearTimeout(timer);
14162
+ }, [message]);
14163
+ const selectable = table.selectionMode === 'multiple';
14164
+ const sortable = table.columns.some(column => column.sortable);
14165
+ const columnCount = table.columns.length + (selectable ? 1 : 0);
14166
+ const pageRows = table.getRows('page');
14167
+ const selectAll = table.getSelectAllProps();
14168
+ const captionNode = hasCaption ? (jsxRuntime.jsxs("caption", { children: [jsxRuntime.jsx("span", { id: nameId, children: caption }), sortable && jsxRuntime.jsx("span", { style: visuallyHidden, children: `, ${words.sortHint}.` })] })) : sortable ? (jsxRuntime.jsx("caption", { children: jsxRuntime.jsx("span", { style: visuallyHidden, children: words.sortHint }) })) : null;
14169
+ const header = (jsxRuntime.jsx(Thead, { children: jsxRuntime.jsxs(Tr, { children: [selectable && (jsxRuntime.jsx(Th, { scope: "col", isNarrow: true, children: jsxRuntime.jsx(Checkbox, { ...selectAll,
14170
+ // While loading, the rows it would select aren't on screen.
14171
+ disabled: selectAll.disabled || Boolean(loading) }) })), table.columns.map(column => (jsxRuntime.jsx(Th, { ...table.getHeaderCellProps(column), children: column.sortable ? (jsxRuntime.jsx("button", { ...table.getSortButtonProps(column), children: column.header })) : (column.header) }, column.id)))] }) }));
14172
+ let body;
14173
+ if (loading) {
14174
+ body = Array.from({ length: size ?? 5 }, (_, index) => (jsxRuntime.jsx(Tr, { "aria-hidden": "true", children: Array.from({ length: columnCount }, (__, cell) => (jsxRuntime.jsx(Td, { children: jsxRuntime.jsx(Skeleton, { variant: "lines", lines: 1 }) }, cell))) }, index)));
14175
+ }
14176
+ else if (pageRows.length === 0) {
14177
+ // With `manual`, no rows under a search or a filter is no match: only
14178
+ // the server knows whether there's anything else.
14179
+ const nothing = table.getRows('all').length === 0 &&
14180
+ !(table.manual && isFiltering(state.search, state.filters));
14181
+ const content = nothing
14182
+ ? isShown(empty)
14183
+ ? empty
14184
+ : empty === undefined && (jsxRuntime.jsx(EmptyState, { size: "small", description: words.empty }))
14185
+ : isShown(noResults)
14186
+ ? noResults
14187
+ : noResults === undefined && (jsxRuntime.jsx(EmptyState, { size: "small", description: words.noResults }));
14188
+ body = (jsxRuntime.jsx(Tr, { children: jsxRuntime.jsx(Td, { colSpan: columnCount, className: emptyClass, children: content }) }));
14189
+ }
14190
+ else {
14191
+ body = pageRows.map(row => {
14192
+ const rowId = table.getRowId(row);
14193
+ return (jsxRuntime.jsxs(Tr, { ...rowProps?.(row), ...table.getRowProps(row), children: [selectable && (jsxRuntime.jsx(Td, { isNarrow: true, children: jsxRuntime.jsx(Checkbox, { ...table.getSelectRowProps(row) }) })), table.columns.map(column => {
14194
+ const text = table.getText(row, column.id);
14195
+ const content = column.cell
14196
+ ? column.cell(row, {
14197
+ value: table.getValue(row, column.id),
14198
+ text,
14199
+ rowId,
14200
+ })
14201
+ : text;
14202
+ const cellProps = table.getCellProps(row, column);
14203
+ return column.isRowHeader ? (jsxRuntime.jsx(Th, { ...cellProps, children: content }, column.id)) : (jsxRuntime.jsx(Td, { ...cellProps, children: content }, column.id));
14204
+ })] }, rowId));
14205
+ });
14206
+ }
14207
+ const tableElement = (jsxRuntime.jsxs(Table, { isBordered: isBordered, isStriped: isStriped, isNarrow: isNarrow, isHoverable: isHoverable, isFullwidth: isFullwidth, "aria-label": ariaLabel, "aria-labelledby": ariaLabelledby, "aria-busy": loading || undefined, children: [captionNode, header, jsxRuntime.jsx(Tbody, { children: body })] }));
14208
+ // The name the scroll area and the pager take from the table.
14209
+ const textName = ariaLabel || (typeof caption === 'string' ? caption : undefined);
14210
+ const scrollName = ariaLabel
14211
+ ? { 'aria-label': ariaLabel }
14212
+ : {
14213
+ 'aria-labelledby': ariaLabelledby || (hasCaption ? nameId : undefined),
14214
+ };
14215
+ // Pagination names itself "pagination" unless given another name.
14216
+ const pagerLabel = words.pagination || (textName ? `${textName} pages` : undefined);
14217
+ const pagerName = pagerLabel ? { 'aria-label': pagerLabel } : {};
14218
+ const toolbarContent = toolbar?.(table);
14219
+ const hasToolbar = hasSearch || isShown(toolbarContent);
14220
+ return (jsxRuntime.jsxs("div", { ref: ref, className: rootClass, ...rest, children: [hasToolbar && (jsxRuntime.jsxs("div", { className: toolbarClass, children: [hasSearch && (jsxRuntime.jsx(Input, { label: words.search, ...table.getSearchProps() })), isShown(toolbarContent) && (jsxRuntime.jsx("div", { className: actionsClass, children: toolbarContent }))] })), isResponsive ? (jsxRuntime.jsx(ScrollArea, { orientation: "horizontal", ...scrollName, children: tableElement })) : (tableElement), jsxRuntime.jsx("div", { role: "status", "aria-live": "polite", style: visuallyHidden, children: said }), pageCount > 1 && (jsxRuntime.jsx(Pagination, { ...table.getPaginationProps(), ...pagerName }))] }));
13552
14221
  });
13553
- const DescriptionList = withSubComponents(DescriptionListRoot, {
13554
- Item: DescriptionListItem,
13555
- Label: DescriptionListLabel,
13556
- Value: DescriptionListValue,
13557
- }, 'DescriptionList');
14222
+ DataTable.displayName = 'DataTable';
13558
14223
 
13559
14224
  /**
13560
- * The `Divider` component renders a styled horizontal rule (`<hr>`) element with Bulma helper class integration.
14225
+ * The `Block` component renders a simple container with Bulma's `.block` class, adding vertical margin between sections of content.
13561
14226
  *
13562
14227
  * @function
13563
- * @param {DividerProps} props - Props for the Divider component.
13564
- * @returns {JSX.Element} The rendered hr element.
14228
+ * @param {BlockProps} props - Props for the Block component.
14229
+ * @returns {JSX.Element} The rendered block element.
14230
+ * @see {@link https://bulma.io/documentation/elements/block/ | Bulma Block documentation}
13565
14231
  */
13566
- const Divider = ({ className, bgColor, ...props }) => {
14232
+ const Block = ({ className, textColor, color, bgColor, children, ...props }) => {
13567
14233
  /**
13568
14234
  * Generates Bulma helper classes and separates out remaining props.
13569
14235
  */
13570
14236
  const { bulmaHelperClasses, rest } = useBulmaClasses({
14237
+ color: textColor ?? color,
13571
14238
  backgroundColor: bgColor,
13572
14239
  ...props,
13573
14240
  });
13574
- const bulmaClasses = usePrefixedClassNames();
13575
- const dividerClasses = classNames(bulmaClasses, bulmaHelperClasses, className);
13576
- return jsxRuntime.jsx("hr", { className: dividerClasses || undefined, ...rest });
14241
+ const bulmaClasses = usePrefixedClassNames('block');
14242
+ const blockClasses = classNames(bulmaClasses, bulmaHelperClasses, className);
14243
+ return (jsxRuntime.jsx("div", { className: blockClasses, ...rest, children: children }));
13577
14244
  };
13578
14245
 
13579
14246
  /**
13580
- * The `Emphasis` component renders a styled `<em>` element with Bulma helper class integration.
14247
+ * The `Box` component renders a bordered, padded container with an optional shadow using Bulma's `.box` class.
13581
14248
  *
13582
14249
  * @function
13583
- * @param {EmphasisProps} props - Props for the Emphasis component.
13584
- * @returns {JSX.Element} The rendered em element.
14250
+ * @param {BoxProps} props - Props for the Box component.
14251
+ * @returns {JSX.Element} The rendered box element.
14252
+ * @see {@link https://bulma.io/documentation/elements/box/ | Bulma Box documentation}
13585
14253
  */
13586
- const Emphasis = ({ className, textColor, bgColor, children, ...props }) => {
13587
- const { bulmaHelperClasses, rest } = useBulmaClasses({
13588
- color: textColor,
14254
+ const Box = ({ className, textColor, color, bgColor, hasShadow = true, children, style, ...props }) => {
14255
+ /**
14256
+ * Generates Bulma helper classes and separates out remaining props.
14257
+ */
14258
+ const { bulmaHelperClasses, bulmaHelperStyles, rest } = useBulmaClasses({
14259
+ color: textColor ?? color,
13589
14260
  backgroundColor: bgColor,
13590
14261
  ...props,
13591
14262
  });
13592
- const bulmaClasses = usePrefixedClassNames();
13593
- const emphasisClasses = classNames(bulmaClasses, bulmaHelperClasses, className);
13594
- return (jsxRuntime.jsx("em", { className: emphasisClasses || undefined, ...rest, children: children }));
14263
+ const bulmaClasses = usePrefixedClassNames('box', {
14264
+ 'is-shadowless': !hasShadow,
14265
+ });
14266
+ const boxClasses = classNames(bulmaClasses, bulmaHelperClasses, className);
14267
+ return (jsxRuntime.jsx("div", { className: boxClasses, style: mergeBulmaStyles(bulmaHelperStyles, style), ...rest, children: children }));
13595
14268
  };
13596
14269
 
13597
14270
  /**
13598
- * FigureCaption component for rendering a styled figcaption element.
13599
- *
13600
- * A FigureCaption wraps the HTML `<figcaption>` element with Bulma helper class integration.
13601
- * Use it inside Figure components to provide captions for images or other media.
14271
+ * The `Code` component renders a styled `<code>` element with Bulma helper class integration.
13602
14272
  *
13603
14273
  * @function
13604
- * @param {FigureCaptionProps} props - Props for the FigureCaption component.
13605
- * @returns {JSX.Element} The rendered figcaption element.
14274
+ * @param {CodeProps} props - Props for the Code component.
14275
+ * @returns {JSX.Element} The rendered code element.
13606
14276
  */
13607
- const FigureCaption = ({ className, textColor, bgColor, children, ...props }) => {
14277
+ const Code = ({ className, textColor, bgColor, children, ...props }) => {
13608
14278
  const { bulmaHelperClasses, rest } = useBulmaClasses({
13609
14279
  color: textColor,
13610
14280
  backgroundColor: bgColor,
13611
14281
  ...props,
13612
14282
  });
13613
14283
  const bulmaClasses = usePrefixedClassNames();
13614
- const captionClasses = classNames(bulmaClasses, bulmaHelperClasses, className);
13615
- return (jsxRuntime.jsx("figcaption", { className: captionClasses || undefined, ...rest, children: children }));
14284
+ const codeClasses = classNames(bulmaClasses, bulmaHelperClasses, className);
14285
+ return (jsxRuntime.jsx("code", { className: codeClasses || undefined, ...rest, children: children }));
13616
14286
  };
14287
+
14288
+ const validColorSwatchSizes = ['small', 'medium', 'large'];
13617
14289
  /**
13618
- * The `Figure` component renders a styled `<figure>` element with Bulma helper class integration.
14290
+ * The `ColorSwatch` component shows a color as a small chip, named by its label or by the color itself.
14291
+ *
14292
+ * It's an image to assistive tech, so the color is always there in words.
14293
+ * In forced colors it keeps its color, edged in the system's text color.
13619
14294
  *
13620
14295
  * @function
13621
- * @param {FigureProps} props - Props for the Figure component.
13622
- * @returns {JSX.Element} The rendered figure element.
14296
+ * @param {ColorSwatchProps} props - Props for the ColorSwatch component.
14297
+ * @param {React.Ref<HTMLSpanElement>} ref - Forwarded ref to the `<span>`.
14298
+ * @returns {JSX.Element} The rendered swatch.
14299
+ *
14300
+ * @example
14301
+ * <ColorSwatch color="#1e6b99" label="Brand blue" />
13623
14302
  */
13624
- const FigureComponent = ({ className, textColor, bgColor, children, ...props }) => {
14303
+ const ColorSwatch = React.forwardRef(({ className, textColor, color, label, size, isRounded, children, style, ...props }, ref) => {
13625
14304
  const { bulmaHelperClasses, rest } = useBulmaClasses({
13626
14305
  color: textColor,
13627
- backgroundColor: bgColor,
13628
14306
  ...props,
13629
14307
  });
13630
- const bulmaClasses = usePrefixedClassNames();
13631
- const figureClasses = classNames(bulmaClasses, bulmaHelperClasses, className);
13632
- return (jsxRuntime.jsx("figure", { className: figureClasses || undefined, ...rest, children: children }));
13633
- };
13634
- /**
13635
- * Figure component with Caption subcomponent.
13636
- *
13637
- * @example
13638
- * <Figure>
13639
- * <img src="image.jpg" alt="Description" />
13640
- * <Figure.Caption>Image caption text</Figure.Caption>
13641
- * </Figure>
13642
- */
13643
- const Figure = withSubComponents(FigureComponent, {
13644
- Caption: FigureCaption,
13645
- }, 'Figure');
14308
+ const unnamed = /^\s*var\(/i.test(color) &&
14309
+ !label &&
14310
+ !rest['aria-label'] &&
14311
+ !rest['aria-labelledby'] &&
14312
+ !rest['aria-hidden'];
14313
+ React.useEffect(() => {
14314
+ if (unnamed) {
14315
+ warnOnce('ColorSwatch:unnamed-var', '[bestax-bulma] <ColorSwatch> shows a CSS variable with no label, ' +
14316
+ 'so a screen reader reads the var() as its name. Pass label, ' +
14317
+ 'such as "Primary".');
14318
+ }
14319
+ }, [unnamed]);
14320
+ const swatchClass = usePrefixedClassNames('color-swatch', {
14321
+ [`is-${size}`]: !!size && validColorSwatchSizes.includes(size),
14322
+ 'is-rounded': !!isRounded,
14323
+ });
14324
+ return (jsxRuntime.jsx("span", { ref: ref, className: classNames(swatchClass, bulmaHelperClasses, className), style: { ...style, ['--color-swatch-color']: color }, ...rest, role: "img", "aria-label": rest['aria-label'] || label || color, children: children !== undefined && children !== null && (jsxRuntime.jsx("span", { "aria-hidden": "true", children: children })) }));
14325
+ });
14326
+ ColorSwatch.displayName = 'ColorSwatch';
13646
14327
 
14328
+ // Valid size modifiers for the content class
14329
+ const validSizes = ['small', 'medium', 'large'];
13647
14330
  /**
13648
- * The `IconText` component provides a Bulma-styled horizontal arrangement of one or more `Icon` components and optional text.
14331
+ * The `Content` component applies Bulma’s typographic styles to its children, enhancing the appearance of HTML elements like paragraphs, headings, lists, and tables.
13649
14332
  *
13650
14333
  * @function
13651
- * @param {IconTextProps} props - Props for the IconText component.
13652
- * @returns {JSX.Element} The rendered icon text element.
13653
- * @see {@link https://bulma.io/documentation/elements/icon/#icon-text | Bulma IconText documentation}
14334
+ * @param {ContentProps} props - Props for the Content component.
14335
+ * @returns {JSX.Element} The rendered content block.
14336
+ * @see {@link https://bulma.io/documentation/elements/content/ | Bulma Content documentation}
13654
14337
  */
13655
- const IconTextComponent = ({ className, textColor, color, bgColor, iconProps, children, items, ...props }) => {
14338
+ const Content = ({ className, textColor, color, bgColor, size, children, ...props }) => {
13656
14339
  /**
13657
14340
  * Generates Bulma helper classes and separates out remaining props.
13658
14341
  */
@@ -13661,23 +14344,24 @@ const IconTextComponent = ({ className, textColor, color, bgColor, iconProps, ch
13661
14344
  backgroundColor: bgColor,
13662
14345
  ...props,
13663
14346
  });
13664
- const bulmaClasses = usePrefixedClassNames('icon-text');
13665
- const iconTextClasses = classNames(bulmaClasses, bulmaHelperClasses, className);
13666
- return (jsxRuntime.jsx("span", { className: iconTextClasses, ...rest, children: items ? (items.map((item, index) => (jsxRuntime.jsxs(React.Fragment, { children: [item.iconProps &&
13667
- (isIconProps(item.iconProps) ? (jsxRuntime.jsx(Icon, { ...item.iconProps })) : (jsxRuntime.jsx(Icon, { children: item.iconProps }))), item.text && jsxRuntime.jsx("span", { children: item.text })] }, index)))) : (jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [iconProps &&
13668
- (isIconProps(iconProps) ? (jsxRuntime.jsx(Icon, { ...iconProps })) : (jsxRuntime.jsx(Icon, { children: iconProps }))), children && jsxRuntime.jsx("span", { children: children })] })) }));
14347
+ const bulmaClasses = usePrefixedClassNames('content', {
14348
+ [`is-${size}`]: size && size !== 'normal' && validSizes.includes(size),
14349
+ });
14350
+ const contentClasses = classNames(bulmaClasses, bulmaHelperClasses, className);
14351
+ return (jsxRuntime.jsx("div", { className: contentClasses, ...rest, children: children }));
13669
14352
  };
13670
- const IconText = withSubComponents(IconTextComponent, { Icon }, 'IconText');
13671
14353
 
13672
14354
  /**
13673
- * The `Image` component wraps images, iframes, or custom content in a Bulma-styled container, supporting fixed sizes, aspect ratios, rounded corners, retina images, and all Bulma helper props for color and spacing.
14355
+ * The `Delete` component provides a Bulma-styled close/delete button for dismissing modals, notifications, tags, messages, and more.
14356
+ *
14357
+ * It renders `type="button"`, so a delete button inside a form does not submit it.
13674
14358
  *
13675
14359
  * @function
13676
- * @param {ImageProps} props - Props for the Image component.
13677
- * @returns {JSX.Element} The rendered image element.
13678
- * @see {@link https://bulma.io/documentation/elements/image/ | Bulma Image documentation}
14360
+ * @param {DeleteProps} props - Props for the Delete component.
14361
+ * @returns {JSX.Element} The rendered delete button.
14362
+ * @see {@link https://bulma.io/documentation/elements/delete/ | Bulma Delete documentation}
13679
14363
  */
13680
- const Image = ({ as, className, textColor, color, bgColor, size, isRounded, isRetina, src, alt, children, ...props }) => {
14364
+ const Delete = ({ className, textColor, color, bgColor, onClick, size, ariaLabel, disabled = false, ...props }) => {
13681
14365
  /**
13682
14366
  * Generates Bulma helper classes and separates out remaining props.
13683
14367
  */
@@ -13686,150 +14370,412 @@ const Image = ({ as, className, textColor, color, bgColor, size, isRounded, isRe
13686
14370
  backgroundColor: bgColor,
13687
14371
  ...props,
13688
14372
  });
13689
- // Bulma reads `.has-ratio` only on a child of the ratio container, so the
13690
- // container carries just its `is-<ratio>` class.
13691
- const bulmaClasses = usePrefixedClassNames('image', {
14373
+ const bulmaClasses = usePrefixedClassNames('delete', {
13692
14374
  [`is-${size}`]: size,
14375
+ 'is-disabled': disabled,
13693
14376
  });
13694
- const imageClasses = classNames(bulmaClasses, bulmaHelperClasses, className);
13695
- // Default tag logic: if "as" is provided, use it.
13696
- // If not, use <figure> for aspect ratios, <div> otherwise.
13697
- const Tag = as ?? (size?.includes('by') ? 'figure' : 'div');
13698
- const roundedClass = usePrefixedClassNames('is-rounded');
13699
- const content = children ? (children) : (jsxRuntime.jsx("img", { className: classNames({ [roundedClass]: isRounded }) || undefined, src: src, alt: alt, ...(isRetina && src ? { srcSet: `${src} 2x` } : {}) }));
13700
- return (jsxRuntime.jsx(Tag, { className: imageClasses, ...rest, children: content }));
14377
+ const classes = classNames(bulmaClasses, bulmaHelperClasses, className);
14378
+ return (jsxRuntime.jsx("button", { className: classes, onClick: onClick, disabled: disabled, ...rest, "aria-label": rest['aria-label'] || ariaLabel || 'Close', type: buttonType(rest.type) }));
13701
14379
  };
13702
14380
 
14381
+ const validDescriptionListOrientations = ['vertical', 'horizontal'];
14382
+ const isOrientation = (orientation) => !!orientation &&
14383
+ validDescriptionListOrientations.includes(orientation);
14384
+ /** Whether a prop was given something to render. */
14385
+ const isGiven = (node) => node !== undefined &&
14386
+ node !== null &&
14387
+ typeof node !== 'boolean' &&
14388
+ node !== '';
13703
14389
  /**
13704
- * Keys pressed together or one after another, in the outer `<kbd>` HTML gives a combination, with the separator between each pair as text.
13705
- *
13706
- * It doesn't wrap in the middle and isn't drawn as a key itself. It sets no
13707
- * font size, so the keys in it are the size of a key on its own.
14390
+ * A label, the name of a fact: a `<dt>`, in the weaker text color.
13708
14391
  *
13709
14392
  * @function
13710
- * @param {KbdGroupProps} props - Props for Kbd.Group.
13711
- * @param {React.Ref<HTMLElement>} ref - Forwarded ref to the outer `<kbd>`.
13712
- * @returns {JSX.Element} The rendered combination.
14393
+ * @param {DescriptionListLabelProps} props - Props for DescriptionList.Label.
14394
+ * @param {React.Ref<HTMLElement>} ref - Forwarded ref to the `<dt>`.
14395
+ * @returns {JSX.Element} The rendered label.
13713
14396
  *
13714
14397
  * @example
13715
- * <Kbd.Group>
13716
- * <Kbd>Ctrl</Kbd>
13717
- * <Kbd>Shift</Kbd>
13718
- * <Kbd>P</Kbd>
13719
- * </Kbd.Group>
14398
+ * <DescriptionList.Label>Placed on</DescriptionList.Label>
14399
+ */
14400
+ const DescriptionListLabel = React.forwardRef(({ className, textColor, bgColor, children, ...props }, ref) => {
14401
+ const { bulmaHelperClasses, rest } = useBulmaClasses({
14402
+ color: textColor,
14403
+ backgroundColor: bgColor,
14404
+ ...props,
14405
+ });
14406
+ const labelClass = usePrefixedClassNames('description-list-label');
14407
+ return (jsxRuntime.jsx("dt", { ref: ref, className: classNames(labelClass, bulmaHelperClasses, className), ...rest, children: children }));
14408
+ });
14409
+ DescriptionListLabel.displayName = 'DescriptionList.Label';
14410
+ /**
14411
+ * A value, what the label says about the thing: a `<dd>`. An item may have several.
14412
+ *
14413
+ * @function
14414
+ * @param {DescriptionListValueProps} props - Props for DescriptionList.Value.
14415
+ * @param {React.Ref<HTMLElement>} ref - Forwarded ref to the `<dd>`.
14416
+ * @returns {JSX.Element} The rendered value.
13720
14417
  *
13721
14418
  * @example
13722
- * <Kbd.Group separator="then">
13723
- * <Kbd>G</Kbd>
13724
- * <Kbd>I</Kbd>
13725
- * </Kbd.Group>
14419
+ * <DescriptionList.Value>14 October 2026</DescriptionList.Value>
13726
14420
  */
13727
- const KbdGroup = React.forwardRef(({ className, textColor, separator = '+', children, ...props }, ref) => {
14421
+ const DescriptionListValue = React.forwardRef(({ className, textColor, bgColor, children, ...props }, ref) => {
13728
14422
  const { bulmaHelperClasses, rest } = useBulmaClasses({
13729
14423
  color: textColor,
14424
+ backgroundColor: bgColor,
13730
14425
  ...props,
13731
14426
  });
13732
- const groupClass = usePrefixedClassNames('kbd-group');
13733
- const separatorClass = usePrefixedClassNames('kbd-separator');
13734
- // Null, undefined and booleans drop out, and Fragments are opened up, so
13735
- // an empty Fragment leaves no separator behind and the keys in one are
13736
- // separated like the rest. The spaces are part of the text, so a screen
13737
- // reader reads "Ctrl plus S" rather than one run-together word, whatever
13738
- // the CSS does.
13739
- const keys = flattenChildren(children);
13740
- return (jsxRuntime.jsx("kbd", { ref: ref, className: classNames(groupClass, bulmaHelperClasses, className), ...rest, children: keys.map((key, index) => (jsxRuntime.jsxs(React.Fragment, { children: [index > 0 && (jsxRuntime.jsx("span", { className: separatorClass, children: ` ${separator} ` })), key] }, React.isValidElement(key) && key.key != null ? key.key : index))) }));
14427
+ const valueClass = usePrefixedClassNames('description-list-value');
14428
+ return (jsxRuntime.jsx("dd", { ref: ref, className: classNames(valueClass, bulmaHelperClasses, className), ...rest, children: children }));
13741
14429
  });
13742
- KbdGroup.displayName = 'Kbd.Group';
14430
+ DescriptionListValue.displayName = 'DescriptionList.Value';
13743
14431
  /**
13744
- * The `Kbd` component renders a `<kbd>` drawn as a keycap, for a key the reader presses.
13745
- *
13746
- * It only shows the key: it doesn't listen for it. The key takes its colors
13747
- * from the text it sits in, and its size in `em`, so it fits a paragraph, a
13748
- * title or a filled button as it is, and `textSize` sizes it outright. For a
13749
- * combination or a sequence, put the keys in a `Kbd.Group`. What tells
13750
- * assistive tech a control has a shortcut is `aria-keyshortcuts` on that
13751
- * control, with `Kbd` as the hint people see next to it.
14432
+ * One fact: its label and its value, in a `<div>`, from the `label` and `value` props or from `Label` and `Value` children.
13752
14433
  *
13753
14434
  * @function
13754
- * @param {KbdProps} props - Props for the Kbd component.
13755
- * @param {React.Ref<HTMLElement>} ref - Forwarded ref to the `<kbd>`.
13756
- * @returns {JSX.Element} The rendered key.
14435
+ * @param {DescriptionListItemProps} props - Props for DescriptionList.Item.
14436
+ * @param {React.Ref<HTMLDivElement>} ref - Forwarded ref to the `<div>`.
14437
+ * @returns {JSX.Element} The rendered item.
13757
14438
  *
13758
14439
  * @example
13759
- * <Kbd>Esc</Kbd>
14440
+ * <DescriptionList.Item label="Order" value="#1234" />
13760
14441
  *
13761
14442
  * @example
13762
- * <Kbd label="Command">⌘</Kbd>
14443
+ * <DescriptionList.Item label="Tags">
14444
+ * <DescriptionList.Value>Gift</DescriptionList.Value>
14445
+ * <DescriptionList.Value>Priority</DescriptionList.Value>
14446
+ * </DescriptionList.Item>
13763
14447
  */
13764
- const KbdRoot = React.forwardRef(({ className, textColor, bgColor, label, children, ...props }, ref) => {
14448
+ const DescriptionListItem = React.forwardRef(({ className, label, value, textColor, bgColor, children, ...props }, ref) => {
13765
14449
  const { bulmaHelperClasses, rest } = useBulmaClasses({
13766
14450
  color: textColor,
13767
14451
  backgroundColor: bgColor,
13768
14452
  ...props,
13769
14453
  });
13770
- const kbdClass = usePrefixedClassNames('kbd');
13771
- // The name is text rather than an aria-label, which ARIA in HTML
13772
- // prohibits on kbd. Its styles are inline, so it stays hidden in a
13773
- // modular build that leaves out Bulma's helper classes.
13774
- return (jsxRuntime.jsx("kbd", { ref: ref, className: classNames(kbdClass, bulmaHelperClasses, className), ...rest, children: label ? (jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [jsxRuntime.jsx("span", { "aria-hidden": "true", children: children }), jsxRuntime.jsx("span", { style: visuallyHidden, children: label })] })) : (children) }));
14454
+ const itemClass = usePrefixedClassNames('description-list-item');
14455
+ return (jsxRuntime.jsxs("div", { ref: ref, className: classNames(itemClass, bulmaHelperClasses, className), ...rest, children: [isGiven(label) && jsxRuntime.jsx(DescriptionListLabel, { children: label }), children, isGiven(value) && jsxRuntime.jsx(DescriptionListValue, { children: value })] }));
13775
14456
  });
13776
- const Kbd = withSubComponents(KbdRoot, { Group: KbdGroup }, 'Kbd');
13777
-
14457
+ DescriptionListItem.displayName = 'DescriptionList.Item';
13778
14458
  /**
13779
- * The `Link` component renders a styled anchor (`<a>`) element with Bulma helper class integration.
14459
+ * The `DescriptionList` component lists facts about one thing, each a label and its value, such as an order's details or a profile.
14460
+ *
14461
+ * It's a `<dl>`, with a `<div>` for each item, a `<dt>` for each label and
14462
+ * a `<dd>` for each value, so assistive tech gets HTML's own list of terms
14463
+ * and definitions and no roles are added. Labels sit above their values on
14464
+ * a phone and beside them from Bulma's tablet breakpoint up, where every
14465
+ * label shares one column so the values line up. For data with two
14466
+ * dimensions, such as several records with the same fields, use a `Table`.
13780
14467
  *
13781
14468
  * @function
13782
- * @param {LinkProps} props - Props for the Link component.
13783
- * @param {React.Ref} ref - Forwarded ref to the element `as` renders.
13784
- * @returns {JSX.Element} The rendered anchor, or whatever `as` names.
13785
- * @see {@link https://bulma.io/documentation/elements/content/ | Bulma Content documentation}
14469
+ * @param {DescriptionListProps} props - Props for the DescriptionList component.
14470
+ * @param {React.Ref<HTMLDListElement>} ref - Forwarded ref to the `<dl>`.
14471
+ * @returns {JSX.Element} The rendered list.
14472
+ *
14473
+ * @example
14474
+ * <DescriptionList aria-labelledby="order-heading">
14475
+ * <DescriptionList.Item label="Order" value="#1234" />
14476
+ * <DescriptionList.Item label="Placed on" value="14 October 2026" />
14477
+ * </DescriptionList>
13786
14478
  */
13787
- const Link = React.forwardRef(function Link(linkProps, ref) {
13788
- const { className, textColor, bgColor, isActive, as: Component = 'a', children, ...props } = linkProps;
13789
- /**
13790
- * Generates Bulma helper classes and separates out remaining props.
13791
- */
14479
+ const DescriptionListRoot = React.forwardRef(({ className, orientation, orientationMobile, orientationTablet, orientationDesktop, orientationWidescreen, orientationFullhd, hasDividers, textColor, bgColor, children, ...props }, ref) => {
13792
14480
  const { bulmaHelperClasses, rest } = useBulmaClasses({
13793
14481
  color: textColor,
13794
14482
  backgroundColor: bgColor,
13795
14483
  ...props,
13796
14484
  });
13797
- const bulmaClasses = usePrefixedClassNames({
13798
- 'is-active': isActive,
14485
+ // A class for each orientation given, which the stylesheet applies from
14486
+ // that viewport's breakpoint, so the layout is right on the first paint
14487
+ // and on the server. With none, the stylesheet's default is responsive.
14488
+ const rootClass = usePrefixedClassNames('description-list', {
14489
+ [`is-${orientation}`]: isOrientation(orientation),
14490
+ [`is-${orientationMobile}-mobile`]: isOrientation(orientationMobile),
14491
+ [`is-${orientationTablet}-tablet`]: isOrientation(orientationTablet),
14492
+ [`is-${orientationDesktop}-desktop`]: isOrientation(orientationDesktop),
14493
+ [`is-${orientationWidescreen}-widescreen`]: isOrientation(orientationWidescreen),
14494
+ [`is-${orientationFullhd}-fullhd`]: isOrientation(orientationFullhd),
14495
+ 'has-dividers': !!hasDividers,
13799
14496
  });
13800
- const linkClasses = classNames(bulmaClasses, bulmaHelperClasses, className);
13801
- return (jsxRuntime.jsx(Component, { ref: ref, className: linkClasses || undefined, ...rest, ...(Component === 'button' ? { type: buttonType(rest.type) } : {}), children: children }));
14497
+ return (jsxRuntime.jsx("dl", { ref: ref, className: classNames(rootClass, bulmaHelperClasses, className), ...rest, children: children }));
13802
14498
  });
13803
- Link.displayName = 'Link';
14499
+ const DescriptionList = withSubComponents(DescriptionListRoot, {
14500
+ Item: DescriptionListItem,
14501
+ Label: DescriptionListLabel,
14502
+ Value: DescriptionListValue,
14503
+ }, 'DescriptionList');
13804
14504
 
13805
14505
  /**
13806
- * The `ListItem` component renders a styled list item (`<li>`) element with Bulma helper class integration.
14506
+ * The `Divider` component renders a styled horizontal rule (`<hr>`) element with Bulma helper class integration.
13807
14507
  *
13808
14508
  * @function
13809
- * @param {ListItemProps} props - Props for the ListItem component.
13810
- * @returns {JSX.Element} The rendered li element.
14509
+ * @param {DividerProps} props - Props for the Divider component.
14510
+ * @returns {JSX.Element} The rendered hr element.
13811
14511
  */
13812
- const ListItem = ({ className, textColor, bgColor, children, ...props }) => {
14512
+ const Divider = ({ className, bgColor, ...props }) => {
13813
14513
  /**
13814
14514
  * Generates Bulma helper classes and separates out remaining props.
13815
14515
  */
13816
14516
  const { bulmaHelperClasses, rest } = useBulmaClasses({
13817
- color: textColor,
13818
14517
  backgroundColor: bgColor,
13819
14518
  ...props,
13820
14519
  });
13821
14520
  const bulmaClasses = usePrefixedClassNames();
13822
- const itemClasses = classNames(bulmaClasses, bulmaHelperClasses, className);
13823
- return (jsxRuntime.jsx("li", { className: itemClasses || undefined, ...rest, children: children }));
14521
+ const dividerClasses = classNames(bulmaClasses, bulmaHelperClasses, className);
14522
+ return jsxRuntime.jsx("hr", { className: dividerClasses || undefined, ...rest });
13824
14523
  };
13825
14524
 
13826
14525
  /**
13827
- * The `Loader` component renders Bulma's `.loader`, a small spinning ring for inline loading states.
14526
+ * The `Emphasis` component renders a styled `<em>` element with Bulma helper class integration.
13828
14527
  *
13829
- * The ring is `1em` square, so `textSize` scales it. It is exposed as an
13830
- * indeterminate `progressbar` named by `ariaLabel`, and it stops spinning
13831
- * under `prefers-reduced-motion: reduce` while staying drawn. In forced
13832
- * colors its two open sides take the page's color, so it stays open.
14528
+ * @function
14529
+ * @param {EmphasisProps} props - Props for the Emphasis component.
14530
+ * @returns {JSX.Element} The rendered em element.
14531
+ */
14532
+ const Emphasis = ({ className, textColor, bgColor, children, ...props }) => {
14533
+ const { bulmaHelperClasses, rest } = useBulmaClasses({
14534
+ color: textColor,
14535
+ backgroundColor: bgColor,
14536
+ ...props,
14537
+ });
14538
+ const bulmaClasses = usePrefixedClassNames();
14539
+ const emphasisClasses = classNames(bulmaClasses, bulmaHelperClasses, className);
14540
+ return (jsxRuntime.jsx("em", { className: emphasisClasses || undefined, ...rest, children: children }));
14541
+ };
14542
+
14543
+ /**
14544
+ * FigureCaption component for rendering a styled figcaption element.
14545
+ *
14546
+ * A FigureCaption wraps the HTML `<figcaption>` element with Bulma helper class integration.
14547
+ * Use it inside Figure components to provide captions for images or other media.
14548
+ *
14549
+ * @function
14550
+ * @param {FigureCaptionProps} props - Props for the FigureCaption component.
14551
+ * @returns {JSX.Element} The rendered figcaption element.
14552
+ */
14553
+ const FigureCaption = ({ className, textColor, bgColor, children, ...props }) => {
14554
+ const { bulmaHelperClasses, rest } = useBulmaClasses({
14555
+ color: textColor,
14556
+ backgroundColor: bgColor,
14557
+ ...props,
14558
+ });
14559
+ const bulmaClasses = usePrefixedClassNames();
14560
+ const captionClasses = classNames(bulmaClasses, bulmaHelperClasses, className);
14561
+ return (jsxRuntime.jsx("figcaption", { className: captionClasses || undefined, ...rest, children: children }));
14562
+ };
14563
+ /**
14564
+ * The `Figure` component renders a styled `<figure>` element with Bulma helper class integration.
14565
+ *
14566
+ * @function
14567
+ * @param {FigureProps} props - Props for the Figure component.
14568
+ * @returns {JSX.Element} The rendered figure element.
14569
+ */
14570
+ const FigureComponent = ({ className, textColor, bgColor, children, ...props }) => {
14571
+ const { bulmaHelperClasses, rest } = useBulmaClasses({
14572
+ color: textColor,
14573
+ backgroundColor: bgColor,
14574
+ ...props,
14575
+ });
14576
+ const bulmaClasses = usePrefixedClassNames();
14577
+ const figureClasses = classNames(bulmaClasses, bulmaHelperClasses, className);
14578
+ return (jsxRuntime.jsx("figure", { className: figureClasses || undefined, ...rest, children: children }));
14579
+ };
14580
+ /**
14581
+ * Figure component with Caption subcomponent.
14582
+ *
14583
+ * @example
14584
+ * <Figure>
14585
+ * <img src="image.jpg" alt="Description" />
14586
+ * <Figure.Caption>Image caption text</Figure.Caption>
14587
+ * </Figure>
14588
+ */
14589
+ const Figure = withSubComponents(FigureComponent, {
14590
+ Caption: FigureCaption,
14591
+ }, 'Figure');
14592
+
14593
+ /**
14594
+ * The `IconText` component provides a Bulma-styled horizontal arrangement of one or more `Icon` components and optional text.
14595
+ *
14596
+ * @function
14597
+ * @param {IconTextProps} props - Props for the IconText component.
14598
+ * @returns {JSX.Element} The rendered icon text element.
14599
+ * @see {@link https://bulma.io/documentation/elements/icon/#icon-text | Bulma IconText documentation}
14600
+ */
14601
+ const IconTextComponent = ({ className, textColor, color, bgColor, iconProps, children, items, ...props }) => {
14602
+ /**
14603
+ * Generates Bulma helper classes and separates out remaining props.
14604
+ */
14605
+ const { bulmaHelperClasses, rest } = useBulmaClasses({
14606
+ color: textColor ?? color,
14607
+ backgroundColor: bgColor,
14608
+ ...props,
14609
+ });
14610
+ const bulmaClasses = usePrefixedClassNames('icon-text');
14611
+ const iconTextClasses = classNames(bulmaClasses, bulmaHelperClasses, className);
14612
+ return (jsxRuntime.jsx("span", { className: iconTextClasses, ...rest, children: items ? (items.map((item, index) => (jsxRuntime.jsxs(React.Fragment, { children: [item.iconProps &&
14613
+ (isIconProps(item.iconProps) ? (jsxRuntime.jsx(Icon, { ...item.iconProps })) : (jsxRuntime.jsx(Icon, { children: item.iconProps }))), item.text && jsxRuntime.jsx("span", { children: item.text })] }, index)))) : (jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [iconProps &&
14614
+ (isIconProps(iconProps) ? (jsxRuntime.jsx(Icon, { ...iconProps })) : (jsxRuntime.jsx(Icon, { children: iconProps }))), children && jsxRuntime.jsx("span", { children: children })] })) }));
14615
+ };
14616
+ const IconText = withSubComponents(IconTextComponent, { Icon }, 'IconText');
14617
+
14618
+ /**
14619
+ * The `Image` component wraps images, iframes, or custom content in a Bulma-styled container, supporting fixed sizes, aspect ratios, rounded corners, retina images, and all Bulma helper props for color and spacing.
14620
+ *
14621
+ * @function
14622
+ * @param {ImageProps} props - Props for the Image component.
14623
+ * @returns {JSX.Element} The rendered image element.
14624
+ * @see {@link https://bulma.io/documentation/elements/image/ | Bulma Image documentation}
14625
+ */
14626
+ const Image = ({ as, className, textColor, color, bgColor, size, isRounded, isRetina, src, alt, children, ...props }) => {
14627
+ /**
14628
+ * Generates Bulma helper classes and separates out remaining props.
14629
+ */
14630
+ const { bulmaHelperClasses, rest } = useBulmaClasses({
14631
+ color: textColor ?? color,
14632
+ backgroundColor: bgColor,
14633
+ ...props,
14634
+ });
14635
+ // Bulma reads `.has-ratio` only on a child of the ratio container, so the
14636
+ // container carries just its `is-<ratio>` class.
14637
+ const bulmaClasses = usePrefixedClassNames('image', {
14638
+ [`is-${size}`]: size,
14639
+ });
14640
+ const imageClasses = classNames(bulmaClasses, bulmaHelperClasses, className);
14641
+ // Default tag logic: if "as" is provided, use it.
14642
+ // If not, use <figure> for aspect ratios, <div> otherwise.
14643
+ const Tag = as ?? (size?.includes('by') ? 'figure' : 'div');
14644
+ const roundedClass = usePrefixedClassNames('is-rounded');
14645
+ const content = children ? (children) : (jsxRuntime.jsx("img", { className: classNames({ [roundedClass]: isRounded }) || undefined, src: src, alt: alt, ...(isRetina && src ? { srcSet: `${src} 2x` } : {}) }));
14646
+ return (jsxRuntime.jsx(Tag, { className: imageClasses, ...rest, children: content }));
14647
+ };
14648
+
14649
+ /**
14650
+ * Keys pressed together or one after another, in the outer `<kbd>` HTML gives a combination, with the separator between each pair as text.
14651
+ *
14652
+ * It doesn't wrap in the middle and isn't drawn as a key itself. It sets no
14653
+ * font size, so the keys in it are the size of a key on its own.
14654
+ *
14655
+ * @function
14656
+ * @param {KbdGroupProps} props - Props for Kbd.Group.
14657
+ * @param {React.Ref<HTMLElement>} ref - Forwarded ref to the outer `<kbd>`.
14658
+ * @returns {JSX.Element} The rendered combination.
14659
+ *
14660
+ * @example
14661
+ * <Kbd.Group>
14662
+ * <Kbd>Ctrl</Kbd>
14663
+ * <Kbd>Shift</Kbd>
14664
+ * <Kbd>P</Kbd>
14665
+ * </Kbd.Group>
14666
+ *
14667
+ * @example
14668
+ * <Kbd.Group separator="then">
14669
+ * <Kbd>G</Kbd>
14670
+ * <Kbd>I</Kbd>
14671
+ * </Kbd.Group>
14672
+ */
14673
+ const KbdGroup = React.forwardRef(({ className, textColor, separator = '+', children, ...props }, ref) => {
14674
+ const { bulmaHelperClasses, rest } = useBulmaClasses({
14675
+ color: textColor,
14676
+ ...props,
14677
+ });
14678
+ const groupClass = usePrefixedClassNames('kbd-group');
14679
+ const separatorClass = usePrefixedClassNames('kbd-separator');
14680
+ // Null, undefined and booleans drop out, and Fragments are opened up, so
14681
+ // an empty Fragment leaves no separator behind and the keys in one are
14682
+ // separated like the rest. The spaces are part of the text, so a screen
14683
+ // reader reads "Ctrl plus S" rather than one run-together word, whatever
14684
+ // the CSS does.
14685
+ const keys = flattenChildren(children);
14686
+ return (jsxRuntime.jsx("kbd", { ref: ref, className: classNames(groupClass, bulmaHelperClasses, className), ...rest, children: keys.map((key, index) => (jsxRuntime.jsxs(React.Fragment, { children: [index > 0 && (jsxRuntime.jsx("span", { className: separatorClass, children: ` ${separator} ` })), key] }, React.isValidElement(key) && key.key != null ? key.key : index))) }));
14687
+ });
14688
+ KbdGroup.displayName = 'Kbd.Group';
14689
+ /**
14690
+ * The `Kbd` component renders a `<kbd>` drawn as a keycap, for a key the reader presses.
14691
+ *
14692
+ * It only shows the key: it doesn't listen for it. The key takes its colors
14693
+ * from the text it sits in, and its size in `em`, so it fits a paragraph, a
14694
+ * title or a filled button as it is, and `textSize` sizes it outright. For a
14695
+ * combination or a sequence, put the keys in a `Kbd.Group`. What tells
14696
+ * assistive tech a control has a shortcut is `aria-keyshortcuts` on that
14697
+ * control, with `Kbd` as the hint people see next to it.
14698
+ *
14699
+ * @function
14700
+ * @param {KbdProps} props - Props for the Kbd component.
14701
+ * @param {React.Ref<HTMLElement>} ref - Forwarded ref to the `<kbd>`.
14702
+ * @returns {JSX.Element} The rendered key.
14703
+ *
14704
+ * @example
14705
+ * <Kbd>Esc</Kbd>
14706
+ *
14707
+ * @example
14708
+ * <Kbd label="Command">⌘</Kbd>
14709
+ */
14710
+ const KbdRoot = React.forwardRef(({ className, textColor, bgColor, label, children, ...props }, ref) => {
14711
+ const { bulmaHelperClasses, rest } = useBulmaClasses({
14712
+ color: textColor,
14713
+ backgroundColor: bgColor,
14714
+ ...props,
14715
+ });
14716
+ const kbdClass = usePrefixedClassNames('kbd');
14717
+ // The name is text rather than an aria-label, which ARIA in HTML
14718
+ // prohibits on kbd. Its styles are inline, so it stays hidden in a
14719
+ // modular build that leaves out Bulma's helper classes.
14720
+ return (jsxRuntime.jsx("kbd", { ref: ref, className: classNames(kbdClass, bulmaHelperClasses, className), ...rest, children: label ? (jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [jsxRuntime.jsx("span", { "aria-hidden": "true", children: children }), jsxRuntime.jsx("span", { style: visuallyHidden, children: label })] })) : (children) }));
14721
+ });
14722
+ const Kbd = withSubComponents(KbdRoot, { Group: KbdGroup }, 'Kbd');
14723
+
14724
+ /**
14725
+ * The `Link` component renders a styled anchor (`<a>`) element with Bulma helper class integration.
14726
+ *
14727
+ * @function
14728
+ * @param {LinkProps} props - Props for the Link component.
14729
+ * @param {React.Ref} ref - Forwarded ref to the element `as` renders.
14730
+ * @returns {JSX.Element} The rendered anchor, or whatever `as` names.
14731
+ * @see {@link https://bulma.io/documentation/elements/content/ | Bulma Content documentation}
14732
+ */
14733
+ const Link = React.forwardRef(function Link(linkProps, ref) {
14734
+ const { className, textColor, bgColor, isActive, as: Component = 'a', children, ...props } = linkProps;
14735
+ /**
14736
+ * Generates Bulma helper classes and separates out remaining props.
14737
+ */
14738
+ const { bulmaHelperClasses, rest } = useBulmaClasses({
14739
+ color: textColor,
14740
+ backgroundColor: bgColor,
14741
+ ...props,
14742
+ });
14743
+ const bulmaClasses = usePrefixedClassNames({
14744
+ 'is-active': isActive,
14745
+ });
14746
+ const linkClasses = classNames(bulmaClasses, bulmaHelperClasses, className);
14747
+ return (jsxRuntime.jsx(Component, { ref: ref, className: linkClasses || undefined, ...rest, ...(Component === 'button' ? { type: buttonType(rest.type) } : {}), children: children }));
14748
+ });
14749
+ Link.displayName = 'Link';
14750
+
14751
+ /**
14752
+ * The `ListItem` component renders a styled list item (`<li>`) element with Bulma helper class integration.
14753
+ *
14754
+ * @function
14755
+ * @param {ListItemProps} props - Props for the ListItem component.
14756
+ * @returns {JSX.Element} The rendered li element.
14757
+ */
14758
+ const ListItem = ({ className, textColor, bgColor, children, ...props }) => {
14759
+ /**
14760
+ * Generates Bulma helper classes and separates out remaining props.
14761
+ */
14762
+ const { bulmaHelperClasses, rest } = useBulmaClasses({
14763
+ color: textColor,
14764
+ backgroundColor: bgColor,
14765
+ ...props,
14766
+ });
14767
+ const bulmaClasses = usePrefixedClassNames();
14768
+ const itemClasses = classNames(bulmaClasses, bulmaHelperClasses, className);
14769
+ return (jsxRuntime.jsx("li", { className: itemClasses || undefined, ...rest, children: children }));
14770
+ };
14771
+
14772
+ /**
14773
+ * The `Loader` component renders Bulma's `.loader`, a small spinning ring for inline loading states.
14774
+ *
14775
+ * The ring is `1em` square, so `textSize` scales it. It is exposed as an
14776
+ * indeterminate `progressbar` named by `ariaLabel`, and it stops spinning
14777
+ * under `prefers-reduced-motion: reduce` while staying drawn. In forced
14778
+ * colors its two open sides take the page's color, so it stays open.
13833
14779
  *
13834
14780
  * @function
13835
14781
  * @param {LoaderProps} props - Props for the Loader component.
@@ -14235,7 +15181,7 @@ const validRingProgressColors = [
14235
15181
  'white',
14236
15182
  ];
14237
15183
  const validRingProgressSizes = ['small', 'medium', 'large'];
14238
- const clamp$2 = (value, low, high) => Math.min(Math.max(value, low), high);
15184
+ const clamp$4 = (value, low, high) => Math.min(Math.max(value, low), high);
14239
15185
  /**
14240
15186
  * The `RingProgress` component draws a value as a ring that fills as it grows, with anything you pass shown in the middle.
14241
15187
  *
@@ -14273,7 +15219,7 @@ const RingProgress = React.forwardRef(({ className, textColor, bgColor, value, m
14273
15219
  }
14274
15220
  }, [named, mixed]);
14275
15221
  const total = max > 0 ? max : 100;
14276
- const share = (part) => (clamp$2(part, 0, total) / total) * 100;
15222
+ const share = (part) => (clamp$4(part, 0, total) / total) * 100;
14277
15223
  // Each arc starts where the one before ended, and the last stops at the
14278
15224
  // top, so a total over max fills the ring and no more.
14279
15225
  const arcs = [];
@@ -14319,7 +15265,7 @@ const RingProgress = React.forwardRef(({ className, textColor, bgColor, value, m
14319
15265
  }
14320
15266
  : {
14321
15267
  role: isMeter ? 'meter' : 'progressbar',
14322
- 'aria-valuenow': clamp$2(value ?? 0, 0, total),
15268
+ 'aria-valuenow': clamp$4(value ?? 0, 0, total),
14323
15269
  'aria-valuemin': 0,
14324
15270
  'aria-valuemax': total,
14325
15271
  'aria-label': label || rest['aria-label'],
@@ -14334,31 +15280,7 @@ const RingProgress = React.forwardRef(({ className, textColor, bgColor, value, m
14334
15280
  RingProgress.displayName = 'RingProgress';
14335
15281
 
14336
15282
  /**
14337
- * The `Skeleton` component provides a Bulma-styled skeleton loader for React applications, useful for indicating that content is loading.
14338
- *
14339
- * @function
14340
- * @param {SkeletonProps} props - Props for the Skeleton component.
14341
- * @returns {JSX.Element} The rendered skeleton element.
14342
- *
14343
- * @example
14344
- * // Block skeleton
14345
- * <Skeleton />
14346
- *
14347
- * @example
14348
- * // Multi-line skeleton
14349
- * <Skeleton variant="lines" lines={5} />
14350
- */
14351
- const Skeleton = ({ className, variant = 'block', lines = 3, children, ...props }) => {
14352
- const linesClass = usePrefixedClassNames('skeleton-lines');
14353
- const blockClass = usePrefixedClassNames('skeleton-block');
14354
- if (variant === 'lines') {
14355
- return (jsxRuntime.jsx("div", { className: classNames(linesClass, className), ...props, children: Array.from({ length: lines }).map((_, i) => (jsxRuntime.jsx("div", {}, i))) }));
14356
- }
14357
- return (jsxRuntime.jsx("div", { className: classNames(blockClass, className), ...props, children: children }));
14358
- };
14359
-
14360
- /**
14361
- * The `Span` component renders a styled inline `<span>` element with Bulma helper class integration.
15283
+ * The `Span` component renders a styled inline `<span>` element with Bulma helper class integration.
14362
15284
  *
14363
15285
  * @function
14364
15286
  * @param {SpanProps} props - Props for the Span component.
@@ -14440,216 +15362,6 @@ const SubTitle = ({ className, size, as = 'h1', hasSkeleton, textColor, bgColor,
14440
15362
  return (jsxRuntime.jsx(Tag, { className: subTitleClasses, ...rest, children: children }));
14441
15363
  };
14442
15364
 
14443
- /**
14444
- * Thead component for rendering a styled Bulma table header.
14445
- *
14446
- * Supports Bulma helper classes for additional styling.
14447
- *
14448
- * @function
14449
- * @param {TheadProps} props - Props for the Thead component.
14450
- * @returns {JSX.Element} The rendered table header element.
14451
- * @see {@link https://bulma.io/documentation/elements/table/#table-head | Bulma Table documentation}
14452
- */
14453
- const Thead = ({ className, children, ...props }) => {
14454
- /**
14455
- * Generates Bulma helper classes and separates out remaining props.
14456
- */
14457
- const { bulmaHelperClasses, rest } = useBulmaClasses({ ...props });
14458
- const theadClasses = classNames(className, bulmaHelperClasses);
14459
- return (jsxRuntime.jsx("thead", { className: theadClasses || undefined, ...rest, children: children }));
14460
- };
14461
-
14462
- /**
14463
- * Tbody component for rendering a styled Bulma table body.
14464
- *
14465
- * Supports Bulma helper classes for additional styling.
14466
- *
14467
- * @function
14468
- * @param {TbodyProps} props - Props for the Tbody component.
14469
- * @returns {JSX.Element} The rendered table body element.
14470
- * @see {@link https://bulma.io/documentation/elements/table/#table-body | Bulma Table documentation}
14471
- */
14472
- const Tbody = ({ className, children, ...props }) => {
14473
- /**
14474
- * Generates Bulma helper classes and separates out remaining props.
14475
- */
14476
- const { bulmaHelperClasses, rest } = useBulmaClasses({ ...props });
14477
- const tbodyClasses = classNames(className, bulmaHelperClasses);
14478
- return (jsxRuntime.jsx("tbody", { className: tbodyClasses || undefined, ...rest, children: children }));
14479
- };
14480
-
14481
- /**
14482
- * Tfoot component for rendering a styled Bulma table footer.
14483
- *
14484
- * Supports Bulma helper classes for additional styling.
14485
- *
14486
- * @function
14487
- * @param {TfootProps} props - Props for the Tfoot component.
14488
- * @returns {JSX.Element} The rendered table footer element.
14489
- * @see {@link https://bulma.io/documentation/elements/table/#table-footer | Bulma Table documentation}
14490
- */
14491
- const Tfoot = ({ className, children, ...props }) => {
14492
- /**
14493
- * Generates Bulma helper classes and separates out remaining props.
14494
- */
14495
- const { bulmaHelperClasses, rest } = useBulmaClasses({ ...props });
14496
- const tfootClasses = classNames(className, bulmaHelperClasses);
14497
- return (jsxRuntime.jsx("tfoot", { className: tfootClasses || undefined, ...rest, children: children }));
14498
- };
14499
-
14500
- /**
14501
- * The `isAligned` values `Th` and `Td` accept. Each one writes
14502
- * `has-text-<value>` on the cell, and a value outside the tuple adds no class.
14503
- *
14504
- * One copy, imported by both cells, so a header and its body cells always
14505
- * take the same values.
14506
- *
14507
- * Its own module rather than `Th.tsx` or `Td.tsx` because `src/index.ts`
14508
- * wildcard-exports those files, and everything published there is public
14509
- * forever. `iconProps.ts` makes the same call.
14510
- */
14511
- const validTableAlignments = ['left', 'right', 'centered'];
14512
-
14513
- /**
14514
- * The values the table `color` prop accepts, as a readonly tuple.
14515
- *
14516
- * `TableColor` is typed from it, and `Tr`, `Th` and `Td` all take a
14517
- * `TableColor`, so the tuple and those props list the same values. Map over
14518
- * it to build a color picker, or check a value that arrives at runtime before
14519
- * passing it in: the components add no color class for a value outside the
14520
- * tuple.
14521
- *
14522
- * @example
14523
- * import { Tr, Td, validTableColors } from '@allxsmith/bestax-bulma';
14524
- *
14525
- * <Tr>
14526
- * {validTableColors.map(color => (
14527
- * <Td key={color} color={color}>
14528
- * {color}
14529
- * </Td>
14530
- * ))}
14531
- * </Tr>;
14532
- */
14533
- const validTableColors = [
14534
- 'primary',
14535
- 'link',
14536
- 'info',
14537
- 'success',
14538
- 'warning',
14539
- 'danger',
14540
- 'black',
14541
- 'dark',
14542
- 'light',
14543
- 'white',
14544
- ];
14545
- /**
14546
- * Td component for rendering a styled Bulma table cell.
14547
- *
14548
- * Supports alignment, Bulma color modifiers, and helper classes for additional styling.
14549
- *
14550
- * @function
14551
- * @param {TdProps} props - Props for the Td component.
14552
- * @returns {JSX.Element} The rendered table cell element.
14553
- * @see {@link https://bulma.io/documentation/elements/table/#table-body | Bulma Table documentation}
14554
- */
14555
- const Td = ({ className, isAligned, color, children, ...props }) => {
14556
- const bulmaClasses = usePrefixedClassNames('', {
14557
- [`has-text-${isAligned}`]: isAligned && validTableAlignments.includes(isAligned),
14558
- [`is-${color}`]: color && validTableColors.includes(color),
14559
- });
14560
- /**
14561
- * Generates Bulma helper classes and separates out remaining props.
14562
- */
14563
- const { bulmaHelperClasses, rest } = useBulmaClasses({ ...props });
14564
- const tdClasses = classNames(bulmaClasses, className, bulmaHelperClasses);
14565
- return (jsxRuntime.jsx("td", { className: tdClasses || undefined, ...rest, children: children }));
14566
- };
14567
-
14568
- /**
14569
- * Tr component for rendering a styled Bulma table row.
14570
- *
14571
- * Supports the is-selected modifier and color modifiers.
14572
- *
14573
- * @function
14574
- * @param {TrProps} props - Props for the Tr component.
14575
- * @returns {JSX.Element} The rendered table row element.
14576
- * @see {@link https://bulma.io/documentation/elements/table/#table-row | Bulma Table documentation}
14577
- */
14578
- const Tr = ({ className, isSelected, color, children, ...props }) => {
14579
- const bulmaClasses = usePrefixedClassNames('', {
14580
- 'is-selected': isSelected,
14581
- [`is-${color}`]: color && validTableColors.includes(color),
14582
- });
14583
- /**
14584
- * Generates Bulma helper classes and separates out remaining props.
14585
- */
14586
- const { bulmaHelperClasses, rest } = useBulmaClasses({ ...props });
14587
- const trClasses = classNames(bulmaClasses, className, bulmaHelperClasses);
14588
- return (jsxRuntime.jsx("tr", { className: trClasses || undefined, ...rest, children: children }));
14589
- };
14590
-
14591
- /**
14592
- * Th component for rendering a styled Bulma table header cell.
14593
- *
14594
- * Supports alignment, width, and color modifiers.
14595
- *
14596
- * @function
14597
- * @param {ThProps} props - Props for the Th component.
14598
- * @returns {JSX.Element} The rendered table header cell element.
14599
- * @see {@link https://bulma.io/documentation/elements/table/#table-head | Bulma Table documentation}
14600
- */
14601
- const Th = ({ className, isAligned, width, color, children, ...props }) => {
14602
- const bulmaClasses = usePrefixedClassNames('', {
14603
- [`has-text-${isAligned}`]: isAligned && validTableAlignments.includes(isAligned),
14604
- [`is-${color}`]: color && validTableColors.includes(color),
14605
- });
14606
- /**
14607
- * Generates Bulma helper classes and separates out remaining props.
14608
- */
14609
- const { bulmaHelperClasses, rest } = useBulmaClasses({ ...props });
14610
- const thClasses = classNames(bulmaClasses, className, bulmaHelperClasses);
14611
- return (jsxRuntime.jsx("th", { className: thClasses || undefined, style: width
14612
- ? { width: typeof width === 'number' ? `${width}px` : width }
14613
- : undefined, ...rest, children: children }));
14614
- };
14615
-
14616
- /**
14617
- * The `Table` component and its subcomponents provide a highly composable, Bulma-styled table system for React.
14618
- *
14619
- * @function
14620
- * @param {TableProps} props - Props for the Table component.
14621
- * @returns {JSX.Element} The rendered table element.
14622
- * @see {@link https://bulma.io/documentation/elements/table/ | Bulma Table documentation}
14623
- */
14624
- const TableComponent = ({ className, isBordered, isStriped, isNarrow, isHoverable, isFullwidth, isFullWidth, isResponsive, children, ...props }) => {
14625
- /**
14626
- * Generates Bulma helper classes and separates out remaining props.
14627
- */
14628
- const { bulmaHelperClasses, rest } = useBulmaClasses({ ...props });
14629
- const bulmaClasses = usePrefixedClassNames('table', {
14630
- 'is-bordered': isBordered,
14631
- 'is-striped': isStriped,
14632
- 'is-narrow': isNarrow,
14633
- 'is-hoverable': isHoverable,
14634
- 'is-fullwidth': isFullwidth ?? isFullWidth,
14635
- });
14636
- const containerClass = usePrefixedClassNames('table-container');
14637
- const tableClasses = classNames(bulmaClasses, bulmaHelperClasses, className);
14638
- const tableElement = (jsxRuntime.jsx("table", { className: tableClasses, ...rest, children: children }));
14639
- if (isResponsive) {
14640
- return jsxRuntime.jsx("div", { className: containerClass, children: tableElement });
14641
- }
14642
- return tableElement;
14643
- };
14644
- const Table = withSubComponents(TableComponent, {
14645
- Thead,
14646
- Tbody,
14647
- Tfoot,
14648
- Tr,
14649
- Th,
14650
- Td,
14651
- }, 'Table');
14652
-
14653
15365
  const validTagColors = [
14654
15366
  'primary',
14655
15367
  'link',
@@ -14964,20 +15676,528 @@ const CheckboxesComponent = ({ label, labelSize, labelProps, horizontal, message
14964
15676
  };
14965
15677
  const Checkboxes = withSubComponents(CheckboxesComponent, { Checkbox }, 'Checkboxes');
14966
15678
 
14967
- // What an unlabeled Field passes on: no label names anything inside.
14968
- const NO_LABEL = {};
15679
+ const noFiles = [];
15680
+ const noRefusals = [];
15681
+ const noAnnouncements = [];
15682
+ const describeAnnouncement = (announcement) => announcement.text;
15683
+ /**
15684
+ * The tokens of an `accept` attribute that HTML recognizes, lowercased: an
15685
+ * extension (`.pdf`), a wildcard (`image/*`) or a MIME type. Others are
15686
+ * ignored, as browsers ignore them.
15687
+ */
15688
+ const acceptTokens = (accept) => (accept ?? '')
15689
+ .split(',')
15690
+ .map(token => token.trim().toLowerCase())
15691
+ .filter(token => token.length > 1 && (token[0] === '.' || token.includes('/')));
15692
+ /** Whether `accept`'s tokens take a file with this name and type. */
15693
+ const accepts = (tokens, name, type) => {
15694
+ if (tokens.length === 0)
15695
+ return true;
15696
+ const lowerName = name.toLowerCase();
15697
+ const lowerType = type.toLowerCase();
15698
+ return tokens.some(token => token[0] === '.'
15699
+ ? lowerName.endsWith(token)
15700
+ : token.endsWith('/*')
15701
+ ? lowerType.startsWith(token.slice(0, -1))
15702
+ : lowerType === token);
15703
+ };
15704
+ /** Whether a drag or a clipboard carries files. */
15705
+ const carriesFiles = (data) => !!data && Array.from(data.types ?? []).includes('Files');
15706
+ /** The files a drop or a paste brings, with folders marked. */
15707
+ const incomingFrom = (data) => {
15708
+ if (!data)
15709
+ return [];
15710
+ const items = Array.from(data.items ?? []).filter(item => item.kind === 'file');
15711
+ if (items.length === 0) {
15712
+ return Array.from(data.files ?? [], file => ({ file, folder: false }));
15713
+ }
15714
+ const incoming = [];
15715
+ for (const item of items) {
15716
+ const file = item.getAsFile();
15717
+ if (!file)
15718
+ continue;
15719
+ const entry = item.webkitGetAsEntry?.();
15720
+ incoming.push({ file, folder: !!entry?.isDirectory });
15721
+ }
15722
+ return incoming;
15723
+ };
15724
+ /** The same file: one object, or one picked again with the same details. */
15725
+ const sameFile = (a, b) => a === b ||
15726
+ (a.name === b.name && a.size === b.size && a.lastModified === b.lastModified);
15727
+ /** Whether two lists hold the same files in the same order. */
15728
+ const sameFiles = (a, b) => !!a && a.length === b.length && b.every((file, i) => sameFile(a[i], file));
15729
+ /**
15730
+ * Writes files to a file input, through a `DataTransfer`, the one way script
15731
+ * can build a `FileList`. Where there's no `DataTransfer` the input is left as
15732
+ * it is.
15733
+ */
15734
+ const writeFiles = (input, files) => {
15735
+ if (typeof DataTransfer !== 'function')
15736
+ return;
15737
+ const transfer = new DataTransfer();
15738
+ for (const file of files)
15739
+ transfer.items.add(file);
15740
+ input.files = transfer.files;
15741
+ };
15742
+ /** The units a file size is written in, largest first. */
15743
+ const sizeUnits = [
15744
+ ['gigabyte', 1e9, 'GB'],
15745
+ ['megabyte', 1e6, 'MB'],
15746
+ ['kilobyte', 1e3, 'kB'],
15747
+ ];
14969
15748
  /**
14970
- * The `Fieldset` component groups a section of a form in a native
14971
- * `<fieldset>`, named by its `legend`, which a `disabled` switches off as a
14972
- * whole.
14973
- *
14974
- * It starts its own Field scope: inside it, nothing reads as inside a
14975
- * `Field` or `Control`, so the convenience inputs render their own `Field`,
14976
- * and a labeled `Field` around the Fieldset names nothing in it.
15749
+ * A file size for display, in decimal units ("1.2 MB"), in the locale of a
15750
+ * `LocaleProvider` around it, or as English writes it outside one.
15751
+ */
15752
+ const formatFileSize = (bytes, formatNumber) => {
15753
+ for (const [unit, factor, symbol] of sizeUnits) {
15754
+ if (bytes >= factor) {
15755
+ const value = bytes / factor;
15756
+ return formatNumber
15757
+ ? formatNumber(value, {
15758
+ style: 'unit',
15759
+ unit,
15760
+ unitDisplay: 'short',
15761
+ maximumFractionDigits: 1,
15762
+ })
15763
+ : `${Math.round(value * 10) / 10} ${symbol}`;
15764
+ }
15765
+ }
15766
+ return formatNumber
15767
+ ? formatNumber(bytes, { style: 'unit', unit: 'byte', unitDisplay: 'long' })
15768
+ : `${bytes} ${bytes === 1 ? 'byte' : 'bytes'}`;
15769
+ };
15770
+ /** A word for each kind of file `accept` names: "PDF", "image". */
15771
+ const typeNames = (tokens) => {
15772
+ const names = [];
15773
+ for (const token of tokens) {
15774
+ let name = token;
15775
+ if (token[0] === '.') {
15776
+ name = token.slice(1).toUpperCase();
15777
+ }
15778
+ else {
15779
+ const [type, subtype] = token.split('/');
15780
+ const core = subtype.split('+')[0];
15781
+ if (subtype === '*')
15782
+ name = type;
15783
+ else if (core === 'plain')
15784
+ name = 'text';
15785
+ else if (/^[a-z0-9]+$/.test(core))
15786
+ name = core.toUpperCase();
15787
+ }
15788
+ if (!names.includes(name))
15789
+ names.push(name);
15790
+ }
15791
+ return names;
15792
+ };
15793
+ /** "A", "A or B", "A, B, or C". */
15794
+ const orList = (items) => items.length <= 2
15795
+ ? items.join(' or ')
15796
+ : `${items.slice(0, -1).join(', ')}, or ${items[items.length - 1]}`;
15797
+ /**
15798
+ * The `Dropzone` component is an area to drop files on or click to pick
15799
+ * them, which checks each file and lists the chosen ones with a way to remove
15800
+ * each.
15801
+ *
15802
+ * It's Bulma's boxed, full-width file widget with the real
15803
+ * `<input type="file">` lying over it, so the input is the one tab stop, the
15804
+ * click target and the drop target, and Enter and Space open the picker. The
15805
+ * input holds the chosen files: a pick, drop or paste that passes the checks
15806
+ * is written to it, so a form submits the files under `name`. It doesn't
15807
+ * upload anything.
14977
15808
  *
14978
15809
  * @function
14979
- * @param {FieldsetProps} props - Props for the Fieldset component.
14980
- * @returns {JSX.Element} The rendered fieldset.
15810
+ * @param {DropzoneProps} props - Props for the Dropzone component.
15811
+ * @returns {JSX.Element} The rendered dropzone.
15812
+ *
15813
+ * @example
15814
+ * <Dropzone
15815
+ * label="CV"
15816
+ * name="cv"
15817
+ * accept=".pdf,application/pdf"
15818
+ * maxSize={5_000_000}
15819
+ * hint="PDF up to 5 MB"
15820
+ * />
15821
+ */
15822
+ const Dropzone = React.forwardRef(({
15823
+ // Field props
15824
+ label, labelSize, labelProps, horizontal, message, messageColor, fieldClassName, color, size, buttonLabel, icon, hint, maxSize, maxFiles, files: filesProp, onFilesChange, onReject, showFileList = true, allowPaste = true, preventDocumentDrop = true, removeLabel, rejectionMessage, addedMessage, removedMessage, className, accept, multiple, disabled: disabledProp, onChange, onClick, onDragEnter, onDragOver, onDragLeave, onDrop, onPaste, 'aria-describedby': ariaDescribedBy, ...props }, ref) => {
15825
+ const disabled = useFieldsetDisabled(disabledProp);
15826
+ const controlled = filesProp !== undefined;
15827
+ const [ownFiles, setOwnFiles] = React.useState(noFiles);
15828
+ const files = filesProp ?? ownFiles;
15829
+ // A render the controlled list needs after a change, so that one the app
15830
+ // didn't take is undone.
15831
+ const [, rerender] = React.useReducer((n) => n + 1, 0);
15832
+ const [refusals, setRefusals] = React.useState(noRefusals);
15833
+ const [drag, setDrag] = React.useState(null);
15834
+ const [announcements, setAnnouncements] = React.useState(noAnnouncements);
15835
+ const inputRef = React.useRef(null);
15836
+ const listRef = React.useRef(null);
15837
+ // Set while Dropzone dispatches the input's own events, so its change
15838
+ // handler hands them on rather than reading them as a pick.
15839
+ const dispatchingRef = React.useRef(false);
15840
+ // The row a removal emptied, whose neighbour takes focus.
15841
+ const removedRowRef = React.useRef(null);
15842
+ const announcedRef = React.useRef(0);
15843
+ // What the input held when it was last clicked (Enter and Space click it
15844
+ // too), which a pick adds to: by the time the pick's change arrives, the
15845
+ // browser has replaced the input's files with the picked ones.
15846
+ const pickBaseRef = React.useRef(null);
15847
+ const setInputRef = React.useCallback((node) => {
15848
+ inputRef.current = node;
15849
+ if (typeof ref === 'function')
15850
+ ref(node);
15851
+ else if (ref) {
15852
+ ref.current =
15853
+ node;
15854
+ }
15855
+ }, [ref]);
15856
+ const formatNumber = useNumberFormat();
15857
+ const tokens = acceptTokens(accept);
15858
+ const sizeText = (bytes) => formatFileSize(bytes, formatNumber);
15859
+ const countText = (count) => formatNumber ? formatNumber(count) : String(count);
15860
+ // The files a pick, drop or paste builds on, and a removal takes one from.
15861
+ // Controlled, that's `files`. Uncontrolled, it's the input itself rather
15862
+ // than the list: a script can rewrite the input through the ref, emptying
15863
+ // it with `value = ''` or setting its `files`, and no event reports that,
15864
+ // so the list catches up only at the next change. Where there's no
15865
+ // DataTransfer to write the input with, the list is all there is.
15866
+ const current = () => controlled || typeof DataTransfer !== 'function'
15867
+ ? files
15868
+ : Array.from(inputRef.current.files);
15869
+ const messageFor = (refusal) => {
15870
+ const { file, code } = refusal;
15871
+ if (rejectionMessage)
15872
+ return rejectionMessage({ file, code });
15873
+ if (code === 'file-too-large') {
15874
+ return `${file.name}: larger than ${sizeText(maxSize)}`;
15875
+ }
15876
+ if (code === 'too-many-files') {
15877
+ const most = multiple ? maxFiles : 1;
15878
+ return `${file.name}: only ${most === 1 ? 'one file' : `${countText(most)} files`}`;
15879
+ }
15880
+ return refusal.folder
15881
+ ? `${file.name}: folders can't be added`
15882
+ : `${file.name}: only ${orList(typeNames(tokens))} files`;
15883
+ };
15884
+ const announce = (texts) => {
15885
+ if (texts.length === 0)
15886
+ return;
15887
+ const batch = ++announcedRef.current;
15888
+ setAnnouncements(texts.map((text, i) => ({ id: `${batch}-${i}`, text })));
15889
+ };
15890
+ /**
15891
+ * Makes `next` the chosen files: writes them to the input, and when they
15892
+ * differ from the list, dispatches the input's `input` and `change` and
15893
+ * reports the new list.
15894
+ */
15895
+ const commit = (next) => {
15896
+ pickBaseRef.current = null;
15897
+ const input = inputRef.current;
15898
+ if (!sameFiles(input.files, next))
15899
+ writeFiles(input, next);
15900
+ if (sameFiles(files, next))
15901
+ return;
15902
+ if (controlled)
15903
+ rerender();
15904
+ else
15905
+ setOwnFiles(next);
15906
+ onFilesChange?.(next);
15907
+ dispatchingRef.current = true;
15908
+ try {
15909
+ input.dispatchEvent(new Event('input', { bubbles: true }));
15910
+ input.dispatchEvent(new Event('change', { bubbles: true }));
15911
+ }
15912
+ finally {
15913
+ dispatchingRef.current = false;
15914
+ }
15915
+ };
15916
+ /** Checks the files a pick, drop or paste brings, and takes those that pass. */
15917
+ const take = (incoming, base = current()) => {
15918
+ const kept = multiple ? base : noFiles;
15919
+ const room = multiple ? (maxFiles ?? Infinity) - kept.length : 1;
15920
+ const added = [];
15921
+ const refused = [];
15922
+ for (const { file, folder } of incoming) {
15923
+ const code = folder || !accepts(tokens, file.name, file.type)
15924
+ ? 'file-invalid-type'
15925
+ : maxSize !== undefined && file.size > maxSize
15926
+ ? 'file-too-large'
15927
+ : null;
15928
+ if (code) {
15929
+ refused.push({ file, code, folder });
15930
+ }
15931
+ else if (multiple &&
15932
+ [...kept, ...added].some(f => sameFile(f, file))) ;
15933
+ else if (added.length >= room) {
15934
+ refused.push({ file, code: 'too-many-files', folder });
15935
+ }
15936
+ else {
15937
+ added.push(file);
15938
+ }
15939
+ }
15940
+ setRefusals(refused.length > 0 ? refused : noRefusals);
15941
+ if (refused.length > 0) {
15942
+ onReject?.(refused.map(({ file, code }) => ({ file, code })));
15943
+ }
15944
+ commit(added.length > 0 ? [...kept, ...added] : base);
15945
+ const addedText = added.length === 0
15946
+ ? []
15947
+ : [
15948
+ addedMessage
15949
+ ? addedMessage(added)
15950
+ : added.length === 1
15951
+ ? `${added[0].name} added`
15952
+ : `${countText(added.length)} files added`,
15953
+ ];
15954
+ announce([...addedText, ...refused.map(messageFor)]);
15955
+ };
15956
+ const remove = (index) => {
15957
+ const file = files[index];
15958
+ removedRowRef.current = index;
15959
+ commit(current().filter(held => !sameFile(held, file)));
15960
+ announce([
15961
+ removedMessage ? removedMessage(file) : `${file.name} removed`,
15962
+ ]);
15963
+ };
15964
+ const handleChange = (event) => {
15965
+ if (dispatchingRef.current) {
15966
+ onChange?.(event);
15967
+ return;
15968
+ }
15969
+ // A pick: the browser has put the picked files in the input. They're
15970
+ // added to what it held when it was clicked, or, for a picker opened
15971
+ // with showPicker(), which clicks nothing, to the list. With none (a
15972
+ // cancelled pick, in a browser that clears it), that stays.
15973
+ const base = pickBaseRef.current ?? files;
15974
+ const picked = Array.from(event.target.files ?? []);
15975
+ if (picked.length === 0)
15976
+ commit(base);
15977
+ else
15978
+ take(picked.map(file => ({ file, folder: false })), base);
15979
+ };
15980
+ const handleClick = (event) => {
15981
+ onClick?.(event);
15982
+ pickBaseRef.current = current();
15983
+ };
15984
+ // While files are dragged over the input it's cancelled, which makes the
15985
+ // input the drop target and keeps the browser's own drop from running.
15986
+ // Disabled, it refuses the drop, so the browser doesn't open the file.
15987
+ const handleDragOver = (event) => {
15988
+ (event.type === 'dragenter' ? onDragEnter : onDragOver)?.(event);
15989
+ if (!carriesFiles(event.dataTransfer))
15990
+ return;
15991
+ event.preventDefault();
15992
+ if (disabled) {
15993
+ event.dataTransfer.dropEffect = 'none';
15994
+ return;
15995
+ }
15996
+ event.dataTransfer.dropEffect = 'copy';
15997
+ // Only the dragged files' types can be read, so a file is known to be
15998
+ // refused only when `accept` names no extension and its type is known.
15999
+ const room = multiple ? (maxFiles ?? Infinity) - current().length : 1;
16000
+ const types = Array.from(event.dataTransfer.items ?? [])
16001
+ .filter(item => item.kind === 'file')
16002
+ .map(item => item.type);
16003
+ const refused = room <= 0 ||
16004
+ (types.length > 0 &&
16005
+ tokens.length > 0 &&
16006
+ !tokens.some(token => token[0] === '.') &&
16007
+ types.every(type => type !== '' && !accepts(tokens, '', type)));
16008
+ setDrag(refused ? 'reject' : 'over');
16009
+ };
16010
+ const handleDragLeave = (event) => {
16011
+ onDragLeave?.(event);
16012
+ setDrag(null);
16013
+ };
16014
+ const handleDrop = (event) => {
16015
+ onDrop?.(event);
16016
+ setDrag(null);
16017
+ if (!carriesFiles(event.dataTransfer))
16018
+ return;
16019
+ event.preventDefault();
16020
+ if (!disabled)
16021
+ take(incomingFrom(event.dataTransfer));
16022
+ };
16023
+ const handlePaste = (event) => {
16024
+ onPaste?.(event);
16025
+ if (!allowPaste || disabled)
16026
+ return;
16027
+ const incoming = incomingFrom(event.clipboardData);
16028
+ if (incoming.length === 0)
16029
+ return;
16030
+ event.preventDefault();
16031
+ take(incoming);
16032
+ };
16033
+ // A controlled list: the input follows `files`, and a change the app
16034
+ // didn't take is undone.
16035
+ React.useEffect(() => {
16036
+ const input = inputRef.current;
16037
+ if (controlled && input && !sameFiles(input.files, files)) {
16038
+ writeFiles(input, files);
16039
+ }
16040
+ });
16041
+ // After a removal, focus goes to the remove button that took the removed
16042
+ // one's place, else the one before, else the input.
16043
+ React.useEffect(() => {
16044
+ const row = removedRowRef.current;
16045
+ if (row === null)
16046
+ return;
16047
+ removedRowRef.current = null;
16048
+ const buttons = listRef.current?.querySelectorAll('button') ?? [];
16049
+ const next = buttons[Math.min(row, buttons.length - 1)];
16050
+ (next ?? inputRef.current)?.focus();
16051
+ });
16052
+ // A reset of the input's form empties the input, so the list and the
16053
+ // refusals go too. The reset event fires before the form resets, and a
16054
+ // listener after this one can still cancel it, so the list is cleared a
16055
+ // task later, and only if the reset went ahead.
16056
+ const latestRef = React.useRef({ files, controlled, onFilesChange });
16057
+ React.useEffect(() => {
16058
+ latestRef.current = { files, controlled, onFilesChange };
16059
+ });
16060
+ const formAttribute = props.form;
16061
+ React.useEffect(() => {
16062
+ const form = inputRef.current?.form;
16063
+ if (!form)
16064
+ return undefined;
16065
+ let timer;
16066
+ const onReset = (event) => {
16067
+ timer = setTimeout(() => {
16068
+ if (event.defaultPrevented)
16069
+ return;
16070
+ const latest = latestRef.current;
16071
+ pickBaseRef.current = null;
16072
+ setRefusals(noRefusals);
16073
+ if (latest.files.length === 0)
16074
+ return;
16075
+ if (latest.controlled)
16076
+ rerender();
16077
+ else
16078
+ setOwnFiles(noFiles);
16079
+ latest.onFilesChange?.(noFiles);
16080
+ });
16081
+ };
16082
+ form.addEventListener('reset', onReset);
16083
+ return () => {
16084
+ form.removeEventListener('reset', onReset);
16085
+ clearTimeout(timer);
16086
+ };
16087
+ }, [formAttribute]);
16088
+ // A file dropped just outside the zone would open in the tab. The guard
16089
+ // cancels a file drag anywhere nothing else takes it, leaving alone
16090
+ // enabled file inputs and editable fields, which take drops of their own,
16091
+ // and drops something else already handled. It reads the event's real
16092
+ // target, so a file input inside a shadow root is left alone too.
16093
+ React.useEffect(() => {
16094
+ if (!preventDocumentDrop)
16095
+ return undefined;
16096
+ const guard = (event) => {
16097
+ if (event.defaultPrevented || !carriesFiles(event.dataTransfer)) {
16098
+ return;
16099
+ }
16100
+ const target = event.composedPath()[0];
16101
+ if (target instanceof Element &&
16102
+ target.closest('input[type="file"]:not(:disabled), textarea, [contenteditable]:not([contenteditable="false"])')) {
16103
+ return;
16104
+ }
16105
+ event.preventDefault();
16106
+ if (event.type === 'dragover')
16107
+ event.dataTransfer.dropEffect = 'none';
16108
+ };
16109
+ document.addEventListener('dragover', guard);
16110
+ document.addEventListener('drop', guard);
16111
+ return () => {
16112
+ document.removeEventListener('dragover', guard);
16113
+ document.removeEventListener('drop', guard);
16114
+ };
16115
+ }, [preventDocumentDrop]);
16116
+ const warnsMaxFiles = maxFiles !== undefined && !multiple;
16117
+ React.useEffect(() => {
16118
+ if (warnsMaxFiles) {
16119
+ warnOnce('Dropzone:maxFiles-without-multiple', '[bestax-bulma] <Dropzone maxFiles> takes effect only with ' +
16120
+ 'multiple. Without it, each new file replaces the one chosen.');
16121
+ }
16122
+ }, [warnsMaxFiles]);
16123
+ const insideField = useInsideField();
16124
+ const insideControl = useInsideControl();
16125
+ const ownField = rendersOwnField('Dropzone', {
16126
+ insideField,
16127
+ insideControl,
16128
+ label,
16129
+ message,
16130
+ horizontal,
16131
+ fieldClassName,
16132
+ });
16133
+ const { controlId, fieldLabelProps } = useAutoLabelId({
16134
+ label,
16135
+ id: props.id,
16136
+ labelProps,
16137
+ rendersLabel: ownField,
16138
+ });
16139
+ const { classPrefix } = useConfig();
16140
+ const pcn = (...args) => prefixedClassNames(classPrefix, ...args);
16141
+ // The input takes `controlId`, so `id` stays out of the spread, where an
16142
+ // undefined `id` key would wipe the id the label points at.
16143
+ const { id: _id, ...inputProps } = props;
16144
+ const { bulmaHelperClasses, rest } = useBulmaClasses(inputProps);
16145
+ const baseId = React.useId();
16146
+ const hintId = `${baseId}-hint`;
16147
+ const refusalsId = `${baseId}-refusals`;
16148
+ const messageId = `${baseId}-message`;
16149
+ const describedBy = [
16150
+ ariaDescribedBy,
16151
+ hint ? hintId : undefined,
16152
+ refusals.length > 0 ? refusalsId : undefined,
16153
+ message ? messageId : undefined,
16154
+ ]
16155
+ .filter(Boolean)
16156
+ .join(' ') || undefined;
16157
+ const rootClass = usePrefixedClassNames('dropzone', {
16158
+ 'is-dragover': drag === 'over',
16159
+ 'is-drag-reject': drag === 'reject',
16160
+ 'is-disabled': disabled,
16161
+ });
16162
+ const zoneClass = usePrefixedClassNames('file', 'is-boxed', 'is-fullwidth', {
16163
+ [`is-${color}`]: !!color,
16164
+ [`is-${size}`]: !!size,
16165
+ });
16166
+ const helpClass = usePrefixedClassNames('help', {
16167
+ [`is-${messageColor}`]: !!messageColor,
16168
+ });
16169
+ // Each row's key, from the file's details, which the list keeps unique.
16170
+ const seen = new Map();
16171
+ const keyOf = (file) => {
16172
+ const key = `${file.name}:${file.size}:${file.lastModified}`;
16173
+ const count = seen.get(key) ?? 0;
16174
+ seen.set(key, count + 1);
16175
+ return count === 0 ? key : `${key}:${count}`;
16176
+ };
16177
+ const dropzoneElement = (jsxRuntime.jsxs("div", { className: classNames(rootClass, bulmaHelperClasses, className), children: [jsxRuntime.jsx("div", { className: zoneClass, children: jsxRuntime.jsxs("label", { className: pcn('file-label'), children: [jsxRuntime.jsx("input", { ...rest, ref: setInputRef, className: pcn('file-input'), type: "file", id: controlId, accept: accept, multiple: multiple, disabled: disabled, "aria-describedby": describedBy, onChange: handleChange, onClick: handleClick, onDragEnter: handleDragOver, onDragOver: handleDragOver, onDragLeave: handleDragLeave, onDrop: handleDrop, onPaste: handlePaste }), jsxRuntime.jsxs("span", { className: pcn('file-cta'), children: [icon && jsxRuntime.jsx("span", { className: pcn('file-icon'), children: icon }), jsxRuntime.jsx("span", { className: pcn('file-label'), children: buttonLabel ||
16178
+ (multiple ? 'Choose or drop files' : 'Choose or drop a file') })] })] }) }), hint && (jsxRuntime.jsx("p", { id: hintId, className: pcn('help', 'dropzone-hint'), children: hint })), showFileList && files.length > 0 && (jsxRuntime.jsx("ul", { ref: listRef, className: pcn('dropzone-list'), children: files.map((file, index) => (jsxRuntime.jsxs("li", { className: pcn('dropzone-item'), children: [jsxRuntime.jsx("span", { className: pcn('dropzone-name'), children: file.name }), jsxRuntime.jsx("span", { className: pcn('dropzone-size'), children: sizeText(file.size) }), !disabled && (jsxRuntime.jsx(Delete, { size: "small", ariaLabel: removeLabel ? removeLabel(file) : `Remove ${file.name}`, onClick: () => remove(index) }))] }, keyOf(file)))) })), refusals.length > 0 && (jsxRuntime.jsx("div", { id: refusalsId, className: pcn('dropzone-rejections'), children: refusals.map((refusal, index) => (jsxRuntime.jsx("p", { className: pcn('help', 'is-danger'), children: messageFor(refusal) }, index))) })), jsxRuntime.jsx(StatusRegion, { items: announcements, describe: describeAnnouncement })] }));
16179
+ const messageEl = message ? (jsxRuntime.jsx("p", { id: messageId, className: helpClass, children: message })) : null;
16180
+ if (ownField) {
16181
+ return (jsxRuntime.jsxs(Field, { label: label, labelSize: labelSize, labelProps: fieldLabelProps, horizontal: horizontal, className: fieldClassName, children: [dropzoneElement, messageEl] }));
16182
+ }
16183
+ return (jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [dropzoneElement, messageEl] }));
16184
+ });
16185
+ Dropzone.displayName = 'Dropzone';
16186
+
16187
+ // What an unlabeled Field passes on: no label names anything inside.
16188
+ const NO_LABEL = {};
16189
+ /**
16190
+ * The `Fieldset` component groups a section of a form in a native
16191
+ * `<fieldset>`, named by its `legend`, which a `disabled` switches off as a
16192
+ * whole.
16193
+ *
16194
+ * It starts its own Field scope: inside it, nothing reads as inside a
16195
+ * `Field` or `Control`, so the convenience inputs render their own `Field`,
16196
+ * and a labeled `Field` around the Fieldset names nothing in it.
16197
+ *
16198
+ * @function
16199
+ * @param {FieldsetProps} props - Props for the Fieldset component.
16200
+ * @returns {JSX.Element} The rendered fieldset.
14981
16201
  *
14982
16202
  * @example
14983
16203
  * <Fieldset legend="Shipping address" message="We only ship within the EU.">
@@ -16993,11 +18213,11 @@ const validComboboxSizes = ['small', 'medium', 'large'];
16993
18213
  // typing.
16994
18214
  const typedNoMatches = [{ id: 'empty' }];
16995
18215
  const nothingToAnnounce = [];
16996
- const noop = () => { };
16997
- const registerNothing = () => noop;
18216
+ const noop$1 = () => { };
18217
+ const registerNothing = () => noop$1;
16998
18218
  // What a part rendered outside a Combobox reads: closed, with nothing to
16999
18219
  // report to, so it renders its markup and does nothing.
17000
- const standalone = {
18220
+ const standalone$1 = {
17001
18221
  engine: {
17002
18222
  open: false,
17003
18223
  expanded: false,
@@ -17010,22 +18230,22 @@ const standalone = {
17010
18230
  listboxRef: { current: null },
17011
18231
  registerPopup: registerNothing,
17012
18232
  registerItem: registerNothing,
17013
- onKeyDown: noop,
17014
- onChange: noop,
17015
- onClick: noop,
17016
- onBlur: noop,
17017
- hover: noop,
17018
- pick: noop,
17019
- keepFocus: noop,
17020
- markInside: noop,
18233
+ onKeyDown: noop$1,
18234
+ onChange: noop$1,
18235
+ onClick: noop$1,
18236
+ onBlur: noop$1,
18237
+ hover: noop$1,
18238
+ pick: noop$1,
18239
+ keepFocus: noop$1,
18240
+ markInside: noop$1,
17021
18241
  },
17022
18242
  inputId: undefined,
17023
- reportInputId: noop,
18243
+ reportInputId: noop$1,
17024
18244
  labelId: undefined,
17025
18245
  ownLabelId: undefined,
17026
- reportLoading: noop,
18246
+ reportLoading: noop$1,
17027
18247
  reportEmpty: registerNothing,
17028
- onFieldChange: noop,
18248
+ onFieldChange: noop$1,
17029
18249
  reportPanel: registerNothing,
17030
18250
  placement: { position: 'bottom-left' },
17031
18251
  multiple: false,
@@ -17033,9 +18253,9 @@ const standalone = {
17033
18253
  appendToBody: false,
17034
18254
  };
17035
18255
  const ComboboxContext = React.createContext(null);
17036
- const useCombobox = () => React.useContext(ComboboxContext) ?? standalone;
18256
+ const useCombobox = () => React.useContext(ComboboxContext) ?? standalone$1;
17037
18257
  /** Hands a node to a forwarded ref and to one of the component's own. */
17038
- function setRefs(node, own, forwarded) {
18258
+ function setRefs$1(node, own, forwarded) {
17039
18259
  own.current = node;
17040
18260
  if (typeof forwarded === 'function')
17041
18261
  forwarded(node);
@@ -17167,7 +18387,7 @@ const ComboboxParts = React.forwardRef(({ children, below, onSettle, open, defau
17167
18387
  appendToBody,
17168
18388
  };
17169
18389
  const { rootRef } = engine;
17170
- const rootRefs = React.useCallback((node) => setRefs(node, rootRef, ref), [rootRef, ref]);
18390
+ const rootRefs = React.useCallback((node) => setRefs$1(node, rootRef, ref), [rootRef, ref]);
17171
18391
  const comboboxElement = (jsxRuntime.jsxs("div", { className: classNames(rootClass, bulmaHelperClasses, className), ...rest, ref: rootRefs, children: [jsxRuntime.jsx(ComboboxContext.Provider, { value: context, children: children }), jsxRuntime.jsx(StatusRegion, { items: announced, describe: describeEmpty })] }));
17172
18392
  const fieldElement = below === undefined ? (comboboxElement) : (jsxRuntime.jsxs("div", { children: [comboboxElement, jsxRuntime.jsx("div", { onPointerDownCapture: engine.markInside, onMouseDown: engine.keepFocus, children: below })] }));
17173
18393
  const messageEl = message ? jsxRuntime.jsx("p", { className: helpClass, children: message }) : null;
@@ -17200,7 +18420,7 @@ const ComboboxInput = React.forwardRef(({ id, isLoading, controlSize, onKeyDown,
17200
18420
  return () => reportLoading(false);
17201
18421
  }, [reportLoading, isLoading]);
17202
18422
  const { inputRef } = engine;
17203
- const inputRefs = React.useCallback((node) => setRefs(node, inputRef, ref), [inputRef, ref]);
18423
+ const inputRefs = React.useCallback((node) => setRefs$1(node, inputRef, ref), [inputRef, ref]);
17204
18424
  const { expanded, highlighted } = engine;
17205
18425
  // An `id` of the field's own reaches the label from a layout effect, so
17206
18426
  // in server markup, and the first render, the label's `for` still names
@@ -17804,158 +19024,1162 @@ label, labelSize, labelProps, horizontal, message, messageColor, fieldClassName,
17804
19024
  const handleKeyDown = React.useCallback((e) => {
17805
19025
  if (disabled || readonly || isComposing(e))
17806
19026
  return;
17807
- // Handle confirm keys
17808
- if (confirmKeys.includes(e.key)) {
17809
- e.preventDefault();
17810
- if (highlightedIndex >= 0 && filteredData[highlightedIndex]) {
17811
- addTags([filteredData[highlightedIndex]]);
17812
- }
17813
- else if (inputValue.trim()) {
17814
- addTags([inputValue]);
17815
- }
19027
+ // Handle confirm keys
19028
+ if (confirmKeys.includes(e.key)) {
19029
+ e.preventDefault();
19030
+ if (highlightedIndex >= 0 && filteredData[highlightedIndex]) {
19031
+ addTags([filteredData[highlightedIndex]]);
19032
+ }
19033
+ else if (inputValue.trim()) {
19034
+ addTags([inputValue]);
19035
+ }
19036
+ return;
19037
+ }
19038
+ switch (e.key) {
19039
+ case 'ArrowDown':
19040
+ e.preventDefault();
19041
+ if (!isActive && data.length > 0) {
19042
+ setIsActive(true);
19043
+ }
19044
+ else {
19045
+ setHighlightedIndex(prev => prev < filteredData.length - 1 ? prev + 1 : prev);
19046
+ }
19047
+ break;
19048
+ case 'ArrowUp':
19049
+ e.preventDefault();
19050
+ setHighlightedIndex(prev => (prev > 0 ? prev - 1 : prev));
19051
+ break;
19052
+ case 'Backspace':
19053
+ if (removeOnKeys && !inputValue && tags.length > 0) {
19054
+ e.preventDefault();
19055
+ removeTag(tags.length - 1);
19056
+ }
19057
+ break;
19058
+ case 'Escape':
19059
+ // Closing the suggestions claims the key, so a Modal around the
19060
+ // field stays open. With none showing, the key goes on to it.
19061
+ if (listOpen)
19062
+ e.preventDefault();
19063
+ setIsActive(false);
19064
+ setHighlightedIndex(-1);
19065
+ break;
19066
+ }
19067
+ }, [
19068
+ disabled,
19069
+ readonly,
19070
+ confirmKeys,
19071
+ highlightedIndex,
19072
+ filteredData,
19073
+ addTags,
19074
+ inputValue,
19075
+ isActive,
19076
+ listOpen,
19077
+ data.length,
19078
+ removeOnKeys,
19079
+ tags.length,
19080
+ removeTag,
19081
+ ]);
19082
+ // Combined ref
19083
+ const combinedRef = React.useCallback((node) => {
19084
+ inputRef.current =
19085
+ node;
19086
+ if (typeof ref === 'function') {
19087
+ ref(node);
19088
+ }
19089
+ else if (ref) {
19090
+ ref.current =
19091
+ node;
19092
+ }
19093
+ }, [ref]);
19094
+ // Focus input when container is clicked
19095
+ const handleContainerClick = React.useCallback(() => {
19096
+ inputRef.current?.focus();
19097
+ }, []);
19098
+ // Generate classes
19099
+ const taginputClasses = pcn('taginput', {
19100
+ 'is-active': isActive,
19101
+ [`is-${size}`]: !!size,
19102
+ [`is-${color}`]: !!color,
19103
+ 'is-disabled': disabled,
19104
+ 'is-rounded': rounded,
19105
+ 'is-ellipsis': ellipsis,
19106
+ });
19107
+ const containerClasses = pcn('taginput-container', {
19108
+ 'is-attached': attached,
19109
+ 'has-icons-left': !!icon,
19110
+ });
19111
+ const inputClasses = pcn('input', {
19112
+ [`is-${size}`]: !!size,
19113
+ });
19114
+ const tagClasses = pcn('tag', {
19115
+ [`is-${tagColor}`]: !!tagColor,
19116
+ [`is-${size}`]: !!size,
19117
+ });
19118
+ const combinedClasses = classNames(taginputClasses, bulmaHelperClasses, className);
19119
+ // Counter text
19120
+ const showCounter = hasCounter && (maxTags !== undefined || maxlength !== undefined);
19121
+ const write = formatNumber ?? String;
19122
+ const counterText = maxTags !== undefined
19123
+ ? `${write(tags.length)} / ${write(maxTags)}`
19124
+ : maxlength !== undefined
19125
+ ? `${write(inputValue.length)} / ${write(maxlength)}`
19126
+ : '';
19127
+ const counterClasses = pcn('help', 'counter');
19128
+ const tagsClasses = pcn('tags');
19129
+ const dropdownMenuClasses = pcn('dropdown-menu', 'is-active');
19130
+ const dropdownContentClasses = pcn('dropdown-content');
19131
+ const iconRightClass = pcn('icon', 'is-right');
19132
+ const loaderClass = pcn('loader', 'is-loading');
19133
+ const helpClass = usePrefixedClassNames('help', {
19134
+ [`is-${messageColor}`]: !!messageColor,
19135
+ });
19136
+ const messageEl = message ? jsxRuntime.jsx("p", { className: helpClass, children: message }) : null;
19137
+ // The combobox controls its listbox and points at the highlighted option,
19138
+ // the way Autocomplete's does. The listbox takes its name from the label
19139
+ // that names the combobox, the one this renders or a surrounding Field's,
19140
+ // and keeps a fallback name when no label names it or the one that does
19141
+ // has no id to point at. While no suggestion matches there's no listbox,
19142
+ // so the combobox is collapsed and points at nothing. It skips a
19143
+ // highlighted suggestion that's blank once trimmed, since that one can't
19144
+ // become a tag, and the option it points at is the one marked selected.
19145
+ const optionId = (index) => `${listboxId}-${index}`;
19146
+ const activeOptionId = listOpen && filteredData[highlightedIndex]?.trim()
19147
+ ? optionId(highlightedIndex)
19148
+ : undefined;
19149
+ const comboboxProps = {
19150
+ role: 'combobox',
19151
+ 'aria-expanded': listOpen,
19152
+ 'aria-controls': listOpen ? listboxId : undefined,
19153
+ 'aria-activedescendant': activeOptionId,
19154
+ 'aria-haspopup': 'listbox',
19155
+ 'aria-autocomplete': 'list',
19156
+ };
19157
+ const taginputElement = (jsxRuntime.jsxs("div", { ref: containerRef, className: combinedClasses, onClick: handleContainerClick, ...rest, children: [jsxRuntime.jsxs("div", { className: containerClasses, children: [icon && (jsxRuntime.jsx(Icon, { name: icon, library: resolvedLibrary, variant: iconVariant, features: iconFeatures, containerClassName: pcn('icon', 'is-left') })), jsxRuntime.jsxs("div", { className: tagsClasses, children: [tags.map((tag, index) => {
19158
+ const displayVal = getDisplayValue(tag);
19159
+ return (jsxRuntime.jsxs("span", { className: tagClasses, children: [tagTemplate ? (tagTemplate(tag)) : ellipsis ? (jsxRuntime.jsx("span", { title: displayVal, children: displayVal })) : (displayVal), closable && !disabled && !readonly && (jsxRuntime.jsx("button", { type: "button", className: pcn('delete', 'is-small'), onClick: e => {
19160
+ e.stopPropagation();
19161
+ removeTag(index);
19162
+ }, "aria-label": ariaCloseLabel || `Remove ${displayVal}` }))] }, index));
19163
+ }), jsxRuntime.jsx("input", { ref: combinedRef, type: "text", className: inputClasses, id: controlId, value: inputValue, placeholder: tags.length === 0 ? placeholder : undefined, disabled: disabled,
19164
+ // Read-only at the tag limit rather than gone, so focus stays
19165
+ // put and a label keeps naming it, and Backspace still makes
19166
+ // room (#1103).
19167
+ readOnly: readonly || isMaxReached, maxLength: maxlength, onChange: handleInputChange, onFocus: handleFocus, onKeyDown: handleKeyDown, onPaste: handlePaste, ...(hasSuggestions && comboboxProps), "aria-label": labelled ? undefined : 'Add tag', "aria-describedby": showCounter ? counterId : undefined })] }), loading && (jsxRuntime.jsx("span", { className: iconRightClass, children: jsxRuntime.jsx("span", { className: loaderClass }) }))] }), listOpen && (jsxRuntime.jsx("div", { className: dropdownMenuClasses, children: jsxRuntime.jsx("div", { ref: dropdownRef, id: listboxId, className: dropdownContentClasses, role: "listbox", "aria-labelledby": listLabelId, "aria-label": listLabelId ? undefined : 'Suggestions', children: filteredData.map((item, index) => (jsxRuntime.jsx("a", { id: optionId(index), className: pcn('dropdown-item', {
19168
+ 'is-active': index === highlightedIndex,
19169
+ }), onClick: () => addTags([item]), onMouseEnter: () => setHighlightedIndex(index), role: "option", "aria-selected": optionId(index) === activeOptionId, children: item }, index))) }) })), showCounter && (jsxRuntime.jsx("small", { id: counterId, className: counterClasses, children: counterText })), name &&
19170
+ tags.map((tag, i) => (jsxRuntime.jsx("input", { type: "hidden", name: name, value: getDisplayValue(tag), form: form }, `tag-input-${i}`)))] }));
19171
+ if (ownField) {
19172
+ return (jsxRuntime.jsxs(Field, { label: label, labelSize: labelSize, labelProps: fieldLabelProps, horizontal: horizontal, className: fieldClassName, children: [taginputElement, messageEl] }));
19173
+ }
19174
+ return (jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [taginputElement, messageEl] }));
19175
+ });
19176
+ Taginput.displayName = 'Taginput';
19177
+ /** @deprecated Use `Taginput` — same component. */
19178
+ const TagInput = Taginput;
19179
+
19180
+ // INTERNAL: deliberately not exported from src/index.ts. Reading, converting
19181
+ // and writing the CSS colors the color picker takes and gives.
19182
+ //
19183
+ // The picker works in HSV (hue, saturation and value, which the square
19184
+ // calls brightness), since its square is saturation by brightness. Every
19185
+ // color it reads becomes HSV, and every value it writes comes out of HSV in
19186
+ // the format asked for.
19187
+ const clamp$3 = (value, low, high) => Math.min(Math.max(value, low), high);
19188
+ /** Rounds to a number of decimal places, so `0.1 + 0.2` writes as `0.3`. */
19189
+ const round = (value, places) => {
19190
+ const factor = 10 ** places;
19191
+ return Math.round(value * factor) / factor;
19192
+ };
19193
+ // ---------------------------------------------------------------------------
19194
+ // Conversions
19195
+ // ---------------------------------------------------------------------------
19196
+ /** HSV to RGB, each channel unrounded. */
19197
+ function hsvaToRgba({ h, s, v, a }) {
19198
+ const sat = s / 100;
19199
+ const val = v / 100;
19200
+ const at = (n) => {
19201
+ const k = (n + h / 60) % 6;
19202
+ return val - val * sat * Math.max(0, Math.min(k, 4 - k, 1));
19203
+ };
19204
+ return { r: at(5) * 255, g: at(3) * 255, b: at(1) * 255, a };
19205
+ }
19206
+ /** RGB to HSV. A gray has no hue, so it gets 0. */
19207
+ function rgbaToHsva({ r, g, b, a }) {
19208
+ const red = r / 255;
19209
+ const green = g / 255;
19210
+ const blue = b / 255;
19211
+ const max = Math.max(red, green, blue);
19212
+ const delta = max - Math.min(red, green, blue);
19213
+ let h = 0;
19214
+ if (delta > 0) {
19215
+ if (max === red)
19216
+ h = ((green - blue) / delta) % 6;
19217
+ else if (max === green)
19218
+ h = (blue - red) / delta + 2;
19219
+ else
19220
+ h = (red - green) / delta + 4;
19221
+ }
19222
+ return {
19223
+ h: (h * 60 + 360) % 360,
19224
+ s: max === 0 ? 0 : (delta / max) * 100,
19225
+ v: max * 100,
19226
+ a,
19227
+ };
19228
+ }
19229
+ /** HSL (saturation and lightness from 0 to 100) to HSV, keeping the hue. */
19230
+ function hslaToHsva(h, s, l, a) {
19231
+ const sat = s / 100;
19232
+ const light = l / 100;
19233
+ const v = light + sat * Math.min(light, 1 - light);
19234
+ return { h, s: v === 0 ? 0 : 2 * (1 - light / v) * 100, v: v * 100, a };
19235
+ }
19236
+ /** HSV to HSL, saturation and lightness from 0 to 100, keeping the hue. */
19237
+ function hsvaToHsla({ h, s, v, a }) {
19238
+ const sat = s / 100;
19239
+ const val = v / 100;
19240
+ const light = val * (1 - sat / 2);
19241
+ const lowest = Math.min(light, 1 - light);
19242
+ return {
19243
+ h,
19244
+ s: lowest === 0 ? 0 : ((val - light) / lowest) * 100,
19245
+ l: light * 100,
19246
+ a,
19247
+ };
19248
+ }
19249
+ // ---------------------------------------------------------------------------
19250
+ // Writing
19251
+ // ---------------------------------------------------------------------------
19252
+ const hexByte = (value) => Math.round(clamp$3(value, 0, 255))
19253
+ .toString(16)
19254
+ .padStart(2, '0');
19255
+ /** Alpha as CSS writes it, to two decimal places: `0.5`, `1`. */
19256
+ const writeAlpha = (a) => String(round(clamp$3(a, 0, 1), 2));
19257
+ /**
19258
+ * Writes a color in a format: `hsl(171, 100%, 41%)`, `#00d1b2` or
19259
+ * `rgb(0, 209, 178)`, with alpha as `hsla(…)`, `#00d1b2ff` and `rgba(…)`.
19260
+ * Channels are whole numbers, and the legacy comma form is used, which every
19261
+ * CSS parser and color library reads.
19262
+ */
19263
+ function formatColor(color, format, alpha) {
19264
+ if (format === 'hsl') {
19265
+ const { h, s, l, a } = hsvaToHsla(color);
19266
+ const channels = `${Math.round(h) % 360}, ${Math.round(s)}%, ${Math.round(l)}%`;
19267
+ return alpha ? `hsla(${channels}, ${writeAlpha(a)})` : `hsl(${channels})`;
19268
+ }
19269
+ const { r, g, b, a } = hsvaToRgba(color);
19270
+ if (format === 'hex') {
19271
+ const hex = `#${hexByte(r)}${hexByte(g)}${hexByte(b)}`;
19272
+ return alpha ? `${hex}${hexByte(a * 255)}` : hex;
19273
+ }
19274
+ const channels = `${Math.round(r)}, ${Math.round(g)}, ${Math.round(b)}`;
19275
+ return alpha ? `rgba(${channels}, ${writeAlpha(a)})` : `rgb(${channels})`;
19276
+ }
19277
+ /** The color as 8-bit RGB and alpha to two places, for telling colors apart. */
19278
+ const key = (color) => {
19279
+ const { r, g, b, a } = hsvaToRgba(color);
19280
+ return [r, g, b].map(c => Math.round(c)).join(',') + `,${writeAlpha(a)}`;
19281
+ };
19282
+ /** Whether two colors are the same once written as 8-bit RGB with alpha. */
19283
+ const sameColor = (one, other) => key(one) === key(other);
19284
+ /**
19285
+ * The relative luminance of a color, from 0 for black to 1 for white, as
19286
+ * WCAG defines it.
19287
+ */
19288
+ function luminance(color) {
19289
+ const { r, g, b } = hsvaToRgba(color);
19290
+ const linear = (channel) => {
19291
+ const c = channel / 255;
19292
+ return c <= 0.04045 ? c / 12.92 : ((c + 0.055) / 1.055) ** 2.4;
19293
+ };
19294
+ return 0.2126 * linear(r) + 0.7152 * linear(g) + 0.0722 * linear(b);
19295
+ }
19296
+ /**
19297
+ * Whether black reads better than white on a color, by WCAG contrast
19298
+ * ratio. Used for the check on a chosen swatch.
19299
+ */
19300
+ function prefersDarkInk(color) {
19301
+ const l = luminance(color);
19302
+ return (l + 0.05) / 0.05 >= 1.05 / (l + 0.05);
19303
+ }
19304
+ // ---------------------------------------------------------------------------
19305
+ // Reading
19306
+ // ---------------------------------------------------------------------------
19307
+ // The CSS named colors, each name followed by its six hex digits.
19308
+ const NAMED = 'aliceblue:f0f8ff,antiquewhite:faebd7,aqua:00ffff,aquamarine:7fffd4,' +
19309
+ 'azure:f0ffff,beige:f5f5dc,bisque:ffe4c4,black:000000,' +
19310
+ 'blanchedalmond:ffebcd,blue:0000ff,blueviolet:8a2be2,brown:a52a2a,' +
19311
+ 'burlywood:deb887,cadetblue:5f9ea0,chartreuse:7fff00,chocolate:d2691e,' +
19312
+ 'coral:ff7f50,cornflowerblue:6495ed,cornsilk:fff8dc,crimson:dc143c,' +
19313
+ 'cyan:00ffff,darkblue:00008b,darkcyan:008b8b,darkgoldenrod:b8860b,' +
19314
+ 'darkgray:a9a9a9,darkgreen:006400,darkgrey:a9a9a9,darkkhaki:bdb76b,' +
19315
+ 'darkmagenta:8b008b,darkolivegreen:556b2f,darkorange:ff8c00,' +
19316
+ 'darkorchid:9932cc,darkred:8b0000,darksalmon:e9967a,' +
19317
+ 'darkseagreen:8fbc8f,darkslateblue:483d8b,darkslategray:2f4f4f,' +
19318
+ 'darkslategrey:2f4f4f,darkturquoise:00ced1,darkviolet:9400d3,' +
19319
+ 'deeppink:ff1493,deepskyblue:00bfff,dimgray:696969,dimgrey:696969,' +
19320
+ 'dodgerblue:1e90ff,firebrick:b22222,floralwhite:fffaf0,' +
19321
+ 'forestgreen:228b22,fuchsia:ff00ff,gainsboro:dcdcdc,' +
19322
+ 'ghostwhite:f8f8ff,gold:ffd700,goldenrod:daa520,gray:808080,' +
19323
+ 'green:008000,greenyellow:adff2f,grey:808080,honeydew:f0fff0,' +
19324
+ 'hotpink:ff69b4,indianred:cd5c5c,indigo:4b0082,ivory:fffff0,' +
19325
+ 'khaki:f0e68c,lavender:e6e6fa,lavenderblush:fff0f5,' +
19326
+ 'lawngreen:7cfc00,lemonchiffon:fffacd,lightblue:add8e6,' +
19327
+ 'lightcoral:f08080,lightcyan:e0ffff,lightgoldenrodyellow:fafad2,' +
19328
+ 'lightgray:d3d3d3,lightgreen:90ee90,lightgrey:d3d3d3,' +
19329
+ 'lightpink:ffb6c1,lightsalmon:ffa07a,lightseagreen:20b2aa,' +
19330
+ 'lightskyblue:87cefa,lightslategray:778899,lightslategrey:778899,' +
19331
+ 'lightsteelblue:b0c4de,lightyellow:ffffe0,lime:00ff00,' +
19332
+ 'limegreen:32cd32,linen:faf0e6,magenta:ff00ff,maroon:800000,' +
19333
+ 'mediumaquamarine:66cdaa,mediumblue:0000cd,mediumorchid:ba55d3,' +
19334
+ 'mediumpurple:9370db,mediumseagreen:3cb371,mediumslateblue:7b68ee,' +
19335
+ 'mediumspringgreen:00fa9a,mediumturquoise:48d1cc,' +
19336
+ 'mediumvioletred:c71585,midnightblue:191970,mintcream:f5fffa,' +
19337
+ 'mistyrose:ffe4e1,moccasin:ffe4b5,navajowhite:ffdead,navy:000080,' +
19338
+ 'oldlace:fdf5e6,olive:808000,olivedrab:6b8e23,orange:ffa500,' +
19339
+ 'orangered:ff4500,orchid:da70d6,palegoldenrod:eee8aa,' +
19340
+ 'palegreen:98fb98,paleturquoise:afeeee,palevioletred:db7093,' +
19341
+ 'papayawhip:ffefd5,peachpuff:ffdab9,peru:cd853f,pink:ffc0cb,' +
19342
+ 'plum:dda0dd,powderblue:b0e0e6,purple:800080,' +
19343
+ 'rebeccapurple:663399,red:ff0000,rosybrown:bc8f8f,' +
19344
+ 'royalblue:4169e1,saddlebrown:8b4513,salmon:fa8072,' +
19345
+ 'sandybrown:f4a460,seagreen:2e8b57,seashell:fff5ee,sienna:a0522d,' +
19346
+ 'silver:c0c0c0,skyblue:87ceeb,slateblue:6a5acd,slategray:708090,' +
19347
+ 'slategrey:708090,snow:fffafa,springgreen:00ff7f,steelblue:4682b4,' +
19348
+ 'tan:d2b48c,teal:008080,thistle:d8bfd8,tomato:ff6347,' +
19349
+ 'turquoise:40e0d0,violet:ee82ee,wheat:f5deb3,white:ffffff,' +
19350
+ 'whitesmoke:f5f5f5,yellow:ffff00,yellowgreen:9acd32';
19351
+ let namedColors;
19352
+ /** The hex digits of a CSS color name, or undefined for any other word. */
19353
+ const namedHex = (name) => {
19354
+ if (!namedColors) {
19355
+ namedColors = new Map(NAMED.split(',').map(entry => entry.split(':')));
19356
+ }
19357
+ return namedColors.get(name);
19358
+ };
19359
+ /** Reads `#rgb`, `#rgba`, `#rrggbb` and `#rrggbbaa`, the `#` optional. */
19360
+ function parseHex(text) {
19361
+ const digits = text.startsWith('#') ? text.slice(1) : text;
19362
+ if (!/^[0-9a-f]+$/.test(digits) || ![3, 4, 6, 8].includes(digits.length)) {
19363
+ return null;
19364
+ }
19365
+ const full = digits.length < 6
19366
+ ? digits
19367
+ .split('')
19368
+ .map(d => d + d)
19369
+ .join('')
19370
+ : digits;
19371
+ const byte = (i) => parseInt(full.slice(i, i + 2), 16);
19372
+ return rgbaToHsva({
19373
+ r: byte(0),
19374
+ g: byte(2),
19375
+ b: byte(4),
19376
+ a: full.length === 8 ? byte(6) / 255 : 1,
19377
+ });
19378
+ }
19379
+ /**
19380
+ * The arguments of a CSS color function, in either syntax: commas
19381
+ * (`rgb(0, 209, 178)`) or spaces with the alpha after a slash
19382
+ * (`rgb(0 209 178 / 50%)`). Null when they're neither.
19383
+ */
19384
+ function splitArguments(body) {
19385
+ if (body.includes(',')) {
19386
+ const parts = body.split(',').map(part => part.trim());
19387
+ return parts.includes('') ? null : parts;
19388
+ }
19389
+ const [channels, alpha, ...more] = body.split('/');
19390
+ const parts = channels.trim().split(/\s+/).filter(Boolean);
19391
+ if (more.length > 0 || parts.length !== 3)
19392
+ return null;
19393
+ if (alpha === undefined)
19394
+ return parts;
19395
+ const a = alpha.trim();
19396
+ return a && !/\s/.test(a) ? [...parts, a] : null;
19397
+ }
19398
+ /** A plain number or a percentage of `whole`, or NaN. */
19399
+ const numberOf = (part, whole) => {
19400
+ if (!/^[+-]?(\d+\.?\d*|\.\d+)(e[+-]?\d+)?%?$/.test(part))
19401
+ return NaN;
19402
+ const value = parseFloat(part);
19403
+ return part.endsWith('%') ? (value / 100) * whole : value;
19404
+ };
19405
+ /** An alpha, `0.5` or `50%`, clamped to 0 to 1, or NaN. */
19406
+ const alphaOf = (part) => part === undefined ? 1 : clamp$3(numberOf(part, 1), 0, 1);
19407
+ const ANGLE_UNITS = {
19408
+ deg: 1,
19409
+ grad: 360 / 400,
19410
+ rad: 180 / Math.PI,
19411
+ turn: 360,
19412
+ };
19413
+ /** A hue in degrees from 0 to 360, from a number or an angle, or NaN. */
19414
+ const hueOf = (part) => {
19415
+ const match = /^([+-]?(?:\d+\.?\d*|\.\d+)(?:e[+-]?\d+)?)(deg|grad|rad|turn)?$/.exec(part);
19416
+ if (!match)
19417
+ return NaN;
19418
+ const degrees = parseFloat(match[1]) * ANGLE_UNITS[match[2] ?? 'deg'];
19419
+ return ((degrees % 360) + 360) % 360;
19420
+ };
19421
+ /** Reads `rgb()`, `rgba()`, `hsl()` and `hsla()` in either syntax. */
19422
+ function parseFunction(text) {
19423
+ const match = /^(rgba?|hsla?)\((.*)\)$/.exec(text);
19424
+ if (!match)
19425
+ return null;
19426
+ const parts = splitArguments(match[2]);
19427
+ if (!parts || parts.length < 3 || parts.length > 4)
19428
+ return null;
19429
+ const [first, second, third, fourth] = parts;
19430
+ const a = alphaOf(fourth);
19431
+ if (match[1].startsWith('rgb')) {
19432
+ const [r, g, b] = [first, second, third].map(part => clamp$3(numberOf(part, 255), 0, 255));
19433
+ if ([r, g, b, a].some(Number.isNaN))
19434
+ return null;
19435
+ return rgbaToHsva({ r, g, b, a });
19436
+ }
19437
+ const h = hueOf(first);
19438
+ // Modern syntax lets saturation and lightness drop the percent sign.
19439
+ const [s, l] = [second, third].map(part => clamp$3(numberOf(part.replace(/%$/, ''), 100), 0, 100));
19440
+ if ([h, s, l, a].some(Number.isNaN))
19441
+ return null;
19442
+ return hslaToHsva(h, s, l, a);
19443
+ }
19444
+ /**
19445
+ * Whether a color's text names its own hue, as `hsl()` and `hsla()` do even
19446
+ * for a gray, where hex, `rgb()` and the color names leave it out.
19447
+ */
19448
+ const namesHue = (text) => /^\s*hsla?\(/i.test(text);
19449
+ /**
19450
+ * Reads a CSS color: hex (with or without `#`), `rgb()`, `rgba()`, `hsl()`
19451
+ * and `hsla()` in either syntax, the named colors, and `transparent`. Null
19452
+ * for anything else, such as `oklch()` or `var(--x)`, which only the
19453
+ * browser can resolve.
19454
+ */
19455
+ function parseColor(input) {
19456
+ if (!input)
19457
+ return null;
19458
+ const text = input.trim().toLowerCase();
19459
+ if (text === 'transparent')
19460
+ return { h: 0, s: 0, v: 0, a: 0 };
19461
+ const named = namedHex(text);
19462
+ if (named)
19463
+ return parseHex(named);
19464
+ if (text.includes('('))
19465
+ return parseFunction(text);
19466
+ return parseHex(text);
19467
+ }
19468
+
19469
+ function useNativeMobilePicker(options = {}) {
19470
+ const { smallViewportMaxPx = 768, force } = options;
19471
+ const [state, setState] = React.useState({
19472
+ isCoarsePointer: false,
19473
+ isSmallViewport: false,
19474
+ shouldUseNative: force === true,
19475
+ });
19476
+ React.useEffect(() => {
19477
+ const isBrowser = typeof window !== 'undefined' && typeof window.matchMedia === 'function';
19478
+ if (!isBrowser)
19479
+ return undefined;
19480
+ const coarse = window.matchMedia('(pointer: coarse)');
19481
+ const small = window.matchMedia(`(max-width: ${smallViewportMaxPx}px)`);
19482
+ const update = () => {
19483
+ const isCoarsePointer = coarse.matches;
19484
+ const isSmallViewport = small.matches;
19485
+ const shouldUseNative = force === true
19486
+ ? true
19487
+ : force === false
19488
+ ? false
19489
+ : isCoarsePointer && isSmallViewport;
19490
+ setState({ isCoarsePointer, isSmallViewport, shouldUseNative });
19491
+ };
19492
+ update();
19493
+ const subscribe = (mql, fn) => {
19494
+ if (mql.addEventListener) {
19495
+ // eslint-disable-next-line @eslint-react/web-api-no-leaked-event-listener -- cleanup returned below and run in the effect teardown
19496
+ mql.addEventListener('change', fn);
19497
+ return () => mql.removeEventListener('change', fn);
19498
+ }
19499
+ // Older Safari fallback
19500
+ mql.addListener(fn);
19501
+ return () => mql.removeListener(fn);
19502
+ };
19503
+ const unsubCoarse = subscribe(coarse, update);
19504
+ const unsubSmall = subscribe(small, update);
19505
+ return () => {
19506
+ unsubCoarse();
19507
+ unsubSmall();
19508
+ };
19509
+ }, [smallViewportMaxPx, force]);
19510
+ return state;
19511
+ }
19512
+
19513
+ // INTERNAL: deliberately not exported from src/index.ts. The color a picker
19514
+ // holds and the context its parts read, shared by ColorPicker and the panel
19515
+ // ColorInput opens, so the panel needs no Field or Control of its own.
19516
+ const DEFAULT_LABELS = {
19517
+ area: 'Saturation and brightness',
19518
+ areaRoleDescription: '2D slider',
19519
+ saturation: 'Saturation',
19520
+ brightness: 'Brightness',
19521
+ hue: 'Hue',
19522
+ alpha: 'Alpha',
19523
+ degrees: 'degrees',
19524
+ value: 'Color value',
19525
+ swatches: 'Swatches',
19526
+ eyeDropper: 'Pick a color from the screen',
19527
+ };
19528
+ /** The labels with each one left out, or empty, at its default. */
19529
+ const mergeColorLabels = (labels) => {
19530
+ const merged = { ...DEFAULT_LABELS };
19531
+ for (const [key, value] of Object.entries(labels ?? {})) {
19532
+ if (value)
19533
+ merged[key] = value;
19534
+ }
19535
+ return merged;
19536
+ };
19537
+ const BLACK = { h: 0, s: 0, v: 0, a: 1 };
19538
+ /**
19539
+ * A color read from outside the picker, written as `source`, with the hue
19540
+ * and saturation the picker had where the new color has none, so the thumbs
19541
+ * don't jump to 0. A gray written as hex, `rgb()` or a name has no hue, so
19542
+ * it keeps the picker's; one written as `hsl()` names its hue, which wins.
19543
+ * Black has no saturation in the square either way, and keeps the picker's,
19544
+ * which writes no differently.
19545
+ */
19546
+ const keepingChannels = (next, previous, source) => ({
19547
+ ...next,
19548
+ h: (next.s === 0 || next.v === 0) && !namesHue(source) ? previous.h : next.h,
19549
+ s: next.v === 0 ? previous.s : next.s,
19550
+ });
19551
+ const ColorPickerContext = React.createContext(null);
19552
+ /** Does nothing, for a part outside a picker and a cancelled eye dropper. */
19553
+ const noop = () => { };
19554
+ // What a part outside a ColorPicker reads, so its hooks run the same; it
19555
+ // then renders nothing.
19556
+ const standalone = {
19557
+ color: BLACK,
19558
+ text: '',
19559
+ format: 'hsl',
19560
+ alpha: false,
19561
+ disabled: true,
19562
+ labels: DEFAULT_LABELS,
19563
+ swatches: undefined,
19564
+ eyeDropper: false,
19565
+ touch: false,
19566
+ percent: String,
19567
+ degrees: String,
19568
+ change: noop,
19569
+ end: noop,
19570
+ };
19571
+ /** The picker around a part, or null with a development warning. */
19572
+ function usePicker(part) {
19573
+ const picker = React.useContext(ColorPickerContext);
19574
+ const outside = !picker;
19575
+ React.useEffect(() => {
19576
+ if (outside) {
19577
+ warnOnce(`ColorPicker:${part}:outside`, `[bestax-bulma] <ColorPicker.${part}> needs a <ColorPicker> around ` +
19578
+ 'it, which holds the color, and renders nothing without one.');
19579
+ }
19580
+ }, [outside, part]);
19581
+ return picker;
19582
+ }
19583
+ /** A percentage, in the provider's locale when there is one. */
19584
+ const writePercent = (formatNumber) => (value) => formatNumber
19585
+ ? formatNumber(value / 100, { style: 'percent' })
19586
+ : `${value}%`;
19587
+ /** Degrees, in the provider's locale when there is one, else with `unit`. */
19588
+ const writeDegrees = (formatNumber, unit) => (value) => formatNumber
19589
+ ? formatNumber(value, {
19590
+ style: 'unit',
19591
+ unit: 'degree',
19592
+ unitDisplay: 'long',
19593
+ })
19594
+ : `${value} ${unit}`;
19595
+ /**
19596
+ * Holds a picker's color and builds what its parts read. Controlled, the
19597
+ * color shown follows `value`; a value the picker handed out comes back as
19598
+ * the exact color it was, thumbs and all.
19599
+ */
19600
+ function useColorPicker({ value, defaultValue, onChange, onChangeEnd, format, alpha, disabled, swatches, eyeDropper, labels, }) {
19601
+ const isControlled = value !== undefined;
19602
+ // The color the picker last set or started with, and its text. While
19603
+ // `value` is that text, this is the color shown.
19604
+ const [own, setOwn] = React.useState(() => {
19605
+ const start = isControlled ? value : defaultValue;
19606
+ return { text: start ?? '', color: parseColor(start) ?? BLACK };
19607
+ });
19608
+ // A controlled value from outside becomes the color held, during this
19609
+ // render, keeping the hue and saturation a gray or black has none of.
19610
+ // One the picker can't read changes nothing, so the last good color
19611
+ // stays on screen.
19612
+ if (isControlled && value !== own.text) {
19613
+ const parsed = parseColor(value);
19614
+ if (parsed) {
19615
+ setOwn({
19616
+ text: value,
19617
+ color: keepingChannels(parsed, own.color, value),
19618
+ });
19619
+ }
19620
+ }
19621
+ const color = alpha ? own.color : { ...own.color, a: 1 };
19622
+ const text = formatColor(color, format, alpha);
19623
+ // The latest text handed out, so a step that writes the same text
19624
+ // doesn't call onChange again, and a drag's end reports the last one.
19625
+ const lastTextRef = React.useRef(text);
19626
+ useClientLayoutEffect(() => {
19627
+ lastTextRef.current = text;
19628
+ });
19629
+ const change = React.useCallback((next, finished = false) => {
19630
+ if (disabled)
19631
+ return;
19632
+ const solid = alpha ? next : { ...next, a: 1 };
19633
+ const nextText = formatColor(solid, format, alpha);
19634
+ setOwn({ text: nextText, color: solid });
19635
+ if (nextText !== lastTextRef.current) {
19636
+ lastTextRef.current = nextText;
19637
+ onChange?.(nextText);
19638
+ }
19639
+ if (finished)
19640
+ onChangeEnd?.(nextText);
19641
+ }, [disabled, alpha, format, onChange, onChangeEnd]);
19642
+ const end = React.useCallback(() => onChangeEnd?.(lastTextRef.current), [onChangeEnd]);
19643
+ const formatNumber = useNumberFormat();
19644
+ const { isCoarsePointer } = useNativeMobilePicker();
19645
+ const merged = mergeColorLabels(labels);
19646
+ return {
19647
+ color,
19648
+ text,
19649
+ format,
19650
+ alpha,
19651
+ disabled,
19652
+ labels: merged,
19653
+ swatches,
19654
+ eyeDropper,
19655
+ touch: isCoarsePointer,
19656
+ percent: writePercent(formatNumber),
19657
+ degrees: writeDegrees(formatNumber, merged.degrees),
19658
+ change,
19659
+ end,
19660
+ };
19661
+ }
19662
+
19663
+ // The theme colors `swatches="bulma"` shows, read off the page.
19664
+ const BULMA_SWATCHES = [
19665
+ { color: 'var(--bulma-primary)', label: 'Primary' },
19666
+ { color: 'var(--bulma-link)', label: 'Link' },
19667
+ { color: 'var(--bulma-info)', label: 'Info' },
19668
+ { color: 'var(--bulma-success)', label: 'Success' },
19669
+ { color: 'var(--bulma-warning)', label: 'Warning' },
19670
+ { color: 'var(--bulma-danger)', label: 'Danger' },
19671
+ ];
19672
+ const clamp$2 = (value, low, high) => Math.min(Math.max(value, low), high);
19673
+ /** Whether the element is laid out right to left. */
19674
+ const isRtl = (element) => !!element && getComputedStyle(element).direction === 'rtl';
19675
+ /** Where a pointer is across an element, from 0 to 1 each way. */
19676
+ const pointIn = (element, event) => {
19677
+ const rect = element.getBoundingClientRect();
19678
+ const across = rect.width > 0 ? clamp$2((event.clientX - rect.left) / rect.width, 0, 1) : 0;
19679
+ const down = rect.height > 0 ? clamp$2((event.clientY - rect.top) / rect.height, 0, 1) : 0;
19680
+ return { x: isRtl(element) ? 1 - across : across, y: down };
19681
+ };
19682
+ /** The color's red, green and blue, rounded, as a CSS `rgb()` takes them. */
19683
+ const rgbChannels = (color) => {
19684
+ const { r, g, b } = hsvaToRgba(color);
19685
+ return `${Math.round(r)} ${Math.round(g)} ${Math.round(b)}`;
19686
+ };
19687
+ /** Whether a press should start a drag: any touch or pen, a mouse's main button. */
19688
+ const startsDrag = (event) => event.pointerType !== 'mouse' || event.button === 0;
19689
+ /** Hands a node to a forwarded ref and to one of the part's own. */
19690
+ function setRefs(node, own, forwarded) {
19691
+ own.current = node;
19692
+ if (typeof forwarded === 'function')
19693
+ forwarded(node);
19694
+ else if (forwarded)
19695
+ forwarded.current = node;
19696
+ }
19697
+ /**
19698
+ * The saturation and brightness square, operated by pointer or by keyboard as one 2D slider.
19699
+ *
19700
+ * @function
19701
+ * @param {ColorPickerAreaProps} props - Props for ColorPicker.Area.
19702
+ * @param {React.Ref<HTMLDivElement>} ref - Forwarded ref to the square's `<div>`.
19703
+ * @returns {JSX.Element | null} The square, or nothing outside a ColorPicker.
19704
+ */
19705
+ const ColorPickerArea = React.forwardRef(({ className, style, ...props }, ref) => {
19706
+ const picker = usePicker('Area');
19707
+ const live = picker ?? standalone;
19708
+ const { color, change, end, disabled, labels, touch, percent } = live;
19709
+ const { bulmaHelperClasses, rest } = useBulmaClasses(props);
19710
+ const rootRef = React.useRef(null);
19711
+ const xRef = React.useRef(null);
19712
+ const yRef = React.useRef(null);
19713
+ // The axis whose input holds the tab stop: the one focused or moved last.
19714
+ const [axis, setAxis] = React.useState('x');
19715
+ // Once a key has moved the square, both inputs are exposed until focus
19716
+ // leaves it, so the one that moved can be read.
19717
+ const [keyed, setKeyed] = React.useState(false);
19718
+ const draggingRef = React.useRef(false);
19719
+ const areaClass = usePrefixedClassNames('color-area', {
19720
+ 'is-disabled': disabled,
19721
+ });
19722
+ const thumbClass = usePrefixedClassNames('color-area-thumb');
19723
+ const inputClass = usePrefixedClassNames('color-area-input');
19724
+ const s = Math.round(color.s);
19725
+ const v = Math.round(color.v);
19726
+ const saturationText = `${labels.saturation} ${percent(s)}`;
19727
+ const brightnessText = `${labels.brightness} ${percent(v)}`;
19728
+ const focusAxis = (which) => (which === 'x' ? xRef : yRef).current?.focus({ preventScroll: true });
19729
+ // The axis a key moved to, focused once the render that shows its input
19730
+ // is on the page, so it's no longer aria-hidden as it takes focus.
19731
+ const focusNextRef = React.useRef(null);
19732
+ useClientLayoutEffect(() => {
19733
+ if (!focusNextRef.current)
19734
+ return;
19735
+ focusAxis(focusNextRef.current);
19736
+ focusNextRef.current = null;
19737
+ });
19738
+ const moveTo = (event) => {
19739
+ const { x, y } = pointIn(event.currentTarget, event);
19740
+ change({ ...color, s: x * 100, v: (1 - y) * 100 });
19741
+ };
19742
+ const onPointerDown = (event) => {
19743
+ if (disabled || !startsDrag(event))
19744
+ return;
19745
+ // No text selection, and focus goes where the square puts it.
19746
+ event.preventDefault();
19747
+ event.currentTarget.setPointerCapture?.(event.pointerId);
19748
+ draggingRef.current = true;
19749
+ moveTo(event);
19750
+ focusAxis(axis);
19751
+ };
19752
+ const onPointerMove = (event) => {
19753
+ if (draggingRef.current)
19754
+ moveTo(event);
19755
+ };
19756
+ const endDrag = (event) => {
19757
+ if (!draggingRef.current)
19758
+ return;
19759
+ draggingRef.current = false;
19760
+ event.currentTarget.releasePointerCapture?.(event.pointerId);
19761
+ end();
19762
+ };
19763
+ const onKeyDown = (event) => {
19764
+ const by = event.shiftKey ? 10 : 1;
19765
+ // The horizontal arrows follow the screen: right to left, saturation
19766
+ // grows to the left.
19767
+ const right = isRtl(rootRef.current) ? -by : by;
19768
+ const moves = {
19769
+ ArrowRight: [right, 0],
19770
+ ArrowLeft: [-right, 0],
19771
+ ArrowUp: [0, by],
19772
+ ArrowDown: [0, -by],
19773
+ PageUp: [0, 10],
19774
+ PageDown: [0, -10],
19775
+ Home: [-10, 0],
19776
+ End: [10, 0],
19777
+ };
19778
+ const move = moves[event.key];
19779
+ if (!move)
19780
+ return;
19781
+ event.preventDefault();
19782
+ const [dx, dy] = move;
19783
+ change({
19784
+ ...color,
19785
+ s: clamp$2(s + dx, 0, 100),
19786
+ v: clamp$2(v + dy, 0, 100),
19787
+ }, true);
19788
+ const moved = dx !== 0 ? 'x' : 'y';
19789
+ setAxis(moved);
19790
+ setKeyed(true);
19791
+ if (moved !== axis)
19792
+ focusNextRef.current = moved;
19793
+ };
19794
+ // A touch screen reader, or a browser's own stepping, sets an input's
19795
+ // value without a key the square handled.
19796
+ const onInput = (which) => (event) => {
19797
+ const value = Number(event.target.value);
19798
+ change(which === 'x' ? { ...color, s: value } : { ...color, v: value }, true);
19799
+ };
19800
+ const onBlur = (event) => {
19801
+ const next = event.relatedTarget;
19802
+ if (next !== xRef.current && next !== yRef.current)
19803
+ setKeyed(false);
19804
+ };
19805
+ // What both inputs share; each adds its axis, and the handlers.
19806
+ const shared = {
19807
+ type: 'range',
19808
+ className: inputClass,
19809
+ min: 0,
19810
+ max: 100,
19811
+ step: 1,
19812
+ disabled,
19813
+ 'aria-label': labels.area,
19814
+ 'aria-roledescription': labels.areaRoleDescription,
19815
+ };
19816
+ // The one not holding the tab stop is hidden until a key moves the
19817
+ // square, so a screen reader lists one control; both show on touch.
19818
+ const tabIndexOf = (which) => (touch || axis === which ? 0 : -1);
19819
+ const hiddenOf = (which) => touch || keyed || axis === which ? undefined : true;
19820
+ if (!picker)
19821
+ return null;
19822
+ return (jsxRuntime.jsx("div", { ref: node => setRefs(node, rootRef, ref), className: classNames(areaClass, bulmaHelperClasses, className), style: {
19823
+ ...style,
19824
+ '--color-picker-hue': Math.round(color.h),
19825
+ '--color-picker-rgb': rgbChannels(color),
19826
+ '--color-area-x': `${color.s}%`,
19827
+ '--color-area-y': `${100 - color.v}%`,
19828
+ }, onPointerDown: onPointerDown, onPointerMove: onPointerMove, onPointerUp: endDrag, onPointerCancel: endDrag, ...rest, children: jsxRuntime.jsxs("div", { className: thumbClass, role: "presentation", children: [jsxRuntime.jsx("input", { ref: xRef, ...shared, value: s, "aria-valuetext": `${saturationText}, ${brightnessText}`, "aria-orientation": "horizontal", tabIndex: tabIndexOf('x'), "aria-hidden": hiddenOf('x'), onChange: onInput('x'), onKeyDown: onKeyDown, onFocus: () => setAxis('x'), onBlur: onBlur }), jsxRuntime.jsx("input", { ref: yRef, ...shared, value: v, "aria-valuetext": `${brightnessText}, ${saturationText}`, "aria-orientation": "vertical", tabIndex: tabIndexOf('y'), "aria-hidden": hiddenOf('y'), onChange: onInput('y'), onKeyDown: onKeyDown, onFocus: () => setAxis('y'), onBlur: onBlur })] }) }));
19829
+ });
19830
+ ColorPickerArea.displayName = 'ColorPicker.Area';
19831
+ /** The hue or the alpha slider: a hidden native range under a drawn track. */
19832
+ const ChannelSlider = React.forwardRef(({ channel, className, style, ...props }, ref) => {
19833
+ const picker = usePicker(channel === 'hue' ? 'HueSlider' : 'AlphaSlider');
19834
+ const live = picker ?? standalone;
19835
+ const { color, change, end, disabled, labels, percent, degrees } = live;
19836
+ const { bulmaHelperClasses, rest } = useBulmaClasses(props);
19837
+ const rootRef = React.useRef(null);
19838
+ const draggingRef = React.useRef(false);
19839
+ const isHue = channel === 'hue';
19840
+ const max = isHue ? 360 : 100;
19841
+ const value = Math.round(isHue ? color.h : color.a * 100);
19842
+ const withValue = (next) => isHue ? { ...color, h: next } : { ...color, a: next / 100 };
19843
+ const sliderClass = usePrefixedClassNames('color-slider', {
19844
+ 'is-hue': isHue,
19845
+ 'is-alpha': !isHue,
19846
+ 'is-disabled': disabled,
19847
+ });
19848
+ const trackClass = usePrefixedClassNames('color-slider-track');
19849
+ const thumbClass = usePrefixedClassNames('color-slider-thumb');
19850
+ const inputClass = usePrefixedClassNames('color-slider-input');
19851
+ const moveTo = (event) => {
19852
+ const { x } = pointIn(event.currentTarget, event);
19853
+ change(withValue(x * max));
19854
+ };
19855
+ const onPointerDown = (event) => {
19856
+ if (disabled || !startsDrag(event))
19857
+ return;
19858
+ event.preventDefault();
19859
+ event.currentTarget.setPointerCapture?.(event.pointerId);
19860
+ draggingRef.current = true;
19861
+ moveTo(event);
19862
+ rootRef.current?.querySelector('input')?.focus({ preventScroll: true });
19863
+ };
19864
+ const onPointerMove = (event) => {
19865
+ if (draggingRef.current)
19866
+ moveTo(event);
19867
+ };
19868
+ const endDrag = (event) => {
19869
+ if (!draggingRef.current)
19870
+ return;
19871
+ draggingRef.current = false;
19872
+ event.currentTarget.releasePointerCapture?.(event.pointerId);
19873
+ end();
19874
+ };
19875
+ const onKeyDown = (event) => {
19876
+ const by = event.shiftKey ? 10 : 1;
19877
+ const right = isRtl(rootRef.current) ? -by : by;
19878
+ const steps = {
19879
+ ArrowRight: right,
19880
+ ArrowLeft: -right,
19881
+ ArrowUp: by,
19882
+ ArrowDown: -by,
19883
+ PageUp: 10,
19884
+ PageDown: -10,
19885
+ Home: -max,
19886
+ End: max,
19887
+ };
19888
+ const step = steps[event.key];
19889
+ if (step === undefined)
19890
+ return;
19891
+ event.preventDefault();
19892
+ change(withValue(clamp$2(value + step, 0, max)), true);
19893
+ };
19894
+ if (!picker)
19895
+ return null;
19896
+ return (jsxRuntime.jsxs("div", { ref: node => setRefs(node, rootRef, ref), className: classNames(sliderClass, bulmaHelperClasses, className), style: {
19897
+ ...style,
19898
+ '--color-picker-hue': Math.round(color.h),
19899
+ '--color-picker-rgb': rgbChannels(color),
19900
+ '--color-slider-position': `${(value / max) * 100}%`,
19901
+ }, onPointerDown: onPointerDown, onPointerMove: onPointerMove, onPointerUp: endDrag, onPointerCancel: endDrag, ...rest, children: [jsxRuntime.jsx("div", { className: trackClass }), jsxRuntime.jsx("div", { className: thumbClass, role: "presentation", children: jsxRuntime.jsx("input", { type: "range", className: inputClass, min: 0, max: max, step: 1, value: value, disabled: disabled,
19902
+ // Spelled out, as the square's is, so Safari's Tab reaches it
19903
+ // with its default settings, which skip a range input without
19904
+ // one.
19905
+ tabIndex: 0, "aria-label": isHue ? labels.hue : labels.alpha, "aria-valuetext": isHue ? degrees(value) : percent(value), onChange: event => change(withValue(Number(event.target.value)), true), onKeyDown: onKeyDown }) })] }));
19906
+ });
19907
+ ChannelSlider.displayName = 'ChannelSlider';
19908
+ /**
19909
+ * The hue slider, from red round the spectrum and back to red.
19910
+ *
19911
+ * @function
19912
+ * @param {ColorPickerSliderProps} props - Props for ColorPicker.HueSlider.
19913
+ * @param {React.Ref<HTMLDivElement>} ref - Forwarded ref to the slider's `<div>`.
19914
+ * @returns {JSX.Element | null} The slider, or nothing outside a ColorPicker.
19915
+ */
19916
+ const ColorPickerHueSlider = React.forwardRef((props, ref) => jsxRuntime.jsx(ChannelSlider, { ref: ref, ...props, channel: "hue" }));
19917
+ ColorPickerHueSlider.displayName = 'ColorPicker.HueSlider';
19918
+ /**
19919
+ * The alpha slider, from see-through to solid.
19920
+ *
19921
+ * @function
19922
+ * @param {ColorPickerSliderProps} props - Props for ColorPicker.AlphaSlider.
19923
+ * @param {React.Ref<HTMLDivElement>} ref - Forwarded ref to the slider's `<div>`.
19924
+ * @returns {JSX.Element | null} The slider, or nothing outside a ColorPicker.
19925
+ */
19926
+ const ColorPickerAlphaSlider = React.forwardRef((props, ref) => jsxRuntime.jsx(ChannelSlider, { ref: ref, ...props, channel: "alpha" }));
19927
+ ColorPickerAlphaSlider.displayName = 'ColorPicker.AlphaSlider';
19928
+ /** Arrow keys and how far they move along the swatches. */
19929
+ const SWATCH_STEPS = {
19930
+ ArrowRight: 1,
19931
+ ArrowDown: 1,
19932
+ ArrowLeft: -1,
19933
+ ArrowUp: -1,
19934
+ };
19935
+ /**
19936
+ * The swatches, a radio group of preset colors that sets the picker's color.
19937
+ *
19938
+ * @function
19939
+ * @param {ColorPickerSwatchesProps} props - Props for ColorPicker.Swatches.
19940
+ * @param {React.Ref<HTMLDivElement>} ref - Forwarded ref to the group's `<div>`.
19941
+ * @returns {JSX.Element | null} The swatches, or nothing outside a ColorPicker or with none to show.
19942
+ */
19943
+ const ColorPickerSwatches = React.forwardRef(({ className, swatches: ownSwatches, ...props }, ref) => {
19944
+ const picker = usePicker('Swatches');
19945
+ const live = picker ?? standalone;
19946
+ const { color, change, disabled, labels, format, alpha } = live;
19947
+ const { bulmaHelperClasses, rest } = useBulmaClasses(props);
19948
+ const classPrefix = useClassPrefix();
19949
+ const groupClass = usePrefixedClassNames('color-picker-swatches');
19950
+ const rootRef = React.useRef(null);
19951
+ const given = ownSwatches ?? live.swatches;
19952
+ const list = React.useMemo(() => (given === 'bulma' ? BULMA_SWATCHES : (given ?? [])).map(entry => typeof entry === 'string' ? { color: entry } : entry), [given]);
19953
+ // A color the picker can't read as written, such as a var(), is read off
19954
+ // the page, as the text the swatch's own custom property resolves to.
19955
+ const chipsRef = React.useRef([]);
19956
+ const textOf = React.useCallback((i) => {
19957
+ const chip = chipsRef.current[i];
19958
+ if (parseColor(list[i].color) || !chip)
19959
+ return list[i].color;
19960
+ return getComputedStyle(chip)
19961
+ .getPropertyValue('--color-swatch-color')
19962
+ .trim();
19963
+ }, [list]);
19964
+ // Those texts, read after every render rather than once: a Theme can
19965
+ // change the variables behind them while the picker is open, and the
19966
+ // chosen swatch has to follow. A read that changes nothing renders
19967
+ // nothing more.
19968
+ const [resolved, setResolved] = React.useState([]);
19969
+ // eslint-disable-next-line react-hooks/exhaustive-deps -- after every render on purpose; the updater keeps the state when nothing changed, so it settles
19970
+ React.useEffect(() => {
19971
+ const next = list.map((_, i) => textOf(i));
19972
+ // eslint-disable-next-line react-hooks/set-state-in-effect -- a var() color can only be read off the page once its swatch is on it
19973
+ setResolved(previous => previous.length === next.length &&
19974
+ previous.every((text, i) => text === next[i])
19975
+ ? previous
19976
+ : next);
19977
+ });
19978
+ const colorOf = (i) => parseColor(list[i].color) ?? parseColor(resolved[i]);
19979
+ // The same color, as far as the picker's value can tell: the same 8-bit
19980
+ // color, or the same text, since an HSL value is rounded.
19981
+ const shownText = formatColor(color, format, alpha);
19982
+ const checked = list.findIndex((_, i) => {
19983
+ const swatch = colorOf(i);
19984
+ if (!swatch)
19985
+ return false;
19986
+ const solid = alpha ? swatch : { ...swatch, a: 1 };
19987
+ return (sameColor(solid, color) || formatColor(solid, format, alpha) === shownText);
19988
+ });
19989
+ const [focused, setFocused] = React.useState(null);
19990
+ const tabStop = focused ?? (checked >= 0 ? checked : 0);
19991
+ // Read afresh, so a theme that changed since the last render counts.
19992
+ const pick = (i) => {
19993
+ const text = textOf(i);
19994
+ const swatch = parseColor(text);
19995
+ if (swatch)
19996
+ change(keepingChannels(swatch, color, text), true);
19997
+ };
19998
+ const onKeyDown = (i) => (event) => {
19999
+ const buttons = Array.from(rootRef.current.querySelectorAll('[role="radio"]'));
20000
+ let next;
20001
+ if (event.key === 'Home')
20002
+ next = 0;
20003
+ else if (event.key === 'End')
20004
+ next = buttons.length - 1;
20005
+ else if (SWATCH_STEPS[event.key]) {
20006
+ // Left and Right follow the screen, so right to left they swap.
20007
+ const sideways = event.key === 'ArrowLeft' || event.key === 'ArrowRight';
20008
+ const flip = sideways && isRtl(rootRef.current) ? -1 : 1;
20009
+ next =
20010
+ (i + SWATCH_STEPS[event.key] * flip + buttons.length) % buttons.length;
20011
+ }
20012
+ if (next === undefined)
20013
+ return;
20014
+ event.preventDefault();
20015
+ buttons[next]?.focus();
20016
+ pick(next);
20017
+ };
20018
+ if (!picker || list.length === 0)
20019
+ return null;
20020
+ return (jsxRuntime.jsx("div", { ref: node => setRefs(node, rootRef, ref), className: classNames(groupClass, bulmaHelperClasses, className), role: "radiogroup", "aria-label": labels.swatches, "aria-disabled": disabled || undefined, ...rest, children: list.map((entry, i) => {
20021
+ const swatch = colorOf(i);
20022
+ const isChecked = i === checked;
20023
+ // The check is drawn in black or white with a ring of the other, so
20024
+ // it shows on any color. On a see-through swatch the page shows
20025
+ // through, so it takes the scheme's text color and background,
20026
+ // which follow dark mode, in place of the swatch's own.
20027
+ const seeThrough = !!swatch && swatch.a < 1;
20028
+ return (jsxRuntime.jsx("button", { type: "button", role: "radio", "aria-checked": isChecked, "aria-label": entry.label || entry.color, title: entry.label, tabIndex: i === tabStop ? 0 : -1, disabled: disabled, className: prefixedClassNames(classPrefix, 'color-picker-swatch', {
20029
+ 'is-checked': isChecked,
20030
+ 'is-see-through': seeThrough,
20031
+ 'is-dark-ink': !!swatch && !seeThrough && prefersDarkInk(swatch),
20032
+ }), onClick: () => pick(i), onKeyDown: onKeyDown(i), onFocus: () => setFocused(i), onBlur: () => setFocused(null), children: jsxRuntime.jsx(ColorSwatch, { ref: node => {
20033
+ chipsRef.current[i] = node;
20034
+ }, color: entry.color, "aria-hidden": "true", children: isChecked && (jsxRuntime.jsx(CheckIcon, { haloClass: prefixedClassNames(classPrefix, 'color-picker-check-halo') })) }) }, `${entry.color}-${i}`));
20035
+ }) }));
20036
+ });
20037
+ ColorPickerSwatches.displayName = 'ColorPicker.Swatches';
20038
+ /**
20039
+ * The check on the chosen swatch: a ring in `haloClass`, and the check over
20040
+ * it in the current color.
20041
+ */
20042
+ const CheckIcon = ({ haloClass }) => (jsxRuntime.jsx("svg", { viewBox: "0 0 16 16", width: "1em", height: "1em", "aria-hidden": "true", children: ['halo', 'ink'].map(part => (jsxRuntime.jsx("path", { className: part === 'halo' ? haloClass : undefined, d: "M3.5 8.5l3 3 6-7", fill: "none", stroke: "currentColor", strokeWidth: part === 'halo' ? 4.5 : 2, strokeLinecap: "round", strokeLinejoin: "round" }, part))) }));
20043
+ /**
20044
+ * The value as text you can type in, beside a preview swatch, with the eye dropper when the picker has `eyeDropper`.
20045
+ *
20046
+ * @function
20047
+ * @param {ColorPickerInputProps} props - Props for ColorPicker.Input.
20048
+ * @param {React.Ref<HTMLDivElement>} ref - Forwarded ref to the row's `<div>`.
20049
+ * @returns {JSX.Element | null} The row, or nothing outside a ColorPicker.
20050
+ */
20051
+ const ColorPickerInput = React.forwardRef(({ className, ...props }, ref) => {
20052
+ const picker = usePicker('Input');
20053
+ const live = picker ?? standalone;
20054
+ const { color, text, change, disabled, labels, eyeDropper } = live;
20055
+ const { bulmaHelperClasses, rest } = useBulmaClasses(props);
20056
+ const rowClass = usePrefixedClassNames('color-picker-value');
20057
+ const previewClass = usePrefixedClassNames('color-picker-preview');
20058
+ // What's been typed and not yet committed, or null while the field
20059
+ // shows the value.
20060
+ const [draft, setDraft] = React.useState(null);
20061
+ const invalid = draft !== null && !parseColor(draft);
20062
+ const inputClass = usePrefixedClassNames('input', {
20063
+ 'is-small': true,
20064
+ 'is-danger': invalid,
20065
+ });
20066
+ const buttonClass = usePrefixedClassNames('button', { 'is-small': true });
20067
+ const hydrated = useIsHydrated();
20068
+ const EyeDropper = hydrated
20069
+ ? window.EyeDropper
20070
+ : undefined;
20071
+ const commit = () => {
20072
+ if (draft === null)
17816
20073
  return;
20074
+ const parsed = parseColor(draft);
20075
+ setDraft(null);
20076
+ if (parsed)
20077
+ change(keepingChannels(parsed, color, draft), true);
20078
+ };
20079
+ const onKeyDown = (event) => {
20080
+ if (draft === null)
20081
+ return;
20082
+ if (event.key === 'Enter') {
20083
+ // Commits rather than submitting a form; an unreadable value stays
20084
+ // to be fixed.
20085
+ event.preventDefault();
20086
+ if (!invalid)
20087
+ commit();
17817
20088
  }
17818
- switch (e.key) {
17819
- case 'ArrowDown':
17820
- e.preventDefault();
17821
- if (!isActive && data.length > 0) {
17822
- setIsActive(true);
17823
- }
17824
- else {
17825
- setHighlightedIndex(prev => prev < filteredData.length - 1 ? prev + 1 : prev);
17826
- }
17827
- break;
17828
- case 'ArrowUp':
17829
- e.preventDefault();
17830
- setHighlightedIndex(prev => (prev > 0 ? prev - 1 : prev));
17831
- break;
17832
- case 'Backspace':
17833
- if (removeOnKeys && !inputValue && tags.length > 0) {
17834
- e.preventDefault();
17835
- removeTag(tags.length - 1);
17836
- }
17837
- break;
17838
- case 'Escape':
17839
- // Closing the suggestions claims the key, so a Modal around the
17840
- // field stays open. With none showing, the key goes on to it.
17841
- if (listOpen)
17842
- e.preventDefault();
17843
- setIsActive(false);
17844
- setHighlightedIndex(-1);
17845
- break;
17846
- }
17847
- }, [
17848
- disabled,
17849
- readonly,
17850
- confirmKeys,
17851
- highlightedIndex,
17852
- filteredData,
17853
- addTags,
17854
- inputValue,
17855
- isActive,
17856
- listOpen,
17857
- data.length,
17858
- removeOnKeys,
17859
- tags.length,
17860
- removeTag,
17861
- ]);
17862
- // Combined ref
17863
- const combinedRef = React.useCallback((node) => {
17864
- inputRef.current =
17865
- node;
17866
- if (typeof ref === 'function') {
17867
- ref(node);
17868
- }
17869
- else if (ref) {
17870
- ref.current =
17871
- node;
20089
+ else if (event.key === 'Escape') {
20090
+ // Puts the value back, and keeps the key from closing a popover too.
20091
+ event.preventDefault();
20092
+ event.stopPropagation();
20093
+ setDraft(null);
17872
20094
  }
17873
- }, [ref]);
17874
- // Focus input when container is clicked
17875
- const handleContainerClick = React.useCallback(() => {
17876
- inputRef.current?.focus();
17877
- }, []);
17878
- // Generate classes
17879
- const taginputClasses = pcn('taginput', {
17880
- 'is-active': isActive,
17881
- [`is-${size}`]: !!size,
17882
- [`is-${color}`]: !!color,
17883
- 'is-disabled': disabled,
17884
- 'is-rounded': rounded,
17885
- 'is-ellipsis': ellipsis,
17886
- });
17887
- const containerClasses = pcn('taginput-container', {
17888
- 'is-attached': attached,
17889
- 'has-icons-left': !!icon,
20095
+ };
20096
+ // Escape, or a click away, cancels it, which rejects.
20097
+ const pickFromScreen = (Dropper) => new Dropper().open().then(({ sRGBHex }) => {
20098
+ const parsed = parseColor(sRGBHex);
20099
+ // The screen gives a solid color as hex; the alpha stays as it was,
20100
+ // and a gray keeps the hue the picker had.
20101
+ if (parsed) {
20102
+ change(keepingChannels({ ...parsed, a: color.a }, color, sRGBHex), true);
20103
+ }
20104
+ }, noop);
20105
+ if (!picker)
20106
+ return null;
20107
+ return (jsxRuntime.jsxs("div", { ref: ref, className: classNames(rowClass, bulmaHelperClasses, className), ...rest, children: [jsxRuntime.jsx(ColorSwatch, { color: text, className: previewClass, "aria-hidden": "true" }), jsxRuntime.jsx("input", { type: "text", className: inputClass, value: draft ?? text, disabled: disabled, "aria-label": labels.value, "aria-invalid": invalid || undefined, spellCheck: false, autoComplete: "off", autoCapitalize: "off", onChange: event => setDraft(event.target.value), onKeyDown: onKeyDown, onBlur: commit }), eyeDropper && EyeDropper && (jsxRuntime.jsx("button", { type: "button", className: buttonClass, "aria-label": labels.eyeDropper, title: labels.eyeDropper, disabled: disabled, onClick: () => pickFromScreen(EyeDropper), children: jsxRuntime.jsx(EyeDropperIcon, {}) }))] }));
20108
+ });
20109
+ ColorPickerInput.displayName = 'ColorPicker.Input';
20110
+ /** A pipette. */
20111
+ const EyeDropperIcon = () => (jsxRuntime.jsx("svg", { viewBox: "0 0 16 16", width: "1em", height: "1em", "aria-hidden": "true", children: jsxRuntime.jsx("path", { d: "M10.5 2.5l3 3M9 4l3 3-6.5 6.5H3v-3z", fill: "none", stroke: "currentColor", strokeWidth: "1.5", strokeLinejoin: "round" }) }));
20112
+ /**
20113
+ * The `ColorPicker` component is a panel for choosing a color: a saturation and brightness square, a hue slider, and the value as text, with alpha and swatches when asked for.
20114
+ *
20115
+ * Every part is operable by keyboard, and the square is two native range
20116
+ * inputs, one per axis, with one tab stop. The value is a CSS color string
20117
+ * in `format`, HSL by default.
20118
+ *
20119
+ * @function
20120
+ * @param {ColorPickerProps} props - Props for the ColorPicker component.
20121
+ * @param {React.Ref<HTMLDivElement>} ref - Forwarded ref to the picker's `<div>`.
20122
+ * @returns {JSX.Element} The rendered picker.
20123
+ *
20124
+ * @example
20125
+ * <ColorPicker label="Brand color" defaultValue="#1e6b99" swatches="bulma" />
20126
+ */
20127
+ const ColorPickerRoot = React.forwardRef(({ className, label, labelSize, labelProps, horizontal, message, messageColor, fieldClassName, value, defaultValue, onChange, onChangeEnd, format = 'hsl', alpha = false, swatches, eyeDropper = false, disabled: disabledProp, name, form, labels, children, ...props }, ref) => {
20128
+ const disabled = useFieldsetDisabled(disabledProp);
20129
+ const insideField = useInsideField();
20130
+ const insideControl = useInsideControl();
20131
+ const ownField = rendersOwnField('ColorPicker', {
20132
+ insideField,
20133
+ insideControl,
20134
+ label,
20135
+ message,
20136
+ horizontal,
20137
+ fieldClassName,
17890
20138
  });
17891
- const inputClasses = pcn('input', {
17892
- [`is-${size}`]: !!size,
20139
+ const { ariaLabelledBy, fieldLabelProps } = useAutoLabelledBy({
20140
+ label,
20141
+ labelProps,
20142
+ rendersLabel: ownField,
20143
+ callerProps: props,
17893
20144
  });
17894
- const tagClasses = pcn('tag', {
17895
- [`is-${tagColor}`]: !!tagColor,
17896
- [`is-${size}`]: !!size,
20145
+ const { bulmaHelperClasses, rest } = useBulmaClasses(props);
20146
+ const pickerClass = usePrefixedClassNames('color-picker', {
20147
+ 'is-disabled': disabled,
17897
20148
  });
17898
- const combinedClasses = classNames(taginputClasses, bulmaHelperClasses, className);
17899
- // Counter text
17900
- const showCounter = hasCounter && (maxTags !== undefined || maxlength !== undefined);
17901
- const write = formatNumber ?? String;
17902
- const counterText = maxTags !== undefined
17903
- ? `${write(tags.length)} / ${write(maxTags)}`
17904
- : maxlength !== undefined
17905
- ? `${write(inputValue.length)} / ${write(maxlength)}`
17906
- : '';
17907
- const counterClasses = pcn('help', 'counter');
17908
- const tagsClasses = pcn('tags');
17909
- const dropdownMenuClasses = pcn('dropdown-menu', 'is-active');
17910
- const dropdownContentClasses = pcn('dropdown-content');
17911
- const iconRightClass = pcn('icon', 'is-right');
17912
- const loaderClass = pcn('loader', 'is-loading');
17913
20149
  const helpClass = usePrefixedClassNames('help', {
17914
20150
  [`is-${messageColor}`]: !!messageColor,
17915
20151
  });
20152
+ const context = useColorPicker({
20153
+ value,
20154
+ defaultValue,
20155
+ onChange,
20156
+ onChangeEnd,
20157
+ format,
20158
+ alpha,
20159
+ disabled,
20160
+ swatches,
20161
+ eyeDropper,
20162
+ labels,
20163
+ });
20164
+ const picker = (jsxRuntime.jsxs("div", { ref: ref, className: classNames(pickerClass, bulmaHelperClasses, className), role: "group", "aria-labelledby": ariaLabelledBy, ...rest, children: [jsxRuntime.jsx(ColorPickerContext.Provider, { value: context, children: children ?? (jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [jsxRuntime.jsx(ColorPickerArea, {}), jsxRuntime.jsx(ColorPickerHueSlider, {}), alpha && jsxRuntime.jsx(ColorPickerAlphaSlider, {}), jsxRuntime.jsx(ColorPickerInput, {}), swatches && jsxRuntime.jsx(ColorPickerSwatches, {})] })) }), name && (jsxRuntime.jsx("input", { type: "hidden", name: name, form: form, value: context.text, disabled: disabled }))] }));
20165
+ let content = picker;
20166
+ if (rendersOwnControl('ColorPicker', { insideControl })) {
20167
+ content = jsxRuntime.jsx(Control, { children: content });
20168
+ }
17916
20169
  const messageEl = message ? jsxRuntime.jsx("p", { className: helpClass, children: message }) : null;
17917
- // The combobox controls its listbox and points at the highlighted option,
17918
- // the way Autocomplete's does. The listbox takes its name from the label
17919
- // that names the combobox, the one this renders or a surrounding Field's,
17920
- // and keeps a fallback name when no label names it or the one that does
17921
- // has no id to point at. While no suggestion matches there's no listbox,
17922
- // so the combobox is collapsed and points at nothing. It skips a
17923
- // highlighted suggestion that's blank once trimmed, since that one can't
17924
- // become a tag, and the option it points at is the one marked selected.
17925
- const optionId = (index) => `${listboxId}-${index}`;
17926
- const activeOptionId = listOpen && filteredData[highlightedIndex]?.trim()
17927
- ? optionId(highlightedIndex)
17928
- : undefined;
17929
- const comboboxProps = {
17930
- role: 'combobox',
17931
- 'aria-expanded': listOpen,
17932
- 'aria-controls': listOpen ? listboxId : undefined,
17933
- 'aria-activedescendant': activeOptionId,
17934
- 'aria-haspopup': 'listbox',
17935
- 'aria-autocomplete': 'list',
17936
- };
17937
- const taginputElement = (jsxRuntime.jsxs("div", { ref: containerRef, className: combinedClasses, onClick: handleContainerClick, ...rest, children: [jsxRuntime.jsxs("div", { className: containerClasses, children: [icon && (jsxRuntime.jsx(Icon, { name: icon, library: resolvedLibrary, variant: iconVariant, features: iconFeatures, containerClassName: pcn('icon', 'is-left') })), jsxRuntime.jsxs("div", { className: tagsClasses, children: [tags.map((tag, index) => {
17938
- const displayVal = getDisplayValue(tag);
17939
- return (jsxRuntime.jsxs("span", { className: tagClasses, children: [tagTemplate ? (tagTemplate(tag)) : ellipsis ? (jsxRuntime.jsx("span", { title: displayVal, children: displayVal })) : (displayVal), closable && !disabled && !readonly && (jsxRuntime.jsx("button", { type: "button", className: pcn('delete', 'is-small'), onClick: e => {
17940
- e.stopPropagation();
17941
- removeTag(index);
17942
- }, "aria-label": ariaCloseLabel || `Remove ${displayVal}` }))] }, index));
17943
- }), jsxRuntime.jsx("input", { ref: combinedRef, type: "text", className: inputClasses, id: controlId, value: inputValue, placeholder: tags.length === 0 ? placeholder : undefined, disabled: disabled,
17944
- // Read-only at the tag limit rather than gone, so focus stays
17945
- // put and a label keeps naming it, and Backspace still makes
17946
- // room (#1103).
17947
- readOnly: readonly || isMaxReached, maxLength: maxlength, onChange: handleInputChange, onFocus: handleFocus, onKeyDown: handleKeyDown, onPaste: handlePaste, ...(hasSuggestions && comboboxProps), "aria-label": labelled ? undefined : 'Add tag', "aria-describedby": showCounter ? counterId : undefined })] }), loading && (jsxRuntime.jsx("span", { className: iconRightClass, children: jsxRuntime.jsx("span", { className: loaderClass }) }))] }), listOpen && (jsxRuntime.jsx("div", { className: dropdownMenuClasses, children: jsxRuntime.jsx("div", { ref: dropdownRef, id: listboxId, className: dropdownContentClasses, role: "listbox", "aria-labelledby": listLabelId, "aria-label": listLabelId ? undefined : 'Suggestions', children: filteredData.map((item, index) => (jsxRuntime.jsx("a", { id: optionId(index), className: pcn('dropdown-item', {
17948
- 'is-active': index === highlightedIndex,
17949
- }), onClick: () => addTags([item]), onMouseEnter: () => setHighlightedIndex(index), role: "option", "aria-selected": optionId(index) === activeOptionId, children: item }, index))) }) })), showCounter && (jsxRuntime.jsx("small", { id: counterId, className: counterClasses, children: counterText })), name &&
17950
- tags.map((tag, i) => (jsxRuntime.jsx("input", { type: "hidden", name: name, value: getDisplayValue(tag), form: form }, `tag-input-${i}`)))] }));
17951
20170
  if (ownField) {
17952
- return (jsxRuntime.jsxs(Field, { label: label, labelSize: labelSize, labelProps: fieldLabelProps, horizontal: horizontal, className: fieldClassName, children: [taginputElement, messageEl] }));
20171
+ return (jsxRuntime.jsxs(Field, { label: label, labelSize: labelSize, labelProps: fieldLabelProps, horizontal: horizontal, className: fieldClassName, children: [content, messageEl] }));
17953
20172
  }
17954
- return (jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [taginputElement, messageEl] }));
20173
+ return (jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [content, messageEl] }));
20174
+ });
20175
+ ColorPickerRoot.displayName = 'ColorPicker';
20176
+ const ColorPicker = withSubComponents(ColorPickerRoot, {
20177
+ Area: ColorPickerArea,
20178
+ HueSlider: ColorPickerHueSlider,
20179
+ AlphaSlider: ColorPickerAlphaSlider,
20180
+ Swatches: ColorPickerSwatches,
20181
+ Input: ColorPickerInput,
17955
20182
  });
17956
- Taginput.displayName = 'Taginput';
17957
- /** @deprecated Use `Taginput` — same component. */
17958
- const TagInput = Taginput;
17959
20183
 
17960
20184
  /**
17961
20185
  * Bulma Select component with full Bulma helper class support.
@@ -19666,50 +21890,6 @@ const PickerPopover = ({ isOpen, onClose, anchorRef, children, position = 'botto
19666
21890
  };
19667
21891
  PickerPopover.displayName = 'PickerPopover';
19668
21892
 
19669
- function useNativeMobilePicker(options = {}) {
19670
- const { smallViewportMaxPx = 768, force } = options;
19671
- const [state, setState] = React.useState({
19672
- isCoarsePointer: false,
19673
- isSmallViewport: false,
19674
- shouldUseNative: force === true,
19675
- });
19676
- React.useEffect(() => {
19677
- const isBrowser = typeof window !== 'undefined' && typeof window.matchMedia === 'function';
19678
- if (!isBrowser)
19679
- return undefined;
19680
- const coarse = window.matchMedia('(pointer: coarse)');
19681
- const small = window.matchMedia(`(max-width: ${smallViewportMaxPx}px)`);
19682
- const update = () => {
19683
- const isCoarsePointer = coarse.matches;
19684
- const isSmallViewport = small.matches;
19685
- const shouldUseNative = force === true
19686
- ? true
19687
- : force === false
19688
- ? false
19689
- : isCoarsePointer && isSmallViewport;
19690
- setState({ isCoarsePointer, isSmallViewport, shouldUseNative });
19691
- };
19692
- update();
19693
- const subscribe = (mql, fn) => {
19694
- if (mql.addEventListener) {
19695
- // eslint-disable-next-line @eslint-react/web-api-no-leaked-event-listener -- cleanup returned below and run in the effect teardown
19696
- mql.addEventListener('change', fn);
19697
- return () => mql.removeEventListener('change', fn);
19698
- }
19699
- // Older Safari fallback
19700
- mql.addListener(fn);
19701
- return () => mql.removeListener(fn);
19702
- };
19703
- const unsubCoarse = subscribe(coarse, update);
19704
- const unsubSmall = subscribe(small, update);
19705
- return () => {
19706
- unsubCoarse();
19707
- unsubSmall();
19708
- };
19709
- }, [smallViewportMaxPx, force]);
19710
- return state;
19711
- }
19712
-
19713
21893
  /**
19714
21894
  * Segment-map utility for manual keyboard entry on a date / time picker input.
19715
21895
  * Given a token-format string like `'YYYY-MM-DD'`, `'HH:mm:ss'`, `'hh:mm A'`,
@@ -24936,6 +27116,13 @@ exports.ClientOnly = ClientOnly;
24936
27116
  exports.Code = Code;
24937
27117
  exports.Collapse = Collapse;
24938
27118
  exports.Collapses = Collapses;
27119
+ exports.ColorPicker = ColorPicker;
27120
+ exports.ColorPickerAlphaSlider = ColorPickerAlphaSlider;
27121
+ exports.ColorPickerArea = ColorPickerArea;
27122
+ exports.ColorPickerHueSlider = ColorPickerHueSlider;
27123
+ exports.ColorPickerInput = ColorPickerInput;
27124
+ exports.ColorPickerSwatches = ColorPickerSwatches;
27125
+ exports.ColorSwatch = ColorSwatch;
24939
27126
  exports.Column = Column;
24940
27127
  exports.Columns = Columns;
24941
27128
  exports.Combobox = Combobox;
@@ -24957,6 +27144,7 @@ exports.Container = Container;
24957
27144
  exports.Content = Content;
24958
27145
  exports.ContextMenu = ContextMenu;
24959
27146
  exports.Control = Control;
27147
+ exports.DataTable = DataTable;
24960
27148
  exports.DateInput = DateInput;
24961
27149
  exports.DateInputBase = DateInputBase;
24962
27150
  exports.DateRangeInput = DateRangeInput;
@@ -24974,6 +27162,7 @@ exports.Divider = Divider;
24974
27162
  exports.Dropdown = Dropdown;
24975
27163
  exports.DropdownDivider = DropdownDivider;
24976
27164
  exports.DropdownItem = DropdownItem;
27165
+ exports.Dropzone = Dropzone;
24977
27166
  exports.Emphasis = Emphasis;
24978
27167
  exports.EmptyState = EmptyState;
24979
27168
  exports.EmptyStateActions = EmptyStateActions;
@@ -25134,6 +27323,7 @@ exports.useClassPrefix = useClassPrefix;
25134
27323
  exports.useColorClasses = useColorClasses;
25135
27324
  exports.useColorStyles = useColorStyles;
25136
27325
  exports.useConfig = useConfig;
27326
+ exports.useDataTable = useDataTable;
25137
27327
  exports.useFlexboxClasses = useFlexboxClasses;
25138
27328
  exports.useFocusTrap = useFocusTrap;
25139
27329
  exports.useIconLibrary = useIconLibrary;