stimulus_plumbers 0.2.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/README.md +75 -0
- data/app/assets/javascripts/stimulus-plumbers/.keep +0 -0
- data/app/assets/stylesheets/stimulus_plumbers/tokens.css +83 -0
- data/lib/stimulus_plumbers/components/action_list/renderer.rb +47 -0
- data/lib/stimulus_plumbers/components/avatar/renderer.rb +74 -0
- data/lib/stimulus_plumbers/components/button/renderer.rb +33 -0
- data/lib/stimulus_plumbers/components/calendar/month/turbo/days_of_month.rb +124 -0
- data/lib/stimulus_plumbers/components/calendar/month/turbo/days_of_week.rb +36 -0
- data/lib/stimulus_plumbers/components/calendar/month/turbo/renderer.rb +57 -0
- data/lib/stimulus_plumbers/components/calendar/renderer.rb +35 -0
- data/lib/stimulus_plumbers/components/card/renderer.rb +41 -0
- data/lib/stimulus_plumbers/components/date_picker/navigation.rb +49 -0
- data/lib/stimulus_plumbers/components/date_picker/navigator.rb +31 -0
- data/lib/stimulus_plumbers/components/date_picker/renderer.rb +82 -0
- data/lib/stimulus_plumbers/components/icon/renderer.rb +51 -0
- data/lib/stimulus_plumbers/components/plumber/base.rb +22 -0
- data/lib/stimulus_plumbers/components/plumber/dispatcher.rb +113 -0
- data/lib/stimulus_plumbers/components/plumber/html_options.rb +34 -0
- data/lib/stimulus_plumbers/components/plumber/renderer.rb +91 -0
- data/lib/stimulus_plumbers/components/popover/renderer.rb +46 -0
- data/lib/stimulus_plumbers/configuration.rb +39 -0
- data/lib/stimulus_plumbers/engine.rb +21 -0
- data/lib/stimulus_plumbers/form/builder.rb +67 -0
- data/lib/stimulus_plumbers/form/field_component.rb +80 -0
- data/lib/stimulus_plumbers/form/fields/choice.rb +25 -0
- data/lib/stimulus_plumbers/form/fields/file.rb +16 -0
- data/lib/stimulus_plumbers/form/fields/renderer.rb +57 -0
- data/lib/stimulus_plumbers/form/fields/select.rb +27 -0
- data/lib/stimulus_plumbers/form/fields/text.rb +25 -0
- data/lib/stimulus_plumbers/form/fields/text_area.rb +16 -0
- data/lib/stimulus_plumbers/helpers/action_list_helper.rb +25 -0
- data/lib/stimulus_plumbers/helpers/avatar_helper.rb +17 -0
- data/lib/stimulus_plumbers/helpers/button_helper.rb +25 -0
- data/lib/stimulus_plumbers/helpers/calendar_helper.rb +26 -0
- data/lib/stimulus_plumbers/helpers/calendar_turbo_helper.rb +31 -0
- data/lib/stimulus_plumbers/helpers/card_helper.rb +21 -0
- data/lib/stimulus_plumbers/helpers/date_picker_helper.rb +17 -0
- data/lib/stimulus_plumbers/helpers/plumber_helper.rb +15 -0
- data/lib/stimulus_plumbers/helpers/popover_helper.rb +17 -0
- data/lib/stimulus_plumbers/helpers.rb +25 -0
- data/lib/stimulus_plumbers/logger.rb +20 -0
- data/lib/stimulus_plumbers/themes/action_list.rb +14 -0
- data/lib/stimulus_plumbers/themes/avatar.rb +14 -0
- data/lib/stimulus_plumbers/themes/base.rb +73 -0
- data/lib/stimulus_plumbers/themes/button.rb +18 -0
- data/lib/stimulus_plumbers/themes/calendar.rb +15 -0
- data/lib/stimulus_plumbers/themes/card.rb +12 -0
- data/lib/stimulus_plumbers/themes/form.rb +30 -0
- data/lib/stimulus_plumbers/themes/layout.rb +12 -0
- data/lib/stimulus_plumbers/themes/schema/ranges.rb +15 -0
- data/lib/stimulus_plumbers/themes/tailwind/action_list.rb +33 -0
- data/lib/stimulus_plumbers/themes/tailwind/avatar.rb +52 -0
- data/lib/stimulus_plumbers/themes/tailwind/button.rb +89 -0
- data/lib/stimulus_plumbers/themes/tailwind/calendar.rb +34 -0
- data/lib/stimulus_plumbers/themes/tailwind/card.rb +24 -0
- data/lib/stimulus_plumbers/themes/tailwind/form.rb +104 -0
- data/lib/stimulus_plumbers/themes/tailwind/layout.rb +25 -0
- data/lib/stimulus_plumbers/themes/tailwind_theme.rb +29 -0
- data/lib/stimulus_plumbers/version.rb +5 -0
- data/lib/stimulus_plumbers.rb +48 -0
- metadata +129 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 1abfd2ca5a0c575652d0d6c650acbde79e656b9b806a76b8f922e682cf2f7526
|
|
4
|
+
data.tar.gz: 621aa551f6fb7b9f3a90c3313e4dedb6c901f475cb96f6631a9fc54edccf5bac
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: e45e2a0ea994b0e8d22edc2098974d6f15c43bb552a92dc7c5baf2dc0700b9a8f22ef570ffa913c0cae84d6bc64ecbe6b6e09613e4b73ccaccc1613d93d898d3
|
|
7
|
+
data.tar.gz: 1be1501a2b77cad35780730e84803bdb5506e6def24699c46cc1797196780aec94dfe7a451a0b4774b748258132a7b0eb3d6bae16c71c22bc7c733bf24c45f1c
|
data/README.md
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
# Stimulus Plumbers
|
|
2
|
+
|
|
3
|
+
A library of semantically correct, accessible UI components for Rails 8.0+ using ViewComponent and Stimulus.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
Add this line to your application's Gemfile:
|
|
8
|
+
|
|
9
|
+
```ruby
|
|
10
|
+
gem 'stimulus_plumbers'
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
And then execute:
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
bundle install
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## Usage
|
|
20
|
+
|
|
21
|
+
Stimulus Plumbers provides ready-to-use ViewComponent components that render semantically correct, accessible HTML. Each component is designed with accessibility as a core requirement, not an afterthought.
|
|
22
|
+
|
|
23
|
+
### Basic Example
|
|
24
|
+
|
|
25
|
+
```erb
|
|
26
|
+
<%# In your Rails view %>
|
|
27
|
+
<%= render ButtonComponent.new(url: root_path) do %>
|
|
28
|
+
Click me
|
|
29
|
+
<% end %>
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
### Available Components
|
|
33
|
+
|
|
34
|
+
- **ActionList**: Accessible lists with items and sections
|
|
35
|
+
- **Avatar**: User avatar component
|
|
36
|
+
- **Button**: Semantic button with optional prefix/suffix
|
|
37
|
+
- **Card**: Card component with sections
|
|
38
|
+
- **Calendar**: Date calendar with navigation
|
|
39
|
+
- **Container**: Layout container
|
|
40
|
+
- **Divider**: Semantic divider/separator
|
|
41
|
+
- **Dropdown**: Accessible dropdown menus
|
|
42
|
+
- **Navigation**: Navigation bars, tabs, and lists
|
|
43
|
+
- **Popover**: Accessible popover component
|
|
44
|
+
|
|
45
|
+
## Component Philosophy
|
|
46
|
+
|
|
47
|
+
All components in this library follow these principles:
|
|
48
|
+
|
|
49
|
+
1. **Accessibility First**: WCAG 2.1 Level AA minimum
|
|
50
|
+
2. **Semantic HTML**: Use native elements before ARIA
|
|
51
|
+
3. **Keyboard Navigation**: Full keyboard support
|
|
52
|
+
4. **Screen Reader Friendly**: Proper announcements and labels
|
|
53
|
+
5. **Focus Management**: Visible focus indicators and logical tab order
|
|
54
|
+
|
|
55
|
+
## Development
|
|
56
|
+
|
|
57
|
+
After checking out the repo, run:
|
|
58
|
+
|
|
59
|
+
```bash
|
|
60
|
+
bundle install
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
Run the test suite with:
|
|
64
|
+
|
|
65
|
+
```bash
|
|
66
|
+
bundle exec rspec
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
## Contributing
|
|
70
|
+
|
|
71
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/ryancyq/stimulus-plumbers.
|
|
72
|
+
|
|
73
|
+
## License
|
|
74
|
+
|
|
75
|
+
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
|
File without changes
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Stimulus Plumbers – Design Tokens
|
|
3
|
+
*
|
|
4
|
+
* CSS custom properties for all presentational values.
|
|
5
|
+
* Structural layout utilities stay in component templates.
|
|
6
|
+
* Override any token in your own :root block to retheme the library.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
:root {
|
|
10
|
+
/* ── Semantic colors ─────────────────────────────────────────────────── */
|
|
11
|
+
--sp-color-primary: oklch(55% 0.2 250);
|
|
12
|
+
--sp-color-primary-fg: oklch(98% 0.01 250);
|
|
13
|
+
|
|
14
|
+
--sp-color-secondary: oklch(55% 0.05 250);
|
|
15
|
+
--sp-color-secondary-fg: oklch(98% 0.01 250);
|
|
16
|
+
|
|
17
|
+
--sp-color-destructive: oklch(55% 0.22 25);
|
|
18
|
+
--sp-color-destructive-fg: oklch(98% 0.01 25);
|
|
19
|
+
|
|
20
|
+
--sp-color-muted: oklch(96% 0.02 250);
|
|
21
|
+
--sp-color-muted-fg: oklch(45% 0.05 250);
|
|
22
|
+
|
|
23
|
+
--sp-color-border: oklch(90% 0.02 250);
|
|
24
|
+
--sp-color-bg: oklch(100% 0 0);
|
|
25
|
+
--sp-color-fg: oklch(10% 0.02 250);
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
/* ── Focus ring ──────────────────────────────────────────────────────── */
|
|
29
|
+
--sp-focus-ring-color: var(--sp-color-primary);
|
|
30
|
+
--sp-focus-ring-width: 2px;
|
|
31
|
+
--sp-focus-ring-offset: 2px;
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
/* ── Avatar palette (10 slots, index chosen by name hash) ────────────── */
|
|
35
|
+
--sp-avatar-color-1: #b45309;
|
|
36
|
+
--sp-avatar-color-2: #4d7c0f;
|
|
37
|
+
--sp-avatar-color-3: #0369a1;
|
|
38
|
+
--sp-avatar-color-4: #0e7490;
|
|
39
|
+
--sp-avatar-color-5: #0f766e;
|
|
40
|
+
--sp-avatar-color-6: #047857;
|
|
41
|
+
--sp-avatar-color-7: #4338ca;
|
|
42
|
+
--sp-avatar-color-8: #a21caf;
|
|
43
|
+
--sp-avatar-color-9: #be123c;
|
|
44
|
+
--sp-avatar-color-10: #be185d;
|
|
45
|
+
--sp-avatar-fg: #ffffff;
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
/* ── Spacing ─────────────────────────────────────────────────────────── */
|
|
49
|
+
--sp-space-1: 0.25rem; /* 4px */
|
|
50
|
+
--sp-space-2: 0.5rem; /* 8px */
|
|
51
|
+
--sp-space-3: 0.75rem; /* 12px */
|
|
52
|
+
--sp-space-4: 1rem; /* 16px */
|
|
53
|
+
--sp-space-6: 1.5rem; /* 24px */
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
/* ── Border radius ───────────────────────────────────────────────────── */
|
|
57
|
+
--sp-radius-sm: 0.25rem;
|
|
58
|
+
--sp-radius-md: 0.375rem;
|
|
59
|
+
--sp-radius-lg: 0.5rem;
|
|
60
|
+
--sp-radius-full: 9999px;
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
/* ── Shadow ──────────────────────────────────────────────────────────── */
|
|
64
|
+
--sp-shadow-sm: 0 1px 2px rgb(0 0 0 / 0.05);
|
|
65
|
+
--sp-shadow-md: 0 4px 6px -1px rgb(0 0 0 / 0.1),
|
|
66
|
+
0 2px 4px -2px rgb(0 0 0 / 0.1);
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
/* ── Typography ──────────────────────────────────────────────────────── */
|
|
70
|
+
--sp-text-sm: 0.875rem;
|
|
71
|
+
--sp-text-base: 1rem;
|
|
72
|
+
--sp-text-lg: 1.125rem;
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
/* ── Sizing ──────────────────────────────────────────────────────────── */
|
|
76
|
+
--sp-avatar-size: 2rem; /* 32px */
|
|
77
|
+
--sp-icon-size: 1.5rem; /* 24px */
|
|
78
|
+
--sp-calendar-day-size: 2rem; /* 32px */
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
/* ── Z-index ─────────────────────────────────────────────────────────── */
|
|
82
|
+
--sp-z-popover: 50;
|
|
83
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module StimulusPlumbers
|
|
4
|
+
module Components
|
|
5
|
+
module ActionList
|
|
6
|
+
class Renderer < Plumber::Base
|
|
7
|
+
def list(**kwargs, &block)
|
|
8
|
+
html_options = merge_html_options(
|
|
9
|
+
{ classes: theme.resolve(:action_list).fetch(:classes, "") },
|
|
10
|
+
kwargs
|
|
11
|
+
)
|
|
12
|
+
template.content_tag(:div, template.capture(&block), **html_options)
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def section(title: nil, **kwargs, &block)
|
|
16
|
+
html_options = merge_html_options(kwargs)
|
|
17
|
+
template.content_tag(:div, **html_options) do
|
|
18
|
+
template.safe_join(
|
|
19
|
+
[
|
|
20
|
+
(template.content_tag(:p, title) if title.present?),
|
|
21
|
+
template.content_tag(:ul, template.capture(&block))
|
|
22
|
+
].compact
|
|
23
|
+
)
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def item(content = nil, url: nil, external: false, active: false, **kwargs, &block)
|
|
28
|
+
content = template.capture(&block) if block_given?
|
|
29
|
+
html_options = merge_html_options(
|
|
30
|
+
{ classes: theme.resolve(:action_list_item, active: active).fetch(:classes, "") },
|
|
31
|
+
kwargs
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
inner = if url
|
|
35
|
+
html_options[:target] = "_blank" if external
|
|
36
|
+
template.content_tag(:a, content, href: url, **html_options)
|
|
37
|
+
else
|
|
38
|
+
html_options[:type] ||= "button"
|
|
39
|
+
template.content_tag(:button, content, **html_options)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
template.content_tag(:li, inner)
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module StimulusPlumbers
|
|
4
|
+
module Components
|
|
5
|
+
module Avatar
|
|
6
|
+
class Renderer < Plumber::Base
|
|
7
|
+
def avatar(name: nil, initials: nil, url: nil, color: nil, size: :md, **kwargs, &block)
|
|
8
|
+
color_css = resolve_color(color, name, initials) unless url || block_given?
|
|
9
|
+
|
|
10
|
+
html_options = merge_html_options(
|
|
11
|
+
{
|
|
12
|
+
classes: [theme.resolve(:avatar, size: size).fetch(:classes, ""), color_css],
|
|
13
|
+
"aria-label": name,
|
|
14
|
+
role: "img"
|
|
15
|
+
},
|
|
16
|
+
kwargs
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
template.content_tag(:span, inner(name, initials, url, &block), **html_options)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
private
|
|
23
|
+
|
|
24
|
+
def inner(name, initials, url, &block)
|
|
25
|
+
if block_given?
|
|
26
|
+
template.capture(&block)
|
|
27
|
+
elsif url
|
|
28
|
+
template.tag.img(src: url, alt: name.present? ? "#{name}'s avatar" : "", onerror: "this.src=''")
|
|
29
|
+
elsif initials
|
|
30
|
+
initials_svg(initials)
|
|
31
|
+
else
|
|
32
|
+
fallback_svg
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def resolve_color(color, name, initials)
|
|
37
|
+
if color
|
|
38
|
+
theme.avatar_colors.fetch(color, nil)
|
|
39
|
+
elsif (seed = name || initials)
|
|
40
|
+
theme.avatar_color_range[seed.bytes.reduce(:^) % theme.avatar_color_range.length]
|
|
41
|
+
else
|
|
42
|
+
theme.avatar_color_range.first
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def initials_svg(initials)
|
|
47
|
+
template.content_tag(:svg, viewBox: "0 0 40 40") do
|
|
48
|
+
template.content_tag(
|
|
49
|
+
:text,
|
|
50
|
+
initials.upcase,
|
|
51
|
+
x: "50%",
|
|
52
|
+
y: "50%",
|
|
53
|
+
dy: "0.35em",
|
|
54
|
+
fill: "currentColor",
|
|
55
|
+
"font-size": "20",
|
|
56
|
+
"text-anchor": "middle"
|
|
57
|
+
)
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def fallback_svg
|
|
62
|
+
template.content_tag(:svg, viewBox: "0 0 40 40") do
|
|
63
|
+
template.tag.path(
|
|
64
|
+
fill: "currentColor",
|
|
65
|
+
d: "M8.28 27.5A14.95 14.95 0 0120 21.8c4.76 0 8.97 2.24 11.72 5.7a14.02 " \
|
|
66
|
+
"14.02 0 01-8.25 5.91 14.82 14.82 0 01-6.94 0 14.02 14.02 0 01-8.25-5.9z" \
|
|
67
|
+
"M13.99 12.78a6.02 6.02 0 1112.03 0 6.02 6.02 0 01-12.03 0z"
|
|
68
|
+
)
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
end
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module StimulusPlumbers
|
|
4
|
+
module Components
|
|
5
|
+
module Button
|
|
6
|
+
class Renderer < Plumber::Base
|
|
7
|
+
def button(content = nil, url: nil, external: false, variant: :primary, size: :md, **kwargs, &block)
|
|
8
|
+
content = template.capture(&block) if block_given?
|
|
9
|
+
html_options = merge_html_options(
|
|
10
|
+
{ classes: theme.resolve(:button, variant: variant, size: size).fetch(:classes, "") },
|
|
11
|
+
kwargs
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
if url
|
|
15
|
+
html_options[:target] = "_blank" if external
|
|
16
|
+
template.content_tag(:a, content, href: url, **html_options)
|
|
17
|
+
else
|
|
18
|
+
html_options[:type] ||= "button"
|
|
19
|
+
template.content_tag(:button, content, **html_options)
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def group(alignment: :left, direction: :row, **kwargs, &block)
|
|
24
|
+
html_options = merge_html_options(
|
|
25
|
+
{ classes: theme.resolve(:button_group, alignment: alignment, direction: direction).fetch(:classes, "") },
|
|
26
|
+
kwargs
|
|
27
|
+
)
|
|
28
|
+
template.content_tag(:div, template.capture(&block), **html_options)
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module StimulusPlumbers
|
|
4
|
+
module Components
|
|
5
|
+
module Calendar
|
|
6
|
+
module Month
|
|
7
|
+
module Turbo
|
|
8
|
+
class DaysOfMonth < Plumber::Base
|
|
9
|
+
DAYS_IN_WEEK = 7
|
|
10
|
+
|
|
11
|
+
attr_reader :date, :today, :selectable, :selected_date, :show_other_months
|
|
12
|
+
|
|
13
|
+
def initialize(
|
|
14
|
+
template,
|
|
15
|
+
date: Date.today,
|
|
16
|
+
today: Date.today,
|
|
17
|
+
selectable: false,
|
|
18
|
+
selected_date: nil,
|
|
19
|
+
show_other_months: false
|
|
20
|
+
)
|
|
21
|
+
super(template)
|
|
22
|
+
@date = date
|
|
23
|
+
@today = today
|
|
24
|
+
@selectable = selectable
|
|
25
|
+
@selected_date = selected_date
|
|
26
|
+
@show_other_months = show_other_months
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def render(**kwargs)
|
|
30
|
+
html_options = merge_html_options(
|
|
31
|
+
{ classes: theme.resolve(:calendar_days_of_month).fetch(:classes, "") },
|
|
32
|
+
kwargs
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
template.content_tag(:div, **html_options, role: "rowgroup") do
|
|
36
|
+
template.safe_join(
|
|
37
|
+
build_days.each_slice(DAYS_IN_WEEK).map do |week|
|
|
38
|
+
template.content_tag(:div, role: "row") do
|
|
39
|
+
days_in_week(week)
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
)
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
private
|
|
47
|
+
|
|
48
|
+
# rubocop:disable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
|
|
49
|
+
def focus_day
|
|
50
|
+
@focus_day ||= if selected_date&.month == date.month && selected_date&.year == date.year
|
|
51
|
+
selected_date
|
|
52
|
+
elsif today.month == date.month && today.year == date.year
|
|
53
|
+
today
|
|
54
|
+
else
|
|
55
|
+
date.beginning_of_month
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
# rubocop:enable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
|
|
59
|
+
|
|
60
|
+
def build_days
|
|
61
|
+
first = date.beginning_of_month
|
|
62
|
+
last = date.end_of_month
|
|
63
|
+
|
|
64
|
+
prev_days = (first - first.wday).upto(first - 1).to_a
|
|
65
|
+
current_days = first.upto(last).to_a
|
|
66
|
+
total = prev_days.length + current_days.length
|
|
67
|
+
next_count = (DAYS_IN_WEEK - (total % DAYS_IN_WEEK)) % DAYS_IN_WEEK
|
|
68
|
+
next_days = next_count.positive? ? (last + 1).upto(last + next_count).to_a : []
|
|
69
|
+
|
|
70
|
+
prev_days + current_days + next_days
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def days_in_week(week)
|
|
74
|
+
template.safe_join(
|
|
75
|
+
week.map do |day|
|
|
76
|
+
if day.month == date.month
|
|
77
|
+
current_month_day_cell(day)
|
|
78
|
+
elsif show_other_months
|
|
79
|
+
other_month_day_cell(day)
|
|
80
|
+
else
|
|
81
|
+
hidden_day_cell(day)
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
)
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def hidden_day_cell(date)
|
|
88
|
+
template.content_tag(:span, role: "gridcell", tabindex: -1, aria: { hidden: "true" }) do
|
|
89
|
+
template.content_tag(:time, nil, datetime: date.iso8601)
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def current_month_day_cell(date)
|
|
94
|
+
tag = selectable ? :button : :span
|
|
95
|
+
selected = selected_date && date == selected_date ? "true" : "false"
|
|
96
|
+
template.content_tag(
|
|
97
|
+
tag,
|
|
98
|
+
role: "gridcell",
|
|
99
|
+
tabindex: date == focus_day ? 0 : -1,
|
|
100
|
+
aria: {
|
|
101
|
+
current: date == today ? "date" : nil,
|
|
102
|
+
selected: selectable ? selected : nil
|
|
103
|
+
}
|
|
104
|
+
) do
|
|
105
|
+
template.content_tag(:time, date.day.to_s, datetime: date.iso8601)
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def other_month_day_cell(date)
|
|
110
|
+
template.content_tag(
|
|
111
|
+
:span,
|
|
112
|
+
role: "gridcell",
|
|
113
|
+
tabindex: -1,
|
|
114
|
+
aria: { disabled: "true", selected: "false" }
|
|
115
|
+
) do
|
|
116
|
+
template.content_tag(:time, date.day.to_s, datetime: date.iso8601)
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
end
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module StimulusPlumbers
|
|
4
|
+
module Components
|
|
5
|
+
module Calendar
|
|
6
|
+
module Month
|
|
7
|
+
module Turbo
|
|
8
|
+
class DaysOfWeek < Plumber::Base
|
|
9
|
+
def render(**kwargs)
|
|
10
|
+
html_options = merge_html_options(
|
|
11
|
+
{ classes: theme.resolve(:calendar_days_of_week).fetch(:classes, "") },
|
|
12
|
+
kwargs
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
template.content_tag(:div, **html_options) do
|
|
16
|
+
template.content_tag(:div, role: "row") do
|
|
17
|
+
template.safe_join(
|
|
18
|
+
day_names.map do |abbr, full|
|
|
19
|
+
template.content_tag(:span, abbr, role: "columnheader", abbr: full)
|
|
20
|
+
end
|
|
21
|
+
)
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
private
|
|
27
|
+
|
|
28
|
+
def day_names
|
|
29
|
+
I18n.t("date.abbr_day_names").zip(I18n.t("date.day_names"))
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module StimulusPlumbers
|
|
4
|
+
module Components
|
|
5
|
+
module Calendar
|
|
6
|
+
module Month
|
|
7
|
+
module Turbo
|
|
8
|
+
class Renderer < Plumber::Base
|
|
9
|
+
STIMULUS_CONTROLLER = "calendar-month-observer"
|
|
10
|
+
|
|
11
|
+
def render(
|
|
12
|
+
date: Date.today,
|
|
13
|
+
today: Date.today,
|
|
14
|
+
selectable: false,
|
|
15
|
+
selected_date: nil,
|
|
16
|
+
show_other_months: false,
|
|
17
|
+
**kwargs
|
|
18
|
+
)
|
|
19
|
+
html_options = merge_html_options(
|
|
20
|
+
{
|
|
21
|
+
classes: theme.resolve(:calendar).fetch(:classes, ""),
|
|
22
|
+
data: { controller: STIMULUS_CONTROLLER, action: "click->#{STIMULUS_CONTROLLER}#select" }
|
|
23
|
+
},
|
|
24
|
+
kwargs
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
template.content_tag(:div, role: "grid", **html_options) do
|
|
28
|
+
template.safe_join(
|
|
29
|
+
[
|
|
30
|
+
days_of_week,
|
|
31
|
+
days_of_month(
|
|
32
|
+
date: date,
|
|
33
|
+
today: today,
|
|
34
|
+
selectable: selectable,
|
|
35
|
+
selected_date: selected_date,
|
|
36
|
+
show_other_months: show_other_months
|
|
37
|
+
)
|
|
38
|
+
]
|
|
39
|
+
)
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
private
|
|
44
|
+
|
|
45
|
+
def days_of_week(**kwargs)
|
|
46
|
+
DaysOfWeek.new(template).render(**kwargs)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def days_of_month(**kwargs)
|
|
50
|
+
DaysOfMonth.new(template, **kwargs).render
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module StimulusPlumbers
|
|
4
|
+
module Components
|
|
5
|
+
module Calendar
|
|
6
|
+
class Renderer < Plumber::Base
|
|
7
|
+
STIMULUS_CONTROLLER = "calendar-month"
|
|
8
|
+
OBSERVER_STIMULUS_CONTROLLER = "calendar-month-observer"
|
|
9
|
+
STIMULUS_DATA = {
|
|
10
|
+
controller: "#{STIMULUS_CONTROLLER} #{OBSERVER_STIMULUS_CONTROLLER}",
|
|
11
|
+
action: "click->#{OBSERVER_STIMULUS_CONTROLLER}#select"
|
|
12
|
+
}.freeze
|
|
13
|
+
|
|
14
|
+
def month(**kwargs)
|
|
15
|
+
html_options = merge_html_options(
|
|
16
|
+
{ classes: theme.resolve(:calendar).fetch(:classes, ""), data: STIMULUS_DATA },
|
|
17
|
+
kwargs
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
template.content_tag(:div, role: "grid", **html_options) do
|
|
21
|
+
template.safe_join(
|
|
22
|
+
[
|
|
23
|
+
template.tag.div(data: { "#{STIMULUS_CONTROLLER}-target" => "daysOfWeek" }),
|
|
24
|
+
template.tag.div(
|
|
25
|
+
role: "rowgroup",
|
|
26
|
+
data: { "#{STIMULUS_CONTROLLER}-target" => "daysOfMonth" }
|
|
27
|
+
)
|
|
28
|
+
]
|
|
29
|
+
)
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module StimulusPlumbers
|
|
4
|
+
module Components
|
|
5
|
+
module Card
|
|
6
|
+
class Renderer < Plumber::Base
|
|
7
|
+
def card(title: nil, **kwargs, &block)
|
|
8
|
+
html_options = merge_html_options(
|
|
9
|
+
{ classes: theme.resolve(:card).fetch(:classes, "") },
|
|
10
|
+
kwargs
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
template.content_tag(:div, **html_options) do
|
|
14
|
+
template.safe_join(
|
|
15
|
+
[
|
|
16
|
+
(template.content_tag(:h2, title) if title.present?),
|
|
17
|
+
template.capture(&block)
|
|
18
|
+
].compact
|
|
19
|
+
)
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def section(title: nil, **kwargs, &block)
|
|
24
|
+
html_options = merge_html_options(
|
|
25
|
+
{ classes: theme.resolve(:card_section).fetch(:classes, "") },
|
|
26
|
+
kwargs
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
template.content_tag(:div, **html_options) do
|
|
30
|
+
template.safe_join(
|
|
31
|
+
[
|
|
32
|
+
(template.content_tag(:h3, title) if title.present?),
|
|
33
|
+
template.capture(&block)
|
|
34
|
+
].compact
|
|
35
|
+
)
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module StimulusPlumbers
|
|
4
|
+
module Components
|
|
5
|
+
module DatePicker
|
|
6
|
+
class Navigation < Plumber::Base
|
|
7
|
+
def render(stimulus_controller:, step:, **kwargs)
|
|
8
|
+
html_options = merge_html_options(
|
|
9
|
+
{ classes: theme.resolve(:calendar_navigation).fetch(:classes, ""), aria: { label: "DatePicker Navigation" } },
|
|
10
|
+
kwargs
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
template.content_tag(:nav, **html_options) do
|
|
14
|
+
template.safe_join(navigators(stimulus_controller, step))
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
private
|
|
19
|
+
|
|
20
|
+
def navigators(stimulus_controller, step)
|
|
21
|
+
[
|
|
22
|
+
Navigator.new(template).render(
|
|
23
|
+
icon_options: { name: "arrow-left" },
|
|
24
|
+
aria: { label: ["previous", step].join(" ").titleize },
|
|
25
|
+
data: { "#{stimulus_controller}-target" => "previous" }
|
|
26
|
+
),
|
|
27
|
+
Navigator.new(template).render(
|
|
28
|
+
aria: { label: "Day" },
|
|
29
|
+
data: { "#{stimulus_controller}-target" => "day" }
|
|
30
|
+
),
|
|
31
|
+
Navigator.new(template).render(
|
|
32
|
+
aria: { label: "Month" },
|
|
33
|
+
data: { "#{stimulus_controller}-target" => "month" }
|
|
34
|
+
),
|
|
35
|
+
Navigator.new(template).render(
|
|
36
|
+
aria: { label: "Year" },
|
|
37
|
+
data: { "#{stimulus_controller}-target" => "year" }
|
|
38
|
+
),
|
|
39
|
+
Navigator.new(template).render(
|
|
40
|
+
icon_options: { name: "arrow-right" },
|
|
41
|
+
aria: { label: ["next", step].join(" ").titleize },
|
|
42
|
+
data: { "#{stimulus_controller}-target" => "next" }
|
|
43
|
+
)
|
|
44
|
+
]
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|