@@ -4,6 +4,7 @@ open FSharp.ViewEngine
44open Domain.Article
55open type Html
66open type Datastar
7+ open type Tailwind
78
89module MiniIcon =
910 let github =
@@ -52,6 +53,15 @@ module MiniIcon =
5253 let moon =
5354 raw """ <svg class="hidden h-5 w-5 dark:block" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" d="M21.752 15.002A9.72 9.72 0 0 1 18 15.75c-5.385 0-9.75-4.365-9.75-9.75 0-1.33.266-2.597.748-3.752A9.753 9.753 0 0 0 3 11.25C3 16.635 7.365 21 12.75 21a9.753 9.753 0 0 0 9.002-5.998Z"/></svg>"""
5455
56+ let sunSmall =
57+ raw """ <svg class="h-5 w-5" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" d="M12 3v2.25m6.364.386-1.591 1.591M21 12h-2.25m-.386 6.364-1.591-1.591M12 18.75V21m-4.773-4.227-1.591 1.591M5.25 12H3m4.227-4.773L5.636 5.636M15.75 12a3.75 3.75 0 1 1-7.5 0 3.75 3.75 0 0 1 7.5 0Z"/></svg>"""
58+
59+ let moonSmall =
60+ raw """ <svg class="h-5 w-5" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" d="M21.752 15.002A9.72 9.72 0 0 1 18 15.75c-5.385 0-9.75-4.365-9.75-9.75 0-1.33.266-2.597.748-3.752A9.753 9.753 0 0 0 3 11.25C3 16.635 7.365 21 12.75 21a9.753 9.753 0 0 0 9.002-5.998Z"/></svg>"""
61+
62+ let monitor =
63+ raw """ <svg class="h-5 w-5" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" d="M9 17.25v1.007a3 3 0 0 1-.879 2.122L7.5 21h9l-.621-.621A3 3 0 0 1 15 18.257V17.25m6-12V15a2.25 2.25 0 0 1-2.25 2.25H5.25A2.25 2.25 0 0 1 3 15V5.25m18 0A2.25 2.25 0 0 0 18.75 3H5.25A2.25 2.25 0 0 0 3 5.25m18 0V12a2.25 2.25 0 0 1-2.25 2.25H5.25A2.25 2.25 0 0 1 3 12V5.25"/></svg>"""
64+
5565 let hamburger =
5666 raw """ <svg class="h-6 w-6" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" d="M3.75 6.75h16.5M3.75 12h16.5m-16.5 5.25h16.5"/></svg>"""
5767
@@ -69,7 +79,7 @@ module ArticleCard =
6979 let url = $" /articles/{article'.permalink}"
7080 article {
7181 _ id article'.permalink
72- _ class " py-6 border-b border-gray-200 dark:border-gray-800 "
82+ _ class " py-6 border-b border-gray-300/60 dark:border-gray-700/60 "
7383 div {
7484 _ class " flex items-center flex-wrap gap-x-4 gap-y-1 text-sm text-gray-400 dark:text-gray-500"
7585 div {
@@ -85,7 +95,7 @@ module ArticleCard =
8595 _ class " mt-2 text-xl font-semibold tracking-tight text-gray-900 dark:text-gray-100"
8696 a {
8797 _ href url
88- _ dsOn ( " click" , $" @get('{url}')" )
98+ _ dataOn ( " click" , $" @get('{url}')" )
8999 _ class " hover:text-emerald-600 dark:hover:text-emerald-400"
90100 article'.title
91101 }
@@ -100,7 +110,7 @@ module ArticleCard =
100110module Footer =
101111 let primary =
102112 div {
103- _ class " flex p-10 bg-gray-50 border-t border-gray-300 dark:bg-gray-900 dark:border-gray-700 dark:text-gray-300"
113+ _ class " flex p-10 bg-gray-100 border-t border-gray-300 dark:bg-gray-900 dark:border-gray-700 dark:text-gray-300"
104114 div {
105115 _ class " text-sm space-y-1"
106116 div { _ class " w-12 h-12 text-emerald-600 dark:text-emerald-400" ; MiniIcon.logo }
@@ -110,82 +120,132 @@ module Footer =
110120 div { _ class " grow" }
111121 div {
112122 _ class " text-sm flex flex-col space-y-1"
113- a { _ class " underline cursor-pointer hover:text-emerald-600 dark:hover:text-emerald-400" ; _ dsOn ( " click" , " @get('/articles')" ); " Articles" }
114- a { _ class " underline cursor-pointer hover:text-emerald-600 dark:hover:text-emerald-400" ; _ dsOn ( " click" , " @get('/services')" ); " Services" }
115- a { _ class " underline cursor-pointer hover:text-emerald-600 dark:hover:text-emerald-400" ; _ dsOn ( " click" , " @get('/projects')" ); " Projects" }
123+ a { _ class " underline cursor-pointer hover:text-emerald-600 dark:hover:text-emerald-400" ; _ dataOn ( " click" , " @get('/articles')" ); " Articles" }
124+ a { _ class " underline cursor-pointer hover:text-emerald-600 dark:hover:text-emerald-400" ; _ dataOn ( " click" , " @get('/services')" ); " Services" }
125+ a { _ class " underline cursor-pointer hover:text-emerald-600 dark:hover:text-emerald-400" ; _ dataOn ( " click" , " @get('/projects')" ); " Projects" }
116126 }
117127 }
118128
119129module TopNav =
120130 let private item ( id : string , el : HtmlElement , href : string ) =
121- let baseClass , inactiveLightClass , inactiveDarkClass =
131+ let baseClass =
122132 if id = " nav-home" then
123- " p-2 text-base font-bold tracking-tight cursor-pointer hover:text-emerald-600 dark:hover:text-emerald-400" , " text-gray-900 " , " dark:text-gray-100 "
133+ " p-2 text-base font-bold tracking-tight cursor-pointer hover:text-emerald-600 dark:hover:text-emerald-400"
124134 else
125- " p-2 text-sm font-semibold cursor-pointer hover:text-emerald-600 dark:hover:text-emerald-400" , " text-gray-800 " , " dark:text-gray-200 "
135+ " p-2 text-sm font-semibold cursor-pointer hover:text-emerald-600 dark:hover:text-emerald-400"
126136
127137 a {
128138 _ id id
129139 _ class baseClass
130- _ dsClass ( " text-emerald-600" , $" $selectedNav == '{id}'" )
131- _ dsClass ( " dark:text-emerald-400" , $" $selectedNav == '{id}'" )
132- _ dsClass ( inactiveLightClass , $" $selectedNav != '{id}'" )
133- _ dsClass ( inactiveDarkClass , $" $selectedNav != '{id}'" )
134- _ dsOn ( " click" , $" @get('{href}')" )
140+ _ dataClass ( " text-emerald-600" , $" $selectedNav == '{id}'" )
141+ _ dataClass ( " dark:text-emerald-400" , $" $selectedNav == '{id}'" )
142+ _ dataClass ( " text-gray-800 " , $" $selectedNav != '{id}'" )
143+ _ dataClass ( " dark:text-gray-200 " , $" $selectedNav != '{id}'" )
144+ _ dataOn ( " click" , $" @get('{href}')" )
135145 el
136146 }
137147
138148 let private mobileItem ( id : string , label : string , href : string ) =
139- a {
149+ button {
140150 _ id $" {id}-mobile"
141- _ class " block p-3 text-base font-semibold cursor-pointer hover:text-emerald-600 hover:bg-gray-200 dark:hover:text-emerald-400 dark:hover:bg-gray-800 rounded-md"
142- _ dsClass ( " text-emerald-600" , $" $selectedNav == '{id}'" )
143- _ dsClass ( " dark:text-emerald-400" , $" $selectedNav == '{id}'" )
144- _ dsClass ( " text-gray-800" , $" $selectedNav != '{id}'" )
145- _ dsClass ( " dark:text-gray-200" , $" $selectedNav != '{id}'" )
146- _ dsOn ( " click" , $" $menuOpen = false; @get('{href}')" )
151+ _ type " button"
152+ _ class " block w-full text-left px-4 py-2 text-sm cursor-pointer hover:bg-gray-100 hover:text-emerald-600 dark:hover:bg-gray-700/50 dark:hover:text-emerald-400"
153+ _ dataClass ( " text-emerald-600" , $" $selectedNav == '{id}'" )
154+ _ dataClass ( " dark:text-emerald-400" , $" $selectedNav == '{id}'" )
155+ _ dataClass ( " font-semibold" , $" $selectedNav == '{id}'" )
156+ _ dataClass ( " text-gray-700" , $" $selectedNav != '{id}'" )
157+ _ dataClass ( " dark:text-gray-300" , $" $selectedNav != '{id}'" )
158+ _ dataOn ( " click" , $" @get('{href}')" )
147159 text label
148160 }
149161
150162 let private themeToggle =
151- button {
152- _ id " theme-toggle"
153- _ type " button"
154- _ class [
155- " p-2 rounded-md text-gray-600 hover:text-emerald-600 hover:bg-gray-100"
156- " dark:text-gray-400 dark:hover:text-emerald-400 dark:hover:bg-gray-800"
157- " hover:cursor-pointer"
158- ]
159- _ onclick " toggleTheme()"
160- MiniIcon.sun
161- MiniIcon.moon
163+ elDropdown {
164+ _ class " relative inline-block text-left"
165+ button {
166+ _ id " theme-toggle"
167+ _ type " button"
168+ _ class [
169+ " inline-flex w-full items-center justify-center rounded-md p-2 text-gray-600 hover:text-emerald-600 hover:bg-gray-100"
170+ " dark:text-gray-400 dark:hover:text-emerald-400 dark:hover:bg-gray-800"
171+ " hover:cursor-pointer"
172+ ]
173+ MiniIcon.sun
174+ MiniIcon.moon
175+ }
176+ elMenu {
177+ _ popover
178+ _ anchor " bottom end"
179+ _ class " mt-2 w-36 origin-top-right rounded-md bg-white py-1 shadow-lg ring-1 ring-black/5 dark:bg-gray-800 dark:ring-white/10"
180+ button {
181+ _ type " button"
182+ _ class " flex w-full items-center gap-2 px-4 py-2 text-sm hover:bg-gray-100 dark:hover:bg-gray-700/50"
183+ _ dataClass ( " text-emerald-600" , " $theme == 'light'" )
184+ _ dataClass ( " dark:text-emerald-400" , " $theme == 'light'" )
185+ _ dataClass ( " font-semibold" , " $theme == 'light'" )
186+ _ dataClass ( " text-gray-700" , " $theme != 'light'" )
187+ _ dataClass ( " dark:text-gray-300" , " $theme != 'light'" )
188+ _ dataOn ( " click" , " $theme = 'light'; setTheme('light')" )
189+ MiniIcon.sunSmall
190+ text " Light"
191+ }
192+ button {
193+ _ type " button"
194+ _ class " flex w-full items-center gap-2 px-4 py-2 text-sm hover:bg-gray-100 dark:hover:bg-gray-700/50"
195+ _ dataClass ( " text-emerald-600" , " $theme == 'dark'" )
196+ _ dataClass ( " dark:text-emerald-400" , " $theme == 'dark'" )
197+ _ dataClass ( " font-semibold" , " $theme == 'dark'" )
198+ _ dataClass ( " text-gray-700" , " $theme != 'dark'" )
199+ _ dataClass ( " dark:text-gray-300" , " $theme != 'dark'" )
200+ _ dataOn ( " click" , " $theme = 'dark'; setTheme('dark')" )
201+ MiniIcon.moonSmall
202+ text " Dark"
203+ }
204+ button {
205+ _ type " button"
206+ _ class " flex w-full items-center gap-2 px-4 py-2 text-sm hover:bg-gray-100 dark:hover:bg-gray-700/50"
207+ _ dataClass ( " text-emerald-600" , " $theme == 'system'" )
208+ _ dataClass ( " dark:text-emerald-400" , " $theme == 'system'" )
209+ _ dataClass ( " font-semibold" , " $theme == 'system'" )
210+ _ dataClass ( " text-gray-700" , " $theme != 'system'" )
211+ _ dataClass ( " dark:text-gray-300" , " $theme != 'system'" )
212+ _ dataOn ( " click" , " $theme = 'system'; setTheme('system')" )
213+ MiniIcon.monitor
214+ text " System"
215+ }
216+ }
162217 }
163218
164- let private hamburgerButton =
165- button {
166- _ id " menu-toggle"
167- _ type " button"
168- _ class [
169- " md:hidden p-2 rounded-md text-gray-600 hover:text-emerald-600 hover:bg-gray-100"
170- " dark:text-gray-400 dark:hover:text-emerald-400 dark:hover:bg-gray-800"
171- " hover:cursor-pointer"
172- ]
173- _ dsOn ( " click" , " $menuOpen = !$menuOpen" )
174- div {
175- _ dsShow " !$menuOpen"
219+ let private mobileDropdown =
220+ elDropdown {
221+ _ class " relative inline-block text-left md:hidden"
222+ button {
223+ _ id " menu-toggle"
224+ _ type " button"
225+ _ class [
226+ " inline-flex w-full items-center justify-center rounded-md p-2 text-gray-600 hover:text-emerald-600 hover:bg-gray-100"
227+ " dark:text-gray-400 dark:hover:text-emerald-400 dark:hover:bg-gray-800"
228+ " hover:cursor-pointer"
229+ ]
176230 MiniIcon.hamburger
177231 }
178- div {
179- _ dsShow " $menuOpen"
180- MiniIcon.close
232+ elMenu {
233+ _ id " mobile-menu"
234+ _ popover
235+ _ anchor " bottom end"
236+ _ class " mt-2 w-56 origin-top-right rounded-md bg-white py-1 shadow-lg ring-1 ring-black/5 dark:bg-gray-800 dark:ring-white/10"
237+ mobileItem( " nav-articles" , " Articles" , " /articles" )
238+ mobileItem( " nav-projects" , " Projects" , " /projects" )
239+ mobileItem( " nav-services" , " Services" , " /services" )
181240 }
182241 }
183242
184243 let primary =
185244 nav {
186245 _ id " top-nav"
187246 _ class " relative bg-gray-100 py-2 px-4 border-b border-gray-300 dark:bg-gray-900 dark:border-gray-700"
188- _ dsSignals ( " menuOpen" , " false" )
247+ _ dataSignals " {theme: 'system'}"
248+ _ dataInit " $theme = getInitialTheme(); applyTheme($theme)"
189249 div {
190250 _ class " flex items-center gap-4"
191251 item( " nav-home" , div { _ class " w-8 h-8 text-emerald-600 dark:text-emerald-400" ; MiniIcon.logo }, " /" )
@@ -197,15 +257,7 @@ module TopNav =
197257 item( " nav-services" , text " Services" , " /services" )
198258 }
199259 themeToggle
200- hamburgerButton
201- }
202- div {
203- _ id " mobile-menu"
204- _ class " md:hidden absolute left-0 right-0 top-full z-50 bg-gray-100 border-b border-gray-300 dark:bg-gray-900 dark:border-gray-700 px-4 pt-2 pb-1 shadow-lg"
205- _ dsShow " $menuOpen"
206- mobileItem( " nav-articles" , " Articles" , " /articles" )
207- mobileItem( " nav-projects" , " Projects" , " /projects" )
208- mobileItem( " nav-services" , " Services" , " /services" )
260+ mobileDropdown
209261 }
210262 }
211263
@@ -225,18 +277,18 @@ type Document =
225277 script { js " let t=localStorage.getItem('theme');if(t==='dark'||(!t||t==='system')&&window.matchMedia('(prefers-color-scheme: dark)').matches){document.documentElement.classList.add('dark')}" }
226278 link { _ href " /css/compiled.css" ; _ rel " stylesheet" }
227279 link { _ href " /css/prism.css" ; _ rel " stylesheet" }
280+ script { _ type " module" ; _ src " /scripts/tailwindplus-elements.1.js" }
228281 script { _ type " module" ; _ src " /scripts/datastar.1.0.0-RC.6.js" }
229282 }
230283 body {
231- _ dsSignals ( " selectedNav" , $ " '{selectedNav}'" )
284+ _ dataSignals $ " {{ selectedNav: '{selectedNav}'}} "
232285 _ class " bg-gray-200 dark:bg-gray-950"
233286 div {
234287 _ class " mx-auto max-w-7xl"
235288 TopNav.primary
236289 page
237290 Footer.primary
238291 }
239- script { _ src " /scripts/prism.1.29.0.js" }
240- script { js " function toggleTheme(){var d=document.documentElement,t=d.classList.contains('dark')?'light':'dark';localStorage.setItem('theme',t);d.classList.toggle('dark',t==='dark')}" }
292+ script { js " function getInitialTheme(){return localStorage.getItem('theme')||'system'};function applyTheme(t){var d=document.documentElement,isDark=t==='dark'||(t==='system'&&window.matchMedia('(prefers-color-scheme: dark)').matches);d.classList.toggle('dark',isDark)};function setTheme(t){localStorage.setItem('theme',t);applyTheme(t)}" }
241293 }
242294 }
0 commit comments