diff --git a/.gitignore b/.gitignore index f0ef2c2..f495c8e 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ -/db/ +**/db/ +**/database.db* .* !.github !.gitignore diff --git a/Caddyfile b/Caddyfile new file mode 100644 index 0000000..dedc07c --- /dev/null +++ b/Caddyfile @@ -0,0 +1,20 @@ +# Caddy file for testing with https locally +# +# You can install Caddy with: +# +# $ brew install caddy +# +# You can start caddy with in the project root directory: +# +# $ Caddy run +# +# Then start your project at the server at repl on port 8080 +# +localhost:3030 { + reverse_proxy localhost:8080 { + # If localhost:8080 is not responding retry every second for + # 30 seconds. This stops deployments from breaking SSE connections. + lb_try_duration 30s + lb_try_interval 1s + } +} \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..9856bc4 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Anders Murphy + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md index 4630101..d663c81 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,138 @@ # Hyperlith: the hypermedia based monolith -This is the start of a very opinionated fullstack datastar library mostly -for personal use. +This is a small and very opinionated fullstack [Datastar](https://data-star.dev/) framework mostly +for my personal use. However, I felt there were some ideas in here that were worth sharing. + +Hyperlith only uses a subset of Datastar's feartures. If you want a production ready full featured Datastar Clojure SDK use [the official SDK](https://github.com/starfederation/datastar/tree/main/sdk/clojure). + +>โš ๏ธ **WARNING:** API can change at any time! Use at your own risk. + +## Goals / Priorities + +- **Opinionated** - does not try to be a general solution. +- **Server driven** - push based multiplayer and streaming updates from day 1. +- **Production REPL first** - a running system should be simple to: introspect, modify and patch. +- **Minimal dependencies** - ensures the project is robust and secure long term. +- **Operationally Simple** - handles all operational tasks in process. +- **Sovereign** - can be deployed on any VPS provider. + +## F.A.Q + +**Q:** *Why do I get 403 when running the examples locally in Chrome or Safari?* + +**A:** *The session and csrf cookies use `Secure` this means that these cookies won't be set on localhost when using chrome or safari (as they require HTTPS) . If you want to use Chrome or Safari for local development you can run `caddy run` to start a local https proxy on https://localhost:3030. The local `caddyfile` can be [found here](https://github.com/andersmurphy/hyperlith/blob/master/Caddyfile). The goal is for development to be as close to production as possible, and Hyperlith is designed to be run behind a reverse proxy.* + +## Rational (more like a collection of opinions) + +#### Why large/fat/main morphs (immediate mode)? + +By only using `data: mergeMode morph` and always targeting the `main` element of the document the API can be massively simplified. This avoids having the explosion of endpoints you get with HTMX and makes reasoning about your app much simpler. + +#### Why have single render function per page? + +By having a single render function per page you can simplify the reasoning about your app to `view = f(state)`. You can then reason about your pushed updates as a continuous signal rather than discrete event stream. The benefit of this is you don't have to handle missed events, disconnects and reconnects. When the state changes on the server you push down the latest view, not the delta between views. On the client idiomorph can translate that into fine grained dom updates. + +#### Why re-render on any database change? + +When your events are not homogeneous, you can't miss events, so you cannot throttle your events without losing data. + +But, wait! Won't that mean every change will cause all users to re-render? Yes, but at a maximum rate determined by the throttle. This, might sound scary at first but in practice: + +- The more shared views the users have the more likely most of the connected users will have to re-render when a change happen. + +- The more events that are happening the more likely most users will have to re-render. + +This means you actually end up doing more work with a non homogeneous event system under heavy load than with this simple homogeneous event system that's throttled (especially it there's any sort of common/shared view between users). + +#### Why no diffing? + +In theory you can optimise network and remove the need for idiomorph if you do diffing between the last view and the current view. However, in practice because the SSE stream is being compressed for the duration of a connection and html compresses really well you get amazing compression (reduction in size by 90-100x! Sometimes more) over a series of view re-renders. The compression is so good that in my experience it's more network efficient and more performant that fine grained updates with diffing (without any of the additional complexity). + +This approach avoids the additional challenges of view and session maintenance (increased server load and memory usage). + +My suspicion is websocket approaches in this space like Phoenix Liveview haven't stumbled across this because you don't get compression out of the box with websockets, and idiomorph is a relatively new invention. Intuitively you would think the diffing approach would be more performant so you wouldn't even consider this approach. + +#### Signals are only for ephemeral client side state + +Signals should only be used for ephemeral client side state. Things like: the current value of a text input, whether a popover is visible, current csrf token, input validation errors. Signals can be controlled on the client via expressions, or from the backend via `merge-signals`. + +#### Signals in fragments should be declared __ifmissing + +Because signals are only being used to represent ephemeral client state that means they can only be initialised by fragments and they can only be changed via expressions on the client or from the server via `merge-signals` in an action. Signals in fragments should be declared `__ifmissing` unless they are "view only". + +#### View only signals + +View only signals, are signals that can only be changed by the server. These should not be declared `__ifmissing` instead they should be made "local" by starting their key with an `_` this prevents the client from sending them up to the server. + +#### Actions should not update the view themselves directly + +Actions should not update the view via merge fragments. This is because the changes they make would get overwritten on the next `render-fn` that pushes a new view down the updates SSE connection. However, they can still be used to update signals as those won't be changed by fragment merges. This allows you to do things like validation on the server. + +#### Stateless + +The only way for actions to affect the view returned by the `render-fn` running in a connection is via the database. The ensures CQRS. This means there is no connection state that needs to be persisted or maintained (so missed events and shutdowns/deploys will not lead to lost state). Even when you are running in a single process there is no way for an action (command) to communicate with/affect a view render (query) without going through the database. + +#### CQRS + +- Actions modify the database and return a 204 or a 200 if they `merge-signals`. +- Render functions re-render when the database changes and send an update down the updates SSE connection. + +#### Work sharing (caching) + +Work sharing is the term I'm using for sharing renders between connected users. This can be useful when a lot of connected users share the same view. For example a leader board, game board, presence indicator etc. It ensures the work (eg: query and html generation) for that view is only done once regardless of the number of connected users. + +There's a lot of ways you can do this. I've settled on a simple cache that gets invalidate when a `:refresh-event` is fired. This means the cache is invalidated at most every X msec (determined by `:max-refresh-ms`) and only if the db state has changed. + +To add something to the cache wrap the function in the `cache` higher order function. + +#### Batching + +Batching pairs really well with CQRS as you have a resolution window, this defines the maximum frequency the view can update, or in other terms the granularity/resolution of the view. Batching can generally be used to improve throughput by batching changes. + +However, there is one downsides with batching to keep in mind and that is you don't get atomic transactions. The transaction move to the batch level, not the transact/insert level. Transaction matter when you are dealing with constraints you want to deal with at the database level, classic example is accounting systems or ledgers where you want to be able to fail an atomic transaction that violates a constraint (like user balance going negative). The problem with batching is that that transaction constraint failure, fails the whole batch not only the transact that was culpable. + +#### Use `data-on-pointerdown/mous` over `data-on-click` + +This is a small one but can make even the slowest of networks feel much snappier. + +## Other Radical choices + +#### No CORS + +By hosting all assets on the same origin we avoid the need for CORS. This avoids additional server round trips and helps reduce latency. + +#### Cookie based sessions + +Hyperlith uses a simple unguessable random uid for managing sessions. This should be used to look up further auth/permission information in the database. + +#### CSRF + +Double submit cookie pattern is used for CSRF. + +#### Rendering an initial shim + +Rather than returning the whole page on initial render and having two render paths, one for initial render and one for subsequent rendering a shell is rendered and then populated when the page connects to the updates endpoint for that page. This has a few advantages: + +- The page will only render dynamic content if the user has javascript and first party cookies enabled. + +- The initial shell page can generated and compressed once. + +- The server only does more work for actual users and less work for link preview crawlers and other bots (that don't support javascript or cookies). + +#### Routing + +Router is a simple map, this means path parameters are not supported use query parameters or body instead. I've found over time that path parameters force you to adopt an arbitrary hierarchy that is often wrong (and place oriented programming). Removing them avoids this and means routing can be simplified to a map and have better performance than a more traditional adaptive radix tree router. + +>๐Ÿ“ Note: The Hyperlith router is completely optional and you can swap it out for reitit if you want to support path params. + +#### Reverse proxy + +Hyperlith is designed to be deployed between a reverse proxy like caddy for handling HTTP2/3 (you want to be using HTTP2/3 with SSE). + +#### Minimal middleware + +Hyperlith doesn't expose middleware and keeps the internal middleware to a minimum. + +#### Minimal dependencies + +Hyperlith tries to keep dependencies to a minimum. diff --git a/deps.edn b/deps.edn index 34cfd5c..21921f9 100644 --- a/deps.edn +++ b/deps.edn @@ -1,22 +1,30 @@ -{:paths ["src" "resources"] - :deps {org.clojure/clojure {:mvn/version "1.12.0"} - org.clojure/data.json {:mvn/version "2.5.1"} - ;; http - http-kit/http-kit - {:git/url "https://github.com/http-kit/http-kit" - :git/sha "76b869fc34536ad0c43afa9a98d971a0fc32c644"} - ;; templating - hiccup/hiccup - {:git/url "https://github.com/weavejester/hiccup" - :git/sha "24d55d1c7c1fc3c249395d261eb723c381843bc0"} - ;; db - datalevin/datalevin {:mvn/version "0.9.17"}} - :aliases {:dev {:extra-paths ["dev"] - :jvm-opts - ["-Duser.timezone=UTC" - "-XX:+UseZGC" - ;; ZGenerational will be the default in future - ;; so this won't need to be specified - "-XX:+ZGenerational" - "--add-opens=java.base/java.nio=ALL-UNNAMED" - "--add-opens=java.base/sun.nio.ch=ALL-UNNAMED"]}}} +{:paths ["src" "resources"] + :deps + {org.clojure/clojure {:mvn/version "1.12.0"} + org.clojure/data.json {:mvn/version "2.5.1"} + org.clojure/core.async {:mvn/version "1.7.701"} + + ;; HTTP + http-kit/http-kit {:mvn/version "2.9.0-beta1"} + + ;; COMPRESSION + com.aayushatharva.brotli4j/brotli4j {:mvn/version "1.18.0"} + ;; Assumes you deploy uberjar on linux x86_64 + com.aayushatharva.brotli4j/native-linux-x86_64 {:mvn/version "1.18.0"} + io.netty/netty-buffer {:mvn/version "4.1.119.Final"} + + ;; TEMPLATING + dev.onionpancakes/chassis {:mvn/version "1.0.365"} + + ;; SQLITE + andersmurphy/sqlite4clj + {:git/url "https://github.com/andersmurphy/sqlite4clj" + :git/sha "c3083c0448cd0cedb4d6b0a497f3dea05334a2f4"} + com.github.seancorfield/honeysql {:mvn/version "2.7.1295"}} + :aliases + {:dev + {:extra-paths ["dev"] + :extra-deps + {com.aayushatharva.brotli4j/native-osx-x86_64 {:mvn/version "1.18.0"} + com.aayushatharva.brotli4j/native-osx-aarch64 {:mvn/version "1.18.0"} + com.aayushatharva.brotli4j/native-linux-aarch64 {:mvn/version "1.18.0"}}}}} diff --git a/dev/example/main.clj b/dev/example/main.clj deleted file mode 100644 index 02731d6..0000000 --- a/dev/example/main.clj +++ /dev/null @@ -1,55 +0,0 @@ -(ns example.main - (:gen-class) - (:require [hyperlith.core :as h] - [clojure.string :as str] - [example.schema :refer [schema]])) - -(defn get-messages [db] - (h/q '[:find ?id ?content ?created-at - :where - [?m :message/id ?id] - [?m :message/content ?content] - [?m :db/created-at ?created-at] - :order-by [?created-at :desc] - :limit 100] - @db)) - -(defn render-home [{:keys [db] :as _req}] - (h/html-str - [:main#morph - [:div - (for [[id content] (get-messages db)] - [:p {:id id} content])] - [:input {:type "text" :data-bind "message"}] - [:button - {:data-on-click "@post('/send'); $message = ''"} "send"]])) - -(defn action-send-message [{:keys [sid db] {:keys [message]} :body}] - (when-not (str/blank? message) - (h/transact! db - [{:user/sid sid} - {:message/id (h/new-uid) - :message/user [:user/sid sid] - :message/content message}]))) - -(def routes - {[:get "/"] (h/shim-handler {:path "/"}) - [:post "/updates"] (h/render-handler #'render-home) - [:post "/send"] (h/action-handler #'action-send-message)}) - -(defn -main [& _] - (h/start-app - {:routes routes - :schema schema - :csrf-secret "fb1704df2b3484223cb5d2a79bf06a508311d8d0f03c68e724d555b6b605966d0ebb8dc54615f8d080e5fa062bd3b5bce5b6ba7ded23333bbd55deea3149b9d5"})) - -(comment - (def server (-main)) - ;; (clojure.java.browse/browse-url "http://localhost:8080/") - - ;; stop server - (let [stop (server :stop)] (stop)) - - ;; query outside of handler - (get-messages (:db-conn server)) - ,) diff --git a/dev/example/schema.clj b/dev/example/schema.clj deleted file mode 100644 index 362b774..0000000 --- a/dev/example/schema.clj +++ /dev/null @@ -1,17 +0,0 @@ -(ns example.schema) - -(def schema - (merge ;; Using merge we can define each logical entity separately - #:user - {:sid {:db/unique :db.unique/identity - :db/valueType :db.type/string - :db/cardinality :db.cardinality/one}} - #:message - {:id {:db/unique :db.unique/identity - :db/valueType :db.type/string - :db/cardinality :db.cardinality/one} - :user {:db/valueType :db.type/ref - :db/cardinality :db.cardinality/one} - :content {:db/valueType :db.type/string - :db/cardinality :db.cardinality/one - :db/fulltext true}})) diff --git a/dev/scratch.clj b/dev/scratch.clj new file mode 100644 index 0000000..ed93d11 --- /dev/null +++ b/dev/scratch.clj @@ -0,0 +1,2 @@ +(ns scratch + (:require [hyperlith.core :as h])) diff --git a/examples/README.md b/examples/README.md new file mode 100644 index 0000000..faffc6c --- /dev/null +++ b/examples/README.md @@ -0,0 +1,57 @@ +# Server setup and management + +## Add a record to DNS + +Add an A record @ which points to the IPV4 address of the VPS. For IPV6 add a AAAA record @ which points to the IPV6 address of the VPS. + +## Initial server setup + +Move the setup script to the server: + +```bash +scp server-setup.sh root@example.andersmurphy.com: +``` + +ssh into server as root: + +```bash +ssh root@example.andersmurphy.com +``` + +run bash script: + +```bash +bash server-setup.sh +``` + +follow instructions. + +## Caddy service + +Check status: + +```bash +systemctl status caddy +``` + +Reload config without downtime. + +```bash +systemctl reload caddy +``` + +Docs: https://caddyserver.com/docs/running#using-the-service + +## Useful systemd commands + +Check status of service. + +```bash +systemctl status app.service +``` + +Restart service manually: + +```bash +systemctl restart app.service +``` diff --git a/examples/billion_checkboxes/README.md b/examples/billion_checkboxes/README.md new file mode 100644 index 0000000..c81afbb --- /dev/null +++ b/examples/billion_checkboxes/README.md @@ -0,0 +1,34 @@ +## Build JAR. + +```bash +clojure -Srepro -T:build uber +``` + +## Run jar locally + +``` +java -Dclojure.server.repl="{:port 5555 :accept clojure.core.server/repl}" -jar target/app.jar -m app.main -Duser.timezone=UTC -XX:+UseZGC -XX:+ZGenerational +``` + +## Deploy + +Move JAR to server (this will trigger a service restart). + +```bash +scp target/app.jar root@checkboxes.andersmurphy.com:/home/app/ +``` + +## After deploying first jar + +Optional: the first time you move the jar onto the server you will need to reboot to trigger/test systemd is working correctly. + +``` +ssh root@checkboxes.andersmurphy.com "reboot" +``` + +## SSH into repl + +```bash +ssh root@checkboxes.andersmurphy.com "nc localhost:5555" +``` + diff --git a/examples/billion_checkboxes/build.clj b/examples/billion_checkboxes/build.clj new file mode 100644 index 0000000..77a679a --- /dev/null +++ b/examples/billion_checkboxes/build.clj @@ -0,0 +1,25 @@ +(ns build + (:require [clojure.tools.build.api :as b])) + +(def lib 'app) +(def class-dir "target/classes") +(def basis (delay (b/create-basis {:project "deps.edn"}))) +(def uber-file (format "target/%s.jar" (name lib))) + +(defn clean [_] (b/delete {:path "target"})) + +(defn uber + [_] + (clean nil) + (b/copy-dir {:src-dirs ["src" "resources"] :target-dir class-dir}) + (b/compile-clj {:basis @basis + :ns-compile '[app.main] + :src-dirs ["src"] + :class-dir class-dir + :java-opts ;; needed for coffi + ["--enable-native-access=ALL-UNNAMED"]}) + (b/uber {:class-dir class-dir + :uber-file uber-file + :basis @basis + :main 'app.main + :manifest {"Enable-Native-Access" "ALL-UNNAMED"}})) diff --git a/examples/billion_checkboxes/deps.edn b/examples/billion_checkboxes/deps.edn new file mode 100644 index 0000000..67f97e4 --- /dev/null +++ b/examples/billion_checkboxes/deps.edn @@ -0,0 +1,11 @@ +{:paths ["src" "resources"] + :deps {org.clojure/clojure {:mvn/version "1.12.0"} + hyperlith/hyperlith {:local/root "../../../hyperlith"}} + :aliases {:dev {:jvm-opts + ["--enable-native-access=ALL-UNNAMED" + "-Duser.timezone=UTC" + "-XX:+UseZGC" + "-XX:+ZGenerational"]} + :build {:deps {io.github.clojure/tools.build + {:git/tag "v0.10.5" :git/sha "2a21b7a"}} + :ns-default build}}} diff --git a/examples/billion_checkboxes/resources/.env.edn b/examples/billion_checkboxes/resources/.env.edn new file mode 100644 index 0000000..0f01b6f --- /dev/null +++ b/examples/billion_checkboxes/resources/.env.edn @@ -0,0 +1,3 @@ +{;; WARNING: .env.edn should not normally be committed to source control + ;; but is here as an example. + :csrf-secret "fb1704df2b3484223cb5d2a79bf06a508311d8d0f03c68e724d555b6b605966d0ebb8dc54615f8d080e5fa062bd3b5bce5b6ba7ded23333bbd55deea3149b9d5"} diff --git a/examples/billion_checkboxes/src/app/main.clj b/examples/billion_checkboxes/src/app/main.clj new file mode 100644 index 0000000..ac9bc75 --- /dev/null +++ b/examples/billion_checkboxes/src/app/main.clj @@ -0,0 +1,386 @@ +(ns app.main + (:gen-class) + (:require [hyperlith.core :as h] + [hyperlith.extras.sqlite :as d])) + +;; (* 198 198 16 16) 10 036 224 +;; (* 625 625 16 16) 100 000 000 +;; (* 1977 1977 16 16) 1 000 583 424 + +(def board-size 1977 #_625 #_198) +(def chunk-size 16) +(def board-size-px (* 3 3 120000)) + +(def states + [0 1 2 3 4 5 6]) + +(def state->class + ["none" "r" "b" "g" "o" "f" "p"]) + +(def css + (let [black :black + board-size-px (str board-size-px "px")] + (h/static-css + [["*, *::before, *::after" + {:box-sizing :border-box + :margin 0 + :padding 0}] + + [:html + {:font-family "Arial, Helvetica, sans-serif" + :font-size :18px + :color black}] + + [:.main + {:height :100dvh + :margin-inline :auto + :padding-block :2dvh + :display :flex + :width "min(100% - 2rem , 42rem)" + :gap :5px + :flex-direction :column}] + + [:.view + {:overflow :scroll + :overflow-anchor :none + :width "min(100% - 2rem , 42rem)" + :aspect-ratio "1/1"}] + + [:.board + {:background :white + :width board-size-px + :display :grid + :aspect-ratio "1/1" + :gap :10px + :grid-template-rows (str "repeat(" board-size ", 1fr)") + :grid-template-columns (str "repeat(" board-size ", 1fr)")}] + + [:.chunk + {:background :white + :display :grid + :gap :10px + :grid-template-rows (str "repeat(" chunk-size ", 1fr)") + :grid-template-columns (str "repeat(" chunk-size ", 1fr)")}] + + ["input[type=\"checkbox\"]" + {:appearance :none + :margin 0 + :font :inherit + :color :currentColor + :border "0.15em solid currentColor" + :border-radius :0.15em + :display :grid + :place-content :center}] + + ["input[type=\"checkbox\"]:checked::before" + {:content "\"\"" + :width "0.80em" + :height "0.80em" + :clip-path "polygon(14% 44%, 0 65%, 50% 100%, 100% 16%, 80% 0%, 43% 62%)" + :box-shadow "inset 1em 1em white"}] + + [:.pop + {:transform "scale(0.8)" + :transition "scale 0.6s ease"}] + + [:.r {:background-color :red}] + [:.b {:background-color :blue}] + [:.g {:background-color :green}] + [:.o {:background-color :orange}] + [:.f {:background-color :fuchsia}] + [:.p {:background-color :purple}]]))) + +(defn Checkbox [local-id state] + (let [checked (not= state 0) + color-class (state->class state)] + (h/html + [:input + {:class (when checked color-class) + :type "checkbox" + :checked checked + :data-id local-id}]))) + +(defn chunk-id->xy [chunk-id] + [(rem chunk-id board-size) + (quot chunk-id board-size)]) + +(defn xy->chunk-id [x y] + (+ x (* y board-size))) + +(defn xy->chunk-ids [x y] + (-> (for [x (range x (+ x 3)) + y (range y (+ y 3))] + (xy->chunk-id x y)) + vec)) + +(defn Chunk [chunk-id chunk-cells] + (let [[x y] (chunk-id->xy chunk-id) + x (inc x) + y (inc y)] + (h/html + [:div.chunk + {:id chunk-id + :data-id chunk-id + :style {:grid-column x :grid-row y}} + (into [] + (map-indexed (fn [local-id box] (Checkbox local-id box))) + chunk-cells)]))) + +(defn UserView [{:keys [x y] :or {x 0 y 0}} db] + (->> (d/q db + {:select [:chunk-id [[:json_group_array :state] :chunk-cells]] + :from :cell + :where [:in :chunk-id (xy->chunk-ids x y)] + :group-by [:chunk-id]}) + (mapv (fn [[chunk-id chunk-cells]] + (Chunk chunk-id (h/json->edn chunk-cells)))))) + +(def mouse-down-js + (str + "evt.target.parentElement.dataset.id &&" + "(evt.target.classList.add('pop')," + "@post(`/tap?id=${evt.target.dataset.id}&pid=${evt.target.parentElement.dataset.id}`))")) + +(defn Board [content] + (h/html + [:div#board.board + {:data-on-mousedown mouse-down-js} + content])) + +(defn scroll-offset-js [n] + (str "Math.round((" n "/" board-size-px ")*" board-size "-1)")) + +(def on-scroll-js + (str + "let x = " (scroll-offset-js "el.scrollLeft") ";" + "let y = " (scroll-offset-js "el.scrollTop") ";" + "let change = x !== $x || y !== $y;" + "$x = x; $y = y;" + "change && @post(`/scroll`)")) + +(defn render-home [{:keys [db sid tab tabid first-render] :as _req}] + (let [user (get-in @tab [sid tabid] tab) + board (Board (UserView user db))] + (if first-render + (h/html + [:link#css {:rel "stylesheet" :type "text/css" :href (css :path)}] + [:main#morph.main {:data-signals-x "0" :data-signals-y "0"} + [:div#view.view + {:data-on-scroll__throttle.100ms.trail.noleading on-scroll-js} + board] + [:h1 "One Billion Checkboxes"] + [:p "(actually 1,000,583,424)"] + [:p "Built with โค๏ธ using " + [:a {:href "https://clojure.org/"} "Clojure"] + " and " + [:a {:href "https://data-star.dev"} "Datastar"] + "๐Ÿš€"] + [:p "Source code can be found " + [:a {:href "https://github.com/andersmurphy/hyperlith/blob/master/examples/billion_checkboxes/src/app/main.clj" } "here"]]]) + board))) + +(defn action-tap-cell + [{:keys [sid tx-batch!] + {:strs [id pid]} :query-params}] + (when (and id pid) + (let [user-color (h/modulo-pick (subvec states 1) sid) + cell-id (int (parse-long id)) + chunk-id (int (parse-long pid))] + (tx-batch! + (fn action-tap-cell-thunk [db] + (let [[checks] (d/q db {:select [:checks] + :from :session + :where [:= :id sid]})] + (if checks + (d/q db {:update :session + :set {:checks (inc checks)} + :where [:= :id sid]}) + (d/q db {:insert-into :session + :values [{:id sid :checks 1}]}))) + (let [[state] (d/q db {:select [:state] + :from :cell + :where + [:and + [:= :chunk-id chunk-id] + [:= :cell-id cell-id]]}) + new-state (if (= 0 state) user-color 0)] + (d/q db {:update :cell + :set {:state new-state} + :where [:and + [:= :chunk-id chunk-id] + [:= :cell-id cell-id]]}))))))) + +(defn action-scroll [{:keys [sid tabid tab] {:keys [x y]} :body}] + (swap! tab + (fn [snapshot] + (-> snapshot + (assoc-in [sid tabid :x] (max (int x) 0)) + (assoc-in [sid tabid :y] (max (int y) 0)))))) + +(def default-shim-handler + (h/shim-handler + (h/html + [:link#css {:rel "stylesheet" :type "text/css" :href (css :path)}] + [:title nil "One billion checkboxes"] + [:meta {:content "So many checkboxes" :name "description"}]))) + +(def router + (h/router + {[:get (css :path)] (css :handler) + [:get "/"] default-shim-handler + [:post "/"] (h/render-handler #'render-home + {:br-window-size 19}) + [:post "/scroll"] (h/action-handler #'action-scroll) + [:post "/tap"] (h/action-handler #'action-tap-cell)})) + +(defn build-chunk [x y] + (mapv (fn [c] + {:chunk_id (xy->chunk-id x y) + :cell_id c + :state 0}) + (range (* chunk-size chunk-size)))) + +(defn initial-board-db-state! [db] + (let [board-range (range board-size)] + (d/with-write-tx [db db] + (run! + (fn [y] + (run! (fn [x] + (d/q db + {:insert-into :cell + :values (build-chunk x y)})) + board-range) + (print ".") (flush)) + board-range))) + nil) + +(defn migrations [db] + ;; Note: all this code must be idempotent + + ;; Create tables + (println "Running migrations...") + (d/q db + "CREATE TABLE IF NOT EXISTS cell(chunk_id INTEGER, cell_id INTEGER, state INTEGER, PRIMARY KEY (chunk_id, cell_id)) WITHOUT ROWID") + (d/q db + "CREATE TABLE IF NOT EXISTS session(id TEXT PRIMARY KEY, checks INTEGER) WITHOUT ROWID") + ;; Populate checkboxes + (when-not (d/q db {:select [:cell-id] :from :cell :limit 1}) + (initial-board-db-state! db))) + +(defn ctx-start [] + (let [tab-state_ (atom {:users {}}) + {:keys [db-write db-read]} + (d/init-db! "database.db" + {:pool-size 4 + :pragma {:foreign_keys false}})] + ;; Run migrations + (migrations db-write) + ;; Watch tab state + (add-watch tab-state_ :refresh-on-change + (fn [_ _ _ _] (h/refresh-all!))) + {:tab tab-state_ + :db db-read + :db-read db-read + :db-write db-write + :tx-batch! (h/batch! ;; TODO: add error handling to batch + (fn [thunks] + #_{:clj-kondo/ignore [:unresolved-symbol]} + (d/with-write-tx [db db-write] + (run! (fn [thunk] (thunk db)) thunks)) + (h/refresh-all!)) + {:run-every-ms 100})})) + +(defn ctx-stop [ctx] + (.close (:db-write ctx)) + (.close (:db-read ctx))) + +(defn -main [& _] + (h/start-app + {:router #'router + :max-refresh-ms 100 + :ctx-start ctx-start + :ctx-stop ctx-stop + :csrf-secret (h/env :csrf-secret) + :on-error (fn [_ctx {:keys [_req error]}] + (let [{:keys [cause trace type]} error] + (println "") + (println type) + (println cause) + (println "") + (run! println trace)) + (flush))})) + +;; Refresh app when you re-eval file +(h/refresh-all!) + +(comment + (do (-main) nil) + ;; (clojure.java.browse/browse-url "http://localhost:8080/") + + ;; stop server + (((h/get-app) :stop)) + + (def db (-> (h/get-app) :ctx :db)) + + ,) + +(comment + (def db (-> (h/get-app) :ctx :db)) + + (UserView {:x 1 :y 1} db) + + ;; Execution time mean : 456.719068 ms + ;; Execution time mean : 218.760262 ms + (user/bench + (->> (mapv + (fn [n] + (future + (let [n (mod n board-size)] + (UserView {:x n :y n} db)))) + (range 0 4000)) + (run! (fn [x] @x)))) + + ;; On server test + (time ;; simulate 1000 concurrent renders + (->> (mapv + (fn [n] + (future (UserView {:x n :y n} db))) + (range 0 1000)) + (run! (fn [x] @x)))) + + ;; (user/bench (do (UserView {:x 1 :y 1} db) nil)) + + (d/pragma-check db) + + (d/q db {:select [[[:count :*]]] :from :session}) + (d/q db {:select [[[:sum :checks]]] :from :session}) + (d/q db {:select [:checks] :from :session + :order-by [[:checks :desc]]}) + + (d/table-info db :cell) + (d/table-list db) + + (user/bench ;; Execution time mean : 455.139383 ยตs + (d/q db + ["SELECT CAST(chunk_id AS TEXT), CAST(state AS TEXT) FROM cell WHERE chunk_id IN (?, ?, ?, ?, ?, ?, ?, ?, ?)" + 1978 3955 5932 1979 3956 5933 1980 3957 5934])) + + ,) + +(comment + (user/bench + (d/q db + ["SELECT chunk_id, JSON_GROUP_ARRAY(state) AS chunk_cells FROM cell WHERE chunk_id IN (?, ?, ?, ?, ?, ?, ?, ?, ?) GROUP BY chunk_id" 1978 3955 5932 1979 3956 5933 1980 3957 5934])) + + (def tab-state (-> (h/get-app) :ctx :tab)) + + (count @tab-state) + + (def db-write (-> (h/get-app) :ctx :db-write)) + + ;; Free up space (slow) + ;; (time (d/q db-write "VACUUM")) + + ,) + +;; TODO: make scroll bars always visible diff --git a/examples/chat_atom/README.md b/examples/chat_atom/README.md new file mode 100644 index 0000000..880858a --- /dev/null +++ b/examples/chat_atom/README.md @@ -0,0 +1,34 @@ +## Build JAR. + +```bash +clojure -Srepro -T:build uber +``` + +## Run jar locally + +``` +java -Dclojure.server.repl="{:port 5555 :accept clojure.core.server/repl}" -jar target/app.jar -m app.main -Duser.timezone=UTC -XX:+UseZGC -XX:+ZGenerational +``` + +## Deploy + +Move JAR to server (this will trigger a service restart). + +```bash +scp target/app.jar root@example.andersmurphy.com:/home/app/ +``` + +## After deploying first jar + +Optional: the first time you move the jar onto the server you will need to reboot to trigger/test systemd is working correctly. + +``` +ssh root@example.andersmurphy.com "reboot" +``` + +## SSH into repl + +```bash +ssh root@example.andersmurphy.com "nc localhost:5555" +``` + diff --git a/examples/chat_atom/build.clj b/examples/chat_atom/build.clj new file mode 100644 index 0000000..13aa5c3 --- /dev/null +++ b/examples/chat_atom/build.clj @@ -0,0 +1,22 @@ +(ns build + (:require [clojure.tools.build.api :as b])) + +(def lib 'app) +(def class-dir "target/classes") +(def basis (delay (b/create-basis {:project "deps.edn"}))) +(def uber-file (format "target/%s.jar" (name lib))) + +(defn clean [_] (b/delete {:path "target"})) + +(defn uber + [_] + (clean nil) + (b/copy-dir {:src-dirs ["src" "resources"] :target-dir class-dir}) + (b/compile-clj {:basis @basis + :ns-compile '[app.main] + :src-dirs ["src"] + :class-dir class-dir}) + (b/uber {:class-dir class-dir + :uber-file uber-file + :basis @basis + :main 'app.main})) diff --git a/examples/chat_atom/deps.edn b/examples/chat_atom/deps.edn new file mode 100644 index 0000000..62af504 --- /dev/null +++ b/examples/chat_atom/deps.edn @@ -0,0 +1,6 @@ +{:paths ["src" "resources"] + :deps {org.clojure/clojure {:mvn/version "1.12.0"} + hyperlith/hyperlith {:local/root "../../../hyperlith"}} + :aliases {:build {:deps {io.github.clojure/tools.build + {:git/tag "v0.10.5" :git/sha "2a21b7a"}} + :ns-default build}}} diff --git a/examples/chat_atom/resources/.env.edn b/examples/chat_atom/resources/.env.edn new file mode 100644 index 0000000..0f01b6f --- /dev/null +++ b/examples/chat_atom/resources/.env.edn @@ -0,0 +1,3 @@ +{;; WARNING: .env.edn should not normally be committed to source control + ;; but is here as an example. + :csrf-secret "fb1704df2b3484223cb5d2a79bf06a508311d8d0f03c68e724d555b6b605966d0ebb8dc54615f8d080e5fa062bd3b5bce5b6ba7ded23333bbd55deea3149b9d5"} diff --git a/examples/chat_atom/src/app/main.clj b/examples/chat_atom/src/app/main.clj new file mode 100644 index 0000000..c3e10ce --- /dev/null +++ b/examples/chat_atom/src/app/main.clj @@ -0,0 +1,96 @@ +(ns app.main + (:gen-class) + (:require [clojure.pprint :as pprint] + [clojure.string :as str] + [hyperlith.core :as h])) + +(def css + (h/static-css + [["*, *::before, *::after" + {:box-sizing :border-box + :margin 0 + :padding 0}] + + [:.main + {:height :100dvh + :width "min(100% - 2rem , 40rem)" + :margin-inline :auto + :padding-block :2dvh + :overflow-y :scroll + :scrollbar-width :none + :display :flex + :gap :3px + :flex-direction :column-reverse}] + + [:.chat + {:display :flex + :flex-direction :column}]])) + +(defn get-messages [db] + (reverse (@db :messages))) + +(def messages + (h/cache + (fn [db] + (for [[id content] (get-messages db)] + [:p {:id id} content])))) + +(defn render-home [{:keys [db] :as _req}] + (h/html + [:link#css {:rel "stylesheet" :type "text/css" :href (css :path)}] + [:main#morph.main + [:div.chat + [:input {:type "text" :data-bind "message"}] + [:button + {:data-on-click "@post('/send')"} "send"]] + (messages db)])) + +(defn action-send-message [{:keys [_sid db] {:keys [message]} :body}] + (when-not (str/blank? message) + (swap! db update :messages conj [(h/new-uid) message]) + (h/signals {:message ""}))) + +;; Allows for shim handler to be reused across shim routes +(def default-shim-handler + (h/shim-handler + (h/html + [:link#css {:rel "stylesheet" :type "text/css" :href (css :path)}] + [:title nil "Chat"] + [:meta {:content "Chat app" :name "description"}]))) + +(def router + (h/router + {[:get (css :path)] (css :handler) + [:get "/"] default-shim-handler + [:post "/"] (h/render-handler #'render-home) + [:post "/send"] (h/action-handler #'action-send-message)})) + +(defn ctx-start [] + (let [db_ (atom {:messages []})] + (add-watch db_ :refresh-on-change (fn [& _] (h/refresh-all!))) + {:db db_})) + +(defn -main [& _] + (h/start-app + {:router #'router + :max-refresh-ms 100 + :ctx-start ctx-start + :ctx-stop (fn [_state] nil) + :csrf-secret (h/env :csrf-secret) + :on-error (fn [_ctx {:keys [req error]}] + (pprint/pprint req) + (pprint/pprint error))})) + +;; Refresh app when you re-eval file +(h/refresh-all!) + +(comment + (-main) + ;; (clojure.java.browse/browse-url "http://localhost:8080/") + + ;; stop server + (((h/get-app) :stop)) + + ;; query outside of handler + (get-messages (-> (h/get-app) :ctx :db)) + ,) diff --git a/examples/communtative_connected_count/README.md b/examples/communtative_connected_count/README.md new file mode 100644 index 0000000..880858a --- /dev/null +++ b/examples/communtative_connected_count/README.md @@ -0,0 +1,34 @@ +## Build JAR. + +```bash +clojure -Srepro -T:build uber +``` + +## Run jar locally + +``` +java -Dclojure.server.repl="{:port 5555 :accept clojure.core.server/repl}" -jar target/app.jar -m app.main -Duser.timezone=UTC -XX:+UseZGC -XX:+ZGenerational +``` + +## Deploy + +Move JAR to server (this will trigger a service restart). + +```bash +scp target/app.jar root@example.andersmurphy.com:/home/app/ +``` + +## After deploying first jar + +Optional: the first time you move the jar onto the server you will need to reboot to trigger/test systemd is working correctly. + +``` +ssh root@example.andersmurphy.com "reboot" +``` + +## SSH into repl + +```bash +ssh root@example.andersmurphy.com "nc localhost:5555" +``` + diff --git a/examples/communtative_connected_count/build.clj b/examples/communtative_connected_count/build.clj new file mode 100644 index 0000000..13aa5c3 --- /dev/null +++ b/examples/communtative_connected_count/build.clj @@ -0,0 +1,22 @@ +(ns build + (:require [clojure.tools.build.api :as b])) + +(def lib 'app) +(def class-dir "target/classes") +(def basis (delay (b/create-basis {:project "deps.edn"}))) +(def uber-file (format "target/%s.jar" (name lib))) + +(defn clean [_] (b/delete {:path "target"})) + +(defn uber + [_] + (clean nil) + (b/copy-dir {:src-dirs ["src" "resources"] :target-dir class-dir}) + (b/compile-clj {:basis @basis + :ns-compile '[app.main] + :src-dirs ["src"] + :class-dir class-dir}) + (b/uber {:class-dir class-dir + :uber-file uber-file + :basis @basis + :main 'app.main})) diff --git a/examples/communtative_connected_count/deps.edn b/examples/communtative_connected_count/deps.edn new file mode 100644 index 0000000..62af504 --- /dev/null +++ b/examples/communtative_connected_count/deps.edn @@ -0,0 +1,6 @@ +{:paths ["src" "resources"] + :deps {org.clojure/clojure {:mvn/version "1.12.0"} + hyperlith/hyperlith {:local/root "../../../hyperlith"}} + :aliases {:build {:deps {io.github.clojure/tools.build + {:git/tag "v0.10.5" :git/sha "2a21b7a"}} + :ns-default build}}} diff --git a/examples/communtative_connected_count/resources/.env.edn b/examples/communtative_connected_count/resources/.env.edn new file mode 100644 index 0000000..0f01b6f --- /dev/null +++ b/examples/communtative_connected_count/resources/.env.edn @@ -0,0 +1,3 @@ +{;; WARNING: .env.edn should not normally be committed to source control + ;; but is here as an example. + :csrf-secret "fb1704df2b3484223cb5d2a79bf06a508311d8d0f03c68e724d555b6b605966d0ebb8dc54615f8d080e5fa062bd3b5bce5b6ba7ded23333bbd55deea3149b9d5"} diff --git a/examples/communtative_connected_count/src/app/main.clj b/examples/communtative_connected_count/src/app/main.clj new file mode 100644 index 0000000..c4d09cc --- /dev/null +++ b/examples/communtative_connected_count/src/app/main.clj @@ -0,0 +1,79 @@ +(ns app.main + (:gen-class) + (:require [hyperlith.core :as h])) + +(def css + (h/static-css + [["*, *::before, *::after" + {:box-sizing :border-box + :margin 0 + :padding 0}] + + [:html + {:font-family "Arial, Helvetica, sans-serif"}] + + [:.main + {:height :100dvh + :width "min(100% - 2rem , 40rem)" + :margin-inline :auto + :padding-block :2dvh + :display :grid + :place-items :center}] + + [:.counter + {:text-align :center + :font-size :50px}]])) + +(defn render-home [{:keys [connected-counter] :as _req}] + (h/html + [:link#css {:rel "stylesheet" :type "text/css" :href (css :path)}] + [:main#morph.main + [:div + [:p nil (str "connected users")] + [:p.counter nil @connected-counter]]])) + +(def default-shim-handler + (h/shim-handler + (h/html + [:link#css {:rel "stylesheet" :type "text/css" :href (css :path)}]))) + +(def router + (h/router + {[:get (css :path)] (css :handler) + [:get "/"] default-shim-handler + [:post "/"] (h/render-handler #'render-home + :on-open + (fn [{:keys [connected-counter]}] + (dosync (commute connected-counter inc))) + :on-close + (fn [{:keys [connected-counter]}] + (dosync (commute connected-counter dec))))})) + +(defn ctx-start [] + ;; By using ref and commute to track user count allows for higher + ;; level of concurrency. + (let [connected-counter_ (ref 0)] + (add-watch connected-counter_ :refresh-on-change + (fn [& _] (h/refresh-all!))) + {:connected-counter connected-counter_})) + +(defn -main [& _] + (h/start-app + {:router #'router + :max-refresh-ms 100 + :ctx-start ctx-start + :ctx-stop (fn [_db] nil) + :csrf-secret (h/env :csrf-secret)})) + +;; Refresh app when you re-eval file +(h/refresh-all!) + +(comment + (-main) + ;; (clojure.java.browse/browse-url "http://localhost:8080/") + + ;; stop server + (((h/get-app) :stop)) + + + ,) diff --git a/examples/drag_drop/README.md b/examples/drag_drop/README.md new file mode 100644 index 0000000..880858a --- /dev/null +++ b/examples/drag_drop/README.md @@ -0,0 +1,34 @@ +## Build JAR. + +```bash +clojure -Srepro -T:build uber +``` + +## Run jar locally + +``` +java -Dclojure.server.repl="{:port 5555 :accept clojure.core.server/repl}" -jar target/app.jar -m app.main -Duser.timezone=UTC -XX:+UseZGC -XX:+ZGenerational +``` + +## Deploy + +Move JAR to server (this will trigger a service restart). + +```bash +scp target/app.jar root@example.andersmurphy.com:/home/app/ +``` + +## After deploying first jar + +Optional: the first time you move the jar onto the server you will need to reboot to trigger/test systemd is working correctly. + +``` +ssh root@example.andersmurphy.com "reboot" +``` + +## SSH into repl + +```bash +ssh root@example.andersmurphy.com "nc localhost:5555" +``` + diff --git a/examples/drag_drop/build.clj b/examples/drag_drop/build.clj new file mode 100644 index 0000000..13aa5c3 --- /dev/null +++ b/examples/drag_drop/build.clj @@ -0,0 +1,22 @@ +(ns build + (:require [clojure.tools.build.api :as b])) + +(def lib 'app) +(def class-dir "target/classes") +(def basis (delay (b/create-basis {:project "deps.edn"}))) +(def uber-file (format "target/%s.jar" (name lib))) + +(defn clean [_] (b/delete {:path "target"})) + +(defn uber + [_] + (clean nil) + (b/copy-dir {:src-dirs ["src" "resources"] :target-dir class-dir}) + (b/compile-clj {:basis @basis + :ns-compile '[app.main] + :src-dirs ["src"] + :class-dir class-dir}) + (b/uber {:class-dir class-dir + :uber-file uber-file + :basis @basis + :main 'app.main})) diff --git a/examples/drag_drop/deps.edn b/examples/drag_drop/deps.edn new file mode 100644 index 0000000..62af504 --- /dev/null +++ b/examples/drag_drop/deps.edn @@ -0,0 +1,6 @@ +{:paths ["src" "resources"] + :deps {org.clojure/clojure {:mvn/version "1.12.0"} + hyperlith/hyperlith {:local/root "../../../hyperlith"}} + :aliases {:build {:deps {io.github.clojure/tools.build + {:git/tag "v0.10.5" :git/sha "2a21b7a"}} + :ns-default build}}} diff --git a/examples/drag_drop/resources/.env.edn b/examples/drag_drop/resources/.env.edn new file mode 100644 index 0000000..0f01b6f --- /dev/null +++ b/examples/drag_drop/resources/.env.edn @@ -0,0 +1,3 @@ +{;; WARNING: .env.edn should not normally be committed to source control + ;; but is here as an example. + :csrf-secret "fb1704df2b3484223cb5d2a79bf06a508311d8d0f03c68e724d555b6b605966d0ebb8dc54615f8d080e5fa062bd3b5bce5b6ba7ded23333bbd55deea3149b9d5"} diff --git a/examples/drag_drop/src/app/main.clj b/examples/drag_drop/src/app/main.clj new file mode 100644 index 0000000..77e2833 --- /dev/null +++ b/examples/drag_drop/src/app/main.clj @@ -0,0 +1,164 @@ +(ns app.main + (:gen-class) + (:require [hyperlith.core :as h])) + +(def css + (h/static-css + [["*, *::before, *::after" + {:box-sizing :border-box + :margin 0 + :padding 0}] + + [:html + {:font-family "Arial, Helvetica, sans-serif" + :overflow :hidden + :background :#212529 + :color :#e9ecef}] + + [:.main + {:margin-top :20px + :display :grid + :place-items :center + :gap :2px}] + + [:.board + {:user-select :none + :-webkit-touch-callout :none + :-webkit-user-select :none + :width :100% + :max-width :500px + :aspect-ratio "1 / 1" + :position :relative}] + + [:.star + {:position :absolute + :touch-action :none + :font-size :30px + :transition "all 0.2s ease-in-out"}] + + [:.dropzone + {:position :absolute + :font-size :30px}] + + [:.counter + {:font-size :16px}] + + [:a {:color :#e9ecef}]])) + +(defn place-stars [db n] + (doseq [_n (range n)] + (let [x (rand-nth (range 0 100 10)) + y (rand-nth (range 0 100 10))] + (swap! db h/assoc-in-if-missing [:stars (str "s" x y)] + {:x x :y y})))) + +(def stars + (h/cache + (fn [db] + (for [[star-id {:keys [x y]}] (:stars @db)] + [:div.star + {:id star-id + :style {:left (str x "%") :top (str y "%")} + :draggable "true" + :data-on-dragstart + "evt.dataTransfer.setData('text/plain', evt.target.id)"} + "โญ"])))) + +(defn render-home [{:keys [db] :as _req}] + (h/html + [:link#css {:rel "stylesheet" :type "text/css" :href (css :path)}] + [:main#morph.main + [:p.counter "DRAG THE STARS TO THE SHIP"] + [:p "(multiplayer co-op)"] + [:div.board nil (stars db) + [:div.dropzone + {:style {:left :55% :top :55%} + :data-on-dragover "evt.preventDefault()" + :data-on-drop + "evt.preventDefault(); @post(`/dropzone?id=${evt.dataTransfer.getData('text/plain')}`)"} + "๐Ÿš€"]] + [:p.counter nil + (str "STARS COLLECTED: " (@db :stars-collected))] + [:a {:href "https://data-star.dev/"} + "Built with โค๏ธ using Datastar"] + [:a {:href "https://github.com/andersmurphy/hyperlith/blob/master/examples/drag_drop/src/app/main.clj"} + "show me the code"]])) + +(defn remove-star [db id] + (-> (update db :stars dissoc id) + (update :stars-collected inc))) + +(defn move-star [db id] + (swap! db assoc-in [:stars id] {:x 55 :y 55}) + (Thread/sleep 250) + (swap! db remove-star id)) + +(defn action-user-move-star-to-dropzone + [{:keys [db] {:strs [id]} :query-params}] + (when id + (move-star db id))) + +(def default-shim-handler + (h/shim-handler + (h/html + ;; Setting the colour here prevents flash on remote stylesheet update + [:style "html {background: #212529}"] + [:link#css {:rel "stylesheet" :type "text/css" :href (css :path)}]))) + +(def router + (h/router + {[:get (css :path)] (css :handler) + [:get "/"] default-shim-handler + [:post "/"] (h/render-handler #'render-home + :on-close + (fn [{:keys [sid db]}] + (swap! db update :cursors dissoc sid))) + [:post "/dropzone"] (h/action-handler #'action-user-move-star-to-dropzone)})) + +(defn ctx-start [] + (let [db_ (atom {:stars-collected 0})] + (place-stars db_ 15) + (add-watch db_ :refresh-on-change + (fn [_ ref _old-state new-state] + (when (empty? (:stars new-state)) + (place-stars ref 15)) + (h/refresh-all!))) + {:db db_})) + +(defn -main [& _] + (h/start-app + {:router #'router + :max-refresh-ms 100 + :ctx-start ctx-start + :ctx-stop (fn [_] nil) + :csrf-secret (h/env :csrf-secret)})) + +;; Refresh app when you re-eval file +(h/refresh-all!) + +(comment + (-main) + ;; (clojure.java.browse/browse-url "http://localhost:8080/") + + ;; stop server + (((h/get-app) :stop)) + + (:db ((h/get-app) :ctx)) + + (place-stars (:db ((h/get-app) :ctx)) 10) + + ,) + +(comment + (def db (:db ((h/get-app) :ctx))) + + (place-stars (:db ((h/get-app) :ctx)) 60) + + (do (mapv + (fn [[k _]] + (action-user-move-star-to-dropzone + {:db db + :query-params {"id" k}})) + (:stars @db)) + nil) + ) diff --git a/examples/game_of_life/README.md b/examples/game_of_life/README.md new file mode 100644 index 0000000..880858a --- /dev/null +++ b/examples/game_of_life/README.md @@ -0,0 +1,34 @@ +## Build JAR. + +```bash +clojure -Srepro -T:build uber +``` + +## Run jar locally + +``` +java -Dclojure.server.repl="{:port 5555 :accept clojure.core.server/repl}" -jar target/app.jar -m app.main -Duser.timezone=UTC -XX:+UseZGC -XX:+ZGenerational +``` + +## Deploy + +Move JAR to server (this will trigger a service restart). + +```bash +scp target/app.jar root@example.andersmurphy.com:/home/app/ +``` + +## After deploying first jar + +Optional: the first time you move the jar onto the server you will need to reboot to trigger/test systemd is working correctly. + +``` +ssh root@example.andersmurphy.com "reboot" +``` + +## SSH into repl + +```bash +ssh root@example.andersmurphy.com "nc localhost:5555" +``` + diff --git a/examples/game_of_life/build.clj b/examples/game_of_life/build.clj new file mode 100644 index 0000000..13aa5c3 --- /dev/null +++ b/examples/game_of_life/build.clj @@ -0,0 +1,22 @@ +(ns build + (:require [clojure.tools.build.api :as b])) + +(def lib 'app) +(def class-dir "target/classes") +(def basis (delay (b/create-basis {:project "deps.edn"}))) +(def uber-file (format "target/%s.jar" (name lib))) + +(defn clean [_] (b/delete {:path "target"})) + +(defn uber + [_] + (clean nil) + (b/copy-dir {:src-dirs ["src" "resources"] :target-dir class-dir}) + (b/compile-clj {:basis @basis + :ns-compile '[app.main] + :src-dirs ["src"] + :class-dir class-dir}) + (b/uber {:class-dir class-dir + :uber-file uber-file + :basis @basis + :main 'app.main})) diff --git a/examples/game_of_life/deps.edn b/examples/game_of_life/deps.edn new file mode 100644 index 0000000..62af504 --- /dev/null +++ b/examples/game_of_life/deps.edn @@ -0,0 +1,6 @@ +{:paths ["src" "resources"] + :deps {org.clojure/clojure {:mvn/version "1.12.0"} + hyperlith/hyperlith {:local/root "../../../hyperlith"}} + :aliases {:build {:deps {io.github.clojure/tools.build + {:git/tag "v0.10.5" :git/sha "2a21b7a"}} + :ns-default build}}} diff --git a/examples/game_of_life/resources/.env.edn b/examples/game_of_life/resources/.env.edn new file mode 100644 index 0000000..0f01b6f --- /dev/null +++ b/examples/game_of_life/resources/.env.edn @@ -0,0 +1,3 @@ +{;; WARNING: .env.edn should not normally be committed to source control + ;; but is here as an example. + :csrf-secret "fb1704df2b3484223cb5d2a79bf06a508311d8d0f03c68e724d555b6b605966d0ebb8dc54615f8d080e5fa062bd3b5bce5b6ba7ded23333bbd55deea3149b9d5"} diff --git a/examples/game_of_life/src/app/game.clj b/examples/game_of_life/src/app/game.clj new file mode 100644 index 0000000..3b14049 --- /dev/null +++ b/examples/game_of_life/src/app/game.clj @@ -0,0 +1,70 @@ +;; This code based of (with some minor modifications): https://github.com/kaepr/game-of-life-cljs/blob/80ff8a16804e03d35d056cc3c64f4d0be9ce301e/src/app/game.cljs#L1C1-L83C6 +(ns app.game) + +(def grid-config + {:name "Square" + :neighbors [[-1 -1] [-1 0] [-1 1] + [0 -1] #_cell [0 1] + [1 -1] [1 0] [1 1]]}) + +(defn dead-cell [] :dead) + +(defn alive-cell [living-neighbors] (rand-nth living-neighbors)) + +(defn alive? [cell] (not= cell :dead)) + +(defn coordinates->index [row col max-cols] + (+ col (* row max-cols))) + +(defn index->coordinates [idx max-cols] + [(quot idx max-cols) (rem idx max-cols)]) + +(defn get-cell [board idx] + (get board idx false)) + +(defn update-cell [board [row col] max-cols cell] + (assoc board (coordinates->index row col max-cols) cell)) + +(defn empty-board [max-rows max-cols] + (vec (repeat (* max-rows max-cols) (dead-cell)))) + +(defn get-neighbors + "Returns all neighbors using 1d based vector indexing." + [neighbors [row col] max-rows max-cols] + (let [valid? (fn [r c] (and (>= r 0) (>= c 0) (< r max-rows) (< c max-cols)))] + (->> neighbors + (map (fn [[dr dc]] + (let [r (+ row dr) + c (+ col dc)] + (when (valid? r c) + (coordinates->index r c max-cols))))) + (filter some?)))) + +(defn cell-transition [cell neighbors-count living-neighbors] + (if (or (and (alive? cell) (or (= neighbors-count 2) (= neighbors-count 3))) + (and (not (alive? cell)) (= neighbors-count 3))) + (alive-cell living-neighbors) + (dead-cell))) + +(defn next-gen-board [{:keys [board max-rows max-cols]}] + (let [next-board (transient board) + size (* max-rows max-cols)] + (dotimes [idx size] + (let [coords (index->coordinates idx max-cols) + cell (get-cell board idx) + neighbors (:neighbors grid-config) + neighbor-cells (get-neighbors neighbors coords max-rows max-cols) + living-neighbors (filter alive? (map #(get-cell board %) + neighbor-cells)) + neighbor-count (count living-neighbors)] + (assoc! next-board idx (cell-transition cell neighbor-count + living-neighbors)))) + (persistent! next-board))) + +(comment + + (empty-board 10 10) + + (next-gen-board {:board (empty-board 10 10) + :max-rows 10 + :max-cols 10})) diff --git a/examples/game_of_life/src/app/main.clj b/examples/game_of_life/src/app/main.clj new file mode 100644 index 0000000..b830034 --- /dev/null +++ b/examples/game_of_life/src/app/main.clj @@ -0,0 +1,209 @@ +(ns app.main + (:gen-class) + (:require [clojure.pprint :as pprint] + [hyperlith.core :as h] + [app.game :as game])) + +(def board-size 50) + +(def colors + [:red :blue :green :orange :fuchsia :purple]) + +(def css + (let [black :black + cell-transition "background 0.6s ease"] + (h/static-css + [["*, *::before, *::after" + {:box-sizing :border-box + :margin 0 + :padding 0}] + + [:html + {:font-family "Arial, Helvetica, sans-serif" + :font-size :18px + :color black}] + + [:.main + {:height :100dvh + :width "min(100% - 2rem , 30rem)" + :margin-inline :auto + :padding-block :2dvh + :display :flex + :gap :5px + :flex-direction :column}] + + [:.board + {:background :white + :width "min(100% - 2rem , 30rem)" + :display :grid + :aspect-ratio "1/1" + :grid-template-rows (str "repeat(" board-size ", 1fr)") + :grid-template-columns (str "repeat(" board-size ", 1fr)")}] + + [:.tile + {:border-bottom "1px solid black" + :border-right "1px solid black"}] + + [:.dead + {:background :white}] + + [:.red + {:background :red + :transition cell-transition}] + [:.blue + {:background :blue + :transition cell-transition}] + [:.green + {:background :green + :transition cell-transition}] + [:.orange + {:background :orange + :transition cell-transition}] + [:.fuchsia + {:background :fuchsia + :transition cell-transition}] + [:.purple + {:background :purple + :transition cell-transition}]]))) + +(def board-state + (h/cache + (fn [db] + (into [] + (comp + (map-indexed + (fn [id color-class] + (let [morph-id (when-not (= :dead color-class) id)] + (h/html + [:div.tile + {:class color-class + :data-id id + :id morph-id}]))))) + (:board db))))) + +(defn board [snapshot] + (let [view (board-state snapshot)] + (h/html + [:div.board + {:data-on-pointerdown "@post(`/tap?id=${evt.target.dataset.id}`)"} + view]))) + +(defn render-home [{:keys [db _sid] :as _req}] + (let [snapshot @db] + (h/html + [:link#css {:rel "stylesheet" :type "text/css" :href (css :path)}] + [:main#morph.main + [:h1 "Game of Life (multiplayer)"] + [:p "Built with โค๏ธ using " + [:a {:href "https://clojure.org/"} "Clojure"] + " and " + [:a {:href "https://data-star.dev"} "Datastar"] + "๐Ÿš€"] + [:p "Source code can be found " + [:a {:href "https://github.com/andersmurphy/hyperlith/blob/master/examples/game_of_life/src/app/main.clj"} "here"]] + (board snapshot)]))) + +(defn render-home-star [{:keys [db _sid] :as _req}] + (let [snapshot @db] + (h/html + [:link#css {:rel "stylesheet" :type "text/css" :href (css :path)}] + [:main#morph.main nil + (board snapshot)]))) + +(defn fill-cell [board color id] + (if ;; crude overflow check + (<= 0 id (dec (* board-size board-size))) + (assoc board id color) + board)) + +(defn fill-cross [db id sid] + (let[user-color (h/modulo-pick colors sid)] + (-> db + (update :board fill-cell user-color (- id board-size)) + (update :board fill-cell user-color (- id 1)) + (update :board fill-cell user-color id) + (update :board fill-cell user-color (+ id 1)) + (update :board fill-cell user-color (+ id board-size))))) + +(defn action-tap-cell [{:keys [sid db] {:strs [id]} :query-params}] + (when id + (swap! db fill-cross (parse-long id) sid))) + +(def default-shim-handler + (h/shim-handler + (h/html + [:link#css {:rel "stylesheet" :type "text/css" :href (css :path)}] + [:title nil "Game of Life"] + [:meta {:content "Conway's Game of Life" :name "description"}]))) + +(defn next-gen-board [current-board] + (game/next-gen-board + {:board current-board + :max-rows board-size + :max-cols board-size})) + +(defn next-generation! [db] + (swap! db update :board next-gen-board)) + +(defn start-game! [db] + (let [running_ (atom true)] + (h/thread + (while @running_ + (Thread/sleep 200) ;; 5 fps + (next-generation! db))) + (fn stop-game! [] (reset! running_ false)))) + +(def router + (h/router + {[:get (css :path)] (css :handler) + [:get "/"] default-shim-handler + [:post "/"] (h/render-handler #'render-home + {:br-window-size 18}) + [:get "/star"] default-shim-handler + [:post "/star"] (h/render-handler #'render-home-star + {:br-window-size 18}) + [:post "/tap"] (h/action-handler #'action-tap-cell)})) + +(defn ctx-start [] + (let [db_ (atom {:board (game/empty-board board-size board-size) + :users {}})] + (add-watch db_ :refresh-on-change + (fn [_ _ old-state new-state] + ;; Only refresh if state has changed + (when-not (= old-state new-state) + (h/refresh-all!)))) + {:db db_ + :game-stop (start-game! db_)})) + +(defn -main [& _] + (h/start-app + {:router #'router + :max-refresh-ms 200 + :ctx-start ctx-start + :ctx-stop (fn [{:keys [game-stop]}] (game-stop)) + :csrf-secret (h/env :csrf-secret) + :on-error (fn [_ctx {:keys [req error]}] + ;; (pprint/pprint req) + (pprint/pprint error) + (flush))})) + +;; Refresh app when you re-eval file +(h/refresh-all!) + +(comment + (-main) + ;; (clojure.java.browse/browse-url "http://localhost:8080/") + + ;; stop server + (((h/get-app) :stop)) + + (def db (-> (h/get-app) :ctx :db)) + + (reset! db {:board (game/empty-board board-size board-size) + :users {}}) + + (->> @db :users) + + (->> @db :board (remove false?)) + + ,) diff --git a/examples/one_million_checkboxes/README.md b/examples/one_million_checkboxes/README.md new file mode 100644 index 0000000..880858a --- /dev/null +++ b/examples/one_million_checkboxes/README.md @@ -0,0 +1,34 @@ +## Build JAR. + +```bash +clojure -Srepro -T:build uber +``` + +## Run jar locally + +``` +java -Dclojure.server.repl="{:port 5555 :accept clojure.core.server/repl}" -jar target/app.jar -m app.main -Duser.timezone=UTC -XX:+UseZGC -XX:+ZGenerational +``` + +## Deploy + +Move JAR to server (this will trigger a service restart). + +```bash +scp target/app.jar root@example.andersmurphy.com:/home/app/ +``` + +## After deploying first jar + +Optional: the first time you move the jar onto the server you will need to reboot to trigger/test systemd is working correctly. + +``` +ssh root@example.andersmurphy.com "reboot" +``` + +## SSH into repl + +```bash +ssh root@example.andersmurphy.com "nc localhost:5555" +``` + diff --git a/examples/one_million_checkboxes/build.clj b/examples/one_million_checkboxes/build.clj new file mode 100644 index 0000000..13aa5c3 --- /dev/null +++ b/examples/one_million_checkboxes/build.clj @@ -0,0 +1,22 @@ +(ns build + (:require [clojure.tools.build.api :as b])) + +(def lib 'app) +(def class-dir "target/classes") +(def basis (delay (b/create-basis {:project "deps.edn"}))) +(def uber-file (format "target/%s.jar" (name lib))) + +(defn clean [_] (b/delete {:path "target"})) + +(defn uber + [_] + (clean nil) + (b/copy-dir {:src-dirs ["src" "resources"] :target-dir class-dir}) + (b/compile-clj {:basis @basis + :ns-compile '[app.main] + :src-dirs ["src"] + :class-dir class-dir}) + (b/uber {:class-dir class-dir + :uber-file uber-file + :basis @basis + :main 'app.main})) diff --git a/examples/one_million_checkboxes/deps.edn b/examples/one_million_checkboxes/deps.edn new file mode 100644 index 0000000..62af504 --- /dev/null +++ b/examples/one_million_checkboxes/deps.edn @@ -0,0 +1,6 @@ +{:paths ["src" "resources"] + :deps {org.clojure/clojure {:mvn/version "1.12.0"} + hyperlith/hyperlith {:local/root "../../../hyperlith"}} + :aliases {:build {:deps {io.github.clojure/tools.build + {:git/tag "v0.10.5" :git/sha "2a21b7a"}} + :ns-default build}}} diff --git a/examples/one_million_checkboxes/resources/.env.edn b/examples/one_million_checkboxes/resources/.env.edn new file mode 100644 index 0000000..0f01b6f --- /dev/null +++ b/examples/one_million_checkboxes/resources/.env.edn @@ -0,0 +1,3 @@ +{;; WARNING: .env.edn should not normally be committed to source control + ;; but is here as an example. + :csrf-secret "fb1704df2b3484223cb5d2a79bf06a508311d8d0f03c68e724d555b6b605966d0ebb8dc54615f8d080e5fa062bd3b5bce5b6ba7ded23333bbd55deea3149b9d5"} diff --git a/examples/one_million_checkboxes/src/app/main.clj b/examples/one_million_checkboxes/src/app/main.clj new file mode 100644 index 0000000..35b9571 --- /dev/null +++ b/examples/one_million_checkboxes/src/app/main.clj @@ -0,0 +1,235 @@ +(ns app.main + (:gen-class) + (:require [clojure.pprint :as pprint] + [hyperlith.core :as h] + [clojure.string :as str])) + +(def board-size 72) +(def chunk-size 14) +(def board-size-px 40000) +(def view-size 3) + +(def colors + [:r :b :g :o :f :p]) + +(def class->color + {:r :red :b :blue :g :green :o :orange :f :fuchsia :p :purple}) + +(def css + (let [black :black + board-size-px (str board-size-px "px")] + (h/static-css + [["*, *::before, *::after" + {:box-sizing :border-box + :margin 0 + :padding 0}] + + [:html + {:font-family "Arial, Helvetica, sans-serif" + :font-size :18px + :color black}] + + [:.main + {:height :100dvh + :margin-inline :auto + :padding-block :2dvh + :display :flex + :width "min(100% - 2rem , 40rem)" + :gap :5px + :flex-direction :column}] + + [:.view + {:overflow :scroll + :overflow-anchor :none + :width "min(100% - 2rem , 40rem)" + :aspect-ratio "1/1"}] + + [:.board + {:background :white + :width board-size-px + :display :grid + :aspect-ratio "1/1" + :gap :10px + :grid-template-rows (str "repeat(" board-size ", 1fr)") + :grid-template-columns (str "repeat(" board-size ", 1fr)")}] + + [:.chunk + {:background :white + :display :grid + :gap :10px + :grid-template-rows (str "repeat(" chunk-size ", 1fr)") + :grid-template-columns (str "repeat(" chunk-size ", 1fr)")}] + + [:.r + {:accent-color :red}] + + [:.o + {:accent-color :orange}] + + [:.g + {:accent-color :green}] + + [:.b + {:accent-color :blue}] + + [:.p + {:accent-color :purple}] + + [:.f + {:accent-color :fuchsia}]]))) + +(defn Checkbox [[id color-class]] + (let [checked (boolean color-class)] + (h/html + [:input + {:class color-class + :type "checkbox" + :checked checked + :data-id id}]))) + +(defn Chunk [x y chunk] + (h/html + [:div.chunk + {:style {:grid-row y :grid-column x}} + (into [] + (map (fn [box] (Checkbox box))) + chunk)])) + +(defn UserView [{:keys [x y] :or {x 0 y 0}} board-state] + (second + (reduce + (fn [[dy view] board-row] + [(inc dy) + (into view + (map-indexed (fn [dx chunk] + (Chunk (inc (+ x dx)) (inc (+ y dy)) chunk))) + (subvec board-row x (min (+ x view-size) board-size)))]) + [0 []] + (subvec board-state y (min (+ y view-size) board-size))))) + +(defn Board [sid content] + (h/html + [:div#board.board + {:style + {:accent-color (class->color (h/modulo-pick colors sid))} + :data-on-mousedown "evt.target.dataset.id && +@post(`/tap?id=${evt.target.dataset.id}`)"} + content])) + +(defn scroll-offset-js [n] + (str "Math.round((" n "/" board-size-px ")*" board-size "-1)")) + +(def on-scroll-js + (str + "let x = " (scroll-offset-js "el.scrollLeft") ";" + "let y = " (scroll-offset-js "el.scrollTop") ";" + "let change = x !== $x || y !== $y;" + "$x = x; $y = y;" + "change && @post(`/scroll`)")) + +(defn render-home [{:keys [db sid tabid first-render] :as _req}] + (let [snapshot @db + user (get-in snapshot [:users sid tabid]) + board (Board sid (UserView user (:board snapshot)))] + (if first-render + (h/html + [:link#css {:rel "stylesheet" :type "text/css" :href (css :path)}] + [:main#morph.main {:data-signals-x "0" :data-signals-y "0"} + [:div#view.view + {:data-on-scroll__throttle.100ms.trail.noleading on-scroll-js} + board] + [:h1 "One Million Checkboxes"] + [:p "Built with โค๏ธ using " + [:a {:href "https://clojure.org/"} "Clojure"] + " and " + [:a {:href "https://data-star.dev"} "Datastar"] + "๐Ÿš€"] + [:p "Source code can be found " + [:a {:href "https://github.com/andersmurphy/hyperlith/blob/master/examples/one_million_checkboxes/src/app/main.clj" } "here"]]]) + board))) + +(defn action-tap-cell [{:keys [sid db] {:strs [id]} :query-params}] + (when id + (let [color-class (h/modulo-pick colors sid) + [x y c] (mapv parse-long (str/split id #"-"))] + (swap! db update-in [:board y x c 1] + (fn [color] (if (nil? color) color-class nil)))))) + +(defn action-scroll [{:keys [sid tabid db] {:keys [x y]} :body}] + (swap! db + (fn [snapshot] + (-> snapshot + (assoc-in [:users sid tabid :x] (max (int x) 0)) + (assoc-in [:users sid tabid :y] (max (int y) 0)))))) + +(def default-shim-handler + (h/shim-handler + (h/html + [:link#css {:rel "stylesheet" :type "text/css" :href (css :path)}] + [:title nil "One Million checkboxes"] + [:meta {:content "So many checkboxes" :name "description"}]))) + +(def router + (h/router + {[:get (css :path)] (css :handler) + [:get "/"] default-shim-handler + [:post "/"] (h/render-handler #'render-home + {:br-window-size 19}) + [:post "/scroll"] (h/action-handler #'action-scroll) + [:post "/tap"] (h/action-handler #'action-tap-cell)})) + +(defn initial-board-state [] + (mapv + (fn [y] + (mapv + (fn [x] + (mapv (fn [c] + ;; building the id once here leads to a 7x speed up + ;; generating hiccup (building strings is expensive) + [(str x "-" y "-" c) nil]) + (range (* chunk-size chunk-size)))) + (range board-size))) + (range board-size))) + +(defn ctx-start [] + (let [db_ (atom {:board (initial-board-state) + :users {}})] + (add-watch db_ :refresh-on-change + (fn [_ _ old-state new-state] + ;; Only refresh if state has changed + (when-not (= old-state new-state) + (h/refresh-all!)))) + {:db db_})) + +(defn -main [& _] + (h/start-app + {:router #'router + :max-refresh-ms 100 + :ctx-start ctx-start + :ctx-stop (fn [{:keys [game-stop]}] (game-stop)) + :csrf-secret (h/env :csrf-secret) + :on-error (fn [_ctx {:keys [_req error]}] + (pprint/pprint error) + (flush))})) + +;; Refresh app when you re-eval file +(h/refresh-all!) + +(comment + (do (-main) nil) + ;; (clojure.java.browse/browse-url "http://localhost:8080/") + + ;; stop server + (((h/get-app) :stop)) + + (def db (-> (h/get-app) :ctx :db)) + + (@db :users) + + ,) + +(comment + (def db (-> (h/get-app) :ctx :db)) + + (user/bench (do (UserView {:x 10 :y 10} (@db :board)) nil)) + ) diff --git a/examples/popover/README.md b/examples/popover/README.md new file mode 100644 index 0000000..880858a --- /dev/null +++ b/examples/popover/README.md @@ -0,0 +1,34 @@ +## Build JAR. + +```bash +clojure -Srepro -T:build uber +``` + +## Run jar locally + +``` +java -Dclojure.server.repl="{:port 5555 :accept clojure.core.server/repl}" -jar target/app.jar -m app.main -Duser.timezone=UTC -XX:+UseZGC -XX:+ZGenerational +``` + +## Deploy + +Move JAR to server (this will trigger a service restart). + +```bash +scp target/app.jar root@example.andersmurphy.com:/home/app/ +``` + +## After deploying first jar + +Optional: the first time you move the jar onto the server you will need to reboot to trigger/test systemd is working correctly. + +``` +ssh root@example.andersmurphy.com "reboot" +``` + +## SSH into repl + +```bash +ssh root@example.andersmurphy.com "nc localhost:5555" +``` + diff --git a/examples/popover/build.clj b/examples/popover/build.clj new file mode 100644 index 0000000..13aa5c3 --- /dev/null +++ b/examples/popover/build.clj @@ -0,0 +1,22 @@ +(ns build + (:require [clojure.tools.build.api :as b])) + +(def lib 'app) +(def class-dir "target/classes") +(def basis (delay (b/create-basis {:project "deps.edn"}))) +(def uber-file (format "target/%s.jar" (name lib))) + +(defn clean [_] (b/delete {:path "target"})) + +(defn uber + [_] + (clean nil) + (b/copy-dir {:src-dirs ["src" "resources"] :target-dir class-dir}) + (b/compile-clj {:basis @basis + :ns-compile '[app.main] + :src-dirs ["src"] + :class-dir class-dir}) + (b/uber {:class-dir class-dir + :uber-file uber-file + :basis @basis + :main 'app.main})) diff --git a/examples/popover/deps.edn b/examples/popover/deps.edn new file mode 100644 index 0000000..62af504 --- /dev/null +++ b/examples/popover/deps.edn @@ -0,0 +1,6 @@ +{:paths ["src" "resources"] + :deps {org.clojure/clojure {:mvn/version "1.12.0"} + hyperlith/hyperlith {:local/root "../../../hyperlith"}} + :aliases {:build {:deps {io.github.clojure/tools.build + {:git/tag "v0.10.5" :git/sha "2a21b7a"}} + :ns-default build}}} diff --git a/examples/popover/resources/.env.edn b/examples/popover/resources/.env.edn new file mode 100644 index 0000000..0f01b6f --- /dev/null +++ b/examples/popover/resources/.env.edn @@ -0,0 +1,3 @@ +{;; WARNING: .env.edn should not normally be committed to source control + ;; but is here as an example. + :csrf-secret "fb1704df2b3484223cb5d2a79bf06a508311d8d0f03c68e724d555b6b605966d0ebb8dc54615f8d080e5fa062bd3b5bce5b6ba7ded23333bbd55deea3149b9d5"} diff --git a/examples/popover/src/app/main.clj b/examples/popover/src/app/main.clj new file mode 100644 index 0000000..8bf1098 --- /dev/null +++ b/examples/popover/src/app/main.clj @@ -0,0 +1,89 @@ +(ns app.main + (:gen-class) + (:require [hyperlith.core :as h])) + +(def css + (h/static-css + [["*, *::before, *::after" + {:box-sizing :border-box + :margin 0 + :padding 0}] + + [:html + {:font-family "Arial, Helvetica, sans-serif"}] + + [:.main + {:height :100dvh + :width "min(100% - 2rem , 40rem)" + :margin-inline :auto + :padding-block :2dvh + :display :grid + :place-items :center}] + + [:.counter + {:text-align :center + :font-size :50px}] + + [:.popover + {:position :absolute + :top :50% + :left :50% + :transform "translate(-50%, -50%)"}]])) + +(defn render-home [{:keys [db] :as _req}] + (h/html + [:link#css {:rel "stylesheet" :type "text/css" :href (css :path)}] + [:main#morph.main + ;; We track connected users as this will cause updates out of bounds + ;; and will show that the popover state is not affected by other users + [:div + [:p nil "connected users"] + [:p.counter nil (@db :connected-users)]] + [:button {:popovertarget "my-popover"} "Open Popover"] + [:div#my-popover.popover {:popover true} "Greetings, one and all!"]])) + +(def default-shim-handler + (h/shim-handler + (h/html + [:link#css {:rel "stylesheet" :type "text/css" :href (css :path)}]))) + +(def router + (h/router + {[:get (css :path)] (css :handler) + [:get "/"] default-shim-handler + [:post "/"] (h/render-handler #'render-home + ;; Example of tracking connected users + ;; This could use a separate atom or a + ;; commute and ref + :on-open + (fn [{:keys [_ db]}] + (swap! db update :connected-users inc)) + :on-close + (fn [{:keys [_ db]}] + (swap! db update :connected-users dec)))})) + +(defn ctx-start [] + (let [db_ (atom {:connected-users 0})] + (add-watch db_ :refresh-on-change (fn [& _] (h/refresh-all!))) + {:db db_})) + +(defn -main [& _] + (h/start-app + {:router #'router + :max-refresh-ms 100 + :ctx-start ctx-start + :ctx-stop (fn [_state] nil) + :csrf-secret (h/env :csrf-secret)})) + +;; Refresh app when you re-eval file +(h/refresh-all!) + +(comment + + (-main) + ;; (clojure.java.browse/browse-url "http://localhost:8080/") + + ;; stop server + (((h/get-app) :stop)) + + ,) diff --git a/examples/presence_cursors/README.md b/examples/presence_cursors/README.md new file mode 100644 index 0000000..880858a --- /dev/null +++ b/examples/presence_cursors/README.md @@ -0,0 +1,34 @@ +## Build JAR. + +```bash +clojure -Srepro -T:build uber +``` + +## Run jar locally + +``` +java -Dclojure.server.repl="{:port 5555 :accept clojure.core.server/repl}" -jar target/app.jar -m app.main -Duser.timezone=UTC -XX:+UseZGC -XX:+ZGenerational +``` + +## Deploy + +Move JAR to server (this will trigger a service restart). + +```bash +scp target/app.jar root@example.andersmurphy.com:/home/app/ +``` + +## After deploying first jar + +Optional: the first time you move the jar onto the server you will need to reboot to trigger/test systemd is working correctly. + +``` +ssh root@example.andersmurphy.com "reboot" +``` + +## SSH into repl + +```bash +ssh root@example.andersmurphy.com "nc localhost:5555" +``` + diff --git a/examples/presence_cursors/build.clj b/examples/presence_cursors/build.clj new file mode 100644 index 0000000..13aa5c3 --- /dev/null +++ b/examples/presence_cursors/build.clj @@ -0,0 +1,22 @@ +(ns build + (:require [clojure.tools.build.api :as b])) + +(def lib 'app) +(def class-dir "target/classes") +(def basis (delay (b/create-basis {:project "deps.edn"}))) +(def uber-file (format "target/%s.jar" (name lib))) + +(defn clean [_] (b/delete {:path "target"})) + +(defn uber + [_] + (clean nil) + (b/copy-dir {:src-dirs ["src" "resources"] :target-dir class-dir}) + (b/compile-clj {:basis @basis + :ns-compile '[app.main] + :src-dirs ["src"] + :class-dir class-dir}) + (b/uber {:class-dir class-dir + :uber-file uber-file + :basis @basis + :main 'app.main})) diff --git a/examples/presence_cursors/deps.edn b/examples/presence_cursors/deps.edn new file mode 100644 index 0000000..62af504 --- /dev/null +++ b/examples/presence_cursors/deps.edn @@ -0,0 +1,6 @@ +{:paths ["src" "resources"] + :deps {org.clojure/clojure {:mvn/version "1.12.0"} + hyperlith/hyperlith {:local/root "../../../hyperlith"}} + :aliases {:build {:deps {io.github.clojure/tools.build + {:git/tag "v0.10.5" :git/sha "2a21b7a"}} + :ns-default build}}} diff --git a/examples/presence_cursors/resources/.env.edn b/examples/presence_cursors/resources/.env.edn new file mode 100644 index 0000000..0f01b6f --- /dev/null +++ b/examples/presence_cursors/resources/.env.edn @@ -0,0 +1,3 @@ +{;; WARNING: .env.edn should not normally be committed to source control + ;; but is here as an example. + :csrf-secret "fb1704df2b3484223cb5d2a79bf06a508311d8d0f03c68e724d555b6b605966d0ebb8dc54615f8d080e5fa062bd3b5bce5b6ba7ded23333bbd55deea3149b9d5"} diff --git a/examples/presence_cursors/src/app/main.clj b/examples/presence_cursors/src/app/main.clj new file mode 100644 index 0000000..aa68ba8 --- /dev/null +++ b/examples/presence_cursors/src/app/main.clj @@ -0,0 +1,94 @@ +(ns app.main + (:gen-class) + (:require [hyperlith.core :as h])) + +(def css + (h/static-css + [["*, *::before, *::after" + {:box-sizing :border-box + :margin 0 + :padding 0}] + + [:.cursor-area + {:user-select :none + :height :100dvh + :width "100%"}] + + [:.cursor + {:position :absolute + :transition "all 0.2s ease-in-out"}]])) + +(def cursors + (h/cache + (fn [db] + (for [[sid [x y]] @db] + [:div.cursor + {:id (h/digest sid) + :style {:left (str x "px") :top (str y "px")}} + "๐Ÿš€"])))) + +(defn render-home [{:keys [db] :as _req}] + (h/html + [:link#css {:rel "stylesheet" :type "text/css" :href (css :path)}] + [:main#morph.main {:data-signals-x__ifmissing 0 + :data-signals-y__ifmissing 0} + [:div.cursor-area + {:data-on-mousemove__debounce.100ms + "$x = evt.clientX; $y = evt.clientY; @post('/position')"} + (cursors db)]])) + +(defn action-user-cursor-position [{:keys [sid db] {:keys [x y]} :body}] + (when (and x y) + (swap! db assoc sid [x y]))) + +(def default-shim-handler + (h/shim-handler + (h/html + [:link#css {:rel "stylesheet" :type "text/css" :href (css :path)}]))) + +(def router + (h/router + {[:get (css :path)] (css :handler) + [:get "/"] default-shim-handler + [:post "/"] (h/render-handler #'render-home + :on-close + (fn [{:keys [sid db]}] (swap! db dissoc sid))) + [:post "/position"] (h/action-handler action-user-cursor-position)})) + +(defn ctx-start [] + (let [db_ (atom {})] + (add-watch db_ :refresh-on-change (fn [& _] (h/refresh-all!))) + {:db db_})) + +(defn -main [& _] + (h/start-app + {:router #'router + :max-refresh-ms 100 + :ctx-start ctx-start + :ctx-stop (fn [_db] nil) + :csrf-secret (h/env :csrf-secret)})) + +;; Refresh app when you re-eval file +(h/refresh-all!) + +(comment + (-main) + ;; (clojure.java.browse/browse-url "http://localhost:8080/") + + ;; stop server + (((h/get-app) :stop)) + + (-> (h/get-app) :ctx :db) + + (reset! (-> (h/get-app) :ctx :db) {}) + + ;; Example backend driven cursor test + (doseq [_x (range 10000)] + (Thread/sleep 1) + (action-user-cursor-position + {:db (-> (h/get-app) :ctx :db) + :sid (rand-nth (range 1000)) + :body {:x (rand-nth (range 1 400 20)) + :y (rand-nth (range 1 400 20))}})) + + ,) diff --git a/examples/server-setup.sh b/examples/server-setup.sh new file mode 100644 index 0000000..9b457d8 --- /dev/null +++ b/examples/server-setup.sh @@ -0,0 +1,90 @@ +#!/usr/bin/env bash +set -x +set -e + +# Dependencies +apt-get update +apt-get upgrade +apt-get -y install openjdk-21-jre-headless ufw caddy + +# App user (you cannot login as this user) +useradd -rms /usr/sbin/nologin app + +# Systemd service +cat > /etc/systemd/system/app.service << EOD +[Unit] +Description=app +StartLimitIntervalSec=500 +StartLimitBurst=5 +ConditionPathExists=/home/app/app.jar + +[Service] +User=app +Restart=on-failure +RestartSec=5s +WorkingDirectory=/home/app +ExecStart=/usr/bin/java -Dclojure.server.repl="{:port 5555 :accept clojure.core.server/repl}" -jar app.jar -m app.main -Duser.timezone=UTC -XX:+UseZGC -XX:+ZGenerational -XX:InitialRAMPercentage 75.0 -XX:MaxRAMPercentage 75.0 -XX:MinRAMPercentage 75.0 + +[Install] +WantedBy=multi-user.target +EOD +systemctl enable app.service + +cat > /etc/systemd/system/app-watcher.service << EOD +[Unit] +Description=Restarts app on jar upload +After=network.target + +[Service] +ExecStart=/usr/bin/env systemctl restart app.service + +[Install] +WantedBy=multi-user.target +EOD +systemctl enable app-watcher.service + +cat > /etc/systemd/system/app-watcher.path << EOD +[Unit] +Wants=app-watcher.service + +[Path] +PathChanged=/home/app/app.jar + +[Install] +WantedBy=multi-user.target +EOD +systemctl enable app-watcher.path + +# Firewall +ufw default deny incoming +ufw default allow outgoing +ufw allow OpenSSH +ufw allow 80 +ufw allow 443 +ufw --force enable + +# Reverse proxy +rm /etc/caddy/Caddyfile +cat > /etc/caddy/Caddyfile << EOD +example.andersmurphy.com { + header -Server + reverse_proxy localhost:8080 { + lb_try_duration 30s + lb_try_interval 1s + } +} +EOD + +# Let's encrypt +systemctl daemon-reload +systemctl enable --now caddy + +# ssh config +cat >> /etc/ssh/sshd_config << EOD +# Setup script changes +PasswordAuthentication no +PubkeyAuthentication yes +AuthorizedKeysFile .ssh/authorized_keys +EOD +systemctl restart ssh + diff --git a/resources/datastar.js b/resources/datastar.js index 88a5e59..cb292d3 100644 --- a/resources/datastar.js +++ b/resources/datastar.js @@ -1,13 +1,11 @@ -// Datastar v1.0.0-beta.2 -var je=/๐Ÿ–•JS_DS๐Ÿš€/.source,pe=je.slice(0,5),ke=je.slice(4),L="datastar";var Be="Datastar-Request",Ge="1.0.0-beta.2",me=300;var Ke="type module",ge=!1,Je=!1,ze=!0,O={Morph:"morph",Inner:"inner",Outer:"outer",Prepend:"prepend",Append:"append",Before:"before",After:"after",UpsertAttributes:"upsertAttributes"},Xe=O.Morph,I={MergeFragments:"datastar-merge-fragments",MergeSignals:"datastar-merge-signals",RemoveFragments:"datastar-remove-fragments",RemoveSignals:"datastar-remove-signals",ExecuteScript:"datastar-execute-script"};var m=(r=>(r[r.Attribute=1]="Attribute",r[r.Watcher=2]="Watcher",r[r.Action=3]="Action",r))(m||{});var xn="computed",Ye={type:1,name:xn,keyReq:1,valReq:1,onLoad:({key:t,signals:e,genRX:n})=>{let r=n();e.setComputed(t,r)}};var $=t=>t.trim()==="true",U=t=>t.replace(/[A-Z]+(?![a-z])|[A-Z]/g,(e,n)=>(n?"-":"")+e.toLowerCase()),Ze=t=>t.replace(/(?:^\w|[A-Z]|\b\w)/g,(e,n)=>n===0?e.toLowerCase():e.toUpperCase()).replace(/\s+/g,""),he=t=>new Function(`return Object.assign({}, ${t})`)(),j=t=>t.startsWith("$")?t.slice(1):t;var Qe={type:1,name:"signals",removeOnLoad:!0,onLoad:t=>{let{key:e,value:n,genRX:r,signals:i,mods:o}=t,s=o.has("ifmissing");if(e!==""&&!s){let a=n===""?n:r()();i.setValue(e,a)}else{let a=he(t.value);t.value=JSON.stringify(a);let c=r()();i.merge(c,s)}}};var et={type:1,name:"star",keyReq:2,valReq:2,onLoad:()=>{alert("YOU ARE PROBABLY OVERCOMPLICATING IT")}};var ie=class{#e=0;#t;constructor(e=L){this.#t=e}with(e){if(typeof e=="string")for(let n of e.split(""))this.with(n.charCodeAt(0));else this.#e=(this.#e<<5)-this.#e+e;return this}reset(){return this.#e=0,this}get value(){return this.#t+Math.abs(this.#e).toString(36)}};function tt(t){if(t.id)return t.id;let e=new ie,n=t;for(;n.parentNode;){if(n.id){e.with(n.id);break}if(n===n.ownerDocument.documentElement)e.with(n.tagName);else{for(let r=1,i=t;i.previousElementSibling;i=i.previousElementSibling,r++)e.with(r);n=n.parentNode}n=n.parentNode}return e.value}function nt(t,e){let n=new MutationObserver(r=>{for(let i of r)for(let o of i.removedNodes)if(o===t){n.disconnect(),e();return}});n.observe(t.parentNode,{childList:!0})}var Pn=`${window.location.origin}/errors`;function De(t,e,n={}){let r=new Error;e=e[0].toUpperCase()+e.slice(1),r.name=`${L} ${t} error`;let i=U(e).replaceAll("-","_"),o=new URLSearchParams({metadata:JSON.stringify(n)}).toString(),s=JSON.stringify(n,null,2);return r.message=`${e} -More info: ${Pn}/${t}/${i}?${o} -Context: ${s}`,r}function N(t,e,n={}){return De("internal",e,Object.assign({from:t},n))}function V(t,e,n={}){let r={plugin:{name:e.plugin.name,type:m[e.plugin.type]}};return De("init",t,Object.assign(r,n))}function h(t,e,n={}){let r={plugin:{name:e.plugin.name,type:m[e.plugin.type]},element:{id:e.el.id,tag:e.el.tagName},expression:{rawKey:e.rawKey,key:e.key,value:e.value,validSignals:e.signals.paths(),fnContent:e.fnContent}};return De("runtime",t,Object.assign(r,n))}var G="preact-signals",Nn=Symbol.for("preact-signals"),F=1,X=2,se=4,Z=8,ye=16,Y=32;function Oe(){ve++}function Ve(){if(ve>1){ve--;return}let t,e=!1;for(;oe!==void 0;){let n=oe;for(oe=void 0,Le++;n!==void 0;){let r=n._nextBatchedEffect;if(n._nextBatchedEffect=void 0,n._flags&=~X,!(n._flags&Z)&&it(n))try{n._callback()}catch(i){e||(t=i,e=!0)}n=r}}if(Le=0,ve--,e)throw N(G,"BatchError, error",{error:t})}var b;var oe,ve=0,Le=0,be=0;function rt(t){if(b===void 0)return;let e=t._node;if(e===void 0||e._target!==b)return e={_version:0,_source:t,_prevSource:b._sources,_nextSource:void 0,_target:b,_prevTarget:void 0,_nextTarget:void 0,_rollbackNode:e},b._sources!==void 0&&(b._sources._nextSource=e),b._sources=e,t._node=e,b._flags&Y&&t._subscribe(e),e;if(e._version===-1)return e._version=0,e._nextSource!==void 0&&(e._nextSource._prevSource=e._prevSource,e._prevSource!==void 0&&(e._prevSource._nextSource=e._nextSource),e._prevSource=b._sources,e._nextSource=void 0,b._sources._nextSource=e,b._sources=e),e}function w(t){this._value=t,this._version=0,this._node=void 0,this._targets=void 0}w.prototype.brand=Nn;w.prototype._refresh=()=>!0;w.prototype._subscribe=function(t){this._targets!==t&&t._prevTarget===void 0&&(t._nextTarget=this._targets,this._targets!==void 0&&(this._targets._prevTarget=t),this._targets=t)};w.prototype._unsubscribe=function(t){if(this._targets!==void 0){let e=t._prevTarget,n=t._nextTarget;e!==void 0&&(e._nextTarget=n,t._prevTarget=void 0),n!==void 0&&(n._prevTarget=e,t._nextTarget=void 0),t===this._targets&&(this._targets=n)}};w.prototype.subscribe=function(t){return Se(()=>{let e=this.value,n=b;b=void 0;try{t(e)}finally{b=n}})};w.prototype.valueOf=function(){return this.value};w.prototype.toString=function(){return`${this.value}`};w.prototype.toJSON=function(){return this.value};w.prototype.peek=function(){let t=b;b=void 0;try{return this.value}finally{b=t}};Object.defineProperty(w.prototype,"value",{get(){let t=rt(this);return t!==void 0&&(t._version=this._version),this._value},set(t){if(t!==this._value){if(Le>100)throw N(G,"SignalCycleDetected");this._value=t,this._version++,be++,Oe();try{for(let e=this._targets;e!==void 0;e=e._nextTarget)e._target._notify()}finally{Ve()}}}});function it(t){for(let e=t._sources;e!==void 0;e=e._nextSource)if(e._source._version!==e._version||!e._source._refresh()||e._source._version!==e._version)return!0;return!1}function ot(t){for(let e=t._sources;e!==void 0;e=e._nextSource){let n=e._source._node;if(n!==void 0&&(e._rollbackNode=n),e._source._node=e,e._version=-1,e._nextSource===void 0){t._sources=e;break}}}function st(t){let e=t._sources,n;for(;e!==void 0;){let r=e._prevSource;e._version===-1?(e._source._unsubscribe(e),r!==void 0&&(r._nextSource=e._nextSource),e._nextSource!==void 0&&(e._nextSource._prevSource=r)):n=e,e._source._node=e._rollbackNode,e._rollbackNode!==void 0&&(e._rollbackNode=void 0),e=r}t._sources=n}function K(t){w.call(this,void 0),this._fn=t,this._sources=void 0,this._globalVersion=be-1,this._flags=se}K.prototype=new w;K.prototype._refresh=function(){if(this._flags&=~X,this._flags&F)return!1;if((this._flags&(se|Y))===Y||(this._flags&=~se,this._globalVersion===be))return!0;if(this._globalVersion=be,this._flags|=F,this._version>0&&!it(this))return this._flags&=~F,!0;let t=b;try{ot(this),b=this;let e=this._fn();(this._flags&ye||this._value!==e||this._version===0)&&(this._value=e,this._flags&=~ye,this._version++)}catch(e){this._value=e,this._flags|=ye,this._version++}return b=t,st(this),this._flags&=~F,!0};K.prototype._subscribe=function(t){if(this._targets===void 0){this._flags|=se|Y;for(let e=this._sources;e!==void 0;e=e._nextSource)e._source._subscribe(e)}w.prototype._subscribe.call(this,t)};K.prototype._unsubscribe=function(t){if(this._targets!==void 0&&(w.prototype._unsubscribe.call(this,t),this._targets===void 0)){this._flags&=~Y;for(let e=this._sources;e!==void 0;e=e._nextSource)e._source._unsubscribe(e)}};K.prototype._notify=function(){if(!(this._flags&X)){this._flags|=se|X;for(let t=this._targets;t!==void 0;t=t._nextTarget)t._target._notify()}};Object.defineProperty(K.prototype,"value",{get(){if(this._flags&F)throw N(G,"SignalCycleDetected");let t=rt(this);if(this._refresh(),t!==void 0&&(t._version=this._version),this._flags&ye)throw N(G,"GetComputedError",{value:this._value});return this._value}});function at(t){return new K(t)}function lt(t){let e=t._cleanup;if(t._cleanup=void 0,typeof e=="function"){Oe();let n=b;b=void 0;try{e()}catch(r){throw t._flags&=~F,t._flags|=Z,Fe(t),N(G,"CleanupEffectError",{error:r})}finally{b=n,Ve()}}}function Fe(t){for(let e=t._sources;e!==void 0;e=e._nextSource)e._source._unsubscribe(e);t._fn=void 0,t._sources=void 0,lt(t)}function Mn(t){if(b!==this)throw N(G,"EndEffectError");st(this),b=t,this._flags&=~F,this._flags&Z&&Fe(this),Ve()}function ae(t){this._fn=t,this._cleanup=void 0,this._sources=void 0,this._nextBatchedEffect=void 0,this._flags=Y}ae.prototype._callback=function(){let t=this._start();try{if(this._flags&Z||this._fn===void 0)return;let e=this._fn();typeof e=="function"&&(this._cleanup=e)}finally{t()}};ae.prototype._start=function(){if(this._flags&F)throw N(G,"SignalCycleDetected");this._flags|=F,this._flags&=~Z,lt(this),ot(this),Oe();let t=b;return b=this,Mn.bind(this,t)};ae.prototype._notify=function(){this._flags&X||(this._flags|=X,this._nextBatchedEffect=oe,oe=this)};ae.prototype._dispose=function(){this._flags|=Z,this._flags&F||Fe(this)};function Se(t){let e=new ae(t);try{e._callback()}catch(n){throw e._dispose(),n}return e._dispose.bind(e)}var ut="namespacedSignals";function ct(t,e=!1){let n={};for(let r in t)if(Object.hasOwn(t,r)){if(e&&r.startsWith("_"))continue;let i=t[r];i instanceof w?n[r]=i.value:n[r]=ct(i)}return n}function ft(t,e,n=!1){for(let r in e)if(Object.hasOwn(e,r)){if(r.match(/\_\_+/))throw N(ut,"InvalidSignalKey",{key:r});let i=e[r];if(i instanceof Object&&!Array.isArray(i))t[r]||(t[r]={}),ft(t[r],i,n);else{if(Object.hasOwn(t,r)){if(n)continue;let s=t[r];if(s instanceof w){s.value=i;continue}}t[r]=new w(i)}}}function dt(t,e){for(let n in t)if(Object.hasOwn(t,n)){let r=t[n];r instanceof w?e(n,r):dt(r,(i,o)=>{e(`${n}.${i}`,o)})}}function Cn(t,...e){let n={};for(let r of e){let i=r.split("."),o=t,s=n;for(let u=0;un());this.setSignal(e,r)}value(e){return this.signal(e)?.value}setValue(e,n){let r=this.upsertIfMissing(e,n);r.value=n}upsertIfMissing(e,n){let r=e.split("."),i=this.#e;for(let u=0;ue.push(n)),e}values(e=!1){return ct(this.#e,e)}JSON(e=!0,n=!1){let r=this.values(n);return e?JSON.stringify(r,null,2):JSON.stringify(r)}toString(){return this.JSON()}};var Te=class{#e=new Ee;#t=[];#r={};#s=[];#n=new Map;get signals(){return this.#e}get version(){return Ge}load(...e){for(let n of e){let r=this,i={get signals(){return r.#e},effect:s=>Se(s),actions:this.#r,apply:this.apply.bind(this),cleanup:this.#i.bind(this),plugin:n},o;switch(n.type){case 2:{let s=n;this.#s.push(s),o=s.onGlobalInit;break}case 3:{this.#r[n.name]=n;break}case 1:{let s=n;this.#t.push(s),o=s.onGlobalInit;break}default:throw V("InvalidPluginType",i)}o&&o(i)}this.#t.sort((n,r)=>{let i=r.name.length-n.name.length;return i!==0?i:n.name.localeCompare(r.name)})}apply(e){this.#o(e,n=>{this.#i(n);for(let r of Object.keys(n.dataset)){let i=this.#t.find(_=>r.startsWith(_.name));if(!i)continue;n.id.length||(n.id=tt(n));let[o,...s]=r.slice(i.name.length).split(/\_\_+/),a=o.length>0;if(a){let _=o.slice(1);o=o.startsWith("-")?_:o[0].toLowerCase()+_}let u=`${n.dataset[r]}`||"",c=u.length>0,l=this,f={get signals(){return l.#e},effect:_=>Se(_),apply:this.apply.bind(this),cleanup:this.#i.bind(this),actions:this.#r,genRX:()=>this.#a(f,...i.argNames||[]),plugin:i,el:n,rawKey:r,key:o,value:u,mods:new Map},y=i.keyReq||0;if(a){if(y===2)throw h(`${i.name}KeyNotAllowed`,f)}else if(y===1)throw h(`${i.name}KeyRequired`,f);let A=i.valReq||0;if(c){if(A===2)throw h(`${i.name}ValueNotAllowed`,f)}else if(A===1)throw h(`${i.name}ValueRequired`,f);if(y===3||A===3){if(a&&c)throw h(`${i.name}KeyAndValueProvided`,f);if(!a&&!c)throw h(`${i.name}KeyOrValueRequired`,f)}for(let _ of s){let[d,...p]=_.split(".");f.mods.set(Ze(d),new Set(p.map(S=>S.toLowerCase())))}let P=i.onLoad(f);P&&(this.#n.has(n)||this.#n.set(n,{id:n.id,fns:[]}),this.#n.get(n)?.fns.push(P)),i?.removeOnLoad&&delete n.dataset[r]}})}#a(e,...n){let r=/(\/(\\\/|[^\/])*\/|"(\\"|[^\"])*"|'(\\'|[^'])*'|`(\\`|[^`])*`|[^;\n])+/gm,i=e.value.trim().match(r),o=i.length-1,s=i[o];s.startsWith("return")||(i[o]=`return (${s});`);let a=i.join(` -`),u=new Map,c=new RegExp(`(?:${pe})(.*?)(?:${ke})`,"gm");for(let d of a.matchAll(c)){let p=d[1],S=new ie("dsEscaped").with(p).value;u.set(S,p),a=a.replace(pe+p+ke,S)}let l=/@(\w*)\(/gm,f=a.matchAll(l),y=new Set;for(let d of f)y.add(d[1]);let A=new RegExp(`@(${Object.keys(this.#r).join("|")})\\(`,"gm");a=a.replaceAll(A,"ctx.actions.$1.fn(ctx,");let P=e.signals.paths();if(P.length){let d=new RegExp(`\\$(${P.join("|")})(\\W|$)`,"gm");a=a.replaceAll(d,"ctx.signals.signal('$1').value$2")}for(let[d,p]of u)a=a.replace(d,p);let _=`return (()=> { -${a} -})()`;e.fnContent=_;try{let d=new Function("ctx",...n,_);return(...p)=>{try{return d(e,...p)}catch(S){throw h("ExecuteExpression",e,{error:S.message})}}}catch(d){throw h("GenerateExpression",e,{error:d.message})}}#o(e,n){if(!e||!(e instanceof HTMLElement||e instanceof SVGElement))return null;let r=e.dataset;if("starIgnore"in r)return null;"starIgnore__self"in r||n(e);let i=e.firstElementChild;for(;i;)this.#o(i,n),i=i.nextElementSibling}#i(e){let n=this.#n.get(e);if(n){for(let r of n.fns)r();this.#n.delete(e)}}};var pt=new Te;pt.load(et,Qe,Ye);var Ae=pt;async function In(t,e){let n=t.getReader(),r;for(;!(r=await n.read()).done;)e(r.value)}function kn(t){let e,n,r,i=!1;return function(s){e===void 0?(e=s,n=0,r=-1):e=Ln(e,s);let a=e.length,u=0;for(;n0){let u=i.decode(s.subarray(0,a)),c=a+(s[a+1]===32?2:1),l=i.decode(s.subarray(c));switch(u){case"data":r.data=r.data?`${r.data} -${l}`:l;break;case"event":r.event=l;break;case"id":t(r.id=l);break;case"retry":{let f=Number.parseInt(l,10);Number.isNaN(f)||e(r.retry=f);break}}}}}function Ln(t,e){let n=new Uint8Array(t.length+e.length);return n.set(t),n.set(e,t.length),n}function mt(){return{data:"",event:"",id:"",retry:void 0}}var On="text/event-stream",gt="last-event-id";function ht(t,e,{signal:n,headers:r,onopen:i,onmessage:o,onclose:s,onerror:a,openWhenHidden:u,fetch:c,retryInterval:l=1e3,retryScaler:f=2,retryMaxWaitMs:y=3e4,retryMaxCount:A=10,...P}){return new Promise((_,d)=>{let p=0,S={...r};S.accept||(S.accept=On);let v;function E(){v.abort(),document.hidden||D()}u||document.addEventListener("visibilitychange",E);let T=0;function x(){document.removeEventListener("visibilitychange",E),window.clearTimeout(T),v.abort()}n?.addEventListener("abort",()=>{x(),_()});let M=c??window.fetch,g=i??function(){};async function D(){v=new AbortController;try{let C=await M(e,{...P,headers:S,signal:v.signal});await g(C),await In(C.body,kn(Dn(R=>{R?S[gt]=R:delete S[gt]},R=>{l=R},o))),s?.(),x(),_()}catch(C){if(!v.signal.aborted)try{let R=a?.(C)??l;window.clearTimeout(T),T=window.setTimeout(D,R),l*=f,l=Math.min(l,y),p++,p>=A?(x(),d(h("SseMaxRetries",t,{retryMaxCount:A}))):console.error(`Datastar failed to reach ${P.method}: ${e.toString()} retry in ${R}ms`)}catch(R){x(),d(R)}}}D()})}var Q=`${L}-sse`,He=`${L}-settling`,J=`${L}-swapping`,_e="started",Re="finished",yt="error";function H(t,e){document.addEventListener(Q,n=>{if(n.detail.type!==t)return;let{argsRaw:r}=n.detail;e(r)})}function we(t,e){document.dispatchEvent(new CustomEvent(Q,{detail:{type:t,argsRaw:e}}))}var vt=t=>`${t}`.includes("text/event-stream"),q=async(t,e,n,r)=>{let{el:{id:i},el:o,signals:s}=t,{headers:a,contentType:u,includeLocal:c,selector:l,openWhenHidden:f,retryInterval:y,retryScaler:A,retryMaxWaitMs:P,retryMaxCount:_,abort:d}=Object.assign({headers:{},contentType:"json",includeLocal:!1,selector:null,openWhenHidden:!1,retryInterval:1e3,retryScaler:2,retryMaxWaitMs:3e4,retryMaxCount:10,abort:void 0},r),p=e.toLowerCase(),S=()=>{};try{if(we(_e,{elId:i}),!n?.length)throw h("SseNoUrlProvided",t,{action:p});let v={};v[Be]=!0,u==="json"&&(v["Content-Type"]="application/json");let E=Object.assign({},v,a),T={method:e,headers:E,openWhenHidden:f,retryInterval:y,retryScaler:A,retryMaxWaitMs:P,retryMaxCount:_,signal:d,onopen:async g=>{if(g.status>=400){let D=g.status.toString();we(yt,{status:D})}},onmessage:g=>{if(!g.event.startsWith(L))return;let D=g.event,C={},R=g.data.split(` -`);for(let re of R){let fe=re.indexOf(" "),Ue=re.slice(0,fe),de=C[Ue];de||(de=[],C[Ue]=de);let wn=re.slice(fe+1).trim();de.push(wn)}let W={};for(let[re,fe]of Object.entries(C))W[re]=fe.join(` -`);we(D,W)},onerror:g=>{if(vt(g))throw h("InvalidContentType",t,{url:n});g&&console.error(g.message)}},x=new URL(n,window.location.origin),M=new URLSearchParams(x.search);if(u==="json"){let g=s.JSON(!1,!c);e==="GET"?M.set(L,g):T.body=g}else if(u==="form"){let g=l?document.querySelector(l):o.closest("form");if(g===null)throw l?h("SseFormNotFound",t,{action:p,selector:l}):h("SseClosestFormNotFound",t,{action:p});if(o!==g){let C=R=>R.preventDefault();g.addEventListener("submit",C),S=()=>g.removeEventListener("submit",C)}if(!g.checkValidity()){g.reportValidity(),S();return}let D=new FormData(g);if(e==="GET"){let C=new URLSearchParams(D);for(let[R,W]of C)M.set(R,W)}else T.body=D}else throw h("SseInvalidContentType",t,{action:p,contentType:u});x.search=M.toString();try{await ht(t,x.toString(),T)}catch(g){if(!vt(g))throw h("SseFetchFailed",t,{method:e,url:n,error:g})}}finally{we(Re,{elId:i}),S()}};var bt={type:3,name:"delete",fn:async(t,e,n)=>q(t,"DELETE",e,{...n})};var St={type:3,name:"get",fn:async(t,e,n)=>q(t,"GET",e,{...n})};var Et={type:3,name:"patch",fn:async(t,e,n)=>q(t,"PATCH",e,{...n})};var Tt={type:3,name:"post",fn:async(t,e,n)=>q(t,"POST",e,{...n})};var At={type:3,name:"put",fn:async(t,e,n)=>q(t,"PUT",e,{...n})};var _t={type:1,name:"indicator",keyReq:3,valReq:3,onLoad:({value:t,signals:e,el:n,key:r})=>{let i=r||j(t),o=e.upsertIfMissing(i,!1),s=a=>{let{type:u,argsRaw:{elId:c}}=a.detail;if(c===n.id)switch(u){case _e:o.value=!0;break;case Re:o.value=!1;break}};return document.addEventListener(Q,s),()=>{document.removeEventListener(Q,s)}}};var Rt={type:2,name:I.ExecuteScript,onGlobalInit:async t=>{H(I.ExecuteScript,({autoRemove:e=`${ze}`,attributes:n=Ke,script:r})=>{let i=$(e);if(!r?.length)throw V("NoScriptProvided",t);let o=document.createElement("script");for(let s of n.split(` -`)){let a=s.indexOf(" "),u=a?s.slice(0,a):s,c=a?s.slice(a):"";o.setAttribute(u.trim(),c.trim())}o.text=r,document.head.appendChild(o),i&&o.remove()})}};var le=document,ee=!!le.startViewTransition;var te="idiomorph",Pe=new WeakSet;function Nt(t,e,n={}){t instanceof Document&&(t=t.documentElement);let r;typeof e=="string"?r=Wn(e):r=e;let i=$n(r),o=Fn(t,i,n);return Mt(t,i,o)}function Mt(t,e,n){if(n.head.block){let r=t.querySelector("head"),i=e.querySelector("head");if(r&&i){let o=It(i,r,n);Promise.all(o).then(()=>{Mt(t,e,Object.assign(n,{head:{block:!1,ignore:!0}}))});return}}if(n.morphStyle==="innerHTML")return Ct(e,t,n),t.children;if(n.morphStyle==="outerHTML"||n.morphStyle==null){let r=jn(e,t,n);if(!r)throw N(te,"NoBestMatchFound",{old:t,new:e});let i=r?.previousSibling,o=r?.nextSibling,s=Ne(t,r,n);return r?Un(i,s,o):[]}throw N(te,"InvalidMorphStyle",{style:n.morphStyle})}function Ne(t,e,n){if(!(n.ignoreActive&&t===document.activeElement))if(e==null){if(n.callbacks.beforeNodeRemoved(t)===!1)return;t.remove(),n.callbacks.afterNodeRemoved(t);return}else{if(Me(t,e))return n.callbacks.beforeNodeMorphed(t,e)===!1?void 0:(t instanceof HTMLHeadElement&&n.head.ignore||(e instanceof HTMLHeadElement&&t instanceof HTMLHeadElement&&n.head.style!==O.Morph?It(e,t,n):(Vn(e,t),Ct(e,t,n))),n.callbacks.afterNodeMorphed(t,e),t);if(n.callbacks.beforeNodeRemoved(t)===!1||n.callbacks.beforeNodeAdded(e)===!1)return;if(!t.parentElement)throw N(te,"NoParentElementFound",{oldNode:t});return t.parentElement.replaceChild(e,t),n.callbacks.afterNodeAdded(e),n.callbacks.afterNodeRemoved(t),e}}function Ct(t,e,n){let r=t.firstChild,i=e.firstChild,o;for(;r;){if(o=r,r=o.nextSibling,i==null){if(n.callbacks.beforeNodeAdded(o)===!1)return;e.appendChild(o),n.callbacks.afterNodeAdded(o),z(n,o);continue}if(kt(o,i,n)){Ne(i,o,n),i=i.nextSibling,z(n,o);continue}let s=Hn(t,e,o,i,n);if(s){i=wt(i,s,n),Ne(s,o,n),z(n,o);continue}let a=qn(t,o,i,n);if(a){i=wt(i,a,n),Ne(a,o,n),z(n,o);continue}if(n.callbacks.beforeNodeAdded(o)===!1)return;e.insertBefore(o,i),n.callbacks.afterNodeAdded(o),z(n,o)}for(;i!==null;){let s=i;i=i.nextSibling,Dt(s,n)}}function Vn(t,e){let n=t.nodeType;if(n===1){for(let r of t.attributes)e.getAttribute(r.name)!==r.value&&e.setAttribute(r.name,r.value);for(let r of e.attributes)t.hasAttribute(r.name)||e.removeAttribute(r.name)}if((n===Node.COMMENT_NODE||n===Node.TEXT_NODE)&&e.nodeValue!==t.nodeValue&&(e.nodeValue=t.nodeValue),t instanceof HTMLInputElement&&e instanceof HTMLInputElement&&t.type!=="file")e.value=t.value||"",xe(t,e,"value"),xe(t,e,"checked"),xe(t,e,"disabled");else if(t instanceof HTMLOptionElement)xe(t,e,"selected");else if(t instanceof HTMLTextAreaElement&&e instanceof HTMLTextAreaElement){let r=t.value,i=e.value;r!==i&&(e.value=r),e.firstChild&&e.firstChild.nodeValue!==r&&(e.firstChild.nodeValue=r)}}function xe(t,e,n){let r=t.getAttribute(n),i=e.getAttribute(n);r!==i&&(r?e.setAttribute(n,r):e.removeAttribute(n))}function It(t,e,n){let r=[],i=[],o=[],s=[],a=n.head.style,u=new Map;for(let l of t.children)u.set(l.outerHTML,l);for(let l of e.children){let f=u.has(l.outerHTML),y=n.head.shouldReAppend(l),A=n.head.shouldPreserve(l);f||A?y?i.push(l):(u.delete(l.outerHTML),o.push(l)):a===O.Append?y&&(i.push(l),s.push(l)):n.head.shouldRemove(l)!==!1&&i.push(l)}s.push(...u.values());let c=[];for(let l of s){let f=document.createRange().createContextualFragment(l.outerHTML).firstChild;if(!f)throw N(te,"NewElementCouldNotBeCreated",{newNode:l});if(n.callbacks.beforeNodeAdded(f)){if(f.hasAttribute("href")||f.hasAttribute("src")){let y,A=new Promise(P=>{y=P});f.addEventListener("load",()=>{y(void 0)}),c.push(A)}e.appendChild(f),n.callbacks.afterNodeAdded(f),r.push(f)}}for(let l of i)n.callbacks.beforeNodeRemoved(l)!==!1&&(e.removeChild(l),n.callbacks.afterNodeRemoved(l));return n.head.afterHeadMorphed(e,{added:r,kept:o,removed:i}),c}function B(){}function Fn(t,e,n){return{target:t,newContent:e,config:n,morphStyle:n.morphStyle,ignoreActive:n.ignoreActive,idMap:Jn(t,e),deadIds:new Set,callbacks:Object.assign({beforeNodeAdded:B,afterNodeAdded:B,beforeNodeMorphed:B,afterNodeMorphed:B,beforeNodeRemoved:B,afterNodeRemoved:B},n.callbacks),head:Object.assign({style:"merge",shouldPreserve:r=>r.getAttribute("im-preserve")==="true",shouldReAppend:r=>r.getAttribute("im-re-append")==="true",shouldRemove:B,afterHeadMorphed:B},n.head)}}function kt(t,e,n){return!t||!e?!1:t.nodeType===e.nodeType&&t.tagName===e.tagName?t?.id?.length&&t.id===e.id?!0:ue(n,t,e)>0:!1}function Me(t,e){return!t||!e?!1:t.nodeType===e.nodeType&&t.tagName===e.tagName}function wt(t,e,n){for(;t!==e;){let r=t;if(t=t?.nextSibling,!r)throw N(te,"NoTemporaryNodeFound",{startInclusive:t,endExclusive:e});Dt(r,n)}return z(n,e),e.nextSibling}function Hn(t,e,n,r,i){let o=ue(i,n,e),s=null;if(o>0){s=r;let a=0;for(;s!=null;){if(kt(n,s,i))return s;if(a+=ue(i,s,t),a>o)return null;s=s.nextSibling}}return s}function qn(t,e,n,r){let i=n,o=e.nextSibling,s=0;for(;i&&o;){if(ue(r,i,t)>0)return null;if(Me(e,i))return i;if(Me(o,i)&&(s++,o=o.nextSibling,s>=2))return null;i=i.nextSibling}return i}var xt=new DOMParser;function Wn(t){let e=t.replace(/]*>|>)([\s\S]*?)<\/svg>/gim,"");if(e.match(/<\/html>/)||e.match(/<\/head>/)||e.match(/<\/body>/)){let i=xt.parseFromString(t,"text/html");if(e.match(/<\/html>/))return Pe.add(i),i;let o=i.firstChild;return o?(Pe.add(o),o):null}let r=xt.parseFromString(``,"text/html").body.querySelector("template")?.content;if(!r)throw N(te,"NoContentFound",{newContent:t});return Pe.add(r),r}function $n(t){if(t==null)return document.createElement("div");if(Pe.has(t))return t;if(t instanceof Node){let n=document.createElement("div");return n.append(t),n}let e=document.createElement("div");for(let n of[...t])e.append(n);return e}function Un(t,e,n){let r=[],i=[];for(;t;)r.push(t),t=t.previousSibling;for(;r.length>0;){let o=r.pop();i.push(o),e?.parentElement?.insertBefore(o,e)}for(i.push(e);n;)r.push(n),i.push(n),n=n.nextSibling;for(;r.length;)e?.parentElement?.insertBefore(r.pop(),e.nextSibling);return i}function jn(t,e,n){let r=t.firstChild,i=r,o=0;for(;r;){let s=Bn(r,e,n);s>o&&(i=r,o=s),r=r.nextSibling}return i}function Bn(t,e,n){return Me(t,e)?.5+ue(n,t,e):0}function Dt(t,e){z(e,t),e.callbacks.beforeNodeRemoved(t)!==!1&&(t.remove(),e.callbacks.afterNodeRemoved(t))}function Gn(t,e){return!t.deadIds.has(e)}function Kn(t,e,n){return t.idMap.get(n)?.has(e)||!1}function z(t,e){let n=t.idMap.get(e);if(n)for(let r of n)t.deadIds.add(r)}function ue(t,e,n){let r=t.idMap.get(e);if(!r)return 0;let i=0;for(let o of r)Gn(t,o)&&Kn(t,o,n)&&++i;return i}function Pt(t,e){let n=t.parentElement,r=t.querySelectorAll("[id]");for(let i of r){let o=i;for(;o!==n&&o;){let s=e.get(o);s==null&&(s=new Set,e.set(o,s)),s.add(i.id),o=o.parentElement}}}function Jn(t,e){let n=new Map;return Pt(t,n),Pt(e,n),n}var Ot={type:2,name:I.MergeFragments,onGlobalInit:async t=>{let e=document.createElement("template");H(I.MergeFragments,({fragments:n="
",selector:r="",mergeMode:i=Xe,settleDuration:o=`${me}`,useViewTransition:s=`${ge}`})=>{let a=Number.parseInt(o),u=$(s);e.innerHTML=n.trim();let c=[...e.content.children];for(let l of c){if(!(l instanceof Element))throw V("NoFragmentsFound",t);let f=r||`#${l.getAttribute("id")}`,y=[...document.querySelectorAll(f)||[]];if(!y.length)throw V("NoTargetsFound",t,{selectorOrID:f});ee&&u?le.startViewTransition(()=>Lt(t,i,a,l,y)):Lt(t,i,a,l,y)}})}};function Lt(t,e,n,r,i){for(let o of i){o.classList.add(J);let s=o.outerHTML,a=o;switch(e){case O.Morph:{let l=Nt(a,r,{callbacks:{beforeNodeRemoved:(f,y)=>(t.cleanup(f),!0)}});if(!l?.length)throw V("MorphFailed",t);a=l[0];break}case O.Inner:a.innerHTML=r.innerHTML;break;case O.Outer:a.replaceWith(r);break;case O.Prepend:a.prepend(r);break;case O.Append:a.append(r);break;case O.Before:a.before(r);break;case O.After:a.after(r);break;case O.UpsertAttributes:for(let l of r.getAttributeNames()){let f=r.getAttribute(l);a.setAttribute(l,f)}break;default:throw V("InvalidMergeMode",t,{mergeMode:e})}t.cleanup(a);let u=a.classList;u.add(J),t.apply(document.body),setTimeout(()=>{o.classList.remove(J),u.remove(J)},n);let c=a.outerHTML;s!==c&&(u.add(He),setTimeout(()=>{u.remove(He)},n))}}var Vt={type:2,name:I.MergeSignals,onGlobalInit:async t=>{H(I.MergeSignals,({signals:e="{}",onlyIfMissing:n=`${Je}`})=>{let{signals:r}=t,i=$(n);r.merge(he(e),i),t.apply(document.body)})}};var Ft={type:2,name:I.RemoveFragments,onGlobalInit:async t=>{H(I.RemoveFragments,({selector:e,settleDuration:n=`${me}`,useViewTransition:r=`${ge}`})=>{if(!e.length)throw V("NoSelectorProvided",t);let i=Number.parseInt(n),o=$(r),s=document.querySelectorAll(e),a=()=>{for(let u of s)u.classList.add(J);setTimeout(()=>{for(let u of s)u.remove()},i)};ee&&o?le.startViewTransition(()=>a()):a()})}};var Ht={type:2,name:I.RemoveSignals,onGlobalInit:async t=>{H(I.RemoveSignals,({paths:e=""})=>{let n=e.split(` -`).map(r=>r.trim());if(!n?.length)throw V("NoPathsProvided",t);t.signals.remove(...n),t.apply(document.body)})}};var qt={type:3,name:"clipboard",fn:(t,e)=>{if(!navigator.clipboard)throw h("ClipboardNotAvailable",t);navigator.clipboard.writeText(e)}};var Wt={type:1,name:"customValidity",keyReq:2,valReq:1,onLoad:t=>{let{el:e,genRX:n,effect:r}=t;if(!(e instanceof HTMLInputElement))throw h("CustomValidityInvalidElement",t);let i=n();return r(()=>{let o=i();if(typeof o!="string")throw h("CustomValidityInvalidExpression",t,{result:o});e.setCustomValidity(o)})}};var $t="once",Ut="half",jt="full",Bt={type:1,name:"intersects",keyReq:2,mods:new Set([$t,Ut,jt]),onLoad:({el:t,rawKey:e,mods:n,genRX:r})=>{let i={threshold:0};n.has(jt)?i.threshold=1:n.has(Ut)&&(i.threshold=.5);let o=r(),s=new IntersectionObserver(a=>{for(let u of a)u.isIntersecting&&(o(),n.has($t)&&(s.disconnect(),delete t.dataset[e]))},i);return s.observe(t),()=>s.disconnect()}};var Gt="session",Kt={type:1,name:"persist",mods:new Set([Gt]),onLoad:({key:t,value:e,signals:n,effect:r,mods:i})=>{t===""&&(t=L);let o=i.has(Gt)?sessionStorage:localStorage,s=e.split(/\s+/).filter(c=>c!=="");s=s.map(c=>j(c));let a=()=>{let c=o.getItem(t)||"{}",l=JSON.parse(c);n.merge(l)},u=()=>{let c;s.length?c=n.subset(...s):c=n.values(),o.setItem(t,JSON.stringify(c))};return a(),r(()=>{u()})}};var Jt={type:1,name:"replaceUrl",keyReq:2,valReq:1,onLoad:({effect:t,genRX:e})=>{let n=e();return t(()=>{let r=n(),i=window.location.href,o=new URL(r,i).toString();window.history.replaceState({},"",o)})}};var Ce="smooth",qe="instant",We="auto",zt="hstart",Xt="hcenter",Yt="hend",Zt="hnearest",Qt="vstart",en="vcenter",tn="vend",nn="vnearest",zn="focus",Ie="center",rn="start",on="end",sn="nearest",an={type:1,name:"scrollIntoView",keyReq:2,valReq:2,mods:new Set([Ce,qe,We,zt,Xt,Yt,Zt,Qt,en,tn,nn,zn]),onLoad:t=>{let{el:e,mods:n,rawKey:r}=t;e.tabIndex||e.setAttribute("tabindex","0");let i={behavior:Ce,block:Ie,inline:Ie};if(n.has(Ce)&&(i.behavior=Ce),n.has(qe)&&(i.behavior=qe),n.has(We)&&(i.behavior=We),n.has(zt)&&(i.inline=rn),n.has(Xt)&&(i.inline=Ie),n.has(Yt)&&(i.inline=on),n.has(Zt)&&(i.inline=sn),n.has(Qt)&&(i.block=rn),n.has(en)&&(i.block=Ie),n.has(tn)&&(i.block=on),n.has(nn)&&(i.block=sn),!(e instanceof HTMLElement||e instanceof SVGElement))throw h("ScrollIntoViewInvalidElement",t);return e.tabIndex||e.setAttribute("tabindex","0"),e.scrollIntoView(i),n.has("focus")&&e.focus(),delete e.dataset[r],()=>{}}};var ln="none",un="display",cn={type:1,name:"show",keyReq:2,valReq:1,onLoad:({el:{style:t},genRX:e,effect:n})=>{let r=e();return n(async()=>{r()?t.display===ln&&t.removeProperty(un):t.setProperty(un,ln)})}};var fn="view-transition",dn={type:1,name:"viewTransition",keyReq:2,valReq:1,onGlobalInit(){let t=!1;for(let e of document.head.childNodes)e instanceof HTMLMetaElement&&e.name===fn&&(t=!0);if(!t){let e=document.createElement("meta");e.name=fn,e.content="same-origin",document.head.appendChild(e)}},onLoad:({effect:t,el:e,genRX:n})=>{if(!ee){console.error("Browser does not support view transitions");return}let r=n();return t(()=>{let i=r();if(!i?.length)return;let o=e.style;o.viewTransitionName=i})}};var pn={type:1,name:"attr",valReq:1,onLoad:({el:t,genRX:e,key:n,effect:r})=>{let i=e();return n===""?r(async()=>{let o=i();for(let[s,a]of Object.entries(o))t.setAttribute(s,a)}):(n=U(n),r(async()=>{let o=!1;try{o=i()}catch{}let s;typeof o=="string"?s=o:s=JSON.stringify(o),!s||s==="false"||s==="null"||s==="undefined"?t.removeAttribute(n):t.setAttribute(n,s)}))}};var Xn=/^data:(?[^;]+);base64,(?.*)$/,mn=["change","input","keydown"],gn={type:1,name:"bind",keyReq:3,valReq:3,onLoad:t=>{let{el:e,value:n,key:r,signals:i,effect:o}=t,s=r||j(n),a=()=>{},u=()=>{},c=e.tagName.toLowerCase(),l="",f=c.includes("input"),y=e.getAttribute("type"),A=c.includes("checkbox")||f&&y==="checkbox";A&&(l=!1),f&&y==="number"&&(l=0);let _=c.includes("select"),d=c.includes("radio")||f&&y==="radio",p=f&&y==="file";d&&(e.getAttribute("name")?.length||e.setAttribute("name",s)),i.upsertIfMissing(s,l),a=()=>{let v="value"in e,E=i.value(s),T=`${E}`;if(A||d){let x=e;A?x.checked=!!E||E==="true":d&&(x.checked=T===x.value)}else if(!p)if(_){let x=e;if(x.multiple)for(let M of x.options){if(M?.disabled)return;Array.isArray(E)||typeof E=="string"?M.selected=E.includes(M.value):typeof E=="number"?M.selected=E===Number(M.value):M.selected=E}else x.value=T}else v?e.value=T:e.setAttribute("value",T)},u=async()=>{if(p){let T=[...e?.files||[]],x=[],M=[],g=[];await Promise.all(T.map(D=>new Promise(C=>{let R=new FileReader;R.onload=()=>{if(typeof R.result!="string")throw h("InvalidFileResultType",t,{resultType:typeof R.result});let W=R.result.match(Xn);if(!W?.groups)throw h("InvalidDataUri",t,{result:R.result});x.push(W.groups.contents),M.push(W.groups.mime),g.push(D.name)},R.onloadend=()=>C(void 0),R.readAsDataURL(D)}))),i.setValue(s,x),i.setValue(`${s}Mimes`,M),i.setValue(`${s}Names`,g);return}let v=i.value(s),E=e||e;if(typeof v=="number"){let T=Number(E.value||E.getAttribute("value"));i.setValue(s,T)}else if(typeof v=="string"){let T=E.value||E.getAttribute("value")||"";i.setValue(s,T)}else if(typeof v=="boolean")if(A){let T=E.checked||E.getAttribute("checked")==="true";i.setValue(s,T)}else{let T=!!(E.value||E.getAttribute("value"));i.setValue(s,T)}else if(!(typeof v>"u"))if(Array.isArray(v))if(_){let M=[...e.selectedOptions].filter(g=>g.selected).map(g=>g.value);i.setValue(s,M)}else{let T=JSON.stringify(E.value.split(","));i.setValue(s,T)}else throw h("BindUnsupportedSignalType",t,{signalType:typeof v})};for(let v of mn)e.addEventListener(v,u);let S=o(()=>a());return()=>{S();for(let v of mn)e.removeEventListener(v,u)}}};var hn={type:1,name:"class",valReq:1,onLoad:({key:t,el:e,genRX:n,effect:r})=>{let i=e.classList,o=n();return r(()=>{if(t===""){let s=o();for(let[a,u]of Object.entries(s)){let c=a.split(/\s+/);u?i.add(...c):i.remove(...c)}}else{let s=o(),a=U(t);s?i.add(a):i.remove(a)}})}};function ce(t){if(!t||t.size<=0)return 0;for(let e of t){if(e.endsWith("ms"))return Number(e.replace("ms",""));if(e.endsWith("s"))return Number(e.replace("s",""))*1e3;try{return Number.parseFloat(e)}catch{}}return 0}function ne(t,e,n=!1){return t?t.has(e.toLowerCase()):n}function yn(t,e){return(...n)=>{setTimeout(()=>{t(...n)},e)}}function vn(t,e,n=!1,r=!0){let i=-1,o=()=>i&&clearTimeout(i);return(...s)=>{o(),n&&!i&&t(...s),i=setTimeout(()=>{r&&t(...s),o()},e)}}function bn(t,e,n=!0,r=!1){let i=!1;return(...o)=>{i||(n&&t(...o),i=!0,setTimeout(()=>{i=!1,r&&t(...o)},e))}}var $e=new Map,Yn="evt",Sn={type:1,name:"on",keyReq:1,valReq:1,argNames:[Yn],onLoad:({el:t,rawKey:e,key:n,value:r,genRX:i,mods:o,signals:s,effect:a})=>{let u=i(),c=t;o.has("window")&&(c=window);let l=d=>{d&&((o.has("prevent")||n==="submit")&&d.preventDefault(),o.has("stop")&&d.stopPropagation()),u(d)},f=o.get("delay");if(f){let d=ce(f);l=yn(l,d)}let y=o.get("debounce");if(y){let d=ce(y),p=ne(y,"leading",!1),S=!ne(y,"notrail",!1);l=vn(l,d,p,S)}let A=o.get("throttle");if(A){let d=ce(A),p=!ne(A,"noleading",!1),S=ne(A,"trail",!1);l=bn(l,d,p,S)}let P={capture:!0,passive:!1,once:!1};o.has("capture")||(P.capture=!1),o.has("passive")&&(P.passive=!0),o.has("once")&&(P.once=!0);let _=U(n).toLowerCase();switch(_){case"load":return l(),delete t.dataset[e],()=>{};case"interval":{let d=1e3,p=o.get("duration");p&&(d=ce(p),ne(p,"leading",!1)&&(t.dataset[e.replace(".leading","")]=r,delete t.dataset[e],l()));let S=setInterval(l,d);return()=>{clearInterval(S)}}case"raf":{let d,p=()=>{l(),d=requestAnimationFrame(p)};return d=requestAnimationFrame(p),()=>{d&&cancelAnimationFrame(d)}}case"signals-change":return nt(t,()=>{$e.delete(t.id)}),a(()=>{let d=o.has("remote"),p=s.JSON(!1,d);($e.get(t.id)||"")!==p&&($e.set(t.id,p),l())});default:{if(o.has("outside")){c=document;let p=l;l=v=>{let E=v?.target;t.contains(E)||p(v)}}return c.addEventListener(_,l,P),()=>{c.removeEventListener(_,l)}}}}};var En={type:1,name:"ref",keyReq:3,valReq:3,onLoad:({el:t,key:e,value:n,signals:r})=>{let i=e||j(n);return r.setValue(i,t),()=>r.setValue(i,null)}};var Tn={type:1,name:"text",keyReq:2,valReq:1,onLoad:t=>{let{el:e,genRX:n,effect:r}=t,i=n();return e instanceof HTMLElement||h("TextInvalidElement",t),r(()=>{let o=i(t);e.textContent=`${o}`})}};var{round:Zn,max:Qn,min:er}=Math,An={type:3,name:"fit",fn:(t,e,n,r,i,o,s=!1,a=!1)=>{let u=(e-n)/(r-n)*(o-i)+i;return a&&(u=Zn(u)),s&&(u=Qn(i,er(o,u))),u}};var _n={type:3,name:"setAll",fn:({signals:t},e,n)=>{t.walk((r,i)=>{r.startsWith(e)&&(i.value=n)})}};var Rn={type:3,name:"toggleAll",fn:({signals:t},e)=>{t.walk((n,r)=>{n.startsWith(e)&&(r.value=!r.value)})}};Ae.load(pn,gn,hn,Sn,En,cn,Tn,_t,St,Tt,At,Et,bt,Ot,Vt,Ft,Ht,Rt,qt,Wt,Bt,Kt,Jt,an,dn,An,_n,Rn);Ae.apply(document.body);var bs=Ae;export{bs as Datastar}; +// Datastar v1.0.0-RC.8 +var gt=/๐Ÿ–•JS_DS๐Ÿš€/.source,De=gt.slice(0,5),re=gt.slice(4),V="datastar",bt="Datastar-Request",yt=1e3,ht="type module",vt=!1,St=!1,Et=!0,Ue="morph",Tt="inner",xt="outer",At="prepend",wt="append",Mt="before",Rt="after",Dt="upsertAttributes",Ct=Ue,ge="datastar-merge-fragments",be="datastar-merge-signals",Ke="datastar-remove-fragments",Be="datastar-remove-signals",ye="datastar-execute-script";var X=e=>e.trim()==="true",G=e=>e.replace(/([a-z0-9])([A-Z])/g,"$1-$2").replace(/([a-z])([0-9]+)/gi,"$1-$2").replace(/([0-9]+)([a-z])/gi,"$1-$2").toLowerCase(),he=e=>G(e).replace(/-./g,t=>t[1].toUpperCase()),ve=e=>G(e).replace(/-/g,"_"),qn=e=>he(e).replace(/^./,t=>t[0].toUpperCase()),Lt=e=>Function(`return Object.assign({}, ${e})`)(),J=e=>e.startsWith("$")?e.slice(1):e,_n={kebab:G,snake:ve,pascal:qn};function L(e,t){for(let n of t.get("case")||[]){let r=_n[n];r&&(e=r(e))}return e}var jn="computed",Ft={type:"attribute",name:jn,keyReq:"must",valReq:"must",isExpr:!0,onLoad:({key:e,mods:t,genRX:n,computed:r,batch:s})=>{e=L(e,t);let{deps:a,rxFn:o}=n();s(()=>{r(a,o,e)})}};var _=`${V}-signal-change`;var Fe=new Set,Pe=new Set,ke=new Set;function Ne(e){Fe.clear(),Pe.clear(),ke.clear();let t=e();return(Fe.size||Pe.size||ke.size)&&document.dispatchEvent(new CustomEvent(_,{detail:{added:[...Fe],removed:[...Pe],updated:[...ke]}})),t}var I=[],Q=new Map;function ie(){return I}function Ze(e){return Q.get(e)}function Un(e){let t=[],n=0,r=I.length-1,s=a=>a!==e&&!a.startsWith(`${e}.`);for(;n<=r;){let a=Math.floor((n+r)/2),o=I[a];if(oe)r=a-1;else{let i=a;do{let c=I[i];if(s(c))break;i--}while(i>=0);let l=a;do{let c=I[l];if(s(c))break;l++}while(lr.length-n.length),Fe.add(e)}function W(e,t){let n=Q.get(e);if(n){if(!(n instanceof se))return;n.value=t}else Te(t,e)}function oe(e){return Q.get(e)?.value}function ae(e,t){let n=!1,r=Q.get(e);return r||(n=!0,r=Te(t,e)),{dep:r,inserted:n}}function Ve(e,t=!1){let n=kt(e);for(let[r,s]of Object.entries(n))Q.has(r)&&t||W(r,s)}function et(...e){for(let t of e){let n=Un(t);for(let r of n){Q.delete(r);let s=I.indexOf(r);s>-1&&I.splice(s,1),Pe?.add(r)}}}function Oe(e=!0,t=!1){return JSON.stringify(Kn(t),null,e?2:0)}function Kn(e,...t){let n=new Map;for(let s of I)if(!(e&&s.match(/^_|\._/))){if(t.length>0){let a=!1;for(let o of t)if(s.startsWith(o)){a=!0;break}if(!a)continue}n.set(s,Q.get(s)?.value)}return zn(n)}function Bn(e){return Object.prototype.toString.call(e)==="[object Object]"}function kt(e,t=[],n="."){return Object.keys(e).reduce((r,s)=>Object.assign({},r,Bn(e[s])?kt(e[s],t.concat([s]),n):{[t.concat([s]).join(n)]:e[s]}),{})}function zn(e,t="."){let n={};for(let[r,s]of e.entries()){let a=r.split(t),o=n;for(let i=0;ie(...r,...n.map((s,a)=>t[a]?s:s.value)))}var Ce=[],Se=[],Le=0,Ee=0;function tt(e,t){switch(e.length){case 0:return t;case 1:return()=>t(e[0].value);case 2:return()=>t(e[0].value,e[1].value);case 3:return()=>t(e[0].value,e[1].value,e[2].value);case 4:return()=>t(e[0].value,e[1].value,e[2].value,e[3].value)}let n=e.length;return Ce[n]||(Ce[n]=Array(n)),()=>{for(let r=0;rn.dispose()}var Je=class{constructor(t,n){this.flags=2;for(let r of t)Vt(r,this);this.run=tt(t,n),this.run()}dispose(){if(this.depsTail=void 0,this.flags&=-249,this.deps){let t=this.deps;do{let n=t.dep,r=t.nextDep,s=t.nextSub,a=t.prevSub;if(s?s.prevSub=a:n.subsTail=a,a?a.nextSub=s:n.subs=s,!n.subs&&"deps"in n){let o=n.flags;o&32||(n.flags=o|32);let i=n.deps;if(i){t=i,n.depsTail.nextDep=r,n.deps=void 0,n.depsTail=void 0;continue}}t=r}while(t);this.deps=void 0}}};function Vt(e,t){let n=t.depsTail;if(n&&n.dep===e)return;let r=n?n.nextDep:t.deps;if(r&&r.dep===e){t.depsTail=r;return}let s=e.subsTail;if(s&&s.sub===t&&Ye(s,t))return;let a={dep:e,sub:t,nextDep:r,prevSub:void 0,nextSub:void 0};if(n?n.nextDep=a:t.deps=a,!e.subs)e.subs=a;else{let o=e.subsTail;a.prevSub=o,o.nextSub=a}return t.depsTail=a,e.subsTail=a,a}function Ot(e){let t,n=0,r;e:do{r=!1;let s=e.dep;if(e.sub.flags&32)r=!0;else if("flags"in s){let a=s.flags;if((a&33)===33){if(ze(s)){let o=s.subs;o.nextSub&&Qe(o),r=!0}}else if((a&65)===65){(e.nextSub||e.prevSub)&&(t={target:e,linked:t}),e=s.deps,++n;continue}}if(!r&&e.nextDep){e=e.nextDep;continue}for(;n;){--n;let a=e.sub,o=a.subs;if(r){if(ze(a)){o.nextSub?(e=t.target,t=t.linked,Qe(o)):e=o;continue}}else a.flags&=-65;if(o.nextSub?(e=t.target,t=t.linked):e=o,e.nextDep){e=e.nextDep;continue e}r=!1}return r}while(!0)}function Qe(e){do{let t=e.sub,n=t.flags;(n&96)===64&&(t.flags=n|32|8,(n&10)===2&&(Se[Ee++]=t)),e=e.nextSub}while(e)}function Ye(e,t){let n=t.depsTail;if(n){let r=t.deps;do{if(r===e)return!0;if(r===n)break;r=r.nextDep}while(r)}return!1}var It={type:"attribute",name:"signals",isExpr:!0,onLoad:e=>{let{key:t,mods:n,value:r,genRX:s,evalRX:a,batch:o}=e,{deps:i,dm:l,rxFn:c}=s(),u=n.has("ifmissing");if(t!==""){let p=L(t,n),m=r===""?r:a(c,l,i);o(()=>{u?ae(p,m):W(p,m)})}else{let p=a(c,l,i);o(()=>{Ve(p,u)})}}};function Ie(e){return e instanceof HTMLElement||e instanceof SVGElement}function le(e,t){let n=Y?`data-${Y}-ignore`:"data-ignore",r=`${n}__self`;if(!Ie(e)||e.closest(`[${n}]`))return;let s=document.createTreeWalker(e,1);for(;e;){if(Ie(e)){if(e.hasAttribute(n)){e=s.nextSibling();continue}e.hasAttribute(r)||t(e)}e=s.nextNode()}}var Qn="https://data-star.dev/errors";function Ht(e,t,n={}){let r=new Error;r.name=`${V} ${e} error`;let s=ve(t),a=new URLSearchParams({metadata:JSON.stringify(n)}).toString(),o=JSON.stringify(n,null,2);return r.message=`${t} +More info: ${Qn}/${e}/${s}?${a} +Context: ${o}`,r}function H(e,t,n={}){let r={plugin:{name:t.plugin.name,type:t.plugin.type}};return Ht("init",e,Object.assign(r,n))}function v(e,t,n={}){let r={plugin:{name:t.plugin.name,type:t.plugin.type},element:{id:t.el.id,tag:t.el.tagName},expression:{rawKey:t.rawKey,key:t.key,value:t.value,validSignals:ie(),fnContent:t.fnContent}};return Ht("runtime",e,Object.assign(r,n))}var xe={},it=[],ce=new Map,st=null,Y="";function $t(e){Y=e}function He(...e){for(let t of e){let n={plugin:t,actions:xe,removals:ce,applyToElement:$e,batch:Ne,signal:Te,computed:nt,effect:rt},r=t.type;if(r==="action")xe[t.name]=t;else if(r==="attribute")it.push(t),t.onGlobalInit?.(n);else if(r==="watcher")t.onGlobalInit?.(n);else throw H("InvalidPluginType",n)}it.sort((t,n)=>{let r=n.name.length-t.name.length;return r!==0?r:t.name.localeCompare(n.name)})}function ot(){queueMicrotask(()=>{le(document.documentElement,$e),st||(st=new MutationObserver(Yn),st.observe(document.body,{subtree:!0,childList:!0,attributes:!0}))})}function Yn(e){let t=new Set;for(let{target:r,type:s,addedNodes:a,removedNodes:o}of e)switch(s){case"childList":{for(let i of o)le(i,l=>{let c=ce.get(l);if(ce.delete(l)){for(let u of c.values())u();c.clear()}});for(let i of a)le(i,l=>t.add(l))}break;case"attributes":{if(!Ie(r)||r.closest(`[${Y?`data-${Y}-ignore`:"data-ignore"}]`))continue;t.add(r);break}}let n=Array.from(t);n.sort((r,s)=>r.compareDocumentPosition(s)&Node.DOCUMENT_POSITION_FOLLOWING?-1:1);for(let r of n)$e(r)}function Gt(e){let t=5831,n=e.length;for(;n--;)t+=(t<<5)+e.charCodeAt(n);return(t>>>0).toString(36)}function $e(e){let t=[],n=ce.get(e)||new Map,r=new Map(n);for(let[s,a]of Object.entries(e.dataset)){if(!s.startsWith(Y))continue;let o=Gt(`${s}${a}`);r.delete(o)||t.push({key:s,value:a,hash:o})}for(let[s,a]of r)a(),n.delete(s);for(let{key:s,value:a,hash:o}of t){let i=Zn(e,s,a);i&&n.set(o,i)}n.size&&ce.set(e,n)}function Zn(e,t,n){let r=he(t.slice(Y.length)),s=it.find(m=>RegExp(`^${m.name}([A-Z]|_|$)`).test(r));if(!s)return;let[a,...o]=r.slice(s.name.length).split(/\_\_+/),i=!!a;i&&(a=he(a));let l=!!n,c={applyToElement:$e,actions:xe,removals:ce,genRX:()=>er(c,...s.argNames||[]),plugin:s,el:e,rawKey:r,key:a,value:n,mods:new Map,batch:Ne,signal:Te,computed:nt,effect:rt,evalRX:Nt},u=s.keyReq||"allowed";if(i){if(u==="denied")throw v(`${s.name}KeyNotAllowed`,c)}else if(u==="must")throw v(`${s.name}KeyRequired`,c);let p=s.valReq||"allowed";if(l){if(p==="denied")throw v(`${s.name}ValueNotAllowed`,c)}else if(p==="must")throw v(`${s.name}ValueRequired`,c);if(u==="exclusive"||p==="exclusive"){if(i&&l)throw v(`${s.name}KeyAndValueProvided`,c);if(!i&&!l)throw v(`${s.name}KeyOrValueRequired`,c)}for(let m of o){let[h,...f]=m.split(".");c.mods.set(he(h),new Set(f.map(b=>b.toLowerCase())))}return s.onLoad(c)||(()=>{})}function er(e,...t){let n=[],r=new Set,s="";if(e.plugin.isExpr){let f=/(\/(\\\/|[^\/])*\/|"(\\"|[^\"])*"|'(\\'|[^'])*'|`(\\`|[^`])*`|\(\s*((function)\s*\(\s*\)|(\(\s*\))\s*=>)\s*(?:\{[\s\S]*?\}|[^;)\{]*)\s*\)\s*\(\s*\)|[^;])+/gm,b=e.value.trim().match(f);if(b){let S=b.length-1,R=b[S].trim();R.startsWith("return")||(b[S]=`return (${R});`),s=b.join(`; +`)}}else s=e.value.trim();let a=new Map,o=RegExp(`(?:${De})(.*?)(?:${re})`,"gm");for(let f of s.matchAll(o)){let b=f[1],S=`dsEscaped${Gt(b)}`;a.set(S,b),s=s.replace(De+b+re,S)}let i=(f,b)=>`${f}${ve(b).replaceAll(/\./g,"_")}`,l=new Set(t),c=ie();if(c.length){let f=c.join("|"),b=RegExp(`\\$(${f})(\\s*[+&^\\/*|-]?=[^=]|\\+\\+|--)`,"gm"),S=[...s.matchAll(b)],R=(k,T,x="")=>{let F=RegExp(`\\$${k[1]}(?!\\w)`,"gm");s=s.replaceAll(F,T+x)};if(S.length){let k=`${V}Mut_`,T=new Set;for(let x of S){let F=x[1],d=Ze(F),A=i(k,F);d&&!T.has(d)&&(r.add(d),T.add(d),n.push(!0),l.add(A)),R(x,A,".value")}}let D=RegExp(`\\$(${f})(\\W|$)`,"gm"),C=[...s.matchAll(D)];if(C.length){let k=`${V}Pure_`,T=new Set;for(let x of C){let F=x[1],d=Ze(F),A=i(k,F);d&&!T.has(d)&&(r.add(d),T.add(d),n.push(!1),l.add(A)),R(x,A)}}}let u=new Set,p=RegExp(`@(${Object.keys(xe).join("|")})\\(`,"gm"),m=[...s.matchAll(p)],h=new Set;if(m.length){let f=`${V}Act_`;for(let b of m){let S=b[1],R=xe[S];if(!R)continue;u.add(S);let D=i(f,S);l.add(D),s=s.replace(`@${S}(`,`${D}(`),h.add((...C)=>R.fn(e,...C))}}for(let[f,b]of a)s=s.replace(f,b);e.fnContent=s;try{let f=Function("el",...l,s);return{dm:n,deps:[...r],rxFn:(...b)=>{try{return f(e.el,...b,...h)}catch(S){throw v("ExecuteExpression",e,{error:S.message})}}}}catch(f){throw v("GenerateExpression",e,{error:f.message})}}He(It,Ft);var Z=`${V}-sse`,Ge="started",We="finished",Wt="error",qt="retrying",_t="retrying";function j(e,t){document.addEventListener(Z,n=>{if(n.detail.type!==e)return;let{argsRaw:r}=n.detail;t(r)})}function te(e,t,n){document.dispatchEvent(new CustomEvent(Z,{detail:{type:e,el:t,argsRaw:n}}))}var jt=e=>`${e}`.includes("text/event-stream"),Ut=e=>e==="GET",zt="text/event-stream",Xt="text/html",Jt="application/json",Qt="application/javascript",tr=[zt,Xt,Jt,Qt],U=async(e,t,n,r)=>{let{el:s,evt:a}=e,{headers:o,contentType:i,includeLocal:l,excludeSignals:c,selector:u,openWhenHidden:p,retryInterval:m,retryScaler:h,retryMaxWaitMs:f,retryMaxCount:b,abort:S}=Object.assign({headers:{},contentType:"json",includeLocal:!1,excludeSignals:!1,selector:null,openWhenHidden:!1,retryInterval:yt,retryScaler:2,retryMaxWaitMs:3e4,retryMaxCount:10,abort:void 0},r),R=t.toLowerCase(),D=()=>{};try{if(!n?.length)throw v("SseNoUrlProvided",e,{action:R});let C={Accept:tr.join(", ")};C[bt]=!0,i==="json"&&(C["Content-Type"]="application/json");let k=Object.assign({},C,o),T={method:t,headers:k,openWhenHidden:p,retryInterval:m,retryScaler:h,retryMaxWaitMs:f,retryMaxCount:b,signal:S,onopen:async d=>{if(d.status>=400){let A=d.status.toString();te(Wt,s,{status:A})}},onmessage:d=>{if(!d.event.startsWith(V))return;let A=d.event,P={},q=d.data.split(` +`);for(let y of q){let w=y.indexOf(" "),E=y.slice(0,w),M=P[E];M||(M=[],P[E]=M);let O=y.slice(w+1);M.push(O)}let g={};for(let[y,w]of Object.entries(P))g[y]=w.join(` +`);te(A,s,g)},onerror:d=>{if(jt(d))throw v("InvalidContentType",e,{url:n});d&&(console.error(d.message),te(qt,s,{message:d.message}))}},x=new URL(n,window.location.href),F=new URLSearchParams(x.search);if(i==="json"){if(!c){let d=Oe(!1,!l);Ut(t)?F.set(V,d):T.body=d}}else if(i==="form"){let d=u?document.querySelector(u):s.closest("form");if(d===null)throw u?v("SseFormNotFound",e,{action:R,selector:u}):v("SseClosestFormNotFound",e,{action:R});if(!d.checkValidity()){d.reportValidity(),D();return}let A=new FormData(d),P=s;if(s===d)a instanceof SubmitEvent&&(P=a.submitter);else{let y=w=>w.preventDefault();d.addEventListener("submit",y),D=()=>d.removeEventListener("submit",y)}if(P instanceof HTMLButtonElement){let y=P.getAttribute("name");y&&A.append(y,P.value)}let q=d.getAttribute("enctype")==="multipart/form-data";q||(k["Content-Type"]="application/x-www-form-urlencoded");let g=new URLSearchParams(A);if(Ut(t))for(let[y,w]of g)F.append(y,w);else q?T.body=A:T.body=g}else throw v("SseInvalidContentType",e,{action:R,contentType:i});te(Ge,s,{}),x.search=F.toString();try{await or(x.toString(),s,T)}catch(d){if(!jt(d))throw v("SseFetchFailed",e,{method:t,url:n,error:d})}}finally{te(We,s,{}),D()}};async function nr(e,t){let n=e.getReader(),r;for(r=await n.read();!r.done;)t(r.value),r=await n.read()}function rr(e){let t,n,r,s=!1;return function(o){t===void 0?(t=o,n=0,r=-1):t=ir(t,o);let i=t.length,l=0;for(;n0){let l=s.decode(o.subarray(0,i)),c=i+(o[i+1]===32?2:1),u=s.decode(o.subarray(c));switch(l){case"data":r.data=r.data?`${r.data} +${u}`:u;break;case"event":r.event=u;break;case"id":e(r.id=u);break;case"retry":{let p=Number.parseInt(u,10);Number.isNaN(p)||t(r.retry=p);break}}}}}function ir(e,t){let n=new Uint8Array(e.length+t.length);return n.set(e),n.set(t,e.length),n}function Kt(){return{data:"",event:"",id:"",retry:void 0}}var Bt="last-event-id";function or(e,t,{signal:n,headers:r,onopen:s,onmessage:a,onclose:o,onerror:i,openWhenHidden:l,fetch:c,retryInterval:u=1e3,retryScaler:p=2,retryMaxWaitMs:m=3e4,retryMaxCount:h=10,overrides:f,...b}){return new Promise((S,R)=>{let D={...r};D.accept||(D.accept=zt);let C;function k(){C.abort(),document.hidden||q()}l||document.addEventListener("visibilitychange",k);let T=0;function x(){document.removeEventListener("visibilitychange",k),window.clearTimeout(T),C.abort()}n?.addEventListener("abort",()=>{x(),S()});let F=c??window.fetch,d=s??function(){},A=0,P=u;async function q(){C=new AbortController;try{let g=await F(e,{...b,headers:D,signal:C.signal});A=0,u=P,await d(g);let y=async(E,M,O,N,...Me)=>{let de={[O]:await M.text()};for(let me of Me){let B=`datastar-${G(me)}`,z=M.headers.get(B);if(N){let Re=N[me];Re&&(typeof Re=="string"?z=Re:z=JSON.stringify(Re))}z&&(de[me]=z)}te(E,t,de)},w=g.headers.get("Content-Type");if(w?.includes(Xt))return await y(ge,g,"fragments",f,"selector","mergeMode","useViewTransition");if(w?.includes(Jt))return await y(be,g,"signals",f,"onlyIfMissing","mergeMode","useViewTransition");if(w?.includes(Qt))return await y(ye,g,"script",f,"autoRemove","attributes");await nr(g.body,rr(sr(E=>{E?D[Bt]=E:delete D[Bt]},E=>{P=E,u=E},a))),o?.(),x(),S()}catch(g){if(!C.signal.aborted)try{let y=i?.(g)??u;window.clearTimeout(T),T=window.setTimeout(q,y),u*=p,u=Math.min(u,m),A++,A>=h?(te(_t,t,{}),x(),R("Max retries reached.")):console.error(`Datastar failed to reach ${e.toString()} retrying in ${y}ms.`)}catch(y){x(),R(y)}}}q()})}var Yt={type:"action",name:"delete",fn:async(e,t,n)=>U(e,"DELETE",t,{...n})};var Zt={type:"action",name:"get",fn:async(e,t,n)=>U(e,"GET",t,{...n})};var en={type:"action",name:"patch",fn:async(e,t,n)=>U(e,"PATCH",t,{...n})};var tn={type:"action",name:"post",fn:async(e,t,n)=>U(e,"POST",t,{...n})};var nn={type:"action",name:"put",fn:async(e,t,n)=>U(e,"PUT",t,{...n})};var rn={type:"watcher",name:ye,onGlobalInit:async e=>{j(ye,({autoRemove:t=`${Et}`,attributes:n=ht,script:r})=>{let s=X(t);if(!r?.length)throw H("NoScriptProvided",e);let a=document.createElement("script");for(let o of n.split(` +`)){let i=o.indexOf(" "),l=i?o.slice(0,i):o,c=i?o.slice(i):"";a.setAttribute(l.trim(),c.trim())}a.text=r,document.head.appendChild(a),s&&a.remove()})}};var Ae=document,we=!!Ae.startViewTransition;function K(e,t){if(t.has("viewtransition")&&we){let n=e;e=(...r)=>document.startViewTransition(()=>n(...r))}return e}var mn="div",at="value",ut=e=>document.createElement(e),gn=()=>ut(mn),ar=e=>`<${e}>`,sn=e=>[...e.querySelectorAll("[id]")],bn=e=>e instanceof Element,on=e=>e instanceof HTMLTemplateElement,an=e=>e instanceof HTMLInputElement,ln=e=>e instanceof HTMLTextAreaElement,cn=e=>e instanceof HTMLOptionElement,yn={type:"watcher",name:ge,onGlobalInit:async e=>{let t=ut("template");j(ge,({fragments:n=ar(mn),selector:r="",mergeMode:s=Ct,useViewTransition:a="false"})=>{let o=X(a);t.innerHTML=n.trim();for(let i of[...t.content.children]){if(!bn(i))throw H("NoFragmentsFound",e);let l=r||`#${i.getAttribute("id")}`,c=document.querySelectorAll(l);if(!c.length)throw H("NoTargetsFound",e,{selectorOrID:l});o&&we?Ae.startViewTransition(()=>un(e,s,i,c)):un(e,s,i,c)}})}};function un(e,t,n,r){for(let s of r){let a=n.cloneNode(!0);switch(t){case Ue:{cr(s,a),le(s,o=>{let i=e.removals.get(o);if(e.removals.delete(o)){for(let l of i.values())l();i.clear()}e.applyToElement(o)});break}case Tt:s.innerHTML=a.outerHTML;break;case xt:s.replaceWith(a);break;case At:s.prepend(a);break;case wt:s.append(a);break;case Mt:s.before(a);break;case Rt:s.after(a);break;case Dt:for(let o of a.getAttributeNames()){let i=a.getAttribute(o);s.setAttribute(o,i)}break;default:throw H("InvalidMergeMode",e,{mergeMode:t})}}}var pe=gn();pe.hidden=!0;var qe,$=new Map,ue=new Set,lr=pe.moveBefore!==void 0;function cr(e,t){let n=gn();n.append(t);let r=sn(n),s=sn(e);e.id&&s.push(e);let a=new Set,o=new Map;for(let{id:m,tagName:h}of s)o.has(m)?a.add(m):o.set(m,h);ue.clear();for(let{id:m,tagName:h}of r)ue.has(m)?a.add(m):o.get(m)===h&&ue.add(m);for(let m of a)ue.delete(m);$.clear(),dn(e.parentElement,s),dn(n,r),document.body.insertAdjacentElement("afterend",pe),qe=e;let i=e.parentNode,l=e.previousSibling,c=e.nextSibling;hn(i,n,e,c);let u=[],p=l?.nextSibling||i.firstChild;for(;p&&p!==c;)u.push(p),p=p.nextSibling;return pe.remove(),u}function hn(e,t,n=null,r=null){on(e)&&on(t)&&(e=e.content,t=t.content),n??=e.firstChild;for(let s of t.childNodes){if(n&&n!==r){let o=ur(s,n,r);if(o){if(o!==n){let i=n;for(;i&&i!==o;){let l=i;i=i.nextSibling,fn(l)}}lt(o,s),n=o.nextSibling;continue}}if(bn(s)&&ue.has(s.id)){let o=s.id,i=`[id="${o}"]`,l=p=>p.querySelector(i),c=qe.id===o&&qe||l(qe)||l(pe),u=c;for(;u=u.parentNode;){let p=$.get(u);p&&(p.delete(c.id),p.size||$.delete(u))}vn(e,c,n),lt(c,s),n=c.nextSibling;continue}if($.has(s)){let o=ut(s.tagName);return e.insertBefore(o,n),lt(o,s),o}let a=document.importNode(s,!0);e.insertBefore(a,n)}for(;n&&n!==r;){let s=n;n=n.nextSibling,fn(s)}}function ur(e,t,n){let r=null,s=e.nextSibling,a=0,o=0,i=$.get(e)?.size||0,l=t;for(;l&&l!==n;){if(pn(l,e)){let c=!1,u=$.get(l),p=$.get(e);if(p&&u){for(let m of u)if(p.has(m)){c=!0;break}}if(c)return l;if(!r&&!$.has(l)){if(!i)return l;r=l}}if(o+=$.get(l)?.size||0,o>i||(r===null&&s&&pn(l,s)&&(a++,s=s.nextSibling,a>=2&&(r=void 0)),l.contains(document.activeElement)))break;l=l.nextSibling}return r||null}function pn(e,t){let n=e,r=t;return n.nodeType===r.nodeType&&n.tagName===r.tagName&&(!n.id||n.id===r.id)}function fn(e){$.has(e)?vn(pe,e,null):e.parentNode?.removeChild(e)}function vn(e,t,n=null){lr?e.moveBefore(t,n):e.insertBefore(t,n)}function lt(e,t){let n=t.nodeType,r=e,s=t;if(n===1){let a=r.attributes,o=s.attributes;for(let i of o)r.getAttribute(i.name)!==i.value&&r.setAttribute(i.name,i.value);for(let i=a.length-1;0<=i;i--){let l=a[i];l&&(s.hasAttribute(l.name)||r.removeAttribute(l.name))}if(an(r)&&an(s)&&s.type!=="file"){let i=s.value,l=r.value;ct(r,s,"checked"),ct(r,s,"disabled"),Object.keys(s.dataset||{}).some(u=>u.startsWith("bind"))||(s.hasAttribute(at)?l!==i&&(r.setAttribute(at,i),r.value=i):(r.value="",r.removeAttribute(at)))}else if(cn(r)&&cn(s))ct(r,s,"selected");else if(ln(r)&&ln(s)){let i=s.value,l=r.value;i!==l&&(r.value=i),r.firstChild&&r.firstChild.nodeValue!==i&&(r.firstChild.nodeValue=i)}}return(n===8||n===3)&&e.nodeValue!==t.nodeValue&&(e.nodeValue=t.nodeValue),r.isEqualNode(s)||hn(r,s),e}function ct(e,t,n){let r=t,s=e,a=r[n],o=s[n];a!==o&&(s[n]=r[n],a?e.setAttribute(n,""):e.removeAttribute(n))}function dn(e,t){for(let n of t)if(ue.has(n.id)){let r=n;for(;r&&r!==e;){let s=$.get(r);s||(s=new Set,$.set(r,s)),s.add(n.id),r=r.parentElement}}}var Sn={type:"watcher",name:be,onGlobalInit:async({batch:e})=>{j(be,({signals:t="{}",onlyIfMissing:n=`${St}`})=>{let r=X(n),s=Lt(t);e(()=>Ve(s,r))})}};var En={type:"watcher",name:Ke,onGlobalInit:async e=>{j(Ke,({selector:t,useViewTransition:n=`${vt}`})=>{if(!t.length)throw H("NoSelectorProvided",e);let r=X(n),s=document.querySelectorAll(t),a=()=>{for(let o of s)o.remove()};r&&we?Ae.startViewTransition(()=>a()):a()})}};var Tn={type:"watcher",name:Be,onGlobalInit:async e=>{let{batch:t}=e;j(Be,({paths:n=""})=>{let r=n.split(` +`).map(s=>s.trim());if(!r?.length)throw H("NoPathsProvided",e);t(()=>{et(...r)})})}};var xn={type:"attribute",name:"attr",valReq:"must",isExpr:!0,onLoad:({el:e,key:t,genRX:n,computed:r,effect:s})=>{let{deps:a,rxFn:o}=n(),i=(c,u)=>{u===""||u===!0?e.setAttribute(c,""):u===!1||u===null||u===void 0?e.removeAttribute(c):e.setAttribute(c,u)};if(t===""){let c=r(a,o);return s([c],u=>{for(let[p,m]of Object.entries(u))i(p,m)})}t=G(t);let l=r(a,o);return s([l],c=>{i(t,c)})}};var pr=/^data:(?[^;]+);base64,(?.*)$/,An=["change","input","keydown"],wn={type:"attribute",name:"bind",keyReq:"exclusive",valReq:"exclusive",onLoad:e=>{let{el:t,key:n,mods:r,value:s,effect:a,batch:o}=e,i=t,l=n?L(n,r):J(s),c=t.tagName.toLowerCase(),u=c.includes("input"),p=c.includes("select"),m=t.getAttribute("type"),h=t.hasAttribute("value"),f="",b=u&&m==="checkbox";b&&(i.hasAttribute("checked")?f=h?i.value:!0:f=h?"":!1);let S=u&&m==="number";S&&(f=0);let R=u&&m==="radio";R&&(t.getAttribute("name")?.length||t.setAttribute("name",l));let D=u&&m==="file",{dep:C,inserted:k}=o(()=>ae(l,f)),T=-1;Array.isArray(C.value)&&(t.getAttribute("name")===null&&t.setAttribute("name",l),T=[...document.querySelectorAll(`[name="${l}"]`)].findIndex(g=>g===e.el));let x=T>=0,F=()=>[...oe(l)],d=()=>{let g=oe(l);x&&!p&&(g=g[T]||f);let y=`${g}`;if(b||R)typeof g=="boolean"?i.checked=g:i.checked=y===i.value;else if(p){let w=t;if(w.multiple){if(!x)throw v("BindSelectMultiple",e);for(let E of w.options){if(E?.disabled)continue;let M=S?Number(E.value):E.value;E.selected=g.includes(M)}}else w.value=y}else D||("value"in t?t.value=y:t.setAttribute("value",y))},A=async()=>{let g=oe(l);if(x){let M=g;for(;T>=M.length;)M.push(f);g=M[T]||f}let y=(M,O)=>{let N=O;x&&!p&&!D&&(N=F(),N[T]=O),W(M,N)};if(D){let M=[...i?.files||[]],O=[],N=[],Me=[];await Promise.all(M.map(de=>new Promise(me=>{let B=new FileReader;B.onload=()=>{if(typeof B.result!="string")throw v("InvalidFileResultType",e,{resultType:typeof B.result});let z=B.result.match(pr);if(!z?.groups)throw v("InvalidDataUri",e,{result:B.result});O.push(z.groups.contents),N.push(z.groups.mime),Me.push(de.name)},B.onloadend=()=>me(void 0),B.readAsDataURL(de)}))),o(()=>{y(l,O),y(`${l}Mimes`,N),y(`${l}Names`,Me)});return}let w=i.value||"",E;if(b){let M=i.checked;h?E=M?w:"":E=M}else if(p){let O=[...t.selectedOptions];x?E=O.filter(N=>N.selected).map(N=>N.value):E=O[0]?.value||f}else typeof g=="boolean"?E=!!w:typeof g=="number"?E=Number(w):E=w||"";o(()=>{y(l,E)})};k&&A();for(let g of An)t.addEventListener(g,A);let P=g=>{g.persisted&&A()};window.addEventListener("pageshow",P);let q=a([C],()=>d());return()=>{q();for(let g of An)t.removeEventListener(g,A);window.removeEventListener("pageshow",P)}}};var Mn={type:"attribute",name:"class",valReq:"must",isExpr:!0,onLoad:({el:e,key:t,mods:n,genRX:r,computed:s,effect:a})=>{let o=e.classList,{deps:i,rxFn:l}=r(),c=s(i,l);return a([c],u=>{if(t===""){let p=u;for(let[m,h]of Object.entries(p)){let f=m.split(/\s+/);h?o.add(...f):o.remove(...f)}}else{let p=G(t);p=L(p,n),u?o.add(p):o.remove(p)}})}};var Rn={type:"attribute",name:"indicator",keyReq:"exclusive",valReq:"exclusive",onLoad:e=>{let{el:t,key:n,mods:r,value:s,batch:a}=e,o=n?L(n,r):J(s),{dep:i}=a(()=>ae(o,!1));if(!(i instanceof se))throw v("not_signal",e,{signalName:o});let l=c=>{let{type:u,el:p}=c.detail;if(p===t)switch(u){case Ge:i.value=!0;break;case We:i.value=!1;break}};return document.addEventListener(Z,l),()=>{i.value=!1,document.removeEventListener(Z,l)}}};var Dn={type:"attribute",name:"jsonSignals",keyReq:"denied",valReq:"denied",onLoad:e=>{let{el:t}=e;t instanceof HTMLElement||v("JsonSignalsInvalidElement",e);let n=()=>{t.textContent=Oe(!0)};return n(),document.addEventListener(_,n),()=>{document.removeEventListener(_,n)}}};function ee(e){if(!e||e.size<=0)return 0;for(let t of e){if(t.endsWith("ms"))return Number(t.replace("ms",""));if(t.endsWith("s"))return Number(t.replace("s",""))*1e3;try{return Number.parseFloat(t)}catch{}}return 0}function ne(e,t,n=!1){return e?e.has(t.toLowerCase()):n}function pt(e,t){return(...n)=>{setTimeout(()=>{e(...n)},t)}}function fr(e,t,n=!1,r=!0){let s=0,a=()=>s&&clearTimeout(s);return(...o)=>{a(),n&&!s&&e(...o),s=setTimeout(()=>{r&&e(...o),a()},t)}}function dr(e,t,n=!0,r=!1){let s=!1;return(...a)=>{s||(n&&e(...a),s=!0,setTimeout(()=>{s=!1,r&&e(...a)},t))}}function fe(e,t){let n=t.get("delay");if(n){let a=ee(n);e=pt(e,a)}let r=t.get("debounce");if(r){let a=ee(r),o=ne(r,"leading",!1),i=!ne(r,"notrail",!1);e=fr(e,a,o,i)}let s=t.get("throttle");if(s){let a=ee(s),o=!ne(s,"noleading",!1),i=ne(s,"trail",!1);e=dr(e,a,o,i)}return e}var Cn={type:"attribute",name:"on",keyReq:"must",valReq:"must",argNames:["evt"],onLoad:e=>{let{el:t,key:n,mods:r,genRX:s,evalRX:a}=e,o=t;r.has("window")&&(o=window);let{dm:i,deps:l,rxFn:c}=s(),u=f=>{if(f){if(r.has("prevent")&&f.preventDefault(),r.has("stop")&&f.stopPropagation(),!(f.isTrusted||f instanceof CustomEvent||r.has("trust")))return;e.evt=f}a(c,i,l,f)};u=fe(u,r),u=K(u,r);let p={capture:!1,passive:!1,once:!1};if(r.has("capture")&&(p.capture=!0),r.has("passive")&&(p.passive=!0),r.has("once")&&(p.once=!0),r.has("outside")){o=document;let f=u;u=b=>{let S=b?.target;t.contains(S)||f(b)}}let h=G(n);if(h=L(h,r),(h===Z||h===_)&&(o=document),t instanceof HTMLFormElement&&h==="submit"){let f=u;u=b=>{b?.preventDefault(),f(b)}}return o.addEventListener(h,u,p),()=>{o.removeEventListener(h,u)}}};var ft=new WeakSet,Ln={type:"attribute",name:"onIntersect",keyReq:"denied",onLoad:({el:e,mods:t,genRX:n,evalRX:r})=>{let{dm:s,deps:a,rxFn:o}=n(),i=()=>r(o,s,a);i=fe(i,t),i=K(i,t);let l={threshold:0};t.has("full")?l.threshold=1:t.has("half")&&(l.threshold=.5);let c=new IntersectionObserver(u=>{for(let p of u)p.isIntersecting&&(i(),c&&ft.has(e)&&c.disconnect())},l);return c.observe(e),t.has("once")&&ft.add(e),()=>{t.has("once")||ft.delete(e),c&&(c.disconnect(),c=null)}}};var Fn={type:"attribute",name:"onInterval",keyReq:"denied",valReq:"must",onLoad:({mods:e,genRX:t,evalRX:n})=>{let{dm:r,deps:s,rxFn:a}=t(),o=()=>n(a,r,s);o=K(o,e);let i=1e3,l=e.get("duration");l&&(i=ee(l),ne(l,"leading",!1)&&o());let c=setInterval(o,i);return()=>{clearInterval(c)}}};var dt=new WeakSet,Pn={type:"attribute",name:"onLoad",keyReq:"denied",valReq:"must",onLoad:({el:e,mods:t,genRX:n,evalRX:r})=>{let{dm:s,deps:a,rxFn:o}=n(),i=()=>r(o,s,a);i=K(i,t);let l=0,c=t.get("delay");return c&&(l=ee(c)),i=pt(i,l),dt.has(e)||i(),t.has("once")&&dt.add(e),()=>{t.has("once")||dt.delete(e)}}};function mt(e,t){return t=t.replaceAll(".","\\.").replaceAll("**",re).replaceAll("*","[^\\.]*").replaceAll(re,".*"),RegExp(`^${t}$`).test(e)}function _e(e){let t=[],n=e.split(/\s+/).filter(r=>r!=="");n=n.map(r=>J(r));for(let r of n)for(let s of ie())mt(s,r)&&t.push(s);return t}var kn={type:"attribute",name:"onSignalChange",valReq:"must",argNames:["evt"],onLoad:({key:e,mods:t,genRX:n,evalRX:r})=>{let{dm:s,deps:a,rxFn:o}=n(),i=c=>r(o,s,a,c);i=fe(i,t),i=K(i,t);let l=c=>{if(e!==""){let u=L(e,t),{added:p,removed:m,updated:h}=c.detail;if(![...p,...m,...h].some(f=>mt(f,u)))return}i(c)};return document.addEventListener(_,l),()=>{document.removeEventListener(_,l)}}};var Nn={type:"attribute",name:"ref",keyReq:"exclusive",valReq:"exclusive",onLoad:({el:e,key:t,mods:n,value:r,batch:s})=>{let a=t?L(t,n):J(r);s(()=>{W(a,e)})}};var Vn="none",On="display",In={type:"attribute",name:"show",keyReq:"denied",valReq:"must",isExpr:!0,onLoad:({el:{style:e},genRX:t,computed:n,effect:r})=>{let{deps:s,rxFn:a}=t(),o=n(s,a);return r([o],async i=>{i?e.display===Vn&&e.removeProperty(On):e.setProperty(On,Vn)})}};var Hn={type:"attribute",name:"text",keyReq:"denied",valReq:"must",isExpr:!0,onLoad:e=>{let{el:t,genRX:n,computed:r,effect:s}=e,{deps:a,rxFn:o}=n();t instanceof HTMLElement||v("TextInvalidElement",e);let i=r(a,o);return s([i],l=>{t.textContent=`${l}`})}};var $n={type:"action",name:"setAll",fn:({batch:e},t,n)=>{e(()=>{let r=_e(t);for(let s of r)W(s,n)})}};var Gn={type:"action",name:"toggleAll",fn:({batch:e},t)=>{e(()=>{let n=_e(t);for(let r of n)W(r,!oe(r))})}};var je=new WeakMap,Wn={type:"attribute",name:"preserveAttr",valReq:"exclusive",keyReq:"exclusive",onLoad:({el:e,key:t,value:n})=>{let r=t?[t]:n.trim().split(" ");if(je.has(e)){let a=je.get(e);for(let o of r){let i=a[o];i!==void 0?e.setAttribute(o,i):e.removeAttribute(o)}}else{let a={};for(let o of r){let i=e.getAttribute(o);i!==null&&(a[o]=i)}je.set(e,a)}let s=new MutationObserver(a=>{for(let{attributeName:o,target:i}of a){let l=i,c=je.get(l);if(c){let u=o,p=l.getAttribute(u);p!==null?c[u]=p:delete c[u]}}});return s.observe(e,{attributes:!0,attributeFilter:r}),()=>{s&&(s.disconnect(),s=null)}}};He(Zt,tn,nn,en,Yt,yn,Sn,En,Tn,rn,xn,wn,Mn,Rn,Dn,Cn,Ln,Fn,Pn,kn,Wn,Nn,In,Hn,$n,Gn);ot();export{ot as apply,He as load,$t as setAlias}; //# sourceMappingURL=datastar.js.map diff --git a/resources/datastar.js.map b/resources/datastar.js.map index a764956..4a676d7 100644 --- a/resources/datastar.js.map +++ b/resources/datastar.js.map @@ -1,7 +1,7 @@ { "version": 3, - "sources": ["../library/src/engine/consts.ts", "../library/src/engine/types.ts", "../library/src/plugins/official/core/attributes/computed.ts", "../library/src/utils/text.ts", "../library/src/plugins/official/core/attributes/signals.ts", "../library/src/plugins/official/core/attributes/star.ts", "../library/src/utils/dom.ts", "../library/src/engine/errors.ts", "../library/src/vendored/preact-core.ts", "../library/src/engine/signals.ts", "../library/src/engine/engine.ts", "../library/src/engine/index.ts", "../library/src/vendored/fetch-event-source.ts", "../library/src/plugins/official/backend/shared.ts", "../library/src/plugins/official/backend/actions/sse.ts", "../library/src/plugins/official/backend/actions/delete.ts", "../library/src/plugins/official/backend/actions/get.ts", "../library/src/plugins/official/backend/actions/patch.ts", "../library/src/plugins/official/backend/actions/post.ts", "../library/src/plugins/official/backend/actions/put.ts", "../library/src/plugins/official/backend/attributes/indicator.ts", "../library/src/plugins/official/backend/watchers/executeScript.ts", "../library/src/utils/view-transtions.ts", "../library/src/vendored/idiomorph.ts", "../library/src/plugins/official/backend/watchers/mergeFragments.ts", "../library/src/plugins/official/backend/watchers/mergeSignals.ts", "../library/src/plugins/official/backend/watchers/removeFragments.ts", "../library/src/plugins/official/backend/watchers/removeSignals.ts", "../library/src/plugins/official/browser/actions/clipboard.ts", "../library/src/plugins/official/browser/attributes/customValidity.ts", "../library/src/plugins/official/browser/attributes/intersects.ts", "../library/src/plugins/official/browser/attributes/persist.ts", "../library/src/plugins/official/browser/attributes/replaceUrl.ts", "../library/src/plugins/official/browser/attributes/scrollIntoView.ts", "../library/src/plugins/official/browser/attributes/show.ts", "../library/src/plugins/official/browser/attributes/viewTransition.ts", "../library/src/plugins/official/dom/attributes/attr.ts", "../library/src/plugins/official/dom/attributes/bind.ts", "../library/src/plugins/official/dom/attributes/class.ts", "../library/src/utils/tags.ts", "../library/src/utils/timing.ts", "../library/src/plugins/official/dom/attributes/on.ts", "../library/src/plugins/official/dom/attributes/ref.ts", "../library/src/plugins/official/dom/attributes/text.ts", "../library/src/plugins/official/logic/actions/fit.ts", "../library/src/plugins/official/logic/actions/setAll.ts", "../library/src/plugins/official/logic/actions/toggleAll.ts", "../library/src/bundles/datastar.ts"], - "sourcesContent": ["// This is auto-generated by Datastar. DO NOT EDIT.\nconst lol = /\uD83D\uDD95JS_DS\uD83D\uDE80/.source\nexport const DSP = lol.slice(0, 5)\nexport const DSS = lol.slice(4)\n\nexport const DATASTAR = \"datastar\";\nexport const DATASTAR_EVENT = \"datastar-event\";\nexport const DATASTAR_REQUEST = \"Datastar-Request\";\nexport const VERSION = \"1.0.0-beta.2\";\n\n// #region Defaults\n\n// #region Default durations\n\n// The default duration for settling during fragment merges. Allows for CSS transitions to complete.\nexport const DefaultFragmentsSettleDurationMs = 300;\n// The default duration for retrying SSE on connection reset. This is part of the underlying retry mechanism of SSE.\nexport const DefaultSseRetryDurationMs = 1000;\n\n// #endregion\n\n\n// #region Default strings\n\n// The default attributes for