loco_motion-rails 0.7.3 → 0.8.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.
- checksums.yaml +4 -4
- data/README.md +80 -8
- data/app/assets/stylesheets/loco.css +41 -4
- data/app/components/daisy/actions/theme_controller.js +342 -17
- data/app/components/daisy/actions/theme_controller_component.rb +129 -12
- data/app/components/daisy/data_display/collapse_component.html.haml +1 -1
- data/app/components/daisy/data_display/collapse_component.rb +48 -2
- data/app/components/daisy/data_display/status_component.rb +0 -2
- data/app/components/daisy/data_display/text_rotate_component.rb +0 -2
- data/app/components/daisy/data_input/cally_input_controller.js +8 -0
- data/app/components/daisy/data_input/select_component.rb +0 -2
- data/app/components/daisy/mockup/browser_component.rb +3 -4
- data/app/components/daisy/mockup/frame_component.rb +3 -4
- data/app/components/daisy/navigation/tabs_component.rb +66 -5
- data/app/components/daisy/navigation/tabs_controller.js +170 -0
- data/app/helpers/daisy/form_builder_helper.rb +0 -2
- data/app/helpers/daisy/theme_helper.rb +35 -5
- data/lib/loco_motion/base_component.rb +2 -2
- data/lib/loco_motion/lint/component_map.rb +139 -0
- data/lib/loco_motion/lint/erb_lint/component_usage.rb +63 -0
- data/lib/loco_motion/lint/haml_lint/component_usage.rb +73 -0
- data/lib/loco_motion/version.rb +1 -1
- metadata +65 -15
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 7f562576e236a298243e3c154e468e081c58471ba23b86e6dbc1486084c72db2
|
|
4
|
+
data.tar.gz: d3958a2260cee9c7d3529e64c4b7252c4921d4368143c9176e58f8545d7e2392
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 5e07962f154fe394cab75b5da4d9f973e8ccf99de55969ac40ab2d0562cdf7ca7c87367774c4f2e490b196a905ef4b35c6c70956c5f4147de5052174e164ed76
|
|
7
|
+
data.tar.gz: 99ada80a6c6237bdc8c315b26e7c45236753f794383e3bad1d2d0035c555e3486b23bb035fc36647f29b1e55b834e33d85967c53c6bb638bffe4e58ee80af0e0
|
data/README.md
CHANGED
|
@@ -13,6 +13,7 @@ ViewComponent, TailwindCSS, DaisyUI and more!
|
|
|
13
13
|
- [Current Status](#current-status)
|
|
14
14
|
- [Installation](#installation)
|
|
15
15
|
- [Using Components](#using-components)
|
|
16
|
+
- [Linting Component Usage](#linting-component-usage)
|
|
16
17
|
- [Guides](#guides)
|
|
17
18
|
- [Documentation & Demo](#documentation--demo)
|
|
18
19
|
- [Developing](#developing)
|
|
@@ -70,7 +71,7 @@ Add the gem to your `Gemfile` and run `bundle`:
|
|
|
70
71
|
|
|
71
72
|
```ruby
|
|
72
73
|
# Gemfile
|
|
73
|
-
gem "loco_motion-rails", "~> 0.
|
|
74
|
+
gem "loco_motion-rails", "~> 0.8.0", require: "loco_motion"
|
|
74
75
|
```
|
|
75
76
|
|
|
76
77
|
Some components also need a JavaScript package (for their Stimulus controllers)
|
|
@@ -104,6 +105,75 @@ For the full set of available components, parts, slots, and live examples, see
|
|
|
104
105
|
the [demo site](https://loco-motion.profoundry.us/) and the
|
|
105
106
|
[API documentation](#documentation--demo).
|
|
106
107
|
|
|
108
|
+
## Linting Component Usage
|
|
109
|
+
|
|
110
|
+
LocoMotion ships a [haml_lint](https://github.com/sds/haml-lint) rule that flags
|
|
111
|
+
views hand-rolling DaisyUI markup instead of calling the helper that owns it —
|
|
112
|
+
`.card` where `daisy_card` belongs. Views compose components; components own
|
|
113
|
+
markup.
|
|
114
|
+
|
|
115
|
+
Wire it up for whichever template language you use — both read the same derived
|
|
116
|
+
map, so HAML and ERB are held to identical rules.
|
|
117
|
+
|
|
118
|
+
**HAML** — add `haml_lint` to your bundle, then `.haml-lint.yml`:
|
|
119
|
+
|
|
120
|
+
```yaml
|
|
121
|
+
require:
|
|
122
|
+
- loco_motion/lint/haml_lint/component_usage
|
|
123
|
+
|
|
124
|
+
linters:
|
|
125
|
+
LocoMotionComponentUsage:
|
|
126
|
+
enabled: true
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
```bash
|
|
130
|
+
bundle exec haml-lint --include-linter LocoMotionComponentUsage app/views
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
**ERB** — add `erb_lint` to your bundle. It loads custom linters from a
|
|
134
|
+
directory rather than a config key, so create `.erb_linters/loco_motion.rb`:
|
|
135
|
+
|
|
136
|
+
```ruby
|
|
137
|
+
require "loco_motion/lint/erb_lint/component_usage"
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
Then `.erb_lint.yml`:
|
|
141
|
+
|
|
142
|
+
```yaml
|
|
143
|
+
linters:
|
|
144
|
+
LocoMotionComponentUsage:
|
|
145
|
+
enabled: true
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
```bash
|
|
149
|
+
bundle exec erb_lint --lint-all --enable-linters LocoMotionComponentUsage
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
Either way the output names the helper to reach for:
|
|
153
|
+
|
|
154
|
+
```
|
|
155
|
+
app/views/orders/show.html.haml:12 [W] LocoMotionComponentUsage: `.card` is
|
|
156
|
+
LocoMotion's daisy_card — call the helper instead of hand-rolling the markup
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
The class list is **derived** from the component registry and each component's
|
|
160
|
+
own `add_css` declaration, so it tracks the library automatically — a component
|
|
161
|
+
added in a release is covered by that release. Tailwind utilities (`flex`,
|
|
162
|
+
`p-4`, `hover:*`) and DaisyUI modifiers (`btn-primary`) are never flagged;
|
|
163
|
+
`css:` remains the sanctioned way to pass those to a helper.
|
|
164
|
+
|
|
165
|
+
Suppress a deliberate exception the usual haml_lint way:
|
|
166
|
+
|
|
167
|
+
```haml
|
|
168
|
+
-# haml-lint:disable LocoMotionComponentUsage
|
|
169
|
+
.card.custom-thing
|
|
170
|
+
-# haml-lint:enable LocoMotionComponentUsage
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
Adopting this on an existing codebase will surface a backlog. `haml-lint
|
|
174
|
+
--auto-gen-config` writes a TODO file that excludes today's offenders so new
|
|
175
|
+
views are held to the rule while the backlog drains.
|
|
176
|
+
|
|
107
177
|
## Guides
|
|
108
178
|
|
|
109
179
|
These optional guides cover the broader LocoMotion philosophies and a
|
|
@@ -139,18 +209,20 @@ for the time being, the latest documentation is available at the links below.
|
|
|
139
209
|
To work on LocoMotion, first clone the repository and make sure you have Docker
|
|
140
210
|
installed and running on your machine.
|
|
141
211
|
|
|
142
|
-
Next, create a
|
|
143
|
-
replace the Unsplash keys with real ones (you can create your own
|
|
144
|
-
Topher for his keys).
|
|
212
|
+
Next, create a `docs/demo/.env.local` file with the following contents, making
|
|
213
|
+
sure to replace the Unsplash keys with real ones (you can create your own
|
|
214
|
+
account or ask Topher for his keys). The demo boots fine without them — only the
|
|
215
|
+
example pages that pull sample photos will fail.
|
|
145
216
|
|
|
146
217
|
```.env
|
|
147
|
-
#
|
|
218
|
+
# docs/demo/.env.local
|
|
148
219
|
UNSPLASH_ACCESS_KEY="<< INSERT ACCESS KEY >>"
|
|
149
220
|
UNSPLASH_SECRET_KEY="<< INSERT SECRET KEY >>"
|
|
150
221
|
```
|
|
151
222
|
|
|
152
|
-
You should then be able to run `just
|
|
153
|
-
`just all-
|
|
223
|
+
You should then be able to run `just build` in the project directory and then
|
|
224
|
+
`just all-fast` to start the services. (`just rebuild` also works, but it passes
|
|
225
|
+
`--no-cache`, which is wasted effort on a fresh clone with an empty cache.)
|
|
154
226
|
|
|
155
227
|
> [!NOTE]
|
|
156
228
|
>
|
|
@@ -165,7 +237,7 @@ You should then be able to run `just rebuild` in the project directory and then
|
|
|
165
237
|
> See https://github.com/profoundry-us/loco_motion-buildpack for more info.
|
|
166
238
|
|
|
167
239
|
From here, you can access the demo site at http://localhost:3000 and the YARD
|
|
168
|
-
docs at http://localhost:8808
|
|
240
|
+
docs at http://localhost:8808
|
|
169
241
|
|
|
170
242
|
You can type `just demo-shell` to open a shell inside the demo Docker container,
|
|
171
243
|
or `just loco-shell` to get a shell inside the gem's Docker container.
|
|
@@ -28,11 +28,48 @@
|
|
|
28
28
|
@custom-variant where (:where(&));
|
|
29
29
|
|
|
30
30
|
/*
|
|
31
|
-
* `dark` variant — make `dark:` utilities
|
|
32
|
-
*
|
|
33
|
-
*
|
|
31
|
+
* `dark` variant — make `dark:` utilities follow the app's actual theme, not
|
|
32
|
+
* just the OS preference. In priority order, `dark:` styles apply when:
|
|
33
|
+
*
|
|
34
|
+
* 1. `data-color-scheme="dark"` is set on an ancestor. LocoMotion's
|
|
35
|
+
* ThemeController stamps this on `<html>` from the active theme's own
|
|
36
|
+
* `color-scheme` declaration, so *any* dark DaisyUI theme (`night`,
|
|
37
|
+
* `synthwave`, a custom theme, …) enables `dark:` utilities — no
|
|
38
|
+
* hardcoded names.
|
|
39
|
+
* 2. `data-theme="dark"` is set on an ancestor — the name-based fallback
|
|
40
|
+
* for apps that manage `data-theme` themselves without stamping a
|
|
41
|
+
* scheme.
|
|
42
|
+
* 3. A checked `.theme-controller` input with `value="dark"` is present —
|
|
43
|
+
* kept for pure-CSS/no-JS DaisyUI theme switching, where the theme
|
|
44
|
+
* changes without `data-theme` ever being set.
|
|
45
|
+
* 4. The OS prefers dark (`prefers-color-scheme`) — the no-choice-saved
|
|
46
|
+
* fallback, suppressed when any theme other than `dark` is explicitly
|
|
47
|
+
* applied via `data-theme`, or when the stamped scheme is `light`
|
|
48
|
+
* (mirrors DaisyUI's `--default` / `--prefersdark` semantics: an
|
|
49
|
+
* explicit choice always wins).
|
|
50
|
+
*
|
|
51
|
+
* Note: without the scheme stamping (1), the fallbacks key on the theme
|
|
52
|
+
* *name* `dark`. Apps not running LocoMotion's ThemeController should either
|
|
53
|
+
* restyle the built-in `dark` theme (`@plugin "daisyui/theme" { name:
|
|
54
|
+
* "dark"; prefersdark: true; ... }`), stamp `data-color-scheme` themselves,
|
|
55
|
+
* or define their own `dark` variant.
|
|
34
56
|
*/
|
|
35
|
-
@custom-variant dark
|
|
57
|
+
@custom-variant dark {
|
|
58
|
+
&:where([data-color-scheme="dark"], [data-color-scheme="dark"] *) {
|
|
59
|
+
@slot;
|
|
60
|
+
}
|
|
61
|
+
&:where([data-theme="dark"], [data-theme="dark"] *) {
|
|
62
|
+
@slot;
|
|
63
|
+
}
|
|
64
|
+
:root:has(input.theme-controller[value="dark"]:checked) & {
|
|
65
|
+
@slot;
|
|
66
|
+
}
|
|
67
|
+
@media (prefers-color-scheme: dark) {
|
|
68
|
+
&:not(:where([data-theme]:not([data-theme="dark"]), [data-theme]:not([data-theme="dark"]) *, [data-color-scheme="light"], [data-color-scheme="light"] *)) {
|
|
69
|
+
@slot;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
36
73
|
|
|
37
74
|
/*
|
|
38
75
|
* Reveal tooltips on keyboard focus when the `tooltip` class is applied
|
|
@@ -4,15 +4,41 @@
|
|
|
4
4
|
* A Stimulus controller that manages theme selection and persistence.
|
|
5
5
|
* It handles theme switching, localStorage persistence, and synchronization
|
|
6
6
|
* across multiple theme selectors on the same page.
|
|
7
|
+
*
|
|
8
|
+
* Themes are stored in two preference SLOTS — day and night — so users can
|
|
9
|
+
* keep a preferred theme for each. Either slot may hold ANY theme (some
|
|
10
|
+
* people run a dark theme during the day):
|
|
11
|
+
*
|
|
12
|
+
* - `savedLightTheme` / `savedDarkTheme` — the theme saved in the day /
|
|
13
|
+
* night slot. Classic switchers file a pick under the slot matching the
|
|
14
|
+
* theme's own `color-scheme` declaration; slot-scoped pickers (the
|
|
15
|
+
* `setSchemeTheme` action) file it under THEIR slot regardless.
|
|
16
|
+
* - `savedLightScheme` / `savedDarkScheme` — the actual `color-scheme`
|
|
17
|
+
* of the theme in each slot, cached at save time so the pre-paint
|
|
18
|
+
* script can stamp it before any CSS loads.
|
|
19
|
+
* - `savedThemeMode` — which slot is active: `"light"` or `"dark"` pin
|
|
20
|
+
* that slot, while `"system"` follows the OS preference live, swapping
|
|
21
|
+
* between the two saved themes as the OS switches.
|
|
22
|
+
* - The legacy single `savedTheme` key is migrated on connect.
|
|
23
|
+
*
|
|
24
|
+
* Whenever a theme is applied, the DISPLAYED theme's own scheme is stamped
|
|
25
|
+
* on `<html>` as `data-color-scheme`, which drives the `dark:` Tailwind
|
|
26
|
+
* variant shipped in loco.css — so `dark:` utilities follow the theme
|
|
27
|
+
* actually showing, even a dark theme sitting in the day slot. The active
|
|
28
|
+
* slot is stamped as `data-theme-mode`.
|
|
7
29
|
*/
|
|
8
30
|
import { Controller } from "@hotwired/stimulus"
|
|
9
31
|
|
|
10
32
|
export default class extends Controller {
|
|
11
33
|
/**
|
|
12
34
|
* Called when the controller is connected to the DOM.
|
|
13
|
-
*
|
|
35
|
+
* Migrates legacy storage, stamps the active color scheme, sets the
|
|
36
|
+
* initial theme input state, and sets up event listeners.
|
|
14
37
|
*/
|
|
15
38
|
connect() {
|
|
39
|
+
this.migrateLegacyStorage()
|
|
40
|
+
this.stampScheme()
|
|
41
|
+
this.stampMode(this.safeStorageGet('savedThemeMode'))
|
|
16
42
|
this.setInput()
|
|
17
43
|
|
|
18
44
|
// Setup a custom listener to watch for changes on the page in case the
|
|
@@ -26,6 +52,11 @@ export default class extends Controller {
|
|
|
26
52
|
// enough to save the theme and keep every other selector in sync.
|
|
27
53
|
this.inputChangeListener = this.handleInputChange.bind(this)
|
|
28
54
|
this.element.addEventListener('change', this.inputChangeListener)
|
|
55
|
+
|
|
56
|
+
// In "system" mode the active theme follows the OS preference live.
|
|
57
|
+
this.mediaQuery = window.matchMedia('(prefers-color-scheme: dark)')
|
|
58
|
+
this.mediaChangeListener = this.mediaChanged.bind(this)
|
|
59
|
+
this.mediaQuery.addEventListener('change', this.mediaChangeListener)
|
|
29
60
|
}
|
|
30
61
|
|
|
31
62
|
/**
|
|
@@ -35,6 +66,7 @@ export default class extends Controller {
|
|
|
35
66
|
disconnect() {
|
|
36
67
|
window.removeEventListener('localstorage-update', this.storageChangeListener)
|
|
37
68
|
this.element.removeEventListener('change', this.inputChangeListener)
|
|
69
|
+
this.mediaQuery.removeEventListener('change', this.mediaChangeListener)
|
|
38
70
|
}
|
|
39
71
|
|
|
40
72
|
/**
|
|
@@ -45,17 +77,44 @@ export default class extends Controller {
|
|
|
45
77
|
*/
|
|
46
78
|
setInput() {
|
|
47
79
|
const theme = this.getCurrentTheme()
|
|
48
|
-
const inputs = this.element.querySelectorAll('input.theme-controller')
|
|
80
|
+
const inputs = this.element.querySelectorAll('input.theme-controller, input[data-loco-theme-scheme]')
|
|
49
81
|
|
|
50
82
|
inputs.forEach((input) => {
|
|
51
|
-
|
|
83
|
+
const scheme = input.dataset.locoThemeScheme
|
|
84
|
+
|
|
85
|
+
if (scheme) {
|
|
86
|
+
// Scheme-scoped picker radios show their SLOT's saved theme
|
|
87
|
+
// (falling back to the built-in theme of the same name), not the
|
|
88
|
+
// theme currently applied to the page.
|
|
89
|
+
const saved = this.safeStorageGet(scheme === 'dark' ? 'savedDarkTheme' : 'savedLightTheme')
|
|
90
|
+
|
|
91
|
+
input.checked = input.value === (saved || scheme)
|
|
92
|
+
} else {
|
|
93
|
+
input.checked = input.value === theme
|
|
94
|
+
}
|
|
95
|
+
})
|
|
96
|
+
|
|
97
|
+
// "Match system appearance" toggles reflect the saved mode, and
|
|
98
|
+
// "Night mode" toggles reflect the scheme actually showing (so they
|
|
99
|
+
// read correctly in system mode too).
|
|
100
|
+
const synced = this.safeStorageGet('savedThemeMode') === 'system'
|
|
101
|
+
const resolved = this.resolveTheme()
|
|
102
|
+
const activeScheme = (resolved && resolved.scheme) || this.computedScheme()
|
|
103
|
+
|
|
104
|
+
this.element.querySelectorAll('input[data-loco-theme-mode-toggle]').forEach((toggle) => {
|
|
105
|
+
toggle.checked = synced
|
|
106
|
+
})
|
|
107
|
+
|
|
108
|
+
this.element.querySelectorAll('input[data-loco-theme-night-toggle]').forEach((toggle) => {
|
|
109
|
+
toggle.checked = activeScheme === 'dark'
|
|
52
110
|
})
|
|
53
111
|
}
|
|
54
112
|
|
|
55
113
|
/**
|
|
56
|
-
* Clears the user's theme
|
|
57
|
-
* Removes
|
|
58
|
-
* Also removes the data-theme
|
|
114
|
+
* Clears the user's theme preferences from localStorage.
|
|
115
|
+
* Removes every saved theme key and dispatches an event to notify other
|
|
116
|
+
* controllers. Also removes the `data-theme` and `data-color-scheme`
|
|
117
|
+
* attributes from the document element.
|
|
59
118
|
*
|
|
60
119
|
* @param {Event} event - The triggering click event
|
|
61
120
|
*/
|
|
@@ -71,15 +130,21 @@ export default class extends Controller {
|
|
|
71
130
|
}
|
|
72
131
|
}
|
|
73
132
|
|
|
74
|
-
// Remove
|
|
133
|
+
// Remove every saved theme key from local storage
|
|
75
134
|
this.safeStorageRemove("savedTheme")
|
|
135
|
+
this.safeStorageRemove("savedThemeMode")
|
|
136
|
+
this.safeStorageRemove("savedLightTheme")
|
|
137
|
+
this.safeStorageRemove("savedDarkTheme")
|
|
138
|
+
this.safeStorageRemove("savedLightScheme")
|
|
139
|
+
this.safeStorageRemove("savedDarkScheme")
|
|
76
140
|
|
|
77
|
-
// Remove the
|
|
141
|
+
// Remove the theme attributes from the document element
|
|
78
142
|
document.documentElement.removeAttribute('data-theme')
|
|
143
|
+
document.documentElement.removeAttribute('data-color-scheme')
|
|
144
|
+
document.documentElement.removeAttribute('data-theme-mode')
|
|
79
145
|
|
|
80
146
|
// Fire off an update
|
|
81
|
-
|
|
82
|
-
window.dispatchEvent(updateEvent)
|
|
147
|
+
this.broadcast(null)
|
|
83
148
|
}
|
|
84
149
|
|
|
85
150
|
/**
|
|
@@ -106,6 +171,94 @@ export default class extends Controller {
|
|
|
106
171
|
event.preventDefault()
|
|
107
172
|
}
|
|
108
173
|
|
|
174
|
+
/**
|
|
175
|
+
* Changes how the active color scheme is chosen. Expects a Stimulus action
|
|
176
|
+
* param, e.g. `data-loco-theme-mode-param="system"`:
|
|
177
|
+
*
|
|
178
|
+
* - `"light"` / `"dark"` — pin that scheme; its saved theme (or the
|
|
179
|
+
* built-in `light`/`dark` theme when none is saved) applies.
|
|
180
|
+
* - `"system"` — follow the OS preference live, swapping between the
|
|
181
|
+
* saved light and dark themes as the OS switches.
|
|
182
|
+
*
|
|
183
|
+
* @param {Event} event - The triggering event with a `mode` param
|
|
184
|
+
*/
|
|
185
|
+
setMode(event) {
|
|
186
|
+
const mode = event && event.params && event.params.mode
|
|
187
|
+
|
|
188
|
+
if (!mode) return
|
|
189
|
+
|
|
190
|
+
this.safeStorageSet('savedThemeMode', mode)
|
|
191
|
+
this.stampMode(mode)
|
|
192
|
+
this.applyResolvedTheme()
|
|
193
|
+
|
|
194
|
+
if (event.preventDefault) event.preventDefault()
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* Applies a theme into a SPECIFIC preference slot — the action behind
|
|
199
|
+
* slot-scoped pickers (a "Day theme" / "Night theme" dropdown pair).
|
|
200
|
+
* Expects a Stimulus action param naming the slot, e.g.
|
|
201
|
+
* `data-loco-theme-scheme-param="light"`. Unlike a classic switcher pick
|
|
202
|
+
* (which files the theme under its own color-scheme), the pick lands in
|
|
203
|
+
* the picker's slot even when the theme's scheme differs — a dark theme
|
|
204
|
+
* can be someone's daytime choice. The pick applies immediately and pins
|
|
205
|
+
* its slot.
|
|
206
|
+
*
|
|
207
|
+
* Like {setTheme}, the action can sit on a wrapper element containing an
|
|
208
|
+
* `<input>` or on the input itself.
|
|
209
|
+
*
|
|
210
|
+
* @param {Event} event - The triggering click event with a `scheme` param
|
|
211
|
+
*/
|
|
212
|
+
setSchemeTheme(event) {
|
|
213
|
+
const slot = event && event.params && event.params.scheme
|
|
214
|
+
const target = event.currentTarget
|
|
215
|
+
const input = target.matches && target.matches('input')
|
|
216
|
+
? target
|
|
217
|
+
: target.querySelector('input')
|
|
218
|
+
|
|
219
|
+
if (input && slot) {
|
|
220
|
+
this.applyTheme(input.value, slot)
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
event.preventDefault()
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* Backs the "Night mode" toggle (see the component's
|
|
228
|
+
* `build_night_toggle`). Checking it pins the dark scheme — the saved
|
|
229
|
+
* night theme applies immediately, no OS setting required — and
|
|
230
|
+
* unchecking pins light. Either direction is an explicit choice, so it
|
|
231
|
+
* leaves `system` mode.
|
|
232
|
+
*
|
|
233
|
+
* @param {Event} event - The change event from the toggle's checkbox
|
|
234
|
+
*/
|
|
235
|
+
toggleNightMode(event) {
|
|
236
|
+
const mode = event.currentTarget.checked ? 'dark' : 'light'
|
|
237
|
+
|
|
238
|
+
this.safeStorageSet('savedThemeMode', mode)
|
|
239
|
+
this.stampMode(mode)
|
|
240
|
+
this.applyResolvedTheme()
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* Backs the "Match system appearance" toggle (see the component's
|
|
245
|
+
* `build_system_toggle`). Checking it enters `system` mode; unchecking it
|
|
246
|
+
* pins whichever scheme is currently showing, so the visible theme does
|
|
247
|
+
* not change when sync turns off.
|
|
248
|
+
*
|
|
249
|
+
* @param {Event} event - The change event from the toggle's checkbox
|
|
250
|
+
*/
|
|
251
|
+
toggleSystemMode(event) {
|
|
252
|
+
const resolved = this.resolveTheme()
|
|
253
|
+
const mode = event.currentTarget.checked
|
|
254
|
+
? 'system'
|
|
255
|
+
: ((resolved && resolved.scheme) || this.computedScheme())
|
|
256
|
+
|
|
257
|
+
this.safeStorageSet('savedThemeMode', mode)
|
|
258
|
+
this.stampMode(mode)
|
|
259
|
+
this.applyResolvedTheme()
|
|
260
|
+
}
|
|
261
|
+
|
|
109
262
|
/**
|
|
110
263
|
* Handles `change` events bubbling up from any theme-controller input within
|
|
111
264
|
* this controller. Persists and broadcasts the newly selected theme so every
|
|
@@ -126,14 +279,79 @@ export default class extends Controller {
|
|
|
126
279
|
* Persists the given theme, applies it to the document, and notifies every
|
|
127
280
|
* other theme controller on the page so they can sync their inputs.
|
|
128
281
|
*
|
|
282
|
+
* Without a `slot`, the theme is saved as the preference for the scheme
|
|
283
|
+
* it belongs to (read from its own computed `color-scheme`) — classic
|
|
284
|
+
* switcher behavior. With a `slot` (from a day / night picker), it lands
|
|
285
|
+
* in that slot regardless of its own scheme. Either way the slot's actual
|
|
286
|
+
* scheme is cached for the pre-paint script, the mode pins to the slot,
|
|
287
|
+
* and `data-color-scheme` reflects the DISPLAYED theme's own scheme so
|
|
288
|
+
* `dark:` utilities stay truthful.
|
|
289
|
+
*
|
|
129
290
|
* @param {string} value - The theme name to apply
|
|
291
|
+
* @param {?string} slot - The preference slot to fill (`"light"` /
|
|
292
|
+
* `"dark"`); defaults to the theme's own scheme
|
|
130
293
|
*/
|
|
131
|
-
applyTheme(value) {
|
|
132
|
-
this.safeStorageSet("savedTheme", value)
|
|
294
|
+
applyTheme(value, slot = null) {
|
|
133
295
|
document.documentElement.setAttribute('data-theme', value)
|
|
134
296
|
|
|
135
|
-
const
|
|
136
|
-
|
|
297
|
+
const scheme = this.schemeForTheme(value)
|
|
298
|
+
const targetSlot = slot || scheme
|
|
299
|
+
|
|
300
|
+
this.safeStorageSet(targetSlot === 'dark' ? 'savedDarkTheme' : 'savedLightTheme', value)
|
|
301
|
+
this.safeStorageSet(targetSlot === 'dark' ? 'savedDarkScheme' : 'savedLightScheme', scheme)
|
|
302
|
+
this.safeStorageSet('savedThemeMode', targetSlot)
|
|
303
|
+
this.safeStorageRemove('savedTheme')
|
|
304
|
+
|
|
305
|
+
document.documentElement.setAttribute('data-color-scheme', scheme)
|
|
306
|
+
this.stampMode(targetSlot)
|
|
307
|
+
|
|
308
|
+
this.broadcast(value)
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
/**
|
|
312
|
+
* Resolves the active theme from the saved mode + per-scheme preferences
|
|
313
|
+
* and applies it: sets `data-theme`, stamps `data-color-scheme`, and
|
|
314
|
+
* broadcasts so all selectors re-sync. Used when the mode or the OS
|
|
315
|
+
* preference changes.
|
|
316
|
+
*/
|
|
317
|
+
applyResolvedTheme() {
|
|
318
|
+
const resolved = this.resolveTheme()
|
|
319
|
+
|
|
320
|
+
if (!resolved) return
|
|
321
|
+
|
|
322
|
+
document.documentElement.setAttribute('data-theme', resolved.theme)
|
|
323
|
+
|
|
324
|
+
// Stamp the DISPLAYED theme's own scheme, not the slot name — the day
|
|
325
|
+
// slot can legitimately hold a dark theme. Prefer the scheme cached at
|
|
326
|
+
// save time (it works even when the theme's CSS isn't loaded); probe
|
|
327
|
+
// the stylesheet only as a fallback.
|
|
328
|
+
const cached = this.safeStorageGet(resolved.scheme === 'dark' ? 'savedDarkScheme' : 'savedLightScheme')
|
|
329
|
+
|
|
330
|
+
document.documentElement.setAttribute('data-color-scheme', cached || this.schemeForTheme(resolved.theme))
|
|
331
|
+
|
|
332
|
+
this.broadcast(resolved.theme)
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
/**
|
|
336
|
+
* Resolves which theme and scheme should be active from localStorage.
|
|
337
|
+
*
|
|
338
|
+
* @returns {?{theme: string, scheme: string}} The resolved theme and
|
|
339
|
+
* scheme, or null when no mode is saved (no explicit choice yet)
|
|
340
|
+
*/
|
|
341
|
+
resolveTheme() {
|
|
342
|
+
const mode = this.safeStorageGet('savedThemeMode')
|
|
343
|
+
|
|
344
|
+
if (!mode) return null
|
|
345
|
+
|
|
346
|
+
const scheme = mode === 'system'
|
|
347
|
+
? (this.mediaQuery && this.mediaQuery.matches ? 'dark' : 'light')
|
|
348
|
+
: mode
|
|
349
|
+
const saved = this.safeStorageGet(scheme === 'dark' ? 'savedDarkTheme' : 'savedLightTheme')
|
|
350
|
+
|
|
351
|
+
// With no saved preference for the scheme, fall back to the built-in
|
|
352
|
+
// theme of the same name — DaisyUI always defines `light` and `dark`,
|
|
353
|
+
// and downstream apps restyle those names rather than renaming them.
|
|
354
|
+
return { theme: saved || scheme, scheme }
|
|
137
355
|
}
|
|
138
356
|
|
|
139
357
|
/**
|
|
@@ -149,15 +367,122 @@ export default class extends Controller {
|
|
|
149
367
|
* @returns {?string} The current theme name, or null if none can be determined
|
|
150
368
|
*/
|
|
151
369
|
getCurrentTheme() {
|
|
152
|
-
const
|
|
370
|
+
const resolved = this.resolveTheme()
|
|
153
371
|
|
|
154
|
-
if (
|
|
155
|
-
return
|
|
372
|
+
if (resolved) {
|
|
373
|
+
return resolved.theme
|
|
156
374
|
}
|
|
157
375
|
|
|
158
376
|
return document.documentElement.getAttribute('data-theme')
|
|
159
377
|
}
|
|
160
378
|
|
|
379
|
+
/**
|
|
380
|
+
* Migrates the legacy single `savedTheme` key to the per-scheme model by
|
|
381
|
+
* re-applying it through `applyTheme`, which classifies the theme by its
|
|
382
|
+
* own `color-scheme`, saves it into the matching slot, pins the mode, and
|
|
383
|
+
* removes the legacy key.
|
|
384
|
+
*/
|
|
385
|
+
migrateLegacyStorage() {
|
|
386
|
+
const legacy = this.safeStorageGet('savedTheme')
|
|
387
|
+
|
|
388
|
+
if (legacy && !this.safeStorageGet('savedThemeMode')) {
|
|
389
|
+
this.applyTheme(legacy)
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
/**
|
|
394
|
+
* Stamps `data-color-scheme` on the document element from the active
|
|
395
|
+
* theme's computed `color-scheme` when an explicit theme is applied but no
|
|
396
|
+
* scheme is stamped yet (e.g. a server-rendered `data-theme`).
|
|
397
|
+
*/
|
|
398
|
+
stampScheme() {
|
|
399
|
+
const root = document.documentElement
|
|
400
|
+
|
|
401
|
+
if (root.getAttribute('data-theme') && !root.getAttribute('data-color-scheme')) {
|
|
402
|
+
root.setAttribute('data-color-scheme', this.computedScheme())
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
/**
|
|
407
|
+
* Stamps the saved theme mode on `<html>` as `data-theme-mode` so UI can
|
|
408
|
+
* reflect the active mode with pure CSS — e.g. the switcher dropdown's
|
|
409
|
+
* "Sync with system" checkmark uses a
|
|
410
|
+
* `[[data-theme-mode=system]_&]:visible` variant. Removes the attribute
|
|
411
|
+
* when no mode is saved.
|
|
412
|
+
*
|
|
413
|
+
* @param {?string} mode - `"light"`, `"dark"`, `"system"`, or null
|
|
414
|
+
*/
|
|
415
|
+
stampMode(mode) {
|
|
416
|
+
if (mode) {
|
|
417
|
+
document.documentElement.setAttribute('data-theme-mode', mode)
|
|
418
|
+
} else {
|
|
419
|
+
document.documentElement.removeAttribute('data-theme-mode')
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
/**
|
|
424
|
+
* Reads the active theme's own scheme from the document element's computed
|
|
425
|
+
* `color-scheme` — every DaisyUI theme declares `color-scheme: light` or
|
|
426
|
+
* `color-scheme: dark`.
|
|
427
|
+
*
|
|
428
|
+
* @returns {string} `"dark"` or `"light"`
|
|
429
|
+
*/
|
|
430
|
+
computedScheme() {
|
|
431
|
+
const value = getComputedStyle(document.documentElement).colorScheme || ''
|
|
432
|
+
|
|
433
|
+
return value.includes('dark') && !value.includes('light') ? 'dark' : 'light'
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
/**
|
|
437
|
+
* Classifies a specific theme by probing its own `color-scheme`
|
|
438
|
+
* declaration on a scratch element.
|
|
439
|
+
*
|
|
440
|
+
* Reading the ROOT's computed value at apply time is unreliable: DaisyUI
|
|
441
|
+
* theme blocks also match `:root:has(input.theme-controller:checked)`,
|
|
442
|
+
* which outranks `[data-theme]` in the cascade — so until the page's
|
|
443
|
+
* radios re-sync, the PREVIOUS theme's checked radio can misclassify the
|
|
444
|
+
* theme being applied (e.g. synthwave filed as a light preference). The
|
|
445
|
+
* probe carries only `data-theme`, so only the theme's own declaration
|
|
446
|
+
* can match it.
|
|
447
|
+
*
|
|
448
|
+
* @param {string} value - The theme name to classify
|
|
449
|
+
* @returns {string} `"dark"` or `"light"`
|
|
450
|
+
*/
|
|
451
|
+
schemeForTheme(value) {
|
|
452
|
+
const probe = document.createElement('div')
|
|
453
|
+
|
|
454
|
+
probe.setAttribute('data-theme', value)
|
|
455
|
+
probe.style.display = 'none'
|
|
456
|
+
document.documentElement.appendChild(probe)
|
|
457
|
+
|
|
458
|
+
const scheme = getComputedStyle(probe).colorScheme || ''
|
|
459
|
+
|
|
460
|
+
probe.remove()
|
|
461
|
+
|
|
462
|
+
return scheme.includes('dark') && !scheme.includes('light') ? 'dark' : 'light'
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
/**
|
|
466
|
+
* Responds to OS color-scheme changes. Only relevant in `"system"` mode,
|
|
467
|
+
* where the active theme swaps between the saved light and dark themes.
|
|
468
|
+
*/
|
|
469
|
+
mediaChanged() {
|
|
470
|
+
if (this.safeStorageGet('savedThemeMode') === 'system') {
|
|
471
|
+
this.applyResolvedTheme()
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
/**
|
|
476
|
+
* Notifies every theme controller on the page (including this one) that
|
|
477
|
+
* the theme changed so they can re-sync their inputs.
|
|
478
|
+
*
|
|
479
|
+
* @param {?string} value - The newly applied theme name, or null on clear
|
|
480
|
+
*/
|
|
481
|
+
broadcast(value) {
|
|
482
|
+
const updateEvent = new CustomEvent('localstorage-update', { detail: { key: 'savedTheme', newValue: value } })
|
|
483
|
+
window.dispatchEvent(updateEvent)
|
|
484
|
+
}
|
|
485
|
+
|
|
161
486
|
/**
|
|
162
487
|
* Safely reads a value from localStorage. Access can throw in some
|
|
163
488
|
* environments (e.g. Safari private browsing), so failures are swallowed
|