unmagic-icon 0.1.0 → 0.2.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 78dfe537a660edce4ede922cf790a6b2a317c24f54143e356674925113f94bb8
4
- data.tar.gz: 513b08d32b96b7f4da9177c814e44b8a1f85a9cb8be54d75b6e4bffb166b6a63
3
+ metadata.gz: 17b7d09638783ea1144e26806aa2e7915d04e08645ed39c2df6a2f3026a69bdf
4
+ data.tar.gz: 39b8e9e8fc1a88c494c1336224b88632c0f5153e002fa2a2663b1f476c42f27b
5
5
  SHA512:
6
- metadata.gz: 3f57f44dd2680988425b6c2e8fc195f2eca2d4647305ba9250ca5220e7b6cc6a807b641927e820ba7bb0a651ab016bf15c8e2e5165350a9e68230822a52fd417
7
- data.tar.gz: 557cb9b7886ef6d7d0bd7ce28c40815c6b0442dd7a8ae6c79d0033b697044a8e6c930b4bd23a3d2338e18cf64ef447a8a06633b91403d70fe75dbeb25c35fef8
6
+ metadata.gz: 58e783d9c8829e92c70149d48f95a012a092123fa85ec2e92f68eb30203d4028722f7db8aad66c487880749b5e9647062c11cc4118c06faf71b5784fd617a9ea
7
+ data.tar.gz: d5ecd53c6ee72f8aaa9050221d4a619fc58a778f9998896a50530a1cee737564f3667e5bb4435040c69c645861dcb8a0ca829bcb3b923db025d51b32800a1882
data/CHANGELOG.md CHANGED
@@ -7,6 +7,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.2.0] - 2026-06-22
11
+
12
+ ### Added
13
+ - New downloadable icon libraries: `coloured-icons` (full-colour brand/tech
14
+ logos), `bootstrap-icons`, `octicons`, `iconoir`, `material-design-icons`
15
+ (Pictogrammers @mdi), and `phosphor` (six weights flattened into one set)
16
+ - Server-side search in the `Unmagic::Icon::Web` Rack app
17
+
10
18
  ## [0.1.0] - 2026-06-21
11
19
 
12
20
  ### Added
@@ -33,5 +41,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
33
41
  hooks into `assets:precompile`) and `unmagic:icons:download[library]`
34
42
  - `Unmagic::Icon::Web`, a Rack app for browsing the configured icon libraries
35
43
 
36
- [Unreleased]: https://github.com/unreasonable-magic/unmagic-icon/compare/v0.1.0...HEAD
44
+ [Unreleased]: https://github.com/unreasonable-magic/unmagic-icon/compare/v0.2.0...HEAD
45
+ [0.2.0]: https://github.com/unreasonable-magic/unmagic-icon/compare/v0.1.0...v0.2.0
37
46
  [0.1.0]: https://github.com/unreasonable-magic/unmagic-icon/releases/tag/v0.1.0
data/README.md CHANGED
@@ -127,6 +127,12 @@ Available libraries:
127
127
  | `simple-icons` | Simple Icons | SVG icons for popular brands |
128
128
  | `material-file-icons` | Material File Icons | Material Design file icons with filename/extension aliases |
129
129
  | `silk` | Silk Icons Scalable | The classic silk icon set recreated as SVG |
130
+ | `coloured-icons` | Coloured Icons | Full-colour brand and technology logos |
131
+ | `bootstrap-icons` | Bootstrap Icons | Official open source SVG icon library for Bootstrap |
132
+ | `octicons` | Octicons | Icons and icon font from GitHub |
133
+ | `iconoir` | Iconoir | Free open source icons designed on a 24x24 grid |
134
+ | `material-design-icons` | Material Design Icons | 7400+ Material Design icons (Pictogrammers @mdi) |
135
+ | `phosphor` | Phosphor Icons | Flexible icon family with six weights (thin to fill, plus duotone) |
130
136
 
131
137
  ### Browsing icons
132
138
 
@@ -0,0 +1,17 @@
1
+ module Unmagic
2
+ class Icon
3
+ class Library
4
+ class Source
5
+ class BootstrapIcons < Source
6
+ key :"bootstrap-icons"
7
+ title "Bootstrap Icons"
8
+ description "Official open source SVG icon library for Bootstrap"
9
+ url "https://github.com/twbs/icons/archive/refs/tags/v1.13.1.zip"
10
+ archive :zip
11
+ dir "bootstrap-icons"
12
+ extract "icons-1.13.1/icons/*.svg"
13
+ end
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,20 @@
1
+ module Unmagic
2
+ class Icon
3
+ class Library
4
+ class Source
5
+ # Coloured Icons: full-colour brand/logo svgs organised by category in
6
+ # the repo (public/logos/<category>/<name>/<name>.svg). No release asset,
7
+ # but GitHub serves a tag archive; we flatten every svg into one library.
8
+ class ColouredIcons < Source
9
+ key :"coloured-icons"
10
+ title "Coloured Icons"
11
+ description "Full-colour brand and technology logos"
12
+ url "https://github.com/dheereshag/coloured-icons/archive/refs/tags/1.9.7.zip"
13
+ archive :zip
14
+ dir "coloured-icons"
15
+ extract "coloured-icons-1.9.7/public/logos/**/*.svg"
16
+ end
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,23 @@
1
+ module Unmagic
2
+ class Icon
3
+ class Library
4
+ class Source
5
+ # Ships regular and solid styles under icons/<style>; keep them as
6
+ # separate sub-libraries (iconoir/regular, iconoir/solid) so the shared
7
+ # icon names don't collide.
8
+ class Iconoir < Source
9
+ key :iconoir
10
+ title "Iconoir"
11
+ description "Free open source icons designed on a 24x24 grid"
12
+ url "https://github.com/iconoir-icons/iconoir/archive/refs/tags/v7.11.1.zip"
13
+ archive :zip
14
+ dir "iconoir"
15
+ extract_into(
16
+ "iconoir-7.11.1/icons/regular" => "regular",
17
+ "iconoir-7.11.1/icons/solid" => "solid"
18
+ )
19
+ end
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,19 @@
1
+ module Unmagic
2
+ class Icon
3
+ class Library
4
+ class Source
5
+ # Pictogrammers' Material Design Icons (@mdi/svg) — the general-purpose
6
+ # icon set, distinct from the file-type icons in MaterialFileIcons.
7
+ class MaterialDesignIcons < Source
8
+ key :"material-design-icons"
9
+ title "Material Design Icons"
10
+ description "7400+ Material Design icons (Pictogrammers @mdi)"
11
+ url "https://github.com/Templarian/MaterialDesign-SVG/archive/refs/tags/v7.4.47.zip"
12
+ archive :zip
13
+ dir "material-design-icons"
14
+ extract "MaterialDesign-SVG-7.4.47/svg/*.svg"
15
+ end
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,18 @@
1
+ module Unmagic
2
+ class Icon
3
+ class Library
4
+ class Source
5
+ # GitHub's icon set. Names carry their size (e.g. alert-16, alert-24).
6
+ class Octicons < Source
7
+ key :octicons
8
+ title "Octicons"
9
+ description "Icons and icon font from GitHub"
10
+ url "https://github.com/primer/octicons/archive/refs/tags/v19.28.1.zip"
11
+ archive :zip
12
+ dir "octicons"
13
+ extract "octicons-19.28.1/icons/*.svg"
14
+ end
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,21 @@
1
+ module Unmagic
2
+ class Icon
3
+ class Library
4
+ class Source
5
+ # Six weights live under assets/<weight>; every non-regular filename
6
+ # already carries its weight suffix (ghost-bold, ghost-fill, …), so we
7
+ # flatten all weights into one library without name collisions. Regular
8
+ # is the bare name (ghost), the rest are ghost-<weight>.
9
+ class Phosphor < Source
10
+ key :phosphor
11
+ title "Phosphor Icons"
12
+ description "Flexible icon family with six weights (thin to fill, plus duotone)"
13
+ url "https://github.com/phosphor-icons/core/archive/refs/tags/v2.0.8.zip"
14
+ archive :zip
15
+ dir "phosphor"
16
+ extract "core-2.0.8/assets/*/*.svg"
17
+ end
18
+ end
19
+ end
20
+ end
21
+ end
@@ -240,3 +240,9 @@ require_relative "source/lucide"
240
240
  require_relative "source/simple_icons"
241
241
  require_relative "source/material_file_icons"
242
242
  require_relative "source/silk"
243
+ require_relative "source/coloured_icons"
244
+ require_relative "source/bootstrap_icons"
245
+ require_relative "source/octicons"
246
+ require_relative "source/iconoir"
247
+ require_relative "source/material_design_icons"
248
+ require_relative "source/phosphor"
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Unmagic
4
4
  class Icon
5
- VERSION = "0.1.0"
5
+ VERSION = "0.2.0"
6
6
  end
7
7
  end
@@ -0,0 +1,6 @@
1
+ <% icons.each do |icon| -%>
2
+ <div class="icon-item" data-icon="<%= escape(icon.name) %>">
3
+ <%= icon.to_svg %>
4
+ <div class="icon-name"><%= escape(icon.name) %></div>
5
+ </div>
6
+ <% end -%>
@@ -181,13 +181,38 @@
181
181
  padding: 40px;
182
182
  font-size: 14px;
183
183
  }
184
-
184
+
185
+ .load-more-wrap {
186
+ display: flex;
187
+ justify-content: center;
188
+ margin: 30px 0 10px;
189
+ }
190
+
191
+ .load-more {
192
+ padding: 10px 24px;
193
+ background: var(--tab-bg);
194
+ border: 1px solid var(--tab-border);
195
+ border-radius: 4px;
196
+ cursor: pointer;
197
+ font-size: 14px;
198
+ color: var(--fg);
199
+ }
200
+
201
+ .load-more:hover {
202
+ background: var(--tab-hover);
203
+ }
204
+
205
+ .load-more:disabled {
206
+ cursor: default;
207
+ opacity: 0.6;
208
+ }
209
+
185
210
  [hidden] {
186
211
  display: none !important;
187
212
  }
188
213
  </style>
189
- <link rel="icon" href="<%= url "/public/favicon.png" %>" type="image/png">
190
- <link rel="apple-touch-icon" href="<%= url "/public/favicon.png" %>">
214
+ <link rel="icon" href="<%= asset_url "/public/favicon.png" %>" type="image/png">
215
+ <link rel="apple-touch-icon" href="<%= asset_url "/public/favicon.png" %>">
191
216
  </head>
192
217
  <body>
193
218
  <div class="header">
@@ -204,8 +229,12 @@
204
229
  </div>
205
230
 
206
231
  <div class="tabs">
232
+ <a href="<%= library_url(nil) %>"
233
+ class="tab <%= @selected_library.nil? ? 'active' : '' %>">
234
+ All (<%= @libraries.sum { |library| library.icons.length } %>)
235
+ </a>
207
236
  <% @libraries.each do |library| %>
208
- <a href="<%= url(library.name, @request.params) %>"
237
+ <a href="<%= library_url(library.name) %>"
209
238
  class="tab <%= library == @selected_library ? 'active' : '' %>">
210
239
  <%= library.name %> (<%= library.icons.length %>)
211
240
  </a>
@@ -214,57 +243,100 @@
214
243
  </div>
215
244
 
216
245
  <div class="stats" id="stats"></div>
217
-
218
- <div class="icon-grid" id="iconGrid"></div>
219
-
246
+
247
+ <div class="icon-grid" id="iconGrid">
248
+ <% if @page_icons.empty? %>
249
+ <div class="no-results">No icons found</div>
250
+ <% else %>
251
+ <%= render_icons(@page_icons) %>
252
+ <% end %>
253
+ </div>
254
+
255
+ <div class="load-more-wrap" id="loadMoreWrap" hidden>
256
+ <button class="load-more" id="loadMore">Load more</button>
257
+ </div>
258
+
220
259
  <div class="copy-toast" id="copyToast">Copied to clipboard!</div>
221
260
 
222
261
  <script>
223
- // Icon data for current library only
224
- const currentLibrary = '<%= @selected_library.name %>';
225
- const iconsData = <%= @selected_library.icons.to_json %>;
226
-
227
- // State
228
- let searchQuery = '<%= @search_query %>';
229
-
262
+ // The current selection and paging state. Icons themselves are rendered by
263
+ // the server (the `icons` partial); the browser only fetches more of them.
264
+ const libraryParam = <%= @selected_param.to_json %>;
265
+ const libraryLabel = <%= (@selected_library&.name || "All").to_json %>;
266
+ const pageSize = <%= Unmagic::Icon::Web::PAGE_SIZE %>;
267
+
268
+ let searchQuery = <%= @search_query.to_json %>;
269
+ let total = <%= @total %>;
270
+ let loadedOffset = <%= @offset %>;
271
+
230
272
  // Elements
231
273
  const searchInput = document.getElementById('searchInput');
232
274
  const iconGrid = document.getElementById('iconGrid');
233
275
  const stats = document.getElementById('stats');
234
276
  const copyToast = document.getElementById('copyToast');
235
277
  const themeSwitcher = document.getElementById('themeSwitcher');
236
-
278
+ const loadMoreWrap = document.getElementById('loadMoreWrap');
279
+ const loadMoreBtn = document.getElementById('loadMore');
280
+
237
281
  // Functions
238
- function filterIcons() {
239
- const query = searchQuery.toLowerCase();
240
- const filtered = query
241
- ? iconsData.filter(icon => icon.name.toLowerCase().includes(query))
242
- : iconsData;
243
-
244
- renderIcons(filtered);
245
- updateStats(filtered.length);
282
+ function renderedCount() {
283
+ return iconGrid.querySelectorAll('.icon-item').length;
246
284
  }
247
-
248
- function renderIcons(icons) {
249
- if (icons.length === 0) {
250
- iconGrid.innerHTML = '<div class="no-results">No icons found</div>';
251
- return;
285
+
286
+ function updateStats() {
287
+ stats.textContent = `Showing ${renderedCount()} of ${total} icons from ${libraryLabel}`;
288
+ }
289
+
290
+ function updateLoadMore() {
291
+ loadMoreWrap.hidden = renderedCount() >= total;
292
+ }
293
+
294
+ // Build the URL for a page of rendered icon markup from the server.
295
+ function pageUrl(offset) {
296
+ const params = new URLSearchParams();
297
+ params.set('library', libraryParam);
298
+ if (searchQuery) params.set('search', searchQuery);
299
+ params.set('offset', offset);
300
+ params.set('format', 'fragment');
301
+ return window.location.pathname + '?' + params.toString();
302
+ }
303
+
304
+ let loading = false;
305
+ async function loadMore() {
306
+ if (loading) return;
307
+ loading = true;
308
+ loadMoreBtn.disabled = true;
309
+ loadMoreBtn.textContent = 'Loading…';
310
+ try {
311
+ const res = await fetch(pageUrl(loadedOffset + pageSize));
312
+ total = parseInt(res.headers.get('X-Total-Count'), 10);
313
+ loadedOffset = parseInt(res.headers.get('X-Offset'), 10);
314
+ iconGrid.insertAdjacentHTML('beforeend', await res.text());
315
+ } finally {
316
+ loading = false;
317
+ loadMoreBtn.disabled = false;
318
+ loadMoreBtn.textContent = 'Load more';
319
+ updateStats();
320
+ updateLoadMore();
252
321
  }
253
-
254
- iconGrid.innerHTML = icons.map(icon => {
255
- return `
256
- <div class="icon-item" data-icon="${icon.name}">
257
- ${icon.svg}
258
- <div class="icon-name">${icon.name}</div>
259
- </div>
260
- `;
261
- }).join('');
262
322
  }
263
-
264
- function updateStats(count) {
265
- stats.textContent = `Showing ${count} icons from ${currentLibrary}`;
323
+
324
+ // Live search: re-request the first page from the server. A sequence guard
325
+ // drops responses that arrive out of order while the user keeps typing.
326
+ let searchSeq = 0;
327
+ async function runSearch() {
328
+ const seq = ++searchSeq;
329
+ const res = await fetch(pageUrl(0));
330
+ const html = await res.text();
331
+ if (seq !== searchSeq) return;
332
+
333
+ total = parseInt(res.headers.get('X-Total-Count'), 10);
334
+ loadedOffset = 0;
335
+ iconGrid.innerHTML = html.trim() ? html : '<div class="no-results">No icons found</div>';
336
+ updateStats();
337
+ updateLoadMore();
266
338
  }
267
-
339
+
268
340
  function copyIcon(iconName) {
269
341
  const text = `${iconName}`;
270
342
  navigator.clipboard.writeText(text).then(() => {
@@ -275,18 +347,26 @@
275
347
  }
276
348
 
277
349
  function updateURL() {
278
- const params = new URLSearchParams();
350
+ // Preserve the current `library` selection, only swap `search`.
351
+ const params = new URLSearchParams(window.location.search);
279
352
  if (searchQuery) {
280
- params.set('q', searchQuery);
353
+ params.set('search', searchQuery);
354
+ } else {
355
+ params.delete('search');
281
356
  }
282
357
  const queryString = params.toString();
283
358
  const newURL = window.location.pathname + (queryString ? '?' + queryString : '');
284
359
  window.history.replaceState({}, '', newURL);
285
-
286
- // Update all tab links with new query
360
+
361
+ // Keep the search query in sync across every tab link.
287
362
  document.querySelectorAll('.tab').forEach(tab => {
288
- const href = tab.getAttribute('href').split('?')[0];
289
- tab.setAttribute('href', href + (queryString ? '?' + queryString : ''));
363
+ const url = new URL(tab.href, window.location.origin);
364
+ if (searchQuery) {
365
+ url.searchParams.set('search', searchQuery);
366
+ } else {
367
+ url.searchParams.delete('search');
368
+ }
369
+ tab.setAttribute('href', url.pathname + url.search);
290
370
  });
291
371
  }
292
372
 
@@ -306,24 +386,29 @@
306
386
  }
307
387
 
308
388
  // Event listeners
389
+ let searchTimer = null;
309
390
  searchInput.addEventListener('input', (e) => {
310
391
  searchQuery = e.target.value;
311
- filterIcons();
312
392
  updateURL();
393
+ clearTimeout(searchTimer);
394
+ searchTimer = setTimeout(runSearch, 200);
313
395
  });
314
-
396
+
397
+ loadMoreBtn.addEventListener('click', loadMore);
398
+
315
399
  iconGrid.addEventListener('click', (e) => {
316
400
  const iconItem = e.target.closest('.icon-item');
317
401
  if (iconItem) {
318
402
  copyIcon(iconItem.dataset.icon);
319
403
  }
320
404
  });
321
-
405
+
322
406
  themeSwitcher.addEventListener('click', toggleTheme);
323
-
407
+
324
408
  // Initialize
325
409
  loadTheme();
326
- filterIcons();
410
+ updateStats();
411
+ updateLoadMore();
327
412
  </script>
328
413
  </body>
329
414
  </html>
@@ -5,6 +5,7 @@ require "rack/utils"
5
5
 
6
6
  require "erb"
7
7
  require "cgi"
8
+ require "json"
8
9
 
9
10
  module Unmagic
10
11
  class Icon
@@ -21,49 +22,109 @@ module Unmagic
21
22
  end.to_app
22
23
  end
23
24
 
25
+ # The library param value that means "show icons from every library".
26
+ ALL_LIBRARIES = "all"
27
+
28
+ # How many icons to send per page (initial render and each "load more").
29
+ PAGE_SIZE = 100
30
+
24
31
  def call(env)
25
32
  @request = Rack::Request.new(env)
26
33
  @libraries = Unmagic::Icon::Library::Registry.all
27
34
 
28
- case @request.path_info
29
- when "/"
30
- # Redirect to first library
31
- first_library = @libraries.first.name.to_param
32
- if first_library.nil?
33
- [ 404, { "content-type" => "text/plain" }, [ "No libraries found" ] ]
34
- else
35
- [ 302, { "location" => url(first_library, @request.params) }, [] ]
36
- end
37
- else
38
- library_name = @request.path_info.delete_prefix("/")
35
+ @search_query = @request.params["search"].to_s
36
+ @offset = @request.params["offset"].to_i
37
+ @offset = 0 if @offset.negative?
38
+ library_param = @request.params["library"].to_s
39
39
 
40
+ # No `library` param (a fresh page load) defaults to "All".
41
+ if library_param.empty? || library_param == ALL_LIBRARIES
42
+ @selected_library = nil
43
+ @selected_param = ALL_LIBRARIES
44
+ candidates = @libraries.flat_map(&:icons)
45
+ else
40
46
  begin
41
- @selected_library = Unmagic::Icon::Library::Registry.find(library_name)
42
- rescue Unmagic::Icon::LibraryNotError
43
- return [ 404, { "content-type" => "text/plain" }, [ "Library not found: #{@selected_library}" ] ]
47
+ @selected_library = Unmagic::Icon::Library::Registry.find(library_param)
48
+ rescue Unmagic::Icon::LibraryNotFoundError
49
+ return [ 404, { "content-type" => "text/plain" }, [ "Library not found: #{library_param}" ] ]
44
50
  end
51
+ @selected_param = @selected_library.name
52
+ candidates = @selected_library.icons
53
+ end
45
54
 
46
- template_path = File.expand_path("web/views/layout.html.erb", __dir__)
47
- template = ERB.new(File.read(template_path))
48
- html = template.result(binding)
55
+ # Search and paginate on the server; only the page's icons get their svg
56
+ # read+serialized, so we never inline the whole (possibly huge) set.
57
+ filtered = filter_icons(candidates, @search_query)
58
+ @total = filtered.length
59
+ @page_icons = filtered[@offset, PAGE_SIZE] || []
49
60
 
50
- [ 200, { "content-type" => "text/html; charset=utf-8" }, [ html ] ]
51
- end
61
+ return fragment_response if fragment_request?
62
+
63
+ template_path = File.expand_path("web/views/layout.html.erb", __dir__)
64
+ template = ERB.new(File.read(template_path))
65
+ html = template.result(binding)
66
+
67
+ [ 200, { "content-type" => "text/html; charset=utf-8" }, [ html ] ]
52
68
  end
53
69
 
54
70
  def escape(text)
55
71
  CGI.escapeHTML(text.to_s)
56
72
  end
57
73
 
58
- def url(path_parts, params_hash = nil)
59
- url = [ @request.env["SCRIPT_NAME"], *path_parts ].compact.join("/")
60
- url = url.gsub(/\/\//, "/")
74
+ # Case-insensitive substring match on the icon name.
75
+ def filter_icons(icons, query)
76
+ return icons if query.empty?
61
77
 
62
- if params_hash
63
- "#{url}?#{Rack::Utils.build_query(params_hash)}"
64
- else
65
- url
66
- end
78
+ needle = query.downcase
79
+ icons.select { |icon| icon.name.downcase.include?(needle) }
80
+ end
81
+
82
+ def fragment_request?
83
+ @request.params["format"] == "fragment"
84
+ end
85
+
86
+ # The page of icons as a rendered HTML fragment, which the browser appends
87
+ # ("load more") or swaps in (live search). Counts ride along as headers so
88
+ # the body stays pure markup. Icons are rendered in exactly one place —
89
+ # the `icons` partial, shared with the full page.
90
+ def fragment_response
91
+ headers = {
92
+ "content-type" => "text/html; charset=utf-8",
93
+ "x-total-count" => @total.to_s,
94
+ "x-offset" => @offset.to_s,
95
+ "x-page-size" => PAGE_SIZE.to_s
96
+ }
97
+
98
+ [ 200, headers, [ render_icons(@page_icons) ] ]
99
+ end
100
+
101
+ # Render the shared icons partial for a list of icons. `-%>` trim keeps the
102
+ # output empty (not whitespace) when there are no icons, so the browser can
103
+ # detect "no results".
104
+ def render_icons(icons)
105
+ template_path = File.expand_path("web/views/icons.html.erb", __dir__)
106
+ ERB.new(File.read(template_path), trim_mode: "-").result(binding)
107
+ end
108
+
109
+ # Build a URL to the gallery selecting the given library, carrying the
110
+ # current search query along. `library_name` of nil selects "All".
111
+ def library_url(library_name)
112
+ params = { "library" => library_name || ALL_LIBRARIES }
113
+ params["search"] = @search_query unless @search_query.empty?
114
+ url(params)
115
+ end
116
+
117
+ # Build a static asset URL, honouring any SCRIPT_NAME mount prefix.
118
+ def asset_url(path)
119
+ [ @request.env["SCRIPT_NAME"], path ].join.gsub(%r{/+}, "/")
120
+ end
121
+
122
+ private
123
+
124
+ def url(params)
125
+ base = @request.env["SCRIPT_NAME"].to_s
126
+ base = "/" if base.empty?
127
+ "#{base}?#{Rack::Utils.build_query(params)}"
67
128
  end
68
129
  end
69
130
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: unmagic-icon
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Keith Pitt
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2026-06-21 00:00:00.000000000 Z
11
+ date: 2026-06-22 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activesupport
@@ -101,17 +101,24 @@ files:
101
101
  - lib/unmagic/icon/library.rb
102
102
  - lib/unmagic/icon/library/registry.rb
103
103
  - lib/unmagic/icon/library/source.rb
104
+ - lib/unmagic/icon/library/source/bootstrap_icons.rb
105
+ - lib/unmagic/icon/library/source/coloured_icons.rb
104
106
  - lib/unmagic/icon/library/source/devicons.rb
105
107
  - lib/unmagic/icon/library/source/feather.rb
106
108
  - lib/unmagic/icon/library/source/heroicons.rb
109
+ - lib/unmagic/icon/library/source/iconoir.rb
107
110
  - lib/unmagic/icon/library/source/lucide.rb
111
+ - lib/unmagic/icon/library/source/material_design_icons.rb
108
112
  - lib/unmagic/icon/library/source/material_file_icons.rb
113
+ - lib/unmagic/icon/library/source/octicons.rb
114
+ - lib/unmagic/icon/library/source/phosphor.rb
109
115
  - lib/unmagic/icon/library/source/silk.rb
110
116
  - lib/unmagic/icon/library/source/simple_icons.rb
111
117
  - lib/unmagic/icon/library/source/tabler.rb
112
118
  - lib/unmagic/icon/version.rb
113
119
  - lib/unmagic/icon/web.rb
114
120
  - lib/unmagic/icon/web/public/favicon.png
121
+ - lib/unmagic/icon/web/views/icons.html.erb
115
122
  - lib/unmagic/icon/web/views/layout.html.erb
116
123
  - lib/unmagic_icon.rb
117
124
  homepage: https://github.com/unreasonable-magic/unmagic-icon