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