kaze 1.3.1 → 1.4.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/CHANGELOG.md +20 -1
- data/lib/kaze/version.rb +1 -1
- data/stubs/hotwire/app/assets/tailwind/application.css +5 -0
- data/stubs/hotwire/app/views/layouts/_appearance.html.erb +18 -0
- data/stubs/hotwire/app/views/layouts/application.html.erb +2 -1
- data/stubs/hotwire/app/views/layouts/guest.html.erb +1 -0
- data/stubs/hotwire/app/views/profile/edit.html.erb +5 -0
- data/stubs/hotwire/app/views/profile/partials/_update_appearance_form.html.erb +39 -0
- data/stubs/hotwire/app/views/welcome/index.html.erb +1 -0
- data/stubs/inertia-react-ts/app/assets/tailwind/application.css +5 -0
- data/stubs/inertia-react-ts/app/javascript/Layouts/AuthenticatedLayout.tsx +8 -8
- data/stubs/inertia-react-ts/app/javascript/Layouts/GuestLayout.tsx +1 -1
- data/stubs/inertia-react-ts/app/javascript/Pages/Profile/Edit.tsx +5 -0
- data/stubs/inertia-react-ts/app/javascript/Pages/Profile/Partials/UpdateAppearanceForm.tsx +47 -0
- data/stubs/inertia-react-ts/app/javascript/lib/appearance.ts +23 -0
- data/stubs/inertia-react-ts/app/views/layouts/application.html.erb +19 -1
- data/stubs/inertia-vue-ts/app/assets/tailwind/application.css +5 -0
- data/stubs/inertia-vue-ts/app/javascript/Pages/Profile/Edit.vue +5 -0
- data/stubs/inertia-vue-ts/app/javascript/Pages/Profile/Partials/UpdateAppearanceForm.vue +47 -0
- data/stubs/inertia-vue-ts/app/javascript/lib/appearance.ts +23 -0
- data/stubs/inertia-vue-ts/app/views/layouts/application.html.erb +19 -1
- metadata +7 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: cc7bad365960dce77e78e930ea414a8bdcf3d03887393200bfde19a7c0299fe4
|
|
4
|
+
data.tar.gz: 7f3a5dffd4210452b1a3da62604e1d4b060c9659b9d596c10778db420b5fc034
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: cc6441a4bae8a2e94d4662a00d2c4dc302cfe9b425b15f112f7c9fdb1d0cf218d860fdcf503bf2d5f4bb92e66b0bf1b7cccf517b663606a2535a48a8a53d0853
|
|
7
|
+
data.tar.gz: ed22d4a753d6a7acdc48d59ca3ef0c76838129a467811ca52c608db891e9c9ecde792ebb37765bed82a635837ce51498dca10607da8a3ddf76374aef7b4bf986
|
data/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,24 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
|
6
6
|
|
|
7
7
|
## [Unreleased]
|
|
8
8
|
|
|
9
|
+
## [1.4.0] - 2026-10-21
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
|
|
13
|
+
- An appearance preference — light, dark or system — on the profile page of all three stacks ([#26](https://github.com/cuonggt/kaze/pull/26)). The stubs already carried 315 `dark:` classes, but nothing could reach them: `dark:` followed `prefers-color-scheme`, so an application took whatever the operating system said. The choice is stored in `localStorage`, and system follows the device and tracks it live.
|
|
14
|
+
|
|
15
|
+
Each stylesheet now redefines the variant, since Tailwind v4 no longer configures dark mode in JavaScript:
|
|
16
|
+
|
|
17
|
+
```css
|
|
18
|
+
@custom-variant dark (&:where(.dark, .dark *));
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
The layouts apply the resulting `dark` class in an inline `<head>` script before first paint, so the page never flashes the wrong theme. A consequence worth knowing: the theme now depends on JavaScript, so with scripting disabled an application always renders light where it previously followed the operating system.
|
|
22
|
+
|
|
23
|
+
### Fixed
|
|
24
|
+
|
|
25
|
+
- Dark mode gaps that were already broken for anyone whose operating system was set to dark, and which the preference control made obvious. React's `AuthenticatedLayout` carried one `dark:` class where Vue's had fifteen, leaving a white navigation bar and header above a dark page; the missing variants are ported from the Vue layout. Four wrappers lacked `dark:bg-gray-900`, leaving a light grey page behind dark cards — hotwire's application layout, React's `AuthenticatedLayout` and `GuestLayout`, and the Inertia ERB wrappers for both stacks.
|
|
26
|
+
|
|
9
27
|
## [1.3.1] - 2026-10-21
|
|
10
28
|
|
|
11
29
|
### Changed
|
|
@@ -108,7 +126,8 @@ Together these take a freshly scaffolded Inertia application from seven npm advi
|
|
|
108
126
|
|
|
109
127
|
Releases before 1.0.0 are not catalogued here. See the [tags](https://github.com/cuonggt/kaze/tags) for their history.
|
|
110
128
|
|
|
111
|
-
[Unreleased]: https://github.com/cuonggt/kaze/compare/v1.
|
|
129
|
+
[Unreleased]: https://github.com/cuonggt/kaze/compare/v1.4.0...HEAD
|
|
130
|
+
[1.4.0]: https://github.com/cuonggt/kaze/compare/v1.3.1...v1.4.0
|
|
112
131
|
[1.3.1]: https://github.com/cuonggt/kaze/compare/v1.3.0...v1.3.1
|
|
113
132
|
[1.3.0]: https://github.com/cuonggt/kaze/compare/v1.2.1...v1.3.0
|
|
114
133
|
[1.2.1]: https://github.com/cuonggt/kaze/compare/v1.2.0...v1.2.1
|
data/lib/kaze/version.rb
CHANGED
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
@import "tailwindcss";
|
|
2
2
|
|
|
3
|
+
/* Drive `dark:` from a `dark` class rather than prefers-color-scheme, so the
|
|
4
|
+
appearance preference can override the operating system. The layouts apply
|
|
5
|
+
the class before first paint. */
|
|
6
|
+
@custom-variant dark (&:where(.dark, .dark *));
|
|
7
|
+
|
|
3
8
|
@plugin "@tailwindcss/forms";
|
|
4
9
|
|
|
5
10
|
@source "../../../public/*.html";
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
<%# Applies the stored appearance before first paint, so the page never flashes
|
|
2
|
+
the wrong theme. Also exposed as window.applyAppearance so the preference
|
|
3
|
+
control can re-apply it after a change. %>
|
|
4
|
+
<%= javascript_tag nonce: true do %>
|
|
5
|
+
window.applyAppearance = function () {
|
|
6
|
+
var preference = localStorage.getItem('appearance') || 'system'
|
|
7
|
+
var dark = preference === 'dark' ||
|
|
8
|
+
(preference === 'system' && window.matchMedia('(prefers-color-scheme: dark)').matches)
|
|
9
|
+
|
|
10
|
+
document.documentElement.classList.toggle('dark', dark)
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
window.applyAppearance()
|
|
14
|
+
|
|
15
|
+
window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', function () {
|
|
16
|
+
window.applyAppearance()
|
|
17
|
+
})
|
|
18
|
+
<% end %>
|
|
@@ -8,12 +8,13 @@
|
|
|
8
8
|
<%= csp_meta_tag %>
|
|
9
9
|
<%= stylesheet_link_tag "tailwind", "data-turbo-track": Rails.env.production? ? "reload" : "" %>
|
|
10
10
|
<%= stylesheet_link_tag "application", "data-turbo-track": Rails.env.production? ? "reload" : "" %>
|
|
11
|
+
<%= render "layouts/appearance" %>
|
|
11
12
|
<%= javascript_importmap_tags %>
|
|
12
13
|
<%= turbo_refreshes_with method: :morph, scroll: :preserve %>
|
|
13
14
|
<%= yield :head %>
|
|
14
15
|
</head>
|
|
15
16
|
<body class="font-sans antialiased">
|
|
16
|
-
<div class="min-h-screen bg-gray-100">
|
|
17
|
+
<div class="min-h-screen bg-gray-100 dark:bg-gray-900">
|
|
17
18
|
<%= render "layouts/navigation" %>
|
|
18
19
|
<!-- Page Heading -->
|
|
19
20
|
<% if content_for?(:header) %>
|
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
<%= csp_meta_tag %>
|
|
9
9
|
<%= stylesheet_link_tag "tailwind", "data-turbo-track": Rails.env.production? ? "reload" : "" %>
|
|
10
10
|
<%= stylesheet_link_tag "application", "data-turbo-track": Rails.env.production? ? "reload" : "" %>
|
|
11
|
+
<%= render "layouts/appearance" %>
|
|
11
12
|
<%= javascript_importmap_tags %>
|
|
12
13
|
<%= turbo_refreshes_with method: :morph, scroll: :preserve %>
|
|
13
14
|
</head>
|
|
@@ -5,6 +5,11 @@
|
|
|
5
5
|
<% end %>
|
|
6
6
|
<div class="py-12">
|
|
7
7
|
<div class="max-w-7xl mx-auto sm:px-6 lg:px-8 space-y-6">
|
|
8
|
+
<div class="p-4 sm:p-8 bg-white dark:bg-gray-800 shadow-sm sm:rounded-lg">
|
|
9
|
+
<div class="max-w-xl">
|
|
10
|
+
<%= render "profile/partials/update_appearance_form" %>
|
|
11
|
+
</div>
|
|
12
|
+
</div>
|
|
8
13
|
<div class="p-4 sm:p-8 bg-white dark:bg-gray-800 shadow-sm sm:rounded-lg">
|
|
9
14
|
<div class="max-w-xl">
|
|
10
15
|
<%= render "profile/partials/update_profile_information_form" %>
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
<section
|
|
2
|
+
x-data="{
|
|
3
|
+
preference: localStorage.getItem('appearance') || 'system',
|
|
4
|
+
select(value) {
|
|
5
|
+
this.preference = value
|
|
6
|
+
localStorage.setItem('appearance', value)
|
|
7
|
+
window.applyAppearance()
|
|
8
|
+
}
|
|
9
|
+
}"
|
|
10
|
+
>
|
|
11
|
+
<header>
|
|
12
|
+
<h2 class="text-lg font-medium text-gray-900 dark:text-gray-100">
|
|
13
|
+
Appearance
|
|
14
|
+
</h2>
|
|
15
|
+
|
|
16
|
+
<p class="mt-1 text-sm text-gray-600 dark:text-gray-400">
|
|
17
|
+
Choose how the application looks to you. System follows your device setting.
|
|
18
|
+
</p>
|
|
19
|
+
</header>
|
|
20
|
+
|
|
21
|
+
<div class="mt-6 inline-flex rounded-lg bg-gray-100 p-1 dark:bg-gray-900">
|
|
22
|
+
<template x-for="option in [
|
|
23
|
+
{ value: 'light', label: 'Light' },
|
|
24
|
+
{ value: 'dark', label: 'Dark' },
|
|
25
|
+
{ value: 'system', label: 'System' }
|
|
26
|
+
]" :key="option.value">
|
|
27
|
+
<button
|
|
28
|
+
type="button"
|
|
29
|
+
@click="select(option.value)"
|
|
30
|
+
:aria-pressed="preference === option.value"
|
|
31
|
+
class="rounded-md px-4 py-1.5 text-sm font-medium transition focus:outline-hidden focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2 dark:focus:ring-offset-gray-800"
|
|
32
|
+
:class="preference === option.value
|
|
33
|
+
? 'bg-white text-gray-900 shadow-xs dark:bg-gray-700 dark:text-gray-100'
|
|
34
|
+
: 'text-gray-500 hover:text-gray-900 dark:text-gray-400 dark:hover:text-gray-100'"
|
|
35
|
+
x-text="option.label"
|
|
36
|
+
></button>
|
|
37
|
+
</template>
|
|
38
|
+
</div>
|
|
39
|
+
</section>
|
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
<%= csp_meta_tag %>
|
|
9
9
|
<%= stylesheet_link_tag "tailwind", "data-turbo-track": Rails.env.production? ? "reload" : "" %>
|
|
10
10
|
<%= stylesheet_link_tag "application", "data-turbo-track": Rails.env.production? ? "reload" : "" %>
|
|
11
|
+
<%= render "layouts/appearance" %>
|
|
11
12
|
<%= javascript_importmap_tags %>
|
|
12
13
|
<%= turbo_refreshes_with method: :morph, scroll: :preserve %>
|
|
13
14
|
</head>
|
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
@import "tailwindcss";
|
|
2
2
|
|
|
3
|
+
/* Drive `dark:` from a `dark` class rather than prefers-color-scheme, so the
|
|
4
|
+
appearance preference can override the operating system. The layouts apply
|
|
5
|
+
the class before first paint. */
|
|
6
|
+
@custom-variant dark (&:where(.dark, .dark *));
|
|
7
|
+
|
|
3
8
|
@plugin "@tailwindcss/forms";
|
|
4
9
|
|
|
5
10
|
@source "../../../public/*.html";
|
|
@@ -14,14 +14,14 @@ export default function AuthenticatedLayout({ header, children }: PropsWithChild
|
|
|
14
14
|
const { pathname = '' } = typeof window !== 'undefined' ? window.location : {}
|
|
15
15
|
|
|
16
16
|
return (
|
|
17
|
-
<div className="min-h-screen bg-gray-100">
|
|
18
|
-
<nav className="bg-white border-b border-gray-100">
|
|
17
|
+
<div className="min-h-screen bg-gray-100 dark:bg-gray-900">
|
|
18
|
+
<nav className="bg-white dark:bg-gray-800 border-b border-gray-100 dark:border-gray-700">
|
|
19
19
|
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
|
20
20
|
<div className="flex justify-between h-16">
|
|
21
21
|
<div className="flex">
|
|
22
22
|
<div className="shrink-0 flex items-center">
|
|
23
23
|
<Link href="/">
|
|
24
|
-
<ApplicationLogo className="block h-9 w-auto fill-current text-red-800" />
|
|
24
|
+
<ApplicationLogo className="block h-9 w-auto fill-current text-red-800 dark:text-red-200" />
|
|
25
25
|
</Link>
|
|
26
26
|
</div>
|
|
27
27
|
|
|
@@ -39,7 +39,7 @@ export default function AuthenticatedLayout({ header, children }: PropsWithChild
|
|
|
39
39
|
<span className="inline-flex rounded-md">
|
|
40
40
|
<button
|
|
41
41
|
type="button"
|
|
42
|
-
className="inline-flex items-center px-3 py-2 border border-transparent text-sm leading-4 font-medium rounded-md text-gray-500 bg-white hover:text-gray-700 focus:outline-hidden transition ease-in-out duration-150"
|
|
42
|
+
className="inline-flex items-center px-3 py-2 border border-transparent text-sm leading-4 font-medium rounded-md text-gray-500 dark:text-gray-400 bg-white dark:bg-gray-800 hover:text-gray-700 dark:hover:text-gray-300 focus:outline-hidden transition ease-in-out duration-150"
|
|
43
43
|
>
|
|
44
44
|
{user.name}
|
|
45
45
|
|
|
@@ -72,7 +72,7 @@ export default function AuthenticatedLayout({ header, children }: PropsWithChild
|
|
|
72
72
|
<div className="-me-2 flex items-center sm:hidden">
|
|
73
73
|
<button
|
|
74
74
|
onClick={() => setShowingNavigationDropdown((previousState) => !previousState)}
|
|
75
|
-
className="inline-flex items-center justify-center p-2 rounded-md text-gray-400 hover:text-gray-500 hover:bg-gray-100 focus:outline-hidden focus:bg-gray-100 focus:text-gray-500 transition duration-150 ease-in-out"
|
|
75
|
+
className="inline-flex items-center justify-center p-2 rounded-md text-gray-400 dark:text-gray-500 hover:text-gray-500 dark:hover:text-gray-400 hover:bg-gray-100 dark:hover:bg-gray-900 focus:outline-hidden focus:bg-gray-100 dark:focus:bg-gray-900 focus:text-gray-500 dark:focus:text-gray-400 transition duration-150 ease-in-out"
|
|
76
76
|
>
|
|
77
77
|
<svg className="h-6 w-6" stroke="currentColor" fill="none" viewBox="0 0 24 24">
|
|
78
78
|
<path
|
|
@@ -102,9 +102,9 @@ export default function AuthenticatedLayout({ header, children }: PropsWithChild
|
|
|
102
102
|
</ResponsiveNavLink>
|
|
103
103
|
</div>
|
|
104
104
|
|
|
105
|
-
<div className="pt-4 pb-1 border-t border-gray-200">
|
|
105
|
+
<div className="pt-4 pb-1 border-t border-gray-200 dark:border-gray-600">
|
|
106
106
|
<div className="px-4">
|
|
107
|
-
<div className="font-medium text-base text-gray-800">{user.name}</div>
|
|
107
|
+
<div className="font-medium text-base text-gray-800 dark:text-gray-200">{user.name}</div>
|
|
108
108
|
<div className="font-medium text-sm text-gray-500">{user.email}</div>
|
|
109
109
|
</div>
|
|
110
110
|
|
|
@@ -119,7 +119,7 @@ export default function AuthenticatedLayout({ header, children }: PropsWithChild
|
|
|
119
119
|
</nav>
|
|
120
120
|
|
|
121
121
|
{header && (
|
|
122
|
-
<header className="bg-white shadow-sm">
|
|
122
|
+
<header className="bg-white dark:bg-gray-800 shadow-sm">
|
|
123
123
|
<div className="max-w-7xl mx-auto py-6 px-4 sm:px-6 lg:px-8">{header}</div>
|
|
124
124
|
</header>
|
|
125
125
|
)}
|
|
@@ -4,7 +4,7 @@ import { PropsWithChildren } from 'react'
|
|
|
4
4
|
|
|
5
5
|
export default function GuestLayout({ children }: PropsWithChildren) {
|
|
6
6
|
return (
|
|
7
|
-
<div className="min-h-screen flex flex-col sm:justify-center items-center pt-6 sm:pt-0 bg-gray-100">
|
|
7
|
+
<div className="min-h-screen flex flex-col sm:justify-center items-center pt-6 sm:pt-0 bg-gray-100 dark:bg-gray-900">
|
|
8
8
|
<div>
|
|
9
9
|
<Link href="/">
|
|
10
10
|
<ApplicationLogo className="w-20 h-20 fill-current text-red-500" />
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import AuthenticatedLayout from '@/Layouts/AuthenticatedLayout'
|
|
2
2
|
import DeleteUserForm from './Partials/DeleteUserForm'
|
|
3
|
+
import UpdateAppearanceForm from './Partials/UpdateAppearanceForm'
|
|
3
4
|
import UpdatePasswordForm from './Partials/UpdatePasswordForm'
|
|
4
5
|
import UpdateProfileInformationForm from './Partials/UpdateProfileInformationForm'
|
|
5
6
|
import { Head } from '@inertiajs/react'
|
|
@@ -14,6 +15,10 @@ export default function Edit({ mustVerifyEmail, status }: PageProps<{ mustVerify
|
|
|
14
15
|
|
|
15
16
|
<div className="py-12">
|
|
16
17
|
<div className="max-w-7xl mx-auto sm:px-6 lg:px-8 space-y-6">
|
|
18
|
+
<div className="p-4 sm:p-8 bg-white dark:bg-gray-800 shadow-sm sm:rounded-lg">
|
|
19
|
+
<UpdateAppearanceForm className="max-w-xl" />
|
|
20
|
+
</div>
|
|
21
|
+
|
|
17
22
|
<div className="p-4 sm:p-8 bg-white dark:bg-gray-800 shadow-sm sm:rounded-lg">
|
|
18
23
|
<UpdateProfileInformationForm mustVerifyEmail={mustVerifyEmail} status={status} className="max-w-xl" />
|
|
19
24
|
</div>
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { useState } from 'react'
|
|
2
|
+
import { Appearance, getAppearance, setAppearance } from '@/lib/appearance'
|
|
3
|
+
|
|
4
|
+
const options: { value: Appearance; label: string }[] = [
|
|
5
|
+
{ value: 'light', label: 'Light' },
|
|
6
|
+
{ value: 'dark', label: 'Dark' },
|
|
7
|
+
{ value: 'system', label: 'System' },
|
|
8
|
+
]
|
|
9
|
+
|
|
10
|
+
export default function UpdateAppearanceForm({ className = '' }: { className?: string }) {
|
|
11
|
+
const [appearance, setCurrentAppearance] = useState<Appearance>(getAppearance)
|
|
12
|
+
|
|
13
|
+
const select = (value: Appearance) => {
|
|
14
|
+
setCurrentAppearance(value)
|
|
15
|
+
setAppearance(value)
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
return (
|
|
19
|
+
<section className={className}>
|
|
20
|
+
<header>
|
|
21
|
+
<h2 className="text-lg font-medium text-gray-900 dark:text-gray-100">Appearance</h2>
|
|
22
|
+
|
|
23
|
+
<p className="mt-1 text-sm text-gray-600 dark:text-gray-400">
|
|
24
|
+
Choose how the application looks to you. System follows your device setting.
|
|
25
|
+
</p>
|
|
26
|
+
</header>
|
|
27
|
+
|
|
28
|
+
<div className="mt-6 inline-flex rounded-lg bg-gray-100 p-1 dark:bg-gray-900">
|
|
29
|
+
{options.map((option) => (
|
|
30
|
+
<button
|
|
31
|
+
key={option.value}
|
|
32
|
+
type="button"
|
|
33
|
+
onClick={() => select(option.value)}
|
|
34
|
+
aria-pressed={appearance === option.value}
|
|
35
|
+
className={`rounded-md px-4 py-1.5 text-sm font-medium transition focus:outline-hidden focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2 dark:focus:ring-offset-gray-800 ${
|
|
36
|
+
appearance === option.value
|
|
37
|
+
? 'bg-white text-gray-900 shadow-xs dark:bg-gray-700 dark:text-gray-100'
|
|
38
|
+
: 'text-gray-500 hover:text-gray-900 dark:text-gray-400 dark:hover:text-gray-100'
|
|
39
|
+
}`}
|
|
40
|
+
>
|
|
41
|
+
{option.label}
|
|
42
|
+
</button>
|
|
43
|
+
))}
|
|
44
|
+
</div>
|
|
45
|
+
</section>
|
|
46
|
+
)
|
|
47
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
export type Appearance = 'light' | 'dark' | 'system'
|
|
2
|
+
|
|
3
|
+
const STORAGE_KEY = 'appearance'
|
|
4
|
+
|
|
5
|
+
export function getAppearance(): Appearance {
|
|
6
|
+
const stored = localStorage.getItem(STORAGE_KEY)
|
|
7
|
+
|
|
8
|
+
return stored === 'light' || stored === 'dark' ? stored : 'system'
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function applyAppearance(appearance: Appearance = getAppearance()): void {
|
|
12
|
+
const dark =
|
|
13
|
+
appearance === 'dark' ||
|
|
14
|
+
(appearance === 'system' && window.matchMedia('(prefers-color-scheme: dark)').matches)
|
|
15
|
+
|
|
16
|
+
document.documentElement.classList.toggle('dark', dark)
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function setAppearance(appearance: Appearance): void {
|
|
20
|
+
localStorage.setItem(STORAGE_KEY, appearance)
|
|
21
|
+
|
|
22
|
+
applyAppearance(appearance)
|
|
23
|
+
}
|
|
@@ -6,12 +6,30 @@
|
|
|
6
6
|
<title><%= content_for(:title) || "Rails" %></title>
|
|
7
7
|
<%= csrf_meta_tags %>
|
|
8
8
|
<%= csp_meta_tag %>
|
|
9
|
+
<%# Apply the stored appearance before first paint, so the page never flashes the wrong theme. %>
|
|
10
|
+
<%= javascript_tag nonce: true do %>
|
|
11
|
+
(function () {
|
|
12
|
+
var preference = localStorage.getItem('appearance') || 'system'
|
|
13
|
+
var query = window.matchMedia('(prefers-color-scheme: dark)')
|
|
14
|
+
|
|
15
|
+
document.documentElement.classList.toggle(
|
|
16
|
+
'dark',
|
|
17
|
+
preference === 'dark' || (preference === 'system' && query.matches)
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
query.addEventListener('change', function () {
|
|
21
|
+
if ((localStorage.getItem('appearance') || 'system') === 'system') {
|
|
22
|
+
document.documentElement.classList.toggle('dark', query.matches)
|
|
23
|
+
}
|
|
24
|
+
})
|
|
25
|
+
})()
|
|
26
|
+
<% end %>
|
|
9
27
|
<%= vite_client_tag %>
|
|
10
28
|
<%= vite_react_refresh_tag %>
|
|
11
29
|
<%= vite_typescript_tag 'application.tsx', defer: true %>
|
|
12
30
|
</head>
|
|
13
31
|
<body class="font-sans antialiased">
|
|
14
|
-
<div class="min-h-screen bg-gray-100">
|
|
32
|
+
<div class="min-h-screen bg-gray-100 dark:bg-gray-900">
|
|
15
33
|
<%= yield %>
|
|
16
34
|
</div>
|
|
17
35
|
</body>
|
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
@import "tailwindcss";
|
|
2
2
|
|
|
3
|
+
/* Drive `dark:` from a `dark` class rather than prefers-color-scheme, so the
|
|
4
|
+
appearance preference can override the operating system. The layouts apply
|
|
5
|
+
the class before first paint. */
|
|
6
|
+
@custom-variant dark (&:where(.dark, .dark *));
|
|
7
|
+
|
|
3
8
|
@plugin "@tailwindcss/forms";
|
|
4
9
|
|
|
5
10
|
@source "../../../public/*.html";
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
2
|
import AuthenticatedLayout from '@/Layouts/AuthenticatedLayout.vue'
|
|
3
3
|
import DeleteUserForm from './Partials/DeleteUserForm.vue'
|
|
4
|
+
import UpdateAppearanceForm from './Partials/UpdateAppearanceForm.vue'
|
|
4
5
|
import UpdatePasswordForm from './Partials/UpdatePasswordForm.vue'
|
|
5
6
|
import UpdateProfileInformationForm from './Partials/UpdateProfileInformationForm.vue'
|
|
6
7
|
import { Head } from '@inertiajs/vue3'
|
|
@@ -21,6 +22,10 @@ defineProps<{
|
|
|
21
22
|
|
|
22
23
|
<div class="py-12">
|
|
23
24
|
<div class="max-w-7xl mx-auto sm:px-6 lg:px-8 space-y-6">
|
|
25
|
+
<div class="p-4 sm:p-8 bg-white dark:bg-gray-800 shadow-sm sm:rounded-lg">
|
|
26
|
+
<UpdateAppearanceForm class="max-w-xl" />
|
|
27
|
+
</div>
|
|
28
|
+
|
|
24
29
|
<div class="p-4 sm:p-8 bg-white dark:bg-gray-800 shadow-sm sm:rounded-lg">
|
|
25
30
|
<UpdateProfileInformationForm :must-verify-email="mustVerifyEmail" :status="status" class="max-w-xl" />
|
|
26
31
|
</div>
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { ref } from 'vue'
|
|
3
|
+
import { getAppearance, setAppearance, type Appearance } from '@/lib/appearance'
|
|
4
|
+
|
|
5
|
+
const options: { value: Appearance; label: string }[] = [
|
|
6
|
+
{ value: 'light', label: 'Light' },
|
|
7
|
+
{ value: 'dark', label: 'Dark' },
|
|
8
|
+
{ value: 'system', label: 'System' },
|
|
9
|
+
]
|
|
10
|
+
|
|
11
|
+
const appearance = ref<Appearance>(getAppearance())
|
|
12
|
+
|
|
13
|
+
const select = (value: Appearance) => {
|
|
14
|
+
appearance.value = value
|
|
15
|
+
setAppearance(value)
|
|
16
|
+
}
|
|
17
|
+
</script>
|
|
18
|
+
|
|
19
|
+
<template>
|
|
20
|
+
<section>
|
|
21
|
+
<header>
|
|
22
|
+
<h2 class="text-lg font-medium text-gray-900 dark:text-gray-100">Appearance</h2>
|
|
23
|
+
|
|
24
|
+
<p class="mt-1 text-sm text-gray-600 dark:text-gray-400">
|
|
25
|
+
Choose how the application looks to you. System follows your device setting.
|
|
26
|
+
</p>
|
|
27
|
+
</header>
|
|
28
|
+
|
|
29
|
+
<div class="mt-6 inline-flex rounded-lg bg-gray-100 p-1 dark:bg-gray-900">
|
|
30
|
+
<button
|
|
31
|
+
v-for="option in options"
|
|
32
|
+
:key="option.value"
|
|
33
|
+
type="button"
|
|
34
|
+
:aria-pressed="appearance === option.value"
|
|
35
|
+
class="rounded-md px-4 py-1.5 text-sm font-medium transition focus:outline-hidden focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2 dark:focus:ring-offset-gray-800"
|
|
36
|
+
:class="
|
|
37
|
+
appearance === option.value
|
|
38
|
+
? 'bg-white text-gray-900 shadow-xs dark:bg-gray-700 dark:text-gray-100'
|
|
39
|
+
: 'text-gray-500 hover:text-gray-900 dark:text-gray-400 dark:hover:text-gray-100'
|
|
40
|
+
"
|
|
41
|
+
@click="select(option.value)"
|
|
42
|
+
>
|
|
43
|
+
{{ option.label }}
|
|
44
|
+
</button>
|
|
45
|
+
</div>
|
|
46
|
+
</section>
|
|
47
|
+
</template>
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
export type Appearance = 'light' | 'dark' | 'system'
|
|
2
|
+
|
|
3
|
+
const STORAGE_KEY = 'appearance'
|
|
4
|
+
|
|
5
|
+
export function getAppearance(): Appearance {
|
|
6
|
+
const stored = localStorage.getItem(STORAGE_KEY)
|
|
7
|
+
|
|
8
|
+
return stored === 'light' || stored === 'dark' ? stored : 'system'
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function applyAppearance(appearance: Appearance = getAppearance()): void {
|
|
12
|
+
const dark =
|
|
13
|
+
appearance === 'dark' ||
|
|
14
|
+
(appearance === 'system' && window.matchMedia('(prefers-color-scheme: dark)').matches)
|
|
15
|
+
|
|
16
|
+
document.documentElement.classList.toggle('dark', dark)
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function setAppearance(appearance: Appearance): void {
|
|
20
|
+
localStorage.setItem(STORAGE_KEY, appearance)
|
|
21
|
+
|
|
22
|
+
applyAppearance(appearance)
|
|
23
|
+
}
|
|
@@ -6,11 +6,29 @@
|
|
|
6
6
|
<title><%= content_for(:title) || "Rails" %></title>
|
|
7
7
|
<%= csrf_meta_tags %>
|
|
8
8
|
<%= csp_meta_tag %>
|
|
9
|
+
<%# Apply the stored appearance before first paint, so the page never flashes the wrong theme. %>
|
|
10
|
+
<%= javascript_tag nonce: true do %>
|
|
11
|
+
(function () {
|
|
12
|
+
var preference = localStorage.getItem('appearance') || 'system'
|
|
13
|
+
var query = window.matchMedia('(prefers-color-scheme: dark)')
|
|
14
|
+
|
|
15
|
+
document.documentElement.classList.toggle(
|
|
16
|
+
'dark',
|
|
17
|
+
preference === 'dark' || (preference === 'system' && query.matches)
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
query.addEventListener('change', function () {
|
|
21
|
+
if ((localStorage.getItem('appearance') || 'system') === 'system') {
|
|
22
|
+
document.documentElement.classList.toggle('dark', query.matches)
|
|
23
|
+
}
|
|
24
|
+
})
|
|
25
|
+
})()
|
|
26
|
+
<% end %>
|
|
9
27
|
<%= vite_client_tag %>
|
|
10
28
|
<%= vite_typescript_tag 'application.ts', defer: true %>
|
|
11
29
|
</head>
|
|
12
30
|
<body class="font-sans antialiased">
|
|
13
|
-
<div class="min-h-screen bg-gray-100">
|
|
31
|
+
<div class="min-h-screen bg-gray-100 dark:bg-gray-900">
|
|
14
32
|
<%= yield %>
|
|
15
33
|
</div>
|
|
16
34
|
</body>
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: kaze
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 1.
|
|
4
|
+
version: 1.4.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Cuong Giang
|
|
@@ -147,6 +147,7 @@ files:
|
|
|
147
147
|
- stubs/hotwire/app/views/auth/reset_password.html.erb
|
|
148
148
|
- stubs/hotwire/app/views/auth/verify_email.html.erb
|
|
149
149
|
- stubs/hotwire/app/views/dashboard/index.html.erb
|
|
150
|
+
- stubs/hotwire/app/views/layouts/_appearance.html.erb
|
|
150
151
|
- stubs/hotwire/app/views/layouts/_navigation.html.erb
|
|
151
152
|
- stubs/hotwire/app/views/layouts/application.html.erb
|
|
152
153
|
- stubs/hotwire/app/views/layouts/guest.html.erb
|
|
@@ -154,6 +155,7 @@ files:
|
|
|
154
155
|
- stubs/hotwire/app/views/profile/edit.html.erb
|
|
155
156
|
- stubs/hotwire/app/views/profile/partials/_confirm_user_deletion_modal.html.erb
|
|
156
157
|
- stubs/hotwire/app/views/profile/partials/_delete_user_form.html.erb
|
|
158
|
+
- stubs/hotwire/app/views/profile/partials/_update_appearance_form.html.erb
|
|
157
159
|
- stubs/hotwire/app/views/profile/partials/_update_password_form.html.erb
|
|
158
160
|
- stubs/hotwire/app/views/profile/partials/_update_profile_information_form.html.erb
|
|
159
161
|
- stubs/hotwire/app/views/welcome/index.html.erb
|
|
@@ -203,11 +205,13 @@ files:
|
|
|
203
205
|
- stubs/inertia-react-ts/app/javascript/Pages/Dashboard.tsx
|
|
204
206
|
- stubs/inertia-react-ts/app/javascript/Pages/Profile/Edit.tsx
|
|
205
207
|
- stubs/inertia-react-ts/app/javascript/Pages/Profile/Partials/DeleteUserForm.tsx
|
|
208
|
+
- stubs/inertia-react-ts/app/javascript/Pages/Profile/Partials/UpdateAppearanceForm.tsx
|
|
206
209
|
- stubs/inertia-react-ts/app/javascript/Pages/Profile/Partials/UpdatePasswordForm.tsx
|
|
207
210
|
- stubs/inertia-react-ts/app/javascript/Pages/Profile/Partials/UpdateProfileInformationForm.tsx
|
|
208
211
|
- stubs/inertia-react-ts/app/javascript/Pages/Welcome.tsx
|
|
209
212
|
- stubs/inertia-react-ts/app/javascript/entrypoints/application.tsx
|
|
210
213
|
- stubs/inertia-react-ts/app/javascript/entrypoints/bootstrap.ts
|
|
214
|
+
- stubs/inertia-react-ts/app/javascript/lib/appearance.ts
|
|
211
215
|
- stubs/inertia-react-ts/app/javascript/types/global.d.ts
|
|
212
216
|
- stubs/inertia-react-ts/app/javascript/types/index.d.ts
|
|
213
217
|
- stubs/inertia-react-ts/app/javascript/types/vite-env.d.ts
|
|
@@ -239,11 +243,13 @@ files:
|
|
|
239
243
|
- stubs/inertia-vue-ts/app/javascript/Pages/Dashboard.vue
|
|
240
244
|
- stubs/inertia-vue-ts/app/javascript/Pages/Profile/Edit.vue
|
|
241
245
|
- stubs/inertia-vue-ts/app/javascript/Pages/Profile/Partials/DeleteUserForm.vue
|
|
246
|
+
- stubs/inertia-vue-ts/app/javascript/Pages/Profile/Partials/UpdateAppearanceForm.vue
|
|
242
247
|
- stubs/inertia-vue-ts/app/javascript/Pages/Profile/Partials/UpdatePasswordForm.vue
|
|
243
248
|
- stubs/inertia-vue-ts/app/javascript/Pages/Profile/Partials/UpdateProfileInformationForm.vue
|
|
244
249
|
- stubs/inertia-vue-ts/app/javascript/Pages/Welcome.vue
|
|
245
250
|
- stubs/inertia-vue-ts/app/javascript/entrypoints/application.ts
|
|
246
251
|
- stubs/inertia-vue-ts/app/javascript/entrypoints/bootstrap.ts
|
|
252
|
+
- stubs/inertia-vue-ts/app/javascript/lib/appearance.ts
|
|
247
253
|
- stubs/inertia-vue-ts/app/javascript/types/global.d.ts
|
|
248
254
|
- stubs/inertia-vue-ts/app/javascript/types/index.d.ts
|
|
249
255
|
- stubs/inertia-vue-ts/app/javascript/types/vite-env.d.ts
|