Compare commits
12
Commits
v0.3.0
..
47d7f9d2c3
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
47d7f9d2c3 | ||
|
|
12e9fcd609 | ||
|
|
72674afe05 | ||
|
|
7ba9b08d76 | ||
|
|
1a45839699 | ||
|
|
be54f54804 | ||
|
|
8212477377 | ||
|
|
e97470c2f6 | ||
|
|
9f26203ed5 | ||
|
|
d6fdcd0bce | ||
|
|
d65c5593b3 | ||
|
|
330ab11fe9 |
@@ -68,11 +68,14 @@ jobs:
|
||||
stage=$(mktemp -d)
|
||||
cp "$CARGO_TARGET_DIR/release/portal" "$stage/portal"
|
||||
cp "$CARGO_TARGET_DIR/release/question_lint" "$stage/question_lint"
|
||||
# hash-files = true: leptos resolves hash.txt next to the running
|
||||
# binary, so it ships beside portal in the release dir.
|
||||
cp "$CARGO_TARGET_DIR/release/hash.txt" "$stage/hash.txt"
|
||||
# site-root ("target/site" in Cargo.toml) is project-relative,
|
||||
# not affected by CARGO_TARGET_DIR - only the plain `cargo build`
|
||||
# outputs (release/, front/) move with that override.
|
||||
cp -r target/site "$stage/site"
|
||||
tar -C "$stage" -czf "portal-$tag.tar.gz" portal question_lint site
|
||||
tar -C "$stage" -czf "portal-$tag.tar.gz" portal question_lint hash.txt site
|
||||
rm -rf "$stage"
|
||||
|
||||
# The run's own ephemeral token has write access to this repo -
|
||||
|
||||
Generated
+1
-1
@@ -2948,7 +2948,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "portal"
|
||||
version = "0.3.0"
|
||||
version = "0.3.5"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"arc-swap",
|
||||
|
||||
+7
-1
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "portal"
|
||||
version = "0.3.0"
|
||||
version = "0.3.5"
|
||||
edition = "2021"
|
||||
|
||||
[lib]
|
||||
@@ -131,6 +131,12 @@ bin-default-features = false
|
||||
lib-features = ["hydrate"]
|
||||
lib-default-features = false
|
||||
lib-profile-release = "wasm-release"
|
||||
# Content-hashed pkg names (portal.<hash>.js/wasm/css) + hash.txt: a
|
||||
# freshly served page can never reference a stale cached bundle - an
|
||||
# iPhone kept a heuristically-cached portal.js across three releases
|
||||
# and paired it with new wasm, silently killing hydration. Runtime
|
||||
# needs LEPTOS_HASH_FILES=true (set in each content repo's deploy env).
|
||||
hash-files = true
|
||||
|
||||
# cargo release <level> is how a portal version is cut: bump, commit,
|
||||
# tag v{{version}}, push. CI (publish.yml) reacts to the tag and
|
||||
|
||||
@@ -105,7 +105,10 @@ cargo release patch # or minor / major
|
||||
That bumps `Cargo.toml`, tags `v<version>`, and pushes; CI
|
||||
(`.gitea/workflows/publish.yml`) reacts to the tag, builds once, and
|
||||
attaches `portal-v<version>.tar.gz` (`portal` + `question_lint` +
|
||||
`site/`) to the Gitea release. Plain pushes to `main` only run the
|
||||
`hash.txt` + `site/`) to the Gitea release. Pkg files are
|
||||
content-hashed (`hash-files = true`; instances run with
|
||||
`LEPTOS_HASH_FILES=true`), so a freshly served page can never pair a
|
||||
stale cached bundle with new wasm. Plain pushes to `main` only run the
|
||||
tests (`test.yml`) — nothing reaches production from this repo.
|
||||
|
||||
**Roll it out** (in a content repo): edit one line in that repo's
|
||||
|
||||
+12
-8
@@ -12,8 +12,10 @@
|
||||
//
|
||||
// When a relay URL is given, the widget also speaks the redoal-relay
|
||||
// protocol (ADR-0013 in the redoal repo): announce the stroke, receive
|
||||
// an ack carrying the stroke's gesture key + the key's own decoded
|
||||
// path ("what the network heard", drawn as a ghost), and receive
|
||||
// an ack carrying the stroke's gesture key plus the key's own decoded
|
||||
// path - "what the network heard", drawn as a ghost under the stroke
|
||||
// when the key is v2 or later (ADR-0014's ordered turning chain; a v1
|
||||
// key's histogram decode is a blob and stays hidden) - and receive
|
||||
// echoes of similar strokes other visitors drew, shown as thumbnails.
|
||||
// Everything network is best-effort: no relay, refused connection, or
|
||||
// a dropped socket all degrade to a plain offline drawing input.
|
||||
@@ -26,13 +28,13 @@
|
||||
const THEMES = {
|
||||
dark: {
|
||||
stroke: '#8ec2c0',
|
||||
ghost: 'rgba(255, 255, 255, 0.28)',
|
||||
echo: '#8ec2c0',
|
||||
ghost: 'rgba(255, 255, 255, 0.28)',
|
||||
},
|
||||
light: {
|
||||
stroke: '#47807e',
|
||||
ghost: 'rgba(29, 29, 27, 0.30)',
|
||||
echo: '#47807e',
|
||||
ghost: 'rgba(29, 29, 27, 0.30)',
|
||||
},
|
||||
};
|
||||
|
||||
@@ -46,8 +48,8 @@ class GestureWidget {
|
||||
this.container = container;
|
||||
this.hidden = hidden || null;
|
||||
this.relayUrl = relayUrl || '';
|
||||
this.points = [];
|
||||
this.ghost = null;
|
||||
this.points = [];
|
||||
this.drawing = false;
|
||||
this.stopped = false;
|
||||
this.ws = null;
|
||||
@@ -111,10 +113,10 @@ class GestureWidget {
|
||||
e.preventDefault();
|
||||
this.canvas.setPointerCapture(e.pointerId);
|
||||
// One stroke only - a new pointerdown replaces the old drawing
|
||||
// (and any ghost/ambient trails from the previous round).
|
||||
// (and any ghost from the previous round).
|
||||
this.drawing = true;
|
||||
this.points = [this.pos(e)];
|
||||
this.ghost = null;
|
||||
this.points = [this.pos(e)];
|
||||
this.render();
|
||||
}
|
||||
|
||||
@@ -215,7 +217,9 @@ class GestureWidget {
|
||||
if (this.points.length >= 2) {
|
||||
this.setValue({ points: this.normalized(), key: msg.key });
|
||||
}
|
||||
this.ghost = msg.path;
|
||||
// Version nibble is the key's first hex digit.
|
||||
const version = parseInt((msg.key || '0')[0], 16);
|
||||
this.ghost = version >= 2 && Array.isArray(msg.path) ? msg.path : null;
|
||||
this.render();
|
||||
} else if (msg.type === 'echo') {
|
||||
this.addEchoThumbnail(msg);
|
||||
|
||||
+34
-6
@@ -1,5 +1,5 @@
|
||||
use leptos::prelude::*;
|
||||
use leptos_meta::{provide_meta_context, MetaTags, Stylesheet, Title};
|
||||
use leptos_meta::{provide_meta_context, HashedStylesheet, MetaTags, Title};
|
||||
use leptos_router::{
|
||||
components::{Route, Router, Routes},
|
||||
hooks::{use_location, use_navigate, use_query_map},
|
||||
@@ -39,6 +39,9 @@ pub fn shell(options: LeptosOptions) -> impl IntoView {
|
||||
// once loaded via <link>/<img> on Safari/iOS.
|
||||
<link rel="icon" media="(prefers-color-scheme: light)" href="/favicon-light.svg" type="image/svg+xml"/>
|
||||
<link rel="icon" media="(prefers-color-scheme: dark)" href="/favicon-dark.svg" type="image/svg+xml"/>
|
||||
// Server-side, so it can read hash.txt and link the
|
||||
// content-hashed stylesheet (hash-files = true).
|
||||
<HashedStylesheet id="leptos" options=options.clone()/>
|
||||
<AutoReload options=options.clone()/>
|
||||
<HydrationScripts options/>
|
||||
<MetaTags/>
|
||||
@@ -67,7 +70,6 @@ pub fn App() -> impl IntoView {
|
||||
provide_context(site);
|
||||
|
||||
view! {
|
||||
<Stylesheet id="leptos" href="/pkg/portal.css"/>
|
||||
<Suspense fallback=|| ()>
|
||||
{move || {
|
||||
site.get()
|
||||
@@ -898,6 +900,7 @@ fn AlternativeCard(
|
||||
alternative=alt_name.clone()
|
||||
feature_name=feature_name.clone()
|
||||
transitions=spec.transitions.clone()
|
||||
empty=spec.empty.clone()
|
||||
pending_transitions=pending_transitions
|
||||
transition_batch=transition_batch
|
||||
/>
|
||||
@@ -1174,7 +1177,17 @@ fn AlternativeCard(
|
||||
children=move |e| view! { <p class="alt-encouragement">{e}</p> }
|
||||
/>
|
||||
</div>
|
||||
{if is_gateway {
|
||||
{if alternative.disabled {
|
||||
// Advertised, not yet takeable: the label
|
||||
// stays (it says what will be possible),
|
||||
// the button doesn't act.
|
||||
view! {
|
||||
<button type="button" class="alt-submit soon" disabled=true title="Not yet">
|
||||
{button_label.clone()}
|
||||
</button>
|
||||
}
|
||||
.into_any()
|
||||
} else if is_gateway {
|
||||
let href = alternative.action.clone().unwrap_or_default();
|
||||
view! {
|
||||
<a class="alt-submit" href=href>{button_label.clone()}</a>
|
||||
@@ -1217,6 +1230,7 @@ fn ResourceFeature(
|
||||
alternative: String,
|
||||
feature_name: String,
|
||||
transitions: Vec<Transition>,
|
||||
empty: Option<String>,
|
||||
pending_transitions: RwSignal<std::collections::HashMap<(String, String), String>>,
|
||||
transition_batch: ServerAction<TransitionAnswers>,
|
||||
) -> impl IntoView {
|
||||
@@ -1250,6 +1264,7 @@ fn ResourceFeature(
|
||||
{move || {
|
||||
let feature_name = feature_name.clone();
|
||||
let transitions = transitions.clone();
|
||||
let empty = empty.clone().unwrap_or_else(|| "Nothing here yet.".to_string());
|
||||
data.get()
|
||||
.map(|res| match res {
|
||||
Ok(value) => {
|
||||
@@ -1257,6 +1272,7 @@ fn ResourceFeature(
|
||||
<ResourceValue
|
||||
feature_name=feature_name
|
||||
transitions=transitions
|
||||
empty=empty
|
||||
value=value
|
||||
pending_transitions=pending_transitions
|
||||
transition_batch=transition_batch
|
||||
@@ -1283,11 +1299,23 @@ fn ResourceFeature(
|
||||
fn ResourceValue(
|
||||
feature_name: String,
|
||||
transitions: Vec<Transition>,
|
||||
empty: String,
|
||||
value: serde_json::Value,
|
||||
pending_transitions: RwSignal<std::collections::HashMap<(String, String), String>>,
|
||||
transition_batch: ServerAction<TransitionAnswers>,
|
||||
) -> impl IntoView {
|
||||
// A jq like `.[] | {...}` over an empty list yields no output at
|
||||
// all - that arrives as null, and it's the same silence as [].
|
||||
if value.is_null() {
|
||||
return view! { <p class="resource-empty">{empty}</p> }.into_any();
|
||||
}
|
||||
if let serde_json::Value::Array(items) = &value {
|
||||
// Before the Answer parse below: an empty list deserializes as
|
||||
// an (empty) Vec<Answer> too, and rendered as a blank
|
||||
// answer-list instead of saying anything.
|
||||
if items.is_empty() {
|
||||
return view! { <p class="resource-empty">{empty}</p> }.into_any();
|
||||
}
|
||||
if let Ok(answers) = serde_json::from_value::<Vec<Answer>>(value.clone()) {
|
||||
let mut answers = answers;
|
||||
answers.sort_by(|a, b| {
|
||||
@@ -1320,9 +1348,6 @@ fn ResourceValue(
|
||||
}
|
||||
.into_any();
|
||||
}
|
||||
if items.is_empty() {
|
||||
return view! { <p class="resource-empty">"Nothing here yet."</p> }.into_any();
|
||||
}
|
||||
// A list of plain objects (e.g. a jq-shaped GiteaStarred/Url
|
||||
// resource) - render as cards rather than dumping raw JSON.
|
||||
// Generic on purpose, no content-declared "kind": `name`/`title`
|
||||
@@ -1845,6 +1870,9 @@ pub async fn submit_answer(
|
||||
.iter()
|
||||
.find(|a| a.name == alternative)
|
||||
.ok_or_else(|| ServerFnError::new("unknown alternative"))?;
|
||||
if alt.disabled {
|
||||
return Err(ServerFnError::new("this alternative isn't open yet"));
|
||||
}
|
||||
let next = alt.action.clone();
|
||||
let record_as = alt.record_as.clone();
|
||||
|
||||
|
||||
@@ -111,6 +111,11 @@ pub struct Alternative {
|
||||
pub description: String,
|
||||
#[serde(default)]
|
||||
pub action: Option<String>,
|
||||
/// Shown but not takeable yet - the button renders disabled and the
|
||||
/// server refuses the submission. For advertising a path before it
|
||||
/// works (lysbue had the same flag).
|
||||
#[serde(default)]
|
||||
pub disabled: bool,
|
||||
#[serde(default)]
|
||||
pub consequence: Vec<String>,
|
||||
#[serde(default)]
|
||||
@@ -202,6 +207,12 @@ pub struct ResourceSpec {
|
||||
/// `.[] | {name, url: .html_url}`. `None` returns it as-is.
|
||||
#[serde(default)]
|
||||
pub jq: Option<String>,
|
||||
/// What to say when the resource yields nothing - an empty list,
|
||||
/// or `null` (a `.[] | ...` jq over an empty list produces no
|
||||
/// output at all). Defaults to "Nothing here yet."; content sets
|
||||
/// it where the silence needs a voice ("Nothing released yet").
|
||||
#[serde(default)]
|
||||
pub empty: Option<String>,
|
||||
}
|
||||
|
||||
/// Where a resource's live data comes from. `Kv` (a NATS KV bucket
|
||||
|
||||
@@ -675,6 +675,13 @@ textarea:focus {
|
||||
cursor: wait;
|
||||
}
|
||||
|
||||
/* A content-disabled alternative (`disabled: true`): announced, not
|
||||
yet takeable - reads as a promise, not as a stuck form. */
|
||||
.alt-submit.soon:disabled {
|
||||
cursor: not-allowed;
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
.gate-card {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user