@allxsmith/bestax-bulma 5.17.0 → 5.18.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. package/dist/bestax.css +1 -1
  2. package/dist/bestax.css.map +1 -1
  3. package/dist/extras.css +1 -1
  4. package/dist/extras.css.map +1 -1
  5. package/dist/index.cjs +494 -102
  6. package/dist/index.cjs.map +1 -1
  7. package/dist/index.esm.js +494 -102
  8. package/dist/index.esm.js.map +1 -1
  9. package/dist/types/components/Avatar.d.ts +14 -3
  10. package/dist/types/components/Dropdown.d.ts +1 -1
  11. package/dist/types/components/Pagination.d.ts +36 -3
  12. package/dist/types/elements/Notification.d.ts +1 -3
  13. package/dist/types/form/Checkbox.d.ts +26 -2
  14. package/dist/types/form/DateTimeInputBase.d.ts +6 -1
  15. package/dist/types/form/Radio.d.ts +28 -2
  16. package/dist/types/form/Switch.d.ts +26 -2
  17. package/dist/types/form/TimeInputBase.d.ts +5 -1
  18. package/dist/types/helpers/Theme.d.ts +27 -1
  19. package/dist/types/helpers/colorDeprecations.d.ts +4 -1
  20. package/dist/types/helpers/devWarnings.d.ts +8 -0
  21. package/dist/types-cjs/components/Avatar.d.ts +14 -3
  22. package/dist/types-cjs/components/Dropdown.d.ts +1 -1
  23. package/dist/types-cjs/components/Pagination.d.ts +36 -3
  24. package/dist/types-cjs/elements/Notification.d.ts +1 -3
  25. package/dist/types-cjs/form/Checkbox.d.ts +26 -2
  26. package/dist/types-cjs/form/DateTimeInputBase.d.ts +6 -1
  27. package/dist/types-cjs/form/Radio.d.ts +28 -2
  28. package/dist/types-cjs/form/Switch.d.ts +26 -2
  29. package/dist/types-cjs/form/TimeInputBase.d.ts +5 -1
  30. package/dist/types-cjs/helpers/Theme.d.ts +27 -1
  31. package/dist/types-cjs/helpers/colorDeprecations.d.ts +4 -1
  32. package/dist/types-cjs/helpers/devWarnings.d.ts +8 -0
  33. package/dist/versions/bestax-no-dark-mode.css +1 -1
  34. package/dist/versions/bestax-no-dark-mode.css.map +1 -1
  35. package/dist/versions/bestax-no-helpers-prefixed.css +1 -1
  36. package/dist/versions/bestax-no-helpers-prefixed.css.map +1 -1
  37. package/dist/versions/bestax-no-helpers.css +1 -1
  38. package/dist/versions/bestax-no-helpers.css.map +1 -1
  39. package/dist/versions/bestax-prefixed.css +1 -1
  40. package/dist/versions/bestax-prefixed.css.map +1 -1
  41. package/package.json +1 -1
  42. package/src/scss/form/_timeinput.scss +42 -14
package/dist/index.cjs CHANGED
@@ -1252,6 +1252,30 @@ function isCustomElement(as) {
1252
1252
  return typeof as === 'string' && as.includes('-');
1253
1253
  }
1254
1254
 
1255
+ // INTERNAL: deliberately not exported from src/index.ts.
1256
+ const warnedKeys = new Set();
1257
+ // Fail closed: with no bundler and no Node (raw CDN ESM), reading `process`
1258
+ // throws and warnings stay off, so production can never warn by accident.
1259
+ const isDev = () => {
1260
+ try {
1261
+ return process.env.NODE_ENV !== 'production';
1262
+ }
1263
+ catch {
1264
+ return false;
1265
+ }
1266
+ };
1267
+ /**
1268
+ * Logs a console warning in development, once per `key` for the life of the
1269
+ * page. Safe to call during render: a re-render, or a second instance hitting
1270
+ * the same case, finds the key already recorded and stays quiet.
1271
+ */
1272
+ const warnOnce = (key, message) => {
1273
+ if (!isDev() || warnedKeys.has(key))
1274
+ return;
1275
+ warnedKeys.add(key);
1276
+ console.warn(message);
1277
+ };
1278
+
1255
1279
  const avatarColors = [
1256
1280
  'primary',
1257
1281
  'link',
@@ -1297,6 +1321,18 @@ const NON_INTERACTIVE_ROLES = [
1297
1321
  'presentation',
1298
1322
  'none',
1299
1323
  ];
1324
+ /**
1325
+ * Elements other than `a` that declare `target` themselves, so on them it is
1326
+ * the element's own attribute and not a link attribute Avatar withholds for
1327
+ * want of one. The link attribute warning leaves `target` out on these, the way
1328
+ * it leaves `rel` out everywhere. `area` and `base` are void elements, which
1329
+ * Avatar's content rules out, so `form` is the one a caller can reach.
1330
+ */
1331
+ const ELEMENTS_WITH_OWN_TARGET = [
1332
+ 'form',
1333
+ 'area',
1334
+ 'base',
1335
+ ];
1300
1336
  /**
1301
1337
  * Derives a small set of initials from a name (e.g. "Ada Lovelace" -> "AL").
1302
1338
  */
@@ -1432,6 +1468,50 @@ const Avatar = React.forwardRef(function Avatar(avatarProps, ref) {
1432
1468
  // Only forward link attributes when rendering an anchor or a custom (non-DOM)
1433
1469
  // component; a plain `as="div"` must not receive a stray `href`/`target`/`rel`.
1434
1470
  const isLinkLike = Tag === 'a' || typeof Tag !== 'string' || isCustomElement(Tag);
1471
+ // A plain element like a `div` declares no `href` or `target`, so the type
1472
+ // accepts them as Avatar's own while the drop below withholds them, and
1473
+ // without this they would vanish in silence (#733). Forwarding them would put
1474
+ // them where HTML has no such attribute, so the drop stays and development
1475
+ // reports it instead, naming only the ones actually passed.
1476
+ //
1477
+ // `rel` is withheld too but left out here. React declares it on every
1478
+ // element, so on a plain `as` it is that element's own attribute, and
1479
+ // "render it as a link" would be the wrong advice for it. `target` is the
1480
+ // same on an element that declares it, such as `form`, so it is left out
1481
+ // there. Whether to forward either one to such an element is a separate
1482
+ // question from this warning.
1483
+ //
1484
+ // Only for an `as` the caller wrote. Without one the element is Avatar's own
1485
+ // choice, and a message naming an `as` they never passed sends them looking
1486
+ // for it. A value counts when it is truthy, the same test that picks `'a'`
1487
+ // over `'figure'`, so an empty `href` from data asks for no link and draws no
1488
+ // warning. The key is the element plus the attributes passed, so a re-render
1489
+ // or a list of avatars warns once, while a different combination on the same
1490
+ // element still gets its own warning rather than hiding behind the first.
1491
+ //
1492
+ // Development-only is `warnOnce`'s job, as it is for the colour warnings, so
1493
+ // this block has no production check of its own. One here would have to read
1494
+ // `process` safely, and a `typeof process` test is not something a bundler
1495
+ // replaces: in a browser it is false, and the warning never fired in the
1496
+ // development builds it exists for. The cost is that production still
1497
+ // collects the attributes and builds the message for an avatar that needs
1498
+ // it, which is small and limited to the case the warning is about.
1499
+ if (as != null && !isLinkLike) {
1500
+ const dropped = Object.entries({
1501
+ href,
1502
+ target: ELEMENTS_WITH_OWN_TARGET.includes(as) ? undefined : target,
1503
+ })
1504
+ .filter(([, value]) => value)
1505
+ .map(([key]) => key);
1506
+ if (dropped.length > 0) {
1507
+ warnOnce(`Avatar:link-props-on-${as}:${dropped.join('+')}`, `[bestax-bulma] <Avatar as="${as}" ${dropped.join(' ')}>: this ` +
1508
+ `<${as}> renders without ` +
1509
+ `${dropped.map(key => `"${key}"`).join(' and ')}, because Avatar ` +
1510
+ `passes link attributes on only to a target that can be a link (an ` +
1511
+ `"a", a custom element, or a component). To make it a link, render ` +
1512
+ `it as="a" or pass a link component to "as".`);
1513
+ }
1514
+ }
1435
1515
  // Present-only, not `{ href, target, rel }`. An unconditional spread hands the
1436
1516
  // target these keys whatever the caller passed, and a key existing is not free:
1437
1517
  // a target that tests for one sees a link where there is none, and one that
@@ -1822,12 +1902,12 @@ const CardComponent = ({ className, children, textColor, color, bgColor, hasShad
1822
1902
  * The set withheld from a non-anchor `Card.FooterItem` (`span`/`button`): the
1823
1903
  * derived anchor-only attributes, minus `type` (also valid on a `<button>` as
1824
1904
  * `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).
1905
+ * attribute; one set for both tags means a `type` also reaches a `<span>`,
1906
+ * which `Dropdown.Item` avoids by choosing its set per tag), plus `rel` (React
1907
+ * declares it on `HTMLAttributes` for every element, so the derived set alone
1908
+ * would not withhold it — the same addition `Level.Item` makes).
1829
1909
  */
1830
- const STRIP_FROM_NON_ANCHOR$2 = (() => {
1910
+ const STRIP_FROM_NON_ANCHOR$1 = (() => {
1831
1911
  const { type: _type, ...rest } = ANCHOR_ONLY_ATTRS;
1832
1912
  return { ...rest, rel: true };
1833
1913
  })();
@@ -1967,7 +2047,7 @@ const CardFooterItem = ({ as = 'span', className, children, color, bgColor, text
1967
2047
  if (as === 'a') {
1968
2048
  return (jsxRuntime.jsx("a", { className: itemClasses, ...omitAttrs(rest, STRIP_FROM_NON_BUTTON), children: children }));
1969
2049
  }
1970
- const forwarded = omitAttrs(rest, STRIP_FROM_NON_ANCHOR$2);
2050
+ const forwarded = omitAttrs(rest, STRIP_FROM_NON_ANCHOR$1);
1971
2051
  if (as === 'button') {
1972
2052
  const forwardedType = forwarded.type;
1973
2053
  return (jsxRuntime.jsx("button", { className: itemClasses, ...forwarded,
@@ -2225,6 +2305,35 @@ const DropdownComponent = React.forwardRef(function DropdownComponent({ label, c
2225
2305
  e.preventDefault();
2226
2306
  items[items.length - 1].focus();
2227
2307
  break;
2308
+ case 'Enter':
2309
+ case ' ': {
2310
+ // Activate the focused item by clicking it, so its `onClick` runs and
2311
+ // closing follows `closeOnClick` as it does for the mouse. A caller's
2312
+ // own key handler that prevented the default has claimed the key, and
2313
+ // is left to it rather than followed by a second activation.
2314
+ if (currentIndex < 0 || e.defaultPrevented)
2315
+ break;
2316
+ const item = items[currentIndex];
2317
+ // Leave the browser's own activation alone, or the item runs twice: a
2318
+ // `<button>` answers both keys, and a link with an `href` answers
2319
+ // Enter. A link does not answer Space, so Space on a link is handled
2320
+ // here too, which also keeps the page from scrolling.
2321
+ if (item.tagName === 'BUTTON')
2322
+ break;
2323
+ if (e.key === 'Enter' &&
2324
+ item.tagName === 'A' &&
2325
+ item.hasAttribute('href')) {
2326
+ break;
2327
+ }
2328
+ e.preventDefault();
2329
+ // A held key sends a keydown per auto-repeat. Clicking on each would
2330
+ // toggle a checkbox item over and over while `closeOnClick` is off,
2331
+ // so only the first press activates. The default is still prevented
2332
+ // on the repeats above, so a held Space does not scroll the page.
2333
+ if (!e.repeat)
2334
+ item.click();
2335
+ break;
2336
+ }
2228
2337
  }
2229
2338
  };
2230
2339
  const dropdownClasses = classNames(bulmaClasses, bulmaHelperClasses, className);
@@ -2234,19 +2343,16 @@ const DropdownComponent = React.forwardRef(function DropdownComponent({ label, c
2234
2343
  * The anchor's attributes, minus the one a `<button>` legitimately takes.
2235
2344
  *
2236
2345
  * `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
2346
+ * there, so stripping it would remove a working attribute. A `<div>` takes no
2347
+ * `type`, so it gets the whole `ANCHOR_ONLY_ATTRS` set instead. The set is
2348
+ * chosen by the rendered tag because that is where the reason lives (#692).
2349
+ *
2350
+ * Neither set withholds `rel`, which `Level.Item` does: React declares `rel` on
2245
2351
  * `HTMLAttributes` for every element, so withholding it would diverge from
2246
- * React's own typing — the call #641 recorded for `Navbar.Link`. Level
2352
+ * React's own typing, the call #641 recorded for `Navbar.Link`. Level
2247
2353
  * withholds it anyway, because it always has.
2248
2354
  */
2249
- const STRIP_FROM_NON_ANCHOR$1 = (() => {
2355
+ const STRIP_FROM_BUTTON = (() => {
2250
2356
  const { type: _type, ...rest } = ANCHOR_ONLY_ATTRS;
2251
2357
  return rest;
2252
2358
  })();
@@ -2276,12 +2382,22 @@ const DropdownItem = ((itemProps) => {
2276
2382
  // are not interchangeable and neither derives from the other.
2277
2383
  //
2278
2384
  // 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);
2385
+ // own their prop contracts. Here the set follows the tag: an `<a>` keeps all
2386
+ // of them, a `<button>` keeps `type`, and any other `as` keeps none of them.
2387
+ const forwarded = Component === 'a'
2388
+ ? rest
2389
+ : Component === 'button'
2390
+ ? omitAttrs(rest, STRIP_FROM_BUTTON)
2391
+ : omitAttrs(rest, ANCHOR_ONLY_ATTRS);
2282
2392
  return (jsxRuntime.jsx(Component, { className: classNames(usePrefixedClassNames('dropdown-item', {
2283
2393
  'is-active': active,
2284
- }), bulmaHelperClasses, className), tabIndex: 0, "data-testid": "dropdown-item", ...forwarded,
2394
+ }), bulmaHelperClasses, className), "data-testid": "dropdown-item", ...forwarded,
2395
+ // After `forwarded` for the same reason as `role` and `type` below: a
2396
+ // spread carrying `tabIndex: undefined` would otherwise erase the
2397
+ // default. The item keeps its menu role and its place in the arrow-key
2398
+ // order, but a `<div>` or an anchor without an `href` cannot take focus
2399
+ // without a tabindex, so the arrow keys stall on it.
2400
+ tabIndex: forwarded.tabIndex ?? 0,
2285
2401
  // After `forwarded` for the same reason as `type` below: a spread
2286
2402
  // carrying `role: undefined` would otherwise erase the default, and an
2287
2403
  // item with no role drops out of the menu and its arrow-key order.
@@ -3163,23 +3279,6 @@ const UNSTYLED_MODIFIER_COLORS = [
3163
3279
  'grey-lighter',
3164
3280
  ];
3165
3281
  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
3282
  /**
3184
3283
  * Dev warning for a `color` value whose `is-<color>` modifier has no shipped
3185
3284
  * CSS. `extraUnstyled` covers per-component dead values beyond the shared
@@ -3206,6 +3305,38 @@ const warnDeprecatedColorProp = (component, value, hint) => {
3206
3305
  `a visual effect. It will be removed in the next major version. ${hint}`);
3207
3306
  };
3208
3307
 
3308
+ /** A count read as a whole number from zero, or `fallback` when it's no number. */
3309
+ function whole(count, fallback) {
3310
+ return Number.isFinite(count) ? Math.max(Math.floor(count), 0) : fallback;
3311
+ }
3312
+ function range(from, to) {
3313
+ return Array.from({ length: Math.max(to - from + 1, 0) }, (_, i) => from + i);
3314
+ }
3315
+ /**
3316
+ * The pages to show for `current` of `total`: the first and last
3317
+ * `boundaries`, `siblings` on each side of the current one, and a gap for
3318
+ * each run left out. A gap of a single page shows that page instead, and the
3319
+ * window around the current page slides inward near either end, so the row
3320
+ * keeps its length as the page moves.
3321
+ */
3322
+ function pageItems(total, current, siblings, boundaries) {
3323
+ const windowSize = siblings * 2 + 1;
3324
+ // Every page fits in the room the boundary pages, the window and two gaps take.
3325
+ if (total <= boundaries * 2 + windowSize + 2)
3326
+ return range(1, total);
3327
+ const lowest = boundaries + 2;
3328
+ const highest = total - boundaries - 1;
3329
+ const start = Math.min(Math.max(current - siblings, lowest), highest - windowSize + 1);
3330
+ const end = start + windowSize - 1;
3331
+ const gap = (from, to, name) => to === from ? [from] : [name];
3332
+ return [
3333
+ ...range(1, boundaries),
3334
+ ...gap(boundaries + 1, start - 1, 'start-gap'),
3335
+ ...range(start, end),
3336
+ ...gap(end + 1, total - boundaries, 'end-gap'),
3337
+ ...range(total - boundaries + 1, total),
3338
+ ];
3339
+ }
3209
3340
  /**
3210
3341
  * "Previous" navigation button.
3211
3342
  *
@@ -3213,9 +3344,9 @@ const warnDeprecatedColorProp = (component, value, hint) => {
3213
3344
  * @param {PaginationPreviousNextProps} props - Props for the PaginationPrevious component.
3214
3345
  * @returns {JSX.Element} The rendered previous button.
3215
3346
  */
3216
- const PaginationPrevious = ({ className, disabled, children, ...props }) => (jsxRuntime.jsx("a", { className: classNames(usePrefixedClassNames('pagination-previous'), className, {
3347
+ const PaginationPrevious = ({ className, disabled, children, ...props }) => (jsxRuntime.jsx("a", { className: classNames(usePrefixedClassNames('pagination-previous', {
3217
3348
  'is-disabled': disabled,
3218
- }), "aria-disabled": disabled, tabIndex: disabled ? -1 : 0, ...props, onClick: disabled
3349
+ }), className), "aria-disabled": disabled, tabIndex: disabled ? -1 : 0, ...props, onClick: disabled
3219
3350
  ? e => {
3220
3351
  e.preventDefault();
3221
3352
  e.stopPropagation();
@@ -3228,9 +3359,7 @@ const PaginationPrevious = ({ className, disabled, children, ...props }) => (jsx
3228
3359
  * @param {PaginationPreviousNextProps} props - Props for the PaginationNext component.
3229
3360
  * @returns {JSX.Element} The rendered next button.
3230
3361
  */
3231
- const PaginationNext = ({ className, disabled, children, ...props }) => (jsxRuntime.jsx("a", { className: classNames(usePrefixedClassNames('pagination-next'), className, {
3232
- 'is-disabled': disabled,
3233
- }), "aria-disabled": disabled, tabIndex: disabled ? -1 : 0, ...props, onClick: disabled
3362
+ const PaginationNext = ({ className, disabled, children, ...props }) => (jsxRuntime.jsx("a", { className: classNames(usePrefixedClassNames('pagination-next', { 'is-disabled': disabled }), className), "aria-disabled": disabled, tabIndex: disabled ? -1 : 0, ...props, onClick: disabled
3234
3363
  ? e => {
3235
3364
  e.preventDefault();
3236
3365
  e.stopPropagation();
@@ -3244,7 +3373,8 @@ const PaginationNext = ({ className, disabled, children, ...props }) => (jsxRunt
3244
3373
  * @returns {JSX.Element} The rendered pagination.
3245
3374
  * @see {@link https://bulma.io/documentation/components/pagination/ | Bulma Pagination documentation}
3246
3375
  */
3247
- const PaginationComponent = ({ color, textColor, bgColor, size, align, rounded, className, children, ...props }) => {
3376
+ const PaginationComponent = ({ color, textColor, bgColor, size, align, rounded, total, current, onPageChange, siblingCount = 1, boundaryCount = 1, previousLabel = 'Previous', nextLabel = 'Next', disabled, getPageHref, getPageLabel, className, children, ...props }) => {
3377
+ const [kept, setKept] = React.useState(1);
3248
3378
  warnDeprecatedColorProp('Pagination', color, 'Use the textColor / bgColor helper props instead.');
3249
3379
  const { bulmaHelperClasses, rest } = useBulmaClasses({
3250
3380
  color: textColor,
@@ -3259,7 +3389,43 @@ const PaginationComponent = ({ color, textColor, bgColor, size, align, rounded,
3259
3389
  'is-rounded': rounded,
3260
3390
  });
3261
3391
  const paginationClasses = classNames(bulmaClasses, bulmaHelperClasses, className);
3262
- return (jsxRuntime.jsx("nav", { className: paginationClasses, role: "navigation", "aria-label": "pagination", ...rest, children: children }));
3392
+ // A child that renders nothing (`{show && …}` when false) is no children.
3393
+ const fromTotal = total !== undefined && React.Children.toArray(children).length === 0;
3394
+ let content = children;
3395
+ if (fromTotal) {
3396
+ const pages = Number.isFinite(total) ? Math.max(Math.floor(total), 0) : 0;
3397
+ // A current page that's no number reads as the first.
3398
+ const page = Math.min(Math.max(whole(current ?? kept, 1), 1), Math.max(pages, 1));
3399
+ const choose = (next) => {
3400
+ if (next === page)
3401
+ return;
3402
+ if (current === undefined)
3403
+ setKept(next);
3404
+ onPageChange?.(next);
3405
+ };
3406
+ /**
3407
+ * What a link to `target` is given, beside what it renders. With no
3408
+ * `getPageHref` it's a button; a link that's `off` goes nowhere.
3409
+ */
3410
+ const linkTo = (target, off) => ({
3411
+ href: off || !getPageHref ? undefined : getPageHref(target),
3412
+ role: getPageHref ? undefined : 'button',
3413
+ // A link that's off is disabled too, and the parts drop its clicks.
3414
+ onClick: () => choose(target),
3415
+ onKeyDown: getPageHref
3416
+ ? undefined
3417
+ : (e) => {
3418
+ if (off || (e.key !== 'Enter' && e.key !== ' '))
3419
+ return;
3420
+ e.preventDefault();
3421
+ choose(target);
3422
+ },
3423
+ });
3424
+ const atStart = disabled || page <= 1;
3425
+ const atEnd = disabled || page >= pages;
3426
+ content = (jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [jsxRuntime.jsx(PaginationPrevious, { disabled: atStart, ...linkTo(page - 1, atStart), children: previousLabel }), jsxRuntime.jsx(PaginationNext, { disabled: atEnd, ...linkTo(page + 1, atEnd), children: nextLabel }), jsxRuntime.jsx(PaginationList, { children: pageItems(pages, page, whole(siblingCount, 1), whole(boundaryCount, 1)).map(item => typeof item === 'number' ? (jsxRuntime.jsx(PaginationLink, { "aria-label": getPageLabel ? getPageLabel(item) : `Page ${item}`, active: item === page, disabled: disabled, ...linkTo(item, Boolean(disabled)), children: item }, item)) : (jsxRuntime.jsx(PaginationEllipsis, {}, item))) })] }));
3427
+ }
3428
+ return (jsxRuntime.jsx("nav", { className: paginationClasses, role: "navigation", "aria-label": "pagination", ...rest, children: content }));
3263
3429
  };
3264
3430
  /**
3265
3431
  * Container for page links and ellipsis.
@@ -3299,10 +3465,10 @@ const PaginationLink = ({ className, textColor, bgColor, active, disabled, onCli
3299
3465
  onClick(e);
3300
3466
  }
3301
3467
  };
3302
- return (jsxRuntime.jsx("li", { children: jsxRuntime.jsx("a", { className: classNames(usePrefixedClassNames('pagination-link'), bulmaHelperClasses, className, {
3468
+ return (jsxRuntime.jsx("li", { children: jsxRuntime.jsx("a", { className: classNames(usePrefixedClassNames('pagination-link', {
3303
3469
  'is-current': active,
3304
3470
  'is-disabled': disabled,
3305
- }), "aria-current": active ? 'page' : undefined, "aria-disabled": disabled, tabIndex: disabled ? -1 : 0, onClick: handleClick, ...rest, children: children }) }));
3471
+ }), bulmaHelperClasses, className), "aria-current": active ? 'page' : undefined, "aria-disabled": disabled, tabIndex: disabled ? -1 : 0, onClick: handleClick, ...rest, children: children }) }));
3306
3472
  };
3307
3473
  /**
3308
3474
  * Ellipsis separator.
@@ -4701,11 +4867,26 @@ let toasts = [];
4701
4867
  // Queued toasts: FIFO queue, one at a time
4702
4868
  let queuedToasts = [];
4703
4869
  let currentQueuedToast = null;
4870
+ // The server has nowhere to portal a toast to, and its copy of this module is
4871
+ // shared by every request, so server rendering reads an empty list. Hydration
4872
+ // reads it too, which keeps the first client render matching the server's.
4873
+ const noToasts = [];
4874
+ const getServerToasts = () => noToasts;
4875
+ // What a container shows right now: the stacked toasts, then the queued one on
4876
+ // screen. ToastContainer renders from this rather than from updates alone, so
4877
+ // toasts raised before it mounted still appear. It is replaced rather than
4878
+ // mutated, and only when listeners are notified, because useSyncExternalStore
4879
+ // needs the same array back between changes. An empty list is `noToasts`
4880
+ // itself, so a container that hydrates with nothing to show reads the same
4881
+ // snapshot the server did and has no reason to render again.
4882
+ let visibleToasts = noToasts;
4883
+ const getVisibleToasts = () => visibleToasts;
4704
4884
  const notifyListeners = () => {
4705
4885
  const allVisible = [...toasts];
4706
4886
  if (currentQueuedToast) {
4707
4887
  allVisible.push(currentQueuedToast);
4708
4888
  }
4889
+ visibleToasts = allVisible.length > 0 ? allVisible : noToasts;
4709
4890
  toastListeners.forEach(listener => listener([...allVisible]));
4710
4891
  };
4711
4892
  const processQueuedToast = () => {
@@ -4817,10 +4998,9 @@ const toast = {
4817
4998
  * @returns {JSX.Element | null} The rendered toast container, or null if empty.
4818
4999
  */
4819
5000
  const ToastContainer = ({ position = 'top-right', }) => {
4820
- const [toastList, setToastList] = React.useState([]);
4821
- React.useEffect(() => {
4822
- return toast.subscribe(setToastList);
4823
- }, []);
5001
+ // Starts from the toasts already showing instead of an empty list, then
5002
+ // follows changes.
5003
+ const toastList = React.useSyncExternalStore(toast.subscribe, getVisibleToasts, getServerToasts);
4824
5004
  if (typeof document === 'undefined' || toastList.length === 0) {
4825
5005
  return null;
4826
5006
  }
@@ -4972,6 +5152,14 @@ const Dialog = React.forwardRef(({ isOpen, title, message, type = 'default', con
4972
5152
  Dialog.displayName = 'Dialog';
4973
5153
  let dialogListeners = new Set();
4974
5154
  let currentDialog = null;
5155
+ // DialogContainer renders from the open dialog rather than from updates alone,
5156
+ // so a dialog raised before it mounted still opens once it does.
5157
+ const getCurrentDialog = () => currentDialog;
5158
+ // The server renders no dialog: its copy of this module is shared by every
5159
+ // request, so the dialog it holds may belong to another page. Hydration reads
5160
+ // the same empty value, which keeps the first client render matching the
5161
+ // server's, and the dialog opens right after.
5162
+ const getServerDialog = () => null;
4975
5163
  const notifyDialogListeners = () => {
4976
5164
  dialogListeners.forEach(listener => listener(currentDialog));
4977
5165
  };
@@ -5033,10 +5221,8 @@ const dialog = {
5033
5221
  * @returns {JSX.Element | null} The rendered dialog, or null if none is active.
5034
5222
  */
5035
5223
  const DialogContainer = () => {
5036
- const [current, setCurrent] = React.useState(null);
5037
- React.useEffect(() => {
5038
- return dialog.subscribe(setCurrent);
5039
- }, []);
5224
+ // Starts from the dialog already open instead of none, then follows changes.
5225
+ const current = React.useSyncExternalStore(dialog.subscribe, getCurrentDialog, getServerDialog);
5040
5226
  if (!current) {
5041
5227
  return null;
5042
5228
  }
@@ -6110,11 +6296,28 @@ let notifications = [];
6110
6296
  // Queue support
6111
6297
  let queuedNotifications = [];
6112
6298
  let currentQueuedNotification = null;
6299
+ // The server has nowhere to portal a notification to, and its copy of this
6300
+ // module is shared by every request, so server rendering reads an empty list.
6301
+ // Hydration reads it too, which keeps the first client render matching the
6302
+ // server's.
6303
+ const noNotifications = [];
6304
+ const getServerNotifications = () => noNotifications;
6305
+ // What a container shows right now: the stacked notifications, then the queued
6306
+ // one on screen. NotificationContainer renders from this rather than from
6307
+ // updates alone, so notifications raised before it mounted still appear. It is
6308
+ // replaced rather than mutated, and only when listeners are notified, because
6309
+ // useSyncExternalStore needs the same array back between changes. An empty
6310
+ // list is `noNotifications` itself, so a container that hydrates with nothing
6311
+ // to show reads the same snapshot the server did and has no reason to render
6312
+ // again.
6313
+ let visibleNotifications = noNotifications;
6314
+ const getVisibleNotifications = () => visibleNotifications;
6113
6315
  const notifyNotificationListeners = () => {
6114
6316
  const allVisible = [...notifications];
6115
6317
  if (currentQueuedNotification) {
6116
6318
  allVisible.push(currentQueuedNotification);
6117
6319
  }
6320
+ visibleNotifications = allVisible.length > 0 ? allVisible : noNotifications;
6118
6321
  notificationListeners.forEach(listener => listener([...allVisible]));
6119
6322
  };
6120
6323
  const processQueuedNotification = () => {
@@ -6250,19 +6453,16 @@ const NotificationItem = ({ instance, onClose }) => {
6250
6453
  };
6251
6454
  /**
6252
6455
  * Container component for rendering programmatic notifications.
6253
- * Place once at your app root to enable the notification API. Mount it
6254
- * before calling `notification`: a notification shown while no container is
6255
- * mounted doesn't appear when one mounts, only alongside the next call.
6456
+ * Place once at your app root to enable the notification API.
6256
6457
  *
6257
6458
  * @function
6258
6459
  * @param {{ position?: NotificationPosition }} props - Container props.
6259
6460
  * @returns {JSX.Element | null} The rendered notification container, or null if empty.
6260
6461
  */
6261
6462
  const NotificationContainer = ({ position = 'top-right' }) => {
6262
- const [items, setItems] = React.useState([]);
6263
- React.useEffect(() => {
6264
- return notification.subscribe(setItems);
6265
- }, []);
6463
+ // Starts from the notifications already showing instead of an empty list,
6464
+ // then follows changes.
6465
+ const items = React.useSyncExternalStore(notification.subscribe, getVisibleNotifications, getServerNotifications);
6266
6466
  if (typeof document === 'undefined' || items.length === 0) {
6267
6467
  return null;
6268
6468
  }
@@ -6828,7 +7028,21 @@ const RadiosProvider = RadiosContext.Provider;
6828
7028
  const CheckboxesProvider = CheckboxesContext.Provider;
6829
7029
 
6830
7030
  /**
6831
- * Valid colors for the Checkbox component.
7031
+ * The values the Checkbox `color` prop accepts, as a readonly tuple.
7032
+ *
7033
+ * `CheckboxProps['color']` is typed from it, so the two list the same values.
7034
+ * Map over it to build a color picker, or check a value that arrives at
7035
+ * runtime before passing it in: the component adds no color class for a value
7036
+ * outside the tuple.
7037
+ *
7038
+ * @example
7039
+ * import { Checkbox, checkboxColors } from '@allxsmith/bestax-bulma';
7040
+ *
7041
+ * checkboxColors.map(color => (
7042
+ * <Checkbox key={color} color={color}>
7043
+ * {color}
7044
+ * </Checkbox>
7045
+ * ));
6832
7046
  */
6833
7047
  const checkboxColors = [
6834
7048
  'primary',
@@ -6839,7 +7053,17 @@ const checkboxColors = [
6839
7053
  'danger',
6840
7054
  ];
6841
7055
  /**
6842
- * Valid sizes for the Checkbox component.
7056
+ * The values the Checkbox `size` prop accepts, as a readonly tuple.
7057
+ *
7058
+ * `CheckboxProps['size']` is typed from it, so the two list the same values.
7059
+ * Use it to offer a size choice or to check a value that arrives at runtime:
7060
+ * the component adds no size class for a value outside the tuple. These are
7061
+ * element sizes, not the spacing scale in `validSizes`.
7062
+ *
7063
+ * @example
7064
+ * import { checkboxSizes } from '@allxsmith/bestax-bulma';
7065
+ *
7066
+ * type CheckboxSize = (typeof checkboxSizes)[number];
6843
7067
  */
6844
7068
  const checkboxSizes = ['small', 'normal', 'medium', 'large'];
6845
7069
  /**
@@ -7284,7 +7508,23 @@ label, labelSize, labelProps, horizontal, message, messageColor, fieldClassName,
7284
7508
  File.displayName = 'File';
7285
7509
 
7286
7510
  /**
7287
- * Valid colors for the Radio component.
7511
+ * The values the Radio `color` prop accepts, as a readonly tuple.
7512
+ *
7513
+ * `RadioProps['color']` is typed from it, so the two list the same values.
7514
+ * Map over it to build a color picker, or check a value that arrives at
7515
+ * runtime before passing it in: the component adds no color class for a value
7516
+ * outside the tuple.
7517
+ *
7518
+ * @example
7519
+ * import { Radio, Radios, radioColors } from '@allxsmith/bestax-bulma';
7520
+ *
7521
+ * <Radios name="accent" defaultValue="primary">
7522
+ * {radioColors.map(color => (
7523
+ * <Radio key={color} value={color} color={color}>
7524
+ * {color}
7525
+ * </Radio>
7526
+ * ))}
7527
+ * </Radios>;
7288
7528
  */
7289
7529
  const radioColors = [
7290
7530
  'primary',
@@ -7295,7 +7535,17 @@ const radioColors = [
7295
7535
  'danger',
7296
7536
  ];
7297
7537
  /**
7298
- * Valid sizes for the Radio component.
7538
+ * The values the Radio `size` prop accepts, as a readonly tuple.
7539
+ *
7540
+ * `RadioProps['size']` is typed from it, so the two list the same values. Use
7541
+ * it to offer a size choice or to check a value that arrives at runtime: the
7542
+ * component adds no size class for a value outside the tuple. These are
7543
+ * element sizes, not the spacing scale in `validSizes`.
7544
+ *
7545
+ * @example
7546
+ * import { radioSizes } from '@allxsmith/bestax-bulma';
7547
+ *
7548
+ * type RadioSize = (typeof radioSizes)[number];
7299
7549
  */
7300
7550
  const radioSizes = ['small', 'normal', 'medium', 'large'];
7301
7551
  /**
@@ -7426,7 +7676,21 @@ const RadiosComponent = ({ label, labelSize, labelProps, horizontal, message, me
7426
7676
  const Radios = withSubComponents(RadiosComponent, { Radio }, 'Radios');
7427
7677
 
7428
7678
  /**
7429
- * Valid colors for the Switch component.
7679
+ * The values the Switch `color` and `passiveType` props accept, as a readonly
7680
+ * tuple.
7681
+ *
7682
+ * Both props are typed from it, so they list the same values. Map over it to
7683
+ * build a color picker, or check a value that arrives at runtime before
7684
+ * passing it in: the component adds no class for a value outside the tuple.
7685
+ *
7686
+ * @example
7687
+ * import { Switch, switchColors } from '@allxsmith/bestax-bulma';
7688
+ *
7689
+ * switchColors.map(color => (
7690
+ * <Switch key={color} color={color} defaultChecked>
7691
+ * {color}
7692
+ * </Switch>
7693
+ * ));
7430
7694
  */
7431
7695
  const switchColors = [
7432
7696
  'primary',
@@ -7437,7 +7701,17 @@ const switchColors = [
7437
7701
  'danger',
7438
7702
  ];
7439
7703
  /**
7440
- * Valid sizes for the Switch component.
7704
+ * The values the Switch `size` prop accepts, as a readonly tuple.
7705
+ *
7706
+ * `SwitchProps['size']` is typed from it, so the two list the same values. Use
7707
+ * it to offer a size choice or to check a value that arrives at runtime: the
7708
+ * component adds no size class for a value outside the tuple. These are
7709
+ * element sizes, not the spacing scale in `validSizes`.
7710
+ *
7711
+ * @example
7712
+ * import { switchSizes } from '@allxsmith/bestax-bulma';
7713
+ *
7714
+ * type SwitchSize = (typeof switchSizes)[number];
7441
7715
  */
7442
7716
  const switchSizes = ['small', 'normal', 'medium', 'large'];
7443
7717
  /**
@@ -12933,17 +13207,130 @@ function cssVarToProp(varName) {
12933
13207
  .join('');
12934
13208
  }
12935
13209
  /**
12936
- * Mapping of camelCase prop names to their Bulma CSS variable counterparts.
13210
+ * Prop names `cssVarToProp` would mint that are already `BulmaOtherProps`
13211
+ * helper props: `--bulma-shadow` becomes `shadow` and `--bulma-radius` becomes
13212
+ * `radius`. They stay out of `bulmaVarPropMap`, so on Theme each is the helper
13213
+ * prop it is on every other component, and both variables are still reachable
13214
+ * through `bulmaVars`.
12937
13215
  *
12938
- * `--bulma-shadow` is excluded: `cssVarToProp` would mint it as `shadow`,
12939
- * which already exists as a `BulmaOtherProps` prop applied via
12940
- * `useBulmaClasses` — keeping it out of this map means that
12941
- * prop keeps its existing class-based meaning, while `--bulma-shadow` is
12942
- * still reachable through the `bulmaVars` object.
13216
+ * `radius` was missed here once and set `--bulma-radius` while typed as the
13217
+ * helper (#694). It still writes that variable, so what it did then keeps
13218
+ * working; see `themeRadiusVar`.
13219
+ */
13220
+ const helperPropNames = ['shadow', 'radius'];
13221
+ /**
13222
+ * Mapping of camelCase prop names to their Bulma CSS variable counterparts,
13223
+ * minus the `helperPropNames` above.
12943
13224
  */
12944
13225
  const bulmaVarPropMap = Object.fromEntries(bulmaCssVars
12945
13226
  .map(cssVar => [cssVarToProp(cssVar), cssVar])
12946
- .filter(([prop]) => prop !== 'shadow'));
13227
+ .filter(([prop]) => !helperPropNames.includes(prop)));
13228
+ /**
13229
+ * What each helper value of `radius` writes to `--bulma-radius` on a Theme,
13230
+ * alongside its class.
13231
+ *
13232
+ * Before #694, `radius="radiusless"` wrote `--bulma-radius: radiusless`. That
13233
+ * is not a length, and a declaration reading an invalid variable falls back
13234
+ * to its property's initial value, which for a border radius is 0. So
13235
+ * everything inside the Theme that takes its radius from that variable lost
13236
+ * it, and under `isRoot` so did everything on the page that does. Writing a
13237
+ * real 0 keeps what
13238
+ * people see and makes it valid CSS. Keyed by the helper values, so adding
13239
+ * one means saying what it writes.
13240
+ */
13241
+ const radiusHelperVars = {
13242
+ radiusless: '0',
13243
+ };
13244
+ /**
13245
+ * What `radius` writes to `--bulma-radius` on a Theme, or `undefined` when it
13246
+ * writes nothing.
13247
+ *
13248
+ * A helper value writes its `radiusHelperVars` entry. Any other non-empty
13249
+ * string is written as given: before #694 every `radius` on Theme set the
13250
+ * variable whatever its type said, and a JavaScript caller passing a length
13251
+ * got the radius it asked for. That keeps working so nothing breaks, but the
13252
+ * type rejects it, so it warns in development and points at `bulmaVars`.
13253
+ *
13254
+ * Only a string writes anything, because only a string ever set the variable
13255
+ * to something usable: an empty value or zero was dropped, and a boolean or
13256
+ * any other bare number is not a length. The helper ignores those too, the
13257
+ * way it does on every other component.
13258
+ */
13259
+ const themeRadiusVar = (radius) => {
13260
+ if (typeof radius !== 'string' || radius === '') {
13261
+ return undefined;
13262
+ }
13263
+ if (validRadii.includes(radius)) {
13264
+ return radiusHelperVars[radius];
13265
+ }
13266
+ warnOnce('Theme:radius-variable', `[bestax-bulma] <Theme radius="${radius}">: setting --bulma-radius ` +
13267
+ 'through the radius prop is deprecated and will stop working in a ' +
13268
+ 'future major version. On Theme, as on every other component, radius ' +
13269
+ `is the "${validRadii.join('", "')}" helper. Set the variable with ` +
13270
+ `bulmaVars={{ '--bulma-radius': '${radius}' }} instead.`);
13271
+ return radius;
13272
+ };
13273
+ /** The one `<style>` element every `isRoot` Theme writes into. */
13274
+ const ROOT_STYLE_ID = 'bestax-bulma-theme-vars';
13275
+ /**
13276
+ * The `:root` rules of every mounted `isRoot` Theme that has any, keyed by the
13277
+ * Theme's render order.
13278
+ *
13279
+ * Root Themes share one `<style>` element, so each keeps its rules here and
13280
+ * the element is rebuilt from all of them whenever one mounts, changes or
13281
+ * unmounts. Before this each Theme overwrote the element with only its own
13282
+ * rules, and the first to unmount removed it for all of them (#736).
13283
+ *
13284
+ * The key is a number each Theme takes when it first renders. React renders a
13285
+ * parent before its children and an earlier sibling before a later one, so an
13286
+ * inner or later-mounted root Theme sorts later, comes later in the
13287
+ * stylesheet, and wins a variable two of them set, as an inner scoped Theme
13288
+ * does. Effects would give the wrong answer for nesting: React runs a child's
13289
+ * effects before its parent's. Only the relative order matters, so a number
13290
+ * skipped by StrictMode calling the initializer twice, or by a render React
13291
+ * throws away, is harmless. The key never changes, so an update keeps its
13292
+ * place and re-rendering one Theme never changes which one wins.
13293
+ */
13294
+ const rootThemeRules = new Map();
13295
+ let nextRootOrder = 0;
13296
+ /**
13297
+ * Write every registered root Theme's rules into the shared element, creating
13298
+ * it when needed and removing it once no Theme has any rules left.
13299
+ */
13300
+ const renderRootThemeRules = () => {
13301
+ let element = document.getElementById(ROOT_STYLE_ID);
13302
+ if (rootThemeRules.size === 0) {
13303
+ element?.remove();
13304
+ return;
13305
+ }
13306
+ if (!element) {
13307
+ element = document.createElement('style');
13308
+ element.id = ROOT_STYLE_ID;
13309
+ document.head.appendChild(element);
13310
+ }
13311
+ element.textContent = [...rootThemeRules]
13312
+ .sort(([a], [b]) => a - b)
13313
+ .map(([, rules]) => rules)
13314
+ .join('\n');
13315
+ };
13316
+ /**
13317
+ * Record one Theme's `:root` rules, where an empty string means it has none,
13318
+ * and rebuild the shared element if that changed anything. A Theme with no
13319
+ * rules never touches the element, which is what it did before the registry
13320
+ * too.
13321
+ */
13322
+ const setRootThemeRules = (order, rules) => {
13323
+ if ((rootThemeRules.get(order) ?? '') === rules) {
13324
+ return;
13325
+ }
13326
+ if (rules) {
13327
+ rootThemeRules.set(order, rules);
13328
+ }
13329
+ else {
13330
+ rootThemeRules.delete(order);
13331
+ }
13332
+ renderRootThemeRules();
13333
+ };
12947
13334
  /**
12948
13335
  * Theme component that injects Bulma CSS variables either globally or locally.
12949
13336
  *
@@ -12960,7 +13347,11 @@ const bulmaVarPropMap = Object.fromEntries(bulmaCssVars
12960
13347
  * <Button color="primary">Themed</Button>
12961
13348
  * </Theme>
12962
13349
  */
12963
- const Theme = ({ bulmaVars = {}, children, className, isRoot = false, colorMode, ...restProps }) => {
13350
+ const Theme = ({ bulmaVars = {}, children, className, isRoot = false, colorMode, radius, ...restProps }) => {
13351
+ const radiusVar = themeRadiusVar(radius);
13352
+ const radiusHelper = validRadii.includes(radius)
13353
+ ? radius
13354
+ : undefined;
12964
13355
  // Extract Bulma variable props from restProps
12965
13356
  const { bulmaVarProps, otherProps } = React.useMemo(() => {
12966
13357
  const varProps = {};
@@ -12975,8 +13366,12 @@ const Theme = ({ bulmaVars = {}, children, className, isRoot = false, colorMode,
12975
13366
  }
12976
13367
  return { bulmaVarProps: varProps, otherProps: otherPropsObj };
12977
13368
  }, [restProps]);
12978
- // Use Bulma classes for styling (only when not isRoot)
12979
- const { bulmaHelperClasses, rest } = useBulmaClasses(otherProps);
13369
+ // Use Bulma classes for styling (only when not isRoot). Only a helper value
13370
+ // of `radius` reaches the helper; a legacy string went to the variable.
13371
+ const { bulmaHelperClasses, rest } = useBulmaClasses({
13372
+ ...otherProps,
13373
+ radius: radiusHelper,
13374
+ });
12980
13375
  // Merge bulmaVars and individual props, with props taking precedence
12981
13376
  const mergedVars = React.useMemo(() => {
12982
13377
  const vars = { ...bulmaVars };
@@ -12985,37 +13380,34 @@ const Theme = ({ bulmaVars = {}, children, className, isRoot = false, colorMode,
12985
13380
  vars[cssVar] = bulmaVarProps[propName];
12986
13381
  }
12987
13382
  }
13383
+ if (radiusVar !== undefined) {
13384
+ vars['--bulma-radius'] = radiusVar;
13385
+ }
12988
13386
  return vars;
12989
- }, [bulmaVars, bulmaVarProps]);
12990
- // Inject CSS variables globally at :root level
12991
- React.useEffect(() => {
13387
+ }, [bulmaVars, bulmaVarProps, radiusVar]);
13388
+ // This Theme's place among root Themes; see `rootThemeRules`.
13389
+ const [rootOrder] = React.useState(() => nextRootOrder++);
13390
+ // The `:root` rules this Theme contributes, or '' when it contributes none.
13391
+ const rootRules = React.useMemo(() => {
12992
13392
  if (!isRoot) {
12993
- return;
12994
- }
12995
- const validVars = Object.entries(mergedVars).filter(([key, value]) => bulmaCssVars.includes(key) && value);
12996
- if (validVars.length === 0) {
12997
- return;
12998
- }
12999
- // Create and inject a style element for global CSS variables
13000
- const styleId = 'bestax-bulma-theme-vars';
13001
- let styleElement = document.getElementById(styleId);
13002
- if (!styleElement) {
13003
- styleElement = document.createElement('style');
13004
- styleElement.id = styleId;
13005
- document.head.appendChild(styleElement);
13393
+ return '';
13006
13394
  }
13007
- const cssRules = validVars
13395
+ const cssRules = Object.entries(mergedVars)
13396
+ .filter(([key, value]) => bulmaCssVars.includes(key) && value)
13008
13397
  .map(([key, value]) => `${key}: ${value};`)
13009
13398
  .join(' ');
13010
- styleElement.textContent = `:root { ${cssRules} }`;
13011
- // Cleanup function to remove the style element when component unmounts
13012
- return () => {
13013
- const element = document.getElementById(styleId);
13014
- if (element) {
13015
- element.remove();
13016
- }
13017
- };
13399
+ return cssRules ? `:root { ${cssRules} }` : '';
13018
13400
  }, [mergedVars, isRoot]);
13401
+ // Inject CSS variables globally at :root level, alongside any other root
13402
+ // Themes. Clearing `isRoot` or every variable passes '', which withdraws
13403
+ // this Theme's rules.
13404
+ React.useEffect(() => {
13405
+ setRootThemeRules(rootOrder, rootRules);
13406
+ }, [rootOrder, rootRules]);
13407
+ // Withdraw them on unmount. Kept apart from the effect above so a change
13408
+ // rewrites the shared element in place rather than removing and recreating
13409
+ // it between the cleanup and the next run.
13410
+ React.useEffect(() => () => setRootThemeRules(rootOrder, ''), [rootOrder]);
13019
13411
  // Toggle Bulma's light/dark scheme by writing the `data-theme` attribute on
13020
13412
  // the document root (<html>). This is always global, even on a scoped Theme.
13021
13413
  // `'system'` removes the attribute so Bulma follows the OS preference.