@allxsmith/bestax-bulma 5.45.0 → 5.46.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (38) hide show
  1. package/dist/esm/components/Modal.js +20 -7
  2. package/dist/esm/components/Modal.js.map +1 -1
  3. package/dist/esm/form/Checkbox.js.map +1 -1
  4. package/dist/esm/form/File.js.map +1 -1
  5. package/dist/esm/form/Numberinput.js.map +1 -1
  6. package/dist/esm/form/PinInput.js.map +1 -1
  7. package/dist/esm/form/Radio.js.map +1 -1
  8. package/dist/esm/form/SelectBase.js.map +1 -1
  9. package/dist/esm/form/Slider.js.map +1 -1
  10. package/dist/esm/form/Switch.js.map +1 -1
  11. package/dist/esm/helpers/Theme.js +191 -49
  12. package/dist/esm/helpers/Theme.js.map +1 -1
  13. package/dist/esm/helpers/useVisibilityClasses.js.map +1 -1
  14. package/dist/index.cjs +209 -55
  15. package/dist/index.cjs.map +1 -1
  16. package/dist/types/components/Modal.d.ts +10 -1
  17. package/dist/types/form/Checkbox.d.ts +3 -0
  18. package/dist/types/form/File.d.ts +3 -0
  19. package/dist/types/form/Numberinput.d.ts +2 -0
  20. package/dist/types/form/PinInput.d.ts +3 -0
  21. package/dist/types/form/Radio.d.ts +3 -0
  22. package/dist/types/form/SelectBase.d.ts +3 -0
  23. package/dist/types/form/Slider.d.ts +3 -0
  24. package/dist/types/form/Switch.d.ts +2 -0
  25. package/dist/types/helpers/Theme.d.ts +37 -10
  26. package/dist/types/helpers/useVisibilityClasses.d.ts +5 -1
  27. package/dist/types-cjs/components/Modal.d.ts +10 -1
  28. package/dist/types-cjs/form/Checkbox.d.ts +3 -0
  29. package/dist/types-cjs/form/File.d.ts +3 -0
  30. package/dist/types-cjs/form/Numberinput.d.ts +2 -0
  31. package/dist/types-cjs/form/PinInput.d.ts +3 -0
  32. package/dist/types-cjs/form/Radio.d.ts +3 -0
  33. package/dist/types-cjs/form/SelectBase.d.ts +3 -0
  34. package/dist/types-cjs/form/Slider.d.ts +3 -0
  35. package/dist/types-cjs/form/Switch.d.ts +2 -0
  36. package/dist/types-cjs/helpers/Theme.d.ts +37 -10
  37. package/dist/types-cjs/helpers/useVisibilityClasses.d.ts +5 -1
  38. package/package.json +1 -1
@@ -6,6 +6,7 @@ import { useBulmaClasses } from './useBulmaClasses.js';
6
6
  import { validRadii, validGaps } from './bulmaClassHelpers.js';
7
7
  import { warnOnce, isDev } from './devWarnings.js';
8
8
  import { useClassPrefix } from './Config.js';
9
+ import { useClientLayoutEffect } from './useClientLayoutEffect.js';
9
10
 
10
11
  // --- Bulma v1 CSS variables Theme sets (originally generated from
11
12
  // CSSVAR_KEYS), in two lists by where Bulma declares them ---
@@ -60,6 +61,21 @@ const bulmaGlobalVars = [
60
61
  '--bulma-danger-h',
61
62
  '--bulma-danger-s',
62
63
  '--bulma-danger-l',
64
+ // the lightness of each color's label on a solid surface, and of its text
65
+ // on the page, which Bulma's Sass build picks for contrast with the color
66
+ // it was built with, so a new trio does not move them
67
+ '--bulma-primary-invert-l',
68
+ '--bulma-primary-on-scheme-l',
69
+ '--bulma-link-invert-l',
70
+ '--bulma-link-on-scheme-l',
71
+ '--bulma-info-invert-l',
72
+ '--bulma-info-on-scheme-l',
73
+ '--bulma-success-invert-l',
74
+ '--bulma-success-on-scheme-l',
75
+ '--bulma-warning-invert-l',
76
+ '--bulma-warning-on-scheme-l',
77
+ '--bulma-danger-invert-l',
78
+ '--bulma-danger-on-scheme-l',
63
79
  // shadow (the upstream token .box/.card/.dropdown/.panel derive their own
64
80
  // shadow variables from; overriding it from an ancestor is the route that
65
81
  // cascades, since those selectors re-declare their derived var on
@@ -810,6 +826,104 @@ const setRootThemeRules = (order, rules) => {
810
826
  }
811
827
  renderRootThemeRules();
812
828
  };
829
+ /** A value given per scheme, rather than as one string. */
830
+ const isSchemeValues = (value) => typeof value === 'object' && value !== null && !Array.isArray(value);
831
+ /**
832
+ * The selector of one of Bulma's schemes, as the stylesheet Theme is used
833
+ * with writes it: `[data-theme=dark], .theme-dark`, or with the class prefix
834
+ * in both names (`[data-bestax-theme=dark], .bestax-theme-dark`) for a
835
+ * prefixed build. A prefix that cannot be written into a selector as it
836
+ * stands gets the plain names, which `colorMode` writes under any prefix.
837
+ */
838
+ const schemeSelector = (classPrefix, scheme) => {
839
+ const prefix = /^[A-Za-z_][\w-]*$/.test(classPrefix) ? classPrefix : '';
840
+ return `[data-${prefix}theme=${scheme}], .${prefix}theme-${scheme}`;
841
+ };
842
+ /**
843
+ * The `:root` rules for variables given a value per scheme, laid out the
844
+ * way Bulma lays out its own schemes so each value applies exactly where
845
+ * Bulma's would: under the visitor's OS preference, unless the page names
846
+ * the other scheme, and wherever the page names this one, on `<html>` or on
847
+ * an element inside. A scheme with no value gets no rules, which leaves the
848
+ * stylesheet's own value there.
849
+ *
850
+ * Every selector is as specific as `:root` (the guard is in `:where()`), so
851
+ * between Themes, and against a plain `:root` rule, the later one still
852
+ * wins, as it does for every other variable.
853
+ */
854
+ const schemeRules = (vars, classPrefix) => {
855
+ const other = { light: 'dark', dark: 'light' };
856
+ const body = (scheme) => vars
857
+ .filter(([, values]) => values[scheme])
858
+ .map(([key, values]) => `${key}: ${values[scheme]};`)
859
+ .join(' ');
860
+ const schemes = ['light', 'dark']
861
+ .map(scheme => [scheme, body(scheme)])
862
+ .filter(([, declarations]) => declarations);
863
+ // Bulma's order: both preferences, then both names, the dark one last.
864
+ return [
865
+ ...schemes.map(([scheme, declarations]) => `@media (prefers-color-scheme: ${scheme}) { :root:where(:not(` +
866
+ `${schemeSelector(classPrefix, other[scheme])})) { ${declarations} } }`),
867
+ ...schemes.map(([scheme, declarations]) => `${schemeSelector(classPrefix, scheme)} { ${declarations} }`),
868
+ ];
869
+ };
870
+ const schemeAttributes = new Map();
871
+ /** Set a theme attribute on `<html>`, or remove it for `null`. */
872
+ const setSchemeAttribute = (name, value) => {
873
+ const root = document.documentElement;
874
+ if (value === null) {
875
+ root.removeAttribute(name);
876
+ }
877
+ else {
878
+ root.setAttribute(name, value);
879
+ }
880
+ };
881
+ /**
882
+ * Write one Theme's value for a theme attribute, `null` removing it, and
883
+ * make that Theme the attribute's latest writer. Throws, recording nothing,
884
+ * when `name` cannot be an attribute name.
885
+ *
886
+ * A Theme that writes the value it already holds keeps its place and leaves
887
+ * the page alone. React runs a layout effect again when a `Suspense`
888
+ * boundary shows its tree again, with nothing changed, and that must not
889
+ * take the attribute back from a Theme that wrote after it.
890
+ */
891
+ const writeSchemeAttribute = (order, name, value) => {
892
+ const held = schemeAttributes.get(name);
893
+ if (held?.writers.some(writer => writer.order === order && writer.value === value)) {
894
+ return;
895
+ }
896
+ const entry = held ?? {
897
+ original: document.documentElement.getAttribute(name),
898
+ writers: [],
899
+ };
900
+ setSchemeAttribute(name, value);
901
+ entry.writers = entry.writers.filter(writer => writer.order !== order);
902
+ entry.writers.push({ order, value });
903
+ schemeAttributes.set(name, entry);
904
+ };
905
+ /**
906
+ * Take one Theme off every theme attribute it wrote, except those named in
907
+ * `keep`. An attribute it was the latest writer of goes back to the value of
908
+ * the writer before it, or to what it held before any of them.
909
+ */
910
+ const releaseSchemeAttributes = (order, keep = []) => {
911
+ for (const [name, entry] of schemeAttributes) {
912
+ const index = entry.writers.findIndex(writer => writer.order === order);
913
+ if (index === -1 || keep.includes(name)) {
914
+ continue;
915
+ }
916
+ const latest = index === entry.writers.length - 1;
917
+ entry.writers.splice(index, 1);
918
+ if (entry.writers.length === 0) {
919
+ schemeAttributes.delete(name);
920
+ setSchemeAttribute(name, entry.original);
921
+ }
922
+ else if (latest) {
923
+ setSchemeAttribute(name, entry.writers[entry.writers.length - 1].value);
924
+ }
925
+ }
926
+ };
813
927
  /**
814
928
  * Theme component that injects Bulma CSS variables either globally or locally.
815
929
  *
@@ -896,7 +1010,9 @@ const Theme = ({ bulmaVars = {}, children, className, isRoot = false, colorMode,
896
1010
  });
897
1011
  // Merge bulmaVars and individual props, with props taking precedence
898
1012
  const mergedVars = useMemo(() => {
899
- const vars = { ...bulmaVars };
1013
+ const vars = {
1014
+ ...bulmaVars,
1015
+ };
900
1016
  for (const [propName, cssVar] of Object.entries(bulmaVarPropMap)) {
901
1017
  if (bulmaVarProps[propName] !== undefined) {
902
1018
  vars[cssVar] = bulmaVarProps[propName];
@@ -910,61 +1026,99 @@ const Theme = ({ bulmaVars = {}, children, className, isRoot = false, colorMode,
910
1026
  }
911
1027
  return vars;
912
1028
  }, [bulmaVars, bulmaVarProps, radiusVar, columnGapVar]);
1029
+ // The variables Theme writes, split by form: a value per scheme, or one
1030
+ // value for every scheme. A key Theme does not take, or an empty value, is
1031
+ // left out. A plain value is written as given, so a number untyped code
1032
+ // passes still works.
1033
+ const { plainVars, perSchemeVars } = useMemo(() => {
1034
+ const plain = [];
1035
+ const perScheme = [];
1036
+ for (const [key, value] of Object.entries(mergedVars)) {
1037
+ if (!value || !bulmaCssVars.includes(key)) {
1038
+ continue;
1039
+ }
1040
+ if (isSchemeValues(value)) {
1041
+ perScheme.push([key, value]);
1042
+ }
1043
+ else {
1044
+ plain.push([key, value]);
1045
+ }
1046
+ }
1047
+ return { plainVars: plain, perSchemeVars: perScheme };
1048
+ }, [mergedVars]);
913
1049
  // From an effect rather than the render body, so rendering does none of
914
1050
  // this work, and a server render none at all. In production it returns
915
1051
  // at its development check before looking at the variables.
916
1052
  useEffect(() => {
917
1053
  warnComponentVars(mergedVars);
918
1054
  }, [mergedVars]);
919
- // This Theme's place among root Themes; see `rootThemeRules`.
1055
+ // A scoped Theme writes its variables as an inline style, which has no way
1056
+ // to say which scheme a value is for, so it skips a value given per
1057
+ // scheme. Say so, since nothing on the page does.
1058
+ useEffect(() => {
1059
+ if (isRoot || perSchemeVars.length === 0 || !isDev()) {
1060
+ return;
1061
+ }
1062
+ const keys = perSchemeVars.map(([key]) => key).sort();
1063
+ warnOnce(`Theme:scoped-scheme-values:${keys.join(',')}`, `[bestax-bulma] <Theme>: a scoped Theme writes its variables as an ` +
1064
+ `inline style, which cannot hold a value per scheme, so ` +
1065
+ `${keys.join(', ')} ${keys.length === 1 ? 'is' : 'are'} not set. ` +
1066
+ 'Give a single string, set the values on an isRoot Theme, or set ' +
1067
+ 'them in your CSS.');
1068
+ }, [isRoot, perSchemeVars]);
1069
+ // This Theme's place among the Themes on the page; see `rootThemeRules`
1070
+ // and `schemeAttributes`.
920
1071
  const [rootOrder] = useState(() => nextRootOrder++);
921
- // The `:root` rules this Theme contributes, or '' when it contributes none.
1072
+ // Bulma names its scheme attribute after the class prefix a stylesheet was
1073
+ // built with (`data-<prefix>theme`), so the prefixed builds read
1074
+ // `data-bestax-theme` and never `data-theme`. Its scheme classes take the
1075
+ // prefix too (`.bestax-theme-dark`).
1076
+ const classPrefix = useClassPrefix();
1077
+ // The `:root` rules this Theme contributes, or '' when it contributes none:
1078
+ // one block for the plain values, then the rules for each value given per
1079
+ // scheme.
922
1080
  const rootRules = useMemo(() => {
923
1081
  if (!isRoot) {
924
1082
  return '';
925
1083
  }
926
- const cssRules = Object.entries(mergedVars)
927
- .filter(([key, value]) => bulmaCssVars.includes(key) && value)
928
- .map(([key, value]) => `${key}: ${value};`)
929
- .join(' ');
930
- return cssRules ? `:root { ${cssRules} }` : '';
931
- }, [mergedVars, isRoot]);
1084
+ const plain = plainVars.map(([key, value]) => `${key}: ${value};`);
1085
+ return [
1086
+ ...(plain.length > 0 ? [`:root { ${plain.join(' ')} }`] : []),
1087
+ ...schemeRules(perSchemeVars, classPrefix),
1088
+ ].join('\n');
1089
+ }, [plainVars, perSchemeVars, classPrefix, isRoot]);
932
1090
  // Inject CSS variables globally at :root level, alongside any other root
933
1091
  // Themes. Clearing `isRoot` or every variable passes '', which withdraws
934
- // this Theme's rules.
935
- useEffect(() => {
1092
+ // this Theme's rules. A layout effect, so they are in place before the
1093
+ // browser paints what this Theme rendered.
1094
+ useClientLayoutEffect(() => {
936
1095
  setRootThemeRules(rootOrder, rootRules);
937
1096
  }, [rootOrder, rootRules]);
938
1097
  // Withdraw them on unmount. Kept apart from the effect above so a change
939
1098
  // rewrites the shared element in place rather than removing and recreating
940
- // it between the cleanup and the next run.
1099
+ // it between the cleanup and the next run, and passive, so a Theme that a
1100
+ // `Suspense` fallback hides keeps its variables meanwhile: React cleans up
1101
+ // a hidden tree's layout effects, but not its passive ones.
941
1102
  useEffect(() => () => setRootThemeRules(rootOrder, ''), [rootOrder]);
942
- // Bulma names its scheme attribute after the class prefix a stylesheet was
943
- // built with (`data-<prefix>theme`), so the prefixed builds read
944
- // `data-bestax-theme` and never `data-theme`.
945
- const classPrefix = useClassPrefix();
946
1103
  // Toggle Bulma's light/dark scheme by writing the theme attribute on the
947
1104
  // document root (<html>). This is always global, even on a scoped Theme.
948
1105
  // Under a class prefix the prefixed attribute is written as well as
949
1106
  // `data-theme`, so whichever build is loaded sees it; with no prefix only
950
1107
  // `data-theme` is. `'system'` removes them so Bulma follows the OS
951
- // preference.
952
- useEffect(() => {
953
- if (colorMode === undefined) {
954
- return;
955
- }
956
- const root = document.documentElement;
957
- const names = classPrefix
958
- ? ['data-theme', `data-${classPrefix}theme`]
959
- : ['data-theme'];
960
- const previous = names.map(name => root.getAttribute(name));
1108
+ // preference. Written before the browser paints, and handed back on
1109
+ // unmount, for the reasons the two effects above give; `schemeAttributes`
1110
+ // keeps the two in step.
1111
+ useClientLayoutEffect(() => {
1112
+ const names = colorMode === undefined
1113
+ ? []
1114
+ : classPrefix
1115
+ ? ['data-theme', `data-${classPrefix}theme`]
1116
+ : ['data-theme'];
1117
+ const value = colorMode === undefined || colorMode === 'system' ? null : colorMode;
1118
+ releaseSchemeAttributes(rootOrder, names);
961
1119
  for (const name of names) {
962
- if (colorMode === 'system') {
963
- root.removeAttribute(name);
964
- continue;
965
- }
966
1120
  try {
967
- root.setAttribute(name, colorMode);
1121
+ writeSchemeAttribute(rootOrder, name, value);
968
1122
  }
969
1123
  catch {
970
1124
  // A class prefix can hold characters an attribute name cannot.
@@ -977,32 +1131,20 @@ const Theme = ({ bulmaVars = {}, children, className, isRoot = false, colorMode,
977
1131
  'with that prefix keeps its scheme.');
978
1132
  }
979
1133
  }
980
- // Restore the previous values when colorMode changes or the component unmounts.
981
- return () => {
982
- names.forEach((name, i) => {
983
- const value = previous[i];
984
- if (value === null) {
985
- root.removeAttribute(name);
986
- }
987
- else {
988
- root.setAttribute(name, value);
989
- }
990
- });
991
- };
992
- }, [colorMode, classPrefix]);
1134
+ }, [colorMode, classPrefix, rootOrder]);
1135
+ // Hand the attributes back on unmount.
1136
+ useEffect(() => () => releaseSchemeAttributes(rootOrder), [rootOrder]);
993
1137
  // For local injection (when isRoot is false), prepare style object for CSS vars
994
1138
  const style = useMemo(() => {
995
1139
  if (isRoot) {
996
1140
  return {};
997
1141
  }
998
1142
  const styleObj = {};
999
- for (const [key, value] of Object.entries(mergedVars)) {
1000
- if (bulmaCssVars.includes(key) && value) {
1001
- styleObj[key] = value;
1002
- }
1143
+ for (const [key, value] of plainVars) {
1144
+ styleObj[key] = value;
1003
1145
  }
1004
1146
  return styleObj;
1005
- }, [mergedVars, isRoot]);
1147
+ }, [plainVars, isRoot]);
1006
1148
  // Generate combined class names for the wrapper div
1007
1149
  const combinedClassName = useMemo(() => {
1008
1150
  if (isRoot) {