b4um_generators 0.1.0 → 0.2.7

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.
@@ -32,6 +32,7 @@ module B4um
32
32
 
33
33
  - bcrypt for password support
34
34
  - Active Storage for image attachments
35
+ - Home page with root route
35
36
  - Hero section
36
37
  - Footer
37
38
  - Footer sitemap
@@ -46,6 +47,38 @@ module B4um
46
47
  b4um installs Active Storage and runs the database migration.
47
48
 
48
49
 
50
+ HOME PAGE
51
+
52
+ The installer can optionally create a home page.
53
+
54
+ During installation, b4um asks:
55
+
56
+ Create a home page? (y/n)
57
+
58
+ When selected, the controller and action can be configured:
59
+
60
+ Home controller [Pages]:
61
+ Home action [home]:
62
+
63
+ Press Enter to use the defaults.
64
+
65
+ With the default configuration, b4um creates:
66
+
67
+ app/controllers/pages_controller.rb
68
+ app/views/pages/home.html.erb
69
+
70
+ and adds:
71
+
72
+ root "pages#home"
73
+
74
+ A Home link using root_path is also added to the
75
+ b4um navigation.
76
+
77
+ If the application already has a root route, b4um keeps
78
+ the existing root route and does not add a new Home
79
+ navigation link.
80
+
81
+
49
82
  HERO
50
83
 
51
84
  The installer can add an optional hero section.
@@ -357,10 +390,10 @@ module B4um
357
390
 
358
391
  READABLE URL PARAMETERS
359
392
 
360
- b4um scaffolds can generate readable URLs while keeping
393
+ b4um can generate readable URLs while keeping
361
394
  the database ID in the URL.
362
395
 
363
- Use:
396
+ When creating a new scaffold, use:
364
397
 
365
398
  --param=ATTRIBUTE
366
399
 
@@ -368,15 +401,26 @@ module B4um
368
401
 
369
402
  bin/rails generate b4um:scaffold Article title:string body:text --param=title
370
403
 
371
- This generates:
404
+ For an existing model, readable URLs can also be
405
+ added later with:
406
+
407
+ bin/rails generate b4um:readable MODEL ATTRIBUTE
408
+
409
+ Example:
410
+
411
+ bin/rails generate b4um:readable Product name
412
+
413
+ Both variants add a to_param method to the model.
414
+
415
+ Example:
372
416
 
373
417
  def to_param
374
- "\#{id} \#{title}".parameterize
418
+ "\#{id} \#{name}".parameterize
375
419
  end
376
420
 
377
421
  Example URL:
378
422
 
379
- /articles/17-my-first-article
423
+ /products/17-macbook-air
380
424
 
381
425
  The ID remains part of the URL, so the standard Rails
382
426
  resource lookup can still be used.
@@ -384,6 +428,14 @@ module B4um
384
428
  The readable part of the URL is updated automatically
385
429
  when the selected attribute changes.
386
430
 
431
+ The b4um:readable generator requires:
432
+
433
+ - An existing model
434
+ - An existing model attribute
435
+ - No existing custom to_param method
436
+
437
+ Existing to_param methods are not overwritten.
438
+
387
439
 
388
440
  PASSWORDS
389
441
 
@@ -408,6 +460,113 @@ module B4um
408
460
  the selected Gem automatically.
409
461
 
410
462
 
463
+ AUTHENTICATION
464
+
465
+ bin/rails generate b4um:authentication MODEL
466
+
467
+ Adds session-based authentication for an existing model.
468
+
469
+ Example:
470
+
471
+ bin/rails generate b4um:authentication User
472
+
473
+ The authentication model can have any name, for example:
474
+
475
+ User
476
+ Admin
477
+ Member
478
+
479
+ The selected model must:
480
+
481
+ - Already exist
482
+ - Use has_secure_password
483
+ - Have a password_digest column
484
+ - Use bcrypt
485
+
486
+ Authentication uses an email attribute for login.
487
+
488
+ Example:
489
+
490
+ bin/rails generate b4um:scaffold User name:string email:string password_digest:string
491
+ bin/rails generate b4um:authentication User
492
+
493
+ The authentication generator adds:
494
+
495
+ - Sessions controller
496
+ - Login form
497
+ - Login and logout routes
498
+ - Session-based current account helper
499
+ - logged_in? helper
500
+ - require_login helper
501
+ - Login and logout navigation controls
502
+
503
+ The generated helper name follows the selected model.
504
+
505
+ For User:
506
+
507
+ current_user
508
+
509
+ For Admin:
510
+
511
+ current_admin
512
+
513
+ Existing controllers can optionally be protected with --protect.
514
+
515
+ Example:
516
+
517
+ bin/rails generate b4um:authentication User --protect=Products
518
+
519
+ Multiple controllers can be protected in one run:
520
+
521
+ bin/rails generate b4um:authentication User --protect=Products,Articles,Comments
522
+
523
+ Protected controllers receive:
524
+
525
+ before_action :require_login, except: [:index, :show]
526
+
527
+ This keeps index and show publicly accessible while requiring
528
+ authentication for actions such as new, create, edit, update
529
+ and destroy.
530
+
531
+ For protected b4um resources, authentication also updates
532
+ the generated interface for logged-out visitors.
533
+
534
+ On index pages:
535
+
536
+ - New is hidden
537
+
538
+ On show pages:
539
+
540
+ - Edit is hidden
541
+ - Destroy is hidden
542
+ - Back remains visible
543
+
544
+ These controls remain available to logged-in users.
545
+ The controller protection remains the security boundary;
546
+ hiding the controls only keeps the public interface consistent.
547
+
548
+ Additional controllers can be protected later by running the
549
+ authentication generator again.
550
+
551
+ Example:
552
+
553
+ bin/rails generate b4um:authentication User --protect=Comments
554
+
555
+ Existing authentication helpers, routes and navigation controls
556
+ are not duplicated on repeated runs.
557
+
558
+ All controllers passed to --protect must already exist.
559
+ If a protected controller cannot be found, the generator stops
560
+ before changing the application.
561
+
562
+ The generator validates its requirements before changing
563
+ the application and reports missing authentication setup
564
+ with a suggested b4um scaffold command.
565
+
566
+ Repeated generator runs do not duplicate the authentication
567
+ helpers, routes or navigation controls.
568
+
569
+
411
570
  ACTIVE STORAGE / IMAGES
412
571
 
413
572
  Image attachments require Active Storage.
@@ -436,6 +595,52 @@ module B4um
436
595
 
437
596
  bin/rails generate b4um:scaffold Gallery title:string images:attachments
438
597
 
598
+ Attachments can also be added later to an existing
599
+ b4um resource.
600
+
601
+ Single attachment:
602
+
603
+ bin/rails generate b4um:attachment MODEL ATTACHMENT
604
+
605
+ Example:
606
+
607
+ bin/rails generate b4um:attachment Admin avatar
608
+
609
+ This adds:
610
+
611
+ has_one_attached :avatar
612
+
613
+ Multiple attachments:
614
+
615
+ bin/rails generate b4um:attachment MODEL ATTACHMENT --multiple
616
+
617
+ Example:
618
+
619
+ bin/rails generate b4um:attachment Product images --multiple
620
+
621
+ This adds:
622
+
623
+ has_many_attached :images
624
+
625
+ When the corresponding b4um files exist, the attachment
626
+ generator also updates:
627
+
628
+ - The existing model
629
+ - The existing b4um form
630
+ - The controller parameters
631
+ - The existing b4um resource partial
632
+
633
+ Multiple attachments additionally support:
634
+
635
+ - Adding new images without replacing existing images
636
+ - Removing individual existing images
637
+ - Multiple image preview
638
+ - Image gallery with lightbox navigation
639
+
640
+ Repeated generator runs do not duplicate existing
641
+ attachment setup.
642
+
643
+
439
644
  Image features:
440
645
 
441
646
  - Active Storage integration
@@ -121,6 +121,37 @@ module B4um
121
121
  )
122
122
  end
123
123
 
124
+ def update_table_partial
125
+ index_content = File.read(
126
+ File.join(destination_root, index_view_path)
127
+ )
128
+
129
+ return unless index_content.match?(
130
+ /render\s+["']table["']/
131
+ )
132
+
133
+ path = File.join(
134
+ "app/views",
135
+ file_name.pluralize,
136
+ "_table.html.erb"
137
+ )
138
+
139
+ full_path = File.join(destination_root, path)
140
+
141
+ return unless File.exist?(full_path)
142
+
143
+ content = File.read(full_path)
144
+
145
+ return if content.include?(%(<tbody id="#{plural_table_name}">))
146
+ return unless content.include?("<tbody>")
147
+
148
+ gsub_file(
149
+ path,
150
+ "<tbody>",
151
+ %(<tbody id="#{plural_table_name}">)
152
+ )
153
+ end
154
+
124
155
  private
125
156
 
126
157
  def paginated_collection
@@ -7,21 +7,32 @@ export default class extends Controller {
7
7
  }
8
8
 
9
9
  connect() {
10
- this.observer = new IntersectionObserver((entries) => {
11
- if (entries.some((entry) => entry.isIntersecting)) {
12
- this.loadMore()
13
- }
14
- })
10
+ this.loading = false
11
+ this.check = this.check.bind(this)
12
+
13
+ window.addEventListener('scroll', this.check, { passive: true })
14
+ window.addEventListener('resize', this.check)
15
15
 
16
- this.observer.observe(this.element)
16
+ this.check()
17
17
  }
18
18
 
19
19
  disconnect() {
20
- this.observer?.disconnect()
20
+ window.removeEventListener('scroll', this.check)
21
+ window.removeEventListener('resize', this.check)
22
+ }
23
+
24
+ check() {
25
+ if (!this.hasNextUrlValue || !this.nextUrlValue || this.loading) return
26
+
27
+ const rect = this.element.getBoundingClientRect()
28
+
29
+ if (rect.top <= window.innerHeight + 300) {
30
+ this.loadMore()
31
+ }
21
32
  }
22
33
 
23
34
  async loadMore() {
24
- if (!this.hasNextUrlValue || this.loading) return
35
+ if (!this.hasNextUrlValue || !this.nextUrlValue || this.loading) return
25
36
 
26
37
  this.loading = true
27
38
 
@@ -51,7 +62,6 @@ export default class extends Controller {
51
62
  if (nextSentinel?.dataset.infiniteScrollNextUrlValue) {
52
63
  this.nextUrlValue = nextSentinel.dataset.infiniteScrollNextUrlValue
53
64
  } else {
54
- this.observer.disconnect()
55
65
  this.element.remove()
56
66
  }
57
67
  } finally {
@@ -71,6 +71,104 @@ module B4um
71
71
  say " Active Storage database tables created."
72
72
  end
73
73
 
74
+ def install_home_page
75
+ unless yes?("Create a home page? (y/n)")
76
+ say " Home page skipped."
77
+ return
78
+ end
79
+
80
+ @home_controller = ask(
81
+ "Home controller [Pages]:"
82
+ ).presence || "Pages"
83
+
84
+ @home_action = ask(
85
+ "Home action [home]:"
86
+ ).presence || "home"
87
+
88
+ controller_name = @home_controller.underscore
89
+ action_name = @home_action.underscore
90
+
91
+ controller_path = File.join(
92
+ "app/controllers",
93
+ "#{controller_name}_controller.rb"
94
+ )
95
+
96
+ view_path = File.join(
97
+ "app/views",
98
+ controller_name,
99
+ "#{action_name}.html.erb"
100
+ )
101
+
102
+ full_controller_path = File.join(
103
+ destination_root,
104
+ controller_path
105
+ )
106
+
107
+ full_view_path = File.join(
108
+ destination_root,
109
+ view_path
110
+ )
111
+
112
+ unless File.exist?(full_controller_path)
113
+ create_file(
114
+ controller_path,
115
+ <<~RUBY
116
+ class #{@home_controller.camelize}Controller < ApplicationController
117
+ def #{action_name}
118
+ end
119
+ end
120
+ RUBY
121
+ )
122
+ end
123
+
124
+ unless File.exist?(full_view_path)
125
+ create_file(
126
+ view_path,
127
+ <<~ERB
128
+ <% content_for :title, "#{action_name.humanize}" %>
129
+
130
+ <div class="page-header">
131
+ <h1>#{action_name.humanize}</h1>
132
+ </div>
133
+
134
+ <div class="b4um-grid">
135
+ <section class="b4um-card b4um-card--full">
136
+ <h2 class="b4um-card__title">
137
+ #{action_name.humanize}
138
+ </h2>
139
+
140
+ <p class="b4um-card__text">
141
+ Add your content here.
142
+ </p>
143
+ </section>
144
+ </div>
145
+ ERB
146
+ )
147
+ end
148
+
149
+ routes_path = File.join(
150
+ destination_root,
151
+ "config/routes.rb"
152
+ )
153
+
154
+ routes = File.read(routes_path)
155
+
156
+ if routes.match?(/^\s*root\b/)
157
+ say " Root route already exists; keeping existing root route."
158
+ @home_navigation_enabled = false
159
+ else
160
+ route(
161
+ %(root "#{controller_name}##{action_name}")
162
+ )
163
+
164
+ @home_navigation_enabled = true
165
+ end
166
+
167
+ @home_page_installed = true
168
+
169
+ say " Home page installed."
170
+ end
171
+
74
172
  def create_stylesheet
75
173
  copy_file "b4um.css", "app/assets/stylesheets/b4um.css"
76
174
 
@@ -190,8 +288,49 @@ module B4um
190
288
  end
191
289
 
192
290
  def copy_navigation
291
+ navigation_path = "app/views/shared/_navigation.html.erb"
292
+
193
293
  copy_file "_navigation.html.erb",
194
- "app/views/shared/_navigation.html.erb"
294
+ navigation_path
295
+
296
+ return unless @home_page_installed
297
+ return unless @home_navigation_enabled
298
+
299
+ full_navigation_path = File.join(
300
+ destination_root,
301
+ navigation_path
302
+ )
303
+
304
+ navigation = File.read(full_navigation_path)
305
+ marker = "<%# B4UM_NAVIGATION_LINKS %>"
306
+
307
+ return unless navigation.include?(marker)
308
+
309
+ controller_name = @home_controller.underscore
310
+ action_name = @home_action.underscore
311
+
312
+ return if navigation.include?("root_path")
313
+
314
+ indented_marker = " #{marker}"
315
+
316
+ home_link = <<~ERB.chomp
317
+ <%= navigation_link_to "#{action_name.humanize}",
318
+ root_path,
319
+ controller: :#{controller_name},
320
+ action: :#{action_name} %>
321
+
322
+ #{marker}
323
+ ERB
324
+
325
+ indented_home_link = home_link.lines.map do |line|
326
+ line.strip.empty? ? line : " #{line}"
327
+ end.join
328
+
329
+ gsub_file(
330
+ navigation_path,
331
+ indented_marker,
332
+ indented_home_link
333
+ )
195
334
  end
196
335
 
197
336
  def copy_navigation_controller
@@ -366,7 +505,7 @@ module B4um
366
505
  '<main class="container">',
367
506
  <<~ERB.chomp
368
507
  <main class="container">
369
- <%= render "shared/hero" if request.path == root_path %>
508
+ <%= render "shared/hero" if respond_to?(:root_path) && request.path == root_path %>
370
509
  ERB
371
510
  )
372
511
  end
@@ -417,12 +417,20 @@
417
417
 
418
418
  .form-field--rich-text {
419
419
  display: grid;
420
- grid-template-columns: 1fr;
420
+ grid-template-columns: minmax(0, 1fr);
421
+
422
+ width: 100%;
423
+ min-width: 0;
424
+ max-width: 100%;
421
425
  }
422
426
 
423
427
  .form-field--rich-text trix-toolbar {
424
428
  grid-row: 1;
425
429
 
430
+ width: 100%;
431
+ min-width: 0;
432
+ max-width: 100%;
433
+
426
434
  position: sticky;
427
435
  top: 61px;
428
436
  z-index: 10;
@@ -496,6 +504,15 @@
496
504
  color: var(--b4um-text);
497
505
  }
498
506
 
507
+ .form-field--rich-text .form-textarea,
508
+ .form-field--rich-text trix-editor {
509
+ width: 100%;
510
+ min-width: 0;
511
+ max-width: 100%;
512
+
513
+ box-sizing: border-box;
514
+ }
515
+
499
516
  .form-field--rich-text .form-textarea {
500
517
  grid-row: 2;
501
518
  }
@@ -686,6 +703,18 @@
686
703
  border-radius: var(--b4um-radius);
687
704
  }
688
705
 
706
+ /* -------------------------------------------------------
707
+ Rich Text Attachments – responsive
708
+ ------------------------------------------------------- */
709
+
710
+ @media (max-width: 700px) {
711
+ .trix-content .attachment-gallery,
712
+ .trix-content .attachment-gallery--2,
713
+ .trix-content .attachment-gallery--4 {
714
+ grid-template-columns: 1fr;
715
+ }
716
+ }
717
+
689
718
  .trix-content action-text-attachment[content-type='vnd.rubyonrails.horizontal-rule.html'] {
690
719
  display: block;
691
720
  width: 100%;
@@ -922,3 +951,134 @@
922
951
 
923
952
  opacity: 1;
924
953
  }
954
+ /* -------------------------------------------------------
955
+ Responsive Rich Text
956
+ ------------------------------------------------------- */
957
+
958
+ @media (max-width: 640px) {
959
+ .form-field--rich-text trix-toolbar {
960
+ overflow: visible;
961
+ }
962
+
963
+ /*
964
+ * Nur die Button-Zeile scrollt horizontal.
965
+ * Die Dialoge liegen außerhalb dieses Scrollcontainers
966
+ * und können deshalb vollständig sichtbar werden.
967
+ */
968
+ .form-field--rich-text trix-toolbar .trix-button-row {
969
+ display: flex;
970
+ flex-wrap: nowrap;
971
+ align-items: center;
972
+ justify-content: flex-start;
973
+
974
+ width: 100%;
975
+ max-width: 100%;
976
+
977
+ gap: 4px;
978
+
979
+ overflow-x: auto;
980
+ overflow-y: hidden;
981
+
982
+ -webkit-overflow-scrolling: touch;
983
+ scrollbar-width: none;
984
+ }
985
+
986
+ .form-field--rich-text trix-toolbar .trix-button-row::-webkit-scrollbar {
987
+ display: none;
988
+ }
989
+
990
+ .form-field--rich-text trix-toolbar .trix-button-group {
991
+ flex: 0 0 auto;
992
+
993
+ margin: 0;
994
+ }
995
+
996
+ .form-field--rich-text trix-toolbar .trix-button {
997
+ flex: 0 0 auto;
998
+
999
+ margin: 0;
1000
+ }
1001
+
1002
+ /*
1003
+ * Trix-Dialoge gehören NICHT zum horizontalen
1004
+ * Scrollbereich der Buttons.
1005
+ */
1006
+ .form-field--rich-text trix-toolbar .trix-dialogs {
1007
+ position: relative;
1008
+
1009
+ width: 100%;
1010
+
1011
+ overflow: visible;
1012
+ }
1013
+
1014
+ .form-field--rich-text trix-toolbar .trix-dialog {
1015
+ left: 0;
1016
+ right: auto;
1017
+
1018
+ max-width: 100%;
1019
+
1020
+ box-sizing: border-box;
1021
+ }
1022
+
1023
+ /*
1024
+ * Link-Dialog nutzt auf kleinen Displays
1025
+ * die komplette verfügbare Toolbarbreite.
1026
+ */
1027
+ .form-field--rich-text trix-toolbar .trix-dialog--link {
1028
+ width: 100%;
1029
+ }
1030
+
1031
+ /*
1032
+ * H1–H6 bleiben kompakt nebeneinander.
1033
+ */
1034
+ .form-field--rich-text trix-toolbar .trix-dialog--heading {
1035
+ width: max-content;
1036
+ max-width: 100%;
1037
+
1038
+ margin-left: 0;
1039
+ }
1040
+
1041
+ .form-field--rich-text trix-toolbar .trix-dialog--heading .trix-dialog__link-fields {
1042
+ width: auto;
1043
+ }
1044
+
1045
+ .form-field--rich-text trix-toolbar .trix-dialog--heading .trix-button-group {
1046
+ display: flex;
1047
+ flex-wrap: nowrap;
1048
+
1049
+ width: auto;
1050
+ margin: 0;
1051
+ }
1052
+
1053
+ /*
1054
+ * Farbpalette ebenfalls vollständig innerhalb
1055
+ * der verfügbaren Breite darstellen.
1056
+ */
1057
+ .form-field--rich-text trix-toolbar .trix-dialog--color {
1058
+ width: max-content;
1059
+ max-width: 100%;
1060
+ }
1061
+
1062
+ .form-field--rich-text trix-editor .attachment-gallery,
1063
+ .form-field--rich-text trix-editor .attachment-gallery--2,
1064
+ .form-field--rich-text trix-editor .attachment-gallery--4 {
1065
+ grid-template-columns: 1fr;
1066
+ }
1067
+ }
1068
+
1069
+ .form-field--rich-text trix-toolbar .trix-button--icon {
1070
+ height: 1.98em;
1071
+ }
1072
+
1073
+ .form-field--rich-text trix-toolbar .trix-button-group--container-tools .trix-button {
1074
+ height: 2.6em;
1075
+ display: inline-flex;
1076
+ align-items: center;
1077
+ justify-content: center;
1078
+ }
1079
+
1080
+ @media (max-width: 768px) {
1081
+ trix-toolbar .trix-button--icon {
1082
+ height: 2em;
1083
+ }
1084
+ }