Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 2 additions & 0 deletions docs/src/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
79 changes: 57 additions & 22 deletions examples/cairomakie_dashboard.jl
Original file line number Diff line number Diff line change
@@ -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
35 changes: 26 additions & 9 deletions examples/counter_app.jl
Original file line number Diff line number Diff line change
Expand Up @@ -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[]),
Expand All @@ -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"
Expand Down
1 change: 1 addition & 0 deletions src/HyperSignal.jl
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
8 changes: 8 additions & 0 deletions src/datastar.jl
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down
6 changes: 6 additions & 0 deletions test/runtests.jl
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,12 @@ using HyperSignal.Helpers: radio_field, checkbox_field, text_field,
@test out == "<ul><li>one</li><li>two</li><li>three</li></ul>"
end

@testset "DATASTAR_SUPPORTED_VERSION pins the targeted Datastar release" begin
# Why: bumps should land as one visible diff; this test fails on
# an unintentional change to the supported protocol/client version.
@test DATASTAR_SUPPORTED_VERSION == v"1.0.1"
end

@testset "ds_post emits the Datastar form-encoded action expression" begin
a = ds_post("/api/x"; form=true)
@test HyperSignal.action_js(a) == "@post('/api/x', {contentType: 'form'})"
Expand Down
Loading