nitro_kit 0.9.0 → 2.0.0.alpha.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 +4 -4
- data/.agents/plugins/marketplace.json +20 -0
- data/CHANGELOG.md +172 -0
- data/LICENSE +28 -0
- data/README.md +22 -15
- data/STYLE_GUIDE.md +425 -0
- data/app/assets/stylesheets/nitro_kit-tailwind-v4.css +38 -0
- data/app/assets/stylesheets/nitro_kit.css +7239 -0
- data/app/components/nitro_kit/accordion.rb +125 -65
- data/app/components/nitro_kit/alert.rb +80 -43
- data/app/components/nitro_kit/app_navigation.rb +307 -0
- data/app/components/nitro_kit/app_shell.rb +227 -0
- data/app/components/nitro_kit/appearance_bootstrap.rb +146 -0
- data/app/components/nitro_kit/appearance_picker.rb +156 -0
- data/app/components/nitro_kit/auth_shell.rb +34 -0
- data/app/components/nitro_kit/avatar.rb +85 -29
- data/app/components/nitro_kit/avatar_stack.rb +155 -9
- data/app/components/nitro_kit/badge.rb +43 -81
- data/app/components/nitro_kit/button.rb +180 -103
- data/app/components/nitro_kit/button_group.rb +63 -11
- data/app/components/nitro_kit/button_to.rb +69 -0
- data/app/components/nitro_kit/card.rb +42 -45
- data/app/components/nitro_kit/checkbox.rb +115 -62
- data/app/components/nitro_kit/checkbox_group.rb +131 -22
- data/app/components/nitro_kit/choice.rb +55 -0
- data/app/components/nitro_kit/combobox.rb +292 -105
- data/app/components/nitro_kit/command_palette.rb +412 -0
- data/app/components/nitro_kit/component.rb +322 -46
- data/app/components/nitro_kit/container.rb +32 -0
- data/app/components/nitro_kit/control_group.rb +45 -0
- data/app/components/nitro_kit/danger_zone.rb +78 -0
- data/app/components/nitro_kit/data_section.rb +106 -0
- data/app/components/nitro_kit/details_table.rb +229 -0
- data/app/components/nitro_kit/dialog.rb +257 -83
- data/app/components/nitro_kit/dropdown.rb +335 -122
- data/app/components/nitro_kit/dropzone.rb +326 -0
- data/app/components/nitro_kit/empty_state.rb +99 -0
- data/app/components/nitro_kit/field.rb +461 -236
- data/app/components/nitro_kit/field_group.rb +15 -8
- data/app/components/nitro_kit/fieldset.rb +37 -39
- data/app/components/nitro_kit/flex.rb +49 -0
- data/app/components/nitro_kit/form_builder.rb +313 -72
- data/app/components/nitro_kit/form_section.rb +79 -0
- data/app/components/nitro_kit/grid.rb +34 -0
- data/app/components/nitro_kit/icon.rb +54 -25
- data/app/components/nitro_kit/input.rb +97 -10
- data/app/components/nitro_kit/label.rb +22 -6
- data/app/components/nitro_kit/layout_options.rb +7 -0
- data/app/components/nitro_kit/page_header.rb +76 -0
- data/app/components/nitro_kit/pagination.rb +355 -69
- data/app/components/nitro_kit/pagination_bar.rb +90 -0
- data/app/components/nitro_kit/progressive_image.rb +181 -0
- data/app/components/nitro_kit/radio_button.rb +77 -55
- data/app/components/nitro_kit/radio_button_group.rb +107 -36
- data/app/components/nitro_kit/responsive_value.rb +94 -0
- data/app/components/nitro_kit/rich_text_area.rb +31 -0
- data/app/components/nitro_kit/select.rb +129 -64
- data/app/components/nitro_kit/settings_layout.rb +152 -0
- data/app/components/nitro_kit/sheet.rb +202 -0
- data/app/components/nitro_kit/stat_grid.rb +75 -0
- data/app/components/nitro_kit/switch.rb +108 -51
- data/app/components/nitro_kit/table.rb +237 -45
- data/app/components/nitro_kit/tabs.rb +168 -63
- data/app/components/nitro_kit/textarea.rb +81 -10
- data/app/components/nitro_kit/toast.rb +211 -80
- data/app/components/nitro_kit/toolbar.rb +75 -0
- data/app/components/nitro_kit/tooltip.rb +235 -34
- data/app/components/nitro_kit/typeset.rb +28 -0
- data/app/javascript/controllers/nk/app_shell_controller.js +224 -0
- data/app/javascript/controllers/nk/appearance_controller.js +73 -0
- data/app/javascript/controllers/nk/avatar_controller.js +15 -0
- data/app/javascript/controllers/nk/button_controller.js +56 -0
- data/app/javascript/controllers/nk/checkable_controller.js +41 -0
- data/app/javascript/controllers/nk/combobox_controller.js +314 -82
- data/app/javascript/controllers/nk/command_palette_controller.js +249 -0
- data/app/javascript/controllers/nk/dialog_controller.js +14 -10
- data/app/javascript/controllers/nk/dropdown_controller.js +106 -58
- data/app/javascript/controllers/nk/dropzone/direct_upload.js +61 -0
- data/app/javascript/controllers/nk/dropzone/file_rules.js +77 -0
- data/app/javascript/controllers/nk/dropzone/form_submit_lock.js +30 -0
- data/app/javascript/controllers/nk/dropzone_controller.js +482 -0
- data/app/javascript/controllers/nk/overlay_position.js +37 -0
- data/app/javascript/controllers/nk/progressive_image_controller.js +116 -0
- data/app/javascript/controllers/nk/tabs_controller.js +135 -23
- data/app/javascript/controllers/nk/toast_controller.js +112 -45
- data/app/javascript/controllers/nk/tooltip_controller.js +12 -47
- data/config/importmap.rb +2 -0
- data/config/locales/en.yml +98 -0
- data/docs/agent_guide.md +139 -0
- data/docs/agent_native_spec.md +422 -0
- data/docs/component_contracts.md +263 -0
- data/docs/customization.md +595 -0
- data/docs/hotwire.md +94 -0
- data/docs/initialization_prompt.md +36 -0
- data/docs/migration_1_to_2.md +318 -0
- data/docs/new_app_strategy.md +22 -0
- data/docs/patterns/application_foundation.md +136 -0
- data/docs/patterns/crud_resource.md +144 -0
- data/docs/patterns/destructive_action.md +105 -0
- data/docs/patterns/flash_and_toast.md +57 -0
- data/docs/patterns/inline_edit.md +59 -0
- data/docs/patterns/queryable_collection.md +187 -0
- data/docs/patterns/resource_form.md +126 -0
- data/docs/rails_conventions.md +95 -0
- data/docs/rails_integration.md +514 -0
- data/lib/generators/nitro_kit/install_generator.rb +48 -0
- data/lib/generators/nitro_kit/upgrade_smoke_tests_generator.rb +67 -0
- data/lib/nitro_kit/engine.rb +20 -0
- data/lib/nitro_kit/installation.rb +671 -0
- data/lib/nitro_kit/migration_inventory.rb +345 -0
- data/lib/nitro_kit/upgrade_smoke_test.rb +403 -0
- data/lib/nitro_kit/version.rb +1 -1
- data/lib/nitro_kit.rb +5 -42
- data/lib/rails/commands/nitro_kit/nitro_kit_command.rb +42 -0
- data/lib/tasks/nitro_kit_tasks.rake +79 -4
- data/plugins/nitro-kit/.codex-plugin/plugin.json +28 -0
- data/plugins/nitro-kit/skills/nitro-kit-hotwire/SKILL.md +67 -0
- data/plugins/nitro-kit/skills/nitro-kit-hotwire/agents/openai.yaml +4 -0
- data/plugins/nitro-kit/skills/nitro-kit-rails/SKILL.md +60 -0
- data/plugins/nitro-kit/skills/nitro-kit-rails/agents/openai.yaml +4 -0
- data/plugins/nitro-kit/skills/nitro-kit-ui/SKILL.md +62 -0
- data/plugins/nitro-kit/skills/nitro-kit-ui/agents/openai.yaml +4 -0
- data/src/stylesheets/nitro_kit/components/accordion.css +119 -0
- data/src/stylesheets/nitro_kit/components/alert.css +82 -0
- data/src/stylesheets/nitro_kit/components/app_navigation.css +156 -0
- data/src/stylesheets/nitro_kit/components/app_shell.css +605 -0
- data/src/stylesheets/nitro_kit/components/appearance_picker.css +117 -0
- data/src/stylesheets/nitro_kit/components/auth_shell.css +8 -0
- data/src/stylesheets/nitro_kit/components/avatar.css +81 -0
- data/src/stylesheets/nitro_kit/components/avatar_stack.css +80 -0
- data/src/stylesheets/nitro_kit/components/badge.css +76 -0
- data/src/stylesheets/nitro_kit/components/button.css +283 -0
- data/src/stylesheets/nitro_kit/components/button_group.css +53 -0
- data/src/stylesheets/nitro_kit/components/button_to.css +6 -0
- data/src/stylesheets/nitro_kit/components/card.css +70 -0
- data/src/stylesheets/nitro_kit/components/checkbox.css +160 -0
- data/src/stylesheets/nitro_kit/components/checkbox_group.css +61 -0
- data/src/stylesheets/nitro_kit/components/combobox.css +148 -0
- data/src/stylesheets/nitro_kit/components/command_palette.css +255 -0
- data/src/stylesheets/nitro_kit/components/container.css +25 -0
- data/src/stylesheets/nitro_kit/components/control_group.css +149 -0
- data/src/stylesheets/nitro_kit/components/danger_zone.css +54 -0
- data/src/stylesheets/nitro_kit/components/data_section.css +40 -0
- data/src/stylesheets/nitro_kit/components/details_table.css +53 -0
- data/src/stylesheets/nitro_kit/components/dialog.css +80 -0
- data/src/stylesheets/nitro_kit/components/dropdown.css +152 -0
- data/src/stylesheets/nitro_kit/components/dropzone.css +198 -0
- data/src/stylesheets/nitro_kit/components/empty_state.css +53 -0
- data/src/stylesheets/nitro_kit/components/field.css +79 -0
- data/src/stylesheets/nitro_kit/components/field_group.css +6 -0
- data/src/stylesheets/nitro_kit/components/fieldset.css +28 -0
- data/src/stylesheets/nitro_kit/components/flex.css +455 -0
- data/src/stylesheets/nitro_kit/components/form_section.css +71 -0
- data/src/stylesheets/nitro_kit/components/grid.css +308 -0
- data/src/stylesheets/nitro_kit/components/icon.css +26 -0
- data/src/stylesheets/nitro_kit/components/input.css +80 -0
- data/src/stylesheets/nitro_kit/components/label.css +9 -0
- data/src/stylesheets/nitro_kit/components/layout.css +440 -0
- data/src/stylesheets/nitro_kit/components/page_header.css +61 -0
- data/src/stylesheets/nitro_kit/components/pagination.css +119 -0
- data/src/stylesheets/nitro_kit/components/pagination_bar.css +41 -0
- data/src/stylesheets/nitro_kit/components/palette.css +113 -0
- data/src/stylesheets/nitro_kit/components/progressive_image.css +139 -0
- data/src/stylesheets/nitro_kit/components/radio_button.css +127 -0
- data/src/stylesheets/nitro_kit/components/radio_button_group.css +143 -0
- data/src/stylesheets/nitro_kit/components/rich_text_area.css +21 -0
- data/src/stylesheets/nitro_kit/components/select.css +70 -0
- data/src/stylesheets/nitro_kit/components/settings_layout.css +100 -0
- data/src/stylesheets/nitro_kit/components/sheet.css +100 -0
- data/src/stylesheets/nitro_kit/components/stat_grid.css +51 -0
- data/src/stylesheets/nitro_kit/components/switch.css +152 -0
- data/src/stylesheets/nitro_kit/components/table.css +116 -0
- data/src/stylesheets/nitro_kit/components/tabs.css +138 -0
- data/src/stylesheets/nitro_kit/components/textarea.css +39 -0
- data/src/stylesheets/nitro_kit/components/toast.css +131 -0
- data/src/stylesheets/nitro_kit/components/toolbar.css +78 -0
- data/src/stylesheets/nitro_kit/components/tooltip.css +124 -0
- data/src/stylesheets/nitro_kit/components/typeset.css +191 -0
- data/src/stylesheets/nitro_kit/layers.css +7 -0
- data/src/stylesheets/nitro_kit/reset.css +192 -0
- data/src/stylesheets/nitro_kit/tokens.css +198 -0
- metadata +182 -51
- data/Rakefile +0 -8
- data/app/assets/tailwind/application.css +0 -49
- data/app/components/nitro_kit/datepicker.rb +0 -9
- data/app/helpers/nitro_kit/accordion_helper.rb +0 -9
- data/app/helpers/nitro_kit/alert_helper.rb +0 -11
- data/app/helpers/nitro_kit/avatar_helper.rb +0 -13
- data/app/helpers/nitro_kit/badge_helper.rb +0 -13
- data/app/helpers/nitro_kit/button_group_helper.rb +0 -9
- data/app/helpers/nitro_kit/button_helper.rb +0 -49
- data/app/helpers/nitro_kit/card_helper.rb +0 -9
- data/app/helpers/nitro_kit/checkbox_helper.rb +0 -50
- data/app/helpers/nitro_kit/combobox_helper.rb +0 -9
- data/app/helpers/nitro_kit/datepicker_helper.rb +0 -9
- data/app/helpers/nitro_kit/dialog_helper.rb +0 -9
- data/app/helpers/nitro_kit/dropdown_helper.rb +0 -9
- data/app/helpers/nitro_kit/field_group_helper.rb +0 -9
- data/app/helpers/nitro_kit/field_helper.rb +0 -9
- data/app/helpers/nitro_kit/fieldset_helper.rb +0 -9
- data/app/helpers/nitro_kit/form_helper.rb +0 -13
- data/app/helpers/nitro_kit/icon_helper.rb +0 -9
- data/app/helpers/nitro_kit/input_helper.rb +0 -37
- data/app/helpers/nitro_kit/label_helper.rb +0 -19
- data/app/helpers/nitro_kit/pagination_helper.rb +0 -46
- data/app/helpers/nitro_kit/radio_button_helper.rb +0 -23
- data/app/helpers/nitro_kit/select_helper.rb +0 -24
- data/app/helpers/nitro_kit/switch_helper.rb +0 -9
- data/app/helpers/nitro_kit/table_helper.rb +0 -9
- data/app/helpers/nitro_kit/tabs_helper.rb +0 -9
- data/app/helpers/nitro_kit/textarea_helper.rb +0 -9
- data/app/helpers/nitro_kit/toast_helper.rb +0 -25
- data/app/helpers/nitro_kit/tooltip_helper.rb +0 -9
- data/app/javascript/controllers/nk/accordion_controller.js +0 -18
- data/app/javascript/controllers/nk/datepicker_controller.js +0 -5
- data/app/javascript/controllers/nk/switch_controller.js +0 -32
- data/lib/generators/nitro_kit/component_generator.rb +0 -84
- data/lib/nitro_kit/schema_builder.rb +0 -129
- data/lib/nitro_kit/variants.rb +0 -21
|
@@ -2,105 +2,391 @@
|
|
|
2
2
|
|
|
3
3
|
module NitroKit
|
|
4
4
|
class Pagination < Component
|
|
5
|
-
|
|
5
|
+
Item = Data.define(:kind, :button, :current_page, :content, :current, :label)
|
|
6
|
+
CurrentPage = Data.define(:text, :attributes)
|
|
7
|
+
private_constant :CurrentPage
|
|
8
|
+
ITEM_KINDS = %i[previous page ellipsis next].freeze
|
|
9
|
+
UNSET = Object.new.freeze
|
|
10
|
+
private_constant :UNSET
|
|
11
|
+
|
|
12
|
+
def initialize(
|
|
13
|
+
label: I18n.t("nitro_kit.pagination.label"),
|
|
14
|
+
pagy: nil,
|
|
15
|
+
page_url: nil,
|
|
16
|
+
id: nil,
|
|
17
|
+
html: {},
|
|
18
|
+
aria: {},
|
|
19
|
+
data: {},
|
|
20
|
+
desperately_need_a_class: nil
|
|
21
|
+
)
|
|
22
|
+
validate_label!(label, name: "label")
|
|
23
|
+
validate_aria!(aria, reserved: %w[label])
|
|
24
|
+
validate_pagy!(pagy, page_url:)
|
|
25
|
+
@pagy = pagy
|
|
26
|
+
@page_url = page_url
|
|
27
|
+
@items = []
|
|
28
|
+
|
|
6
29
|
super(
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
30
|
+
component: :pagination,
|
|
31
|
+
attributes: { id: },
|
|
32
|
+
html:,
|
|
33
|
+
aria: aria.merge(label:),
|
|
34
|
+
data:,
|
|
35
|
+
desperately_need_a_class:
|
|
10
36
|
)
|
|
11
37
|
end
|
|
12
38
|
|
|
13
|
-
def view_template
|
|
14
|
-
|
|
15
|
-
|
|
39
|
+
def view_template(&content)
|
|
40
|
+
if @pagy && content
|
|
41
|
+
raise ArgumentError, "Pagination accepts a Pagy object or a block, not both"
|
|
16
42
|
end
|
|
17
|
-
end
|
|
18
43
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
plain("Previous")
|
|
27
|
-
end
|
|
44
|
+
append_pagy_items if @pagy
|
|
45
|
+
yield self if content
|
|
46
|
+
validate_collection!
|
|
47
|
+
|
|
48
|
+
nav(**root_attributes) do
|
|
49
|
+
ol(**slot_attributes(:list)) do
|
|
50
|
+
@items.each { |item| render_item(item) }
|
|
28
51
|
end
|
|
29
52
|
end
|
|
30
53
|
end
|
|
31
54
|
|
|
32
|
-
def
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
55
|
+
def prev(
|
|
56
|
+
text = UNSET,
|
|
57
|
+
href: nil,
|
|
58
|
+
icon: "arrow-left",
|
|
59
|
+
disabled: false,
|
|
60
|
+
id: nil,
|
|
61
|
+
html: {},
|
|
62
|
+
aria: {},
|
|
63
|
+
data: {},
|
|
64
|
+
desperately_need_a_class: nil,
|
|
65
|
+
&content
|
|
66
|
+
)
|
|
67
|
+
text = default_text(text, I18n.t("nitro_kit.pagination.previous"), content)
|
|
68
|
+
validate_boolean!(:disabled, disabled)
|
|
69
|
+
append_navigation_item(
|
|
70
|
+
:previous,
|
|
71
|
+
text:,
|
|
72
|
+
href:,
|
|
73
|
+
icon:,
|
|
74
|
+
disabled: disabled || missing_href?(href),
|
|
75
|
+
default_label: I18n.t("nitro_kit.pagination.previous_page"),
|
|
76
|
+
id:,
|
|
77
|
+
html:,
|
|
78
|
+
aria:,
|
|
79
|
+
data:,
|
|
80
|
+
desperately_need_a_class:,
|
|
81
|
+
&content
|
|
82
|
+
)
|
|
43
83
|
end
|
|
44
84
|
|
|
45
|
-
def page(
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
85
|
+
def page(
|
|
86
|
+
text = nil,
|
|
87
|
+
href: nil,
|
|
88
|
+
current: false,
|
|
89
|
+
id: nil,
|
|
90
|
+
html: {},
|
|
91
|
+
aria: {},
|
|
92
|
+
data: {},
|
|
93
|
+
desperately_need_a_class: nil,
|
|
94
|
+
&content
|
|
95
|
+
)
|
|
96
|
+
validate_boolean!(:current, current)
|
|
97
|
+
if !text.nil? && content
|
|
98
|
+
raise ArgumentError, "Pagination page accepts text or a block, not both"
|
|
99
|
+
end
|
|
100
|
+
validate_content!(text, content, name: "page label")
|
|
101
|
+
if missing_href?(href) && !current
|
|
102
|
+
raise ArgumentError, "Pagination page href is required unless the page is current"
|
|
103
|
+
end
|
|
104
|
+
validate_aria!(aria, reserved: %w[current disabled])
|
|
105
|
+
|
|
106
|
+
item_aria = current ? aria.merge(current: "page") : aria
|
|
107
|
+
button = nil
|
|
108
|
+
current_page = nil
|
|
109
|
+
|
|
110
|
+
if current && missing_href?(href)
|
|
111
|
+
current_page = CurrentPage.new(
|
|
112
|
+
text:,
|
|
113
|
+
attributes: slot_attributes(
|
|
114
|
+
:current,
|
|
115
|
+
attributes: { id: }.compact,
|
|
116
|
+
html:,
|
|
117
|
+
aria: item_aria,
|
|
118
|
+
data:,
|
|
119
|
+
desperately_need_a_class:
|
|
55
120
|
)
|
|
56
|
-
)
|
|
57
|
-
|
|
58
|
-
|
|
121
|
+
)
|
|
122
|
+
else
|
|
123
|
+
button = pagination_button(
|
|
124
|
+
text,
|
|
125
|
+
href:,
|
|
126
|
+
disabled: false,
|
|
127
|
+
id:,
|
|
128
|
+
html:,
|
|
129
|
+
aria: item_aria,
|
|
130
|
+
data:,
|
|
131
|
+
desperately_need_a_class:
|
|
132
|
+
)
|
|
59
133
|
end
|
|
134
|
+
|
|
135
|
+
append(Item.new(kind: :page, button:, current_page:, content:, current:, label: nil))
|
|
60
136
|
end
|
|
61
137
|
|
|
62
|
-
def ellipsis(
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
138
|
+
def ellipsis(label: I18n.t("nitro_kit.pagination.ellipsis"))
|
|
139
|
+
validate_label!(label, name: "ellipsis label")
|
|
140
|
+
|
|
141
|
+
append(Item.new(kind: :ellipsis, button: nil, current_page: nil, content: nil, current: false, label:))
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
def next(
|
|
145
|
+
text = UNSET,
|
|
146
|
+
href: nil,
|
|
147
|
+
icon: "arrow-right",
|
|
148
|
+
disabled: false,
|
|
149
|
+
id: nil,
|
|
150
|
+
html: {},
|
|
151
|
+
aria: {},
|
|
152
|
+
data: {},
|
|
153
|
+
desperately_need_a_class: nil,
|
|
154
|
+
&content
|
|
155
|
+
)
|
|
156
|
+
text = default_text(text, I18n.t("nitro_kit.pagination.next"), content)
|
|
157
|
+
validate_boolean!(:disabled, disabled)
|
|
158
|
+
append_navigation_item(
|
|
159
|
+
:next,
|
|
160
|
+
text:,
|
|
161
|
+
href:,
|
|
162
|
+
icon_end: icon,
|
|
163
|
+
disabled: disabled || missing_href?(href),
|
|
164
|
+
default_label: I18n.t("nitro_kit.pagination.next_page"),
|
|
165
|
+
id:,
|
|
166
|
+
html:,
|
|
167
|
+
aria:,
|
|
168
|
+
data:,
|
|
169
|
+
desperately_need_a_class:,
|
|
170
|
+
&content
|
|
171
|
+
)
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
private
|
|
175
|
+
|
|
176
|
+
def append_pagy_items
|
|
177
|
+
previous = @pagy.previous
|
|
178
|
+
prev(href: previous && pagy_page_url(previous))
|
|
179
|
+
|
|
180
|
+
pagy_series.each do |item|
|
|
181
|
+
case item
|
|
182
|
+
when Integer
|
|
183
|
+
page(item, href: pagy_page_url(item))
|
|
184
|
+
when String
|
|
185
|
+
page(item, current: true)
|
|
186
|
+
when :gap
|
|
187
|
+
ellipsis
|
|
188
|
+
else
|
|
189
|
+
raise ArgumentError, "Pagination received an unknown Pagy series item: #{item.inspect}"
|
|
75
190
|
end
|
|
76
191
|
end
|
|
192
|
+
|
|
193
|
+
following = @pagy.next
|
|
194
|
+
self.next(href: following && pagy_page_url(following))
|
|
77
195
|
end
|
|
78
196
|
|
|
79
|
-
|
|
197
|
+
# Pagy's `series` is protected; `data_hash` is its public accessor for it.
|
|
198
|
+
def pagy_series
|
|
199
|
+
@pagy.data_hash(data_keys: [ :series ]).fetch(:series)
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
def pagy_page_url(page)
|
|
203
|
+
return @page_url.call(page) if @page_url
|
|
204
|
+
|
|
205
|
+
@pagy.page_url(page)
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
def validate_pagy!(pagy, page_url:)
|
|
209
|
+
if page_url && !page_url.respond_to?(:call)
|
|
210
|
+
raise ArgumentError, "Pagination page_url must be callable"
|
|
211
|
+
end
|
|
212
|
+
return unless pagy
|
|
213
|
+
|
|
214
|
+
required = %i[previous next data_hash]
|
|
215
|
+
required << :page_url unless page_url
|
|
216
|
+
missing = required.reject { |method| pagy.respond_to?(method) }
|
|
217
|
+
return if missing.empty?
|
|
218
|
+
|
|
219
|
+
raise ArgumentError,
|
|
220
|
+
"Pagination pagy must respond to #{missing.join(", ")}"
|
|
221
|
+
end
|
|
222
|
+
|
|
223
|
+
def append_navigation_item(
|
|
224
|
+
kind,
|
|
225
|
+
text:,
|
|
226
|
+
href:,
|
|
227
|
+
icon: nil,
|
|
228
|
+
icon_end: nil,
|
|
229
|
+
disabled:,
|
|
230
|
+
default_label:,
|
|
231
|
+
id:,
|
|
232
|
+
html:,
|
|
233
|
+
aria:,
|
|
234
|
+
data:,
|
|
235
|
+
desperately_need_a_class:,
|
|
236
|
+
&content
|
|
237
|
+
)
|
|
238
|
+
validate_content!(text, content, icon || icon_end, name: "#{kind} label or icon")
|
|
239
|
+
validate_aria!(aria, reserved: %w[disabled current])
|
|
240
|
+
item_aria = accessible_item_aria(aria, text:, content:, default_label:)
|
|
241
|
+
|
|
242
|
+
button = pagination_button(
|
|
243
|
+
text,
|
|
244
|
+
href:,
|
|
245
|
+
icon:,
|
|
246
|
+
icon_end:,
|
|
247
|
+
disabled:,
|
|
248
|
+
id:,
|
|
249
|
+
html:,
|
|
250
|
+
aria: item_aria,
|
|
251
|
+
data:,
|
|
252
|
+
desperately_need_a_class:
|
|
253
|
+
)
|
|
80
254
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
255
|
+
append(Item.new(kind:, button:, current_page: nil, content:, current: false, label: nil))
|
|
256
|
+
end
|
|
257
|
+
|
|
258
|
+
def pagination_button(
|
|
259
|
+
text,
|
|
260
|
+
href:,
|
|
261
|
+
disabled:,
|
|
262
|
+
icon: nil,
|
|
263
|
+
icon_end: nil,
|
|
264
|
+
id:,
|
|
265
|
+
html:,
|
|
266
|
+
aria:,
|
|
267
|
+
data:,
|
|
268
|
+
desperately_need_a_class:
|
|
269
|
+
)
|
|
270
|
+
Button.new(
|
|
271
|
+
text,
|
|
272
|
+
href: missing_href?(href) ? "#" : href,
|
|
273
|
+
variant: :ghost,
|
|
274
|
+
size: :sm,
|
|
275
|
+
icon:,
|
|
276
|
+
icon_end:,
|
|
277
|
+
id:,
|
|
278
|
+
disabled:,
|
|
279
|
+
html:,
|
|
280
|
+
aria:,
|
|
281
|
+
data:,
|
|
282
|
+
desperately_need_a_class:
|
|
283
|
+
)
|
|
284
|
+
end
|
|
285
|
+
|
|
286
|
+
def append(item)
|
|
287
|
+
validate_choice!(:item_kind, item.kind, ITEM_KINDS)
|
|
288
|
+
|
|
289
|
+
case item.kind
|
|
290
|
+
when :previous
|
|
291
|
+
raise ArgumentError, "Pagination accepts at most one previous item" if @items.any? { |entry| entry.kind == :previous }
|
|
292
|
+
raise ArgumentError, "Pagination previous must be the first item" if @items.any?
|
|
293
|
+
when :next
|
|
294
|
+
raise ArgumentError, "Pagination accepts at most one next item" if @items.any? { |entry| entry.kind == :next }
|
|
295
|
+
else
|
|
296
|
+
raise ArgumentError, "Pagination next must be the last item" if @items.any? { |entry| entry.kind == :next }
|
|
297
|
+
end
|
|
298
|
+
|
|
299
|
+
if item.current && @items.any?(&:current)
|
|
300
|
+
raise ArgumentError, "Pagination accepts at most one current page"
|
|
301
|
+
end
|
|
302
|
+
|
|
303
|
+
@items << item
|
|
304
|
+
nil
|
|
305
|
+
end
|
|
306
|
+
|
|
307
|
+
def render_item(item)
|
|
308
|
+
li(
|
|
309
|
+
**slot_attributes(
|
|
310
|
+
:item,
|
|
311
|
+
attributes: { data: { kind: item.kind } }
|
|
88
312
|
)
|
|
89
313
|
) do
|
|
90
|
-
|
|
314
|
+
if item.kind == :ellipsis
|
|
315
|
+
render_ellipsis(item)
|
|
316
|
+
elsif item.current_page
|
|
317
|
+
render_current_page(item)
|
|
318
|
+
else
|
|
319
|
+
render_in_slot(item.button, item.kind, &item.content)
|
|
320
|
+
end
|
|
321
|
+
end
|
|
322
|
+
end
|
|
323
|
+
|
|
324
|
+
def render_current_page(item)
|
|
325
|
+
span(**item.current_page.attributes) do
|
|
326
|
+
span(**slot_attributes(:current_label)) do
|
|
327
|
+
item.content ? render(item.content) : plain(item.current_page.text.to_s)
|
|
328
|
+
end
|
|
329
|
+
end
|
|
330
|
+
end
|
|
331
|
+
|
|
332
|
+
def render_ellipsis(item)
|
|
333
|
+
span(**slot_attributes(:ellipsis, aria: { hidden: true })) { "…" }
|
|
334
|
+
span(**slot_attributes(:ellipsis_label)) { plain(item.label) }
|
|
335
|
+
end
|
|
336
|
+
|
|
337
|
+
def validate_collection!
|
|
338
|
+
raise ArgumentError, "Pagination requires at least one item" if @items.empty?
|
|
339
|
+
|
|
340
|
+
@items.each_cons(2) do |left, right|
|
|
341
|
+
if left.kind == :ellipsis && right.kind == :ellipsis
|
|
342
|
+
raise ArgumentError, "Pagination cannot contain consecutive ellipses"
|
|
343
|
+
end
|
|
344
|
+
end
|
|
345
|
+
|
|
346
|
+
if @items.any? { |item| item.kind == :ellipsis } && @items.none? { |item| item.kind == :page }
|
|
347
|
+
raise ArgumentError, "Pagination ellipsis requires at least one page"
|
|
91
348
|
end
|
|
92
349
|
end
|
|
93
350
|
|
|
94
|
-
def
|
|
95
|
-
|
|
351
|
+
def validate_content!(text, content, icon = nil, name:)
|
|
352
|
+
return if content || icon || (!text.nil? && !text.to_s.strip.empty?)
|
|
353
|
+
|
|
354
|
+
raise ArgumentError, "Pagination #{name} is required"
|
|
96
355
|
end
|
|
97
356
|
|
|
98
|
-
def
|
|
99
|
-
|
|
357
|
+
def validate_label!(label, name:)
|
|
358
|
+
return if label.is_a?(String) && !label.strip.empty?
|
|
359
|
+
|
|
360
|
+
raise ArgumentError, "Pagination #{name} must be a non-blank String"
|
|
361
|
+
end
|
|
362
|
+
|
|
363
|
+
# An omitted label falls back to the default text, unless a content block
|
|
364
|
+
# supplies its own label.
|
|
365
|
+
def default_text(text, default, content)
|
|
366
|
+
return text unless text.equal?(UNSET)
|
|
367
|
+
|
|
368
|
+
content ? nil : default
|
|
369
|
+
end
|
|
370
|
+
|
|
371
|
+
def validate_aria!(aria, reserved:)
|
|
372
|
+
raise ArgumentError, "aria must be a Hash" unless aria.is_a?(Hash)
|
|
373
|
+
|
|
374
|
+
normalized_keys = aria.keys.map { |key| key.to_s.downcase.tr("_", "-") }
|
|
375
|
+
collision = normalized_keys.find { |key| reserved.include?(key) }
|
|
376
|
+
return unless collision
|
|
377
|
+
|
|
378
|
+
raise ArgumentError, "Pagination owns aria-#{collision}"
|
|
379
|
+
end
|
|
380
|
+
|
|
381
|
+
def accessible_item_aria(aria, text:, content:, default_label:)
|
|
382
|
+
return aria if content || (!text.nil? && !text.to_s.strip.empty?)
|
|
383
|
+
return aria if aria.any? { |key, value| key.to_s.tr("_", "-") == "label" && value.to_s.strip.present? }
|
|
384
|
+
|
|
385
|
+
aria.merge(label: default_label)
|
|
100
386
|
end
|
|
101
387
|
|
|
102
|
-
def
|
|
103
|
-
|
|
388
|
+
def missing_href?(href)
|
|
389
|
+
href.nil? || (href.respond_to?(:empty?) && href.empty?)
|
|
104
390
|
end
|
|
105
391
|
end
|
|
106
392
|
end
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module NitroKit
|
|
4
|
+
class PaginationBar < Component
|
|
5
|
+
Summary = Data.define(:text, :content, :html, :aria, :data, :css_class)
|
|
6
|
+
Child = Data.define(:component, :content)
|
|
7
|
+
|
|
8
|
+
def initialize(id: nil, html: {}, aria: {}, data: {}, desperately_need_a_class: nil)
|
|
9
|
+
@summary = nil
|
|
10
|
+
@pagination = nil
|
|
11
|
+
|
|
12
|
+
super(
|
|
13
|
+
component: :pagination_bar,
|
|
14
|
+
attributes: { id: }.compact,
|
|
15
|
+
html:,
|
|
16
|
+
aria:,
|
|
17
|
+
data:,
|
|
18
|
+
desperately_need_a_class:
|
|
19
|
+
)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def view_template
|
|
23
|
+
yield self if block_given?
|
|
24
|
+
validate_regions!
|
|
25
|
+
|
|
26
|
+
div(**root_attributes) do
|
|
27
|
+
render_summary if @summary
|
|
28
|
+
render_in_slot(@pagination.component, :pagination, &@pagination.content)
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
alias :html_summary :summary
|
|
33
|
+
|
|
34
|
+
def summary(text = nil, html: {}, aria: {}, data: {}, desperately_need_a_class: nil, &block)
|
|
35
|
+
raise ArgumentError, "PaginationBar accepts at most one summary" if @summary
|
|
36
|
+
raise ArgumentError, "PaginationBar summary accepts text or a block, not both" if !text.nil? && block
|
|
37
|
+
unless block || (text.is_a?(String) && !text.strip.empty?)
|
|
38
|
+
raise ArgumentError, "PaginationBar summary requires non-blank text or a block"
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
@summary = Summary.new(
|
|
42
|
+
text:,
|
|
43
|
+
content: block,
|
|
44
|
+
html:,
|
|
45
|
+
aria:,
|
|
46
|
+
data:,
|
|
47
|
+
css_class: desperately_need_a_class
|
|
48
|
+
)
|
|
49
|
+
nil
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def pagination(component, &content)
|
|
53
|
+
raise ArgumentError, "PaginationBar accepts exactly one Pagination" if @pagination
|
|
54
|
+
unless component.is_a?(NitroKit::Pagination)
|
|
55
|
+
raise ArgumentError, "PaginationBar pagination must be a NitroKit::Pagination"
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
@pagination = Child.new(component:, content:)
|
|
59
|
+
nil
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
private
|
|
63
|
+
|
|
64
|
+
def render_summary
|
|
65
|
+
p(
|
|
66
|
+
**slot_attributes(
|
|
67
|
+
:summary,
|
|
68
|
+
html: @summary.html,
|
|
69
|
+
aria: summary_aria,
|
|
70
|
+
data: @summary.data,
|
|
71
|
+
desperately_need_a_class: @summary.css_class
|
|
72
|
+
)
|
|
73
|
+
) { text_or_block(@summary.text, &@summary.content) }
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# Turbo Frame page changes replace the summary in place, so it announces
|
|
77
|
+
# politely unless the caller declares its own live region behavior.
|
|
78
|
+
def summary_aria
|
|
79
|
+
return @summary.aria if @summary.aria.keys.any? { |key| key.to_s.tr("_", "-") == "live" }
|
|
80
|
+
|
|
81
|
+
@summary.aria.merge(live: "polite")
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def validate_regions!
|
|
85
|
+
return if @pagination
|
|
86
|
+
|
|
87
|
+
raise ArgumentError, "PaginationBar requires one Pagination"
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
end
|