motif-breadcrumbs 0.4.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 +7 -0
- data/README.md +418 -0
- data/Rakefile +9 -0
- data/lib/motif/breadcrumbs/components/crumb_component.html.erb +13 -0
- data/lib/motif/breadcrumbs/components/crumb_component.rb +26 -0
- data/lib/motif/breadcrumbs/components/json_ld_component.html.erb +3 -0
- data/lib/motif/breadcrumbs/components/json_ld_component.rb +41 -0
- data/lib/motif/breadcrumbs/components/navigation_component.html.erb +14 -0
- data/lib/motif/breadcrumbs/components/navigation_component.rb +28 -0
- data/lib/motif/breadcrumbs/controller.rb +23 -0
- data/lib/motif/breadcrumbs/crumb.rb +24 -0
- data/lib/motif/breadcrumbs/railtie.rb +10 -0
- data/lib/motif/breadcrumbs/segment.rb +25 -0
- data/lib/motif/breadcrumbs/trail.rb +61 -0
- data/lib/motif/breadcrumbs.rb +18 -0
- metadata +115 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 58b85b39ff42181e6eec1001c1b8121bea0a1b842c3a97a2213e0abc0365bf9d
|
|
4
|
+
data.tar.gz: '01800f3aeae6ff1d248d1c30b74eea3ec0751ff1dc1df207a214b928be282b53'
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: ce067650e5e59d0ba7eb8cf783b33905b355962e687ceeca77ff92981551135ae5b6753f5facdb294c2a7fafcdaacab49d692affde41a4bbbf059cdbd59136c0
|
|
7
|
+
data.tar.gz: d459e96bb31f8df2f0ad42eb6a3d006cbb34ec13efbb4b932dbaef5ff939060bd27068d8ad841695b019d67eff0b122684c58bd1b3e7e1d5e19591b3521926ad
|
data/README.md
ADDED
|
@@ -0,0 +1,418 @@
|
|
|
1
|
+
# Motif::Breadcrumbs
|
|
2
|
+
|
|
3
|
+
Breadcrumb trail management for Rails applications.
|
|
4
|
+
|
|
5
|
+
Provides a typed data model, ViewComponents for rendering, schema.org JSON-LD
|
|
6
|
+
structured data, and a composable Segment pattern for encapsulating complex
|
|
7
|
+
breadcrumb logic.
|
|
8
|
+
|
|
9
|
+
## Installation
|
|
10
|
+
|
|
11
|
+
Add to your `Gemfile`:
|
|
12
|
+
|
|
13
|
+
```ruby
|
|
14
|
+
gem "motif-breadcrumbs"
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
Run `bundle install`.
|
|
18
|
+
|
|
19
|
+
## Quick Start
|
|
20
|
+
|
|
21
|
+
### 1. Include the controller concern
|
|
22
|
+
|
|
23
|
+
```ruby
|
|
24
|
+
class ApplicationController < ActionController::Base
|
|
25
|
+
include Motif::Breadcrumbs::Controller
|
|
26
|
+
|
|
27
|
+
# if needed, override initialize_breadcrumbs to add a default crumb
|
|
28
|
+
def initialize_breadcrumbs
|
|
29
|
+
super
|
|
30
|
+
breadcrumbs.push("Home", root_path)
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
The concern provides:
|
|
36
|
+
- `@breadcrumbs` — a `Motif::Breadcrumbs::Trail` initialized before each action
|
|
37
|
+
- `breadcrumbs` — a helper method available in controllers and views
|
|
38
|
+
|
|
39
|
+
### 2. Push crumbs in your controllers
|
|
40
|
+
|
|
41
|
+
```ruby
|
|
42
|
+
class PostsController < ApplicationController
|
|
43
|
+
def index
|
|
44
|
+
breadcrumbs.push("Posts", posts_path)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def show
|
|
48
|
+
@post = Post.find(params[:id])
|
|
49
|
+
breadcrumbs.push("Posts", posts_path)
|
|
50
|
+
breadcrumbs.push(@post.title)
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
### 3. Render in your layout
|
|
56
|
+
|
|
57
|
+
```erb
|
|
58
|
+
<%# app/views/layouts/application.html.erb %>
|
|
59
|
+
<head>
|
|
60
|
+
<%= render Motif::Breadcrumbs::JsonLdComponent.new(
|
|
61
|
+
trail: breadcrumbs,
|
|
62
|
+
base_url: root_url,
|
|
63
|
+
current_url: request.original_url
|
|
64
|
+
) %>
|
|
65
|
+
</head>
|
|
66
|
+
<body>
|
|
67
|
+
<%= render Motif::Breadcrumbs::NavigationComponent.new(
|
|
68
|
+
trail: breadcrumbs,
|
|
69
|
+
html_class: "breadcrumbs text-sm"
|
|
70
|
+
) %>
|
|
71
|
+
<%= yield %>
|
|
72
|
+
</body>
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
## Core Concepts
|
|
76
|
+
|
|
77
|
+
### Trail
|
|
78
|
+
|
|
79
|
+
`Motif::Breadcrumbs::Trail` is the core data structure — an ordered stack of
|
|
80
|
+
crumbs. It includes `Enumerable` and provides `push`, `pop`, `size`, `any?`,
|
|
81
|
+
`empty?`, `each`, `last`, and `[]`.
|
|
82
|
+
|
|
83
|
+
```ruby
|
|
84
|
+
trail = Motif::Breadcrumbs::Trail.new
|
|
85
|
+
trail.push("Home", "/")
|
|
86
|
+
trail.push("Posts", "/posts")
|
|
87
|
+
trail.push("My Post") # no href = current page
|
|
88
|
+
trail.push("Settings", "/settings", "gear") # with icon identifier
|
|
89
|
+
|
|
90
|
+
trail.size # => 4
|
|
91
|
+
trail.last # => Crumb(text: "Settings", href: "/settings", icon: "gear")
|
|
92
|
+
trail.map(&:text) # => ["Home", "Posts", "My Post", "Settings"]
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
Trail does **not** include Rails URL helpers. It is a pure data structure with
|
|
96
|
+
no dependency on routing.
|
|
97
|
+
|
|
98
|
+
### Crumb
|
|
99
|
+
|
|
100
|
+
`Motif::Breadcrumbs::Crumb` is a Sorbet `T::Struct` with three fields:
|
|
101
|
+
|
|
102
|
+
| Field | Type | Description |
|
|
103
|
+
|--------|-------------------|------------------------------------|
|
|
104
|
+
| `text` | `String` | Display text for the breadcrumb |
|
|
105
|
+
| `href` | `String?` | URL path (nil for the current page)|
|
|
106
|
+
| `icon` | `String?` | Icon identifier (app-interpreted) |
|
|
107
|
+
|
|
108
|
+
```ruby
|
|
109
|
+
crumb = Motif::Breadcrumbs::Crumb.new(text: "Home", href: "/", icon: "home")
|
|
110
|
+
crumb.text # => "Home"
|
|
111
|
+
crumb.href # => "/"
|
|
112
|
+
crumb.icon # => "home"
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
### Segment
|
|
116
|
+
|
|
117
|
+
`Motif::Breadcrumbs::Segment` is an abstract base class for encapsulating
|
|
118
|
+
reusable breadcrumb-building logic. A Segment is a self-contained object that
|
|
119
|
+
produces an array of `Crumb` structs via `to_crumbs`.
|
|
120
|
+
|
|
121
|
+
Segments can be pushed directly onto a trail — `Trail#push` detects objects that
|
|
122
|
+
respond to `to_crumbs` and expands them automatically:
|
|
123
|
+
|
|
124
|
+
```ruby
|
|
125
|
+
breadcrumbs.push("Home", root_path)
|
|
126
|
+
breadcrumbs.push(Motif::Breadcrumbs::WorkspaceCrumbs.new(workspace: @workspace))
|
|
127
|
+
breadcrumbs.push("Settings", settings_path)
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
This makes complex multi-crumb logic **encapsulated**, **testable**, and
|
|
131
|
+
**composable**.
|
|
132
|
+
|
|
133
|
+
## Creating Segments
|
|
134
|
+
|
|
135
|
+
### Directory convention
|
|
136
|
+
|
|
137
|
+
Place your segments in `app/motif/breadcrumbs/`. Rails treats `app/motif` as a
|
|
138
|
+
Zeitwerk autoload root, so the `motif` directory name is stripped from the
|
|
139
|
+
namespace. Files map to `Breadcrumbs::*`:
|
|
140
|
+
|
|
141
|
+
```
|
|
142
|
+
app/motif/breadcrumbs/application_segment.rb → Breadcrumbs::ApplicationSegment
|
|
143
|
+
app/motif/breadcrumbs/workspace_crumbs.rb → Breadcrumbs::WorkspaceCrumbs
|
|
144
|
+
app/motif/breadcrumbs/foo/bar_crumbs.rb → Breadcrumbs::Foo::BarCrumbs
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
Segments inherit from `Motif::Breadcrumbs::Segment` using the fully qualified
|
|
148
|
+
name.
|
|
149
|
+
|
|
150
|
+
### ApplicationSegment
|
|
151
|
+
|
|
152
|
+
Create an app-level base class that includes Rails URL helpers. This mirrors the
|
|
153
|
+
`ApplicationRecord` / `ApplicationController` pattern:
|
|
154
|
+
|
|
155
|
+
```ruby
|
|
156
|
+
# app/motif/breadcrumbs/application_segment.rb
|
|
157
|
+
module Breadcrumbs
|
|
158
|
+
class ApplicationSegment < Motif::Breadcrumbs::Segment
|
|
159
|
+
# Route helpers are included at runtime via `send` to avoid Sorbet
|
|
160
|
+
# error 4002 (dynamic constant in `include`).
|
|
161
|
+
send(:include, Rails.application.routes.url_helpers)
|
|
162
|
+
end
|
|
163
|
+
end
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
### Writing a segment
|
|
167
|
+
|
|
168
|
+
Subclass `ApplicationSegment`, accept dependencies via `initialize`, and
|
|
169
|
+
implement `to_crumbs` returning an array of `Crumb` structs. Use the `crumb`
|
|
170
|
+
factory method for convenience:
|
|
171
|
+
|
|
172
|
+
```ruby
|
|
173
|
+
# app/motif/breadcrumbs/workspace_crumbs.rb
|
|
174
|
+
module Breadcrumbs
|
|
175
|
+
class WorkspaceCrumbs < ApplicationSegment
|
|
176
|
+
def initialize(workspace:)
|
|
177
|
+
@workspace = workspace
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
def to_crumbs
|
|
181
|
+
[crumb("Workspaces", workspaces_path),
|
|
182
|
+
crumb(@workspace.name, workspace_path(@workspace))]
|
|
183
|
+
end
|
|
184
|
+
end
|
|
185
|
+
end
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
### Composing segments
|
|
189
|
+
|
|
190
|
+
Segments can include other segments by concatenating their `to_crumbs` output:
|
|
191
|
+
|
|
192
|
+
```ruby
|
|
193
|
+
# app/motif/breadcrumbs/github/repository_crumbs.rb
|
|
194
|
+
module Breadcrumbs
|
|
195
|
+
class Github::RepositoryCrumbs < ApplicationSegment
|
|
196
|
+
def initialize(repository:, user: nil)
|
|
197
|
+
@repository = repository
|
|
198
|
+
@user = user
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
def to_crumbs
|
|
202
|
+
crumbs = []
|
|
203
|
+
if @user && (workspace = shared_workspace)
|
|
204
|
+
crumbs.concat(Breadcrumbs::WorkspaceCrumbs.new(workspace: workspace).to_crumbs)
|
|
205
|
+
crumbs << crumb("GitHub", github_installations_path)
|
|
206
|
+
end
|
|
207
|
+
crumbs << crumb(@repository.full_name, github_repository_path(@repository))
|
|
208
|
+
crumbs
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
private
|
|
212
|
+
|
|
213
|
+
def shared_workspace
|
|
214
|
+
workspaces = @repository.common_workspaces_with(user: @user)
|
|
215
|
+
workspaces.first if workspaces.size == 1
|
|
216
|
+
end
|
|
217
|
+
end
|
|
218
|
+
end
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
### Testing segments
|
|
222
|
+
|
|
223
|
+
Segments are independently testable — no controller or request context needed:
|
|
224
|
+
|
|
225
|
+
```ruby
|
|
226
|
+
RSpec.describe Breadcrumbs::Github::RepositoryCrumbs do
|
|
227
|
+
let(:repository) { create(:github_repository) }
|
|
228
|
+
|
|
229
|
+
it "includes repository name" do
|
|
230
|
+
crumbs = described_class.new(repository: repository).to_crumbs
|
|
231
|
+
|
|
232
|
+
expect(crumbs.last.text).to eq(repository.full_name)
|
|
233
|
+
end
|
|
234
|
+
|
|
235
|
+
it "includes workspace segment for single-workspace users" do
|
|
236
|
+
user = create(:user_with_workspace)
|
|
237
|
+
crumbs = described_class.new(repository: repository, user: user).to_crumbs
|
|
238
|
+
|
|
239
|
+
expect(crumbs.first.text).to eq("Workspaces")
|
|
240
|
+
end
|
|
241
|
+
end
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
## View Components
|
|
245
|
+
|
|
246
|
+
### NavigationComponent
|
|
247
|
+
|
|
248
|
+
Renders the breadcrumb trail as a `<nav>` element with semantic HTML.
|
|
249
|
+
|
|
250
|
+
```ruby
|
|
251
|
+
Motif::Breadcrumbs::NavigationComponent.new(
|
|
252
|
+
trail: breadcrumbs,
|
|
253
|
+
html_class: "breadcrumbs text-sm", # CSS classes on <nav> (default)
|
|
254
|
+
aria_current_on_last: true, # aria-current="page" on last crumb (default)
|
|
255
|
+
min_crumbs: 2, # minimum crumbs to render (default)
|
|
256
|
+
icon_renderer: nil # optional lambda for icon rendering (default: nil)
|
|
257
|
+
)
|
|
258
|
+
```
|
|
259
|
+
|
|
260
|
+
Output structure:
|
|
261
|
+
|
|
262
|
+
```html
|
|
263
|
+
<nav class="breadcrumbs text-sm" aria-label="Breadcrumb">
|
|
264
|
+
<ul>
|
|
265
|
+
<li><a href="/">Home</a></li>
|
|
266
|
+
<li><a href="/posts">Posts</a></li>
|
|
267
|
+
<li aria-current="page"><span>My Post</span></li>
|
|
268
|
+
</ul>
|
|
269
|
+
</nav>
|
|
270
|
+
```
|
|
271
|
+
|
|
272
|
+
The component uses the DaisyUI `.breadcrumbs` class by default. Override
|
|
273
|
+
`html_class` to customize styling per layout:
|
|
274
|
+
|
|
275
|
+
```erb
|
|
276
|
+
<%# Public layout %>
|
|
277
|
+
<%= render Motif::Breadcrumbs::NavigationComponent.new(
|
|
278
|
+
trail: breadcrumbs,
|
|
279
|
+
html_class: "px-4 breadcrumbs text-sm font-light text-base-content/65"
|
|
280
|
+
) %>
|
|
281
|
+
|
|
282
|
+
<%# Admin layout %>
|
|
283
|
+
<%= render Motif::Breadcrumbs::NavigationComponent.new(
|
|
284
|
+
trail: breadcrumbs,
|
|
285
|
+
html_class: "breadcrumbs text-sm",
|
|
286
|
+
min_crumbs: 1
|
|
287
|
+
) %>
|
|
288
|
+
```
|
|
289
|
+
|
|
290
|
+
The component does not render when the trail has fewer crumbs than `min_crumbs`.
|
|
291
|
+
|
|
292
|
+
### CrumbComponent
|
|
293
|
+
|
|
294
|
+
Renders a single breadcrumb item. Used internally by `NavigationComponent`, but
|
|
295
|
+
can be used directly for custom rendering.
|
|
296
|
+
|
|
297
|
+
```ruby
|
|
298
|
+
Motif::Breadcrumbs::CrumbComponent.new(
|
|
299
|
+
text: "Home", # display text (required)
|
|
300
|
+
href: "/", # URL (optional — <a> if present, <span> if nil)
|
|
301
|
+
icon: "home", # icon identifier (optional)
|
|
302
|
+
current: false # adds aria-current="page" (default: false)
|
|
303
|
+
)
|
|
304
|
+
```
|
|
305
|
+
|
|
306
|
+
#### Icon rendering
|
|
307
|
+
|
|
308
|
+
The `icon` field is an opaque string identifier. The gem does not depend on any
|
|
309
|
+
icon library — your app provides the rendering logic.
|
|
310
|
+
|
|
311
|
+
**Via `icon_renderer` (recommended):** Pass a lambda to `NavigationComponent`
|
|
312
|
+
that receives an icon name and returns HTML. The renderer is forwarded to each
|
|
313
|
+
`CrumbComponent` automatically:
|
|
314
|
+
|
|
315
|
+
```erb
|
|
316
|
+
<%= render Motif::Breadcrumbs::NavigationComponent.new(
|
|
317
|
+
trail: breadcrumbs,
|
|
318
|
+
icon_renderer: ->(name) { lucide_icon(name, class: "w-4 h-4") }
|
|
319
|
+
) %>
|
|
320
|
+
```
|
|
321
|
+
|
|
322
|
+
The lambda must return an `html_safe` string (most icon helpers do this).
|
|
323
|
+
|
|
324
|
+
**Via icon slot (full control):** For standalone `CrumbComponent` usage, the
|
|
325
|
+
`renders_one :icon` slot gives complete control over icon markup:
|
|
326
|
+
|
|
327
|
+
```erb
|
|
328
|
+
<%= render Motif::Breadcrumbs::CrumbComponent.new(text: "Home", href: "/") do |c| %>
|
|
329
|
+
<% c.with_icon do %>
|
|
330
|
+
<%= lucide_icon("home", class: "w-4 h-4") %>
|
|
331
|
+
<% end %>
|
|
332
|
+
<% end %>
|
|
333
|
+
```
|
|
334
|
+
|
|
335
|
+
When both an icon slot and an `icon_renderer` are provided, the slot takes
|
|
336
|
+
priority.
|
|
337
|
+
|
|
338
|
+
### JsonLdComponent
|
|
339
|
+
|
|
340
|
+
Renders schema.org `BreadcrumbList` structured data as JSON-LD.
|
|
341
|
+
|
|
342
|
+
```ruby
|
|
343
|
+
Motif::Breadcrumbs::JsonLdComponent.new(
|
|
344
|
+
trail: breadcrumbs,
|
|
345
|
+
base_url: root_url, # absolute base URL for building full URLs
|
|
346
|
+
current_url: request.original_url # fallback URL for crumbs without href
|
|
347
|
+
)
|
|
348
|
+
```
|
|
349
|
+
|
|
350
|
+
Output:
|
|
351
|
+
|
|
352
|
+
```html
|
|
353
|
+
<script type="application/ld+json">
|
|
354
|
+
{"@context":"https://schema.org","@type":"BreadcrumbList","itemListElement":[
|
|
355
|
+
{"@type":"ListItem","position":1,"name":"Home","item":"https://example.com/"},
|
|
356
|
+
{"@type":"ListItem","position":2,"name":"Posts","item":"https://example.com/posts"}
|
|
357
|
+
]}
|
|
358
|
+
</script>
|
|
359
|
+
```
|
|
360
|
+
|
|
361
|
+
The component does not render when the trail has 1 or fewer crumbs.
|
|
362
|
+
|
|
363
|
+
## Customization
|
|
364
|
+
|
|
365
|
+
### CSS classes
|
|
366
|
+
|
|
367
|
+
Pass `html_class` to `NavigationComponent` to customize the CSS classes on the
|
|
368
|
+
`<nav>` element. The default is `"breadcrumbs text-sm"` which uses DaisyUI's
|
|
369
|
+
breadcrumb styling.
|
|
370
|
+
|
|
371
|
+
### Template overrides
|
|
372
|
+
|
|
373
|
+
For deeper customization, subclass a component and provide your own sidecar
|
|
374
|
+
`.html.erb` template. ViewComponent resolves templates from the subclass
|
|
375
|
+
directory first:
|
|
376
|
+
|
|
377
|
+
```ruby
|
|
378
|
+
# app/components/app_breadcrumbs_nav.rb
|
|
379
|
+
class AppBreadcrumbsNav < Motif::Breadcrumbs::NavigationComponent
|
|
380
|
+
end
|
|
381
|
+
```
|
|
382
|
+
|
|
383
|
+
```erb
|
|
384
|
+
<%# app/components/app_breadcrumbs_nav.html.erb %>
|
|
385
|
+
<div class="my-custom-breadcrumbs">
|
|
386
|
+
<%# your custom markup %>
|
|
387
|
+
</div>
|
|
388
|
+
```
|
|
389
|
+
|
|
390
|
+
### Icon rendering
|
|
391
|
+
|
|
392
|
+
Pass an `icon_renderer` lambda to `NavigationComponent` to integrate with your
|
|
393
|
+
icon library:
|
|
394
|
+
|
|
395
|
+
```erb
|
|
396
|
+
<%= render Motif::Breadcrumbs::NavigationComponent.new(
|
|
397
|
+
trail: breadcrumbs,
|
|
398
|
+
icon_renderer: ->(name) { heroicon(name, class: "w-4 h-4") }
|
|
399
|
+
) %>
|
|
400
|
+
```
|
|
401
|
+
|
|
402
|
+
The `CrumbComponent`'s `renders_one :icon` slot is also available for
|
|
403
|
+
fine-grained control when rendering crumbs individually.
|
|
404
|
+
|
|
405
|
+
## Sorbet
|
|
406
|
+
|
|
407
|
+
All core classes are typed (`# typed: true`) with Sorbet signatures.
|
|
408
|
+
|
|
409
|
+
After adding the gem to your app, generate RBIs:
|
|
410
|
+
|
|
411
|
+
```bash
|
|
412
|
+
bundle exec tapioca gems
|
|
413
|
+
bundle exec tapioca dsl
|
|
414
|
+
```
|
|
415
|
+
|
|
416
|
+
## License
|
|
417
|
+
|
|
418
|
+
MIT
|
data/Rakefile
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
<li<%= " aria-current=\"page\"".html_safe if @current %>>
|
|
2
|
+
<% if @href.present? %>
|
|
3
|
+
<a href="<%= @href %>">
|
|
4
|
+
<% if icon? %><%= icon %><% elsif rendered_icon %><%= rendered_icon %><% end %>
|
|
5
|
+
<%= @text %>
|
|
6
|
+
</a>
|
|
7
|
+
<% else %>
|
|
8
|
+
<span>
|
|
9
|
+
<% if icon? %><%= icon %><% elsif rendered_icon %><%= rendered_icon %><% end %>
|
|
10
|
+
<%= @text %>
|
|
11
|
+
</span>
|
|
12
|
+
<% end %>
|
|
13
|
+
</li>
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# typed: false
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
module Motif
|
|
5
|
+
module Breadcrumbs
|
|
6
|
+
class CrumbComponent < ViewComponent::Base
|
|
7
|
+
renders_one :icon
|
|
8
|
+
|
|
9
|
+
#: (text: String, ?href: String?, ?icon: String?, ?current: bool, ?icon_renderer: ^(String) -> String | nil) -> void
|
|
10
|
+
def initialize(text:, href: nil, icon: nil, current: false, icon_renderer: nil)
|
|
11
|
+
@text = text
|
|
12
|
+
@href = href
|
|
13
|
+
@icon = icon
|
|
14
|
+
@current = current
|
|
15
|
+
@icon_renderer = icon_renderer
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
#: -> String?
|
|
19
|
+
def rendered_icon
|
|
20
|
+
return unless @icon.present? && @icon_renderer
|
|
21
|
+
|
|
22
|
+
@icon_renderer.call(@icon)
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# typed: false
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
module Motif
|
|
5
|
+
module Breadcrumbs
|
|
6
|
+
class JsonLdComponent < ViewComponent::Base
|
|
7
|
+
#: (trail: Trail, base_url: String, current_url: String) -> void
|
|
8
|
+
def initialize(trail:, base_url:, current_url:)
|
|
9
|
+
@trail = trail
|
|
10
|
+
@base_url = base_url.chomp("/")
|
|
11
|
+
@current_url = current_url
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
#: -> bool
|
|
15
|
+
def render?
|
|
16
|
+
@trail.size > 1
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
private
|
|
20
|
+
|
|
21
|
+
#: -> String
|
|
22
|
+
def json_ld
|
|
23
|
+
items = @trail.each_with_index.map do |crumb, idx|
|
|
24
|
+
item = {
|
|
25
|
+
"@type" => "ListItem",
|
|
26
|
+
"position" => idx + 1,
|
|
27
|
+
"name" => crumb.text
|
|
28
|
+
}
|
|
29
|
+
item["item"] = crumb.href.present? ? "#{@base_url}#{crumb.href}" : @current_url
|
|
30
|
+
item
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
{
|
|
34
|
+
"@context" => "https://schema.org",
|
|
35
|
+
"@type" => "BreadcrumbList",
|
|
36
|
+
"itemListElement" => items
|
|
37
|
+
}.to_json
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
<nav class="<%= @html_class %>" aria-label="Breadcrumb">
|
|
2
|
+
<ul>
|
|
3
|
+
<% @trail.each_with_index do |crumb, idx| %>
|
|
4
|
+
<% current = @aria_current_on_last && idx == @trail.size - 1 %>
|
|
5
|
+
<%= render Motif::Breadcrumbs::CrumbComponent.new(
|
|
6
|
+
text: crumb.text,
|
|
7
|
+
href: current ? nil : crumb.href,
|
|
8
|
+
icon: crumb.icon,
|
|
9
|
+
current: current,
|
|
10
|
+
icon_renderer: @icon_renderer
|
|
11
|
+
) %>
|
|
12
|
+
<% end %>
|
|
13
|
+
</ul>
|
|
14
|
+
</nav>
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# typed: false
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
module Motif
|
|
5
|
+
module Breadcrumbs
|
|
6
|
+
class NavigationComponent < ViewComponent::Base
|
|
7
|
+
#: (
|
|
8
|
+
#| trail: Trail,
|
|
9
|
+
#| ?html_class: String,
|
|
10
|
+
#| ?aria_current_on_last: bool,
|
|
11
|
+
#| ?min_crumbs: Integer,
|
|
12
|
+
#| ?icon_renderer: ^(String) -> String | nil
|
|
13
|
+
#| ) -> void
|
|
14
|
+
def initialize(trail:, html_class: "breadcrumbs text-sm", aria_current_on_last: true, min_crumbs: 2, icon_renderer: nil)
|
|
15
|
+
@trail = trail
|
|
16
|
+
@html_class = html_class
|
|
17
|
+
@aria_current_on_last = aria_current_on_last
|
|
18
|
+
@min_crumbs = min_crumbs
|
|
19
|
+
@icon_renderer = icon_renderer
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
#: -> bool
|
|
23
|
+
def render?
|
|
24
|
+
@trail.size >= @min_crumbs
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# typed: false
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
module Motif
|
|
5
|
+
module Breadcrumbs
|
|
6
|
+
module Controller
|
|
7
|
+
extend ActiveSupport::Concern
|
|
8
|
+
|
|
9
|
+
included do
|
|
10
|
+
before_action :initialize_breadcrumbs
|
|
11
|
+
helper_method :breadcrumbs
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def initialize_breadcrumbs
|
|
15
|
+
@breadcrumbs = Trail.new
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def breadcrumbs
|
|
19
|
+
@breadcrumbs
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# typed: strict
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
module Motif
|
|
5
|
+
module Breadcrumbs
|
|
6
|
+
class Crumb
|
|
7
|
+
#: String
|
|
8
|
+
attr_reader :text
|
|
9
|
+
|
|
10
|
+
#: String?
|
|
11
|
+
attr_reader :href
|
|
12
|
+
|
|
13
|
+
#: String?
|
|
14
|
+
attr_reader :icon
|
|
15
|
+
|
|
16
|
+
#: (text: String, ?href: String?, ?icon: String?) -> void
|
|
17
|
+
def initialize(text:, href: nil, icon: nil)
|
|
18
|
+
@text = text
|
|
19
|
+
@href = href
|
|
20
|
+
@icon = icon
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# typed: strict
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
module Motif
|
|
5
|
+
module Breadcrumbs
|
|
6
|
+
class Segment
|
|
7
|
+
#: -> void
|
|
8
|
+
def initialize
|
|
9
|
+
raise "#{self.class} is abstract and cannot be instantiated directly" if instance_of?(Segment)
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
#: -> Array[Crumb]
|
|
13
|
+
def to_crumbs
|
|
14
|
+
raise NotImplementedError
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
private
|
|
18
|
+
|
|
19
|
+
#: (String, ?String?, ?String?) -> Crumb
|
|
20
|
+
def crumb(text, href = nil, icon = nil)
|
|
21
|
+
Crumb.new(text: text, href: href, icon: icon)
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
# typed: false
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
module Motif
|
|
5
|
+
module Breadcrumbs
|
|
6
|
+
class Trail
|
|
7
|
+
include Enumerable
|
|
8
|
+
|
|
9
|
+
#: Array[Crumb]
|
|
10
|
+
attr_reader :crumbs
|
|
11
|
+
|
|
12
|
+
#: -> void
|
|
13
|
+
def initialize
|
|
14
|
+
@crumbs = [] #: Array[Crumb]
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
#: (String | Segment, ?String?, ?String?) -> void
|
|
18
|
+
def push(text_or_segment, href = nil, icon = nil)
|
|
19
|
+
if text_or_segment.respond_to?(:to_crumbs)
|
|
20
|
+
@crumbs.concat(text_or_segment.to_crumbs)
|
|
21
|
+
else
|
|
22
|
+
@crumbs.push(Crumb.new(text: text_or_segment, href: href, icon: icon))
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
#: -> Crumb?
|
|
27
|
+
def pop
|
|
28
|
+
@crumbs.pop
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def each(&block)
|
|
32
|
+
@crumbs.each(&block)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
#: -> Integer
|
|
36
|
+
def size
|
|
37
|
+
@crumbs.size
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
#: -> bool
|
|
41
|
+
def any?
|
|
42
|
+
@crumbs.any?
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
#: -> bool
|
|
46
|
+
def empty?
|
|
47
|
+
@crumbs.empty?
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
#: -> Crumb?
|
|
51
|
+
def last
|
|
52
|
+
@crumbs.last
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
#: (Integer) -> Crumb?
|
|
56
|
+
def [](index)
|
|
57
|
+
@crumbs[index]
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# typed: strict
|
|
2
|
+
|
|
3
|
+
require "motif/breadcrumbs/railtie"
|
|
4
|
+
|
|
5
|
+
require "motif/breadcrumbs/controller"
|
|
6
|
+
require "motif/breadcrumbs/crumb"
|
|
7
|
+
require "motif/breadcrumbs/segment"
|
|
8
|
+
require "motif/breadcrumbs/trail"
|
|
9
|
+
|
|
10
|
+
require "motif/breadcrumbs/components/navigation_component"
|
|
11
|
+
require "motif/breadcrumbs/components/crumb_component"
|
|
12
|
+
require "motif/breadcrumbs/components/json_ld_component"
|
|
13
|
+
|
|
14
|
+
module Motif
|
|
15
|
+
module Breadcrumbs
|
|
16
|
+
# Your code goes here...
|
|
17
|
+
end
|
|
18
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: motif-breadcrumbs
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.4.1
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Marc Weistroff
|
|
8
|
+
bindir: bin
|
|
9
|
+
cert_chain: []
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
|
+
dependencies:
|
|
12
|
+
- !ruby/object:Gem::Dependency
|
|
13
|
+
name: rails
|
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
|
15
|
+
requirements:
|
|
16
|
+
- - ">="
|
|
17
|
+
- !ruby/object:Gem::Version
|
|
18
|
+
version: 8.0.3
|
|
19
|
+
type: :runtime
|
|
20
|
+
prerelease: false
|
|
21
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
22
|
+
requirements:
|
|
23
|
+
- - ">="
|
|
24
|
+
- !ruby/object:Gem::Version
|
|
25
|
+
version: 8.0.3
|
|
26
|
+
- !ruby/object:Gem::Dependency
|
|
27
|
+
name: view_component
|
|
28
|
+
requirement: !ruby/object:Gem::Requirement
|
|
29
|
+
requirements:
|
|
30
|
+
- - ">="
|
|
31
|
+
- !ruby/object:Gem::Version
|
|
32
|
+
version: '3.12'
|
|
33
|
+
type: :runtime
|
|
34
|
+
prerelease: false
|
|
35
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
36
|
+
requirements:
|
|
37
|
+
- - ">="
|
|
38
|
+
- !ruby/object:Gem::Version
|
|
39
|
+
version: '3.12'
|
|
40
|
+
- !ruby/object:Gem::Dependency
|
|
41
|
+
name: rspec-rails
|
|
42
|
+
requirement: !ruby/object:Gem::Requirement
|
|
43
|
+
requirements:
|
|
44
|
+
- - "~>"
|
|
45
|
+
- !ruby/object:Gem::Version
|
|
46
|
+
version: '7.0'
|
|
47
|
+
type: :development
|
|
48
|
+
prerelease: false
|
|
49
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
50
|
+
requirements:
|
|
51
|
+
- - "~>"
|
|
52
|
+
- !ruby/object:Gem::Version
|
|
53
|
+
version: '7.0'
|
|
54
|
+
- !ruby/object:Gem::Dependency
|
|
55
|
+
name: capybara
|
|
56
|
+
requirement: !ruby/object:Gem::Requirement
|
|
57
|
+
requirements:
|
|
58
|
+
- - ">="
|
|
59
|
+
- !ruby/object:Gem::Version
|
|
60
|
+
version: '0'
|
|
61
|
+
type: :development
|
|
62
|
+
prerelease: false
|
|
63
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
64
|
+
requirements:
|
|
65
|
+
- - ">="
|
|
66
|
+
- !ruby/object:Gem::Version
|
|
67
|
+
version: '0'
|
|
68
|
+
description: A Rails Engine providing breadcrumb trail management, ViewComponents
|
|
69
|
+
for rendering, and schema.org JSON-LD structured data.
|
|
70
|
+
email:
|
|
71
|
+
- marc@weistroff.net
|
|
72
|
+
executables: []
|
|
73
|
+
extensions: []
|
|
74
|
+
extra_rdoc_files: []
|
|
75
|
+
files:
|
|
76
|
+
- README.md
|
|
77
|
+
- Rakefile
|
|
78
|
+
- lib/motif/breadcrumbs.rb
|
|
79
|
+
- lib/motif/breadcrumbs/components/crumb_component.html.erb
|
|
80
|
+
- lib/motif/breadcrumbs/components/crumb_component.rb
|
|
81
|
+
- lib/motif/breadcrumbs/components/json_ld_component.html.erb
|
|
82
|
+
- lib/motif/breadcrumbs/components/json_ld_component.rb
|
|
83
|
+
- lib/motif/breadcrumbs/components/navigation_component.html.erb
|
|
84
|
+
- lib/motif/breadcrumbs/components/navigation_component.rb
|
|
85
|
+
- lib/motif/breadcrumbs/controller.rb
|
|
86
|
+
- lib/motif/breadcrumbs/crumb.rb
|
|
87
|
+
- lib/motif/breadcrumbs/railtie.rb
|
|
88
|
+
- lib/motif/breadcrumbs/segment.rb
|
|
89
|
+
- lib/motif/breadcrumbs/trail.rb
|
|
90
|
+
homepage: https://github.com/marcw/motif
|
|
91
|
+
licenses:
|
|
92
|
+
- MIT
|
|
93
|
+
metadata:
|
|
94
|
+
allowed_push_host: https://rubygems.org
|
|
95
|
+
homepage_uri: https://github.com/marcw/motif
|
|
96
|
+
source_code_uri: https://github.com/marcw/motif
|
|
97
|
+
changelog_uri: https://github.com/marcw/motif/blob/main/CHANGELOG.md
|
|
98
|
+
rdoc_options: []
|
|
99
|
+
require_paths:
|
|
100
|
+
- lib
|
|
101
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
102
|
+
requirements:
|
|
103
|
+
- - ">="
|
|
104
|
+
- !ruby/object:Gem::Version
|
|
105
|
+
version: '0'
|
|
106
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
107
|
+
requirements:
|
|
108
|
+
- - ">="
|
|
109
|
+
- !ruby/object:Gem::Version
|
|
110
|
+
version: '0'
|
|
111
|
+
requirements: []
|
|
112
|
+
rubygems_version: 3.6.9
|
|
113
|
+
specification_version: 4
|
|
114
|
+
summary: Breadcrumb trails for Rails applications
|
|
115
|
+
test_files: []
|