element_component 0.6.0 → 0.8.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.
Files changed (69) hide show
  1. checksums.yaml +4 -4
  2. data/AGENTS.md +5 -5
  3. data/README.md +348 -41
  4. data/examples/alert_example.rb +17 -17
  5. data/examples/badge_example.rb +63 -0
  6. data/examples/breadcrumb_example.rb +40 -0
  7. data/examples/button_example.rb +86 -0
  8. data/examples/button_group_example.rb +55 -0
  9. data/examples/card_example.rb +62 -0
  10. data/examples/carousel_example.rb +97 -0
  11. data/examples/close_button_example.rb +40 -0
  12. data/examples/dropdown_example.rb +131 -0
  13. data/examples/list_group_example.rb +68 -0
  14. data/examples/modal_example.rb +122 -0
  15. data/examples/nav_example.rb +77 -0
  16. data/examples/navbar_example.rb +104 -0
  17. data/examples/pagination_example.rb +47 -0
  18. data/examples/progress_example.rb +54 -0
  19. data/examples/spinner_example.rb +49 -0
  20. data/examples/table_example.rb +41 -0
  21. data/lib/element_component/components/alert.rb +3 -3
  22. data/lib/element_component/components/badge.rb +18 -0
  23. data/lib/element_component/components/breadcrumb/item.rb +29 -0
  24. data/lib/element_component/components/breadcrumb.rb +26 -0
  25. data/lib/element_component/components/button.rb +25 -0
  26. data/lib/element_component/components/button_group.rb +18 -0
  27. data/lib/element_component/components/card/body.rb +14 -0
  28. data/lib/element_component/components/card/footer.rb +14 -0
  29. data/lib/element_component/components/card/header.rb +14 -0
  30. data/lib/element_component/components/card/image.rb +17 -0
  31. data/lib/element_component/components/card/text.rb +14 -0
  32. data/lib/element_component/components/card/title.rb +14 -0
  33. data/lib/element_component/components/card.rb +21 -0
  34. data/lib/element_component/components/carousel/caption.rb +14 -0
  35. data/lib/element_component/components/carousel/item.rb +16 -0
  36. data/lib/element_component/components/carousel.rb +70 -0
  37. data/lib/element_component/components/close_button.rb +17 -0
  38. data/lib/element_component/components/dropdown/divider.rb +20 -0
  39. data/lib/element_component/components/dropdown/header.rb +22 -0
  40. data/lib/element_component/components/dropdown/item.rb +33 -0
  41. data/lib/element_component/components/dropdown/menu.rb +15 -0
  42. data/lib/element_component/components/dropdown.rb +43 -0
  43. data/lib/element_component/components/list_group/item.rb +23 -0
  44. data/lib/element_component/components/list_group.rb +18 -0
  45. data/lib/element_component/components/modal/body.rb +14 -0
  46. data/lib/element_component/components/modal/content.rb +14 -0
  47. data/lib/element_component/components/modal/dialog.rb +21 -0
  48. data/lib/element_component/components/modal/footer.rb +14 -0
  49. data/lib/element_component/components/modal/header.rb +16 -0
  50. data/lib/element_component/components/modal/title.rb +14 -0
  51. data/lib/element_component/components/modal.rb +42 -0
  52. data/lib/element_component/components/nav/item.rb +14 -0
  53. data/lib/element_component/components/nav/link.rb +18 -0
  54. data/lib/element_component/components/nav.rb +23 -0
  55. data/lib/element_component/components/navbar/brand.rb +15 -0
  56. data/lib/element_component/components/navbar/collapse.rb +16 -0
  57. data/lib/element_component/components/navbar/nav.rb +14 -0
  58. data/lib/element_component/components/navbar/toggler.rb +22 -0
  59. data/lib/element_component/components/navbar.rb +51 -0
  60. data/lib/element_component/components/pagination/item.rb +27 -0
  61. data/lib/element_component/components/pagination.rb +33 -0
  62. data/lib/element_component/components/progress/bar.rb +24 -0
  63. data/lib/element_component/components/progress.rb +17 -0
  64. data/lib/element_component/components/spinner.rb +19 -0
  65. data/lib/element_component/components/table.rb +21 -0
  66. data/lib/element_component/components.rb +16 -0
  67. data/lib/element_component/element.rb +5 -3
  68. data/lib/element_component/version.rb +1 -1
  69. metadata +61 -1
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: a8b23fff0ff2bf1424f46a3db32fb706973f1601f153dd68f9786018979eb1ef
4
- data.tar.gz: 631db1928c37d32d3a82190a0285e9fc85fb32693dc9fdba0fab9f118476fbcb
3
+ metadata.gz: 9cbfcfb824fb827c1bd03d08c5b2d0eec50fefa7b06c392b3e3393ce07c445be
4
+ data.tar.gz: ef7c00a35e38318c158fd43397552d37559d62c6996ca2f9a1a0dd05c2e32fc7
5
5
  SHA512:
6
- metadata.gz: a1cf9210199d4eecaf263df1f99f8862541291cf0fa22fb9c6f92cf55cb1a68f60f72b154503dbce5889a5ffe3c42fd520ce915c7d3e08259d98008692b2d627
7
- data.tar.gz: 248a8b0ad689bc29c4c8b9c05b42479c6c613789f87d8992ae34cfbc5d06710955117782eb0db683073e8239b2dfc339c65ffbeb4f46e60414427423e87d8c84
6
+ metadata.gz: f8297bcae5e86b8201fec505845b928dcf77bcfe4fc28d2d034c29b702446cf0e5566cc82468826b70173aa2392ea4c8b400b9eeeeeabad173923688ff4b5364
7
+ data.tar.gz: 934369f2c1d3a14b74437f6b71427ae1c92fa4ba3e27054f5755fe793e1442fa3c565783d96ce1cfa209a8cbbd35b5da338688910228240c291736ed9d24d46c
data/AGENTS.md CHANGED
@@ -44,21 +44,21 @@ Components live under `ElementComponent::Components`. Each component folder cont
44
44
 
45
45
  | Component | Class | Description |
46
46
  |---|---|---|
47
- | Alert | `Components::Alert` | `.alert` container with context |
47
+ | Alert | `Components::Alert` | `.alert` container with variant |
48
48
  | Alert Heading | `Components::AlertHeading` | `.alert-heading` (`<h4>`) |
49
49
  | Alert Link | `Components::AlertLink` | `.alert-link` (`<a>`) |
50
50
  | Alert Close | `Components::AlertCloseButton` | `.btn-close` (self-closing `<button>`) |
51
51
 
52
- **Alert constructor**: `Alert.new(context: :primary, dismissible: false, **attributes, &block)`
53
- - `context`: one of `:primary`, `:secondary`, `:success`, `:danger`, `:warning`, `:info`, `:light`, `:dark`
52
+ **Alert constructor**: `Alert.new(variant: :primary, dismissible: false, **attributes, &block)`
53
+ - `variant`: one of `:primary`, `:secondary`, `:success`, `:danger`, `:warning`, `:info`, `:light`, `:dark`
54
54
  - `dismissible`: adds `.alert-dismissible` class and appends a `CloseButton`
55
- - `&block`: instance_eval DSL for adding content inside the element
55
+ - `&block`: block with element parameter for adding content inside the element
56
56
 
57
57
  **Component guidelines**:
58
58
  - Always use `add_attribute()` instead of manipulating the `attributes` hash directly
59
59
  - Each class in its own file under a component-named folder
60
60
  - Call `super("tag_name", closing_tag: ..., &block)` first, then chain `add_attribute` calls
61
- - `instance_eval(&block)` lives in `Element#initialize` — do NOT repeat it in components
61
+ - `block.call(self)` lives in `Element#initialize` — passes element to block via block parameter
62
62
  - Pass user attributes last via `add_attribute(attributes)`
63
63
 
64
64
  ### Sub-component example pattern:
data/README.md CHANGED
@@ -1,6 +1,15 @@
1
1
  # ElementComponent
2
2
 
3
- A lightweight and flexible HTML builder for Ruby. `ElementComponent` provides a simple, object-oriented way to construct HTML structures programmatically, with dynamic attribute management, content nesting, rendering hooks, and pre-built components.
3
+ A lightweight and flexible HTML builder for Ruby. `ElementComponent` provides a simple, object-oriented way to construct HTML structures programmatically, with dynamic attribute management, content nesting, rendering hooks, and a comprehensive set of pre-built Bootstrap 5 components.
4
+
5
+ ## Key Features
6
+
7
+ - **Object-Oriented HTML Construction** — Build HTML trees using Ruby objects with dynamic attribute management
8
+ - **Block DSL** — Nest content inline with block parameters and the `new_element` helper
9
+ - **Rendering Hooks** — `before_render`, `after_render`, and `around_render` callbacks for dynamic content
10
+ - **17 Bootstrap 5 Components** — Ready-to-use Alert, Badge, Breadcrumb, Button, ButtonGroup, Card, Carousel, CloseButton, Dropdown, ListGroup, Modal, Nav, Navbar, Pagination, Progress, Spinner, and Table
11
+ - **Chained API** — All `add_*` methods return `self` for method chaining
12
+ - **Self-Closing Tags** — Support for void elements like `<img>`, `<input>`, `<br>`
4
13
 
5
14
  ## Installation
6
15
 
@@ -32,9 +41,9 @@ puts p.render
32
41
  Use a block to add content inline:
33
42
 
34
43
  ```ruby
35
- div = ElementComponent::Element.new("div", class: "container") do
36
- add_content("Welcome")
37
- add_content(ElementComponent::Element.new("h1") { add_content("Title") })
44
+ div = ElementComponent::Element.new("div", class: "container") do |e|
45
+ e.add_content("Welcome")
46
+ e.add_content(ElementComponent::Element.new("h1") { |h| h.add_content("Title") })
38
47
  end
39
48
  puts div.render
40
49
  # => <div class="container">Welcome<h1>Title</h1></div>
@@ -45,9 +54,9 @@ puts div.render
45
54
  Inside a block, use `new_element` as a shorthand:
46
55
 
47
56
  ```ruby
48
- div = ElementComponent::Element.new("div") do
49
- add_content(new_element("h1") { add_content("Hello") })
50
- add_content(new_element("p", class: "lead") { add_content("World") })
57
+ div = ElementComponent::Element.new("div") do |e|
58
+ e.add_content(e.new_element("h1") { |h| h.add_content("Hello") })
59
+ e.add_content(e.new_element("p", class: "lead") { |p| p.add_content("World") })
51
60
  end
52
61
  puts div.render
53
62
  # => <div><h1>Hello</h1><p class="lead">World</p></div>
@@ -64,10 +73,10 @@ div = ElementComponent::Element.new("div")
64
73
  div.add_content("plain text")
65
74
 
66
75
  # Element instance
67
- div.add_content(ElementComponent::Element.new("span") { add_content("nested") })
76
+ div.add_content(ElementComponent::Element.new("span") { |s| s.add_content("nested") })
68
77
 
69
78
  # Block (evaluated at render time, has access to new_element)
70
- div.add_content { new_element("em") { add_content("deferred") } }
79
+ div.add_content { |e| e.new_element("em") { |em| em.add_content("deferred") } }
71
80
 
72
81
  puts div.render
73
82
  # => <div>plain text<span>nested</span><em>deferred</em></div>
@@ -111,59 +120,356 @@ puts div.render
111
120
  # => <div class="dynamic">content</div>
112
121
  ```
113
122
 
114
- ## Pre-built Components
123
+ ## Pre-built Components (Bootstrap 5)
124
+
125
+ All components live under `ElementComponent::Components` and support the block DSL, chained `add_content`, and custom HTML attributes via `**attributes`.
126
+
127
+ ### Quick Reference
128
+
129
+ | Component | Class | Tag | Key Options |
130
+ |---|---|---|---|
131
+ | Alert | `Alert` | `<div>` | `variant`, `dismissible` |
132
+ | Badge | `Badge` | `<span>` | `variant`, `pill` |
133
+ | Breadcrumb | `Breadcrumb` | `<nav>` → `<ol>` | `BreadcrumbItem` (`href`, `active`) |
134
+ | Button | `Button` | `<button>` / `<a>` | `variant`, `outline`, `size`, `href` |
135
+ | ButtonGroup | `ButtonGroup` | `<div>` | `size`, `vertical` |
136
+ | Card | `Card` | `<div>` | Sub-components: Header, Body, Footer, Title, Text, Image |
137
+ | Carousel | `Carousel` | `<div>` | `fade`, `indicators`, `controls`; `CarouselItem`, `CarouselCaption` |
138
+ | CloseButton | `CloseButton` | `<button>` (self-closing) | `disabled` |
139
+ | Dropdown | `Dropdown` | `<div>` | `direction`; `DropdownMenu`, `DropdownItem`, `DropdownDivider`, `DropdownHeader` |
140
+ | ListGroup | `ListGroup` | `<ul>` | `flush`, `numbered`; `ListGroupItem` (`variant`, `active`, `disabled`, `href`) |
141
+ | Modal | `Modal` | `<div>` | `fade`, `static`, `scrollable`, `centered`, `size`; `ModalContent`, `ModalHeader`, `ModalBody`, `ModalFooter` |
142
+ | Nav | `Nav` | `<ul>` | `type` (tabs/pills/underline), `fill`, `justified`, `vertical` |
143
+ | Navbar | `Navbar` | `<nav>` | `expand`, `theme`, `background`, `fixed`, `sticky`; `NavbarBrand`, `NavbarNav`, `NavbarToggler` |
144
+ | Pagination | `Pagination` | `<nav>` → `<ul>` | `size`; `PageItem` (`active`, `disabled`) |
145
+ | Progress | `Progress` | `<div>` | `ProgressBar` (`value`, `variant`, `striped`, `animated`) |
146
+ | Spinner | `Spinner` | `<div>` | `type` (border/grow), `variant` |
147
+ | Table | `Table` | `<table>` | `striped`, `bordered`, `hover`, `small`, `variant` |
115
148
 
116
- Components live under `ElementComponent::Components`.
149
+ ### Alert
117
150
 
118
- ### Alert (Bootstrap 5)
119
151
 
120
152
  ```ruby
121
- alert = ElementComponent::Components::Alert.new(context: :success) do
122
- add_content("Operation completed!")
153
+ alert = ElementComponent::Components::Alert.new(variant: :success) do |e|
154
+ e.add_content("Operation completed!")
123
155
  end
124
- puts alert.render
125
156
  # => <div class="alert alert-success" role="alert">Operation completed!</div>
126
157
  ```
127
158
 
128
- **Contexts**: `:primary`, `:secondary`, `:success`, `:danger`, `:warning`, `:info`, `:light`, `:dark`
159
+ **Variants**: `:primary`, `:secondary`, `:success`, `:danger`, `:warning`, `:info`, `:light`, `:dark`
129
160
 
130
- #### Dismissible Alert
161
+ Dismissible alerts, headings, and links use sub-components:
131
162
 
132
163
  ```ruby
133
- alert = ElementComponent::Components::Alert.new(context: :warning, dismissible: true) do
134
- add_content("This can be closed.")
164
+ alert = ElementComponent::Components::Alert.new(variant: :warning, dismissible: true) do |e|
165
+ e.add_content(ElementComponent::Components::AlertHeading.new { |h| h.add_content("Warning") })
166
+ e.add_content("Please review. ")
167
+ e.add_content(ElementComponent::Components::AlertLink.new(href: "/details") { |l| l.add_content("Details") })
135
168
  end
136
- puts alert.render
137
169
  # => <div class="alert alert-warning alert-dismissible" role="alert">
138
- # This can be closed.
170
+ # <h4 class="alert-heading">Warning</h4>
171
+ # Please review.
172
+ # <a class="alert-link" href="/details">Details</a>
139
173
  # <button class="btn-close" data-bs-dismiss="alert" aria-label="Close">
140
174
  # </div>
141
175
  ```
142
176
 
143
- #### Alert with Heading and Link
177
+ **Sub-components**:
178
+
179
+ | Class | Tag | CSS Class |
180
+ |---|---|---|
181
+ | `AlertHeading` | `<h4>` | `.alert-heading` |
182
+ | `AlertLink` | `<a>` | `.alert-link` |
183
+ | `AlertCloseButton` | `<button>` (self-closing) | `.btn-close` |
184
+
185
+ ### Button
186
+
144
187
 
145
188
  ```ruby
146
- alert = ElementComponent::Components::Alert.new(context: :info) do
147
- add_content(ElementComponent::Components::AlertHeading.new.tap { |h| h.add_content("Notice") })
148
- add_content("Please review. ")
149
- add_content(ElementComponent::Components::AlertLink.new(href: "/details").tap { |l| l.add_content("Details") })
189
+ # Standard button
190
+ btn = ElementComponent::Components::Button.new(variant: :primary) { |b| b.add_content("Click") }
191
+ # => <button class="btn btn-primary" type="button">Click</button>
192
+
193
+ # Outline variant
194
+ btn = ElementComponent::Components::Button.new(variant: :danger, outline: true) { |b| b.add_content("Delete") }
195
+ # => <button class="btn btn-outline-danger" type="button">Delete</button>
196
+
197
+ # As a link
198
+ btn = ElementComponent::Components::Button.new(variant: :primary, href: "/home") { |b| b.add_content("Home") }
199
+ # => <a class="btn btn-primary" href="/home">Home</a>
200
+ ```
201
+
202
+ **Options**: `variant` (primary/secondary/success/danger/warning/info/light/dark/link), `outline`, `size` (sm/lg), `href`
203
+
204
+ ### Badge
205
+
206
+
207
+ ```ruby
208
+ badge = ElementComponent::Components::Badge.new(variant: :primary) { |b| b.add_content("New") }
209
+ # => <span class="badge bg-primary">New</span>
210
+
211
+ pill = ElementComponent::Components::Badge.new(variant: :danger, pill: true) { |b| b.add_content("99+") }
212
+ # => <span class="badge bg-danger rounded-pill">99+</span>
213
+ ```
214
+
215
+ ### Card
216
+
217
+
218
+ ```ruby
219
+ card = ElementComponent::Components::Card.new do |c|
220
+ c.add_content(ElementComponent::Components::CardImage.new(src: "photo.jpg", top: true))
221
+ c.add_content(ElementComponent::Components::CardBody.new do |body|
222
+ body.add_content(ElementComponent::Components::CardTitle.new { |t| t.add_content("Title") })
223
+ body.add_content(ElementComponent::Components::CardText.new { |t| t.add_content("Some text.") })
224
+ end)
225
+ c.add_content(ElementComponent::Components::CardFooter.new { |f| f.add_content("Footer") })
150
226
  end
151
- puts alert.render
152
- # => <div class="alert alert-info" role="alert">
153
- # <h4 class="alert-heading">Notice</h4>
154
- # Please review.
155
- # <a class="alert-link" href="/details">Details</a>
156
- # </div>
157
227
  ```
158
228
 
159
- #### Sub-components
229
+ **Sub-components**:
160
230
 
161
- | Class | Tag | Class |
231
+ | Class | Tag | CSS Class |
162
232
  |---|---|---|
163
- | `Alert` | `<div>` | `.alert .alert-{context}` |
164
- | `AlertHeading` | `<h4>` | `.alert-heading` |
165
- | `AlertLink` | `<a>` | `.alert-link` |
166
- | `AlertCloseButton` | `<button>` (self-closing) | `.btn-close` |
233
+ | `CardHeader` | `<div>` | `.card-header` |
234
+ | `CardBody` | `<div>` | `.card-body` |
235
+ | `CardFooter` | `<div>` | `.card-footer` |
236
+ | `CardTitle` | `<h5>` | `.card-title` |
237
+ | `CardText` | `<p>` | `.card-text` |
238
+ | `CardImage` | `<img>` (self-closing) | `.card-img[-top\|-bottom]` |
239
+
240
+ ### Nav
241
+
242
+
243
+ ```ruby
244
+ nav = ElementComponent::Components::Nav.new(type: :tabs) do |n|
245
+ n.add_content(ElementComponent::Components::NavItem.new do |item|
246
+ item.add_content(ElementComponent::Components::NavLink.new(href: "/", active: true) { |l| l.add_content("Home") })
247
+ end)
248
+ n.add_content(ElementComponent::Components::NavItem.new do |item|
249
+ item.add_content(ElementComponent::Components::NavLink.new(href: "/profile") { |l| l.add_content("Profile") })
250
+ end)
251
+ end
252
+ # => <ul class="nav nav-tabs">...</ul>
253
+ ```
254
+
255
+ **Options**: `type` (tabs/pills/underline), `fill`, `justified`, `vertical`
256
+
257
+ ### Breadcrumb
258
+
259
+
260
+ ```ruby
261
+ crumb = ElementComponent::Components::Breadcrumb.new do |b|
262
+ b.add_content(ElementComponent::Components::BreadcrumbItem.new(href: "/") { |i| i.add_content("Home") })
263
+ b.add_content(ElementComponent::Components::BreadcrumbItem.new(href: "/section") { |i| i.add_content("Section") })
264
+ b.add_content(ElementComponent::Components::BreadcrumbItem.new(active: true) { |i| i.add_content("Current") })
265
+ end
266
+ # => <nav aria-label="breadcrumb"><ol class="breadcrumb">...</ol></nav>
267
+ ```
268
+
269
+ ### ListGroup
270
+
271
+
272
+ ```ruby
273
+ list = ElementComponent::Components::ListGroup.new(flush: true) do |l|
274
+ l.add_content(ElementComponent::Components::ListGroupItem.new { |i| i.add_content("Item 1") })
275
+ l.add_content(ElementComponent::Components::ListGroupItem.new(active: true) { |i| i.add_content("Item 2") })
276
+ l.add_content(ElementComponent::Components::ListGroupItem.new(href: "/link") { |i| i.add_content("Link") })
277
+ end
278
+ # => <ul class="list-group list-group-flush">...</ul>
279
+ ```
280
+
281
+ **Options**: `flush`, `numbered`; `ListGroupItem` options: `variant`, `active`, `disabled`, `href`
282
+
283
+ ### Progress
284
+
285
+
286
+ ```ruby
287
+ progress = ElementComponent::Components::Progress.new do |p|
288
+ p.add_content(ElementComponent::Components::ProgressBar.new(value: 75, variant: :success, striped: true) do |bar|
289
+ bar.add_content("75%")
290
+ end)
291
+ end
292
+ # => <div class="progress" role="progressbar"><div class="progress-bar bg-success progress-bar-striped" ...>75%</div></div>
293
+ ```
294
+
295
+ ### Spinner
296
+
297
+
298
+ ```ruby
299
+ border = ElementComponent::Components::Spinner.new(type: :border, variant: :primary)
300
+ # => <div class="spinner-border text-primary" role="status"></div>
301
+
302
+ grow = ElementComponent::Components::Spinner.new(type: :grow, variant: :success)
303
+ # => <div class="spinner-grow text-success" role="status"></div>
304
+ ```
305
+
306
+ ### Table
307
+
308
+
309
+ ```ruby
310
+ table = ElementComponent::Components::Table.new(striped: true, bordered: true, hover: true) do |t|
311
+ t.add_content("<thead><tr><th>Name</th><th>Age</th></tr></thead>")
312
+ t.add_content("<tbody><tr><td>John</td><td>30</td></tr></tbody>")
313
+ end
314
+ # => <table class="table table-striped table-bordered table-hover">...</table>
315
+ ```
316
+
317
+ **Options**: `striped`, `bordered`, `hover`, `small`, `variant`
318
+
319
+ ### Pagination
320
+
321
+
322
+ ```ruby
323
+ nav = ElementComponent::Components::Pagination.new(size: :lg) do |p|
324
+ p.add_content(ElementComponent::Components::PageItem.new(active: true) { |i| i.add_content("1") })
325
+ p.add_content(ElementComponent::Components::PageItem.new { |i| i.add_content("2") })
326
+ end
327
+ # => <nav aria-label="Pagination"><ul class="pagination pagination-lg">...</ul></nav>
328
+ ```
329
+
330
+ ### ButtonGroup
331
+
332
+
333
+ ```ruby
334
+ group = ElementComponent::Components::ButtonGroup.new do |g|
335
+ g.add_content(ElementComponent::Components::Button.new(variant: :primary) { |b| b.add_content("Left") })
336
+ g.add_content(ElementComponent::Components::Button.new(variant: :primary) { |b| b.add_content("Middle") })
337
+ g.add_content(ElementComponent::Components::Button.new(variant: :primary) { |b| b.add_content("Right") })
338
+ end
339
+ # => <div class="btn-group" role="group">...</div>
340
+ ```
341
+
342
+ **Options**: `size` (sm/lg), `vertical`
343
+
344
+ ### CloseButton
345
+
346
+
347
+ ```ruby
348
+ btn = ElementComponent::Components::CloseButton.new
349
+ # => <button class="btn-close" type="button" aria-label="Close">
350
+
351
+ disabled = ElementComponent::Components::CloseButton.new(disabled: true)
352
+ # => <button class="btn-close" type="button" aria-label="Close" disabled>
353
+ ```
354
+
355
+ ### Modal
356
+
357
+
358
+ ```ruby
359
+ modal = ElementComponent::Components::Modal.new(id: "exampleModal") do |m|
360
+ m.add_content(ElementComponent::Components::ModalContent.new do |content|
361
+ content.add_content(ElementComponent::Components::ModalHeader.new do |header|
362
+ header.add_content(ElementComponent::Components::ModalTitle.new { |t| t.add_content("Modal title") })
363
+ end)
364
+ content.add_content(ElementComponent::Components::ModalBody.new { |body| body.add_content("Modal body text.") })
365
+ content.add_content(ElementComponent::Components::ModalFooter.new do |footer|
366
+ footer.add_content(ElementComponent::Components::Button.new(variant: :secondary) { |b| b.add_content("Close") })
367
+ footer.add_content(ElementComponent::Components::Button.new(variant: :primary) { |b| b.add_content("Save") })
368
+ end)
369
+ end)
370
+ end
371
+ ```
372
+
373
+ **Options**: `fade`, `static`, `scrollable`, `centered`, `size` (sm/lg/xl), `fullscreen`
374
+
375
+ **Sub-components**:
376
+
377
+ | Class | Tag | CSS Class |
378
+ |---|---|---|
379
+ | `ModalDialog` | `<div>` | `.modal-dialog` |
380
+ | `ModalContent` | `<div>` | `.modal-content` |
381
+ | `ModalHeader` | `<div>` | `.modal-header` |
382
+ | `ModalTitle` | `<h5>` | `.modal-title` |
383
+ | `ModalBody` | `<div>` | `.modal-body` |
384
+ | `ModalFooter` | `<div>` | `.modal-footer` |
385
+
386
+ ### Carousel
387
+
388
+
389
+ ```ruby
390
+ carousel = ElementComponent::Components::Carousel.new(id: "slides") do |c|
391
+ c.add_content(ElementComponent::Components::CarouselItem.new(active: true) do |item|
392
+ item.add_content(%(<img src="slide1.jpg" class="d-block w-100" alt="...">))
393
+ end)
394
+ c.add_content(ElementComponent::Components::CarouselItem.new do |item|
395
+ item.add_content(%(<img src="slide2.jpg" class="d-block w-100" alt="...">))
396
+ end)
397
+ end
398
+ ```
399
+
400
+ **Options**: `fade` (crossfade), `indicators`, `controls`; indicators and navigation controls are auto-generated
401
+
402
+ **Sub-components**:
403
+
404
+ | Class | Tag | CSS Class |
405
+ |---|---|---|
406
+ | `CarouselItem` | `<div>` | `.carousel-item` |
407
+ | `CarouselCaption` | `<div>` | `.carousel-caption` |
408
+
409
+ ### Dropdown
410
+
411
+
412
+ ```ruby
413
+ dropdown = ElementComponent::Components::Dropdown.new do |d|
414
+ d.add_content(
415
+ ElementComponent::Element.new("button",
416
+ class: "btn btn-secondary dropdown-toggle",
417
+ type: "button",
418
+ "data-bs-toggle": "dropdown",
419
+ "aria-expanded": "false") { |b| b.add_content("Dropdown") }
420
+ )
421
+ d.add_content(
422
+ ElementComponent::Components::DropdownMenu.new do |menu|
423
+ menu.add_content(ElementComponent::Components::DropdownItem.new { |i| i.add_content("Action") })
424
+ menu.add_content(ElementComponent::Components::DropdownItem.new(active: true) { |i| i.add_content("Active") })
425
+ menu.add_content(ElementComponent::Components::DropdownDivider.new)
426
+ menu.add_content(ElementComponent::Components::DropdownItem.new(disabled: true) { |i| i.add_content("Disabled") })
427
+ end
428
+ )
429
+ end
430
+ ```
431
+
432
+ **Options**: `direction` (dropup/dropend/dropstart)
433
+
434
+ **Sub-components**:
435
+
436
+ | Class | Tag | CSS Class |
437
+ |---|---|---|
438
+ | `DropdownMenu` | `<ul>` | `.dropdown-menu` |
439
+ | `DropdownItem` | `<li>` → `<a>`/`<button>` | `.dropdown-item` |
440
+ | `DropdownDivider` | `<li>` → `<hr>` | `.dropdown-divider` |
441
+ | `DropdownHeader` | `<li>` → `<h6>` | `.dropdown-header` |
442
+
443
+ ### Navbar
444
+
445
+
446
+ ```ruby
447
+ navbar = ElementComponent::Components::Navbar.new(theme: :dark, background: :dark) do |n|
448
+ n.add_content(ElementComponent::Components::NavbarBrand.new(href: "/") { |b| b.add_content("Brand") })
449
+ n.add_content(ElementComponent::Components::NavbarToggler.new(target: "nav"))
450
+ n.add_content(ElementComponent::Components::NavbarCollapse.new(id: "nav") do |collapse|
451
+ collapse.add_content(ElementComponent::Components::NavbarNav.new do |nav|
452
+ nav.add_content(ElementComponent::Components::NavItem.new do |item|
453
+ item.add_content(ElementComponent::Components::NavLink.new(href: "/", active: true) { |l| l.add_content("Home") })
454
+ end)
455
+ nav.add_content(ElementComponent::Components::NavItem.new do |item|
456
+ item.add_content(ElementComponent::Components::NavLink.new(href: "/about") { |l| l.add_content("About") })
457
+ end)
458
+ end)
459
+ end)
460
+ end
461
+ ```
462
+
463
+ **Options**: `expand` (sm/md/lg/xl/xxl), `theme` (light/dark), `background`, `fixed` (top/bottom), `sticky` (top/bottom), `container`
464
+
465
+ **Sub-components**:
466
+
467
+ | Class | Tag | CSS Class |
468
+ |---|---|---|
469
+ | `NavbarBrand` | `<a>` | `.navbar-brand` |
470
+ | `NavbarToggler` | `<button>` (self-closing) | `.navbar-toggler` |
471
+ | `NavbarCollapse` | `<div>` | `.collapse .navbar-collapse` |
472
+ | `NavbarNav` | `<ul>` | `.navbar-nav` |
167
473
 
168
474
  ## Development
169
475
 
@@ -174,6 +480,9 @@ bundle exec rubocop # Lint
174
480
  bundle exec rake # Spec + RuboCop
175
481
  bin/console # Interactive console
176
482
  ruby examples/alert_example.rb # Run Alert examples
483
+ ruby examples/button_example.rb # Run Button examples
484
+ ruby examples/card_example.rb # Run Card examples
485
+ # See all examples in the examples/ directory
177
486
  ```
178
487
 
179
488
  ### Test Coverage
@@ -196,9 +505,7 @@ Or push a version tag (e.g., `v0.6.0`) to trigger the automated release workflow
196
505
  ## Roadmap
197
506
 
198
507
  - [ ] Support for Caching
199
- - [ ] Pre-built Bulma components
200
- - [ ] Pre-built Bootstrap components (more)
201
- - [ ] Enhanced DSL for nested structures
508
+ - [x] Pre-built Bootstrap components (Alert, Badge, Breadcrumb, Button, ButtonGroup, Card, Carousel, CloseButton, Dropdown, ListGroup, Modal, Nav, Navbar, Pagination, Progress, Spinner, Table)
202
509
 
203
510
  ## Contributing
204
511
 
@@ -5,19 +5,19 @@ require_relative "../lib/element_component"
5
5
  # =============================================================================
6
6
  # Basic Alert
7
7
  # =============================================================================
8
- alert = ElementComponent::Components::Alert.new(context: :success)
8
+ alert = ElementComponent::Components::Alert.new(variant: :success)
9
9
  alert.add_content("Operation completed successfully!")
10
10
  puts "=== Basic Alert ==="
11
11
  puts alert.render
12
12
  puts
13
13
 
14
14
  # =============================================================================
15
- # All Contexts
15
+ # All Variants
16
16
  # =============================================================================
17
17
  puts "=== All Alert Contexts ==="
18
- ElementComponent::Components::Alert::VALID_CONTEXTS.each do |context|
19
- alert = ElementComponent::Components::Alert.new(context: context)
20
- alert.add_content("#{context.capitalize} alert message here.")
18
+ ElementComponent::Components::Alert::VALID_VARIANTS.each do |variant|
19
+ alert = ElementComponent::Components::Alert.new(variant: variant)
20
+ alert.add_content("#{variant.capitalize} alert message here.")
21
21
  puts alert.render
22
22
  end
23
23
  puts
@@ -25,8 +25,8 @@ puts
25
25
  # =============================================================================
26
26
  # Dismissible Alert (use block DSL so close button is always last)
27
27
  # =============================================================================
28
- alert = ElementComponent::Components::Alert.new(context: :warning, dismissible: true) do
29
- add_content("This alert can be dismissed.")
28
+ alert = ElementComponent::Components::Alert.new(variant: :warning, dismissible: true) do |e|
29
+ e.add_content("This alert can be dismissed.")
30
30
  end
31
31
  puts "=== Dismissible Alert ==="
32
32
  puts alert.render
@@ -36,7 +36,7 @@ puts
36
36
  # Alert with Custom Attributes
37
37
  # =============================================================================
38
38
  alert = ElementComponent::Components::Alert.new(
39
- context: :primary,
39
+ variant: :primary,
40
40
  id: "main-alert",
41
41
  style: "margin-top: 20px;"
42
42
  )
@@ -48,10 +48,10 @@ puts
48
48
  # =============================================================================
49
49
  # Alert with Heading and Link (block DSL)
50
50
  # =============================================================================
51
- alert = ElementComponent::Components::Alert.new(context: :info) do
52
- add_content(ElementComponent::Components::AlertHeading.new.tap { |h| h.add_content("Information") })
53
- add_content("This is an important notice. ")
54
- add_content(ElementComponent::Components::AlertLink.new(href: "/details").tap { |l| l.add_content("View details") })
51
+ alert = ElementComponent::Components::Alert.new(variant: :info) do |e|
52
+ e.add_content(ElementComponent::Components::AlertHeading.new.tap { |h| h.add_content("Information") })
53
+ e.add_content("This is an important notice. ")
54
+ e.add_content(ElementComponent::Components::AlertLink.new(href: "/details").tap { |l| l.add_content("View details") })
55
55
  end
56
56
  puts "=== Alert with Heading and Link ==="
57
57
  puts alert.render
@@ -60,10 +60,10 @@ puts
60
60
  # =============================================================================
61
61
  # Dismissible Alert with Heading and Link
62
62
  # =============================================================================
63
- alert = ElementComponent::Components::Alert.new(context: :danger, dismissible: true) do
64
- add_content(ElementComponent::Components::AlertHeading.new.tap { |h| h.add_content("Error!") })
65
- add_content("Something went wrong. ")
66
- add_content(ElementComponent::Components::AlertLink.new(href: "/support").tap do |l|
63
+ alert = ElementComponent::Components::Alert.new(variant: :danger, dismissible: true) do |e|
64
+ e.add_content(ElementComponent::Components::AlertHeading.new.tap { |h| h.add_content("Error!") })
65
+ e.add_content("Something went wrong. ")
66
+ e.add_content(ElementComponent::Components::AlertLink.new(href: "/support").tap do |l|
67
67
  l.add_content("Contact support")
68
68
  end)
69
69
  end
@@ -74,7 +74,7 @@ puts
74
74
  # =============================================================================
75
75
  # Chained API Usage
76
76
  # =============================================================================
77
- alert = ElementComponent::Components::Alert.new(context: :secondary)
77
+ alert = ElementComponent::Components::Alert.new(variant: :secondary)
78
78
  .add_content("This alert was built using chained calls.")
79
79
  puts "=== Chained API ==="
80
80
  puts alert.render
@@ -0,0 +1,63 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../lib/element_component"
4
+
5
+ # =============================================================================
6
+ # Basic Badge
7
+ # =============================================================================
8
+ badge = ElementComponent::Components::Badge.new(variant: :primary)
9
+ badge.add_content("New")
10
+ puts "=== Basic Badge ==="
11
+ puts badge.render
12
+ puts
13
+
14
+ # =============================================================================
15
+ # Pill Badge
16
+ # =============================================================================
17
+ badge = ElementComponent::Components::Badge.new(variant: :danger, pill: true)
18
+ badge.add_content("99+")
19
+ puts "=== Pill Badge ==="
20
+ puts badge.render
21
+ puts
22
+
23
+ # =============================================================================
24
+ # All Contexts
25
+ # =============================================================================
26
+ puts "=== All Badge Variants ==="
27
+ ElementComponent::Components::Badge::VALID_VARIANTS.each do |variant|
28
+ badge = ElementComponent::Components::Badge.new(variant: variant)
29
+ badge.add_content(variant.to_s.capitalize)
30
+ puts badge.render
31
+ end
32
+ puts
33
+
34
+ # =============================================================================
35
+ # Badge with Block
36
+ # =============================================================================
37
+ badge = ElementComponent::Components::Badge.new(variant: :success) do |b|
38
+ b.add_content("Completed")
39
+ end
40
+ puts "=== Badge with Block ==="
41
+ puts badge.render
42
+ puts
43
+
44
+ # =============================================================================
45
+ # Chained API
46
+ # =============================================================================
47
+ badge = ElementComponent::Components::Badge.new(variant: :warning)
48
+ .add_content("Warning")
49
+ puts "=== Chained API ==="
50
+ puts badge.render
51
+ puts
52
+
53
+ # =============================================================================
54
+ # Badge with Custom Attributes
55
+ # =============================================================================
56
+ badge = ElementComponent::Components::Badge.new(
57
+ variant: :info,
58
+ id: "badge-1",
59
+ style: "font-size: 1.2em;"
60
+ )
61
+ badge.add_content("Custom")
62
+ puts "=== Badge with Custom Attributes ==="
63
+ puts badge.render