element_component 0.5.0 → 0.7.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 (73) hide show
  1. checksums.yaml +4 -4
  2. data/AGENTS.md +126 -0
  3. data/README.md +440 -25
  4. data/examples/alert_example.rb +97 -0
  5. data/examples/badge_example.rb +63 -0
  6. data/examples/breadcrumb_example.rb +36 -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 +95 -0
  11. data/examples/close_button_example.rb +40 -0
  12. data/examples/dropdown_example.rb +123 -0
  13. data/examples/list_group_example.rb +60 -0
  14. data/examples/modal_example.rb +120 -0
  15. data/examples/nav_example.rb +75 -0
  16. data/examples/navbar_example.rb +96 -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/close_button.rb +17 -0
  22. data/lib/element_component/components/alert/heading.rb +14 -0
  23. data/lib/element_component/components/alert/link.rb +15 -0
  24. data/lib/element_component/components/alert.rb +25 -0
  25. data/lib/element_component/components/badge.rb +18 -0
  26. data/lib/element_component/components/breadcrumb/item.rb +29 -0
  27. data/lib/element_component/components/breadcrumb.rb +26 -0
  28. data/lib/element_component/components/button.rb +25 -0
  29. data/lib/element_component/components/button_group.rb +18 -0
  30. data/lib/element_component/components/card/body.rb +14 -0
  31. data/lib/element_component/components/card/footer.rb +14 -0
  32. data/lib/element_component/components/card/header.rb +14 -0
  33. data/lib/element_component/components/card/image.rb +17 -0
  34. data/lib/element_component/components/card/text.rb +14 -0
  35. data/lib/element_component/components/card/title.rb +14 -0
  36. data/lib/element_component/components/card.rb +21 -0
  37. data/lib/element_component/components/carousel/caption.rb +14 -0
  38. data/lib/element_component/components/carousel/item.rb +16 -0
  39. data/lib/element_component/components/carousel.rb +70 -0
  40. data/lib/element_component/components/close_button.rb +17 -0
  41. data/lib/element_component/components/dropdown/divider.rb +20 -0
  42. data/lib/element_component/components/dropdown/header.rb +22 -0
  43. data/lib/element_component/components/dropdown/item.rb +33 -0
  44. data/lib/element_component/components/dropdown/menu.rb +15 -0
  45. data/lib/element_component/components/dropdown.rb +43 -0
  46. data/lib/element_component/components/list_group/item.rb +23 -0
  47. data/lib/element_component/components/list_group.rb +18 -0
  48. data/lib/element_component/components/modal/body.rb +14 -0
  49. data/lib/element_component/components/modal/content.rb +14 -0
  50. data/lib/element_component/components/modal/dialog.rb +21 -0
  51. data/lib/element_component/components/modal/footer.rb +14 -0
  52. data/lib/element_component/components/modal/header.rb +16 -0
  53. data/lib/element_component/components/modal/title.rb +14 -0
  54. data/lib/element_component/components/modal.rb +42 -0
  55. data/lib/element_component/components/nav/item.rb +14 -0
  56. data/lib/element_component/components/nav/link.rb +18 -0
  57. data/lib/element_component/components/nav.rb +23 -0
  58. data/lib/element_component/components/navbar/brand.rb +15 -0
  59. data/lib/element_component/components/navbar/collapse.rb +16 -0
  60. data/lib/element_component/components/navbar/nav.rb +14 -0
  61. data/lib/element_component/components/navbar/toggler.rb +22 -0
  62. data/lib/element_component/components/navbar.rb +51 -0
  63. data/lib/element_component/components/pagination/item.rb +27 -0
  64. data/lib/element_component/components/pagination.rb +33 -0
  65. data/lib/element_component/components/progress/bar.rb +24 -0
  66. data/lib/element_component/components/progress.rb +17 -0
  67. data/lib/element_component/components/spinner.rb +19 -0
  68. data/lib/element_component/components/table.rb +21 -0
  69. data/lib/element_component/components.rb +24 -0
  70. data/lib/element_component/element.rb +21 -7
  71. data/lib/element_component/version.rb +3 -1
  72. data/lib/element_component.rb +3 -1
  73. metadata +68 -1
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 9ebf642383684b4ac3419d63b6538ab4ddccc14c6275ad9913e2aa4d6fd14938
4
- data.tar.gz: 1bfd4f8d575c2f92804c7c62868e3c303ca8a01f6238addee9d4bf93cd3b6d68
3
+ metadata.gz: 27a79b6578d299e674340e4c1e3c7a0061ff6ac3b9c233f102bb3d4d3fd89011
4
+ data.tar.gz: 501d6617270b0e5bc9cef324d270126d5cd531677cdd8ee43bb3a8e18bf1cd4c
5
5
  SHA512:
6
- metadata.gz: 99e132b0e8a5e168dfb9bae745cb46c235a3249ef42e61263bbbd4ae384472523ac5d110a3dc3afe12fec1a0436defd795afdd227f36efa11f515d4eb64a52f1
7
- data.tar.gz: 5d721e3aa5aefc886c74eea0a04c94e9d408417b6e5660ef6976e5f8ea603e13f1c2c4ef9b559e256532e195e289f14ebe9cf60820cd33efbc492da731133296
6
+ metadata.gz: d7317e82f7fc54008e4f4152372fc1f3575edf3f0956790a87298025707bbcd507422c3a3e32800dd6aa6a3954f1006ac470fde4e44ffb35ba7aba1bfe9ed5e8
7
+ data.tar.gz: c358120b8d90a4ef0ea864094cae08d245286a20266c4c645814f7f60ec5795f5a62756a740e91be566b08159914674b91b0ba9af76013de02c0f3e20927d731
data/AGENTS.md ADDED
@@ -0,0 +1,126 @@
1
+ # ElementComponent - Agent Instructions
2
+
3
+ ## Project Overview
4
+ Ruby gem for building HTML programmatically with an object-oriented API. Provides dynamic attribute management, content nesting, and rendering hooks.
5
+
6
+ ## Architecture
7
+
8
+ ```
9
+ lib/
10
+ element_component.rb # Entry point, requires modules
11
+ element_component/
12
+ version.rb # VERSION constant
13
+ element.rb # Core Element class
14
+ components.rb # Component index, requires all components
15
+ components/
16
+ alert.rb # Alert component
17
+ alert/
18
+ heading.rb # AlertHeading component
19
+ link.rb # AlertLink component
20
+ close_button.rb # AlertCloseButton component
21
+ spec/
22
+ element_component_spec.rb # Version check
23
+ lib/
24
+ element_spec.rb # Element unit tests
25
+ components/
26
+ alert_spec.rb # Alert component tests
27
+ spec_helper.rb # RSpec config
28
+ examples/
29
+ alert_example.rb # Complete Alert usage examples
30
+ ```
31
+
32
+ ## Core Classes
33
+
34
+ ### `ElementComponent::Element`
35
+ - **Attributes**: `element` (tag name), `attributes` (Hash), `contents` (Array), `html` (rendered output)
36
+ - **Constructor**: `Element.new(tag_name, closing_tag: true, **attributes)`
37
+ - **Content methods**: `add_content`, `add_content!`, `add_content(&block)`
38
+ - **Attribute methods**: `add_attribute`, `add_attribute!`, `remove_attribute`, `remove_attribute_value`
39
+ - **Render**: `render` (with hooks: `before_render`, `after_render`, `around_render`)
40
+
41
+ ### Pre-built Components
42
+
43
+ Components live under `ElementComponent::Components`. Each component folder contains the main class and its sub-components in separate files.
44
+
45
+ | Component | Class | Description |
46
+ |---|---|---|
47
+ | Alert | `Components::Alert` | `.alert` container with variant |
48
+ | Alert Heading | `Components::AlertHeading` | `.alert-heading` (`<h4>`) |
49
+ | Alert Link | `Components::AlertLink` | `.alert-link` (`<a>`) |
50
+ | Alert Close | `Components::AlertCloseButton` | `.btn-close` (self-closing `<button>`) |
51
+
52
+ **Alert constructor**: `Alert.new(variant: :primary, dismissible: false, **attributes, &block)`
53
+ - `variant`: one of `:primary`, `:secondary`, `:success`, `:danger`, `:warning`, `:info`, `:light`, `:dark`
54
+ - `dismissible`: adds `.alert-dismissible` class and appends a `CloseButton`
55
+ - `&block`: instance_eval DSL for adding content inside the element
56
+
57
+ **Component guidelines**:
58
+ - Always use `add_attribute()` instead of manipulating the `attributes` hash directly
59
+ - Each class in its own file under a component-named folder
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
62
+ - Pass user attributes last via `add_attribute(attributes)`
63
+
64
+ ### Sub-component example pattern:
65
+
66
+ ```ruby
67
+ class AlertHeading < Element
68
+ def initialize(**attributes, &block)
69
+ super("h4", &block)
70
+ add_attribute(class: "alert-heading")
71
+ add_attribute(attributes)
72
+ end
73
+ end
74
+ ```
75
+
76
+ ## Coding Conventions
77
+
78
+ - **Language**: Ruby 3.1+
79
+ - **Style**: Double quotes for strings
80
+ - **Testing**: RSpec with `expect` syntax, `subject` pattern
81
+ - **Linting**: RuboCop (run `bundle exec rubocop`)
82
+ - **Pattern matching**: Use `in` pattern matching (`case content; in Element; ...`)
83
+
84
+ ## Common Commands
85
+
86
+ ```bash
87
+ bin/setup # Install dependencies
88
+ bundle exec rspec # Run tests
89
+ bundle exec rubocop # Lint
90
+ bundle exec rake spec # Run tests via rake
91
+ bundle exec rake rubocop # Lint via rake
92
+ bundle exec rake # Default: spec + rubocop
93
+ bin/console # Interactive console
94
+ ruby examples/alert_example.rb # Run Alert examples
95
+ ```
96
+
97
+ ## Testing Patterns
98
+
99
+ - Use `subject` for the element under test
100
+ - Use `before` blocks to set up state
101
+ - Test render output as raw HTML strings
102
+ - Test attribute hashes directly
103
+ - Follow existing describe/context/it structure
104
+
105
+ ## Development Workflow
106
+
107
+ 1. Write/update specs first
108
+ 2. Implement in `lib/element_component/element.rb` or `lib/element_component/components/`
109
+ 3. Run `bundle exec rspec` to verify
110
+ 4. Run `bundle exec rubocop` for linting
111
+ 5. Run `bundle exec rake` for full check
112
+
113
+ ## Key Design Decisions
114
+
115
+ - Attributes stored as Hash of Symbol => Array of values (supports multiple values per attribute)
116
+ - Contents stored as Array (supports strings, Elements, and Procs/blocks)
117
+ - `!` suffix methods reset state before adding (e.g., `add_content!` clears then adds)
118
+ - Hooks (`before_render`, `after_render`, `around_render`) are optional, detected via `respond_to?`
119
+ - Self-closing tags controlled by `closing_tag:` parameter
120
+ - Component classes use `add_attribute()` instead of direct hash manipulation
121
+
122
+ ## Roadmap Features (from README)
123
+ - Caching support
124
+ - Pre-built Bulma components
125
+ - Pre-built Bootstrap components
126
+ - Enhanced DSL for nested structures
data/README.md CHANGED
@@ -1,16 +1,25 @@
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, allowing for dynamic attribute management and content nesting.
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 `instance_eval`-based blocks 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
 
7
- Install the gem and add to the application's Gemfile by executing:
16
+ Add to your application's Gemfile:
8
17
 
9
18
  ```bash
10
19
  bundle add element_component
11
20
  ```
12
21
 
13
- If bundler is not being used to manage dependencies, install the gem by executing:
22
+ Or install directly:
14
23
 
15
24
  ```bash
16
25
  gem install element_component
@@ -18,9 +27,7 @@ gem install element_component
18
27
 
19
28
  ## Usage
20
29
 
21
- ### Basic Usage
22
-
23
- Create a simple element and render it to HTML:
30
+ ### Creating Elements
24
31
 
25
32
  ```ruby
26
33
  p = ElementComponent::Element.new("p", class: "text-bold")
@@ -29,30 +36,58 @@ puts p.render
29
36
  # => <p class="text-bold">Hello, World!</p>
30
37
  ```
31
38
 
32
- ### Nesting Elements
39
+ ### Block DSL
33
40
 
34
- You can nest elements by adding another `ElementComponent::Element` instance as content:
41
+ Use a block to add content inline:
35
42
 
36
43
  ```ruby
37
- div = ElementComponent::Element.new("div", class: "container")
38
- h1 = ElementComponent::Element.new("h1")
39
- h1.add_content("Welcome")
44
+ div = ElementComponent::Element.new("div", class: "container") do
45
+ add_content("Welcome")
46
+ add_content(ElementComponent::Element.new("h1") { add_content("Title") })
47
+ end
48
+ puts div.render
49
+ # => <div class="container">Welcome<h1>Title</h1></div>
50
+ ```
40
51
 
41
- div.add_content(h1)
42
- div.add_content("This is a simple HTML builder.")
52
+ ### The `new_element` Helper
43
53
 
54
+ Inside a block, use `new_element` as a shorthand:
55
+
56
+ ```ruby
57
+ div = ElementComponent::Element.new("div") do
58
+ add_content(new_element("h1") { add_content("Hello") })
59
+ add_content(new_element("p", class: "lead") { add_content("World") })
60
+ end
44
61
  puts div.render
45
- # => <div class="container"><h1>Welcome</h1>This is a simple HTML builder.</div>
62
+ # => <div><h1>Hello</h1><p class="lead">World</p></div>
46
63
  ```
47
64
 
48
- ### Attribute Management
65
+ ### Content Types
66
+
67
+ Content can be a string, an Element, or a block (Proc):
68
+
69
+ ```ruby
70
+ div = ElementComponent::Element.new("div")
71
+
72
+ # String
73
+ div.add_content("plain text")
74
+
75
+ # Element instance
76
+ div.add_content(ElementComponent::Element.new("span") { add_content("nested") })
77
+
78
+ # Block (evaluated at render time, has access to new_element)
79
+ div.add_content { new_element("em") { add_content("deferred") } }
80
+
81
+ puts div.render
82
+ # => <div>plain text<span>nested</span><em>deferred</em></div>
83
+ ```
49
84
 
50
- `ElementComponent` allows for easy manipulation of HTML attributes:
85
+ ### Attribute Management
51
86
 
52
87
  ```ruby
53
88
  btn = ElementComponent::Element.new("button", class: "btn", type: "button")
54
89
 
55
- # Add more values to an attribute (e.g., adding another class)
90
+ # Add more values to an attribute
56
91
  btn.add_attribute(class: "btn-primary")
57
92
 
58
93
  # Reset attributes and set new ones
@@ -67,8 +102,6 @@ btn.remove_attribute_value(:class, "btn-primary")
67
102
 
68
103
  ### Self-Closing Tags
69
104
 
70
- You can specify if an element should have a closing tag:
71
-
72
105
  ```ruby
73
106
  img = ElementComponent::Element.new("img", closing_tag: false, src: "image.png", alt: "Logo")
74
107
  puts img.render
@@ -77,20 +110,402 @@ puts img.render
77
110
 
78
111
  ### Rendering Hooks
79
112
 
80
- `ElementComponent` supports `before_render`, `after_render`, and `around_render` hooks if implemented in a subclass or by extending an instance.
113
+ `ElementComponent` supports `before_render`, `after_render`, and `around_render` hooks:
114
+
115
+ ```ruby
116
+ div = ElementComponent::Element.new("div")
117
+ div.define_singleton_method(:before_render) { add_attribute(class: "dynamic") }
118
+ div.add_content("content")
119
+ puts div.render
120
+ # => <div class="dynamic">content</div>
121
+ ```
122
+
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` |
148
+
149
+ ### Alert
150
+
151
+
152
+ ```ruby
153
+ alert = ElementComponent::Components::Alert.new(variant: :success) do
154
+ add_content("Operation completed!")
155
+ end
156
+ # => <div class="alert alert-success" role="alert">Operation completed!</div>
157
+ ```
158
+
159
+ **Variants**: `:primary`, `:secondary`, `:success`, `:danger`, `:warning`, `:info`, `:light`, `:dark`
160
+
161
+ Dismissible alerts, headings, and links use sub-components:
162
+
163
+ ```ruby
164
+ alert = ElementComponent::Components::Alert.new(variant: :warning, dismissible: true) do
165
+ add_content(ElementComponent::Components::AlertHeading.new { add_content("Warning") })
166
+ add_content("Please review. ")
167
+ add_content(ElementComponent::Components::AlertLink.new(href: "/details") { add_content("Details") })
168
+ end
169
+ # => <div class="alert alert-warning alert-dismissible" role="alert">
170
+ # <h4 class="alert-heading">Warning</h4>
171
+ # Please review.
172
+ # <a class="alert-link" href="/details">Details</a>
173
+ # <button class="btn-close" data-bs-dismiss="alert" aria-label="Close">
174
+ # </div>
175
+ ```
176
+
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
+
187
+
188
+ ```ruby
189
+ # Standard button
190
+ btn = ElementComponent::Components::Button.new(variant: :primary) { 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) { 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") { 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) { add_content("New") }
209
+ # => <span class="badge bg-primary">New</span>
210
+
211
+ pill = ElementComponent::Components::Badge.new(variant: :danger, pill: true) { 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
220
+ add_content(ElementComponent::Components::CardImage.new(src: "photo.jpg", top: true))
221
+ add_content(ElementComponent::Components::CardBody.new do
222
+ add_content(ElementComponent::Components::CardTitle.new { add_content("Title") })
223
+ add_content(ElementComponent::Components::CardText.new { add_content("Some text.") })
224
+ end)
225
+ add_content(ElementComponent::Components::CardFooter.new { add_content("Footer") })
226
+ end
227
+ ```
228
+
229
+ **Sub-components**:
230
+
231
+ | Class | Tag | CSS Class |
232
+ |---|---|---|
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
245
+ add_content(ElementComponent::Components::NavItem.new do
246
+ add_content(ElementComponent::Components::NavLink.new(href: "/", active: true) { add_content("Home") })
247
+ end)
248
+ add_content(ElementComponent::Components::NavItem.new do
249
+ add_content(ElementComponent::Components::NavLink.new(href: "/profile") { 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
262
+ add_content(ElementComponent::Components::BreadcrumbItem.new(href: "/") { add_content("Home") })
263
+ add_content(ElementComponent::Components::BreadcrumbItem.new(href: "/section") { add_content("Section") })
264
+ add_content(ElementComponent::Components::BreadcrumbItem.new(active: true) { 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
274
+ add_content(ElementComponent::Components::ListGroupItem.new { add_content("Item 1") })
275
+ add_content(ElementComponent::Components::ListGroupItem.new(active: true) { add_content("Item 2") })
276
+ add_content(ElementComponent::Components::ListGroupItem.new(href: "/link") { 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
288
+ add_content(ElementComponent::Components::ProgressBar.new(value: 75, variant: :success, striped: true) do
289
+ 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
311
+ add_content("<thead><tr><th>Name</th><th>Age</th></tr></thead>")
312
+ 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
324
+ add_content(ElementComponent::Components::PageItem.new(active: true) { add_content("1") })
325
+ add_content(ElementComponent::Components::PageItem.new { 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
335
+ add_content(ElementComponent::Components::Button.new(variant: :primary) { add_content("Left") })
336
+ add_content(ElementComponent::Components::Button.new(variant: :primary) { add_content("Middle") })
337
+ add_content(ElementComponent::Components::Button.new(variant: :primary) { 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
360
+ add_content(ElementComponent::Components::ModalContent.new do
361
+ add_content(ElementComponent::Components::ModalHeader.new do
362
+ add_content(ElementComponent::Components::ModalTitle.new { add_content("Modal title") })
363
+ end)
364
+ add_content(ElementComponent::Components::ModalBody.new { add_content("Modal body text.") })
365
+ add_content(ElementComponent::Components::ModalFooter.new do
366
+ add_content(ElementComponent::Components::Button.new(variant: :secondary) { add_content("Close") })
367
+ add_content(ElementComponent::Components::Button.new(variant: :primary) { 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
391
+ add_content(ElementComponent::Components::CarouselItem.new(active: true) do
392
+ add_content(%(<img src="slide1.jpg" class="d-block w-100" alt="...">))
393
+ end)
394
+ add_content(ElementComponent::Components::CarouselItem.new do
395
+ 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
414
+ 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") { add_content("Dropdown") }
420
+ )
421
+ add_content(
422
+ ElementComponent::Components::DropdownMenu.new do
423
+ add_content(ElementComponent::Components::DropdownItem.new { add_content("Action") })
424
+ add_content(ElementComponent::Components::DropdownItem.new(active: true) { add_content("Active") })
425
+ add_content(ElementComponent::Components::DropdownDivider.new)
426
+ add_content(ElementComponent::Components::DropdownItem.new(disabled: true) { 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
448
+ add_content(ElementComponent::Components::NavbarBrand.new(href: "/") { add_content("Brand") })
449
+ add_content(ElementComponent::Components::NavbarToggler.new(target: "nav"))
450
+ add_content(ElementComponent::Components::NavbarCollapse.new(id: "nav") do
451
+ add_content(ElementComponent::Components::NavbarNav.new do
452
+ add_content(ElementComponent::Components::NavItem.new do
453
+ add_content(ElementComponent::Components::NavLink.new(href: "/", active: true) { add_content("Home") })
454
+ end)
455
+ add_content(ElementComponent::Components::NavItem.new do
456
+ add_content(ElementComponent::Components::NavLink.new(href: "/about") { 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` |
81
473
 
82
474
  ## Development
83
475
 
84
- After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
476
+ ```bash
477
+ bin/setup # Install dependencies
478
+ bundle exec rspec # Run tests
479
+ bundle exec rubocop # Lint
480
+ bundle exec rake # Spec + RuboCop
481
+ bin/console # Interactive console
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
486
+ ```
487
+
488
+ ### Test Coverage
489
+
490
+ Run with coverage reporting:
491
+
492
+ ```bash
493
+ COVERAGE=true bundle exec rspec
494
+ ```
495
+
496
+ ### Release
497
+
498
+ ```bash
499
+ # Update version in lib/element_component/version.rb
500
+ bundle exec rake release
501
+ ```
85
502
 
86
- To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and the created tag, and push the `.gem` file to [rubygems.org](https://rubygems.org).
503
+ Or push a version tag (e.g., `v0.6.0`) to trigger the automated release workflow.
87
504
 
88
505
  ## Roadmap
89
506
 
90
507
  - [ ] Support for Caching
91
- - [ ] Pre-built Bulma components
92
- - [ ] Pre-built Bootstrap components
93
- - [ ] 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)
94
509
 
95
510
  ## Contributing
96
511