create-bestax 4.2.18 → 4.3.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 (55) hide show
  1. package/README.md +13 -3
  2. package/dist/cli.d.ts.map +1 -1
  3. package/dist/cli.js +1 -0
  4. package/dist/constants.d.ts +12 -0
  5. package/dist/constants.d.ts.map +1 -1
  6. package/dist/constants.js +69 -1
  7. package/dist/file-system.d.ts +5 -1
  8. package/dist/file-system.d.ts.map +1 -1
  9. package/dist/file-system.js +11 -1
  10. package/dist/library-range.d.ts +16 -0
  11. package/dist/library-range.d.ts.map +1 -0
  12. package/dist/library-range.js +40 -0
  13. package/dist/project-creator.d.ts +8 -0
  14. package/dist/project-creator.d.ts.map +1 -1
  15. package/dist/project-creator.js +62 -3
  16. package/dist/prompts.d.ts +1 -0
  17. package/dist/prompts.d.ts.map +1 -1
  18. package/dist/prompts.js +15 -1
  19. package/package.json +8 -8
  20. package/templates/skills/bestax-custom-component/references/component-catalog.md +5 -1
  21. package/templates/skills/bestax-form/SKILL.md +40 -15
  22. package/templates/skills/bestax-form/references/api.md +10 -8
  23. package/templates/skills/bestax-form/references/patterns.md +68 -0
  24. package/templates/skills/bestax-icons/references/icon-libraries.md +14 -1
  25. package/templates/skills/bestax-layout-scaffold/SKILL.md +31 -5
  26. package/templates/skills/bestax-layout-scaffold/references/archetypes.md +2 -2
  27. package/templates/skills/bestax-layout-scaffold/references/layout-components.md +52 -5
  28. package/templates/skills/bestax-migrate/SKILL.md +19 -5
  29. package/templates/skills/bestax-migrate/references/bloomer/component-map.md +1 -1
  30. package/templates/skills/bestax-migrate/references/bloomer/prop-map.md +9 -6
  31. package/templates/skills/bestax-migrate/references/bloomer/unmappables.md +4 -1
  32. package/templates/skills/bestax-migrate/references/bulma-classes/component-map.md +182 -105
  33. package/templates/skills/bestax-migrate/references/bulma-classes/prop-map.md +50 -17
  34. package/templates/skills/bestax-migrate/references/bulma-classes/unmappables.md +372 -64
  35. package/templates/skills/bestax-migrate/references/css-migration.md +12 -5
  36. package/templates/skills/bestax-migrate/references/rbx/prop-map.md +20 -14
  37. package/templates/skills/bestax-migrate/references/react-bulma-components/prop-map.md +27 -22
  38. package/templates/skills/bestax-migrate/references/react-bulma-components/unmappables.md +3 -3
  39. package/templates/skills/bestax-optimize/references/modular-build.md +59 -17
  40. package/templates/skills/bestax-theming/SKILL.md +22 -8
  41. package/templates/skills/bestax-theming/examples/dark-mode.tsx +100 -17
  42. package/templates/skills/bestax-theming/examples/theme-config.tsx +20 -8
  43. package/templates/skills/bestax-theming/references/css-variables.md +268 -20
  44. package/templates/skills/bestax-theming/references/themeable-components.md +4 -0
  45. package/templates/vite/README.md +2 -2
  46. package/templates/vite/_oxlintrc.json +8 -0
  47. package/templates/vite/package.json +9 -13
  48. package/templates/vite/src/main.jsx +1 -1
  49. package/templates/vite/vite.config.js +1 -1
  50. package/templates/vite-ts/README.md +2 -2
  51. package/templates/vite-ts/_oxlintrc.json +8 -0
  52. package/templates/vite-ts/package.json +11 -16
  53. package/templates/vite-ts/src/main.tsx +1 -1
  54. package/templates/vite/eslint.config.js +0 -29
  55. package/templates/vite-ts/eslint.config.js +0 -26
@@ -26,11 +26,18 @@ choice below applies.
26
26
  **Visual note**: `bestax.css` ships with `$primary` set to bestax blue (`#1e6b99`),
27
27
  not Bulma's stock turquoise — `is-primary` surfaces (hero, navbar, buttons) change
28
28
  color. Keep the stock Bulma look with `--css bulma`, or set your own brand color via
29
- `--bulma-primary-*` CSS variables / a Sass build. Set `--bulma-primary-invert-l` along with
30
- `--bulma-primary-h`, `--bulma-primary-s` and `--bulma-primary-l`: it is the lightness of the
31
- text on a primary surface, and bestax's build fixes it for its dark blue, so a light primary set
32
- through the other three alone keeps white labels. To get stock Bulma's turquoise back on top of
33
- `bestax.css`, copy those four from Bulma's own `bulma/css/bulma.css`.
29
+ `--bulma-primary-*` CSS variables / a Sass build. With the variables, set
30
+ `--bulma-primary-invert-l` and `--bulma-primary-on-scheme-l` along with `--bulma-primary-h`,
31
+ `--bulma-primary-s` and `--bulma-primary-l`. The first is the lightness of the label on a
32
+ primary surface and the second of primary text on the page, and bestax's build fixes both for
33
+ its dark blue, so a light primary set through the trio alone keeps white labels and pale text.
34
+ To get stock Bulma's turquoise back on top of `bestax.css`, copy those five from Bulma's own
35
+ `bulma/css/bulma.css`. The on-scheme lightness differs between its light and dark schemes, so
36
+ set each scheme's value where the app shows both; the `bestax-theming` skill's
37
+ `references/css-variables.md` ("The lightnesses Bulma's build picks for contrast") shows how,
38
+ and what reads each one. Text the page colors with `has-text-primary` converts to
39
+ `textColor="primary"`, which reads the trio's own lightness, under 2:1 on white for turquoise
40
+ (in stock Bulma too); add `colorShade="on-scheme"` to take the shade made for text.
34
41
 
35
42
  - **SCSS files**: Bulma 0.9's `@import 'bulma/bulma.sass'` (plus preceding
36
43
  `$var: value !default;` overrides) became Bulma v1 module syntax, with simple literal
@@ -88,19 +88,25 @@ several of those narrow it to specific tags (`Footer` is `'footer' | 'div'`, `Co
88
88
  `'div' | 'p'`). The codemod passes `as` through only where bestax really accepts it, and TODOs it
89
89
  everywhere else:
90
90
 
91
- | bestax component | `as` accepts |
92
- | ---------------------------- | -------------------------- |
93
- | `Button` | any element type |
94
- | `Title`, `SubTitle` | `h1`–`h6`, `p` |
95
- | `Image` | `'figure' \| 'div' \| 'p'` |
96
- | `Footer` | `'footer' \| 'div'` |
97
- | `Media` | `'article' \| 'div'` |
98
- | `Media.Left` | `'figure' \| 'div'` |
99
- | `Level.Item` | `'div' \| 'p' \| 'a'` |
100
- | `Control` | `'div' \| 'p'` |
101
- | `Menu.Item` | any element type |
102
- | `Dropdown.Item` | `'a' \| 'div' \| 'button'` |
103
- | `Navbar.Item`, `Navbar.Link` | any element type |
91
+ | bestax component | `as` accepts |
92
+ | ---------------------------- | --------------------------------------------------------------------------------------- |
93
+ | `Button` | any element type |
94
+ | `Title`, `SubTitle` | `h1`–`h6`, `p` |
95
+ | `Image` | `'figure' \| 'div' \| 'p'` |
96
+ | `Footer` | `'footer' \| 'div'` |
97
+ | `Media` | `'article' \| 'div'` |
98
+ | `Media.Left` | `'figure' \| 'div'` |
99
+ | `Level.Item` | `'div' \| 'p' \| 'a'` |
100
+ | `Control` | `'div' \| 'p'` |
101
+ | `Menu.Item` | any element type |
102
+ | `Dropdown.Item` | `'a' \| 'div' \| 'button'` |
103
+ | `Navbar.Item`, `Navbar.Link` | any element type |
104
+ | `Container` | `'div' \| 'main' \| 'section' \| 'article' \| 'aside' \| 'header' \| 'footer' \| 'nav'` |
105
+ | `Hero` | `'section' \| 'div' \| 'main' \| 'header' \| 'footer' \| 'article' \| 'aside'` |
106
+ | `Hero.Head` | `'div' \| 'header' \| 'nav'` |
107
+ | `Hero.Foot` | `'div' \| 'footer' \| 'nav'` |
108
+ | `Panel` | `'nav' \| 'article' \| 'section' \| 'aside' \| 'div'` |
109
+ | `Panel.Block` | `'a' \| 'div' \| 'label'` |
104
110
 
105
111
  Three traps worth naming, because rbx supplies `as` universally and the obvious guesses are
106
112
  wrong:
@@ -183,7 +189,7 @@ An existing `ref` is passed through untouched — which is safe only where the b
183
189
  one. rbx forwards a ref on every component; bestax does so on the form controls, plus `Avatar`,
184
190
  `Button`, `Carousel`, `CarouselItem`, `Dialog`, `Dropdown`, `EmptyState`, `Kbd`, `Kbd.Group`,
185
191
  `Link`, `LinkButton`, `Loader`, `Menu.Item`, `Modal`, `Navbar`, `Navbar.Burger`, `Navbar.Dropdown`,
186
- `Navbar.Item`, `Navbar.Link`, `SegmentedControl`, `SegmentedControl.Segment`, `Sidebar`, `Timeline`,
192
+ `Navbar.Item`, `Navbar.Link`, `ScrollArea`, `SegmentedControl`, `SegmentedControl.Segment`, `Sidebar`, `Splitter`, `Splitter.Pane`, `Timeline`,
187
193
  `Timeline.Item`, `Toast`, `ToggleButton`, `TreeView` and `TreeView.Item`. "The form controls" there
188
194
  means the inputs themselves: the `Field`, `Field.Label`, `Field.Body`, `Checkboxes` and `Radios`
189
195
  wrappers around them forward nothing, and `Field` is a target this codemod emits.
@@ -6,31 +6,36 @@ this automatically for literal values; dynamic values get TODOs.
6
6
 
7
7
  ## Renames and value changes
8
8
 
9
- | RBC | bestax |
10
- | --------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------- |
11
- | `renderAs` | `as` — **only** on polymorphic components (Button, Title/SubTitle, Footer, Level.Item, Media\*, Menu.Item, Navbar.Item/Link, Dropdown.Item); elsewhere flagged |
12
- | `domRef` | flagged — rename to `ref` on the components that forward one, else use a DOM child |
13
- | `backgroundColor` | `bgColor` |
14
- | `textColor` | `textColor` (same) |
15
- | `colorVariant` | flagged — use `isLight` or a color shade |
16
- | `textSize={4}` | `textSize="4"` (string union `'1'`–`'7'`) |
17
- | `textAlign="center"` | `textAlign="centered"` (`justify`→`justified`) |
18
- | `textFamily` | `fontFamily` |
19
- | `italic` | `textTransform="italic"` |
20
- | `m/mt/…/py={3}` | `m="3"` (string union `'0'`–`'6'`) |
21
- | `pull="right"` | `float="right"` |
22
- | `marginless` / `paddingless` | `m="0"` / `p="0"` |
23
- | `clipped` | `overflow="clipped"` |
24
- | `radiusless` / `shadowless` | `radius="radiusless"` / `shadow="shadowless"` |
25
- | `unselectable` | `interaction="unselectable"` |
26
- | `hidden` / `invisible` / `srOnly` | `visibility="hidden"` / `"invisible"` / `"sr-only"` |
27
- | `display="hidden"` | `visibility="hidden"` |
28
- | `display="relative"` | boolean `relative` |
29
- | flexbox props | same names/values (`flexDirection`, `justifyContent`, …) |
30
- | `clearfix`, `overlay` | same |
9
+ | RBC | bestax |
10
+ | --------------------------------- | ------------------------------------------------------------------------------------------------------------------------- |
11
+ | `renderAs` | `as` — **only** where bestax declares one ([listed below](#renderas--as-and-the-props-that-follow-it)); elsewhere flagged |
12
+ | `domRef` | flagged — rename to `ref` on the components that forward one, else use a DOM child |
13
+ | `backgroundColor` | `bgColor` |
14
+ | `textColor` | `textColor` (same) |
15
+ | `colorVariant` | flagged — use `isLight` or a color shade |
16
+ | `textSize={4}` | `textSize="4"` (string union `'1'`–`'7'`) |
17
+ | `textAlign="center"` | `textAlign="centered"` (`justify`→`justified`) |
18
+ | `textFamily` | `fontFamily` |
19
+ | `italic` | `textTransform="italic"` |
20
+ | `m/mt/…/py={3}` | `m="3"` (string union `'0'`–`'6'`) |
21
+ | `pull="right"` | `float="right"` |
22
+ | `marginless` / `paddingless` | `m="0"` / `p="0"` |
23
+ | `clipped` | `overflow="clipped"` |
24
+ | `radiusless` / `shadowless` | `radius="radiusless"` / `shadow="shadowless"` |
25
+ | `unselectable` | `interaction="unselectable"` |
26
+ | `hidden` / `invisible` / `srOnly` | `visibility="hidden"` / `"invisible"` / `"sr-only"` |
27
+ | `display="hidden"` | `visibility="hidden"` |
28
+ | `display="relative"` | boolean `relative` |
29
+ | flexbox props | same names/values (`flexDirection`, `justifyContent`, …) |
30
+ | `clearfix`, `overlay` | same |
31
31
 
32
32
  ## `renderAs` → `as`, and the props that follow it
33
33
 
34
+ `renderAs` becomes `as` on the components bestax declares one on: `Button`, `Title` and
35
+ `SubTitle` (from `Heading`), `Image`, `Form.Control`, `Footer`, `Level.Item`, `Media` and its
36
+ `Item`s, `Menu.Item`, `Navbar.Item`, `Navbar.Link`, `Dropdown.Item`, `Container`, `Hero`,
37
+ `Hero.Header`, `Hero.Footer`, `Panel` and `Panel.Block`. Everywhere else it is flagged.
38
+
34
39
  bestax gives an element the attributes of the tag `as` names, and several components narrow
35
40
  `as` to the tags Bulma's markup allows there (`Footer` is `'footer' | 'div'`, `Control` is
36
41
  `'div' | 'p'`, `Level.Item` is `'div' | 'p' | 'a'`). RBC's `renderAs` took any tag, so two
@@ -132,7 +132,7 @@ bestax `Button` colors are the semantic set + `text`/`ghost`. For shades use
132
132
  ## `colorVariant` / Hero `gradient` / Hero `halfheight` (`prop:size`)
133
133
 
134
134
  - `colorVariant="light"` → `isLight` where supported (Button, Notification), otherwise a
135
- shade: `bgColor="primary-90"`.
135
+ shade: `bgColor="primary" backgroundColorShade="90"`.
136
136
  - Bulma v1 removed `is-bold` hero gradients — delete `gradient` or restyle with CSS.
137
137
  - `halfheight` has no bestax size; use `size="medium"` or a CSS height.
138
138
 
@@ -165,8 +165,8 @@ The same TODO marks a `Heading` that sets `size` and `textSize` both: drop `size
165
165
  bestax components don't take `domRef`, but many forward a plain `ref` — the form controls, plus
166
166
  `Avatar`, `Button`, `Carousel`, `CarouselItem`, `Dialog`, `Dropdown`, `EmptyState`, `Kbd`,
167
167
  `Kbd.Group`, `Link`, `LinkButton`, `Loader`, `Menu.Item`, `Modal`, `Navbar`, `Navbar.Burger`,
168
- `Navbar.Dropdown`, `Navbar.Item`, `Navbar.Link`, `SegmentedControl`, `SegmentedControl.Segment`,
169
- `Sidebar`, `Timeline`, `Timeline.Item`, `Toast`, `ToggleButton`, `TreeView` and `TreeView.Item`. On
168
+ `Navbar.Dropdown`, `Navbar.Item`, `Navbar.Link`, `ScrollArea`, `SegmentedControl`, `SegmentedControl.Segment`,
169
+ `Sidebar`, `Splitter`, `Splitter.Pane`, `Timeline`, `Timeline.Item`, `Toast`, `ToggleButton`, `TreeView` and `TreeView.Item`. On
170
170
  those, rename `domRef` to `ref` and it works; do not restructure the markup. "The form controls"
171
171
  means the inputs themselves: the `Field`, `Field.Label`, `Field.Body`, `Checkboxes` and `Radios`
172
172
  wrappers around them forward nothing, and `Field` is a target this codemod emits.
@@ -49,6 +49,11 @@ extras map to `@allxsmith/bestax-bulma/scss/<category>/<lowercased-name>`. Compo
49
49
  are pure stock Bulma (Button, Box, Card, Navbar, Modal, Hero, …) have **no** extras partial —
50
50
  Bulma's own module is all they need.
51
51
 
52
+ Bulma groups its form controls, so their modules don't follow the name: `Input` and `TextArea`
53
+ are `bulma/sass/form/input-textarea`, `Checkbox`, `Checkboxes`, `Radio` and `Radios` are
54
+ `bulma/sass/form/checkbox-radio`, and `Field` and `Control` are `bulma/sass/form/tools`.
55
+ `Select` and `File` follow the rule.
56
+
52
57
  Stock Bulma examples:
53
58
 
54
59
  ```scss
@@ -56,8 +61,9 @@ Stock Bulma examples:
56
61
  @use 'bulma/sass/elements/box'; // Box
57
62
  @use 'bulma/sass/elements/notification'; // Notification
58
63
  @use 'bulma/sass/elements/title'; // Title, SubTitle
59
- @use 'bulma/sass/form/input'; // Input
64
+ @use 'bulma/sass/form/input-textarea'; // Input, TextArea
60
65
  @use 'bulma/sass/form/select'; // Select
66
+ @use 'bulma/sass/form/tools'; // Field, Control
61
67
  @use 'bulma/sass/components/card'; // Card
62
68
  @use 'bulma/sass/components/modal'; // Modal
63
69
  @use 'bulma/sass/components/navbar'; // Navbar
@@ -74,12 +80,15 @@ is **more complete** than the docs page's Option C example, which omits several)
74
80
 
75
81
  **Components** (`@allxsmith/bestax-bulma/scss/components/<name>`):
76
82
  `loading`, `collapse`, `tooltip`, `popover`, `hovercard`, `contextmenu`, `steps`, `sidebar`,
77
- `toast`, `dialog`, `carousel`, `tabs`, `reveal`, `avatar`, `avatars`, `badge`, `emptystate`,
78
- `timeline`, `treeview`
83
+ `toast`, `dialog`, `carousel`, `tabs`, `menu`, `reveal`, `avatar`, `avatars`, `badge`,
84
+ `emptystate`, `timeline`, `treeview`, `scrollarea`
85
+
86
+ **Layout** (`@allxsmith/bestax-bulma/scss/layout/<name>`): `splitter`
79
87
 
80
88
  **Form** (`@allxsmith/bestax-bulma/scss/form/<name>`):
81
- `checkbox`, `radio`, `switch`, `slider`, `numberinput`, `rate`, `autocomplete`, `taginput`,
82
- `picker-popover`, `dateinput`, `timeinput`, `datetimeinput`, `daterangeinput`
89
+ `checkbox`, `radio`, `switch`, `slider`, `numberinput`, `pininput`, `rate`, `autocomplete`,
90
+ `combobox`, `taginput`, `picker-popover`, `dateinput`, `timeinput`, `datetimeinput`,
91
+ `daterangeinput`, `file`
83
92
 
84
93
  **Elements** (`@allxsmith/bestax-bulma/scss/elements/<name>`): `kbd`, `linkbutton`, `loader`,
85
94
  `notification`, `progress`, `skeleton`
@@ -90,29 +99,62 @@ Notes:
90
99
 
91
100
  - The extras partial name is the component name lowercased (`Dialog` →
92
101
  `scss/components/dialog`; `NumberInput` → `scss/form/numberinput`).
93
- - Extras `Tabs` **extends** stock Bulma tabs (vertical variant) — an app using `Tabs` needs
94
- both `bulma/sass/components/tabs` and `@allxsmith/bestax-bulma/scss/components/tabs`.
102
+ - Sass names a `@use` after the last part of its URL and refuses two with the same name in one
103
+ file. A partial that extends a stock component often shares that name with the component's
104
+ Bulma module (`components/tabs`, `elements/notification`), so give the bestax line a name of
105
+ its own with `as`, the way the notes below do. The name changes nothing in the compiled CSS.
106
+ - Extras `Tabs` **extends** stock Bulma tabs (vertical variant), so an app using `Tabs` needs
107
+ both:
108
+
109
+ ```scss
110
+ @use 'bulma/sass/components/tabs';
111
+ @use '@allxsmith/bestax-bulma/scss/components/tabs' as bestax-tabs;
112
+ ```
113
+
114
+ - The `menu` partial **extends** stock Bulma menu (it draws `Menu.Section`'s heading and chevron,
115
+ and keeps the active item marked in forced colors, Windows' contrast themes), so an app using
116
+ `Menu` needs both, in either order:
117
+
118
+ ```scss
119
+ @use 'bulma/sass/components/menu';
120
+ @use '@allxsmith/bestax-bulma/scss/components/menu' as bestax-menu;
121
+ ```
122
+
123
+ - The `file` partial **extends** stock Bulma file (File's keyboard focus ring, and the rounded
124
+ corners of a boxed `hasName` File with no name to show), so an app using `File` needs both, in
125
+ either order:
126
+
127
+ ```scss
128
+ @use 'bulma/sass/form/file';
129
+ @use '@allxsmith/bestax-bulma/scss/form/file' as bestax-file;
130
+ ```
131
+
95
132
  - `DateInput`/`TimeInput`/`DateTimeInput` also need `picker-popover`.
96
133
  - `DateRangeInput` needs `daterangeinput` for its field and `dateinput` for its calendar, and
97
134
  `picker-popover` too.
135
+ - `PinInput`'s boxes are Bulma inputs, so an app using it needs `bulma/sass/form/input-textarea`
136
+ next to `scss/form/pininput`, and `bulma/sass/form/tools` for the label and help text around it.
98
137
  - `Popover.Close` renders a Bulma button, so an app using it needs `bulma/sass/elements/button`
99
138
  next to `scss/components/popover`.
100
139
  - `ContextMenu`'s menu is Bulma's dropdown markup, so an app using it needs
101
140
  `bulma/sass/components/dropdown` next to `scss/components/contextmenu`.
102
141
  - `Loader` is stock Bulma styled by `bulma/sass/elements/loader`, and the `isLoading`
103
142
  spinners on `Button`, `Control` and `Select` are Bulma's too. The `loader` extras partial
104
- stops all of them under `prefers-reduced-motion: reduce` and keeps each ring open in forced
105
- colors (Windows' contrast themes), so include it next to the Bulma modules in any build that
106
- shows one. Its rules are `!important`, so the order of the two
107
- `@use` lines does not matter.
143
+ stops all of them under `prefers-reduced-motion: reduce`, hides everything inside a loading
144
+ button where Bulma hides only the label, and keeps each ring open in forced colors (Windows'
145
+ contrast themes), so include it (`as bestax-loader`) next to the Bulma modules in any build
146
+ that shows one. Its
147
+ rules win over Bulma's either way, so the order of the two `@use` lines does not matter.
108
148
  - `Progress` and the skeletons are stock Bulma too (`bulma/sass/elements/progress`,
109
- `bulma/sass/base/skeleton`). The `progress` and `skeleton` extras partials stop the
110
- indeterminate bar's sweep and the skeleton pulse (`Skeleton`, the `skeleton` prop,
111
- `hasSkeleton`) under `prefers-reduced-motion: reduce`, the same way and in either order.
149
+ `bulma/sass/base/skeleton`). The `progress` and `skeleton` extras partials
150
+ (`as bestax-progress`, `as bestax-skeleton`) stop the indeterminate bar's sweep and the
151
+ skeleton pulse (`Skeleton`, the `skeleton` prop, `hasSkeleton`) under
152
+ `prefers-reduced-motion: reduce`, the same way and in either order.
112
153
  - `Notification` is stock Bulma too (`bulma/sass/elements/notification`). The `notification`
113
154
  extras partial pads the end of one with a close button (`hasDelete`, or a `Delete` passed in)
114
- so its text clears the button, so include it next to the Bulma module in any build that shows
115
- a dismissible notification. It doesn't depend on the order of the two `@use` lines either.
155
+ so its text clears the button, so include it (`as bestax-notification`) next to the Bulma
156
+ module in any build that shows a dismissible notification. It doesn't depend on the order of
157
+ the two `@use` lines either.
116
158
  - Stock-Bulma form controls still need their Bulma module (`bulma/sass/form/…`); the extras
117
159
  form partials above style only the bestax-specific behavior.
118
160
  - Bulma component modules pull their own internal sub-elements — e.g.
@@ -194,7 +236,7 @@ Scaffolded with an icon library, the starter also needs `bulma/sass/elements/ico
194
236
  @use 'bulma/sass/helpers/flexbox'; // flexGrow
195
237
  @use 'bulma/sass/helpers/typography'; // textAlign
196
238
 
197
- @use '@allxsmith/bestax-bulma/scss/elements/notification'; // text clears the close button
239
+ @use '@allxsmith/bestax-bulma/scss/elements/notification' as bestax-notification; // text clears the close button
198
240
  ```
199
241
 
200
242
  ```tsx
@@ -11,9 +11,15 @@ properties. Theme an app by overriding the right variables — no component re-s
11
11
 
12
12
  ## Approach
13
13
 
14
- Recolor a brand color by overriding its **hue/saturation/lightness trio** — Bulma derives every
15
- shade, light/dark, and invert variant from `--bulma-<color>-h` / `-s` / `-l`. Override the trio and
16
- the whole palette follows.
14
+ Recolor a brand color by overriding its **hue/saturation/lightness trio**
15
+ (`--bulma-<color>-h` / `-s` / `-l`). The color itself follows all three. Its shades and variants
16
+ follow the hue and saturation, at lightnesses the trio doesn't set. Among those, the stylesheet's
17
+ build picked the label on a solid color (`--bulma-<color>-invert-l`) and the color's text on the
18
+ page (`--bulma-<color>-on-scheme-l`, which for `link` is every link) for contrast with the color
19
+ it was built with, so when the new color reads lighter or darker than the built one, set them as
20
+ well, or labels and text lose their contrast. `Theme`'s `bulmaVars` takes both, and on an
21
+ `isRoot` `Theme` the on-scheme one takes a value per scheme, `{ light, dark }`
22
+ (`references/css-variables.md`, "Colors").
17
23
 
18
24
  Choose an override path:
19
25
 
@@ -31,7 +37,12 @@ For **dark mode**, pass `colorMode` to `Theme` (`'light' | 'dark' | 'system'`).
31
37
  `data-theme` attribute on `<html>` (plus `data-<prefix>theme` inside a `ConfigProvider` with a
32
38
  `classPrefix`, which the prefixed flavors read), flipping the light/dark scheme — global, even on a scoped
33
39
  `Theme`; `'system'` follows the OS `prefers-color-scheme`. Drive it from state on the app-root
34
- `Theme`: `<Theme isRoot colorMode={mode}>`.
40
+ `Theme`: `<Theme isRoot colorMode={mode}>`. `Theme` writes the attribute, and an `isRoot` Theme's
41
+ variables, before the browser paints what it rendered, but the page can paint before the app's
42
+ script runs at all, so the scheme of that first frame comes from `index.html`: `data-theme` on
43
+ the `<html>` for a scheme that never changes, or an inline `<head>` script that writes the
44
+ visitor's saved choice for a toggle (`examples/dark-mode.tsx`). A server render includes neither,
45
+ so a server-rendered page sets what it needs from its first paint in `index.html` and CSS.
35
46
 
36
47
  ## Contrast rules (dark mode is on by default)
37
48
 
@@ -42,8 +53,10 @@ theme. Custom fixed tokens (`--my-canvas: #f6f4ec`) do **not** flip — producin
42
53
  text on the author's fixed light background. Apply exactly one of these rules whenever custom
43
54
  color tokens or fixed-color surfaces exist:
44
55
 
45
- - **Single-mode design → pin the scheme.** `<Theme isRoot colorMode="light">` (or `"dark"`), so
46
- an OS preference can never invert text out from under the fixed palette.
56
+ - **Single-mode design → pin the scheme on `<html>`.** `<html lang="en" data-theme="light">` (or
57
+ `"dark"`) in `index.html`, so the first paint has it and an OS preference can never invert text
58
+ out from under the fixed palette. `<Theme isRoot colorMode="light">` alone sets it only once
59
+ the app's script runs, and the page can show the OS scheme before that.
47
60
  - **Both modes → no exposed fixed tokens.** Derive custom tokens from scheme variables
48
61
  (`--my-canvas: var(--bulma-scheme-main)`) — or flip them yourself under **both** dark-mode
49
62
  paths: `[data-theme='dark']` **and** `@media (prefers-color-scheme: dark)` scoped to
@@ -113,13 +126,14 @@ Nest `Theme` and `ConfigProvider` together at the root (order doesn't matter).
113
126
  ## Examples
114
127
 
115
128
  - `examples/theme-config.tsx` — a custom brand theme at the app root, plus a scoped override.
116
- - `examples/dark-mode.tsx` — a light/dark toggle using Bulma's `data-theme` (plus `data-<prefix>theme` inside a `ConfigProvider` with a `classPrefix`).
129
+ - `examples/dark-mode.tsx` — a light/dark toggle that keeps the visitor's choice, with the `index.html` script that writes Bulma's `data-theme` from it before the first paint.
117
130
 
118
131
  ## Checklist
119
132
 
120
- - [ ] Recolor brand colors via the HSL trio (`*-h` / `*-s` / `*-l`), not by hard-coding hex on components.
133
+ - [ ] Recolor brand colors via the HSL trio (`*-h` / `*-s` / `*-l`), not by hard-coding hex on components. A color that reads lighter or darker than the built one also sets `*-invert-l` and `*-on-scheme-l`, through `Theme bulmaVars` (the on-scheme one per scheme, on an `isRoot` Theme) or CSS.
121
134
  - [ ] Apply a global theme once with `<Theme isRoot>` (or `:root`); use scoped `<Theme>` for one-off sections.
122
135
  - [ ] Set non-color tokens (radius, fonts, sizes) through `bulmaVars` or `:root`; a custom `--bulma-family-*` needs its font actually loaded (`index.html` `<link>` or an `@fontsource` import).
123
136
  - [ ] Implement dark mode with `data-theme` on `<html>` (plus `data-<prefix>theme` inside a `ConfigProvider` with a `classPrefix`, which the prefixed flavors read); do not expect a shipped dark-mode component.
137
+ - [ ] Set the first paint's scheme in `index.html`: on `<html>` for a fixed scheme, from an inline `<head>` script for a saved choice.
124
138
  - [ ] Pass `color`/`textColor`/`bgColor` (not custom CSS) to color individual components.
125
139
  - [ ] Set the icon library once with `<ConfigProvider iconLibrary="…">` at the root, not `library` on every `<Icon>`.
@@ -1,38 +1,121 @@
1
- // Dark mode for an app built on @allxsmith/bestax-bulma.
1
+ // A light/dark toggle for an app built on @allxsmith/bestax-bulma that keeps
2
+ // the visitor's choice.
2
3
  //
3
- // The Theme component drives the light/dark scheme: pass `colorMode` and it
4
- // writes Bulma's `data-theme` attribute on <html>, plus `data-<prefix>theme`
5
- // under a ConfigProvider `classPrefix`, which the prefixed flavors read. This
6
- // is global (even on a scoped Theme); `'system'` removes the attributes so
7
- // Bulma follows the OS `prefers-color-scheme`. Wrap the app once at the root.
4
+ // Bulma reads the scheme from `data-theme` on <html>. `Theme colorMode` writes
5
+ // that attribute before the browser paints what the app rendered, but only
6
+ // once the app's script runs, and the page can paint before that, so on its
7
+ // own it can show the OS scheme for a frame and then flip. The scheme for
8
+ // that first frame comes from a script in index.html, which writes the saved
9
+ // choice, or else the OS preference, before anything renders. Put it inside
10
+ // <head> as a plain inline script, not `type="module"`, so it runs before the
11
+ // body renders:
12
+ //
13
+ // <script>
14
+ // // Bulma's scheme on <html> before the first paint: the saved choice,
15
+ // // or else the OS preference. The app saves under the same key.
16
+ // (() => {
17
+ // let saved = null;
18
+ // try {
19
+ // saved = localStorage.getItem('theme');
20
+ // } catch {
21
+ // // Storage is blocked, so fall back to the OS preference.
22
+ // }
23
+ // const dark =
24
+ // saved === 'light' || saved === 'dark'
25
+ // ? saved === 'dark'
26
+ // : window.matchMedia('(prefers-color-scheme: dark)').matches;
27
+ // document.documentElement.setAttribute(
28
+ // 'data-theme',
29
+ // dark ? 'dark' : 'light'
30
+ // );
31
+ // })();
32
+ // </script>
33
+ //
34
+ // This file is the app's side: it starts from the attribute the script wrote,
35
+ // passes its state to `Theme colorMode`, which keeps the attribute in step
36
+ // with the toggle, and saves each choice under the key the script reads.
37
+ // Under a ConfigProvider `classPrefix`, colorMode also writes
38
+ // `data-<prefix>theme`, which the prefixed flavors read, so the script writes
39
+ // that attribute too.
8
40
  import { useState } from 'react';
9
41
  import {
10
42
  Theme,
11
43
  Box,
12
- Button,
13
44
  Title,
14
45
  Notification,
46
+ ToggleButton,
15
47
  } from '@allxsmith/bestax-bulma';
16
48
 
17
- type Mode = 'light' | 'dark' | 'system';
49
+ export type ColorMode = 'light' | 'dark';
50
+
51
+ /** The key the index.html script reads the saved choice from. */
52
+ const STORAGE_KEY = 'theme';
53
+
54
+ const isColorMode = (value: unknown): value is ColorMode =>
55
+ value === 'light' || value === 'dark';
56
+
57
+ function savedColorMode(): ColorMode | null {
58
+ try {
59
+ const saved = localStorage.getItem(STORAGE_KEY);
60
+ return isColorMode(saved) ? saved : null;
61
+ } catch {
62
+ // A browser can block storage, in a private window or an embedded
63
+ // preview.
64
+ return null;
65
+ }
66
+ }
67
+
68
+ /**
69
+ * The scheme the index.html script wrote. Without the script, the same
70
+ * choice: the saved one, or the OS preference.
71
+ *
72
+ * `useState` calls this during render, and a server render has no document,
73
+ * so there it is 'light' and the browser's own render starts from the
74
+ * attribute.
75
+ */
76
+ export function initialColorMode(): ColorMode {
77
+ if (typeof document === 'undefined') return 'light';
78
+ const written = document.documentElement.getAttribute('data-theme');
79
+ if (isColorMode(written)) return written;
80
+ return (
81
+ savedColorMode() ??
82
+ (window.matchMedia('(prefers-color-scheme: dark)').matches
83
+ ? 'dark'
84
+ : 'light')
85
+ );
86
+ }
87
+
88
+ /** Remember the visitor's choice for their next visit. */
89
+ export function saveColorMode(mode: ColorMode) {
90
+ try {
91
+ localStorage.setItem(STORAGE_KEY, mode);
92
+ } catch {
93
+ // Storage is blocked, so the choice lasts until the page reloads.
94
+ }
95
+ }
18
96
 
19
97
  export function DarkModeToggle() {
20
- const [mode, setMode] = useState<Mode>('system');
98
+ const [mode, setMode] = useState<ColorMode>(initialColorMode);
99
+
100
+ const setDark = (dark: boolean) => {
101
+ const next = dark ? 'dark' : 'light';
102
+ setMode(next);
103
+ saveColorMode(next);
104
+ };
21
105
 
22
106
  return (
107
+ // Wrap the app once, at its root.
23
108
  <Theme isRoot colorMode={mode}>
24
109
  <Box>
25
- <Title size="5">Color mode: {mode}</Title>
110
+ <Title size="5">Color scheme</Title>
26
111
  <Notification color="info">
27
112
  Components below follow the current Bulma color scheme.
28
113
  </Notification>
29
- <Button color="primary" onClick={() => setMode('light')}>
30
- Light
31
- </Button>
32
- <Button color="primary" onClick={() => setMode('dark')}>
33
- Dark
34
- </Button>
35
- <Button onClick={() => setMode('system')}>System</Button>
114
+ {/* One setting and its state: the label stays "Dark mode", and
115
+ ToggleButton says whether it is on through aria-pressed. */}
116
+ <ToggleButton pressed={mode === 'dark'} onPressedChange={setDark}>
117
+ Dark mode
118
+ </ToggleButton>
36
119
  </Box>
37
120
  </Theme>
38
121
  );
@@ -1,11 +1,21 @@
1
1
  // Custom brand theme for an app built on @allxsmith/bestax-bulma.
2
2
  // Copy this to your app and adjust the HSL values to your brand.
3
3
  //
4
- // Bulma 1.x derives every shade of a color from its hue/saturation/lightness
5
- // trio (--bulma-<color>-h / -s / -l), so overriding the trio recolors the whole
6
- // palette. The <Theme> component writes those CSS variables for you:
4
+ // Bulma 1.x builds each color from a hue/saturation/lightness trio
5
+ // (--bulma-<color>-h / -s / -l). Overriding the trio recolors the color
6
+ // itself, and its shades and variants take the new hue and saturation at the
7
+ // lightnesses the stylesheet was built with.
8
+ // The <Theme> component writes those CSS variables for you:
7
9
  // - isRoot -> inject the variables globally at :root (use once, at the app root)
8
10
  // - without isRoot -> wrap children in a <div> and scope the variables to it
11
+ //
12
+ // Of those built lightnesses, the label on a solid color
13
+ // (--bulma-<color>-invert-l) and the color's text on the page
14
+ // (--bulma-<color>-on-scheme-l, which for `link` is every link) were picked
15
+ // for contrast with the stylesheet's own color. A color that reads lighter or
16
+ // darker than that one sets them too, through bulmaVars. On an isRoot Theme
17
+ // the on-scheme one takes a value for each scheme. references/css-variables.md
18
+ // ("Colors") says how to pick them.
9
19
  import type { ReactNode } from 'react';
10
20
  import {
11
21
  Theme,
@@ -25,13 +35,15 @@ export function ThemedApp({ children }: { children: ReactNode }) {
25
35
  primaryH="265"
26
36
  primaryS="65%"
27
37
  primaryL="55%"
28
- // A matching link color.
29
- linkH="200"
30
- linkS="80%"
31
- linkL="45%"
32
38
  // Anything without a named prop goes through `bulmaVars`, keyed by the real
33
39
  // --bulma-* variable names (string values).
34
40
  bulmaVars={{
41
+ // Purple text reads darker than the stylesheet's blue at the same
42
+ // lightness, so on a dark page it needs a lighter shade than the one
43
+ // built for the blue. Each scheme gets its own, and these read at 4.5:1
44
+ // or more on Bulma's white and near-black pages. The white label the
45
+ // blue was built with still reads on this purple, so the invert stays.
46
+ '--bulma-primary-on-scheme-l': { light: '41%', dark: '65%' },
35
47
  '--bulma-radius': '0.75rem',
36
48
  '--bulma-radius-large': '1.25rem',
37
49
  // 'Inter' must actually be loaded (index.html <link> or an @fontsource
@@ -47,7 +59,7 @@ export function ThemedApp({ children }: { children: ReactNode }) {
47
59
  // 2) Scoped override — theme just one section; the rest of the app is untouched.
48
60
  export function PromoPanel() {
49
61
  return (
50
- <Theme primaryH="16" primaryS="85%" primaryL="55%">
62
+ <Theme primaryH="16" primaryS="85%" primaryL="40%">
51
63
  <Box>
52
64
  <Title size="4">Limited offer</Title>
53
65
  <Notification color="primary">