kward 0.69.1 → 0.70.0

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.
@@ -1,30 +1,4 @@
1
- const resetScrollPosition = () => {
2
- if (window.location.hash) return
3
-
4
- document.documentElement.scrollTop = 0
5
- if (document.body) document.body.scrollTop = 0
6
- window.scrollTo(0, 0)
7
-
8
- const main = document.getElementById('main')
9
- if (main) {
10
- main.scrollTop = 0
11
- main.scrollLeft = 0
12
- }
13
- }
14
-
15
- if (!window.location.hash && 'scrollRestoration' in history) {
16
- history.scrollRestoration = 'manual'
17
- }
18
-
19
- resetScrollPosition()
20
- window.addEventListener('load', resetScrollPosition)
21
- window.addEventListener('pageshow', resetScrollPosition)
22
- window.addEventListener('DOMContentLoaded', resetScrollPosition)
23
- window.setTimeout(resetScrollPosition, 0)
24
- window.setTimeout(resetScrollPosition, 50)
25
- window.setTimeout(resetScrollPosition, 250)
26
- if (window.requestAnimationFrame) window.requestAnimationFrame(resetScrollPosition)
27
-
1
+ (() => {
28
2
  const guideLinks = {
29
3
  'doc/getting-started.md': 'file.getting-started.html',
30
4
  'doc/usage.md': 'file.usage.html',
@@ -58,9 +32,12 @@ const setupGuideSearch = (signal) => {
58
32
  return
59
33
  }
60
34
 
35
+ let selectedIndex = -1
36
+
61
37
  const closeResults = () => {
62
38
  results.classList.remove('open')
63
39
  results.innerHTML = ''
40
+ selectedIndex = -1
64
41
  }
65
42
 
66
43
  const excerpt = (text, query) => {
@@ -98,6 +75,18 @@ const setupGuideSearch = (signal) => {
98
75
  .map((result) => result.item)
99
76
  }
100
77
 
78
+ const updateActiveItem = () => {
79
+ const items = results.querySelectorAll('a')
80
+ items.forEach((item, i) => {
81
+ if (i === selectedIndex) {
82
+ item.classList.add('kward-search-active')
83
+ item.scrollIntoView({ block: 'nearest' })
84
+ } else {
85
+ item.classList.remove('kward-search-active')
86
+ }
87
+ })
88
+ }
89
+
101
90
  const renderResults = () => {
102
91
  const query = input.value.trim()
103
92
  if (query.length < 2) {
@@ -107,6 +96,7 @@ const setupGuideSearch = (signal) => {
107
96
 
108
97
  const matches = search(query)
109
98
  results.innerHTML = ''
99
+ selectedIndex = -1
110
100
 
111
101
  if (matches.length === 0) {
112
102
  const empty = document.createElement('div')
@@ -139,17 +129,37 @@ const setupGuideSearch = (signal) => {
139
129
  input.addEventListener('input', renderResults, { signal })
140
130
 
141
131
  input.addEventListener('keydown', (event) => {
132
+ const items = results.querySelectorAll('a')
133
+
142
134
  if (event.key === 'Escape') {
143
135
  input.value = ''
144
136
  closeResults()
145
137
  input.blur()
138
+ return
139
+ }
140
+
141
+ if (items.length === 0) return
142
+
143
+ if (event.key === 'ArrowDown') {
144
+ event.preventDefault()
145
+ selectedIndex = Math.min(selectedIndex + 1, items.length - 1)
146
+ updateActiveItem()
147
+ } else if (event.key === 'ArrowUp') {
148
+ event.preventDefault()
149
+ selectedIndex = Math.max(selectedIndex - 1, 0)
150
+ updateActiveItem()
151
+ } else if (event.key === 'Enter') {
152
+ if (selectedIndex >= 0 && items[selectedIndex]) {
153
+ event.preventDefault()
154
+ window.location.href = items[selectedIndex].href
155
+ }
146
156
  }
147
157
  }, { signal })
148
158
 
149
159
  form.addEventListener('submit', (event) => {
150
160
  event.preventDefault()
151
161
  const firstResult = results.querySelector('a')
152
- if (firstResult) visitPage(firstResult.href)
162
+ if (firstResult) window.location.href = firstResult.href
153
163
  }, { signal })
154
164
 
155
165
  document.addEventListener('click', (event) => {
@@ -159,26 +169,80 @@ const setupGuideSearch = (signal) => {
159
169
 
160
170
  const setupNavigation = (signal) => {
161
171
  const toggle = document.querySelector('.kward-nav-toggle')
172
+ const nav = document.getElementById('kward-primary-nav')
173
+
174
+ const closeMenu = () => {
175
+ document.body.classList.remove('kward-nav-open')
176
+ if (toggle) toggle.setAttribute('aria-expanded', 'false')
177
+ document.querySelectorAll('.kward-nav-menu.open').forEach((menu) => {
178
+ menu.classList.remove('open')
179
+ const btn = menu.querySelector('.kward-nav-menu-button')
180
+ if (btn) btn.setAttribute('aria-expanded', 'false')
181
+ })
182
+ }
162
183
 
163
184
  if (toggle) {
185
+ // The inline onclick handles the toggle. We only need to close
186
+ // sub-menus when the main menu closes.
164
187
  toggle.addEventListener('click', () => {
165
- const isOpen = document.body.classList.toggle('kward-nav-open')
166
- toggle.setAttribute('aria-expanded', String(isOpen))
188
+ if (!document.body.classList.contains('kward-nav-open')) {
189
+ document.querySelectorAll('.kward-nav-menu.open').forEach((menu) => {
190
+ menu.classList.remove('open')
191
+ const btn = menu.querySelector('.kward-nav-menu-button')
192
+ if (btn) btn.setAttribute('aria-expanded', 'false')
193
+ })
194
+ }
167
195
  }, { signal })
168
196
  }
169
197
 
170
198
  document.querySelectorAll('.kward-nav-menu-button').forEach((button) => {
171
199
  button.addEventListener('click', (event) => {
172
200
  event.stopPropagation()
173
- button.parentElement.classList.toggle('open')
201
+ const menu = button.parentElement
202
+ const wasOpen = menu.classList.contains('open')
203
+ document.querySelectorAll('.kward-nav-menu.open').forEach((m) => {
204
+ m.classList.remove('open')
205
+ const b = m.querySelector('.kward-nav-menu-button')
206
+ if (b) b.setAttribute('aria-expanded', 'false')
207
+ })
208
+ if (!wasOpen) {
209
+ menu.classList.add('open')
210
+ button.setAttribute('aria-expanded', 'true')
211
+ }
174
212
  }, { signal })
175
213
  })
176
214
 
215
+ // Close menu when a nav link is clicked (mobile navigation)
216
+ if (nav) {
217
+ nav.addEventListener('click', (event) => {
218
+ const link = event.target.closest('a[href]')
219
+ if (link) closeMenu()
220
+ }, { signal })
221
+ }
222
+
223
+ // Close on outside click
177
224
  document.addEventListener('click', (event) => {
225
+ if (document.body.classList.contains('kward-nav-open')) {
226
+ if (nav && !nav.contains(event.target) && toggle && !toggle.contains(event.target)) {
227
+ closeMenu()
228
+ }
229
+ }
178
230
  document.querySelectorAll('.kward-nav-menu.open').forEach((menu) => {
179
- if (!menu.contains(event.target)) menu.classList.remove('open')
231
+ if (!menu.contains(event.target)) {
232
+ menu.classList.remove('open')
233
+ const btn = menu.querySelector('.kward-nav-menu-button')
234
+ if (btn) btn.setAttribute('aria-expanded', 'false')
235
+ }
180
236
  })
181
237
  }, { signal })
238
+
239
+ // Close on Escape
240
+ document.addEventListener('keydown', (event) => {
241
+ if (event.key === 'Escape' && document.body.classList.contains('kward-nav-open')) {
242
+ closeMenu()
243
+ if (toggle) toggle.focus()
244
+ }
245
+ }, { signal })
182
246
  }
183
247
 
184
248
  const rewriteGuideLinks = () => {
@@ -221,76 +285,15 @@ const initializePage = () => {
221
285
  if (pageController) pageController.abort()
222
286
  pageController = new AbortController()
223
287
 
224
- resetScrollPosition()
225
288
  setupGuideSearch(pageController.signal)
226
289
  setupNavigation(pageController.signal)
227
290
  rewriteGuideLinks()
228
291
  setupCodeCopy()
229
292
  }
230
293
 
231
- const samePageUrl = (url) => {
232
- return url.origin === window.location.origin &&
233
- url.pathname === window.location.pathname &&
234
- url.search === window.location.search
235
- }
236
-
237
- const navigableUrl = (url) => {
238
- if (url.hash) return false
239
- if (url.origin !== window.location.origin) return false
240
- if (!url.pathname.endsWith('.html') && !url.pathname.endsWith('/')) return false
241
- return !samePageUrl(url)
242
- }
243
-
244
- const replacePage = (html, url) => {
245
- const nextDocument = new DOMParser().parseFromString(html, 'text/html')
246
- const nextBody = nextDocument.body
247
- if (!nextBody) throw new Error('Missing response body')
248
-
249
- document.title = nextDocument.title
250
- document.body.className = nextBody.className
251
- document.body.innerHTML = nextBody.innerHTML
252
- window.history.pushState({}, '', url.href)
294
+ if (document.readyState === 'loading') {
295
+ document.addEventListener('DOMContentLoaded', initializePage, { once: true })
296
+ } else {
253
297
  initializePage()
254
298
  }
255
-
256
- const visitPage = async (href) => {
257
- const url = new URL(href, window.location.href)
258
-
259
- if (!navigableUrl(url)) {
260
- window.location.href = url.href
261
- return
262
- }
263
-
264
- document.documentElement.classList.add('kward-page-loading')
265
-
266
- try {
267
- const response = await fetch(url.href)
268
- if (!response.ok) throw new Error(`Failed to load ${url.href}`)
269
-
270
- const html = await response.text()
271
- replacePage(html, url)
272
- } catch (_error) {
273
- window.location.href = url.href
274
- } finally {
275
- document.documentElement.classList.remove('kward-page-loading')
276
- }
277
- }
278
-
279
- document.addEventListener('click', (event) => {
280
- if (event.defaultPrevented || event.button !== 0 || event.metaKey || event.ctrlKey || event.shiftKey || event.altKey) return
281
-
282
- const link = event.target.closest('a[href]')
283
- if (!link || link.target || link.hasAttribute('download')) return
284
-
285
- const url = new URL(link.href, window.location.href)
286
- if (!navigableUrl(url)) return
287
-
288
- event.preventDefault()
289
- visitPage(url.href)
290
- })
291
-
292
- window.addEventListener('popstate', () => {
293
- window.location.reload()
294
- })
295
-
296
- document.addEventListener('DOMContentLoaded', initializePage)
299
+ })()
@@ -1,10 +1,10 @@
1
1
  <!DOCTYPE html>
2
- <html>
2
+ <html lang="en">
3
3
  <head>
4
4
  <%= erb(:headers) %>
5
5
  </head>
6
6
  <body class="kward-docs <%= home_page? ? 'kward-home-body' : 'kward-content-body' %>">
7
- <div id="main_progress" aria-hidden="true"></div>
7
+ <a href="#main" class="kward-skip-link">Skip to content</a>
8
8
 
9
9
  <header class="kward-topbar">
10
10
  <a class="kward-brand" href="<%= url_for('index.html') %>">
@@ -13,14 +13,17 @@
13
13
  <strong>Kward</strong>
14
14
  </span>
15
15
  </a>
16
- <button class="kward-nav-toggle" type="button" aria-expanded="false" aria-controls="kward-primary-nav" onclick="document.body.classList.toggle('kward-nav-open'); this.setAttribute('aria-expanded', document.body.classList.contains('kward-nav-open'))">
17
- Menu
16
+ <button class="kward-nav-toggle" type="button" aria-expanded="false" aria-controls="kward-primary-nav" aria-label="Toggle navigation menu" onclick="document.body.classList.toggle('kward-nav-open'); this.setAttribute('aria-expanded', document.body.classList.contains('kward-nav-open'))">
17
+ <span class="kward-nav-toggle-bar"></span>
18
+ <span class="kward-nav-toggle-bar"></span>
19
+ <span class="kward-nav-toggle-bar"></span>
20
+ <span class="kward-sr-only">Menu</span>
18
21
  </button>
19
22
  <nav id="kward-primary-nav" class="kward-topnav" aria-label="Primary navigation">
20
23
  <a href="<%= url_for('index.html') %>" class="<%= 'active' if home_page? %>">Home</a>
21
24
  <div class="kward-nav-menu <%= 'active' if guide_page? %>">
22
25
  <a class="kward-nav-menu-link" href="<%= url_for('file.README.html') %>">User Guides</a>
23
- <button class="kward-nav-menu-button" type="button" aria-label="Open user guides menu">⌄</button>
26
+ <button class="kward-nav-menu-button" type="button" aria-label="Expand user guides" aria-expanded="false">⌄</button>
24
27
  <div class="kward-nav-dropdown">
25
28
  <% guide_groups.each do |title, items| %>
26
29
  <section>
@@ -47,7 +50,17 @@
47
50
  <section class="kward-hero">
48
51
  <div class="kward-hero-copy">
49
52
  <p class="kward-eyebrow">⌘ Ruby CLI Coding Agent</p>
50
- <h1>Your terminal.<br><span>Your agent.</span></h1>
53
+ <h1>
54
+ <span class="kward-swosh" aria-label="Your terminal.">
55
+ <span class="kward-swosh-text kward-swosh-text-a" aria-hidden="true">Your terminal.</span>
56
+ <span class="kward-swosh-text kward-swosh-text-b" aria-hidden="true">Your RPC frontend.</span>
57
+ </span>
58
+ <br>
59
+ <span class="kward-swosh" aria-label="Your agent.">
60
+ <span class="kward-swosh-text kward-swosh-text-c" aria-hidden="true">Your agent.</span>
61
+ <span class="kward-swosh-text kward-swosh-text-d" aria-hidden="true">Your LLM engine.</span>
62
+ </span>
63
+ </h1>
51
64
  <p class="kward-lede">Kward is an extendable Ruby CLI coding agent that helps you understand your project, edit files, run commands, search the web, and automate workflows—right from your terminal.</p>
52
65
  <div class="kward-actions">
53
66
  <a class="kward-primary-button" href="<%= url_for('file.README.html') %>">Get Started ›</a>
@@ -137,5 +150,7 @@ kward --working-directory ~/code/project "Explain this project"</code></pre>
137
150
  </main>
138
151
  </div>
139
152
  <% end %>
153
+
154
+ <script type="text/javascript" charset="utf-8" src="<%= url_for('js/kward.js') %>"></script>
140
155
  </body>
141
156
  </html>
@@ -131,7 +131,7 @@ end
131
131
  include KwardDocsNavigation
132
132
 
133
133
  def javascripts
134
- super + %w(js/kward.js)
134
+ super
135
135
  end
136
136
 
137
137
  def stylesheets
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: kward
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.69.1
4
+ version: 0.70.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Kai Wood