sinatra-kagero 1.0.0 → 1.0.1

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: f2d24a386e65a603a75eb521362caec745f0f922a1823019c461a40234c5e1ce
4
+ data.tar.gz: 44f6627eb0b1764478386a37df1b43810153a920a3a32f8384b7c85404be172e
5
5
  SHA512:
6
- metadata.gz: e99c1e6efea707ff1d72ff992a2bb18ac80a214f8bf99d63a645c664b471eba89f03f6f37702f91291dd262ebc019991141ba7a01d2d476e536f6f52e83deace
7
- data.tar.gz: 49012fc6c23ab5d6923b84c826bdfdec376f569145b94961855d48b0dba686eed8c68846783014ee88126cd026194be576c6794b9136be20917e468d6dcb0231
6
+ metadata.gz: 13021231a7a1b035bba99546bdad6b8be9b927a277abc29f2d3abafe27aab26f3b09cf7987c68459aaecbec59208e3a0b7bdea1fa4dde32d48dd90b7d5ad856e
7
+ data.tar.gz: 167ad169bc9e924bbe47a18f4c5d4012e4a4b6caf62d00492b908a9e0a5d8be7bff4e848563b4da4563c34a685b28da893eae815ad25733c6c7e056db6e5569e
data/CHANGELOG.md CHANGED
@@ -1,5 +1,10 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.0.1 — 2026-06-26
4
+
5
+ - Add a thin top loading bar to the browser runtime while Kagero SPA visits
6
+ are in flight.
7
+
3
8
  ## 1.0.0 — 2026-06-25
4
9
 
5
10
  - 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,32 +112,37 @@ 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) {
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Sinatra
4
4
  module Kagero
5
- VERSION = "1.0.0"
5
+ VERSION = "1.0.1"
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,32 +107,37 @@ 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) {
@@ -83,7 +158,10 @@ function formUrl(form) {
83
158
  }
84
159
 
85
160
  document.addEventListener("click", (event) => {
86
- const link = event.target.closest("a[data-kagero]");
161
+ const target = event.target;
162
+ if (!target || !target.closest) return;
163
+
164
+ const link = target.closest("a[data-kagero]");
87
165
  if (link) {
88
166
  event.preventDefault();
89
167
  visit(link.href, {
@@ -93,7 +171,7 @@ document.addEventListener("click", (event) => {
93
171
  return;
94
172
  }
95
173
 
96
- const reload = event.target.closest("[data-kagero-reload]");
174
+ const reload = target.closest("[data-kagero-reload]");
97
175
  if (reload) {
98
176
  event.preventDefault();
99
177
  const only = reload.dataset.kageroOnly || "";
@@ -107,7 +185,9 @@ document.addEventListener("click", (event) => {
107
185
  });
108
186
 
109
187
  document.addEventListener("submit", (event) => {
110
- const form = event.target.closest("form[data-kagero]");
188
+ const target = event.target;
189
+ if (!target || !target.closest) return;
190
+ const form = target.closest("form[data-kagero]");
111
191
  if (!form) return;
112
192
 
113
193
  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.1
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: []