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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: d3f5f3c5e03dd426b4ea2f76d4dfbbba1220d5d366cbc368784675fd27c6944d
4
- data.tar.gz: 7b71df421ac13b8575d459d0a0a25eadd6108b26271f5581e80f28d397fcc5d4
3
+ metadata.gz: 6f7387c7811a71ac630427e87886f19c0c4600d2a6f94afc8bb356395cb5af53
4
+ data.tar.gz: 607a20b6823c5905d807d95c119951b9be974edf50f3ca089c726482e631872d
5
5
  SHA512:
6
- metadata.gz: e99c1e6efea707ff1d72ff992a2bb18ac80a214f8bf99d63a645c664b471eba89f03f6f37702f91291dd262ebc019991141ba7a01d2d476e536f6f52e83deace
7
- data.tar.gz: 49012fc6c23ab5d6923b84c826bdfdec376f569145b94961855d48b0dba686eed8c68846783014ee88126cd026194be576c6794b9136be20917e468d6dcb0231
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
- const response = await fetch(url, {
52
- method: options.method || "GET",
53
- body: options.body,
54
- headers,
55
- credentials: "same-origin",
56
- redirect: "follow"
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
- if (response.status === 409 && response.headers.get("X-Inertia-Location")) {
60
- location.href = response.headers.get("X-Inertia-Location");
61
- return;
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
- if (!response.ok) throw new Error(`Kagero visit failed: ${response.status}`);
136
+ if (!response.ok) throw new Error(`Kagero visit failed: ${response.status}`);
65
137
 
66
- const page = await response.json();
67
- applyPage(page, {
68
- replace: options.replace === true,
69
- preserveScroll: options.preserveScroll === true
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
- return new FormData(form);
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) {
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Sinatra
4
4
  module Kagero
5
- VERSION = "1.0.0"
5
+ VERSION = "1.0.2"
6
6
  end
7
7
  end
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
- const response = await fetch(url, {
47
- method: options.method || "GET",
48
- body: options.body,
49
- headers,
50
- credentials: "same-origin",
51
- redirect: "follow"
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
- if (response.status === 409 && response.headers.get("X-Inertia-Location")) {
55
- location.href = response.headers.get("X-Inertia-Location");
56
- return;
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
- if (!response.ok) throw new Error(`Kagero visit failed: ${response.status}`);
131
+ if (!response.ok) throw new Error(`Kagero visit failed: ${response.status}`);
60
132
 
61
- const page = await response.json();
62
- applyPage(page, {
63
- replace: options.replace === true,
64
- preserveScroll: options.preserveScroll === true
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
- return new FormData(form);
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 link = event.target.closest("a[data-kagero]");
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 = event.target.closest("[data-kagero-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 form = event.target.closest("form[data-kagero]");
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.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: 2026-06-25 00:00:00.000000000 Z
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.0.3.1
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: []