diff --git a/CHANGELOG.md b/CHANGELOG.md index e4d916d..0c8daf3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,12 @@ follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). ## Unreleased +### Added +- `DATASTAR_SUPPORTED_VERSION = v"1.0.1"` constant (exported) pinning + the Datastar protocol/client version HyperSignal targets. Examples + reference the constant instead of hard-coding the literal so future + bumps land as a single visible diff. + ## 0.2.0 — 2026-05-26 ### Removed diff --git a/README.md b/README.md index 9402330..9e4f6fe 100644 --- a/README.md +++ b/README.md @@ -8,6 +8,8 @@ Datastar-flavored HTML for Julia, with front-row support for inlining CairoMakie figures into your pages. Build hypermedia UIs that read top-to-bottom and stay out of the way. +Compatible with Datastar v1.0.1. + ```julia using HyperSignal using HyperSignal.Helpers: radio_field # app-grade helpers live here diff --git a/docs/src/index.md b/docs/src/index.md index ffa188e..14e8721 100644 --- a/docs/src/index.md +++ b/docs/src/index.md @@ -3,6 +3,8 @@ Datastar-flavored HTML for Julia, with front-row support for inlining CairoMakie figures into your pages. +Compatible with Datastar v1.0.1. + ```julia using HyperSignal using HyperSignal.Helpers: radio_field diff --git a/examples/cairomakie_dashboard.jl b/examples/cairomakie_dashboard.jl index 9eb9233..c689b8a 100644 --- a/examples/cairomakie_dashboard.jl +++ b/examples/cairomakie_dashboard.jl @@ -1,59 +1,94 @@ -# A CairoMakie dashboard — two figures inlined on the same page. +# A CairoMakie dashboard — two figures inlined on the same page, +# auto-refreshed every second via a Datastar polling action. # # Run: # julia --project=examples examples/cairomakie_dashboard.jl -# then open http://127.0.0.1:8080 in a browser. Refresh and the figures -# regenerate with new random data; the two id_prefixes keep the SVG -# clip-path / glyph IDs from colliding. +# then open http://127.0.0.1:8080 in a browser. Datastar polls +# /plots every second and morphs the two figures in place; the +# two id_prefixes keep the SVG clip-path / glyph IDs from colliding. # # This is the proof of the front-row CairoMakie claim: drop a Figure # straight into a page tree, no fork-and-rewrite step. -using HTTP, HyperSignal, CairoMakie +using HTTP, HyperSignal, CairoMakie, Downloads using HyperSignal: div +const DATASTAR_VERSION = "v$(HyperSignal.DATASTAR_SUPPORTED_VERSION)" +const DATASTAR_URL = "https://cdn.jsdelivr.net/gh/starfederation/datastar@$(DATASTAR_VERSION)/bundles/datastar.js" +const DATASTAR_PATH = joinpath(@__DIR__, "datastar-$(DATASTAR_VERSION).js") + +function ensure_datastar() + isfile(DATASTAR_PATH) && return DATASTAR_PATH + @info "Downloading Datastar bundle" DATASTAR_URL DATASTAR_PATH + Downloads.download(DATASTAR_URL, DATASTAR_PATH) + DATASTAR_PATH +end + +const DATASTAR_BODY = read(ensure_datastar()) + function make_line_figure() fig = Figure(size=(640, 320)) - ax = Axis(fig[1, 1], title="Random walk", xlabel="step", ylabel="value") + ax = Axis(fig[1, 1], title="Random walk", xlabel="step", ylabel="value", + limits=(1, 50, -15, 15)) lines!(ax, 1:50, cumsum(randn(50))) fig end function make_scatter_figure() fig = Figure(size=(640, 320)) - ax = Axis(fig[1, 1], title="Random scatter", xlabel="x", ylabel="y") + ax = Axis(fig[1, 1], title="Random scatter", xlabel="x", ylabel="y", + limits=(-4, 4, -4, 4)) scatter!(ax, randn(80), randn(80); markersize=8) fig end -function dashboard(_req) +function plots_fragment() + div(id="plots", + on_interval(ds_get("/plots"); ms=1000), + article(class="card", + h2("Line"), + div(class="plot", + inline_svg(make_line_figure(); + id_prefix="line_", + aria_label="Cumulative random walk over 50 steps"))), + article(class="card", + h2("Scatter"), + div(class="plot", + inline_svg(make_scatter_figure(); + id_prefix="scatter_", + aria_label="80 random points on standard normal axes")))) +end + +function home(_req) page = Frag(DOCTYPE, html(lang="en", head(meta(charset="UTF-8"), title("HyperSignal × CairoMakie"), + script(type="module", src="/datastar.js"), style(""".plot { max-width: 720px; margin: 1em 0; } body { font-family: system-ui; max-width: 800px; margin: 2em auto; }""")), body( h1("CairoMakie dashboard"), - p("Refresh for new random data. Two figures, one page, ", + p("Datastar polls every second. Two figures, one page, ", "zero id collisions."), - article(class="card", - h2("Line"), - div(class="plot", - inline_svg(make_line_figure(); - id_prefix="line_", - aria_label="Cumulative random walk over 50 steps"))), - article(class="card", - h2("Scatter"), - div(class="plot", - inline_svg(make_scatter_figure(); - id_prefix="scatter_", - aria_label="80 random points on standard normal axes"))), + plots_fragment(), ))) html_response(page) end +plots(_req) = fragment_response(plots_fragment()) + +datastar_js(_req) = HTTP.Response(200, + ["Content-Type" => "application/javascript; charset=utf-8", + "Cache-Control" => "public, max-age=31536000, immutable"], + DATASTAR_BODY) + +const ROUTER = HTTP.Router() +HTTP.register!(ROUTER, "GET", "/", home) +HTTP.register!(ROUTER, "GET", "/plots", plots) +HTTP.register!(ROUTER, "GET", "/datastar.js", datastar_js) + if abspath(PROGRAM_FILE) == @__FILE__ @info "Serving on http://127.0.0.1:8080 — Ctrl-C to stop" - HTTP.serve(dashboard, "127.0.0.1", 8080) + HTTP.serve(ROUTER, "127.0.0.1", 8080) end diff --git a/examples/counter_app.jl b/examples/counter_app.jl index b68719e..201ebaf 100644 --- a/examples/counter_app.jl +++ b/examples/counter_app.jl @@ -9,18 +9,29 @@ # pasteable shape of a HyperSignal + Datastar server. For a richer # walkthrough see the docs site. -using HTTP, HyperSignal +using HTTP, HyperSignal, Downloads using HyperSignal: div -const COUNTER = Ref(0) +const COUNTER = Ref(0) +const DATASTAR_VERSION = "v$(HyperSignal.DATASTAR_SUPPORTED_VERSION)" +const DATASTAR_URL = "https://cdn.jsdelivr.net/gh/starfederation/datastar@$(DATASTAR_VERSION)/bundles/datastar.js" +const DATASTAR_PATH = joinpath(@__DIR__, "datastar-$(DATASTAR_VERSION).js") + +function ensure_datastar() + isfile(DATASTAR_PATH) && return DATASTAR_PATH + @info "Downloading Datastar bundle" DATASTAR_URL DATASTAR_PATH + Downloads.download(DATASTAR_URL, DATASTAR_PATH) + DATASTAR_PATH +end + +const DATASTAR_BODY = read(ensure_datastar()) function home(_req) page = Frag(DOCTYPE, html(lang="en", head(meta(charset="UTF-8"), title("HyperSignal counter"), - script(type="module", - src="https://cdn.jsdelivr.net/gh/starfederation/datastar@v1.0.0-beta.11/bundles/datastar.js")), + script(type="module", src="/datastar.js")), body( h1("Counter"), div(id="counter", COUNTER[]), @@ -34,18 +45,24 @@ end function increment(_req) COUNTER[] += 1 - fragment_response(div(id="counter", COUNTER[]), "#counter") + fragment_response(div(id="counter", COUNTER[])) end function reset(_req) COUNTER[] = 0 - fragment_response(div(id="counter", COUNTER[]), "#counter") + fragment_response(div(id="counter", COUNTER[])) end +datastar_js(_req) = HTTP.Response(200, + ["Content-Type" => "application/javascript; charset=utf-8", + "Cache-Control" => "public, max-age=31536000, immutable"], + DATASTAR_BODY) + const ROUTER = HTTP.Router() -HTTP.register!(ROUTER, "GET", "/", home) -HTTP.register!(ROUTER, "POST", "/increment", increment) -HTTP.register!(ROUTER, "POST", "/reset", reset) +HTTP.register!(ROUTER, "GET", "/", home) +HTTP.register!(ROUTER, "GET", "/datastar.js", datastar_js) +HTTP.register!(ROUTER, "POST", "/increment", increment) +HTTP.register!(ROUTER, "POST", "/reset", reset) if abspath(PROGRAM_FILE) == @__FILE__ @info "Serving on http://127.0.0.1:8080 — Ctrl-C to stop" diff --git a/src/HyperSignal.jl b/src/HyperSignal.jl index 6b4b867..79b7f4d 100644 --- a/src/HyperSignal.jl +++ b/src/HyperSignal.jl @@ -96,6 +96,7 @@ export progress, details, dialog, meter, output, data export audio, video, picture, source, track, iframe, embed, object, param, area # Datastar +export DATASTAR_SUPPORTED_VERSION export DSAction, ds_get, ds_post, ds_put, ds_delete export ds_indicator, ds_ignore_morph, ds_bind, ds_signal, ds_signals, ds_show, ds_text export ds_ref, ds_attr, ds_class, ds_effect, ds_init diff --git a/src/datastar.jl b/src/datastar.jl index ed75a11..ec873d6 100644 --- a/src/datastar.jl +++ b/src/datastar.jl @@ -5,6 +5,14 @@ # and the lib emits the right attribute name + JS expression. Typos turn # into method errors at the right call site, not silent client behavior. +""" + DATASTAR_SUPPORTED_VERSION + +The Datastar protocol/client version HyperSignal is built and tested against. +Pin your served `datastar.js` to this version; bumps land as one visible diff. +""" +const DATASTAR_SUPPORTED_VERSION = v"1.0.1" + """ DSAction(verb, url, form, extras) diff --git a/test/runtests.jl b/test/runtests.jl index 91e4363..2b7130c 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -43,6 +43,12 @@ using HyperSignal.Helpers: radio_field, checkbox_field, text_field, @test out == "