trek 2.1.0 → 2.3.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 (142) hide show
  1. checksums.yaml +4 -4
  2. data/.slim-lint.yml +0 -3
  3. data/AGENTS.md +8 -1
  4. data/CHANGELOG.md +75 -0
  5. data/Gemfile +4 -0
  6. data/Gemfile.lock +20 -3
  7. data/Makefile +11 -9
  8. data/README.md +1 -1
  9. data/app/assets/icons/trek/audit.svg +1 -0
  10. data/app/assets/icons/trek/chevron-down.svg +1 -0
  11. data/app/assets/icons/trek/chevron-up.svg +1 -0
  12. data/app/assets/icons/trek/external.svg +1 -0
  13. data/app/assets/icons/trek/print.svg +1 -0
  14. data/app/assets/icons/trek/theme-auto.svg +1 -0
  15. data/app/assets/icons/trek/theme-dark.svg +1 -0
  16. data/app/assets/icons/trek/theme-light.svg +1 -0
  17. data/app/assets/stylesheets/trek/base.css +13 -0
  18. data/app/components/trek/button_component/button_component.css +9 -0
  19. data/app/components/trek/button_component.rb +13 -1
  20. data/app/components/trek/dashboard/analytics_component/analytics_component.html.slim +2 -0
  21. data/app/components/trek/dashboard/analytics_component.rb +1 -0
  22. data/app/components/trek/dashboard/chart_component/chart_component.css +192 -0
  23. data/app/components/trek/dashboard/chart_component/chart_component.html.slim +6 -0
  24. data/app/components/trek/dashboard/chart_component/chart_component.js +1 -0
  25. data/app/components/trek/dashboard/chart_component.rb +27 -0
  26. data/app/components/trek/dashboard/tile_component/tile_component.css +40 -0
  27. data/app/components/trek/dashboard/tile_component/tile_component.html.slim +14 -4
  28. data/app/components/trek/dashboard/tile_component.rb +20 -2
  29. data/app/components/trek/dialog_component/dialog_component.html.slim +6 -2
  30. data/app/components/trek/dialog_component/dialog_component.js +62 -2
  31. data/app/components/trek/dropdown_component/dropdown_component.css +66 -0
  32. data/app/components/trek/dropdown_component/dropdown_component.html.slim +31 -0
  33. data/app/components/trek/dropdown_component/dropdown_component.js +114 -0
  34. data/app/components/trek/dropdown_component.rb +29 -0
  35. data/app/components/trek/form/actions_component/actions_component.css +6 -7
  36. data/app/components/trek/form/content_editor_component/content_editor_component.html.slim +10 -12
  37. data/app/components/trek/form/content_editor_component/content_editor_component.js +4 -0
  38. data/app/components/trek/form/group_component/group_component.css +34 -0
  39. data/app/components/trek/header_component/header_component.css +10 -4
  40. data/app/components/trek/header_component/header_component.html.slim +17 -10
  41. data/app/components/trek/header_component/header_component.js +29 -5
  42. data/app/components/trek/header_component.rb +20 -3
  43. data/app/components/trek/layout_component/layout_component.css +20 -0
  44. data/app/components/trek/layout_component/layout_component.html.slim +16 -0
  45. data/app/components/trek/layout_component/layout_component.js +35 -6
  46. data/app/components/trek/menu_component/menu_component.css +6 -0
  47. data/app/components/trek/menu_component.rb +29 -4
  48. data/app/components/trek/toaster/toast_component.rb +1 -1
  49. data/app/controllers/concerns/trek/authorization.rb +12 -0
  50. data/app/formatters/prosemirror_to_html/global_id_to_links_formatter.rb +3 -1
  51. data/app/helpers/trek/page_path_helper.rb +4 -3
  52. data/app/helpers/trek/theme_helper.rb +16 -1
  53. data/app/models/concerns/trek/phonable.rb +15 -3
  54. data/app/models/concerns/trek/sectionable.rb +5 -0
  55. data/app/models/concerns/trek/users/validations.rb +1 -1
  56. data/app/models/prosemirror.rb +5 -2
  57. data/app/uploaders/audio_uploader.rb +11 -0
  58. data/app/uploaders/record_image_uploader.rb +3 -0
  59. data/app/views/trek/panels/images/_form.html.slim +1 -1
  60. data/app/views/trek/panels/images/create.turbo_stream.slim +3 -3
  61. data/app/views/trek/panels/images/new.html.slim +1 -1
  62. data/app/views/trek/panels/links/_form.html.slim +1 -1
  63. data/app/views/trek/panels/links/create.turbo_stream.slim +3 -3
  64. data/app/views/trek/panels/links/new.html.slim +1 -1
  65. data/app/views/trek/panels/prompts/_form.html.slim +1 -1
  66. data/app/views/trek/panels/prompts/create.turbo_stream.slim +3 -3
  67. data/app/views/trek/panels/prompts/new.html.slim +1 -1
  68. data/config/locales/trek.en.yml +3 -2
  69. data/config/locales/trek.fr.yml +1 -0
  70. data/docs/customize.md +18 -2
  71. data/docs/deploy.md +7 -2
  72. data/docs/getting-started.md +1 -1
  73. data/docs/index.md +1 -1
  74. data/docs/reference/components.md +15 -0
  75. data/docs/reference/concerns/phonable.md +27 -1
  76. data/docs/reference/environment-variables.md +2 -0
  77. data/docs/reference/forms.md +2 -0
  78. data/docs/reference/generators/install.md +4 -1
  79. data/docs/reference/icons.md +1 -1
  80. data/docs/reference/models/tag.md +1 -1
  81. data/docs/reference/models/user.md +4 -2
  82. data/docs/reference/uploaders.md +31 -6
  83. data/docs/what-is-trek.md +1 -1
  84. data/lib/generators/trek/install/admin/routes_generator.rb +7 -1
  85. data/lib/generators/trek/install/appsignal_generator.rb +102 -0
  86. data/lib/generators/trek/install/model_specs_generator.rb +64 -0
  87. data/lib/generators/trek/install/postmark_generator.rb +3 -0
  88. data/lib/generators/trek/install/ruby_generator.rb +1 -1
  89. data/lib/generators/trek/install/scalingo_generator.rb +1 -1
  90. data/lib/generators/trek/install/uploaders_generator.rb +2 -0
  91. data/lib/generators/trek/install_generator.rb +5 -0
  92. data/lib/generators/trek/taxonomies_generator.rb +7 -0
  93. data/lib/generators/trek/templates/.gitlab-ci.yml +1 -1
  94. data/lib/generators/trek/templates/config/appsignal.rb.tt +25 -0
  95. data/lib/generators/trek/templates/controllers/admin/uploads_controller.rb +4 -0
  96. data/lib/generators/trek/templates/controllers/admin/users_controller.rb +4 -1
  97. data/lib/generators/trek/templates/controllers/concerns/appsignal_context.rb +37 -0
  98. data/lib/generators/trek/templates/db/seeds/setup/fragments.rb +3 -0
  99. data/lib/generators/trek/templates/db/seeds/setup/users.rb +6 -0
  100. data/lib/generators/trek/templates/db/seeds/test/cases/page_tree.rb +5 -0
  101. data/lib/generators/trek/templates/initializers/appsignal_context.rb +5 -0
  102. data/lib/generators/trek/templates/locales/scaffold/model.en.yml.tt +1 -1
  103. data/lib/generators/trek/templates/migrations/fragments.rb.erb +1 -1
  104. data/lib/generators/trek/templates/migrations/menu_nodes.rb.erb +0 -1
  105. data/lib/generators/trek/templates/migrations/tag_categories.rb.erb +1 -1
  106. data/lib/generators/trek/templates/migrations/taggings.rb.erb +1 -1
  107. data/lib/generators/trek/templates/migrations/users.rb.erb +2 -1
  108. data/lib/generators/trek/templates/models/tag.rb +6 -1
  109. data/lib/generators/trek/templates/policies/admin/tag_policy.rb +1 -1
  110. data/lib/generators/trek/templates/policies/external_link_policy.rb +1 -1
  111. data/lib/generators/trek/templates/spec/fixtures/files/image.png +0 -0
  112. data/lib/generators/trek/templates/spec/fixtures/files/sound.mp3 +0 -0
  113. data/lib/generators/trek/templates/spec/models/page_spec.rb +24 -0
  114. data/lib/generators/trek/templates/spec/models/user_spec.rb +76 -0
  115. data/lib/generators/trek/templates/spec/policies/admin/external_link_policy_spec.rb +17 -0
  116. data/lib/generators/trek/templates/spec/policies/admin/fragment_policy_spec.rb +33 -0
  117. data/lib/generators/trek/templates/spec/policies/admin/page_policy_spec.rb +39 -0
  118. data/lib/generators/trek/templates/spec/policies/admin/preview_policy_spec.rb +9 -0
  119. data/lib/generators/trek/templates/spec/policies/admin/tag_category_policy_spec.rb +31 -0
  120. data/lib/generators/trek/templates/spec/policies/admin/tag_policy_spec.rb +31 -0
  121. data/lib/generators/trek/templates/spec/policies/admin/user_policy_spec.rb +58 -0
  122. data/lib/generators/trek/templates/spec/requests/admin/fragments_spec.rb +50 -0
  123. data/lib/generators/trek/templates/spec/requests/admin/pages_spec.rb +47 -0
  124. data/lib/generators/trek/templates/spec/requests/admin/uploads_spec.rb +22 -0
  125. data/lib/generators/trek/templates/spec/requests/admin/user_password_resets_spec.rb +71 -0
  126. data/lib/generators/trek/templates/spec/requests/admin/user_sessions_spec.rb +45 -0
  127. data/lib/generators/trek/templates/spec/requests/admin/users_spec.rb +99 -0
  128. data/lib/generators/trek/templates/views/admin/user_password_resets/edit.html.slim +1 -1
  129. data/lib/generators/trek/templates/views/admin/user_password_resets/new.html.slim +1 -1
  130. data/lib/generators/trek/templates/views/admin/user_sessions/new.html.slim +2 -2
  131. data/lib/generators/trek/templates/views/admin/users/theme.turbo_stream.slim +2 -0
  132. data/lib/generators/trek/upgrade/v2_generator.rb +3 -0
  133. data/lib/tasks/trek/attachments.rake +18 -0
  134. data/lib/trek/cached_attachments.rb +53 -0
  135. data/lib/trek/saved_attachments_reload.rb +44 -0
  136. data/lib/trek/version.rb +1 -1
  137. data/lib/trek.rb +3 -0
  138. data/package.json +1 -1
  139. data/postcss.config.js +36 -0
  140. data/trek.gemspec +5 -0
  141. metadata +76 -3
  142. data/app/assets/icons/trek/theme.svg +0 -11
@@ -0,0 +1,192 @@
1
+ .root {
2
+ border: 0.1rem solid;
3
+ border-radius: 0.8rem;
4
+ font-weight: var(--semibold);
5
+ margin-bottom: 2rem;
6
+ max-width: 83.7rem;
7
+ padding: 2rem;
8
+ transition: background-color 0.2s ease-in-out, border-color 0.2s ease-in-out,
9
+ color 0.2s ease-in-out;
10
+ }
11
+
12
+ .root:last-child {
13
+ margin-bottom: 0;
14
+ }
15
+
16
+ .title {
17
+ color: var(--mint-12);
18
+ font-size: 1.6rem;
19
+ font-weight: var(--semibold);
20
+ margin: 0 0 0.5rem;
21
+ }
22
+
23
+ .data {
24
+ margin-bottom: 2rem;
25
+ }
26
+
27
+ .info {
28
+ color: var(--slate-11);
29
+ font-size: 1.4rem;
30
+ font-weight: var(--medium);
31
+ padding-bottom: 1.6rem;
32
+ }
33
+
34
+ .root.color-amber {
35
+ --icon-color: var(--amber-11);
36
+
37
+ background-color: var(--amber-2);
38
+ border-color: var(--amber-5);
39
+ color: var(--amber-12);
40
+ }
41
+
42
+ .root.color-amber:hover {
43
+ --icon-color: var(--amber-12);
44
+
45
+ background-color: var(--amber-3);
46
+ border-color: var(--amber-6);
47
+ }
48
+
49
+ .root.color-amber:active {
50
+ background-color: var(--amber-5);
51
+ border-color: var(--amber-7);
52
+ }
53
+
54
+ .root.color-lime {
55
+ --icon-color: var(--lime-11);
56
+
57
+ background-color: var(--lime-2);
58
+ border-color: var(--lime-5);
59
+ color: var(--lime-12);
60
+ }
61
+
62
+ .root.color-lime:hover {
63
+ --icon-color: var(--lime-12);
64
+
65
+ background-color: var(--lime-3);
66
+ border-color: var(--lime-6);
67
+ }
68
+
69
+ .root.color-lime:active {
70
+ background-color: var(--lime-5);
71
+ border-color: var(--lime-7);
72
+ }
73
+
74
+ .root.color-mint {
75
+ --icon-color: var(--mint-11);
76
+
77
+ background-color: var(--mint-2);
78
+ border-color: var(--mint-5);
79
+ color: var(--mint-12);
80
+ }
81
+
82
+ .root.color-mint:hover {
83
+ --icon-color: var(--mint-12);
84
+
85
+ background-color: var(--mint-3);
86
+ border-color: var(--mint-6);
87
+ }
88
+
89
+ .root.color-mint:active {
90
+ background-color: var(--mint-5);
91
+ border-color: var(--mint-7);
92
+ }
93
+
94
+ .root.color-purple {
95
+ --icon-color: var(--purple-11);
96
+
97
+ background-color: var(--purple-2);
98
+ border-color: var(--purple-5);
99
+ color: var(--purple-12);
100
+ }
101
+
102
+ .root.color-purple:hover {
103
+ --icon-color: var(--purple-12);
104
+
105
+ background-color: var(--purple-3);
106
+ border-color: var(--purple-6);
107
+ }
108
+
109
+ .root.color-purple:active {
110
+ background-color: var(--purple-5);
111
+ border-color: var(--purple-7);
112
+ }
113
+
114
+ .root.color-sky {
115
+ --icon-color: var(--sky-11);
116
+
117
+ background-color: var(--sky-2);
118
+ border-color: var(--sky-5);
119
+ color: var(--sky-12);
120
+ }
121
+
122
+ .root.color-sky:hover {
123
+ --icon-color: var(--sky-12);
124
+
125
+ background-color: var(--sky-3);
126
+ border-color: var(--sky-6);
127
+ }
128
+
129
+ .root.color-sky:active {
130
+ background-color: var(--sky-5);
131
+ border-color: var(--sky-7);
132
+ }
133
+
134
+ .root.color-slate {
135
+ --icon-color: var(--slate-11);
136
+
137
+ background-color: var(--slate-2);
138
+ border-color: var(--slate-5);
139
+ color: var(--slate-12);
140
+ }
141
+
142
+ .root.color-slate:hover {
143
+ --icon-color: var(--slate-12);
144
+
145
+ background-color: var(--slate-3);
146
+ border-color: var(--slate-6);
147
+ }
148
+
149
+ .root.color-slate:active {
150
+ background-color: var(--slate-5);
151
+ border-color: var(--slate-7);
152
+ }
153
+
154
+ .root.color-teal {
155
+ --icon-color: var(--teal-11);
156
+
157
+ background-color: var(--teal-2);
158
+ border-color: var(--teal-5);
159
+ color: var(--teal-12);
160
+ }
161
+
162
+ .root.color-teal:hover {
163
+ --icon-color: var(--teal-12);
164
+
165
+ background-color: var(--teal-3);
166
+ border-color: var(--teal-6);
167
+ }
168
+
169
+ .root.color-teal:active {
170
+ background-color: var(--teal-5);
171
+ border-color: var(--teal-7);
172
+ }
173
+
174
+ .root.color-violet {
175
+ --icon-color: var(--violet-11);
176
+
177
+ background-color: var(--violet-2);
178
+ border-color: var(--violet-5);
179
+ color: var(--violet-12);
180
+ }
181
+
182
+ .root.color-violet:hover {
183
+ --icon-color: var(--violet-12);
184
+
185
+ background-color: var(--violet-3);
186
+ border-color: var(--violet-6);
187
+ }
188
+
189
+ .root.color-violet:active {
190
+ background-color: var(--violet-5);
191
+ border-color: var(--violet-7);
192
+ }
@@ -0,0 +1,6 @@
1
+ div(class=classes)
2
+ div(class=class_for("title")) = title
3
+ - if info.present?
4
+ span(class=class_for("info")) = info
5
+ div(class=class_for("data"))
6
+ = chart
@@ -0,0 +1 @@
1
+ import "./chart_component.css";
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Trek
4
+ class Dashboard::ChartComponent < Trek::Component
5
+ # Chartkick is an optional dependency: host apps that use this component must
6
+ # add `gem "chartkick"` and import "chartkick/chart.js" in their JS bundle.
7
+ include Chartkick::Helper if defined?(Chartkick)
8
+
9
+ option :title
10
+ option :info, optional: true
11
+ option :data
12
+ option :type
13
+ option :options, optional: true, default: proc { {} }
14
+ option :color, default: proc { :slate }
15
+
16
+ def chart
17
+ public_send(type, data, **options)
18
+ end
19
+
20
+ def classes
21
+ class_names(
22
+ root_class,
23
+ class_for("color-#{color}")
24
+ )
25
+ end
26
+ end
27
+ end
@@ -20,6 +20,14 @@
20
20
  width: 20rem;
21
21
  }
22
22
 
23
+ .root.wide {
24
+ width: calc(40rem + 1.2rem);
25
+ }
26
+
27
+ .title {
28
+ font-size: 3.6rem;
29
+ }
30
+
23
31
  .root:hover .icon {
24
32
  transform: scale(1.125);
25
33
  }
@@ -58,6 +66,10 @@
58
66
  border-color: var(--amber-7);
59
67
  }
60
68
 
69
+ .root.color-amber .title {
70
+ color: var(--amber-11);
71
+ }
72
+
61
73
  .root.color-lime {
62
74
  --icon-color: var(--lime-11);
63
75
 
@@ -78,6 +90,10 @@
78
90
  border-color: var(--lime-7);
79
91
  }
80
92
 
93
+ .root.color-lime .title {
94
+ color: var(--lime-11);
95
+ }
96
+
81
97
  .root.color-mint {
82
98
  --icon-color: var(--mint-11);
83
99
 
@@ -98,6 +114,10 @@
98
114
  border-color: var(--mint-7);
99
115
  }
100
116
 
117
+ .root.color-mint .title {
118
+ color: var(--mint-11);
119
+ }
120
+
101
121
  .root.color-purple {
102
122
  --icon-color: var(--purple-11);
103
123
 
@@ -118,6 +138,10 @@
118
138
  border-color: var(--purple-7);
119
139
  }
120
140
 
141
+ .root.color-purple .title {
142
+ color: var(--purple-11);
143
+ }
144
+
121
145
  .root.color-sky {
122
146
  --icon-color: var(--sky-11);
123
147
 
@@ -138,6 +162,10 @@
138
162
  border-color: var(--sky-7);
139
163
  }
140
164
 
165
+ .root.color-sky .title {
166
+ color: var(--sky-11);
167
+ }
168
+
141
169
  .root.color-slate {
142
170
  --icon-color: var(--slate-11);
143
171
 
@@ -158,6 +186,10 @@
158
186
  border-color: var(--slate-7);
159
187
  }
160
188
 
189
+ .root.color-slate .title {
190
+ color: var(--slate-11);
191
+ }
192
+
161
193
  .root.color-teal {
162
194
  --icon-color: var(--teal-11);
163
195
 
@@ -178,6 +210,10 @@
178
210
  border-color: var(--teal-7);
179
211
  }
180
212
 
213
+ .root.color-teal .title {
214
+ color: var(--teal-11);
215
+ }
216
+
181
217
  .root.color-violet {
182
218
  --icon-color: var(--violet-11);
183
219
 
@@ -197,3 +233,7 @@
197
233
  background-color: var(--violet-5);
198
234
  border-color: var(--violet-7);
199
235
  }
236
+
237
+ .root.color-violet .title {
238
+ color: var(--violet-11);
239
+ }
@@ -1,4 +1,14 @@
1
- = link_to href, class: classes, rel: rel, target: target
2
- - if icon
3
- = render Trek::IconComponent.new(icon, classnames: class_for("icon"))
4
- span(class=class_for("text")) = formatted_text
1
+ - if href.present?
2
+ = link_to href, class: classes, data: link_data, rel: link_rel, target: target
3
+ - if title
4
+ span(class=class_for("title")) = title
5
+ - if icon
6
+ = render Trek::IconComponent.new(icon, classnames: class_for("icon"))
7
+ span(class=class_for("text")) = formatted_text
8
+ - else
9
+ div(class=classes)
10
+ - if title
11
+ span(class=class_for("title")) = title
12
+ - if icon
13
+ = render Trek::IconComponent.new(icon, classnames: class_for("icon"))
14
+ span(class=class_for("text")) = formatted_text
@@ -3,12 +3,21 @@
3
3
  module Trek
4
4
  class Dashboard::TileComponent < Trek::Component
5
5
  option :color, default: proc { :slate }
6
- option :href
6
+ # Extra data attributes for the link, e.g. `turbo_confirm:`.
7
+ option :data, default: proc { {} }
8
+ option :href, optional: true
7
9
  option :icon, optional: true
10
+ # An HTTP method other than GET (`:post`, `:delete`…), issued through
11
+ # Turbo like Trek::ButtonComponent's `to:` links: a tile can create a
12
+ # record in one click rather than open a form.
13
+ option :method, optional: true
8
14
  option :rel, optional: true
9
15
  option :target, optional: true
10
16
  option :text
17
+ option :title, optional: true
11
18
  option :allowed_to, default: -> { true }
19
+ # Spans two tile columns (including the gap between them).
20
+ option :wide, default: proc { false }
12
21
 
13
22
  def render?
14
23
  allowed_to
@@ -17,10 +26,19 @@ module Trek
17
26
  def classes
18
27
  class_names(
19
28
  root_class,
20
- class_for("color-#{color}")
29
+ class_for("color-#{color}"),
30
+ class_for("wide") => wide
21
31
  )
22
32
  end
23
33
 
34
+ def link_data
35
+ data.merge(turbo_method: method).compact
36
+ end
37
+
38
+ def link_rel
39
+ rel || ("noopener" if target == "_blank")
40
+ end
41
+
24
42
  def formatted_text
25
43
  # rubocop:disable Rails/OutputSafety
26
44
  text.gsub("\n", "#{tag.br} ")&.html_safe
@@ -1,5 +1,8 @@
1
1
  dialog(class=root_class data-controller=identifier)
2
- h2(class=class_for("title")) = title || t(".title")
2
+ h2(
3
+ class=class_for("title")
4
+ data=stimulus_target_hash("title")
5
+ ) = title || t(".title")
3
6
  p(
4
7
  class=class_for("text")
5
8
  data=stimulus_target_hash("text")
@@ -22,5 +25,6 @@ dialog(class=root_class data-controller=identifier)
22
25
  text: t("admin.actions.delete"),
23
26
  color: :tomato,
24
27
  value: "confirm",
25
- data: { action: stimulus_action("close") },
28
+ data: { action: stimulus_action("close") }
29
+ .merge(stimulus_target_hash("confirm_button")),
26
30
  )
@@ -3,11 +3,26 @@ import "./dialog_component.css";
3
3
  import { Controller as BaseController } from "@hotwired/stimulus";
4
4
 
5
5
  export class Controller extends BaseController {
6
- static targets = ["text"];
6
+ static targets = ["title", "text", "confirmButton"];
7
7
 
8
8
  connect() {
9
+ this.saveDefaults();
10
+
11
+ // For a link with data-turbo-method, Turbo submits an ephemeral form it
12
+ // only copies data-turbo-confirm onto, and calls confirm() without a
13
+ // submitter — remember the clicked element to read the other
14
+ // data-turbo-confirm-* attributes from.
15
+ this.rememberConfirmSource = (event) => {
16
+ this.confirmSource = event.target.closest?.("[data-turbo-confirm]");
17
+ };
18
+ document.addEventListener("click", this.rememberConfirmSource, {
19
+ capture: true,
20
+ });
21
+
9
22
  // eslint-disable-next-line no-undef
10
- Turbo.config.forms.confirm = (message) => {
23
+ Turbo.config.forms.confirm = (message, _formElement, submitter) => {
24
+ this.customize({ ...this.confirmSource?.dataset, ...submitter?.dataset });
25
+ this.confirmSource = null;
11
26
  this.setText(message);
12
27
  this.open();
13
28
 
@@ -23,6 +38,51 @@ export class Controller extends BaseController {
23
38
  };
24
39
  }
25
40
 
41
+ disconnect() {
42
+ document.removeEventListener("click", this.rememberConfirmSource, {
43
+ capture: true,
44
+ });
45
+ }
46
+
47
+ saveDefaults() {
48
+ if (this.hasTitleTarget) {
49
+ this.defaultTitle = this.titleTarget.textContent;
50
+ }
51
+
52
+ if (this.hasConfirmButtonTarget) {
53
+ this.defaultConfirmLabel = this.confirmLabelElement.textContent;
54
+ this.defaultConfirmColor = this.confirmButtonTarget.dataset.accentColor;
55
+ }
56
+ }
57
+
58
+ customize({ turboConfirmTitle, turboConfirmButton, turboConfirmColor }) {
59
+ if (this.hasTitleTarget) {
60
+ this.titleTarget.textContent = turboConfirmTitle ?? this.defaultTitle;
61
+ }
62
+
63
+ if (this.hasConfirmButtonTarget) {
64
+ this.confirmLabelElement.textContent =
65
+ turboConfirmButton ?? this.defaultConfirmLabel;
66
+ this.setConfirmColor(turboConfirmColor ?? this.defaultConfirmColor);
67
+ }
68
+ }
69
+
70
+ setConfirmColor(color) {
71
+ const button = this.confirmButtonTarget;
72
+
73
+ // Swap the postcss-modules-scoped color-* class along with the accent
74
+ // variables (see generateScopedName in postcss.config.js).
75
+ button.classList.replace(
76
+ `c-trek--button-color-${button.dataset.accentColor}`,
77
+ `c-trek--button-color-${color}`
78
+ );
79
+ button.dataset.accentColor = color;
80
+ }
81
+
82
+ get confirmLabelElement() {
83
+ return this.confirmButtonTarget.querySelector("span");
84
+ }
85
+
26
86
  setText(text) {
27
87
  if (this.hasTextTarget) {
28
88
  this.textTarget.textContent = text;
@@ -0,0 +1,66 @@
1
+ /* Use component-local class names and add them to HTML via #class_for(name) helper */
2
+
3
+ .root {
4
+ display: inline-flex;
5
+ position: relative;
6
+ }
7
+
8
+ .trigger {
9
+ align-items: center;
10
+ background: none;
11
+ border: none;
12
+ color: var(--slate-12);
13
+ cursor: pointer;
14
+ display: inline-flex;
15
+ gap: 0.4rem;
16
+ }
17
+
18
+ .indicator {
19
+ transition: transform 0.2s ease-in-out;
20
+ }
21
+
22
+ .trigger[aria-expanded="true"] .indicator {
23
+ transform: rotate(180deg);
24
+ }
25
+
26
+ .menu {
27
+ background-color: var(--slate-2);
28
+ border: 1px solid var(--slate-6);
29
+ border-radius: 0.8rem;
30
+ box-shadow: 0 4px 12px rgb(0 0 0 / 12%);
31
+ list-style: none;
32
+ margin: 0;
33
+ min-width: 14rem;
34
+ padding: 0.4rem;
35
+ position: absolute;
36
+ right: 0;
37
+ top: calc(100% + 0.4rem);
38
+ z-index: 10;
39
+ }
40
+
41
+ .menu[hidden] {
42
+ display: none;
43
+ }
44
+
45
+ .-item-root {
46
+ background: none;
47
+ border: none;
48
+ border-radius: 0.6rem;
49
+ color: var(--slate-12);
50
+ cursor: pointer;
51
+ display: inline-flex;
52
+ font-size: 1.3rem;
53
+ gap: 1rem;
54
+ line-height: 1.23;
55
+ padding: 0.8rem 1.2rem;
56
+ text-align: left;
57
+ text-decoration: none;
58
+ width: 100%;
59
+ }
60
+
61
+ .-item-root:hover,
62
+ .-item-root:focus {
63
+ background-color: var(--accent-3);
64
+ color: var(--accent-11);
65
+ outline: none;
66
+ }
@@ -0,0 +1,31 @@
1
+ div(class=classes data-controller=identifier)
2
+ button(
3
+ type="button"
4
+ class=class_for("trigger")
5
+ aria-haspopup="true"
6
+ aria-expanded="false"
7
+ aria-controls=menu_id
8
+ aria-label=label
9
+ data=stimulus_target_hash("trigger")
10
+ data-action=stimulus_action(click: :toggle, keydown: :on_trigger_keydown)
11
+ )
12
+ = trigger
13
+ - unless hide_chevron
14
+ = render Trek::IconComponent.new( \
15
+ "trek/chevron-down",
16
+ classnames: class_for("indicator"),
17
+ aria_hidden: true,
18
+ )
19
+ ul(
20
+ id=menu_id
21
+ class=class_for("menu")
22
+ role="menu"
23
+ aria-label=label
24
+ hidden=true
25
+ data=stimulus_target_hash("menu")
26
+ data-action=stimulus_action( \
27
+ keydown: :on_menu_keydown,
28
+ click: :on_menu_click,
29
+ )
30
+ )
31
+ = safe_join(items)
@@ -0,0 +1,114 @@
1
+ import "./dropdown_component.css";
2
+
3
+ import { Controller as BaseController } from "@hotwired/stimulus";
4
+
5
+ export class Controller extends BaseController {
6
+ static targets = ["trigger", "menu"];
7
+
8
+ connect() {
9
+ this.onDocumentClick = this.onDocumentClick.bind(this);
10
+ }
11
+
12
+ disconnect() {
13
+ document.removeEventListener("click", this.onDocumentClick);
14
+ }
15
+
16
+ get items() {
17
+ return Array.from(this.menuTarget.querySelectorAll('[role="menuitem"]'));
18
+ }
19
+
20
+ get expanded() {
21
+ return this.triggerTarget.getAttribute("aria-expanded") === "true";
22
+ }
23
+
24
+ toggle(event) {
25
+ event.preventDefault();
26
+ this.expanded ? this.collapse() : this.open();
27
+ }
28
+
29
+ onTriggerKeydown(event) {
30
+ switch (event.key) {
31
+ case "ArrowDown":
32
+ case "Enter":
33
+ case " ":
34
+ event.preventDefault();
35
+ this.open();
36
+ this.focusItem(0);
37
+ break;
38
+ case "ArrowUp":
39
+ event.preventDefault();
40
+ this.open();
41
+ this.focusItem(this.items.length - 1);
42
+ break;
43
+ }
44
+ }
45
+
46
+ onMenuKeydown(event) {
47
+ const items = this.items;
48
+ const current = items.indexOf(document.activeElement);
49
+ switch (event.key) {
50
+ case "ArrowDown":
51
+ event.preventDefault();
52
+ this.focusItem((current + 1) % items.length);
53
+ break;
54
+ case "ArrowUp":
55
+ event.preventDefault();
56
+ this.focusItem((current - 1 + items.length) % items.length);
57
+ break;
58
+ case "Home":
59
+ event.preventDefault();
60
+ this.focusItem(0);
61
+ break;
62
+ case "End":
63
+ event.preventDefault();
64
+ this.focusItem(items.length - 1);
65
+ break;
66
+ case "Escape":
67
+ event.preventDefault();
68
+ this.collapse();
69
+ this.triggerTarget.focus();
70
+ break;
71
+ case "Enter":
72
+ case " ":
73
+ // Menu items are not native buttons: activate the focused one as a
74
+ // click so it runs its own action, then onMenuClick closes the menu.
75
+ if (current === -1) break;
76
+ event.preventDefault();
77
+ items[current].click();
78
+ break;
79
+ case "Tab":
80
+ this.collapse();
81
+ break;
82
+ }
83
+ }
84
+
85
+ onMenuClick(event) {
86
+ if (event.target.closest('[role="menuitem"]')) {
87
+ this.collapse();
88
+ this.triggerTarget.focus();
89
+ }
90
+ }
91
+
92
+ open() {
93
+ if (this.expanded) return;
94
+ this.menuTarget.hidden = false;
95
+ this.triggerTarget.setAttribute("aria-expanded", "true");
96
+ document.addEventListener("click", this.onDocumentClick);
97
+ }
98
+
99
+ collapse() {
100
+ if (!this.expanded) return;
101
+ this.menuTarget.hidden = true;
102
+ this.triggerTarget.setAttribute("aria-expanded", "false");
103
+ document.removeEventListener("click", this.onDocumentClick);
104
+ }
105
+
106
+ focusItem(index) {
107
+ const item = this.items[index];
108
+ if (item) item.focus();
109
+ }
110
+
111
+ onDocumentClick(event) {
112
+ if (!this.element.contains(event.target)) this.collapse();
113
+ }
114
+ }