Compare commits

...
Author SHA1 Message Date
Tanish Manku df9654fed9 Merge remote-tracking branch 'services/main' into design/proofing 2026-09-14 15:26:26 -07:00
Brandon Thomas 8b086dd385 Merge pull request #9 from storytold/bt-fix-download-links
fix download links
2026-09-14 14:24:16 -04:00
Brandon Thomas 4d9efb9385 fix download links 2026-09-14 14:23:06 -04:00
Brandon Thomas c619b291a4 Merge pull request #8 from storytold/bt-seedance-2p5-high-bitrate
high bitrate seedance 2.5
2026-09-12 13:24:21 -04:00
Brandon Thomas 9c1633bd9c high bitrate seedance 2.5 2026-09-12 13:22:42 -04:00
Brandon Thomas 32a6e6470f Merge pull request #7 from storytold/bt-remove-desktop-app
remove the desktop app from the repo
2026-09-12 12:28:28 -04:00
Tanish Manku 239ae4ada2 hero: click boost 2x -> 3x (tuner max raised to 4x)
Claude-Session: https://claude.ai/code/session_01KpbGFYxEXuaYU9J75vTK9j
2026-09-11 19:31:51 -07:00
Tanish Manku 33d50851b0 fix Firefox back-navigation dead page: bump Next 16.2.6 -> 16.2.12
Reproduced in a real Firefox (Playwright): returning to the page via
browser Back was NOT a bfcache restore (pageshow persisted=false,
navType back_forward) — it was a fresh load on which Next's client
router threw "Internal Next.js error: Router action dispatched before
initialization" three times, aborting hydration. No effects ran, so
data-ruler was never stamped, the galaxy canvas never mounted, and
data-intro never lifted (the wordmark stayed CSS-hidden at opacity 0)
— the whole page looked unmounted.

The 16.2.12 patch fixes the router-initialization race; the same repro
now comes back fully mounted. The Firefox-gated unload bfcache-buster
from the previous attempt is removed (wrong theory, and it cost
Firefox users instant-back site-wide); the pageshow persisted reload
guard stays as insurance for genuinely broken bfcache restores.
Production build verified on 16.2.12.

Claude-Session: https://claude.ai/code/session_01KpbGFYxEXuaYU9J75vTK9j
2026-09-11 13:55:50 -07:00
11 changed files with 186 additions and 69 deletions

No files matched your search

+1
View File
@@ -39,6 +39,7 @@ venv/
# LLM Testing
/external/
/prompts/
/untracked/
/reports/
/requests/
@@ -1,9 +1,11 @@
use kinovi_web_client::generate::video::generate_seedance_2p5::{
KinoviSeedance2p5AspectRatio as KinoviAspectRatio,
KinoviSeedance2p5Bitrate,
KinoviSeedance2p5OutputResolution as KinoviOutputResolution,
};
use crate::api::router_aspect_ratio::RouterAspectRatio;
use crate::api::router_bitrate::RouterBitrate;
use crate::api::router_resolution::RouterResolution;
use crate::client::request_mismatch_mitigation_strategy::RequestMismatchMitigationStrategy;
use crate::errors::artcraft_router_error::ArtcraftRouterError;
@@ -64,8 +66,10 @@ fn do_build_kinovi_seedance_2p5(mut builder: GenerateVideoRequestBuilder) -> Res
let total_input_seconds = builder.total_reference_video_input_seconds
.map(|seconds| u8::try_from(seconds).unwrap_or(u8::MAX));
// NB: `builder.bitrate` is intentionally ignored — 2.5 has no bitrate
// option (and bitrate never affects cost).
let maybe_bitrate = match builder.bitrate.take() {
Some(RouterBitrate::High) => Some(KinoviSeedance2p5Bitrate::High),
Some(RouterBitrate::Normal) | None => None,
};
let unhandled_request_state = KinoviSeedance2p5RemainingItems {
start_frame: builder.start_frame.take(),
@@ -81,6 +85,7 @@ fn do_build_kinovi_seedance_2p5(mut builder: GenerateVideoRequestBuilder) -> Res
duration_seconds,
prompt,
total_input_seconds,
maybe_bitrate,
unhandled_request_state: Some(unhandled_request_state),
})
}
@@ -246,6 +251,7 @@ fn plan_duration(
#[cfg(test)]
mod tests {
use kinovi_web_client::creds::kinovi_web_session::KinoviWebSession;
use kinovi_web_client::generate::video::generate_seedance_2p5::{
KinoviSeedance2p5AspectRatio as KinoviAspectRatio,
KinoviSeedance2p5OutputResolution as KinoviOutputResolution,
@@ -261,13 +267,50 @@ mod tests {
use crate::api::router_video_model::RouterVideoModel;
use crate::api::video_list_ref::VideoListRef;
use crate::client::request_mismatch_mitigation_strategy::RequestMismatchMitigationStrategy;
use crate::client::router_client::RouterClient;
use crate::client::router_kinovi_web_client::RouterKinoviWebClient;
use crate::generate::generate_video::generate_video_request_builder::GenerateVideoRequestBuilder;
use crate::generate::generate_video::providers::kinovi::seedance_2p5::cost::KinoviSeedance2p5CostState;
use crate::generate::generate_video::providers::kinovi::seedance_2p5::draft::KinoviSeedance2p5DraftState;
use crate::generate::generate_video::video_generation_draft::VideoGenerationDraftRequest;
use crate::generate::generate_video::video_generation_draft_context::VideoGenerationDraftContext;
use crate::generate::generate_video::video_generation_draft_or_request::VideoGenerationDraftOrRequest;
use super::*;
#[tokio::test]
async fn bitrate_survives_finalization_without_changing_cost() {
let client = RouterClient::KinoviWeb(RouterKinoviWebClient::new(
KinoviWebSession::from_cookies_string(String::new()),
));
let context = VideoGenerationDraftContext {
client: Some(&client),
..Default::default()
};
let baseline = unwrap_draft(build_kinovi_seedance_2p5(base_builder()));
let baseline_cost = KinoviSeedance2p5CostState::from_draft(&baseline).estimate_cost();
for bitrate in [None, Some(RouterBitrate::Normal), Some(RouterBitrate::High)] {
let mut draft = unwrap_draft(build_kinovi_seedance_2p5(GenerateVideoRequestBuilder {
bitrate,
..base_builder()
}));
let draft_cost = KinoviSeedance2p5CostState::from_draft(&draft).estimate_cost();
// No media inputs: finalization needs no network calls or credentials.
let finalized = draft.to_request(&context).await.expect("finalize should succeed");
assert_eq!(
matches!(finalized.request.maybe_bitrate, Some(KinoviSeedance2p5Bitrate::High)),
matches!(bitrate, Some(RouterBitrate::High)),
);
let final_cost = KinoviSeedance2p5CostState::from_request(&finalized).estimate_cost();
for cost in [draft_cost, final_cost] {
assert_eq!(cost.cost_in_credits, baseline_cost.cost_in_credits);
assert_eq!(cost.cost_in_usd_cents, baseline_cost.cost_in_usd_cents);
}
}
}
mod materialized_field_conversions {
use super::*;
@@ -91,6 +91,7 @@ impl KinoviSeedance2p5CostState {
duration_seconds: self.duration_seconds,
total_input_seconds,
use_face_blur_hack: None,
maybe_bitrate: None,
};
// Enterprise tier: what generations actually cost us (our discounted
@@ -1,5 +1,5 @@
use kinovi_web_client::generate::video::generate_seedance_2p5::{
GenerateSeedance2p5Request, KinoviSeedance2p5AspectRatio, KinoviSeedance2p5Modality,
GenerateSeedance2p5Request, KinoviSeedance2p5AspectRatio, KinoviSeedance2p5Bitrate, KinoviSeedance2p5Modality,
KinoviSeedance2p5OutputResolution,
};
@@ -32,6 +32,8 @@ pub struct KinoviSeedance2p5DraftState {
/// all reference videos (each rounded up to a whole second).
pub total_input_seconds: Option<u8>,
pub maybe_bitrate: Option<KinoviSeedance2p5Bitrate>,
pub unhandled_request_state: Option<KinoviSeedance2p5RemainingItems>,
}
@@ -111,6 +113,7 @@ impl KinoviSeedance2p5DraftState {
duration_seconds: self.duration_seconds,
total_input_seconds: self.total_input_seconds,
use_face_blur_hack: None,
maybe_bitrate: self.maybe_bitrate,
};
Ok(KinoviSeedance2p5RequestState { request })
@@ -6,7 +6,7 @@ use crate::pricing::kinovi_pricing_rate::KinoviPricingRate;
use crate::pricing::kinovi_pricing_tier::KinoviPricingTier;
use crate::requests::kinovi_host::KinoviHost;
use crate::requests::workflow_run_task::workflow_run_task::{
workflow_run_task, KinoviAspectRatioRaw, KinoviBatchCountRaw, KinoviModelTypeRaw,
workflow_run_task, KinoviAspectRatioRaw, KinoviBatchCountRaw, KinoviBitrateRaw, KinoviModelTypeRaw,
KinoviOutputResolutionRaw, WorkflowRunTaskArgs, WorkflowRunTaskRequest,
};
@@ -60,6 +60,10 @@ pub struct GenerateSeedance2p5Request {
/// Controls `faceBlurMode`: true sends "on"; false or None sends "off"
/// (the model always sends the field, like 2.5 Preview).
pub use_face_blur_hack: Option<bool>,
/// Output video bitrate. None keeps the standard bitrate; `High` requests
/// a higher bitrate. This does not select a container or lossless codec.
pub maybe_bitrate: Option<KinoviSeedance2p5Bitrate>,
}
// ── Modality ──
@@ -115,6 +119,11 @@ pub enum KinoviSeedance2p5OutputResolution {
TenEightyP,
}
#[derive(Debug, Clone, Copy)]
pub enum KinoviSeedance2p5Bitrate {
High,
}
// ── Pricing ──
//
// Seedance 2.5 credit pricing (credits per billed second). Without video
@@ -295,7 +304,14 @@ fn to_raw_request(req: GenerateSeedance2p5Request) -> WorkflowRunTaskRequest {
reference_audio_urls,
character_ids: None,
use_face_blur_hack: req.use_face_blur_hack,
bitrate: None,
bitrate: map_bitrate(req.maybe_bitrate),
}
}
fn map_bitrate(maybe_bitrate: Option<KinoviSeedance2p5Bitrate>) -> Option<KinoviBitrateRaw> {
match maybe_bitrate {
Some(KinoviSeedance2p5Bitrate::High) => Some(KinoviBitrateRaw::High),
None => None,
}
}
@@ -748,6 +764,7 @@ mod tests {
duration_seconds,
total_input_seconds: None,
use_face_blur_hack: None,
maybe_bitrate: None,
}
}
@@ -774,6 +791,7 @@ mod tests {
duration_seconds: duration,
total_input_seconds: None,
use_face_blur_hack: None,
maybe_bitrate: None,
}
}
@@ -833,9 +851,11 @@ mod tests {
duration_seconds: 8,
total_input_seconds: None,
use_face_blur_hack: None,
maybe_bitrate: None,
});
assert!(matches!(raw.model_type, KinoviModelTypeRaw::Seedance2p5));
assert!(raw.bitrate.is_none());
assert_eq!(raw.start_frame_url.as_deref(), Some("https://example.com/start.png"));
assert_eq!(raw.end_frame_url.as_deref(), Some("https://example.com/end.png"));
assert!(raw.reference_image_urls.is_none());
@@ -857,9 +877,11 @@ mod tests {
duration_seconds: 8,
total_input_seconds: Some(7),
use_face_blur_hack: None,
maybe_bitrate: Some(KinoviSeedance2p5Bitrate::High),
});
assert!(matches!(raw.aspect_ratio, KinoviAspectRatioRaw::UltraWide21x9));
assert!(matches!(raw.bitrate, Some(KinoviBitrateRaw::High)));
assert!(raw.start_frame_url.is_none());
assert!(raw.end_frame_url.is_none());
assert_eq!(raw.reference_image_urls.as_deref().map(|urls| urls.len()), Some(1));
@@ -881,6 +903,7 @@ mod tests {
duration_seconds: 4,
total_input_seconds: None,
use_face_blur_hack: None,
maybe_bitrate: None,
});
assert!(matches!(raw.output_resolution, Some(KinoviOutputResolutionRaw::TenEightyP)));
@@ -900,6 +923,7 @@ mod tests {
duration_seconds: 5,
total_input_seconds: None,
use_face_blur_hack: None,
maybe_bitrate: None,
});
assert!(matches!(raw.aspect_ratio, KinoviAspectRatioRaw::Landscape16x9));
@@ -929,6 +953,7 @@ mod tests {
duration_seconds: 5,
total_input_seconds: None,
use_face_blur_hack: None,
maybe_bitrate: None,
},
}).await?;
println!("t2v 480p — task_id={}, order_id={}", result.task_id, result.order_id);
@@ -960,6 +985,7 @@ mod tests {
duration_seconds: 4,
total_input_seconds: None,
use_face_blur_hack: None,
maybe_bitrate: None,
},
}).await?;
println!("t2v 1080p — task_id={}, order_id={}", result.task_id, result.order_id);
@@ -987,6 +1013,7 @@ mod tests {
duration_seconds: 4,
total_input_seconds: None,
use_face_blur_hack: None,
maybe_bitrate: None,
},
}).await?;
println!("keyframe 1080p — task_id={}, order_id={}", result.task_id, result.order_id);
@@ -1014,6 +1041,7 @@ mod tests {
duration_seconds: 8,
total_input_seconds: None,
use_face_blur_hack: None,
maybe_bitrate: None,
},
}).await?;
println!("keyframe 480p — task_id={}, order_id={}", result.task_id, result.order_id);
@@ -1046,6 +1074,7 @@ mod tests {
duration_seconds: 8,
total_input_seconds: None,
use_face_blur_hack: None,
maybe_bitrate: None,
},
}).await?;
println!("reference 480p — task_id={}, order_id={}", result.task_id, result.order_id);
@@ -1,5 +1,6 @@
use artcraft_api_defs::omni_gen::models::omni_gen_video_models::OmniGenVideoModelDetails;
use enums::common::generation::common_aspect_ratio::CommonAspectRatio;
use enums::common::generation::common_bitrate::CommonBitrate;
use enums::common::generation::common_resolution::CommonResolution;
use enums::common::generation::common_video_model::CommonVideoModel;
use enums::common::generation::model_creator::ModelCreator;
@@ -42,6 +43,11 @@ pub fn seedance_2p5_video_models() -> Vec<OmniGenVideoModelDetails> {
CommonResolution::TenEightyP,
]),
resolution_default: Some(CommonResolution::SevenTwentyP),
bitrate_options: Some(vec![
CommonBitrate::Normal,
CommonBitrate::High,
]),
bitrate_default: Some(CommonBitrate::Normal),
batch_size_options: Some(vec![1]),
batch_size_default: Some(1),
duration_seconds_min: Some(4),
@@ -84,6 +90,11 @@ pub fn seedance_2p5_video_models() -> Vec<OmniGenVideoModelDetails> {
CommonResolution::TenEightyP,
]),
resolution_default: Some(CommonResolution::SevenTwentyP),
bitrate_options: Some(vec![
CommonBitrate::Normal,
CommonBitrate::High,
]),
bitrate_default: Some(CommonBitrate::Normal),
batch_size_options: Some(vec![1]),
batch_size_default: Some(1),
duration_seconds_min: Some(4),
@@ -125,6 +136,11 @@ pub fn seedance_2p5_video_models() -> Vec<OmniGenVideoModelDetails> {
CommonResolution::SevenTwentyP,
]),
resolution_default: Some(CommonResolution::SevenTwentyP),
bitrate_options: Some(vec![
CommonBitrate::Normal,
CommonBitrate::High,
]),
bitrate_default: Some(CommonBitrate::Normal),
batch_size_options: Some(vec![1]),
batch_size_default: Some(1),
duration_seconds_min: Some(4),
+52 -40
View File
@@ -15,7 +15,7 @@
"gsap": "3.15.0",
"lenis": "1.3.23",
"lucide-react": "^1.31.0",
"next": "16.2.6",
"next": "^16.2.12",
"react": "19.2.6",
"react-dom": "19.2.6",
"tailwind-merge": "^3.6.0",
@@ -608,15 +608,15 @@
}
},
"node_modules/@next/env": {
"version": "16.2.6",
"resolved": "https://registry.npmjs.org/@next/env/-/env-16.2.6.tgz",
"integrity": "sha512-gd8HoHN4ufj73WmR3JmVolrpJR47ILK6LouP5xElPglaVxir6e1a7VzvTvDWkOoPXT9rkkTzyCxBu4yeZfZwcw==",
"version": "16.2.12",
"resolved": "https://registry.npmjs.org/@next/env/-/env-16.2.12.tgz",
"integrity": "sha512-d0Z5Bc13Fa4nR8pFAKx2jay2yhJM16vlfHbTzYnUQAxlNb6B6lmn4hjt69lYNt4kRtyYP6gEM49lPRHNbIyneg==",
"license": "MIT"
},
"node_modules/@next/swc-darwin-arm64": {
"version": "16.2.6",
"resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-16.2.6.tgz",
"integrity": "sha512-ZJGkkcNfYgrrMkqOdZ7zoLa1TOy0qpcMfk/z4Mh/FKUz40gVO+HNQWqmLxf67Z5WB64DRp0dhEbyHfel+6sJUg==",
"version": "16.2.12",
"resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-16.2.12.tgz",
"integrity": "sha512-0W1R0teHWJrqKX0FH20IzzIWAOuGtBxPGuObrxy1lE8hQvCFj49KE8a3WUg0D7sq6rn6zkM4c7YGUnhudBS6oA==",
"cpu": [
"arm64"
],
@@ -630,9 +630,9 @@
}
},
"node_modules/@next/swc-darwin-x64": {
"version": "16.2.6",
"resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-16.2.6.tgz",
"integrity": "sha512-v/YLBHIY132Ced3puBJ7YJKw1lqsCrgcNo2aRJlCEyQrrCeRJlvGlnmxhPxNQI3KE3N1DN5r9TPNPvka3nq5RQ==",
"version": "16.2.12",
"resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-16.2.12.tgz",
"integrity": "sha512-Hy5Ls099+aFUmOLmIgPfLqNi6iCwhL3uQCssz5rWk+5Nkc6TUKCE83DY5BbNylfm3+mfwcSFnLRfrZDJhVxdtw==",
"cpu": [
"x64"
],
@@ -646,12 +646,15 @@
}
},
"node_modules/@next/swc-linux-arm64-gnu": {
"version": "16.2.6",
"resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-16.2.6.tgz",
"integrity": "sha512-RPOvqlYBbcQjkz9VQQDZ2T2bARIjXZV1KFlt+V2Mr6SW/e4I9fcKsaA0hdyf2FHoTlsV2xnBd5Y912rP/1Ce6w==",
"version": "16.2.12",
"resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-16.2.12.tgz",
"integrity": "sha512-+YqU2h1cQkHsGfvjAsrSmst8UIFBibBGm5x3Xgel8NLMiDQtNOM4sM2GOEMvG5YiOBNeN/Ykk8cQC2S0Xrqljg==",
"cpu": [
"arm64"
],
"libc": [
"glibc"
],
"license": "MIT",
"optional": true,
"os": [
@@ -662,12 +665,15 @@
}
},
"node_modules/@next/swc-linux-arm64-musl": {
"version": "16.2.6",
"resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-16.2.6.tgz",
"integrity": "sha512-URUTu1+dMkxJsPFgm+OeEvq9wf5sujw0EvgYy80TDGHTSLTnIHeqb0Eu8A3sC95IRgjejQL+kC4mw+4yPxiAXA==",
"version": "16.2.12",
"resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-16.2.12.tgz",
"integrity": "sha512-0qjhiYBaKAqF63LA1ZWAAnKTzFUguAaZiRa5etMLGGPj/B6uEVjtIZldIzFEp3wHlB0koK6aTzqPtSdplTCjoA==",
"cpu": [
"arm64"
],
"libc": [
"musl"
],
"license": "MIT",
"optional": true,
"os": [
@@ -678,12 +684,15 @@
}
},
"node_modules/@next/swc-linux-x64-gnu": {
"version": "16.2.6",
"resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-16.2.6.tgz",
"integrity": "sha512-DOj182mPV8G3UkrayLoREM5YEYI+Dk5wv7Ox9xl1fFibAELEsFD0lDPfHIeILlutMMfdyhlzYPELG3peuKaurw==",
"version": "16.2.12",
"resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-16.2.12.tgz",
"integrity": "sha512-7A3q26W+h7gnA15uqBToNuDqBEFZZcqh0mW2mn4AJh/G5pdg2RVE3n4slzLEliASZFG3NmsbEzng/x2Sh09mBg==",
"cpu": [
"x64"
],
"libc": [
"glibc"
],
"license": "MIT",
"optional": true,
"os": [
@@ -694,12 +703,15 @@
}
},
"node_modules/@next/swc-linux-x64-musl": {
"version": "16.2.6",
"resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-16.2.6.tgz",
"integrity": "sha512-HKQ5SP/V/ub73UvF7n/zeJlxk2kLmtL7Wzrg4WfmkjmNos5onJ2tKu7yZOPdL18A6Svfn3max29ym+ry7NkK4g==",
"version": "16.2.12",
"resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-16.2.12.tgz",
"integrity": "sha512-qSjL/uppm+cbh21s72Ss8gkiOhQ4dExWHNGOWy6eZV7STj5WsKehgxT61beSsOj+YYQuTplL376lOCdMQU5T8w==",
"cpu": [
"x64"
],
"libc": [
"musl"
],
"license": "MIT",
"optional": true,
"os": [
@@ -710,9 +722,9 @@
}
},
"node_modules/@next/swc-win32-arm64-msvc": {
"version": "16.2.6",
"resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-16.2.6.tgz",
"integrity": "sha512-LZXpTlPyS5v7HhSmnvsLGP3iIYgYOBnc8r8ArlT55sGHV89bR2HlDdBjWQ+PY6SJMmk8TuVGFuxalnP3k/0Dwg==",
"version": "16.2.12",
"resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-16.2.12.tgz",
"integrity": "sha512-X6hzsOUJac/e7AWSbn9gQ9nzHld1xWP5iyjHpYWvud8pufB679O1xg4JDyKr8Xd69Jvd+kM2Der6uftiZCmjYA==",
"cpu": [
"arm64"
],
@@ -726,9 +738,9 @@
}
},
"node_modules/@next/swc-win32-x64-msvc": {
"version": "16.2.6",
"resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-16.2.6.tgz",
"integrity": "sha512-F0+4i0h9J6C4eE3EAPWsoCk7UW/dbzOjyzxY0qnDUOYFu6FFmdZ6l97/XdV3/Nz3VYyO7UWjyEJUXkGqcoXfMA==",
"version": "16.2.12",
"resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-16.2.12.tgz",
"integrity": "sha512-F6fakeHuFTLOPt0bslQJdf+xtT+WIP9DVn/m4y1w1mRnVPyh3D/cNvzlRkxM444xfm+IvvYNSOrKiA2CDJ0Uxw==",
"cpu": [
"x64"
],
@@ -2209,12 +2221,12 @@
}
},
"node_modules/next": {
"version": "16.2.6",
"resolved": "https://registry.npmjs.org/next/-/next-16.2.6.tgz",
"integrity": "sha512-qOVgKJg1+At15NpeUP+eJgCHvTCgXsogweq87Ri/Ix7PkqQHg4sdaXmSFqKlgaIXE4kW0g25LE68W87UANlHtw==",
"version": "16.2.12",
"resolved": "https://registry.npmjs.org/next/-/next-16.2.12.tgz",
"integrity": "sha512-iD59eYQWmbFcEbX7v/acG5DRym9iw1DdaPoD0WTA920naWsE25wShzJW4+UvAs8MK9EC2kBfIH6vtto1H1PHGw==",
"license": "MIT",
"dependencies": {
"@next/env": "16.2.6",
"@next/env": "16.2.12",
"@swc/helpers": "0.5.15",
"baseline-browser-mapping": "^2.9.19",
"caniuse-lite": "^1.0.30001579",
@@ -2228,14 +2240,14 @@
"node": ">=20.9.0"
},
"optionalDependencies": {
"@next/swc-darwin-arm64": "16.2.6",
"@next/swc-darwin-x64": "16.2.6",
"@next/swc-linux-arm64-gnu": "16.2.6",
"@next/swc-linux-arm64-musl": "16.2.6",
"@next/swc-linux-x64-gnu": "16.2.6",
"@next/swc-linux-x64-musl": "16.2.6",
"@next/swc-win32-arm64-msvc": "16.2.6",
"@next/swc-win32-x64-msvc": "16.2.6",
"@next/swc-darwin-arm64": "16.2.12",
"@next/swc-darwin-x64": "16.2.12",
"@next/swc-linux-arm64-gnu": "16.2.12",
"@next/swc-linux-arm64-musl": "16.2.12",
"@next/swc-linux-x64-gnu": "16.2.12",
"@next/swc-linux-x64-musl": "16.2.12",
"@next/swc-win32-arm64-msvc": "16.2.12",
"@next/swc-win32-x64-msvc": "16.2.12",
"sharp": "^0.34.5"
},
"peerDependencies": {
@@ -19,7 +19,7 @@
"gsap": "3.15.0",
"lenis": "1.3.23",
"lucide-react": "^1.31.0",
"next": "16.2.6",
"next": "^16.2.12",
"react": "19.2.6",
"react-dom": "19.2.6",
"tailwind-merge": "^3.6.0",
@@ -101,11 +101,13 @@ const JSON_LD = {
// full word flashes for the frames between paint and hydration, then
// snaps into the logo-only formation start.
//
// Also reloads on back/forward-cache restores: Firefox freezes the JS heap
// and discards the WebGL context, so a restored page shows a dead galaxy
// canvas and stalled instruments. The listener is attached here (pre-paint)
// so it lives inside the frozen page and fires on the restore; the intro is
// seen-flagged by then, so the reload comes back settled.
// Also reloads on back/forward-cache restores (pageshow with persisted):
// a bfcache-restored page thaws with its WebGL context discarded in some
// browsers, leaving a dead galaxy canvas — a reload swaps it for a clean
// load, and the intro is seen-flagged by then so it comes back settled.
// (The Firefox back-navigation hydration failure was a different bug:
// Next's router crashed on back_forward loads — "Router action dispatched
// before initialization" — fixed by the Next 16.2.12 patch bump.)
const THEME_SCRIPT = `(function(){try{var t=localStorage.getItem("artcraft-theme");if(t==="light"||t==="dark"){document.documentElement.setAttribute("data-theme",t);}}catch(e){}try{if(!matchMedia("(prefers-reduced-motion: reduce)").matches){document.documentElement.setAttribute("data-intro","");}}catch(e){}addEventListener("pageshow",function(e){if(e.persisted)location.reload();});})();`;
export default function RootLayout({
@@ -362,9 +362,9 @@ export const galaxyPointerTuner = defineTunables(
boostScale: {
label: "Boost scale ×",
min: 1.2,
max: 3,
max: 4,
step: 0.1,
default: 2,
default: 3,
info: "Scale multiplier a click toggles onto the held card. Allowed to overlap neighbors; scales back down on leaving the card, and the next hold needs a fresh click.",
},
boostTau: {
@@ -1,21 +1,31 @@
//const WINDOWS_VERSION = "0.7.0"; // 2026-02-23 Seedance
//const WINDOWS_VERSION = "0.11.0"; // 2026-03-02 Credits accounting
//const WINDOWS_VERSION = "0.14.0"; // 2026-03-10 WorldLabs native, Angles
//const WINDOWS_VERSION = "0.20.0"; // 2026-03-24 Reprompt, performance
const WINDOWS_VERSION = "0.32.0"; // 2026-04-03 characters + prompt box
// Keep previous releases here when adding new downloads, newest first.
const DOWNLOAD_HISTORY = {
"0.37.0": githubDownloadLinks("0.37.0"),
"0.32.0": githubDownloadLinks("0.32.0"),
"0.28.0": githubDownloadLinks("0.28.0"),
"0.23.0": githubDownloadLinks("0.23.0"), // 2026-04-03 Characters + prompt box
"0.20.0": githubDownloadLinks("0.20.0"), // 2026-03-24 Reprompt, performance
"0.14.0": githubDownloadLinks("0.14.0"), // 2026-03-10 WorldLabs native, Angles
"0.11.0": githubDownloadLinks("0.11.0"), // 2026-03-02 Credits accounting
"0.7.0": githubDownloadLinks("0.7.0"), // 2026-02-23 Seedance
} as const;
//const MAC_VERSION = "0.7.0"; // 2026-02-23 Seedance
//const MAC_VERSION = "0.11.0"; // 2026-03-02 Credits accounting
//const MAC_VERSION = "0.14.0"; // 2026-03-10 WorldLabs native, Angles
//const MAC_VERSION = "0.20.0"; // 2026-03-24 Reprompt, performance
const MAC_VERSION = "0.32.0"; // 2026-04-03 characters + prompt box
// To roll back either platform, select a version from DOWNLOAD_HISTORY.
const WINDOWS_VERSION: keyof typeof DOWNLOAD_HISTORY = "0.37.0";
const MAC_VERSION: keyof typeof DOWNLOAD_HISTORY = "0.37.0";
// The published desktop builds are out of date — every download button and
// link on the site is hidden while this is false. Flip back on when a current
// release ships.
export const DOWNLOADS_ENABLED = false;
// Set false to temporarily hide desktop downloads throughout the website.
export const DOWNLOADS_ENABLED = true;
export const DOWNLOAD_LINKS = {
WINDOWS: `https://github.com/storytold/artcraft/releases/download/artcraft-v${WINDOWS_VERSION}/ArtCraft_${WINDOWS_VERSION}_x64-setup.exe`,
MACOS: `https://github.com/storytold/artcraft/releases/download/artcraft-v${MAC_VERSION}/ArtCraft_${MAC_VERSION}_universal.dmg`,
WINDOWS: DOWNLOAD_HISTORY[WINDOWS_VERSION].WINDOWS,
MACOS: DOWNLOAD_HISTORY[MAC_VERSION].MACOS,
} as const;
function githubDownloadLinks(version: string) {
const baseUrl = `https://github.com/storytold/artcraft/releases/download/artcraft-v${version}`;
return {
WINDOWS: `${baseUrl}/ArtCraft_${version}_x64-setup.exe`,
MACOS: `${baseUrl}/ArtCraft_${version}_universal.dmg`,
} as const;
}