tailwind_theme_picker 0.1.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 +7 -0
- data/LICENSE.txt +21 -0
- data/README.md +84 -0
- data/app/helpers/tailwind_theme_picker/view_helper.rb +73 -0
- data/app/javascript/tailwind_theme_picker/controllers/theme_controller.js +111 -0
- data/app/views/tailwind_theme_picker/_picker.html.slim +15 -0
- data/assets/stylesheets/tailwind_theme_picker/themes.css +41 -0
- data/config/importmap.rb +1 -0
- data/lib/generators/tailwind_theme_picker/install/install_generator.rb +52 -0
- data/lib/tailwind_theme_picker/configuration.rb +23 -0
- data/lib/tailwind_theme_picker/engine.rb +28 -0
- data/lib/tailwind_theme_picker/version.rb +5 -0
- data/lib/tailwind_theme_picker.rb +25 -0
- metadata +87 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: f6b8bf4b275e9146174dd904158ed0a212c4be814070e8425fddceee8cd17829
|
|
4
|
+
data.tar.gz: 37df132c76d2f17771d8307edfb2f66fb0e3c47072cd6801f3c0f6f84c5d50dc
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: aa50cef29ff1643fe124814939db157180b7569ebd152004276b64b16821a22084ca45e252c438368da34544d0ba92f849df71ee815dd98ccbcba3c3fd8d3c30
|
|
7
|
+
data.tar.gz: 375d7df84f0c51866e2423f5cb813b2f9ac689f95e6309a3ecf78431997a6d17144e9381c2addfd83962812e6747185c6f63d19f36ead61c556048f8c472f8a3
|
data/LICENSE.txt
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Tayden Miller
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
# TailwindThemePicker
|
|
2
|
+
|
|
3
|
+
Drop-in theme + light/dark picker for Tailwind-based Rails apps.
|
|
4
|
+
|
|
5
|
+
## What's in the box
|
|
6
|
+
|
|
7
|
+
- Floating Stimulus-powered picker UI (palette toggle, 27 color swatches, light/dark switch)
|
|
8
|
+
- Two-cookie persistence (`theme`, `mode`) — the server can render `<html class="theme-sky dark">` before any JS runs
|
|
9
|
+
- localStorage fallback so cross-tab updates and offline still feel snappy
|
|
10
|
+
- 27 ready-to-go `.theme-*` blocks targeting Tailwind's color palette
|
|
11
|
+
- Inline FOUC script that's only emitted when cookies aren't yet set
|
|
12
|
+
|
|
13
|
+
## Install
|
|
14
|
+
|
|
15
|
+
```ruby
|
|
16
|
+
# Gemfile
|
|
17
|
+
gem "tailwind_theme_picker", path: "../theme_picker" # or git: / version once published
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
bundle install
|
|
22
|
+
bin/rails g tailwind_theme_picker:install # copies themes.css into app/assets/tailwind/tailwind_theme_picker/
|
|
23
|
+
bin/rails g tailwind_theme_picker:install --initializer # also generate config/initializers/tailwind_theme_picker.rb
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
Re-run the generator after upgrading the gem to pick up any new theme rules.
|
|
27
|
+
|
|
28
|
+
### Configure (optional)
|
|
29
|
+
|
|
30
|
+
```ruby
|
|
31
|
+
# config/initializers/tailwind_theme_picker.rb
|
|
32
|
+
TailwindThemePicker.configure do |c|
|
|
33
|
+
c.themes = %w[red blue green] # subset or extend the default 27
|
|
34
|
+
c.default = "blue"
|
|
35
|
+
end
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
### Wire up the layout
|
|
39
|
+
|
|
40
|
+
```slim
|
|
41
|
+
html lang="en" *theme_picker_html_attrs
|
|
42
|
+
head
|
|
43
|
+
/ ...stylesheets, importmap...
|
|
44
|
+
= theme_picker_fouc_script
|
|
45
|
+
body
|
|
46
|
+
= render_theme_picker
|
|
47
|
+
= yield
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
### Import the CSS
|
|
51
|
+
|
|
52
|
+
```css
|
|
53
|
+
/* app/assets/tailwind/application.css */
|
|
54
|
+
@import 'tailwindcss';
|
|
55
|
+
|
|
56
|
+
/* ...your other rules... */
|
|
57
|
+
|
|
58
|
+
@import './tailwind_theme_picker/themes';
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
The gem's CSS rules are intentionally unlayered so they win over a `:root { --color-primary: ... }` default inside `@layer base`. Keep this import outside any `@layer` block.
|
|
62
|
+
|
|
63
|
+
Tailwind needs `darkMode: 'class'` in your config. Your app should also define the `--color-primary` custom property and any utilities (`.bg-app`, `.btn-primary`, etc.) that consume it.
|
|
64
|
+
|
|
65
|
+
### Remove your old controller
|
|
66
|
+
|
|
67
|
+
If you previously had `app/javascript/controllers/theme_controller.js`, delete it — the gem pins one at the same import name.
|
|
68
|
+
|
|
69
|
+
## How persistence works
|
|
70
|
+
|
|
71
|
+
1. Stimulus controller writes both cookies on every change.
|
|
72
|
+
2. On the next request, `theme_picker_html_attrs` reads them and paints `<html>` server-side. No FOUC.
|
|
73
|
+
3. On the user's *first* visit (no cookies), `theme_picker_fouc_script` emits ~250 bytes of inline JS that paints from localStorage or system preference. After that, cookies take over and the helper returns an empty string.
|
|
74
|
+
|
|
75
|
+
## Configuration reference
|
|
76
|
+
|
|
77
|
+
| Setting | Default | Notes |
|
|
78
|
+
|------------------|------------------------------|-------|
|
|
79
|
+
| `themes` | 27-color rainbow | Must match `.theme-*` rules you actually ship. |
|
|
80
|
+
| `default` | `"sky"` | Used when cookie is absent or refers to an unknown theme. |
|
|
81
|
+
| `theme_cookie` | `"theme"` | |
|
|
82
|
+
| `mode_cookie` | `"mode"` | |
|
|
83
|
+
| `cookie_max_age` | one year | Seconds. |
|
|
84
|
+
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
module TailwindThemePicker
|
|
2
|
+
|
|
3
|
+
module ViewHelper
|
|
4
|
+
|
|
5
|
+
# Emoji defaults so the gem has no icon-library dependency. Override by
|
|
6
|
+
# passing :icons to render_theme_picker.
|
|
7
|
+
DEFAULT_ICONS = {
|
|
8
|
+
palette: "🎨",
|
|
9
|
+
times: "❌",
|
|
10
|
+
sun: "☀️",
|
|
11
|
+
moon: "🌙"
|
|
12
|
+
}.freeze
|
|
13
|
+
|
|
14
|
+
# Returns HTML attributes for the <html> tag based on the request's cookies.
|
|
15
|
+
# Use like: html lang="en" *theme_picker_html_attrs
|
|
16
|
+
def theme_picker_html_attrs
|
|
17
|
+
config = TailwindThemePicker.configuration
|
|
18
|
+
theme = cookies[config.theme_cookie].presence
|
|
19
|
+
theme = config.default unless config.themes.include?(theme)
|
|
20
|
+
mode = cookies[config.mode_cookie] == "dark" ? "dark" : nil
|
|
21
|
+
|
|
22
|
+
classes = ["theme-#{theme}", mode].compact.join(" ")
|
|
23
|
+
{ class: classes }
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# Whether the current request already has theme cookies set. Useful for
|
|
27
|
+
# skipping the FOUC fallback script.
|
|
28
|
+
def theme_picker_cookies_present?
|
|
29
|
+
config = TailwindThemePicker.configuration
|
|
30
|
+
cookies[config.theme_cookie].present? && cookies[config.mode_cookie].present?
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# Inline <script> that paints theme + mode classes from cookies/localStorage
|
|
34
|
+
# before the rest of the page renders. Only needed on the first visit (when
|
|
35
|
+
# the cookies aren't yet set server-side) — pass force: true to always emit.
|
|
36
|
+
def theme_picker_fouc_script(force: false)
|
|
37
|
+
return "".html_safe if !force && theme_picker_cookies_present?
|
|
38
|
+
|
|
39
|
+
config = TailwindThemePicker.configuration
|
|
40
|
+
nonce = (respond_to?(:content_security_policy_nonce) ? content_security_policy_nonce : nil)
|
|
41
|
+
|
|
42
|
+
js = <<~JS
|
|
43
|
+
(function(){try{
|
|
44
|
+
var d=document.documentElement;
|
|
45
|
+
var rc=function(n){var m=document.cookie.match(new RegExp('(?:^|; )'+n+'=([^;]*)'));return m?decodeURIComponent(m[1]):null};
|
|
46
|
+
var tn=rc('#{config.theme_cookie}')||localStorage.getItem('theme')||'#{config.default}';
|
|
47
|
+
d.classList.add('theme-'+tn);
|
|
48
|
+
var m=rc('#{config.mode_cookie}')||localStorage.getItem('mode');
|
|
49
|
+
if(m==='dark'||(!m&&window.matchMedia('(prefers-color-scheme: dark)').matches)){d.classList.add('dark');}else{d.classList.remove('dark');}
|
|
50
|
+
}catch(e){}})();
|
|
51
|
+
JS
|
|
52
|
+
|
|
53
|
+
content_tag(:script, js.html_safe, nonce: nonce)
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def render_theme_picker(themes: nil, default: nil, icons: {})
|
|
57
|
+
config = TailwindThemePicker.configuration
|
|
58
|
+
render(
|
|
59
|
+
partial: "tailwind_theme_picker/picker",
|
|
60
|
+
locals: {
|
|
61
|
+
themes: (themes || config.themes).map(&:to_s),
|
|
62
|
+
default: (default || config.default).to_s,
|
|
63
|
+
theme_cookie: config.theme_cookie,
|
|
64
|
+
mode_cookie: config.mode_cookie,
|
|
65
|
+
cookie_max_age: config.cookie_max_age,
|
|
66
|
+
icons: DEFAULT_ICONS.merge(icons)
|
|
67
|
+
}
|
|
68
|
+
)
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
end
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
/*
|
|
2
|
+
TailwindThemePicker Stimulus controller.
|
|
3
|
+
|
|
4
|
+
Persists user choice in TWO cookies (theme, mode) so the server can render the
|
|
5
|
+
matching classes on <html> before any JS runs. localStorage is kept as a tab-local
|
|
6
|
+
cache so cross-tab updates and offline still work.
|
|
7
|
+
|
|
8
|
+
If you change cookie names here, update TailwindThemePicker::Configuration and any
|
|
9
|
+
FOUC script the host emits.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { Controller } from "@hotwired/stimulus"
|
|
13
|
+
|
|
14
|
+
export default class extends Controller {
|
|
15
|
+
|
|
16
|
+
static values = {
|
|
17
|
+
themes: { type: Array, default: [] },
|
|
18
|
+
defaultTheme: { type: String, default: "" },
|
|
19
|
+
theme: { type: String, default: "" },
|
|
20
|
+
darkMode: { type: Boolean, default: false },
|
|
21
|
+
themeCookie: { type: String, default: "theme" },
|
|
22
|
+
modeCookie: { type: String, default: "mode" },
|
|
23
|
+
cookieMaxAge: { type: Number, default: 31536000 }
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
static targets = [ "toggle", "panel", "mode" ]
|
|
27
|
+
|
|
28
|
+
connect() {
|
|
29
|
+
this.themeValue = this.defaultThemeValue || this.themesValue[0]
|
|
30
|
+
|
|
31
|
+
const prefersDark = window.matchMedia("(prefers-color-scheme: dark)").matches
|
|
32
|
+
|
|
33
|
+
// Cookie wins over localStorage (server painted html based on cookie).
|
|
34
|
+
const cookieTheme = this.readCookie(this.themeCookieValue)
|
|
35
|
+
const cookieMode = this.readCookie(this.modeCookieValue)
|
|
36
|
+
|
|
37
|
+
try {
|
|
38
|
+
this.themeValue = cookieTheme || localStorage.getItem("theme") || this.themeValue
|
|
39
|
+
try { localStorage.setItem("theme", this.themeValue) } catch(_) { }
|
|
40
|
+
|
|
41
|
+
const storedMode = cookieMode || localStorage.getItem("mode")
|
|
42
|
+
this.darkModeValue = storedMode === null ? prefersDark : storedMode === "dark"
|
|
43
|
+
try { localStorage.setItem("mode", this.darkModeValue ? "dark" : "light") } catch(_) { }
|
|
44
|
+
} catch(_) {}
|
|
45
|
+
|
|
46
|
+
if (!this.themesValue.includes(this.themeValue)) this.themeValue = this.defaultThemeValue
|
|
47
|
+
|
|
48
|
+
// Backfill cookies in case this is the user's first visit.
|
|
49
|
+
this.writeCookie(this.themeCookieValue, this.themeValue)
|
|
50
|
+
this.writeCookie(this.modeCookieValue, this.darkModeValue ? "dark" : "light")
|
|
51
|
+
|
|
52
|
+
this.applyThemeClass(this.themeValue)
|
|
53
|
+
|
|
54
|
+
if (this.darkModeValue) {
|
|
55
|
+
document.documentElement.classList.add("dark")
|
|
56
|
+
this.swapIcon(this.modeTarget)
|
|
57
|
+
} else
|
|
58
|
+
document.documentElement.classList.remove("dark")
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
togglePanel() {
|
|
62
|
+
this.panelTarget.classList.toggle("hidden")
|
|
63
|
+
this.swapIcon(this.toggleTarget)
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
toggleMode() {
|
|
67
|
+
this.darkModeValue = !this.darkModeValue
|
|
68
|
+
document.documentElement.classList.toggle("dark")
|
|
69
|
+
this.swapIcon(this.modeTarget)
|
|
70
|
+
const mode = this.darkModeValue ? "dark" : "light"
|
|
71
|
+
try { localStorage.setItem("mode", mode) } catch(_) { }
|
|
72
|
+
this.writeCookie(this.modeCookieValue, mode)
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
choose(event) {
|
|
76
|
+
const name = event.currentTarget.dataset.themeName
|
|
77
|
+
this.applyThemeClass(name)
|
|
78
|
+
this.themeValue = name
|
|
79
|
+
try { localStorage.setItem("theme", name) } catch(_) { }
|
|
80
|
+
this.writeCookie(this.themeCookieValue, name)
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
preview(event) {
|
|
84
|
+
this.applyThemeClass(event.currentTarget?.dataset.themeName)
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
cancelPreview() {
|
|
88
|
+
this.applyThemeClass(this.themeValue)
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
applyThemeClass(theme) {
|
|
92
|
+
document.documentElement.className = document.documentElement.className.replace(/theme-[a-z]+/g, "").trim()
|
|
93
|
+
document.documentElement.classList.add(`theme-${theme}`)
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
swapIcon(element) {
|
|
97
|
+
const current = element.innerHTML
|
|
98
|
+
element.innerHTML = element.dataset.icon
|
|
99
|
+
element.dataset.icon = current
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
readCookie(name) {
|
|
103
|
+
const match = document.cookie.match(new RegExp("(?:^|; )" + name.replace(/[.$?*|{}()[\]\\\/+^]/g, "\\$&") + "=([^;]*)"))
|
|
104
|
+
return match ? decodeURIComponent(match[1]) : null
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
writeCookie(name, value) {
|
|
108
|
+
document.cookie = `${name}=${encodeURIComponent(value)}; path=/; max-age=${this.cookieMaxAgeValue}; SameSite=Lax`
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
.fixed.top-2.right-2.z-50.flex.flex-col.items-end.gap-2 data-controller="theme" data-theme-themes-value=themes data-theme-default-theme-value=default data-theme-theme-value="" data-theme-theme-cookie-value=theme_cookie data-theme-mode-cookie-value=mode_cookie data-theme-cookie-max-age-value=cookie_max_age
|
|
2
|
+
|
|
3
|
+
button.flex-center.w-8.aspect-square.border.border-base.rounded.btn-primary.text-fg.text-lg.cursor-pointer type="button" data-action="theme#togglePanel" data-theme-target="toggle" data-icon=icons[:times].to_s.gsub('"', "'")
|
|
4
|
+
|
|
5
|
+
= icons[:palette]
|
|
6
|
+
|
|
7
|
+
.flex.flex-row.items-end.flex-wrap.hidden.bg-surface.backdrop-blur-sm.rounded.p-2.shadow-lg.border.border-base.overflow-hidden data-theme-target="panel"
|
|
8
|
+
|
|
9
|
+
- themes.each do |name|
|
|
10
|
+
|
|
11
|
+
button.relative.flex-center.w-8.aspect-square.bg-primary/85.cursor-pointer class="theme-#{ name }" type="button" data-action="mouseenter->theme#preview mouseleave->theme#cancelPreview theme#choose" data-theme-name=name
|
|
12
|
+
|
|
13
|
+
button.flex-center.justify-center.w-8.aspect-square.border.border-base.rounded.btn-primary.text-sm.cursor-pointer.text-yellow-500.ml-2 class="text-shadow-[0px_0px_3px_black]" type="button" data-action="theme#toggleMode" data-theme-target="mode" data-icon=icons[:moon].to_s.gsub('"', "'")
|
|
14
|
+
|
|
15
|
+
= icons[:sun]
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/*
|
|
2
|
+
TailwindThemePicker default themes. Each rule maps one `.theme-*` selector to a
|
|
3
|
+
primary color CSS custom property. Host apps consume this via Tailwind's
|
|
4
|
+
color system (e.g. `bg-primary` reads `var(--color-primary)`).
|
|
5
|
+
|
|
6
|
+
These rules are intentionally UNLAYERED. Hosts typically define their default
|
|
7
|
+
`--color-primary` inside `@layer base { :root { ... } }`. Unlayered rules win
|
|
8
|
+
the cascade over any @layer rule of equal specificity, so `.theme-red` always
|
|
9
|
+
overrides `:root` regardless of where the host @imports this file.
|
|
10
|
+
|
|
11
|
+
To add or override a theme, define another `.theme-<name>` rule (also
|
|
12
|
+
unlayered) AFTER this import in your stylesheet, and pass the name to
|
|
13
|
+
TailwindThemePicker.configure so it appears in the picker UI.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
.theme-red { --color-primary: var(--color-red-500); }
|
|
17
|
+
.theme-orange { --color-primary: var(--color-orange-500); }
|
|
18
|
+
.theme-amber { --color-primary: var(--color-amber-500); }
|
|
19
|
+
.theme-yellow { --color-primary: var(--color-yellow-500); }
|
|
20
|
+
.theme-lime { --color-primary: var(--color-lime-500); }
|
|
21
|
+
.theme-green { --color-primary: var(--color-green-500); }
|
|
22
|
+
.theme-emerald { --color-primary: var(--color-emerald-500); }
|
|
23
|
+
.theme-teal { --color-primary: var(--color-teal-500); }
|
|
24
|
+
.theme-cyan { --color-primary: var(--color-cyan-500); }
|
|
25
|
+
.theme-sky { --color-primary: var(--color-sky-500); }
|
|
26
|
+
.theme-blue { --color-primary: var(--color-blue-500); }
|
|
27
|
+
.theme-indigo { --color-primary: var(--color-indigo-500); }
|
|
28
|
+
.theme-violet { --color-primary: var(--color-violet-500); }
|
|
29
|
+
.theme-purple { --color-primary: var(--color-purple-500); }
|
|
30
|
+
.theme-fuchsia { --color-primary: var(--color-fuchsia-500); }
|
|
31
|
+
.theme-pink { --color-primary: var(--color-pink-500); }
|
|
32
|
+
.theme-rose { --color-primary: var(--color-rose-500); }
|
|
33
|
+
.theme-slate { --color-primary: var(--color-slate-400); }
|
|
34
|
+
.theme-gray { --color-primary: var(--color-gray-400); }
|
|
35
|
+
.theme-zinc { --color-primary: var(--color-zinc-400); }
|
|
36
|
+
.theme-neutral { --color-primary: var(--color-neutral-400); }
|
|
37
|
+
.theme-stone { --color-primary: var(--color-stone-400); }
|
|
38
|
+
.theme-taupe { --color-primary: var(--color-taupe-400); }
|
|
39
|
+
.theme-mauve { --color-primary: var(--color-mauve-400); }
|
|
40
|
+
.theme-mist { --color-primary: var(--color-mist-400); }
|
|
41
|
+
.theme-olive { --color-primary: var(--color-olive-400); }
|
data/config/importmap.rb
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
pin "controllers/theme_controller", to: "tailwind_theme_picker/controllers/theme_controller.js"
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
require "rails/generators/base"
|
|
2
|
+
|
|
3
|
+
module TailwindThemePicker
|
|
4
|
+
|
|
5
|
+
module Generators
|
|
6
|
+
|
|
7
|
+
class InstallGenerator < Rails::Generators::Base
|
|
8
|
+
|
|
9
|
+
source_root File.expand_path("../../../..", __dir__)
|
|
10
|
+
|
|
11
|
+
desc "Copies tailwind_theme_picker themes.css into app/assets/tailwind/tailwind_theme_picker/ and prints layout wiring instructions."
|
|
12
|
+
|
|
13
|
+
class_option :initializer, type: :boolean, default: false,
|
|
14
|
+
desc: "Also create config/initializers/tailwind_theme_picker.rb"
|
|
15
|
+
|
|
16
|
+
def copy_themes_stylesheet
|
|
17
|
+
copy_file "assets/stylesheets/tailwind_theme_picker/themes.css",
|
|
18
|
+
"app/assets/tailwind/tailwind_theme_picker/themes.css"
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def create_initializer
|
|
22
|
+
return unless options[:initializer]
|
|
23
|
+
create_file "config/initializers/tailwind_theme_picker.rb", <<~RUBY
|
|
24
|
+
TailwindThemePicker.configure do |c|
|
|
25
|
+
# c.themes = %w[red blue green] # subset / extend default 27
|
|
26
|
+
# c.default = "sky"
|
|
27
|
+
# c.theme_cookie = "theme"
|
|
28
|
+
# c.mode_cookie = "mode"
|
|
29
|
+
# c.cookie_max_age = 60 * 60 * 24 * 365
|
|
30
|
+
end
|
|
31
|
+
RUBY
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def show_post_install
|
|
35
|
+
say "\nTailwindThemePicker installed.", :green
|
|
36
|
+
say ""
|
|
37
|
+
say "Next steps:"
|
|
38
|
+
say " 1. Add to your Tailwind input CSS (e.g. app/assets/tailwind/application.css):"
|
|
39
|
+
say " @import './tailwind_theme_picker/themes';", :cyan
|
|
40
|
+
say " 2. Wire up your layout <html> tag and body:"
|
|
41
|
+
say " html lang=\"en\" *theme_picker_html_attrs", :cyan
|
|
42
|
+
say " = theme_picker_fouc_script # in <head>"
|
|
43
|
+
say " = render_theme_picker # in <body>"
|
|
44
|
+
say ""
|
|
45
|
+
say "After upgrading the gem, re-run this generator to pick up any new themes."
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
end
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
module TailwindThemePicker
|
|
2
|
+
|
|
3
|
+
class Configuration
|
|
4
|
+
|
|
5
|
+
attr_accessor :themes, :default, :theme_cookie, :mode_cookie, :cookie_max_age
|
|
6
|
+
|
|
7
|
+
DEFAULT_THEMES = %w[
|
|
8
|
+
red orange amber yellow lime green emerald teal cyan sky blue
|
|
9
|
+
indigo violet purple fuchsia pink rose
|
|
10
|
+
slate gray zinc neutral stone taupe mauve mist olive
|
|
11
|
+
].freeze
|
|
12
|
+
|
|
13
|
+
def initialize
|
|
14
|
+
@themes = DEFAULT_THEMES.dup
|
|
15
|
+
@default = "sky"
|
|
16
|
+
@theme_cookie = "theme"
|
|
17
|
+
@mode_cookie = "mode"
|
|
18
|
+
@cookie_max_age = 60 * 60 * 24 * 365
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
end
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
module TailwindThemePicker
|
|
2
|
+
|
|
3
|
+
class Engine < ::Rails::Engine
|
|
4
|
+
|
|
5
|
+
initializer "tailwind_theme_picker.helpers" do
|
|
6
|
+
ActiveSupport.on_load(:action_view) do
|
|
7
|
+
include TailwindThemePicker::ViewHelper
|
|
8
|
+
end
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
# 1. Serve the Stimulus controller as a propshaft asset.
|
|
12
|
+
# 2. Append our importmap.rb so the host doesn't need to pin manually.
|
|
13
|
+
initializer "tailwind_theme_picker.importmap", before: "importmap" do |app|
|
|
14
|
+
if app.config.respond_to?(:importmap)
|
|
15
|
+
app.config.importmap.paths << root.join("config/importmap.rb")
|
|
16
|
+
app.config.importmap.cache_sweepers << root.join("app/javascript")
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
initializer "tailwind_theme_picker.assets" do |app|
|
|
21
|
+
if app.config.respond_to?(:assets)
|
|
22
|
+
app.config.assets.paths << root.join("app/javascript")
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
end
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
require_relative "tailwind_theme_picker/version"
|
|
2
|
+
require_relative "tailwind_theme_picker/configuration"
|
|
3
|
+
require_relative "tailwind_theme_picker/engine" if defined?(Rails)
|
|
4
|
+
|
|
5
|
+
module TailwindThemePicker
|
|
6
|
+
|
|
7
|
+
class << self
|
|
8
|
+
|
|
9
|
+
attr_writer :configuration
|
|
10
|
+
|
|
11
|
+
def configuration
|
|
12
|
+
@configuration ||= Configuration.new
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def configure
|
|
16
|
+
yield(configuration)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def reset_configuration!
|
|
20
|
+
@configuration = Configuration.new
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: tailwind_theme_picker
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- 16554289+optimuspwnius@users.noreply.github.com
|
|
8
|
+
bindir: bin
|
|
9
|
+
cert_chain: []
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
|
+
dependencies:
|
|
12
|
+
- !ruby/object:Gem::Dependency
|
|
13
|
+
name: railties
|
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
|
15
|
+
requirements:
|
|
16
|
+
- - ">="
|
|
17
|
+
- !ruby/object:Gem::Version
|
|
18
|
+
version: '7.0'
|
|
19
|
+
type: :runtime
|
|
20
|
+
prerelease: false
|
|
21
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
22
|
+
requirements:
|
|
23
|
+
- - ">="
|
|
24
|
+
- !ruby/object:Gem::Version
|
|
25
|
+
version: '7.0'
|
|
26
|
+
- !ruby/object:Gem::Dependency
|
|
27
|
+
name: actionview
|
|
28
|
+
requirement: !ruby/object:Gem::Requirement
|
|
29
|
+
requirements:
|
|
30
|
+
- - ">="
|
|
31
|
+
- !ruby/object:Gem::Version
|
|
32
|
+
version: '7.0'
|
|
33
|
+
type: :runtime
|
|
34
|
+
prerelease: false
|
|
35
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
36
|
+
requirements:
|
|
37
|
+
- - ">="
|
|
38
|
+
- !ruby/object:Gem::Version
|
|
39
|
+
version: '7.0'
|
|
40
|
+
description: Floating theme picker for Tailwind-based Rails apps. Ships a Stimulus
|
|
41
|
+
controller, Slim partial, helpers, and a themes stylesheet. Persists choice in two
|
|
42
|
+
cookies (theme, mode) so the server can paint the right classes on <html> before
|
|
43
|
+
any JS runs — no flash of unstyled content on returning visits.
|
|
44
|
+
email:
|
|
45
|
+
- 16554289+optimuspwnius@users.noreply.github.com
|
|
46
|
+
executables: []
|
|
47
|
+
extensions: []
|
|
48
|
+
extra_rdoc_files: []
|
|
49
|
+
files:
|
|
50
|
+
- LICENSE.txt
|
|
51
|
+
- README.md
|
|
52
|
+
- app/helpers/tailwind_theme_picker/view_helper.rb
|
|
53
|
+
- app/javascript/tailwind_theme_picker/controllers/theme_controller.js
|
|
54
|
+
- app/views/tailwind_theme_picker/_picker.html.slim
|
|
55
|
+
- assets/stylesheets/tailwind_theme_picker/themes.css
|
|
56
|
+
- config/importmap.rb
|
|
57
|
+
- lib/generators/tailwind_theme_picker/install/install_generator.rb
|
|
58
|
+
- lib/tailwind_theme_picker.rb
|
|
59
|
+
- lib/tailwind_theme_picker/configuration.rb
|
|
60
|
+
- lib/tailwind_theme_picker/engine.rb
|
|
61
|
+
- lib/tailwind_theme_picker/version.rb
|
|
62
|
+
homepage: https://github.com/optimuspwnius/tailwind-theme-picker
|
|
63
|
+
licenses:
|
|
64
|
+
- MIT
|
|
65
|
+
metadata:
|
|
66
|
+
homepage_uri: https://github.com/optimuspwnius/tailwind-theme-picker
|
|
67
|
+
source_code_uri: https://github.com/optimuspwnius/tailwind-theme-picker
|
|
68
|
+
changelog_uri: https://github.com/optimuspwnius/tailwind-theme-picker/blob/main/CHANGELOG.md
|
|
69
|
+
allowed_push_host: https://rubygems.org
|
|
70
|
+
rdoc_options: []
|
|
71
|
+
require_paths:
|
|
72
|
+
- lib
|
|
73
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
74
|
+
requirements:
|
|
75
|
+
- - ">="
|
|
76
|
+
- !ruby/object:Gem::Version
|
|
77
|
+
version: '3.2'
|
|
78
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
79
|
+
requirements:
|
|
80
|
+
- - ">="
|
|
81
|
+
- !ruby/object:Gem::Version
|
|
82
|
+
version: '0'
|
|
83
|
+
requirements: []
|
|
84
|
+
rubygems_version: 4.0.7
|
|
85
|
+
specification_version: 4
|
|
86
|
+
summary: Drop-in theme + light/dark picker for Rails apps.
|
|
87
|
+
test_files: []
|