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.
- checksums.yaml +4 -4
- data/AGENTS.md +126 -0
- data/README.md +440 -25
- data/examples/alert_example.rb +97 -0
- data/examples/badge_example.rb +63 -0
- data/examples/breadcrumb_example.rb +36 -0
- data/examples/button_example.rb +86 -0
- data/examples/button_group_example.rb +55 -0
- data/examples/card_example.rb +62 -0
- data/examples/carousel_example.rb +95 -0
- data/examples/close_button_example.rb +40 -0
- data/examples/dropdown_example.rb +123 -0
- data/examples/list_group_example.rb +60 -0
- data/examples/modal_example.rb +120 -0
- data/examples/nav_example.rb +75 -0
- data/examples/navbar_example.rb +96 -0
- data/examples/pagination_example.rb +47 -0
- data/examples/progress_example.rb +54 -0
- data/examples/spinner_example.rb +49 -0
- data/examples/table_example.rb +41 -0
- data/lib/element_component/components/alert/close_button.rb +17 -0
- data/lib/element_component/components/alert/heading.rb +14 -0
- data/lib/element_component/components/alert/link.rb +15 -0
- data/lib/element_component/components/alert.rb +25 -0
- data/lib/element_component/components/badge.rb +18 -0
- data/lib/element_component/components/breadcrumb/item.rb +29 -0
- data/lib/element_component/components/breadcrumb.rb +26 -0
- data/lib/element_component/components/button.rb +25 -0
- data/lib/element_component/components/button_group.rb +18 -0
- data/lib/element_component/components/card/body.rb +14 -0
- data/lib/element_component/components/card/footer.rb +14 -0
- data/lib/element_component/components/card/header.rb +14 -0
- data/lib/element_component/components/card/image.rb +17 -0
- data/lib/element_component/components/card/text.rb +14 -0
- data/lib/element_component/components/card/title.rb +14 -0
- data/lib/element_component/components/card.rb +21 -0
- data/lib/element_component/components/carousel/caption.rb +14 -0
- data/lib/element_component/components/carousel/item.rb +16 -0
- data/lib/element_component/components/carousel.rb +70 -0
- data/lib/element_component/components/close_button.rb +17 -0
- data/lib/element_component/components/dropdown/divider.rb +20 -0
- data/lib/element_component/components/dropdown/header.rb +22 -0
- data/lib/element_component/components/dropdown/item.rb +33 -0
- data/lib/element_component/components/dropdown/menu.rb +15 -0
- data/lib/element_component/components/dropdown.rb +43 -0
- data/lib/element_component/components/list_group/item.rb +23 -0
- data/lib/element_component/components/list_group.rb +18 -0
- data/lib/element_component/components/modal/body.rb +14 -0
- data/lib/element_component/components/modal/content.rb +14 -0
- data/lib/element_component/components/modal/dialog.rb +21 -0
- data/lib/element_component/components/modal/footer.rb +14 -0
- data/lib/element_component/components/modal/header.rb +16 -0
- data/lib/element_component/components/modal/title.rb +14 -0
- data/lib/element_component/components/modal.rb +42 -0
- data/lib/element_component/components/nav/item.rb +14 -0
- data/lib/element_component/components/nav/link.rb +18 -0
- data/lib/element_component/components/nav.rb +23 -0
- data/lib/element_component/components/navbar/brand.rb +15 -0
- data/lib/element_component/components/navbar/collapse.rb +16 -0
- data/lib/element_component/components/navbar/nav.rb +14 -0
- data/lib/element_component/components/navbar/toggler.rb +22 -0
- data/lib/element_component/components/navbar.rb +51 -0
- data/lib/element_component/components/pagination/item.rb +27 -0
- data/lib/element_component/components/pagination.rb +33 -0
- data/lib/element_component/components/progress/bar.rb +24 -0
- data/lib/element_component/components/progress.rb +17 -0
- data/lib/element_component/components/spinner.rb +19 -0
- data/lib/element_component/components/table.rb +21 -0
- data/lib/element_component/components.rb +24 -0
- data/lib/element_component/element.rb +21 -7
- data/lib/element_component/version.rb +3 -1
- data/lib/element_component.rb +3 -1
- metadata +68 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 27a79b6578d299e674340e4c1e3c7a0061ff6ac3b9c233f102bb3d4d3fd89011
|
|
4
|
+
data.tar.gz: 501d6617270b0e5bc9cef324d270126d5cd531677cdd8ee43bb3a8e18bf1cd4c
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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,
|
|
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
|
-
|
|
16
|
+
Add to your application's Gemfile:
|
|
8
17
|
|
|
9
18
|
```bash
|
|
10
19
|
bundle add element_component
|
|
11
20
|
```
|
|
12
21
|
|
|
13
|
-
|
|
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
|
-
###
|
|
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
|
-
###
|
|
39
|
+
### Block DSL
|
|
33
40
|
|
|
34
|
-
|
|
41
|
+
Use a block to add content inline:
|
|
35
42
|
|
|
36
43
|
```ruby
|
|
37
|
-
div = ElementComponent::Element.new("div", class: "container")
|
|
38
|
-
|
|
39
|
-
h1
|
|
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
|
-
|
|
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="
|
|
62
|
+
# => <div><h1>Hello</h1><p class="lead">World</p></div>
|
|
46
63
|
```
|
|
47
64
|
|
|
48
|
-
###
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
- [
|
|
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
|
|