@allxsmith/bestax-bulma 5.18.0 → 5.18.2

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 (48) hide show
  1. package/dist/bestax.css +1 -1
  2. package/dist/bestax.css.map +1 -1
  3. package/dist/extras.css +1 -1
  4. package/dist/extras.css.map +1 -1
  5. package/dist/index.cjs +551 -124
  6. package/dist/index.cjs.map +1 -1
  7. package/dist/index.esm.js +551 -124
  8. package/dist/index.esm.js.map +1 -1
  9. package/dist/types/components/Avatar.d.ts +17 -3
  10. package/dist/types/components/Dropdown.d.ts +7 -2
  11. package/dist/types/components/Menu.d.ts +6 -1
  12. package/dist/types/components/Toast.d.ts +15 -4
  13. package/dist/types/elements/Notification.d.ts +16 -6
  14. package/dist/types/form/Checkbox.d.ts +26 -2
  15. package/dist/types/form/DateTimeInputBase.d.ts +6 -1
  16. package/dist/types/form/Radio.d.ts +28 -2
  17. package/dist/types/form/Switch.d.ts +26 -2
  18. package/dist/types/form/TimeInputBase.d.ts +5 -1
  19. package/dist/types/helpers/Theme.d.ts +27 -1
  20. package/dist/types/helpers/buttonType.d.ts +19 -0
  21. package/dist/types/helpers/colorDeprecations.d.ts +4 -1
  22. package/dist/types/helpers/devWarnings.d.ts +8 -0
  23. package/dist/types/helpers/positionStacks.d.ts +33 -0
  24. package/dist/types-cjs/components/Avatar.d.ts +17 -3
  25. package/dist/types-cjs/components/Dropdown.d.ts +7 -2
  26. package/dist/types-cjs/components/Menu.d.ts +6 -1
  27. package/dist/types-cjs/components/Toast.d.ts +15 -4
  28. package/dist/types-cjs/elements/Notification.d.ts +16 -6
  29. package/dist/types-cjs/form/Checkbox.d.ts +26 -2
  30. package/dist/types-cjs/form/DateTimeInputBase.d.ts +6 -1
  31. package/dist/types-cjs/form/Radio.d.ts +28 -2
  32. package/dist/types-cjs/form/Switch.d.ts +26 -2
  33. package/dist/types-cjs/form/TimeInputBase.d.ts +5 -1
  34. package/dist/types-cjs/helpers/Theme.d.ts +27 -1
  35. package/dist/types-cjs/helpers/buttonType.d.ts +19 -0
  36. package/dist/types-cjs/helpers/colorDeprecations.d.ts +4 -1
  37. package/dist/types-cjs/helpers/devWarnings.d.ts +8 -0
  38. package/dist/types-cjs/helpers/positionStacks.d.ts +33 -0
  39. package/dist/versions/bestax-no-dark-mode.css +1 -1
  40. package/dist/versions/bestax-no-dark-mode.css.map +1 -1
  41. package/dist/versions/bestax-no-helpers-prefixed.css +1 -1
  42. package/dist/versions/bestax-no-helpers-prefixed.css.map +1 -1
  43. package/dist/versions/bestax-no-helpers.css +1 -1
  44. package/dist/versions/bestax-no-helpers.css.map +1 -1
  45. package/dist/versions/bestax-prefixed.css +1 -1
  46. package/dist/versions/bestax-prefixed.css.map +1 -1
  47. package/package.json +1 -1
  48. package/src/scss/form/_timeinput.scss +42 -14
package/dist/index.cjs CHANGED
@@ -1237,6 +1237,27 @@ const ColumnsComponent = ({ className, textColor, color: _fieldColor, bgColor, i
1237
1237
  };
1238
1238
  const Columns = withSubComponents(ColumnsComponent, { Column }, 'Columns');
1239
1239
 
1240
+ /**
1241
+ * The `type` to render on a `<button>` a component defaults to `button`: the
1242
+ * caller's own value when it is `button`, `submit` or `reset`, and `button`
1243
+ * otherwise.
1244
+ *
1245
+ * A missing `type` is not the only way a button ends up submitting the form
1246
+ * around it. HTML reads an invalid value, such as the anchor's MIME-type
1247
+ * `type="text/html"`, as submit too, and a props spread carrying
1248
+ * `type: undefined` removes the attribute outright. Both can arrive through a
1249
+ * spread or an untyped caller the component's own types never see, so this
1250
+ * checks the value rather than trusting where it came from.
1251
+ *
1252
+ * Call it AFTER the forwarded props are spread and pass it the forwarded
1253
+ * `type`, so its result is the attribute that renders.
1254
+ */
1255
+ function buttonType(given) {
1256
+ return given === 'submit' || given === 'reset' || given === 'button'
1257
+ ? given
1258
+ : 'button';
1259
+ }
1260
+
1240
1261
  /**
1241
1262
  * Whether an `as` target is a custom element rather than a built-in tag.
1242
1263
  *
@@ -1252,6 +1273,30 @@ function isCustomElement(as) {
1252
1273
  return typeof as === 'string' && as.includes('-');
1253
1274
  }
1254
1275
 
1276
+ // INTERNAL: deliberately not exported from src/index.ts.
1277
+ const warnedKeys = new Set();
1278
+ // Fail closed: with no bundler and no Node (raw CDN ESM), reading `process`
1279
+ // throws and warnings stay off, so production can never warn by accident.
1280
+ const isDev = () => {
1281
+ try {
1282
+ return process.env.NODE_ENV !== 'production';
1283
+ }
1284
+ catch {
1285
+ return false;
1286
+ }
1287
+ };
1288
+ /**
1289
+ * Logs a console warning in development, once per `key` for the life of the
1290
+ * page. Safe to call during render: a re-render, or a second instance hitting
1291
+ * the same case, finds the key already recorded and stays quiet.
1292
+ */
1293
+ const warnOnce = (key, message) => {
1294
+ if (!isDev() || warnedKeys.has(key))
1295
+ return;
1296
+ warnedKeys.add(key);
1297
+ console.warn(message);
1298
+ };
1299
+
1255
1300
  const avatarColors = [
1256
1301
  'primary',
1257
1302
  'link',
@@ -1297,6 +1342,18 @@ const NON_INTERACTIVE_ROLES = [
1297
1342
  'presentation',
1298
1343
  'none',
1299
1344
  ];
1345
+ /**
1346
+ * Elements other than `a` that declare `target` themselves, so on them it is
1347
+ * the element's own attribute and not a link attribute Avatar withholds for
1348
+ * want of one. The link attribute warning leaves `target` out on these, the way
1349
+ * it leaves `rel` out everywhere. `area` and `base` are void elements, which
1350
+ * Avatar's content rules out, so `form` is the one a caller can reach.
1351
+ */
1352
+ const ELEMENTS_WITH_OWN_TARGET = [
1353
+ 'form',
1354
+ 'area',
1355
+ 'base',
1356
+ ];
1300
1357
  /**
1301
1358
  * Derives a small set of initials from a name (e.g. "Ada Lovelace" -> "AL").
1302
1359
  */
@@ -1432,6 +1489,50 @@ const Avatar = React.forwardRef(function Avatar(avatarProps, ref) {
1432
1489
  // Only forward link attributes when rendering an anchor or a custom (non-DOM)
1433
1490
  // component; a plain `as="div"` must not receive a stray `href`/`target`/`rel`.
1434
1491
  const isLinkLike = Tag === 'a' || typeof Tag !== 'string' || isCustomElement(Tag);
1492
+ // A plain element like a `div` declares no `href` or `target`, so the type
1493
+ // accepts them as Avatar's own while the drop below withholds them, and
1494
+ // without this they would vanish in silence (#733). Forwarding them would put
1495
+ // them where HTML has no such attribute, so the drop stays and development
1496
+ // reports it instead, naming only the ones actually passed.
1497
+ //
1498
+ // `rel` is withheld too but left out here. React declares it on every
1499
+ // element, so on a plain `as` it is that element's own attribute, and
1500
+ // "render it as a link" would be the wrong advice for it. `target` is the
1501
+ // same on an element that declares it, such as `form`, so it is left out
1502
+ // there. Whether to forward either one to such an element is a separate
1503
+ // question from this warning.
1504
+ //
1505
+ // Only for an `as` the caller wrote. Without one the element is Avatar's own
1506
+ // choice, and a message naming an `as` they never passed sends them looking
1507
+ // for it. A value counts when it is truthy, the same test that picks `'a'`
1508
+ // over `'figure'`, so an empty `href` from data asks for no link and draws no
1509
+ // warning. The key is the element plus the attributes passed, so a re-render
1510
+ // or a list of avatars warns once, while a different combination on the same
1511
+ // element still gets its own warning rather than hiding behind the first.
1512
+ //
1513
+ // Development-only is `warnOnce`'s job, as it is for the colour warnings, so
1514
+ // this block has no production check of its own. One here would have to read
1515
+ // `process` safely, and a `typeof process` test is not something a bundler
1516
+ // replaces: in a browser it is false, and the warning never fired in the
1517
+ // development builds it exists for. The cost is that production still
1518
+ // collects the attributes and builds the message for an avatar that needs
1519
+ // it, which is small and limited to the case the warning is about.
1520
+ if (as != null && !isLinkLike) {
1521
+ const dropped = Object.entries({
1522
+ href,
1523
+ target: ELEMENTS_WITH_OWN_TARGET.includes(as) ? undefined : target,
1524
+ })
1525
+ .filter(([, value]) => value)
1526
+ .map(([key]) => key);
1527
+ if (dropped.length > 0) {
1528
+ warnOnce(`Avatar:link-props-on-${as}:${dropped.join('+')}`, `[bestax-bulma] <Avatar as="${as}" ${dropped.join(' ')}>: this ` +
1529
+ `<${as}> renders without ` +
1530
+ `${dropped.map(key => `"${key}"`).join(' and ')}, because Avatar ` +
1531
+ `passes link attributes on only to a target that can be a link (an ` +
1532
+ `"a", a custom element, or a component). To make it a link, render ` +
1533
+ `it as="a" or pass a link component to "as".`);
1534
+ }
1535
+ }
1435
1536
  // Present-only, not `{ href, target, rel }`. An unconditional spread hands the
1436
1537
  // target these keys whatever the caller passed, and a key existing is not free:
1437
1538
  // a target that tests for one sees a link where there is none, and one that
@@ -1454,7 +1555,7 @@ const Avatar = React.forwardRef(function Avatar(avatarProps, ref) {
1454
1555
  // The decorative opt-out never applies to an interactive avatar — a link or
1455
1556
  // button must always expose an accessible name.
1456
1557
  const isDecorative = alt === '' && !isInteractive;
1457
- const a11yProps = showImage
1558
+ const a11yDefaults = showImage
1458
1559
  ? isInteractive && !accessibleName
1459
1560
  ? // The img alt normally names the control; with no alt/name (an API
1460
1561
  // returning only a photo URL) the link/button would be nameless.
@@ -1466,10 +1567,18 @@ const Avatar = React.forwardRef(function Avatar(avatarProps, ref) {
1466
1567
  ...(isInteractive ? {} : { role: 'img' }),
1467
1568
  'aria-label': accessibleName || name || 'Avatar',
1468
1569
  };
1469
- // A clickable avatar inside a form must not submit it; default the native
1470
- // button type (an explicit type passed through rest still wins).
1471
- const buttonTypeProps = Tag === 'button' ? { type: 'button' } : {};
1472
- return (jsxRuntime.jsxs(Tag, { ref: ref, className: combinedClasses, style: { ...sizeStyle, ...style }, ...buttonTypeProps, ...linkProps, ...a11yProps, ...rest, children: [showImage && (jsxRuntime.jsx("img", { ...imageProps, ref: imgRef, src: src, alt: accessibleName ?? '', onError: e => {
1570
+ // Each default yields to a value the caller passed, and is applied after
1571
+ // `rest` reading through it rather than spread before it. React treats an
1572
+ // `undefined` attribute as "remove it", and a spread carrying the key with no
1573
+ // value is how that arrives, so a default spread first was erased by it: an
1574
+ // `aria-label: undefined` left a button avatar nameless, and `role` and
1575
+ // `aria-hidden` went the same way. The button `type` below has the same
1576
+ // shape (#690).
1577
+ const a11yProps = Object.fromEntries(Object.entries(a11yDefaults).map(([key, fallback]) => [
1578
+ key,
1579
+ rest[key] ?? fallback,
1580
+ ]));
1581
+ return (jsxRuntime.jsxs(Tag, { ref: ref, className: combinedClasses, style: { ...sizeStyle, ...style }, ...linkProps, ...rest, ...a11yProps, ...(Tag === 'button' ? { type: buttonType(rest.type) } : {}), children: [showImage && (jsxRuntime.jsx("img", { ...imageProps, ref: imgRef, src: src, alt: accessibleName ?? '', onError: e => {
1473
1582
  imageProps?.onError?.(e);
1474
1583
  setErroredSrc(src);
1475
1584
  } }, src)), showInitials && (jsxRuntime.jsx("span", { className: initialsClass, children: resolvedInitials })), showIcon && icon, showDefaultIcon && jsxRuntime.jsx(DefaultAvatarIcon, {})] }));
@@ -1822,12 +1931,12 @@ const CardComponent = ({ className, children, textColor, color, bgColor, hasShad
1822
1931
  * The set withheld from a non-anchor `Card.FooterItem` (`span`/`button`): the
1823
1932
  * derived anchor-only attributes, minus `type` (also valid on a `<button>` as
1824
1933
  * `submit`/`button`/`reset`, so stripping it there would remove a working
1825
- * attribute — the same trade `DropdownItem`'s own `STRIP_FROM_NON_ANCHOR`
1826
- * accepts, in `./Dropdown.tsx`), plus `rel` (React declares it on
1827
- * `HTMLAttributes` for every element, so the derived set alone would not
1828
- * withhold it — the same addition `Level.Item` makes).
1934
+ * attribute; one set for both tags means a `type` also reaches a `<span>`,
1935
+ * which `Dropdown.Item` avoids by choosing its set per tag), plus `rel` (React
1936
+ * declares it on `HTMLAttributes` for every element, so the derived set alone
1937
+ * would not withhold it — the same addition `Level.Item` makes).
1829
1938
  */
1830
- const STRIP_FROM_NON_ANCHOR$2 = (() => {
1939
+ const STRIP_FROM_NON_ANCHOR$1 = (() => {
1831
1940
  const { type: _type, ...rest } = ANCHOR_ONLY_ATTRS;
1832
1941
  return { ...rest, rel: true };
1833
1942
  })();
@@ -1967,9 +2076,8 @@ const CardFooterItem = ({ as = 'span', className, children, color, bgColor, text
1967
2076
  if (as === 'a') {
1968
2077
  return (jsxRuntime.jsx("a", { className: itemClasses, ...omitAttrs(rest, STRIP_FROM_NON_BUTTON), children: children }));
1969
2078
  }
1970
- const forwarded = omitAttrs(rest, STRIP_FROM_NON_ANCHOR$2);
2079
+ const forwarded = omitAttrs(rest, STRIP_FROM_NON_ANCHOR$1);
1971
2080
  if (as === 'button') {
1972
- const forwardedType = forwarded.type;
1973
2081
  return (jsxRuntime.jsx("button", { className: itemClasses, ...forwarded,
1974
2082
  // A footer item button must not submit an enclosing form by default —
1975
2083
  // `<button>` defaults to `type="submit"`, and a Save/Cancel action row
@@ -1979,13 +2087,9 @@ const CardFooterItem = ({ as = 'span', className, children, color, bgColor, text
1979
2087
  // typed here as the `<a>` MIME string (`STRIP_FROM_NON_ANCHOR` above
1980
2088
  // says why it is not withheld), so `as="button" type="text/html"`
1981
2089
  // compiles, and HTML's INVALID-value default for a button's `type` is
1982
- // submit too. So anything that is not one of the three native button
1983
- // types falls back to `button` rather than being forwarded.
1984
- type: forwardedType === 'submit' ||
1985
- forwardedType === 'reset' ||
1986
- forwardedType === 'button'
1987
- ? forwardedType
1988
- : 'button', children: children }));
2090
+ // submit too. `buttonType` keeps `button`, `submit` and `reset` and
2091
+ // turns anything else into `button`.
2092
+ type: buttonType(forwarded.type), children: children }));
1989
2093
  }
1990
2094
  return (jsxRuntime.jsx("span", { className: itemClasses, ...omitAttrs(forwarded, STRIP_FROM_NON_BUTTON), children: children }));
1991
2095
  };
@@ -2225,6 +2329,35 @@ const DropdownComponent = React.forwardRef(function DropdownComponent({ label, c
2225
2329
  e.preventDefault();
2226
2330
  items[items.length - 1].focus();
2227
2331
  break;
2332
+ case 'Enter':
2333
+ case ' ': {
2334
+ // Activate the focused item by clicking it, so its `onClick` runs and
2335
+ // closing follows `closeOnClick` as it does for the mouse. A caller's
2336
+ // own key handler that prevented the default has claimed the key, and
2337
+ // is left to it rather than followed by a second activation.
2338
+ if (currentIndex < 0 || e.defaultPrevented)
2339
+ break;
2340
+ const item = items[currentIndex];
2341
+ // Leave the browser's own activation alone, or the item runs twice: a
2342
+ // `<button>` answers both keys, and a link with an `href` answers
2343
+ // Enter. A link does not answer Space, so Space on a link is handled
2344
+ // here too, which also keeps the page from scrolling.
2345
+ if (item.tagName === 'BUTTON')
2346
+ break;
2347
+ if (e.key === 'Enter' &&
2348
+ item.tagName === 'A' &&
2349
+ item.hasAttribute('href')) {
2350
+ break;
2351
+ }
2352
+ e.preventDefault();
2353
+ // A held key sends a keydown per auto-repeat. Clicking on each would
2354
+ // toggle a checkbox item over and over while `closeOnClick` is off,
2355
+ // so only the first press activates. The default is still prevented
2356
+ // on the repeats above, so a held Space does not scroll the page.
2357
+ if (!e.repeat)
2358
+ item.click();
2359
+ break;
2360
+ }
2228
2361
  }
2229
2362
  };
2230
2363
  const dropdownClasses = classNames(bulmaClasses, bulmaHelperClasses, className);
@@ -2234,19 +2367,16 @@ const DropdownComponent = React.forwardRef(function DropdownComponent({ label, c
2234
2367
  * The anchor's attributes, minus the one a `<button>` legitimately takes.
2235
2368
  *
2236
2369
  * `type` stays: `as="button"` is a supported form and `type="submit"` is valid
2237
- * there, so stripping it would remove a working attribute. The exclusion is per
2238
- * COMPONENT where the reason is per TAG, so it also lets a `type` reach a
2239
- * `<div>`; selecting the set from the rendered element would close that, and
2240
- * moves output.
2241
- *
2242
- * This set differs from `Level.Item`'s in both directions, not just one. The
2243
- * `type` above, which Level strips and this component keeps, and `rel`, which
2244
- * Level adds and this component does not: React declares `rel` on
2370
+ * there, so stripping it would remove a working attribute. A `<div>` takes no
2371
+ * `type`, so it gets the whole `ANCHOR_ONLY_ATTRS` set instead. The set is
2372
+ * chosen by the rendered tag because that is where the reason lives (#692).
2373
+ *
2374
+ * Neither set withholds `rel`, which `Level.Item` does: React declares `rel` on
2245
2375
  * `HTMLAttributes` for every element, so withholding it would diverge from
2246
- * React's own typing — the call #641 recorded for `Navbar.Link`. Level
2376
+ * React's own typing, the call #641 recorded for `Navbar.Link`. Level
2247
2377
  * withholds it anyway, because it always has.
2248
2378
  */
2249
- const STRIP_FROM_NON_ANCHOR$1 = (() => {
2379
+ const STRIP_FROM_BUTTON = (() => {
2250
2380
  const { type: _type, ...rest } = ANCHOR_ONLY_ATTRS;
2251
2381
  return rest;
2252
2382
  })();
@@ -2276,19 +2406,27 @@ const DropdownItem = ((itemProps) => {
2276
2406
  // are not interchangeable and neither derives from the other.
2277
2407
  //
2278
2408
  // Menu's condition also admits a custom component and a custom element, which
2279
- // own their prop contracts. `as` is closed to three intrinsic tags here, so
2280
- // neither can arrive and the anchor test is the whole rule.
2281
- const forwarded = Component === 'a' ? rest : omitAttrs(rest, STRIP_FROM_NON_ANCHOR$1);
2409
+ // own their prop contracts. Here the set follows the tag: an `<a>` keeps all
2410
+ // of them, a `<button>` keeps `type`, and any other `as` keeps none of them.
2411
+ const forwarded = Component === 'a'
2412
+ ? rest
2413
+ : Component === 'button'
2414
+ ? omitAttrs(rest, STRIP_FROM_BUTTON)
2415
+ : omitAttrs(rest, ANCHOR_ONLY_ATTRS);
2282
2416
  return (jsxRuntime.jsx(Component, { className: classNames(usePrefixedClassNames('dropdown-item', {
2283
2417
  'is-active': active,
2284
- }), bulmaHelperClasses, className), tabIndex: 0, "data-testid": "dropdown-item", ...forwarded,
2418
+ }), bulmaHelperClasses, className), "data-testid": "dropdown-item", ...forwarded,
2419
+ // After `forwarded` for the same reason as `role` and `type` below: a
2420
+ // spread carrying `tabIndex: undefined` would otherwise erase the
2421
+ // default. The item keeps its menu role and its place in the arrow-key
2422
+ // order, but a `<div>` or an anchor without an `href` cannot take focus
2423
+ // without a tabindex, so the arrow keys stall on it.
2424
+ tabIndex: forwarded.tabIndex ?? 0,
2285
2425
  // After `forwarded` for the same reason as `type` below: a spread
2286
2426
  // carrying `role: undefined` would otherwise erase the default, and an
2287
2427
  // item with no role drops out of the menu and its arrow-key order.
2288
2428
  role: forwarded.role ?? 'menuitem', ...(Component === 'button'
2289
- ? {
2290
- type: forwarded.type ?? 'button',
2291
- }
2429
+ ? { type: buttonType(forwarded.type) }
2292
2430
  : {}), children: children }));
2293
2431
  });
2294
2432
  /**
@@ -2397,7 +2535,9 @@ const MenuItem = React.forwardRef(function MenuItem(itemProps, ref) {
2397
2535
  labelChildren.push(child);
2398
2536
  }
2399
2537
  });
2400
- return (jsxRuntime.jsxs("li", { className: className, "data-testid": testId, style: style, id: id, title: title, role: role, tabIndex: tabIndex, children: [jsxRuntime.jsx(Component, { ref: ref, className: itemClass || undefined, ...linkProps, children: labelChildren }), nestedMenuLists] }));
2538
+ return (jsxRuntime.jsxs("li", { className: className, "data-testid": testId, style: style, id: id, title: title, role: role, tabIndex: tabIndex, children: [jsxRuntime.jsx(Component, { ref: ref, className: itemClass || undefined, ...linkProps, ...(Component === 'button'
2539
+ ? { type: buttonType(linkProps.type) }
2540
+ : {}), children: labelChildren }), nestedMenuLists] }));
2401
2541
  });
2402
2542
  MenuItem.displayName = 'MenuItem';
2403
2543
  // Attach static subcomponents
@@ -3163,23 +3303,6 @@ const UNSTYLED_MODIFIER_COLORS = [
3163
3303
  'grey-lighter',
3164
3304
  ];
3165
3305
  const CSS_BACKED = 'primary, link, info, success, warning, danger, black, white, light, dark';
3166
- const warnedKeys = new Set();
3167
- // Fail closed: with no bundler and no Node (raw CDN ESM), reading `process`
3168
- // throws and warnings stay off, so production can never warn by accident.
3169
- const isDev = () => {
3170
- try {
3171
- return process.env.NODE_ENV !== 'production';
3172
- }
3173
- catch {
3174
- return false;
3175
- }
3176
- };
3177
- const warnOnce = (key, message) => {
3178
- if (!isDev() || warnedKeys.has(key))
3179
- return;
3180
- warnedKeys.add(key);
3181
- console.warn(message);
3182
- };
3183
3306
  /**
3184
3307
  * Dev warning for a `color` value whose `is-<color>` modifier has no shipped
3185
3308
  * CSS. `extraUnstyled` covers per-component dead values beyond the shared
@@ -4614,6 +4737,48 @@ const Sidebar = withSubComponents(SidebarComponent, {
4614
4737
  Footer: SidebarFooter,
4615
4738
  }, 'Sidebar');
4616
4739
 
4740
+ /**
4741
+ * Splits the items a container shows into one stack per position in use. An
4742
+ * item goes to its own position when it has one and to the container's
4743
+ * otherwise. A position with no items gets no stack.
4744
+ *
4745
+ * Stacks come back in `order`, so their order in the page follows the screen
4746
+ * rather than which position happened to be used first, and a stack keeps its
4747
+ * place while the others come and go. A position missing from `order` sorts
4748
+ * after the known ones.
4749
+ *
4750
+ * @function groupIntoPositionStacks
4751
+ * @param items - The items the container shows, in the order they were shown.
4752
+ * @param positionOf - Reads an item's own position, if it has one.
4753
+ * @param containerPosition - Where an item without a position of its own goes.
4754
+ * @param order - Every known position, in the order stacks should render.
4755
+ * @returns One stack per position in use.
4756
+ */
4757
+ function groupIntoPositionStacks(items, positionOf, containerPosition, order) {
4758
+ const byPosition = new Map();
4759
+ for (const item of items) {
4760
+ const position = positionOf(item) ?? containerPosition;
4761
+ const stack = byPosition.get(position);
4762
+ if (stack) {
4763
+ stack.push(item);
4764
+ }
4765
+ else {
4766
+ byPosition.set(position, [item]);
4767
+ }
4768
+ }
4769
+ const rank = (position) => {
4770
+ const index = order.indexOf(position);
4771
+ return index === -1 ? order.length : index;
4772
+ };
4773
+ // Array.prototype.sort is stable, so unknown positions keep the order they
4774
+ // were first used in.
4775
+ return Array.from(byPosition, ([position, stackItems]) => ({
4776
+ key: position === containerPosition ? 'container' : `at-${position}`,
4777
+ position,
4778
+ items: stackItems,
4779
+ })).sort((a, b) => rank(a.position) - rank(b.position));
4780
+ }
4781
+
4617
4782
  /**
4618
4783
  * The `Toast` component provides brief notification messages with optional action and cancel buttons.
4619
4784
  *
@@ -4768,11 +4933,26 @@ let toasts = [];
4768
4933
  // Queued toasts: FIFO queue, one at a time
4769
4934
  let queuedToasts = [];
4770
4935
  let currentQueuedToast = null;
4936
+ // The server has nowhere to portal a toast to, and its copy of this module is
4937
+ // shared by every request, so server rendering reads an empty list. Hydration
4938
+ // reads it too, which keeps the first client render matching the server's.
4939
+ const noToasts = [];
4940
+ const getServerToasts = () => noToasts;
4941
+ // What a container shows right now: the stacked toasts, then the queued one on
4942
+ // screen. ToastContainer renders from this rather than from updates alone, so
4943
+ // toasts raised before it mounted still appear. It is replaced rather than
4944
+ // mutated, and only when listeners are notified, because useSyncExternalStore
4945
+ // needs the same array back between changes. An empty list is `noToasts`
4946
+ // itself, so a container that hydrates with nothing to show reads the same
4947
+ // snapshot the server did and has no reason to render again.
4948
+ let visibleToasts = noToasts;
4949
+ const getVisibleToasts = () => visibleToasts;
4771
4950
  const notifyListeners = () => {
4772
4951
  const allVisible = [...toasts];
4773
4952
  if (currentQueuedToast) {
4774
4953
  allVisible.push(currentQueuedToast);
4775
4954
  }
4955
+ visibleToasts = allVisible.length > 0 ? allVisible : noToasts;
4776
4956
  toastListeners.forEach(listener => listener([...allVisible]));
4777
4957
  };
4778
4958
  const processQueuedToast = () => {
@@ -4875,26 +5055,38 @@ const toast = {
4875
5055
  return () => toastListeners.delete(listener);
4876
5056
  },
4877
5057
  };
5058
+ // The order ToastContainer renders its stacks in: across the top of the
5059
+ // screen, then across the bottom.
5060
+ const toastStackOrder = [
5061
+ 'top-left',
5062
+ 'top-center',
5063
+ 'top-right',
5064
+ 'bottom-left',
5065
+ 'bottom-center',
5066
+ 'bottom-right',
5067
+ ];
4878
5068
  /**
4879
5069
  * Container component for rendering programmatic toasts.
4880
- * Place once at your app root to enable the toast API.
5070
+ * Place once at your app root to enable the toast API. A toast shown with a
5071
+ * `position` appears there, and one shown without goes to the container's
5072
+ * `position`.
4881
5073
  *
4882
5074
  * @function
4883
5075
  * @param {{ position?: ToastPosition }} props - Container props.
4884
5076
  * @returns {JSX.Element | null} The rendered toast container, or null if empty.
4885
5077
  */
4886
- const ToastContainer = ({ position = 'top-right', }) => {
4887
- const [toastList, setToastList] = React.useState([]);
4888
- React.useEffect(() => {
4889
- return toast.subscribe(setToastList);
4890
- }, []);
5078
+ const ToastContainer = ({ position = 'top-right' }) => {
5079
+ // Starts from the toasts already showing instead of an empty list, then
5080
+ // follows changes.
5081
+ const toastList = React.useSyncExternalStore(toast.subscribe, getVisibleToasts, getServerToasts);
4891
5082
  if (typeof document === 'undefined' || toastList.length === 0) {
4892
5083
  return null;
4893
5084
  }
4894
- return reactDom.createPortal(jsxRuntime.jsx("div", { className: `toast-container is-${position}`, children: toastList.map(t => {
5085
+ const stacks = groupIntoPositionStacks(toastList, t => t.props.position, position, toastStackOrder);
5086
+ return reactDom.createPortal(stacks.map(stack => (jsxRuntime.jsx("div", { className: `toast-container is-${stack.position}`, children: stack.items.map(t => {
4895
5087
  const { queue: _queue, ...toastProps } = t.props;
4896
- return (jsxRuntime.jsx(Toast, { ...toastProps, position: position, onClose: () => toast.close(t.id) }, t.id));
4897
- }) }), document.body);
5088
+ return (jsxRuntime.jsx(Toast, { ...toastProps, position: stack.position, onClose: () => toast.close(t.id) }, t.id));
5089
+ }) }, stack.key))), document.body);
4898
5090
  };
4899
5091
 
4900
5092
  /**
@@ -5039,6 +5231,14 @@ const Dialog = React.forwardRef(({ isOpen, title, message, type = 'default', con
5039
5231
  Dialog.displayName = 'Dialog';
5040
5232
  let dialogListeners = new Set();
5041
5233
  let currentDialog = null;
5234
+ // DialogContainer renders from the open dialog rather than from updates alone,
5235
+ // so a dialog raised before it mounted still opens once it does.
5236
+ const getCurrentDialog = () => currentDialog;
5237
+ // The server renders no dialog: its copy of this module is shared by every
5238
+ // request, so the dialog it holds may belong to another page. Hydration reads
5239
+ // the same empty value, which keeps the first client render matching the
5240
+ // server's, and the dialog opens right after.
5241
+ const getServerDialog = () => null;
5042
5242
  const notifyDialogListeners = () => {
5043
5243
  dialogListeners.forEach(listener => listener(currentDialog));
5044
5244
  };
@@ -5100,10 +5300,8 @@ const dialog = {
5100
5300
  * @returns {JSX.Element | null} The rendered dialog, or null if none is active.
5101
5301
  */
5102
5302
  const DialogContainer = () => {
5103
- const [current, setCurrent] = React.useState(null);
5104
- React.useEffect(() => {
5105
- return dialog.subscribe(setCurrent);
5106
- }, []);
5303
+ // Starts from the dialog already open instead of none, then follows changes.
5304
+ const current = React.useSyncExternalStore(dialog.subscribe, getCurrentDialog, getServerDialog);
5107
5305
  if (!current) {
5108
5306
  return null;
5109
5307
  }
@@ -6177,11 +6375,28 @@ let notifications = [];
6177
6375
  // Queue support
6178
6376
  let queuedNotifications = [];
6179
6377
  let currentQueuedNotification = null;
6378
+ // The server has nowhere to portal a notification to, and its copy of this
6379
+ // module is shared by every request, so server rendering reads an empty list.
6380
+ // Hydration reads it too, which keeps the first client render matching the
6381
+ // server's.
6382
+ const noNotifications = [];
6383
+ const getServerNotifications = () => noNotifications;
6384
+ // What a container shows right now: the stacked notifications, then the queued
6385
+ // one on screen. NotificationContainer renders from this rather than from
6386
+ // updates alone, so notifications raised before it mounted still appear. It is
6387
+ // replaced rather than mutated, and only when listeners are notified, because
6388
+ // useSyncExternalStore needs the same array back between changes. An empty
6389
+ // list is `noNotifications` itself, so a container that hydrates with nothing
6390
+ // to show reads the same snapshot the server did and has no reason to render
6391
+ // again.
6392
+ let visibleNotifications = noNotifications;
6393
+ const getVisibleNotifications = () => visibleNotifications;
6180
6394
  const notifyNotificationListeners = () => {
6181
6395
  const allVisible = [...notifications];
6182
6396
  if (currentQueuedNotification) {
6183
6397
  allVisible.push(currentQueuedNotification);
6184
6398
  }
6399
+ visibleNotifications = allVisible.length > 0 ? allVisible : noNotifications;
6185
6400
  notificationListeners.forEach(listener => listener([...allVisible]));
6186
6401
  };
6187
6402
  const processQueuedNotification = () => {
@@ -6315,28 +6530,28 @@ const NotificationItem = ({ instance, onClose }) => {
6315
6530
  }, [pauseOnHover]);
6316
6531
  return (jsxRuntime.jsx(Notification, { color: color, isLight: isLight, hasDelete: hasDelete, onDelete: handleClose, onMouseEnter: handleMouseEnter, onMouseLeave: handleMouseLeave, style: { pointerEvents: 'auto' }, children: urgent ? (jsxRuntime.jsx("span", { role: "alert", "aria-live": "assertive", children: message })) : (jsxRuntime.jsx("span", { role: "status", "aria-live": "polite", children: message })) }));
6317
6532
  };
6533
+ // The order NotificationContainer renders its stacks in: across the top of the
6534
+ // screen, then across the bottom.
6535
+ const notificationStackOrder = [
6536
+ 'top-left',
6537
+ 'top',
6538
+ 'top-right',
6539
+ 'bottom-left',
6540
+ 'bottom',
6541
+ 'bottom-right',
6542
+ ];
6318
6543
  /**
6319
- * Container component for rendering programmatic notifications.
6320
- * Place once at your app root to enable the notification API. Mount it
6321
- * before calling `notification`: a notification shown while no container is
6322
- * mounted doesn't appear when one mounts, only alongside the next call.
6544
+ * Inline style that fixes a stack of notifications to its place on the screen.
6323
6545
  *
6324
6546
  * @function
6325
- * @param {{ position?: NotificationPosition }} props - Container props.
6326
- * @returns {JSX.Element | null} The rendered notification container, or null if empty.
6547
+ * @param {NotificationPosition} position - Where the stack sits.
6548
+ * @returns {React.CSSProperties} The stack's style.
6327
6549
  */
6328
- const NotificationContainer = ({ position = 'top-right' }) => {
6329
- const [items, setItems] = React.useState([]);
6330
- React.useEffect(() => {
6331
- return notification.subscribe(setItems);
6332
- }, []);
6333
- if (typeof document === 'undefined' || items.length === 0) {
6334
- return null;
6335
- }
6550
+ const notificationStackStyle = (position) => {
6336
6551
  const isBottom = position.startsWith('bottom');
6337
6552
  const isCenter = position === 'top' || position === 'bottom';
6338
6553
  const isRight = position.endsWith('right');
6339
- const containerStyle = {
6554
+ return {
6340
6555
  position: 'fixed',
6341
6556
  zIndex: 100,
6342
6557
  display: 'flex',
@@ -6352,7 +6567,27 @@ const NotificationContainer = ({ position = 'top-right' }) => {
6352
6567
  ? { right: 0, alignItems: 'flex-end' }
6353
6568
  : { left: 0, alignItems: 'flex-start' }),
6354
6569
  };
6355
- return reactDom.createPortal(jsxRuntime.jsx("div", { style: containerStyle, children: items.map(item => (jsxRuntime.jsx(NotificationItem, { instance: item, onClose: notification.close }, item.id))) }), document.body);
6570
+ };
6571
+ /**
6572
+ * Container component for rendering programmatic notifications.
6573
+ * Place once at your app root to enable the notification API. A notification
6574
+ * shown with a `position` appears there, and one shown without goes to the
6575
+ * container's `position`, so the container renders a stack for each position
6576
+ * in use.
6577
+ *
6578
+ * @function
6579
+ * @param {{ position?: NotificationPosition }} props - Container props.
6580
+ * @returns {JSX.Element | null} The rendered notification container, or null if empty.
6581
+ */
6582
+ const NotificationContainer = ({ position = 'top-right' }) => {
6583
+ // Starts from the notifications already showing instead of an empty list,
6584
+ // then follows changes.
6585
+ const items = React.useSyncExternalStore(notification.subscribe, getVisibleNotifications, getServerNotifications);
6586
+ if (typeof document === 'undefined' || items.length === 0) {
6587
+ return null;
6588
+ }
6589
+ const stacks = groupIntoPositionStacks(items, item => item.options.position, position, notificationStackOrder);
6590
+ return reactDom.createPortal(stacks.map(stack => (jsxRuntime.jsx("div", { style: notificationStackStyle(stack.position), children: stack.items.map(item => (jsxRuntime.jsx(NotificationItem, { instance: item, onClose: notification.close }, item.id))) }, stack.key))), document.body);
6356
6591
  };
6357
6592
 
6358
6593
  /**
@@ -6895,7 +7130,21 @@ const RadiosProvider = RadiosContext.Provider;
6895
7130
  const CheckboxesProvider = CheckboxesContext.Provider;
6896
7131
 
6897
7132
  /**
6898
- * Valid colors for the Checkbox component.
7133
+ * The values the Checkbox `color` prop accepts, as a readonly tuple.
7134
+ *
7135
+ * `CheckboxProps['color']` is typed from it, so the two list the same values.
7136
+ * Map over it to build a color picker, or check a value that arrives at
7137
+ * runtime before passing it in: the component adds no color class for a value
7138
+ * outside the tuple.
7139
+ *
7140
+ * @example
7141
+ * import { Checkbox, checkboxColors } from '@allxsmith/bestax-bulma';
7142
+ *
7143
+ * checkboxColors.map(color => (
7144
+ * <Checkbox key={color} color={color}>
7145
+ * {color}
7146
+ * </Checkbox>
7147
+ * ));
6899
7148
  */
6900
7149
  const checkboxColors = [
6901
7150
  'primary',
@@ -6906,7 +7155,17 @@ const checkboxColors = [
6906
7155
  'danger',
6907
7156
  ];
6908
7157
  /**
6909
- * Valid sizes for the Checkbox component.
7158
+ * The values the Checkbox `size` prop accepts, as a readonly tuple.
7159
+ *
7160
+ * `CheckboxProps['size']` is typed from it, so the two list the same values.
7161
+ * Use it to offer a size choice or to check a value that arrives at runtime:
7162
+ * the component adds no size class for a value outside the tuple. These are
7163
+ * element sizes, not the spacing scale in `validSizes`.
7164
+ *
7165
+ * @example
7166
+ * import { checkboxSizes } from '@allxsmith/bestax-bulma';
7167
+ *
7168
+ * type CheckboxSize = (typeof checkboxSizes)[number];
6910
7169
  */
6911
7170
  const checkboxSizes = ['small', 'normal', 'medium', 'large'];
6912
7171
  /**
@@ -7351,7 +7610,23 @@ label, labelSize, labelProps, horizontal, message, messageColor, fieldClassName,
7351
7610
  File.displayName = 'File';
7352
7611
 
7353
7612
  /**
7354
- * Valid colors for the Radio component.
7613
+ * The values the Radio `color` prop accepts, as a readonly tuple.
7614
+ *
7615
+ * `RadioProps['color']` is typed from it, so the two list the same values.
7616
+ * Map over it to build a color picker, or check a value that arrives at
7617
+ * runtime before passing it in: the component adds no color class for a value
7618
+ * outside the tuple.
7619
+ *
7620
+ * @example
7621
+ * import { Radio, Radios, radioColors } from '@allxsmith/bestax-bulma';
7622
+ *
7623
+ * <Radios name="accent" defaultValue="primary">
7624
+ * {radioColors.map(color => (
7625
+ * <Radio key={color} value={color} color={color}>
7626
+ * {color}
7627
+ * </Radio>
7628
+ * ))}
7629
+ * </Radios>;
7355
7630
  */
7356
7631
  const radioColors = [
7357
7632
  'primary',
@@ -7362,7 +7637,17 @@ const radioColors = [
7362
7637
  'danger',
7363
7638
  ];
7364
7639
  /**
7365
- * Valid sizes for the Radio component.
7640
+ * The values the Radio `size` prop accepts, as a readonly tuple.
7641
+ *
7642
+ * `RadioProps['size']` is typed from it, so the two list the same values. Use
7643
+ * it to offer a size choice or to check a value that arrives at runtime: the
7644
+ * component adds no size class for a value outside the tuple. These are
7645
+ * element sizes, not the spacing scale in `validSizes`.
7646
+ *
7647
+ * @example
7648
+ * import { radioSizes } from '@allxsmith/bestax-bulma';
7649
+ *
7650
+ * type RadioSize = (typeof radioSizes)[number];
7366
7651
  */
7367
7652
  const radioSizes = ['small', 'normal', 'medium', 'large'];
7368
7653
  /**
@@ -7493,7 +7778,21 @@ const RadiosComponent = ({ label, labelSize, labelProps, horizontal, message, me
7493
7778
  const Radios = withSubComponents(RadiosComponent, { Radio }, 'Radios');
7494
7779
 
7495
7780
  /**
7496
- * Valid colors for the Switch component.
7781
+ * The values the Switch `color` and `passiveType` props accept, as a readonly
7782
+ * tuple.
7783
+ *
7784
+ * Both props are typed from it, so they list the same values. Map over it to
7785
+ * build a color picker, or check a value that arrives at runtime before
7786
+ * passing it in: the component adds no class for a value outside the tuple.
7787
+ *
7788
+ * @example
7789
+ * import { Switch, switchColors } from '@allxsmith/bestax-bulma';
7790
+ *
7791
+ * switchColors.map(color => (
7792
+ * <Switch key={color} color={color} defaultChecked>
7793
+ * {color}
7794
+ * </Switch>
7795
+ * ));
7497
7796
  */
7498
7797
  const switchColors = [
7499
7798
  'primary',
@@ -7504,7 +7803,17 @@ const switchColors = [
7504
7803
  'danger',
7505
7804
  ];
7506
7805
  /**
7507
- * Valid sizes for the Switch component.
7806
+ * The values the Switch `size` prop accepts, as a readonly tuple.
7807
+ *
7808
+ * `SwitchProps['size']` is typed from it, so the two list the same values. Use
7809
+ * it to offer a size choice or to check a value that arrives at runtime: the
7810
+ * component adds no size class for a value outside the tuple. These are
7811
+ * element sizes, not the spacing scale in `validSizes`.
7812
+ *
7813
+ * @example
7814
+ * import { switchSizes } from '@allxsmith/bestax-bulma';
7815
+ *
7816
+ * type SwitchSize = (typeof switchSizes)[number];
7508
7817
  */
7509
7818
  const switchSizes = ['small', 'normal', 'medium', 'large'];
7510
7819
  /**
@@ -13000,17 +13309,130 @@ function cssVarToProp(varName) {
13000
13309
  .join('');
13001
13310
  }
13002
13311
  /**
13003
- * Mapping of camelCase prop names to their Bulma CSS variable counterparts.
13312
+ * Prop names `cssVarToProp` would mint that are already `BulmaOtherProps`
13313
+ * helper props: `--bulma-shadow` becomes `shadow` and `--bulma-radius` becomes
13314
+ * `radius`. They stay out of `bulmaVarPropMap`, so on Theme each is the helper
13315
+ * prop it is on every other component, and both variables are still reachable
13316
+ * through `bulmaVars`.
13004
13317
  *
13005
- * `--bulma-shadow` is excluded: `cssVarToProp` would mint it as `shadow`,
13006
- * which already exists as a `BulmaOtherProps` prop applied via
13007
- * `useBulmaClasses` — keeping it out of this map means that
13008
- * prop keeps its existing class-based meaning, while `--bulma-shadow` is
13009
- * still reachable through the `bulmaVars` object.
13318
+ * `radius` was missed here once and set `--bulma-radius` while typed as the
13319
+ * helper (#694). It still writes that variable, so what it did then keeps
13320
+ * working; see `themeRadiusVar`.
13321
+ */
13322
+ const helperPropNames = ['shadow', 'radius'];
13323
+ /**
13324
+ * Mapping of camelCase prop names to their Bulma CSS variable counterparts,
13325
+ * minus the `helperPropNames` above.
13010
13326
  */
13011
13327
  const bulmaVarPropMap = Object.fromEntries(bulmaCssVars
13012
13328
  .map(cssVar => [cssVarToProp(cssVar), cssVar])
13013
- .filter(([prop]) => prop !== 'shadow'));
13329
+ .filter(([prop]) => !helperPropNames.includes(prop)));
13330
+ /**
13331
+ * What each helper value of `radius` writes to `--bulma-radius` on a Theme,
13332
+ * alongside its class.
13333
+ *
13334
+ * Before #694, `radius="radiusless"` wrote `--bulma-radius: radiusless`. That
13335
+ * is not a length, and a declaration reading an invalid variable falls back
13336
+ * to its property's initial value, which for a border radius is 0. So
13337
+ * everything inside the Theme that takes its radius from that variable lost
13338
+ * it, and under `isRoot` so did everything on the page that does. Writing a
13339
+ * real 0 keeps what
13340
+ * people see and makes it valid CSS. Keyed by the helper values, so adding
13341
+ * one means saying what it writes.
13342
+ */
13343
+ const radiusHelperVars = {
13344
+ radiusless: '0',
13345
+ };
13346
+ /**
13347
+ * What `radius` writes to `--bulma-radius` on a Theme, or `undefined` when it
13348
+ * writes nothing.
13349
+ *
13350
+ * A helper value writes its `radiusHelperVars` entry. Any other non-empty
13351
+ * string is written as given: before #694 every `radius` on Theme set the
13352
+ * variable whatever its type said, and a JavaScript caller passing a length
13353
+ * got the radius it asked for. That keeps working so nothing breaks, but the
13354
+ * type rejects it, so it warns in development and points at `bulmaVars`.
13355
+ *
13356
+ * Only a string writes anything, because only a string ever set the variable
13357
+ * to something usable: an empty value or zero was dropped, and a boolean or
13358
+ * any other bare number is not a length. The helper ignores those too, the
13359
+ * way it does on every other component.
13360
+ */
13361
+ const themeRadiusVar = (radius) => {
13362
+ if (typeof radius !== 'string' || radius === '') {
13363
+ return undefined;
13364
+ }
13365
+ if (validRadii.includes(radius)) {
13366
+ return radiusHelperVars[radius];
13367
+ }
13368
+ warnOnce('Theme:radius-variable', `[bestax-bulma] <Theme radius="${radius}">: setting --bulma-radius ` +
13369
+ 'through the radius prop is deprecated and will stop working in a ' +
13370
+ 'future major version. On Theme, as on every other component, radius ' +
13371
+ `is the "${validRadii.join('", "')}" helper. Set the variable with ` +
13372
+ `bulmaVars={{ '--bulma-radius': '${radius}' }} instead.`);
13373
+ return radius;
13374
+ };
13375
+ /** The one `<style>` element every `isRoot` Theme writes into. */
13376
+ const ROOT_STYLE_ID = 'bestax-bulma-theme-vars';
13377
+ /**
13378
+ * The `:root` rules of every mounted `isRoot` Theme that has any, keyed by the
13379
+ * Theme's render order.
13380
+ *
13381
+ * Root Themes share one `<style>` element, so each keeps its rules here and
13382
+ * the element is rebuilt from all of them whenever one mounts, changes or
13383
+ * unmounts. Before this each Theme overwrote the element with only its own
13384
+ * rules, and the first to unmount removed it for all of them (#736).
13385
+ *
13386
+ * The key is a number each Theme takes when it first renders. React renders a
13387
+ * parent before its children and an earlier sibling before a later one, so an
13388
+ * inner or later-mounted root Theme sorts later, comes later in the
13389
+ * stylesheet, and wins a variable two of them set, as an inner scoped Theme
13390
+ * does. Effects would give the wrong answer for nesting: React runs a child's
13391
+ * effects before its parent's. Only the relative order matters, so a number
13392
+ * skipped by StrictMode calling the initializer twice, or by a render React
13393
+ * throws away, is harmless. The key never changes, so an update keeps its
13394
+ * place and re-rendering one Theme never changes which one wins.
13395
+ */
13396
+ const rootThemeRules = new Map();
13397
+ let nextRootOrder = 0;
13398
+ /**
13399
+ * Write every registered root Theme's rules into the shared element, creating
13400
+ * it when needed and removing it once no Theme has any rules left.
13401
+ */
13402
+ const renderRootThemeRules = () => {
13403
+ let element = document.getElementById(ROOT_STYLE_ID);
13404
+ if (rootThemeRules.size === 0) {
13405
+ element?.remove();
13406
+ return;
13407
+ }
13408
+ if (!element) {
13409
+ element = document.createElement('style');
13410
+ element.id = ROOT_STYLE_ID;
13411
+ document.head.appendChild(element);
13412
+ }
13413
+ element.textContent = [...rootThemeRules]
13414
+ .sort(([a], [b]) => a - b)
13415
+ .map(([, rules]) => rules)
13416
+ .join('\n');
13417
+ };
13418
+ /**
13419
+ * Record one Theme's `:root` rules, where an empty string means it has none,
13420
+ * and rebuild the shared element if that changed anything. A Theme with no
13421
+ * rules never touches the element, which is what it did before the registry
13422
+ * too.
13423
+ */
13424
+ const setRootThemeRules = (order, rules) => {
13425
+ if ((rootThemeRules.get(order) ?? '') === rules) {
13426
+ return;
13427
+ }
13428
+ if (rules) {
13429
+ rootThemeRules.set(order, rules);
13430
+ }
13431
+ else {
13432
+ rootThemeRules.delete(order);
13433
+ }
13434
+ renderRootThemeRules();
13435
+ };
13014
13436
  /**
13015
13437
  * Theme component that injects Bulma CSS variables either globally or locally.
13016
13438
  *
@@ -13027,7 +13449,11 @@ const bulmaVarPropMap = Object.fromEntries(bulmaCssVars
13027
13449
  * <Button color="primary">Themed</Button>
13028
13450
  * </Theme>
13029
13451
  */
13030
- const Theme = ({ bulmaVars = {}, children, className, isRoot = false, colorMode, ...restProps }) => {
13452
+ const Theme = ({ bulmaVars = {}, children, className, isRoot = false, colorMode, radius, ...restProps }) => {
13453
+ const radiusVar = themeRadiusVar(radius);
13454
+ const radiusHelper = validRadii.includes(radius)
13455
+ ? radius
13456
+ : undefined;
13031
13457
  // Extract Bulma variable props from restProps
13032
13458
  const { bulmaVarProps, otherProps } = React.useMemo(() => {
13033
13459
  const varProps = {};
@@ -13042,8 +13468,12 @@ const Theme = ({ bulmaVars = {}, children, className, isRoot = false, colorMode,
13042
13468
  }
13043
13469
  return { bulmaVarProps: varProps, otherProps: otherPropsObj };
13044
13470
  }, [restProps]);
13045
- // Use Bulma classes for styling (only when not isRoot)
13046
- const { bulmaHelperClasses, rest } = useBulmaClasses(otherProps);
13471
+ // Use Bulma classes for styling (only when not isRoot). Only a helper value
13472
+ // of `radius` reaches the helper; a legacy string went to the variable.
13473
+ const { bulmaHelperClasses, rest } = useBulmaClasses({
13474
+ ...otherProps,
13475
+ radius: radiusHelper,
13476
+ });
13047
13477
  // Merge bulmaVars and individual props, with props taking precedence
13048
13478
  const mergedVars = React.useMemo(() => {
13049
13479
  const vars = { ...bulmaVars };
@@ -13052,37 +13482,34 @@ const Theme = ({ bulmaVars = {}, children, className, isRoot = false, colorMode,
13052
13482
  vars[cssVar] = bulmaVarProps[propName];
13053
13483
  }
13054
13484
  }
13485
+ if (radiusVar !== undefined) {
13486
+ vars['--bulma-radius'] = radiusVar;
13487
+ }
13055
13488
  return vars;
13056
- }, [bulmaVars, bulmaVarProps]);
13057
- // Inject CSS variables globally at :root level
13058
- React.useEffect(() => {
13489
+ }, [bulmaVars, bulmaVarProps, radiusVar]);
13490
+ // This Theme's place among root Themes; see `rootThemeRules`.
13491
+ const [rootOrder] = React.useState(() => nextRootOrder++);
13492
+ // The `:root` rules this Theme contributes, or '' when it contributes none.
13493
+ const rootRules = React.useMemo(() => {
13059
13494
  if (!isRoot) {
13060
- return;
13061
- }
13062
- const validVars = Object.entries(mergedVars).filter(([key, value]) => bulmaCssVars.includes(key) && value);
13063
- if (validVars.length === 0) {
13064
- return;
13065
- }
13066
- // Create and inject a style element for global CSS variables
13067
- const styleId = 'bestax-bulma-theme-vars';
13068
- let styleElement = document.getElementById(styleId);
13069
- if (!styleElement) {
13070
- styleElement = document.createElement('style');
13071
- styleElement.id = styleId;
13072
- document.head.appendChild(styleElement);
13495
+ return '';
13073
13496
  }
13074
- const cssRules = validVars
13497
+ const cssRules = Object.entries(mergedVars)
13498
+ .filter(([key, value]) => bulmaCssVars.includes(key) && value)
13075
13499
  .map(([key, value]) => `${key}: ${value};`)
13076
13500
  .join(' ');
13077
- styleElement.textContent = `:root { ${cssRules} }`;
13078
- // Cleanup function to remove the style element when component unmounts
13079
- return () => {
13080
- const element = document.getElementById(styleId);
13081
- if (element) {
13082
- element.remove();
13083
- }
13084
- };
13501
+ return cssRules ? `:root { ${cssRules} }` : '';
13085
13502
  }, [mergedVars, isRoot]);
13503
+ // Inject CSS variables globally at :root level, alongside any other root
13504
+ // Themes. Clearing `isRoot` or every variable passes '', which withdraws
13505
+ // this Theme's rules.
13506
+ React.useEffect(() => {
13507
+ setRootThemeRules(rootOrder, rootRules);
13508
+ }, [rootOrder, rootRules]);
13509
+ // Withdraw them on unmount. Kept apart from the effect above so a change
13510
+ // rewrites the shared element in place rather than removing and recreating
13511
+ // it between the cleanup and the next run.
13512
+ React.useEffect(() => () => setRootThemeRules(rootOrder, ''), [rootOrder]);
13086
13513
  // Toggle Bulma's light/dark scheme by writing the `data-theme` attribute on
13087
13514
  // the document root (<html>). This is always global, even on a scoped Theme.
13088
13515
  // `'system'` removes the attribute so Bulma follows the OS preference.