sinatra-kagero 1.0.0 → 1.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +10 -0
- data/lib/sinatra/kagero/runtime.rb +101 -18
- data/lib/sinatra/kagero/version.rb +1 -1
- data/runtime/kagero.js +109 -21
- metadata +3 -7
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 6f7387c7811a71ac630427e87886f19c0c4600d2a6f94afc8bb356395cb5af53
|
|
4
|
+
data.tar.gz: 607a20b6823c5905d807d95c119951b9be974edf50f3ca089c726482e631872d
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 5e905a9e0543d1f1b08e53a2d45eff24a48b367b45a0a57f3b342825470d5ef96677f4a20f3a8947bf956c1c8171f5ab8d0671edef4c2d919507682e943ce717
|
|
7
|
+
data.tar.gz: 6f48eb473d1d374ac5937b9da0ecd80abecde61469b1cac88f05d4ef6cccbacc947133908794230392addee7ef48ef3bd9c792ff982587f6c66971febaea59f0
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,15 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.0.2 — 2026-06-26
|
|
4
|
+
|
|
5
|
+
- Submit regular Kagero forms as UTF-8 URL-encoded bodies, preserving
|
|
6
|
+
non-ASCII text on Workers while keeping multipart for file forms.
|
|
7
|
+
|
|
8
|
+
## 1.0.1 — 2026-06-26
|
|
9
|
+
|
|
10
|
+
- Add a thin top loading bar to the browser runtime while Kagero SPA visits
|
|
11
|
+
are in flight.
|
|
12
|
+
|
|
3
13
|
## 1.0.0 — 2026-06-25
|
|
4
14
|
|
|
5
15
|
- Extract Kagero from `sinatra-inertia` into its own gem.
|
|
@@ -7,6 +7,76 @@ module Sinatra
|
|
|
7
7
|
const root = document.querySelector("[data-kagero-root]");
|
|
8
8
|
let currentPage = null;
|
|
9
9
|
let currentScroll = { left: 0, top: 0 };
|
|
10
|
+
let pendingVisits = 0;
|
|
11
|
+
let loadingBar = null;
|
|
12
|
+
|
|
13
|
+
function ensureLoadingBar() {
|
|
14
|
+
if (loadingBar) return loadingBar;
|
|
15
|
+
|
|
16
|
+
const style = document.createElement("style");
|
|
17
|
+
style.textContent = `
|
|
18
|
+
#kagero-loading-bar {
|
|
19
|
+
position: fixed;
|
|
20
|
+
top: 0;
|
|
21
|
+
left: 0;
|
|
22
|
+
right: 0;
|
|
23
|
+
z-index: 2147483647;
|
|
24
|
+
height: 3px;
|
|
25
|
+
overflow: hidden;
|
|
26
|
+
pointer-events: none;
|
|
27
|
+
opacity: 0;
|
|
28
|
+
background: rgba(147, 197, 253, 0.24);
|
|
29
|
+
transition: opacity 120ms ease;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
#kagero-loading-bar[data-active="true"] {
|
|
33
|
+
opacity: 1;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
#kagero-loading-bar::before {
|
|
37
|
+
content: "";
|
|
38
|
+
position: absolute;
|
|
39
|
+
top: 0;
|
|
40
|
+
bottom: 0;
|
|
41
|
+
left: 0;
|
|
42
|
+
width: 48%;
|
|
43
|
+
background: linear-gradient(
|
|
44
|
+
90deg,
|
|
45
|
+
rgba(59, 130, 246, 0),
|
|
46
|
+
rgba(96, 165, 250, 0.35),
|
|
47
|
+
rgba(37, 99, 235, 0.9),
|
|
48
|
+
rgba(96, 165, 250, 0.35),
|
|
49
|
+
rgba(59, 130, 246, 0)
|
|
50
|
+
);
|
|
51
|
+
transform: translateX(-120%);
|
|
52
|
+
animation: kagero-loading-wave 700ms linear infinite;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
@keyframes kagero-loading-wave {
|
|
56
|
+
from { transform: translateX(-120%); }
|
|
57
|
+
to { transform: translateX(260%); }
|
|
58
|
+
}
|
|
59
|
+
`;
|
|
60
|
+
document.head.appendChild(style);
|
|
61
|
+
|
|
62
|
+
loadingBar = document.createElement("div");
|
|
63
|
+
loadingBar.id = "kagero-loading-bar";
|
|
64
|
+
loadingBar.setAttribute("aria-hidden", "true");
|
|
65
|
+
document.body.appendChild(loadingBar);
|
|
66
|
+
return loadingBar;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function startLoading() {
|
|
70
|
+
pendingVisits += 1;
|
|
71
|
+
ensureLoadingBar().dataset.active = "true";
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function finishLoading() {
|
|
75
|
+
pendingVisits = Math.max(0, pendingVisits - 1);
|
|
76
|
+
if (pendingVisits === 0 && loadingBar) {
|
|
77
|
+
loadingBar.dataset.active = "false";
|
|
78
|
+
}
|
|
79
|
+
}
|
|
10
80
|
|
|
11
81
|
function readInitialPage() {
|
|
12
82
|
if (!root) return null;
|
|
@@ -42,38 +112,51 @@ module Sinatra
|
|
|
42
112
|
|
|
43
113
|
async function visit(url, options = {}) {
|
|
44
114
|
rememberScroll();
|
|
115
|
+
startLoading();
|
|
45
116
|
const headers = new Headers(options.headers || {});
|
|
46
117
|
headers.set("X-Inertia", "true");
|
|
47
118
|
headers.set("X-Inertia-Version", currentPage ? currentPage.version : "");
|
|
48
119
|
headers.set("X-Requested-With", "XMLHttpRequest");
|
|
49
120
|
headers.set("Accept", "application/json, text/html;q=0.9");
|
|
50
121
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
122
|
+
try {
|
|
123
|
+
const response = await fetch(url, {
|
|
124
|
+
method: options.method || "GET",
|
|
125
|
+
body: options.body,
|
|
126
|
+
headers,
|
|
127
|
+
credentials: "same-origin",
|
|
128
|
+
redirect: "follow"
|
|
129
|
+
});
|
|
58
130
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
131
|
+
if (response.status === 409 && response.headers.get("X-Inertia-Location")) {
|
|
132
|
+
location.href = response.headers.get("X-Inertia-Location");
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
63
135
|
|
|
64
|
-
|
|
136
|
+
if (!response.ok) throw new Error(`Kagero visit failed: ${response.status}`);
|
|
65
137
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
138
|
+
const page = await response.json();
|
|
139
|
+
applyPage(page, {
|
|
140
|
+
replace: options.replace === true,
|
|
141
|
+
preserveScroll: options.preserveScroll === true
|
|
142
|
+
});
|
|
143
|
+
} finally {
|
|
144
|
+
finishLoading();
|
|
145
|
+
}
|
|
71
146
|
}
|
|
72
147
|
|
|
73
148
|
function formBody(form) {
|
|
74
149
|
const method = (form.getAttribute("method") || "GET").toUpperCase();
|
|
75
150
|
if (method === "GET") return null;
|
|
76
|
-
|
|
151
|
+
|
|
152
|
+
const enctype = (form.getAttribute("enctype") || "").toLowerCase();
|
|
153
|
+
const data = new FormData(form);
|
|
154
|
+
const hasFile = Array.from(data.values()).some((value) => value instanceof File);
|
|
155
|
+
if (enctype === "multipart/form-data" || hasFile) return data;
|
|
156
|
+
|
|
157
|
+
const params = new URLSearchParams();
|
|
158
|
+
for (const [key, value] of data.entries()) params.append(key, value);
|
|
159
|
+
return params;
|
|
77
160
|
}
|
|
78
161
|
|
|
79
162
|
function formUrl(form) {
|
data/runtime/kagero.js
CHANGED
|
@@ -2,6 +2,76 @@ const root = document.querySelector("[data-kagero-root]");
|
|
|
2
2
|
|
|
3
3
|
let currentPage = null;
|
|
4
4
|
let currentScroll = { left: 0, top: 0 };
|
|
5
|
+
let pendingVisits = 0;
|
|
6
|
+
let loadingBar = null;
|
|
7
|
+
|
|
8
|
+
function ensureLoadingBar() {
|
|
9
|
+
if (loadingBar) return loadingBar;
|
|
10
|
+
|
|
11
|
+
const style = document.createElement("style");
|
|
12
|
+
style.textContent = `
|
|
13
|
+
#kagero-loading-bar {
|
|
14
|
+
position: fixed;
|
|
15
|
+
top: 0;
|
|
16
|
+
left: 0;
|
|
17
|
+
right: 0;
|
|
18
|
+
z-index: 2147483647;
|
|
19
|
+
height: 3px;
|
|
20
|
+
overflow: hidden;
|
|
21
|
+
pointer-events: none;
|
|
22
|
+
opacity: 0;
|
|
23
|
+
background: rgba(147, 197, 253, 0.24);
|
|
24
|
+
transition: opacity 120ms ease;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
#kagero-loading-bar[data-active="true"] {
|
|
28
|
+
opacity: 1;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
#kagero-loading-bar::before {
|
|
32
|
+
content: "";
|
|
33
|
+
position: absolute;
|
|
34
|
+
top: 0;
|
|
35
|
+
bottom: 0;
|
|
36
|
+
left: 0;
|
|
37
|
+
width: 48%;
|
|
38
|
+
background: linear-gradient(
|
|
39
|
+
90deg,
|
|
40
|
+
rgba(59, 130, 246, 0),
|
|
41
|
+
rgba(96, 165, 250, 0.35),
|
|
42
|
+
rgba(37, 99, 235, 0.9),
|
|
43
|
+
rgba(96, 165, 250, 0.35),
|
|
44
|
+
rgba(59, 130, 246, 0)
|
|
45
|
+
);
|
|
46
|
+
transform: translateX(-120%);
|
|
47
|
+
animation: kagero-loading-wave 700ms linear infinite;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
@keyframes kagero-loading-wave {
|
|
51
|
+
from { transform: translateX(-120%); }
|
|
52
|
+
to { transform: translateX(260%); }
|
|
53
|
+
}
|
|
54
|
+
`;
|
|
55
|
+
document.head.appendChild(style);
|
|
56
|
+
|
|
57
|
+
loadingBar = document.createElement("div");
|
|
58
|
+
loadingBar.id = "kagero-loading-bar";
|
|
59
|
+
loadingBar.setAttribute("aria-hidden", "true");
|
|
60
|
+
document.body.appendChild(loadingBar);
|
|
61
|
+
return loadingBar;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function startLoading() {
|
|
65
|
+
pendingVisits += 1;
|
|
66
|
+
ensureLoadingBar().dataset.active = "true";
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function finishLoading() {
|
|
70
|
+
pendingVisits = Math.max(0, pendingVisits - 1);
|
|
71
|
+
if (pendingVisits === 0 && loadingBar) {
|
|
72
|
+
loadingBar.dataset.active = "false";
|
|
73
|
+
}
|
|
74
|
+
}
|
|
5
75
|
|
|
6
76
|
function readInitialPage() {
|
|
7
77
|
if (!root) return null;
|
|
@@ -37,38 +107,51 @@ function applyPage(page, { replace = false, preserveScroll = false } = {}) {
|
|
|
37
107
|
|
|
38
108
|
async function visit(url, options = {}) {
|
|
39
109
|
rememberScroll();
|
|
110
|
+
startLoading();
|
|
40
111
|
const headers = new Headers(options.headers || {});
|
|
41
112
|
headers.set("X-Inertia", "true");
|
|
42
113
|
headers.set("X-Inertia-Version", currentPage ? currentPage.version : "");
|
|
43
114
|
headers.set("X-Requested-With", "XMLHttpRequest");
|
|
44
115
|
headers.set("Accept", "application/json, text/html;q=0.9");
|
|
45
116
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
117
|
+
try {
|
|
118
|
+
const response = await fetch(url, {
|
|
119
|
+
method: options.method || "GET",
|
|
120
|
+
body: options.body,
|
|
121
|
+
headers,
|
|
122
|
+
credentials: "same-origin",
|
|
123
|
+
redirect: "follow"
|
|
124
|
+
});
|
|
53
125
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
126
|
+
if (response.status === 409 && response.headers.get("X-Inertia-Location")) {
|
|
127
|
+
location.href = response.headers.get("X-Inertia-Location");
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
58
130
|
|
|
59
|
-
|
|
131
|
+
if (!response.ok) throw new Error(`Kagero visit failed: ${response.status}`);
|
|
60
132
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
133
|
+
const page = await response.json();
|
|
134
|
+
applyPage(page, {
|
|
135
|
+
replace: options.replace === true,
|
|
136
|
+
preserveScroll: options.preserveScroll === true
|
|
137
|
+
});
|
|
138
|
+
} finally {
|
|
139
|
+
finishLoading();
|
|
140
|
+
}
|
|
66
141
|
}
|
|
67
142
|
|
|
68
143
|
function formBody(form) {
|
|
69
144
|
const method = (form.getAttribute("method") || "GET").toUpperCase();
|
|
70
145
|
if (method === "GET") return null;
|
|
71
|
-
|
|
146
|
+
|
|
147
|
+
const enctype = (form.getAttribute("enctype") || "").toLowerCase();
|
|
148
|
+
const data = new FormData(form);
|
|
149
|
+
const hasFile = Array.from(data.values()).some((value) => value instanceof File);
|
|
150
|
+
if (enctype === "multipart/form-data" || hasFile) return data;
|
|
151
|
+
|
|
152
|
+
const params = new URLSearchParams();
|
|
153
|
+
for (const [key, value] of data.entries()) params.append(key, value);
|
|
154
|
+
return params;
|
|
72
155
|
}
|
|
73
156
|
|
|
74
157
|
function formUrl(form) {
|
|
@@ -83,7 +166,10 @@ function formUrl(form) {
|
|
|
83
166
|
}
|
|
84
167
|
|
|
85
168
|
document.addEventListener("click", (event) => {
|
|
86
|
-
const
|
|
169
|
+
const target = event.target;
|
|
170
|
+
if (!target || !target.closest) return;
|
|
171
|
+
|
|
172
|
+
const link = target.closest("a[data-kagero]");
|
|
87
173
|
if (link) {
|
|
88
174
|
event.preventDefault();
|
|
89
175
|
visit(link.href, {
|
|
@@ -93,7 +179,7 @@ document.addEventListener("click", (event) => {
|
|
|
93
179
|
return;
|
|
94
180
|
}
|
|
95
181
|
|
|
96
|
-
const reload =
|
|
182
|
+
const reload = target.closest("[data-kagero-reload]");
|
|
97
183
|
if (reload) {
|
|
98
184
|
event.preventDefault();
|
|
99
185
|
const only = reload.dataset.kageroOnly || "";
|
|
@@ -107,7 +193,9 @@ document.addEventListener("click", (event) => {
|
|
|
107
193
|
});
|
|
108
194
|
|
|
109
195
|
document.addEventListener("submit", (event) => {
|
|
110
|
-
const
|
|
196
|
+
const target = event.target;
|
|
197
|
+
if (!target || !target.closest) return;
|
|
198
|
+
const form = target.closest("form[data-kagero]");
|
|
111
199
|
if (!form) return;
|
|
112
200
|
|
|
113
201
|
event.preventDefault();
|
metadata
CHANGED
|
@@ -1,14 +1,13 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: sinatra-kagero
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 1.0.
|
|
4
|
+
version: 1.0.2
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Kazuhiro Homma
|
|
8
|
-
autorequire:
|
|
9
8
|
bindir: bin
|
|
10
9
|
cert_chain: []
|
|
11
|
-
date:
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
12
11
|
dependencies:
|
|
13
12
|
- !ruby/object:Gem::Dependency
|
|
14
13
|
name: sinatra-inertia
|
|
@@ -75,7 +74,6 @@ description: |
|
|
|
75
74
|
Phlex page classes, Literal-style props schemas, Ruby form/command
|
|
76
75
|
validation, and a hidden browser runtime for SPA-like navigation without
|
|
77
76
|
exposing JavaScript as the primary userland authoring model.
|
|
78
|
-
email:
|
|
79
77
|
executables: []
|
|
80
78
|
extensions: []
|
|
81
79
|
extra_rdoc_files: []
|
|
@@ -100,7 +98,6 @@ metadata:
|
|
|
100
98
|
changelog_uri: https://github.com/kazuph/homura/blob/main/gems/sinatra-kagero/CHANGELOG.md
|
|
101
99
|
readme_uri: https://github.com/kazuph/homura/blob/main/gems/sinatra-kagero/README.md
|
|
102
100
|
homura.auto_await: 'true'
|
|
103
|
-
post_install_message:
|
|
104
101
|
rdoc_options: []
|
|
105
102
|
require_paths:
|
|
106
103
|
- lib
|
|
@@ -115,8 +112,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
115
112
|
- !ruby/object:Gem::Version
|
|
116
113
|
version: '0'
|
|
117
114
|
requirements: []
|
|
118
|
-
rubygems_version: 3.
|
|
119
|
-
signing_key:
|
|
115
|
+
rubygems_version: 3.6.9
|
|
120
116
|
specification_version: 4
|
|
121
117
|
summary: Ruby-way Inertia experience for Sinatra and Homura
|
|
122
118
|
test_files: []
|