create-bestax 4.2.18 → 4.2.19

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/constants.d.ts.map +1 -1
  2. package/dist/constants.js +4 -1
  3. package/dist/file-system.d.ts +5 -1
  4. package/dist/file-system.d.ts.map +1 -1
  5. package/dist/file-system.js +11 -1
  6. package/dist/library-range.d.ts +16 -0
  7. package/dist/library-range.d.ts.map +1 -0
  8. package/dist/library-range.js +40 -0
  9. package/dist/project-creator.d.ts.map +1 -1
  10. package/dist/project-creator.js +4 -1
  11. package/package.json +7 -7
  12. package/templates/skills/bestax-custom-component/references/component-catalog.md +3 -1
  13. package/templates/skills/bestax-form/SKILL.md +40 -15
  14. package/templates/skills/bestax-form/references/api.md +10 -8
  15. package/templates/skills/bestax-form/references/patterns.md +68 -0
  16. package/templates/skills/bestax-layout-scaffold/SKILL.md +16 -5
  17. package/templates/skills/bestax-layout-scaffold/references/archetypes.md +2 -2
  18. package/templates/skills/bestax-layout-scaffold/references/layout-components.md +52 -5
  19. package/templates/skills/bestax-migrate/SKILL.md +11 -3
  20. package/templates/skills/bestax-migrate/references/bloomer/component-map.md +1 -1
  21. package/templates/skills/bestax-migrate/references/bloomer/prop-map.md +8 -5
  22. package/templates/skills/bestax-migrate/references/bulma-classes/component-map.md +144 -101
  23. package/templates/skills/bestax-migrate/references/bulma-classes/prop-map.md +47 -16
  24. package/templates/skills/bestax-migrate/references/bulma-classes/unmappables.md +217 -45
  25. package/templates/skills/bestax-migrate/references/css-migration.md +12 -5
  26. package/templates/skills/bestax-migrate/references/rbx/prop-map.md +19 -13
  27. package/templates/skills/bestax-migrate/references/react-bulma-components/prop-map.md +27 -22
  28. package/templates/skills/bestax-migrate/references/react-bulma-components/unmappables.md +1 -1
  29. package/templates/skills/bestax-optimize/references/modular-build.md +57 -17
  30. package/templates/skills/bestax-theming/SKILL.md +19 -8
  31. package/templates/skills/bestax-theming/examples/dark-mode.tsx +99 -17
  32. package/templates/skills/bestax-theming/examples/theme-config.tsx +13 -8
  33. package/templates/skills/bestax-theming/references/css-variables.md +188 -18
  34. package/templates/skills/bestax-theming/references/themeable-components.md +2 -0
  35. package/templates/vite/package.json +1 -1
  36. package/templates/vite/src/main.jsx +1 -1
  37. package/templates/vite-ts/package.json +1 -1
  38. package/templates/vite-ts/src/main.tsx +1 -1
@@ -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
 
@@ -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,13 @@ 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`
79
85
 
80
86
  **Form** (`@allxsmith/bestax-bulma/scss/form/<name>`):
81
- `checkbox`, `radio`, `switch`, `slider`, `numberinput`, `rate`, `autocomplete`, `taginput`,
82
- `picker-popover`, `dateinput`, `timeinput`, `datetimeinput`, `daterangeinput`
87
+ `checkbox`, `radio`, `switch`, `slider`, `numberinput`, `pininput`, `rate`, `autocomplete`,
88
+ `combobox`, `taginput`, `picker-popover`, `dateinput`, `timeinput`, `datetimeinput`,
89
+ `daterangeinput`, `file`
83
90
 
84
91
  **Elements** (`@allxsmith/bestax-bulma/scss/elements/<name>`): `kbd`, `linkbutton`, `loader`,
85
92
  `notification`, `progress`, `skeleton`
@@ -90,29 +97,62 @@ Notes:
90
97
 
91
98
  - The extras partial name is the component name lowercased (`Dialog` →
92
99
  `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`.
100
+ - Sass names a `@use` after the last part of its URL and refuses two with the same name in one
101
+ file. A partial that extends a stock component often shares that name with the component's
102
+ Bulma module (`components/tabs`, `elements/notification`), so give the bestax line a name of
103
+ its own with `as`, the way the notes below do. The name changes nothing in the compiled CSS.
104
+ - Extras `Tabs` **extends** stock Bulma tabs (vertical variant), so an app using `Tabs` needs
105
+ both:
106
+
107
+ ```scss
108
+ @use 'bulma/sass/components/tabs';
109
+ @use '@allxsmith/bestax-bulma/scss/components/tabs' as bestax-tabs;
110
+ ```
111
+
112
+ - The `menu` partial **extends** stock Bulma menu (it draws `Menu.Section`'s heading and chevron,
113
+ and keeps the active item marked in forced colors, Windows' contrast themes), so an app using
114
+ `Menu` needs both, in either order:
115
+
116
+ ```scss
117
+ @use 'bulma/sass/components/menu';
118
+ @use '@allxsmith/bestax-bulma/scss/components/menu' as bestax-menu;
119
+ ```
120
+
121
+ - The `file` partial **extends** stock Bulma file (File's keyboard focus ring, and the rounded
122
+ corners of a boxed `hasName` File with no name to show), so an app using `File` needs both, in
123
+ either order:
124
+
125
+ ```scss
126
+ @use 'bulma/sass/form/file';
127
+ @use '@allxsmith/bestax-bulma/scss/form/file' as bestax-file;
128
+ ```
129
+
95
130
  - `DateInput`/`TimeInput`/`DateTimeInput` also need `picker-popover`.
96
131
  - `DateRangeInput` needs `daterangeinput` for its field and `dateinput` for its calendar, and
97
132
  `picker-popover` too.
133
+ - `PinInput`'s boxes are Bulma inputs, so an app using it needs `bulma/sass/form/input-textarea`
134
+ next to `scss/form/pininput`, and `bulma/sass/form/tools` for the label and help text around it.
98
135
  - `Popover.Close` renders a Bulma button, so an app using it needs `bulma/sass/elements/button`
99
136
  next to `scss/components/popover`.
100
137
  - `ContextMenu`'s menu is Bulma's dropdown markup, so an app using it needs
101
138
  `bulma/sass/components/dropdown` next to `scss/components/contextmenu`.
102
139
  - `Loader` is stock Bulma styled by `bulma/sass/elements/loader`, and the `isLoading`
103
140
  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.
141
+ stops all of them under `prefers-reduced-motion: reduce`, hides everything inside a loading
142
+ button where Bulma hides only the label, and keeps each ring open in forced colors (Windows'
143
+ contrast themes), so include it (`as bestax-loader`) next to the Bulma modules in any build
144
+ that shows one. Its
145
+ rules win over Bulma's either way, so the order of the two `@use` lines does not matter.
108
146
  - `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.
147
+ `bulma/sass/base/skeleton`). The `progress` and `skeleton` extras partials
148
+ (`as bestax-progress`, `as bestax-skeleton`) stop the indeterminate bar's sweep and the
149
+ skeleton pulse (`Skeleton`, the `skeleton` prop, `hasSkeleton`) under
150
+ `prefers-reduced-motion: reduce`, the same way and in either order.
112
151
  - `Notification` is stock Bulma too (`bulma/sass/elements/notification`). The `notification`
113
152
  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.
153
+ so its text clears the button, so include it (`as bestax-notification`) next to the Bulma
154
+ module in any build that shows a dismissible notification. It doesn't depend on the order of
155
+ the two `@use` lines either.
116
156
  - Stock-Bulma form controls still need their Bulma module (`bulma/sass/form/…`); the extras
117
157
  form partials above style only the bestax-specific behavior.
118
158
  - Bulma component modules pull their own internal sub-elements — e.g.
@@ -194,7 +234,7 @@ Scaffolded with an icon library, the starter also needs `bulma/sass/elements/ico
194
234
  @use 'bulma/sass/helpers/flexbox'; // flexGrow
195
235
  @use 'bulma/sass/helpers/typography'; // textAlign
196
236
 
197
- @use '@allxsmith/bestax-bulma/scss/elements/notification'; // text clears the close button
237
+ @use '@allxsmith/bestax-bulma/scss/elements/notification' as bestax-notification; // text clears the close button
198
238
  ```
199
239
 
200
240
  ```tsx
@@ -11,9 +11,14 @@ 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 in
20
+ your CSS as well, or labels and text lose their contrast (`references/css-variables.md`,
21
+ "Colors").
17
22
 
18
23
  Choose an override path:
19
24
 
@@ -31,7 +36,10 @@ For **dark mode**, pass `colorMode` to `Theme` (`'light' | 'dark' | 'system'`).
31
36
  `data-theme` attribute on `<html>` (plus `data-<prefix>theme` inside a `ConfigProvider` with a
32
37
  `classPrefix`, which the prefixed flavors read), flipping the light/dark scheme — global, even on a scoped
33
38
  `Theme`; `'system'` follows the OS `prefers-color-scheme`. Drive it from state on the app-root
34
- `Theme`: `<Theme isRoot colorMode={mode}>`.
39
+ `Theme`: `<Theme isRoot colorMode={mode}>`. `Theme` writes the attribute from an effect, after a
40
+ client-rendered app's first paint, so the scheme of that first frame comes from `index.html`:
41
+ `data-theme` on the `<html>` for a scheme that never changes, or an inline `<head>` script that
42
+ writes the visitor's saved choice for a toggle (`examples/dark-mode.tsx`).
35
43
 
36
44
  ## Contrast rules (dark mode is on by default)
37
45
 
@@ -42,8 +50,10 @@ theme. Custom fixed tokens (`--my-canvas: #f6f4ec`) do **not** flip — producin
42
50
  text on the author's fixed light background. Apply exactly one of these rules whenever custom
43
51
  color tokens or fixed-color surfaces exist:
44
52
 
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.
53
+ - **Single-mode design → pin the scheme on `<html>`.** `<html lang="en" data-theme="light">` (or
54
+ `"dark"`) in `index.html`, so the first paint has it and an OS preference can never invert text
55
+ out from under the fixed palette. `<Theme isRoot colorMode="light">` alone sets it only after
56
+ that paint, which shows the OS scheme first.
47
57
  - **Both modes → no exposed fixed tokens.** Derive custom tokens from scheme variables
48
58
  (`--my-canvas: var(--bulma-scheme-main)`) — or flip them yourself under **both** dark-mode
49
59
  paths: `[data-theme='dark']` **and** `@media (prefers-color-scheme: dark)` scoped to
@@ -113,13 +123,14 @@ Nest `Theme` and `ConfigProvider` together at the root (order doesn't matter).
113
123
  ## Examples
114
124
 
115
125
  - `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`).
126
+ - `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
127
 
118
128
  ## Checklist
119
129
 
120
- - [ ] Recolor brand colors via the HSL trio (`*-h` / `*-s` / `*-l`), not by hard-coding hex on components.
130
+ - [ ] 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` in CSS.
121
131
  - [ ] Apply a global theme once with `<Theme isRoot>` (or `:root`); use scoped `<Theme>` for one-off sections.
122
132
  - [ ] 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
133
  - [ ] 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.
134
+ - [ ] 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
135
  - [ ] Pass `color`/`textColor`/`bgColor` (not custom CSS) to color individual components.
125
136
  - [ ] Set the icon library once with `<ConfigProvider iconLibrary="…">` at the root, not `library` on every `<Icon>`.
@@ -1,38 +1,120 @@
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 from an effect, which runs after a client-rendered app's
6
+ // first paint, so on its own the page shows the OS scheme for a frame and then
7
+ // flips. The scheme for that first frame comes from a script in index.html,
8
+ // which writes the saved choice, or else the OS preference, before anything
9
+ // renders. Put it inside <head> as a plain inline script, not
10
+ // `type="module"`, so it runs before the body renders:
11
+ //
12
+ // <script>
13
+ // // Bulma's scheme on <html> before the first paint: the saved choice,
14
+ // // or else the OS preference. The app saves under the same key.
15
+ // (() => {
16
+ // let saved = null;
17
+ // try {
18
+ // saved = localStorage.getItem('theme');
19
+ // } catch {
20
+ // // Storage is blocked, so fall back to the OS preference.
21
+ // }
22
+ // const dark =
23
+ // saved === 'light' || saved === 'dark'
24
+ // ? saved === 'dark'
25
+ // : window.matchMedia('(prefers-color-scheme: dark)').matches;
26
+ // document.documentElement.setAttribute(
27
+ // 'data-theme',
28
+ // dark ? 'dark' : 'light'
29
+ // );
30
+ // })();
31
+ // </script>
32
+ //
33
+ // This file is the app's side: it starts from the attribute the script wrote,
34
+ // passes its state to `Theme colorMode`, which keeps the attribute in step
35
+ // with the toggle, and saves each choice under the key the script reads.
36
+ // Under a ConfigProvider `classPrefix`, colorMode also writes
37
+ // `data-<prefix>theme`, which the prefixed flavors read, so the script writes
38
+ // that attribute too.
8
39
  import { useState } from 'react';
9
40
  import {
10
41
  Theme,
11
42
  Box,
12
- Button,
13
43
  Title,
14
44
  Notification,
45
+ ToggleButton,
15
46
  } from '@allxsmith/bestax-bulma';
16
47
 
17
- type Mode = 'light' | 'dark' | 'system';
48
+ export type ColorMode = 'light' | 'dark';
49
+
50
+ /** The key the index.html script reads the saved choice from. */
51
+ const STORAGE_KEY = 'theme';
52
+
53
+ const isColorMode = (value: unknown): value is ColorMode =>
54
+ value === 'light' || value === 'dark';
55
+
56
+ function savedColorMode(): ColorMode | null {
57
+ try {
58
+ const saved = localStorage.getItem(STORAGE_KEY);
59
+ return isColorMode(saved) ? saved : null;
60
+ } catch {
61
+ // A browser can block storage, in a private window or an embedded
62
+ // preview.
63
+ return null;
64
+ }
65
+ }
66
+
67
+ /**
68
+ * The scheme the index.html script wrote. Without the script, the same
69
+ * choice: the saved one, or the OS preference.
70
+ *
71
+ * `useState` calls this during render, and a server render has no document,
72
+ * so there it is 'light' and the browser's own render starts from the
73
+ * attribute.
74
+ */
75
+ export function initialColorMode(): ColorMode {
76
+ if (typeof document === 'undefined') return 'light';
77
+ const written = document.documentElement.getAttribute('data-theme');
78
+ if (isColorMode(written)) return written;
79
+ return (
80
+ savedColorMode() ??
81
+ (window.matchMedia('(prefers-color-scheme: dark)').matches
82
+ ? 'dark'
83
+ : 'light')
84
+ );
85
+ }
86
+
87
+ /** Remember the visitor's choice for their next visit. */
88
+ export function saveColorMode(mode: ColorMode) {
89
+ try {
90
+ localStorage.setItem(STORAGE_KEY, mode);
91
+ } catch {
92
+ // Storage is blocked, so the choice lasts until the page reloads.
93
+ }
94
+ }
18
95
 
19
96
  export function DarkModeToggle() {
20
- const [mode, setMode] = useState<Mode>('system');
97
+ const [mode, setMode] = useState<ColorMode>(initialColorMode);
98
+
99
+ const setDark = (dark: boolean) => {
100
+ const next = dark ? 'dark' : 'light';
101
+ setMode(next);
102
+ saveColorMode(next);
103
+ };
21
104
 
22
105
  return (
106
+ // Wrap the app once, at its root.
23
107
  <Theme isRoot colorMode={mode}>
24
108
  <Box>
25
- <Title size="5">Color mode: {mode}</Title>
109
+ <Title size="5">Color scheme</Title>
26
110
  <Notification color="info">
27
111
  Components below follow the current Bulma color scheme.
28
112
  </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>
113
+ {/* One setting and its state: the label stays "Dark mode", and
114
+ ToggleButton says whether it is on through aria-pressed. */}
115
+ <ToggleButton pressed={mode === 'dark'} onPressedChange={setDark}>
116
+ Dark mode
117
+ </ToggleButton>
36
118
  </Box>
37
119
  </Theme>
38
120
  );
@@ -1,11 +1,20 @@
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. A color that reads lighter or darker than the stylesheet's
16
+ // own needs those set in your CSS too, which Theme does not do;
17
+ // references/css-variables.md ("Colors") says how.
9
18
  import type { ReactNode } from 'react';
10
19
  import {
11
20
  Theme,
@@ -25,10 +34,6 @@ export function ThemedApp({ children }: { children: ReactNode }) {
25
34
  primaryH="265"
26
35
  primaryS="65%"
27
36
  primaryL="55%"
28
- // A matching link color.
29
- linkH="200"
30
- linkS="80%"
31
- linkL="45%"
32
37
  // Anything without a named prop goes through `bulmaVars`, keyed by the real
33
38
  // --bulma-* variable names (string values).
34
39
  bulmaVars={{
@@ -47,7 +52,7 @@ export function ThemedApp({ children }: { children: ReactNode }) {
47
52
  // 2) Scoped override — theme just one section; the rest of the app is untouched.
48
53
  export function PromoPanel() {
49
54
  return (
50
- <Theme primaryH="16" primaryS="85%" primaryL="55%">
55
+ <Theme primaryH="16" primaryS="85%" primaryL="40%">
51
56
  <Box>
52
57
  <Title size="4">Limited offer</Title>
53
58
  <Notification color="primary">