Skip to content

Commit 3d6e8c9

Browse files
committed
MEIER-230: Update ViewEngine, Datastar attrs, Tailwind dropdowns, theme toggle, borders, lazy Prism
1 parent 3f2d7eb commit 3d6e8c9

File tree

11 files changed

+136
-73
lines changed

11 files changed

+136
-73
lines changed

app/paket.lock

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ NUGET
77
System.Text.Json (>= 6.0.10) - restriction: >= netstandard2.0
88
FSharp.UMX (1.1)
99
FSharp.Core (>= 4.3.4) - restriction: >= netstandard2.0
10-
FSharp.ViewEngine (2026.2.2)
10+
FSharp.ViewEngine (2026.2.5)
1111
JetBrains.Annotations (>= 2025.2.4) - restriction: >= net8.0
1212
FsToolkit.ErrorHandling (5.1)
1313
FSharp.Core (>= 6.0.4) - restriction: || (&& (< net9.0) (>= netstandard2.1)) (&& (>= netstandard2.0) (< netstandard2.1))
@@ -225,7 +225,7 @@ NUGET
225225
SourceGear.sqlite3 (>= 3.50.4.2) - restriction: >= netstandard2.0
226226
SQLitePCLRaw.config.e_sqlite3 (>= 3.0.2) - restriction: >= netstandard2.0
227227
SQLitePCLRaw.config.e_sqlite3 (3.0.2) - restriction: >= netstandard2.0
228-
SQLitePCLRaw.provider.e_sqlite3 (>= 3.0.2) - restriction: || (>= net471) (&& (>= net8.0) (< net8.0-ios) (< net8.0-tvos)) (&& (< net8.0) (>= netstandard2.0)) (< netstandard2.0)
228+
SQLitePCLRaw.provider.e_sqlite3 (>= 3.0.2) - restriction: || (>= net471) (&& (>= net8.0) (< net8.0-ios) (< net8.0-tvos)) (&& (< net8.0) (>= netstandard2.0))
229229
SQLitePCLRaw.provider.internal (>= 3.0.2) - restriction: || (>= net8.0-ios) (>= net8.0-tvos)
230230
SQLitePCLRaw.core (3.0.2) - restriction: >= netstandard2.0
231231
System.Memory (>= 4.6.3) - restriction: >= netstandard2.0

app/src/App/src/Articles/Handler.fs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,9 @@ let private getArticlesPage (services:Services) : HttpHandler =
1616

1717
if ctx.IsDatastar then
1818
let ds = ctx.GetService<IDatastarService>()
19+
do! patchSignals ds {| selectedNav = "nav-articles" |}
1920
do! patchElement ds page
2021
do! pushUrl ds "/articles"
21-
do! patchSignals ds {| selectedNav = "nav-articles" |}
2222
return Some ctx
2323
else
2424
return! renderPage page "nav-articles" next ctx
@@ -34,9 +34,9 @@ let private getArticlePage (services:Services) (id:string) : HttpHandler =
3434

3535
if ctx.IsDatastar then
3636
let ds = ctx.GetService<IDatastarService>()
37+
do! patchSignals ds {| selectedNav = "nav-articles" |}
3738
do! patchElement ds page
3839
do! pushUrl ds url
39-
do! patchSignals ds {| selectedNav = "nav-articles" |}
4040
return Some ctx
4141
else
4242
return! renderPage page "nav-articles" next ctx

app/src/App/src/Articles/View.fs

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -111,7 +111,7 @@ module Block =
111111
| Domain.Notion.BlockType.Image url ->
112112
img { _class "drop-shadow-xl rounded"; _src url }
113113
| Domain.Notion.BlockType.Divider ->
114-
div { _class "border-b-2 border-gray-300 dark:border-gray-600" }
114+
div { _class "border-b-2 border-gray-300/60 dark:border-gray-700/60" }
115115
| Domain.Notion.BlockType.Quote richText ->
116116
blockquote { for t in richText do RichTextView.toHtml t }
117117
| Domain.Notion.BlockType.Callout richText ->
@@ -179,10 +179,12 @@ let articlePage (article':Article) =
179179
_class "mx-auto max-w-5xl px-4"
180180
div {
181181
_class "mt-8 prose prose-lg dark:prose-invert prose-code:before:hidden prose-code:after:hidden max-w-none"
182-
_dsInit "Prism.highlightAllUnder($el)"
182+
_dataInit "highlightCode($el)"
183183
for el in Content.toHtml article'.blocks do el
184184
}
185185
}
186+
script { _src "/scripts/prism.1.29.0.js" }
187+
script { js "function highlightCode(el){if(el?.querySelectorAll)Prism.highlightAllUnder(el)}" }
186188
}
187189
Page.primary content
188190

app/src/App/src/Common/View.fs

Lines changed: 112 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ open FSharp.ViewEngine
44
open Domain.Article
55
open type Html
66
open type Datastar
7+
open type Tailwind
78

89
module 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 =
100110
module 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

119129
module 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
}

app/src/App/src/Index/Handler.fs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,9 @@ let private getHomePage (services:Services) : HttpHandler =
1616

1717
if ctx.IsDatastar then
1818
let ds = ctx.GetService<IDatastarService>()
19+
do! patchSignals ds {| selectedNav = "nav-home" |}
1920
do! patchElement ds page
2021
do! pushUrl ds "/"
21-
do! patchSignals ds {| selectedNav = "nav-home" |}
2222
return Some ctx
2323
else
2424
return! renderPage page "nav-home" next ctx

app/src/App/src/Index/View.fs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ let homePage (recentArticles:Article list) =
5656
_class "mt-4"
5757
a {
5858
_class "text-sm text-emerald-600 hover:underline hover:cursor-pointer dark:text-emerald-400"
59-
_dsOn ("click", "@get('/articles')")
59+
_dataOn ("click", "@get('/articles')")
6060
"View all"
6161
}
6262
}

0 commit comments

Comments
 (0)