rails_admin_next 1.0.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 (184) hide show
  1. checksums.yaml +7 -0
  2. data/Gemfile +46 -0
  3. data/LICENSE.md +21 -0
  4. data/README.md +317 -0
  5. data/Rakefile +25 -0
  6. data/app/assets/javascripts/rails_admin/custom/ui.js +1 -0
  7. data/app/assets/stylesheets/rails_admin/framework.css +2375 -0
  8. data/app/assets/stylesheets/rails_admin/skin.css +472 -0
  9. data/app/assets/stylesheets/rails_admin/tokens.css +281 -0
  10. data/app/assets/stylesheets/rails_admin.css +47 -0
  11. data/app/controllers/rails_admin_next/application_controller.rb +98 -0
  12. data/app/controllers/rails_admin_next/main_controller.rb +185 -0
  13. data/app/helpers/rails_admin_next/application_helper.rb +254 -0
  14. data/app/helpers/rails_admin_next/form_builder.rb +173 -0
  15. data/app/helpers/rails_admin_next/main_helper.rb +63 -0
  16. data/app/views/layouts/rails_admin_next/_head.html.erb +29 -0
  17. data/app/views/layouts/rails_admin_next/_navigation.html.erb +18 -0
  18. data/app/views/layouts/rails_admin_next/_secondary_navigation.html.erb +27 -0
  19. data/app/views/layouts/rails_admin_next/_sidebar_navigation.html.erb +6 -0
  20. data/app/views/layouts/rails_admin_next/application.html.erb +42 -0
  21. data/app/views/layouts/rails_admin_next/content.html.erb +22 -0
  22. data/app/views/layouts/rails_admin_next/modal.js.erb +7 -0
  23. data/app/views/rails_admin_next/main/_dashboard_history.html.erb +46 -0
  24. data/app/views/rails_admin_next/main/_delete_notice.html.erb +36 -0
  25. data/app/views/rails_admin_next/main/_form_action_text.html.erb +2 -0
  26. data/app/views/rails_admin_next/main/_form_boolean.html.erb +15 -0
  27. data/app/views/rails_admin_next/main/_form_colorpicker.html.erb +6 -0
  28. data/app/views/rails_admin_next/main/_form_datetime.html.erb +14 -0
  29. data/app/views/rails_admin_next/main/_form_enumeration.html.erb +26 -0
  30. data/app/views/rails_admin_next/main/_form_field.html.erb +2 -0
  31. data/app/views/rails_admin_next/main/_form_file_upload.html.erb +18 -0
  32. data/app/views/rails_admin_next/main/_form_filtering_multiselect.html.erb +20 -0
  33. data/app/views/rails_admin_next/main/_form_filtering_select.html.erb +22 -0
  34. data/app/views/rails_admin_next/main/_form_multiple_file_upload.html.erb +21 -0
  35. data/app/views/rails_admin_next/main/_form_nested_many.html.erb +20 -0
  36. data/app/views/rails_admin_next/main/_form_nested_one.html.erb +19 -0
  37. data/app/views/rails_admin_next/main/_form_polymorphic_association.html.erb +24 -0
  38. data/app/views/rails_admin_next/main/_form_text.html.erb +2 -0
  39. data/app/views/rails_admin_next/main/_pagination.html.erb +54 -0
  40. data/app/views/rails_admin_next/main/_submit_buttons.html.erb +26 -0
  41. data/app/views/rails_admin_next/main/bulk_delete.html.erb +19 -0
  42. data/app/views/rails_admin_next/main/dashboard.html.erb +64 -0
  43. data/app/views/rails_admin_next/main/delete.html.erb +21 -0
  44. data/app/views/rails_admin_next/main/edit.html.erb +3 -0
  45. data/app/views/rails_admin_next/main/export.html.erb +149 -0
  46. data/app/views/rails_admin_next/main/history.html.erb +80 -0
  47. data/app/views/rails_admin_next/main/index.html.erb +186 -0
  48. data/app/views/rails_admin_next/main/new.html.erb +3 -0
  49. data/app/views/rails_admin_next/main/show.html.erb +30 -0
  50. data/config/brakeman.ignore +145 -0
  51. data/config/importmap.rails_admin.rb +29 -0
  52. data/config/initializers/active_record_extensions.rb +21 -0
  53. data/config/locales/rails_admin_next.en.yml +158 -0
  54. data/config/routes.rb +14 -0
  55. data/lib/generators/rails_admin_next/install_generator.rb +39 -0
  56. data/lib/generators/rails_admin_next/templates/initializer.erb +40 -0
  57. data/lib/generators/rails_admin_next/utils.rb +21 -0
  58. data/lib/rails_admin_next/abstract_model.rb +266 -0
  59. data/lib/rails_admin_next/adapters/active_record/association.rb +130 -0
  60. data/lib/rails_admin_next/adapters/active_record/object_extension.rb +13 -0
  61. data/lib/rails_admin_next/adapters/active_record/property.rb +58 -0
  62. data/lib/rails_admin_next/adapters/active_record.rb +371 -0
  63. data/lib/rails_admin_next/config/actions/base.rb +160 -0
  64. data/lib/rails_admin_next/config/actions/bulk_delete.rb +67 -0
  65. data/lib/rails_admin_next/config/actions/dashboard.rb +62 -0
  66. data/lib/rails_admin_next/config/actions/delete.rb +58 -0
  67. data/lib/rails_admin_next/config/actions/edit.rb +56 -0
  68. data/lib/rails_admin_next/config/actions/export.rb +47 -0
  69. data/lib/rails_admin_next/config/actions/history_index.rb +40 -0
  70. data/lib/rails_admin_next/config/actions/history_show.rb +40 -0
  71. data/lib/rails_admin_next/config/actions/index.rb +108 -0
  72. data/lib/rails_admin_next/config/actions/new.rb +69 -0
  73. data/lib/rails_admin_next/config/actions/show.rb +36 -0
  74. data/lib/rails_admin_next/config/actions/show_in_app.rb +37 -0
  75. data/lib/rails_admin_next/config/actions.rb +122 -0
  76. data/lib/rails_admin_next/config/configurable.rb +107 -0
  77. data/lib/rails_admin_next/config/const_load_suppressor.rb +78 -0
  78. data/lib/rails_admin_next/config/fields/association.rb +169 -0
  79. data/lib/rails_admin_next/config/fields/base.rb +391 -0
  80. data/lib/rails_admin_next/config/fields/collection_association.rb +90 -0
  81. data/lib/rails_admin_next/config/fields/factories/action_text.rb +14 -0
  82. data/lib/rails_admin_next/config/fields/factories/active_storage.rb +34 -0
  83. data/lib/rails_admin_next/config/fields/factories/association.rb +32 -0
  84. data/lib/rails_admin_next/config/fields/factories/encrypted_password.rb +28 -0
  85. data/lib/rails_admin_next/config/fields/factories/enum.rb +23 -0
  86. data/lib/rails_admin_next/config/fields/factories/password.rb +19 -0
  87. data/lib/rails_admin_next/config/fields/group.rb +77 -0
  88. data/lib/rails_admin_next/config/fields/singular_association.rb +62 -0
  89. data/lib/rails_admin_next/config/fields/types/action_text.rb +20 -0
  90. data/lib/rails_admin_next/config/fields/types/active_record_enum.rb +62 -0
  91. data/lib/rails_admin_next/config/fields/types/active_storage.rb +70 -0
  92. data/lib/rails_admin_next/config/fields/types/all.rb +32 -0
  93. data/lib/rails_admin_next/config/fields/types/belongs_to_association.rb +48 -0
  94. data/lib/rails_admin_next/config/fields/types/boolean.rb +70 -0
  95. data/lib/rails_admin_next/config/fields/types/citext.rb +15 -0
  96. data/lib/rails_admin_next/config/fields/types/color.rb +43 -0
  97. data/lib/rails_admin_next/config/fields/types/date.rb +38 -0
  98. data/lib/rails_admin_next/config/fields/types/datetime.rb +79 -0
  99. data/lib/rails_admin_next/config/fields/types/decimal.rb +23 -0
  100. data/lib/rails_admin_next/config/fields/types/enum.rb +52 -0
  101. data/lib/rails_admin_next/config/fields/types/file_upload.rb +87 -0
  102. data/lib/rails_admin_next/config/fields/types/float.rb +23 -0
  103. data/lib/rails_admin_next/config/fields/types/has_and_belongs_to_many_association.rb +16 -0
  104. data/lib/rails_admin_next/config/fields/types/has_many_association.rb +16 -0
  105. data/lib/rails_admin_next/config/fields/types/has_one_association.rb +35 -0
  106. data/lib/rails_admin_next/config/fields/types/hidden.rb +31 -0
  107. data/lib/rails_admin_next/config/fields/types/inet.rb +15 -0
  108. data/lib/rails_admin_next/config/fields/types/integer.rb +20 -0
  109. data/lib/rails_admin_next/config/fields/types/json.rb +37 -0
  110. data/lib/rails_admin_next/config/fields/types/multiple_active_storage.rb +82 -0
  111. data/lib/rails_admin_next/config/fields/types/multiple_file_upload.rb +134 -0
  112. data/lib/rails_admin_next/config/fields/types/numeric.rb +24 -0
  113. data/lib/rails_admin_next/config/fields/types/password.rb +45 -0
  114. data/lib/rails_admin_next/config/fields/types/polymorphic_association.rb +118 -0
  115. data/lib/rails_admin_next/config/fields/types/serialized.rb +28 -0
  116. data/lib/rails_admin_next/config/fields/types/string.rb +50 -0
  117. data/lib/rails_admin_next/config/fields/types/string_like.rb +25 -0
  118. data/lib/rails_admin_next/config/fields/types/text.rb +28 -0
  119. data/lib/rails_admin_next/config/fields/types/time.rb +39 -0
  120. data/lib/rails_admin_next/config/fields/types/timestamp.rb +16 -0
  121. data/lib/rails_admin_next/config/fields/types/uuid.rb +15 -0
  122. data/lib/rails_admin_next/config/fields/types.rb +29 -0
  123. data/lib/rails_admin_next/config/fields.rb +88 -0
  124. data/lib/rails_admin_next/config/groupable.rb +20 -0
  125. data/lib/rails_admin_next/config/has_description.rb +14 -0
  126. data/lib/rails_admin_next/config/has_fields.rb +144 -0
  127. data/lib/rails_admin_next/config/has_groups.rb +27 -0
  128. data/lib/rails_admin_next/config/hideable.rb +30 -0
  129. data/lib/rails_admin_next/config/inspectable.rb +39 -0
  130. data/lib/rails_admin_next/config/lazy_model.rb +74 -0
  131. data/lib/rails_admin_next/config/model.rb +126 -0
  132. data/lib/rails_admin_next/config/proxyable/proxy.rb +42 -0
  133. data/lib/rails_admin_next/config/proxyable.rb +26 -0
  134. data/lib/rails_admin_next/config/sections/base.rb +36 -0
  135. data/lib/rails_admin_next/config/sections/create.rb +13 -0
  136. data/lib/rails_admin_next/config/sections/edit.rb +13 -0
  137. data/lib/rails_admin_next/config/sections/export.rb +13 -0
  138. data/lib/rails_admin_next/config/sections/list.rb +63 -0
  139. data/lib/rails_admin_next/config/sections/modal.rb +12 -0
  140. data/lib/rails_admin_next/config/sections/nested.rb +12 -0
  141. data/lib/rails_admin_next/config/sections/show.rb +12 -0
  142. data/lib/rails_admin_next/config/sections/update.rb +12 -0
  143. data/lib/rails_admin_next/config/sections.rb +39 -0
  144. data/lib/rails_admin_next/config.rb +436 -0
  145. data/lib/rails_admin_next/engine.rb +91 -0
  146. data/lib/rails_admin_next/extension.rb +35 -0
  147. data/lib/rails_admin_next/extensions/cancancan/authorization_adapter.rb +90 -0
  148. data/lib/rails_admin_next/extensions/cancancan.rb +5 -0
  149. data/lib/rails_admin_next/extensions/controller_extension.rb +8 -0
  150. data/lib/rails_admin_next/extensions/paper_trail/auditing_adapter.rb +192 -0
  151. data/lib/rails_admin_next/extensions/paper_trail.rb +5 -0
  152. data/lib/rails_admin_next/extensions/pundit/authorization_adapter.rb +72 -0
  153. data/lib/rails_admin_next/extensions/pundit.rb +5 -0
  154. data/lib/rails_admin_next/extensions/url_for_extension.rb +15 -0
  155. data/lib/rails_admin_next/icons.rb +143 -0
  156. data/lib/rails_admin_next/support/composite_keys_serializer.rb +15 -0
  157. data/lib/rails_admin_next/support/csv_converter.rb +100 -0
  158. data/lib/rails_admin_next/support/hash_helper.rb +31 -0
  159. data/lib/rails_admin_next/support/paginated_collection.rb +79 -0
  160. data/lib/rails_admin_next/version.rb +17 -0
  161. data/lib/rails_admin_next.rb +72 -0
  162. data/lib/tasks/rails_admin.rake +40 -0
  163. data/src/rails_admin/application.js +15 -0
  164. data/src/rails_admin/base.js +12 -0
  165. data/src/rails_admin/controllers/alert_controller.js +28 -0
  166. data/src/rails_admin/controllers/collapse_controller.js +16 -0
  167. data/src/rails_admin/controllers/dropdown_controller.js +110 -0
  168. data/src/rails_admin/controllers/feedback_controller.js +73 -0
  169. data/src/rails_admin/controllers/filter_box_controller.js +346 -0
  170. data/src/rails_admin/controllers/filtering_multiselect_controller.js +386 -0
  171. data/src/rails_admin/controllers/filtering_select_controller.js +509 -0
  172. data/src/rails_admin/controllers/index.js +31 -0
  173. data/src/rails_admin/controllers/modal_controller.js +142 -0
  174. data/src/rails_admin/controllers/nested_form_controller.js +117 -0
  175. data/src/rails_admin/controllers/polymorphic_controller.js +26 -0
  176. data/src/rails_admin/controllers/remote_form_controller.js +173 -0
  177. data/src/rails_admin/controllers/sidescroll_controller.js +23 -0
  178. data/src/rails_admin/dom-ready.js +20 -0
  179. data/src/rails_admin/dom.js +19 -0
  180. data/src/rails_admin/i18n.js +20 -0
  181. data/src/rails_admin/icons.js +64 -0
  182. data/src/rails_admin/interactions.js +221 -0
  183. data/src/rails_admin/select_query.js +65 -0
  184. metadata +346 -0
@@ -0,0 +1,281 @@
1
+ /*
2
+ * rails_admin_next — design tokens (CSS custom properties).
3
+ * Loaded by rails_admin.css into the ra.tokens cascade layer; see that file's header for the
4
+ * browser floor and the styled-vocabulary contract.
5
+ *
6
+ * Every color token is a light-dark() pair, resolved against the page scheme that the layout's
7
+ * <meta name="color-scheme"> advertises (RailsAdminNext.config.color_scheme: :auto/:light/:dark).
8
+ * The light arms are exact oklch() conversions of the original Bootstrap 5.1.3 hexes (noted
9
+ * alongside), precise enough to re-convert to the same sRGB value within 1/255 per channel.
10
+ * The dark arms keep each light arm's hue and invert the lightness ramp: the page turns to a
11
+ * near-black neutral, the gray ladder mirrors around gray-500, and the status colors lift their
12
+ * oklch lightness so they read as text against the dark background (WCAG AA). Solid fills pair
13
+ * with --ra-tint-target text (white in light, black in dark), so the lightened fills keep AA too.
14
+ */
15
+
16
+ /*
17
+ * Supported override tokens (public API)
18
+ *
19
+ * Hosts may override these tokens (unlayered, e.g. `:root { --ra-primary: #663399; }`)
20
+ * and every derived color — button hover/active states, focus rings, alert tints,
21
+ * table tints, links — follows automatically:
22
+ *
23
+ * --ra-primary, --ra-secondary, --ra-success, --ra-info, --ra-warning, --ra-danger,
24
+ * --ra-light, --ra-dark brand and status colors
25
+ * --ra-body-bg, --ra-body-color body surface and text
26
+ * --ra-link-color link color (defaults to a tint of --ra-primary)
27
+ *
28
+ * A single-color override applies to both schemes; hosts that want a tuned dark
29
+ * mode should override with their own pair, e.g.
30
+ * `:root { --ra-primary: light-dark(#663399, #a97fd6); }`.
31
+ *
32
+ * Every other --ra-* name is internal and may change without notice.
33
+ */
34
+
35
+ /* === CSS custom properties ============================================== */
36
+ :root {
37
+ /* Scheme-aware mix targets. All state derivation (hover/active shades, tints,
38
+ translucent overlays) mixes toward these — never toward literal black/white —
39
+ so the derivations flip direction under the dark scheme. */
40
+ --ra-shade-target: light-dark(black, white);
41
+ --ra-tint-target: light-dark(white, black);
42
+
43
+ /* Scheme probe for the one artifact a <color> can't express: a url() background
44
+ image (the .form-select caret). light-dark() exists only for colors, so this
45
+ registered color (@property in rails_admin.css) flips with the scheme and
46
+ framework.css swaps the caret data-URI under
47
+ @container style(--ra-scheme-probe: black). */
48
+ --ra-scheme-probe: light-dark(white, black);
49
+
50
+ /* Brand and status palette (public). Dark arms: same hue, lightness lifted so
51
+ each color passes 4.5:1 as text on the dark body bg (WCAG AA). --ra-info and
52
+ --ra-warning are already light enough — and always pair with dark text — so
53
+ their arms match. */
54
+ --ra-primary: light-dark(
55
+ oklch(57.8201% 0.228209 260.0251deg),
56
+ oklch(65.31% 0.17 260.0251deg)
57
+ ); /* #0d6efd / ~#4e8df6 */
58
+ --ra-secondary: light-dark(
59
+ oklch(55.7535% 0.016495 244.8934deg),
60
+ oklch(66.72% 0.016495 244.8934deg)
61
+ ); /* #6c757d / ~#8c969e */
62
+ --ra-success: light-dark(
63
+ oklch(55.2026% 0.123397 157.0121deg),
64
+ oklch(67.39% 0.123397 157.0121deg)
65
+ ); /* #198754 / ~#4aad77 */
66
+ --ra-info: light-dark(
67
+ oklch(77.4929% 0.137971 218.0547deg),
68
+ oklch(77.4929% 0.137971 218.0547deg)
69
+ ); /* #0dcaf0, both schemes */
70
+ --ra-warning: light-dark(
71
+ oklch(84.4171% 0.172159 84.9338deg),
72
+ oklch(84.4171% 0.172159 84.9338deg)
73
+ ); /* #ffc107, both schemes */
74
+ --ra-danger: light-dark(
75
+ oklch(59.155% 0.202026 21.2388deg),
76
+ oklch(66.59% 0.16 21.2388deg)
77
+ ); /* #dc3545 / ~#e56568 */
78
+ /* --ra-light/--ra-dark are absolutes by name: .btn-light stays a light button
79
+ and .table-dark a dark table in both schemes, so their arms match. The
80
+ scheme-flipping muted surface is --ra-surface-muted below. */
81
+ --ra-light: light-dark(
82
+ oklch(98.1639% 0.001707 247.8395deg),
83
+ oklch(98.1639% 0.001707 247.8395deg)
84
+ ); /* #f8f9fa */
85
+ --ra-dark: light-dark(
86
+ oklch(26.2093% 0.009493 248.1898deg),
87
+ oklch(26.2093% 0.009493 248.1898deg)
88
+ ); /* #212529 */
89
+
90
+ /* Neutrals (internal). White and black are scheme-locked absolutes on purpose:
91
+ the .navbar-light/.navbar-dark ladders, the solid-button text pairs, the
92
+ bg-white/bg-black utilities and the modal backdrop mean "really white/black",
93
+ not "the scheme's paper color" — giving these arms would flip the navbar
94
+ skins with the OS scheme. */
95
+ --ra-white: oklch(100% 0 0deg); /* #fff */
96
+ --ra-black: oklch(0% 0 0deg); /* #000 */
97
+ /* Gray ladder (internal). Dark arms mirror the ladder: 200 lands on Bootstrap's
98
+ gray-800, 300↔700, 400↔600 and 900↔200, so "subtle background" stays
99
+ subtle and "border" stays border-weight against the dark surface. The one
100
+ asymmetry: gray-600 (muted text) takes gray-500's value, not gray-400's,
101
+ so muted text stays visibly muted next to the dark body color. */
102
+ --ra-gray-200: light-dark(
103
+ oklch(94.1654% 0.005176 247.8791deg),
104
+ oklch(34.5143% 0.013291 248.2124deg)
105
+ ); /* #e9ecef / #343a40 */
106
+ --ra-gray-300: light-dark(
107
+ oklch(91.0874% 0.006959 247.9015deg),
108
+ oklch(42.7562% 0.014702 248.1716deg)
109
+ ); /* #dee2e6 / #495057 */
110
+ --ra-gray-400: light-dark(
111
+ oklch(86.7074% 0.010568 247.9486deg),
112
+ oklch(55.7535% 0.016495 244.8934deg)
113
+ ); /* #ced4da / #6c757d */
114
+ --ra-gray-500: light-dark(
115
+ oklch(76.9244% 0.014518 248.0166deg),
116
+ oklch(76.9244% 0.014518 248.0166deg)
117
+ ); /* #adb5bd, both schemes — the ladder's pivot */
118
+ --ra-gray-600: light-dark(
119
+ oklch(55.7535% 0.016495 244.8934deg),
120
+ oklch(76.9244% 0.014518 248.0166deg)
121
+ ); /* #6c757d / #adb5bd */
122
+ --ra-gray-700: light-dark(
123
+ oklch(42.7562% 0.014702 248.1716deg),
124
+ oklch(91.0874% 0.006959 247.9015deg)
125
+ ); /* #495057 / #dee2e6 */
126
+ --ra-gray-900: light-dark(
127
+ oklch(26.2093% 0.009493 248.1898deg),
128
+ oklch(94.1654% 0.005176 247.8791deg)
129
+ ); /* #212529 / #e9ecef */
130
+
131
+ /* Body surface and text (public). The dark page is a near-black neutral
132
+ (distinct from the locked --ra-dark) under light-gray text. */
133
+ --ra-body-color: light-dark(
134
+ oklch(26.2093% 0.009493 248.1898deg),
135
+ oklch(91.0874% 0.006959 247.9015deg)
136
+ ); /* #212529 / #dee2e6 */
137
+ --ra-body-bg: light-dark(
138
+ oklch(100% 0 0deg),
139
+ oklch(22% 0.0095 248.19deg)
140
+ ); /* #fff / ~#171b1f */
141
+
142
+ /* The muted surface behind .bg-light (sidebar, filter cards). Light arm equals
143
+ --ra-light (#f8f9fa) so light rendering is unchanged; unlike --ra-light it
144
+ turns into a raised dark surface under the dark scheme. */
145
+ --ra-surface-muted: light-dark(
146
+ oklch(98.1639% 0.001707 247.8395deg),
147
+ oklch(30.6326% 0.011412 248.2deg)
148
+ ); /* #f8f9fa / #2b3035 */
149
+
150
+ /* Semantic derivations (internal; --ra-link-color itself is public) */
151
+ --ra-link-color: color-mix(
152
+ in srgb,
153
+ light-dark(var(--ra-primary), var(--ra-white)) 40%,
154
+ var(--ra-primary)
155
+ ); /* light: mixing primary into itself is a no-op, so this stays exactly
156
+ --ra-primary; dark: primary tinted 40% toward white — no fill color can
157
+ be both AA text on the dark bg and an AA backdrop for its own label, so
158
+ link text derives lighter while fills keep --ra-primary. Follows host
159
+ --ra-primary overrides in both schemes. */
160
+ --ra-link-hover-color: color-mix(
161
+ in srgb,
162
+ var(--ra-shade-target) 20%,
163
+ var(--ra-link-color)
164
+ ); /* matches Bootstrap 5.1.3's shade-color($link-color, 20%): #0a58ca */
165
+ --ra-border-color: var(--ra-gray-300);
166
+ --ra-focus-ring-color: color-mix(
167
+ in srgb,
168
+ var(--ra-primary) 25%,
169
+ transparent
170
+ ); /* matches Bootstrap 5.1.3's rgba(13, 110, 253, 0.25) */
171
+ --ra-input-focus-border-color: color-mix(
172
+ in srgb,
173
+ var(--ra-white) 50%,
174
+ var(--ra-primary)
175
+ ); /* Matches Bootstrap 5.1.3's tint-color($primary, 50%) (#86b7fe). Mixes
176
+ toward locked white, not the flipping tint target: a focus border must
177
+ lift off the input surface in both schemes, and mixing toward black in
178
+ dark would sink it into the bg. */
179
+
180
+ /* One-off component colors (internal) */
181
+ --ra-code-color: light-dark(
182
+ oklch(59.5598% 0.20859 355.4835deg),
183
+ oklch(73.01% 0.15 355.4835deg)
184
+ ); /* #d63384 / ~#ef7cac */
185
+ --ra-icon-close: light-dark(
186
+ oklch(0% 0 0deg),
187
+ oklch(100% 0 0deg)
188
+ ); /* the .btn-close X glyph: matches Bootstrap 5.1.3's data-URI black exactly,
189
+ white in dark (the element's own opacity supplies the 50/75% strength) */
190
+ --ra-header-sort-shadow: light-dark(
191
+ oklch(98.1639% 0.001707 247.8395deg),
192
+ oklch(22% 0.0095 248.19deg)
193
+ ); /* #f8f9fa halo behind sorted-header text; the dark arm matches the dark
194
+ body bg so it stays a surface-colored halo, not a glow */
195
+ --ra-mark-bg: light-dark(
196
+ oklch(97.6656% 0.027837 98.0579deg),
197
+ oklch(35% 0.055 98.0579deg)
198
+ ); /* #fcf8e3 / ~#423b16 — muted dark amber keeps AA under the light body text */
199
+ --ra-nested-form-accent-1: light-dark(
200
+ oklch(84.7875% 0.109283 212.4507deg),
201
+ oklch(52% 0.075 212.4507deg)
202
+ ); /* #6ce0f7 / ~#2a7482 */
203
+ --ra-nested-form-accent-2: light-dark(
204
+ oklch(93.1505% 0.092939 91.5476deg),
205
+ oklch(52% 0.07 91.5476deg)
206
+ ); /* #ffe7a0 / ~#786837 */
207
+ --ra-nested-form-accent-3: light-dark(
208
+ oklch(85.7197% 0.115462 162.7335deg),
209
+ oklch(52% 0.085 162.7335deg)
210
+ ); /* #84e8ba / ~#33785a */
211
+ --ra-sticky-sort-bg: light-dark(
212
+ oklch(94.4459% 0.016779 230.9098deg),
213
+ oklch(30% 0.03 230.9098deg)
214
+ ); /* #e2eff6 / ~#1e313a */
215
+
216
+ /* Typography and misc */
217
+ --ra-font-sans-serif: system-ui, -apple-system, "Segoe UI", Roboto,
218
+ "Helvetica Neue", Arial, "Noto Sans", "Liberation Sans", sans-serif,
219
+ "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
220
+ --ra-font-monospace: SFMono-Regular, Menlo, Monaco, Consolas,
221
+ "Liberation Mono", "Courier New", monospace;
222
+ --ra-gradient: linear-gradient(
223
+ 180deg,
224
+ color-mix(in srgb, var(--ra-white) 15%, transparent),
225
+ color-mix(in srgb, var(--ra-white) 0%, transparent)
226
+ );
227
+ --ra-body-font-family: var(--ra-font-sans-serif);
228
+ --ra-body-font-size: 0.85rem;
229
+ --ra-body-font-weight: 400;
230
+ --ra-body-line-height: 1.5;
231
+ }
232
+
233
+ /*
234
+ * UPGRADING mapping (--bs-* -> --ra-*)
235
+ *
236
+ * Renamed (same meaning; read/override the new name):
237
+ * --bs-body-bg -> --ra-body-bg
238
+ * --bs-body-color -> --ra-body-color
239
+ * --bs-body-font-family -> --ra-body-font-family
240
+ * --bs-body-font-size -> --ra-body-font-size
241
+ * --bs-body-font-weight -> --ra-body-font-weight
242
+ * --bs-body-line-height -> --ra-body-line-height
243
+ * --bs-body-text-align -> --ra-body-text-align (still undeclared by default;
244
+ * set it to opt in, exactly like Bootstrap's knob)
245
+ * --bs-breadcrumb-divider -> --ra-breadcrumb-divider
246
+ * --bs-font-monospace -> --ra-font-monospace
247
+ * --bs-font-sans-serif -> --ra-font-sans-serif
248
+ * --bs-gradient -> --ra-gradient
249
+ * --bs-gutter-x -> --ra-gutter-x (grid-scoped)
250
+ * --bs-gutter-y -> --ra-gutter-y (grid-scoped)
251
+ * --bs-table-bg -> --ra-table-bg (table-scoped; likewise
252
+ * -accent-bg, -striped-bg, -striped-color,
253
+ * -active-bg, -active-color, -hover-bg, -hover-color)
254
+ *
255
+ * Renamed with a value-space change (number -> percentage):
256
+ * --bs-bg-opacity: 0.5 -> --ra-bg-opacity: 50%
257
+ * --bs-text-opacity: 0.5 -> --ra-text-opacity: 50%
258
+ *
259
+ * Previously declared but consumed by nothing — now live semantic tokens
260
+ * (overriding the --bs-* name never did anything; the --ra-* name works):
261
+ * --bs-primary -> --ra-primary --bs-warning -> --ra-warning
262
+ * --bs-secondary -> --ra-secondary --bs-danger -> --ra-danger
263
+ * --bs-success -> --ra-success --bs-light -> --ra-light
264
+ * --bs-info -> --ra-info --bs-dark -> --ra-dark
265
+ *
266
+ * Removed — replaced by derivation (never override these; override the base token):
267
+ * --bs-primary-rgb, --bs-secondary-rgb, --bs-success-rgb, --bs-info-rgb,
268
+ * --bs-warning-rgb, --bs-danger-rgb, --bs-light-rgb, --bs-dark-rgb,
269
+ * --bs-white-rgb, --bs-black-rgb, --bs-body-color-rgb, --bs-body-bg-rgb
270
+ * -> gone; utilities now derive translucency via
271
+ * color-mix(in srgb, var(--ra-*) <pct>, transparent)
272
+ *
273
+ * Removed without replacement (declared but never consumed):
274
+ * --bs-blue, --bs-indigo, --bs-purple, --bs-pink, --bs-red, --bs-orange,
275
+ * --bs-yellow, --bs-green, --bs-teal, --bs-cyan (raw color ladder; the code-pink
276
+ * role of --bs-pink lives on as the internal --ra-code-color),
277
+ * --bs-white, --bs-gray, --bs-gray-dark, --bs-gray-100 ... --bs-gray-900
278
+ * (the grays the CSS actually uses exist as internal --ra-gray-* tokens),
279
+ * --bs-position (set by .dropdown-menu-end for Bootstrap's dropdown JS,
280
+ * which this engine does not ship)
281
+ */
@@ -0,0 +1,47 @@
1
+ /*
2
+ * rails_admin_next stylesheet
3
+ *
4
+ * Owned, hand-maintained CSS: a hand-authored flatten of Bootstrap 5.1.3 plus the engine's own
5
+ * legacy SCSS. Not vendored and does not track upstream Bootstrap. Edit it directly — there is no
6
+ * build step and no upstream to stay mergeable with.
7
+ *
8
+ * Browser floor: latest stable Chrome and Safari only. The engine's JS and CSS may assume :has(),
9
+ * <dialog>, popover, CSS nesting, oklch(), color-mix(), light-dark(), @starting-style, and CSS
10
+ * anchor positioning.
11
+ *
12
+ * Styled vocabulary (the public styling contract for hosts):
13
+ * - the Bootstrap-derived class names actually emitted by engine views/helpers/JS, plus
14
+ * - the host-facing config value spaces, kept even where no engine view emits them:
15
+ * .navbar-light / .navbar-dark (navbar_css_classes), alert-* (flash_alert_class),
16
+ * bg-* utilities (badges, indicators, host navbar), .badge colors, and table-* contextual
17
+ * rows (row_css_class conventions).
18
+ * Class names this file does not style are out of contract and may stay unstyled.
19
+ */
20
+
21
+ /*
22
+ * Cascade-layer topology (public API — the layer names are a documented contract):
23
+ * ra.tokens — design tokens (custom properties)
24
+ * ra.framework — the trimmed ex-Bootstrap component CSS
25
+ * ra.skin — the engine's own widgets, icons, and admin skin
26
+ * Later layers win; anything a host ships UNLAYERED beats all engine CSS by design,
27
+ * so a plain host rule overrides the admin without specificity games.
28
+ */
29
+ @layer ra.tokens, ra.framework, ra.skin;
30
+
31
+ @import url("rails_admin/tokens.css") layer(ra.tokens);
32
+ @import url("rails_admin/framework.css") layer(ra.framework);
33
+ @import url("rails_admin/skin.css") layer(ra.skin);
34
+
35
+ /*
36
+ * Registration for the scheme probe declared in tokens.css. Registered as a <color> so its
37
+ * computed value is the RESOLVED light-dark() arm, letting framework.css swap the one artifact
38
+ * a <color> value can't reach — the .form-select caret's url() background image — with an
39
+ * @container style(--ra-scheme-probe: black) query. Lives here, outside the cascade layers,
40
+ * because @property registrations are document-global. The light initial value means a failed
41
+ * registration degrades to the light caret.
42
+ */
43
+ @property --ra-scheme-probe {
44
+ syntax: "<color>";
45
+ inherits: true;
46
+ initial-value: white;
47
+ }
@@ -0,0 +1,98 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rails_admin_next/abstract_model"
4
+
5
+ module RailsAdminNext
6
+ class ModelNotFound < ::StandardError
7
+ end
8
+
9
+ class ObjectNotFound < ::StandardError
10
+ end
11
+
12
+ class ActionNotAllowed < ::StandardError
13
+ end
14
+
15
+ class ApplicationController < Config.parent_controller.constantize
16
+ include RailsAdminNext::Extensions::ControllerExtension
17
+
18
+ protect_from_forgery(with: :exception)
19
+
20
+ before_action :_set_security_headers
21
+ before_action :_authenticate!
22
+ before_action :_authorize!
23
+ before_action :_audit!
24
+
25
+ helper_method :_current_user
26
+
27
+ attr_reader :object, :model_config, :abstract_model, :authorization_adapter
28
+
29
+ def get_model
30
+ @model_name = to_model_name(params[:model_name])
31
+ raise RailsAdminNext::ModelNotFound unless (@abstract_model = RailsAdminNext::AbstractModel.new(@model_name))
32
+ raise RailsAdminNext::ModelNotFound if (@model_config = @abstract_model.config).excluded?
33
+
34
+ @properties = @abstract_model.properties
35
+ end
36
+
37
+ def get_object
38
+ raise RailsAdminNext::ObjectNotFound unless (@object = @abstract_model.get(params[:id], @model_config.scope))
39
+ end
40
+
41
+ def to_model_name(param)
42
+ param.split("~").collect(&:camelize).join("::")
43
+ end
44
+
45
+ def _current_user
46
+ instance_eval(&RailsAdminNext::Config.current_user_method)
47
+ end
48
+
49
+ private
50
+
51
+ # The admin is a privileged surface: always advertise SAMEORIGIN framing, and apply the
52
+ # host-opted-in Content Security Policy (with a per-request nonce for the engine's inline
53
+ # tags) per-request so it covers only admin responses, never the host app.
54
+ def _set_security_headers
55
+ response.headers["X-Frame-Options"] = "SAMEORIGIN"
56
+
57
+ policy_block = RailsAdminNext::Config.content_security_policy
58
+ return unless policy_block
59
+
60
+ policy = ActionDispatch::ContentSecurityPolicy.new
61
+ policy_block.call(policy)
62
+ request.content_security_policy = policy
63
+ request.content_security_policy_report_only = RailsAdminNext::Config.content_security_policy_report_only
64
+ request.content_security_policy_nonce_generator ||= ->(_request) { SecureRandom.base64(16) }
65
+ request.content_security_policy_nonce_directives ||= %w[script-src style-src]
66
+ end
67
+
68
+ def _authenticate!
69
+ instance_eval(&RailsAdminNext::Config.authenticate_with)
70
+ end
71
+
72
+ def _authorize!
73
+ instance_eval(&RailsAdminNext::Config.authorize_with)
74
+ end
75
+
76
+ def _audit!
77
+ instance_eval(&RailsAdminNext::Config.audit_with)
78
+ end
79
+
80
+ def rails_admin_controller?
81
+ true
82
+ end
83
+
84
+ rescue_from RailsAdminNext::ObjectNotFound do
85
+ flash[:error] = I18n.t("admin.flash.object_not_found", model: @model_name, id: params[:id])
86
+ params[:action] = "index"
87
+ @status_code = :not_found
88
+ index
89
+ end
90
+
91
+ rescue_from RailsAdminNext::ModelNotFound do
92
+ flash[:error] = I18n.t("admin.flash.model_not_found", model: @model_name)
93
+ params[:action] = "dashboard"
94
+ @status_code = :not_found
95
+ dashboard
96
+ end
97
+ end
98
+ end
@@ -0,0 +1,185 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RailsAdminNext
4
+ class MainController < RailsAdminNext::ApplicationController
5
+ include ActionView::Helpers::TextHelper
6
+ include RailsAdminNext::MainHelper
7
+ include RailsAdminNext::ApplicationHelper
8
+
9
+ before_action :check_for_cancel
10
+
11
+ def bulk_action
12
+ get_model
13
+ process(params[:bulk_action]) if params[:bulk_action].in?(RailsAdminNext::Config::Actions.all(:bulkable, controller: self, abstract_model: @abstract_model).collect(&:route_fragment))
14
+ end
15
+
16
+ def list_entries(model_config = @model_config, auth_scope_key = :index, additional_scope = get_association_scope_from_params, pagination = !(params[:associated_collection] || params[:all] || params[:bulk_ids]))
17
+ scope = model_config.scope
18
+ auth_scope = @authorization_adapter&.query(auth_scope_key, model_config.abstract_model)
19
+ scope = scope.merge(auth_scope) if auth_scope
20
+ scope = scope.instance_eval(&additional_scope) if additional_scope
21
+ get_collection(model_config, scope, pagination)
22
+ end
23
+
24
+ private
25
+
26
+ def action_missing(name, *_args)
27
+ action = RailsAdminNext::Config::Actions.find(name.to_sym)
28
+ raise AbstractController::ActionNotFound.new("The action '#{name}' could not be found for #{self.class.name}") unless action
29
+
30
+ get_model unless action.root?
31
+ get_object if action.member?
32
+ @authorization_adapter.try(:authorize, action.authorization_key, @abstract_model, @object)
33
+ @action = action.with({controller: self, abstract_model: @abstract_model, object: @object})
34
+ raise(ActionNotAllowed) unless @action.enabled?
35
+
36
+ @page_name = wording_for(:title)
37
+
38
+ instance_eval(&@action.controller)
39
+ end
40
+
41
+ def method_missing(name, *, &)
42
+ action = RailsAdminNext::Config::Actions.find(name.to_sym)
43
+ if action
44
+ action_missing(name, *, &)
45
+ else
46
+ super
47
+ end
48
+ end
49
+
50
+ def respond_to_missing?(sym, include_private)
51
+ if RailsAdminNext::Config::Actions.find(sym)
52
+ true
53
+ else
54
+ super
55
+ end
56
+ end
57
+
58
+ def back_or_index
59
+ allowed_return_to?(params[:return_to].to_s) ? params[:return_to] : index_path
60
+ end
61
+
62
+ # A leading '/' alone isn't enough to prove same-origin: browsers normalize a leading
63
+ # backslash to '/', so '/\evil.com' would render as the protocol-relative '//evil.com'.
64
+ # Parsing with URI and comparing scheme+host+port (rather than a raw string prefix) also
65
+ # rejects userinfo tricks ('http://legit@evil.com') and no-boundary substring host
66
+ # confusion ('http://legit.com.evil.com').
67
+ def allowed_return_to?(url)
68
+ return false if url.blank? || url == request.fullpath
69
+
70
+ uri = URI.parse(url)
71
+ if uri.host
72
+ uri.scheme == request.scheme && uri.host == request.host && uri.port == request.port
73
+ else
74
+ url.start_with?("/") && !url.start_with?("//")
75
+ end
76
+ rescue URI::InvalidURIError
77
+ false
78
+ end
79
+
80
+ def get_sort_hash(model_config)
81
+ field = model_config.list.fields.detect { |f| f.name.to_s == params[:sort] }
82
+ # If no sort param, default to the `sort_by` specified in the list config
83
+ field ||= model_config.list.possible_fields.detect { |f| f.name == model_config.list.sort_by.try(:to_sym) }
84
+
85
+ column =
86
+ if field.nil? || field.sortable == false # use default sort, asked field does not exist or is not sortable
87
+ model_config.list.sort_by
88
+ else
89
+ field.sort_column
90
+ end
91
+
92
+ params[:sort_reverse] ||= "false"
93
+ {sort: column, sort_reverse: (params[:sort_reverse] == (field&.sort_reverse&.to_s || "true"))}
94
+ end
95
+
96
+ def redirect_to_on_success
97
+ notice = I18n.t("admin.flash.successful", name: @model_config.label, action: I18n.t("admin.actions.#{@action.key}.done"))
98
+ if params[:_add_another]
99
+ redirect_to new_path(return_to: params[:return_to]), flash: {success: notice}
100
+ elsif params[:_add_edit]
101
+ redirect_to edit_path(id: @object.id, return_to: params[:return_to]), flash: {success: notice}
102
+ else
103
+ redirect_to back_or_index, flash: {success: notice}
104
+ end
105
+ end
106
+
107
+ def visible_fields(action, model_config = @model_config)
108
+ model_config.send(action).with(controller: self, view: view_context, object: @object).visible_fields
109
+ end
110
+
111
+ def sanitize_params_for!(action, model_config = @model_config, target_params = params[@abstract_model.param_key])
112
+ return unless target_params.present?
113
+
114
+ fields = visible_fields(action, model_config)
115
+ allowed_methods = fields.collect(&:allowed_methods).flatten.uniq.collect(&:to_s) << "id" << "_destroy"
116
+ fields.each { |field| field.parse_input(target_params) }
117
+ target_params.slice!(*allowed_methods)
118
+ target_params.permit! if target_params.respond_to?(:permit!)
119
+ fields.select(&:nested_form).each do |association|
120
+ nested = target_params[association.method_name]
121
+ # accepts_nested_attributes_for takes a hash-of-records ({idx => {…}}) or an
122
+ # array-of-records ([{…}]) for collections, and a single record ({…}) for singular
123
+ # associations. Recurse into every record regardless of the submitted cardinality so a
124
+ # cardinality-flipped param (e.g. an array sent to a has_many) can't smuggle keys past
125
+ # the slice that permit! has already blanket-permitted.
126
+ children_params =
127
+ if nested.is_a?(Array)
128
+ nested
129
+ elsif nested.respond_to?(:values)
130
+ association.multiple? ? nested.values : [nested]
131
+ else
132
+ []
133
+ end
134
+ children_params.each do |children_param|
135
+ sanitize_params_for!(:nested, association.associated_model_config, children_param) if children_param.respond_to?(:values)
136
+ end
137
+ end
138
+ end
139
+
140
+ def handle_save_error(whereto = :new)
141
+ flash.now[:error] = I18n.t("admin.flash.error", name: @model_config.label, action: I18n.t("admin.actions.#{@action.key}.done").html_safe).html_safe
142
+ # full_messages can carry user-supplied values (e.g. interpolated into a custom validation
143
+ # message), so escape each before marking the assembled list html_safe — otherwise a
144
+ # `<script>` in a validation message would render live in the flash.
145
+ escaped_messages = @object.errors.full_messages.map { |message| ERB::Util.html_escape(message) }
146
+ flash.now[:error] += %(<br>- #{escaped_messages.join("<br>- ")}).html_safe
147
+
148
+ respond_to do |format|
149
+ format.html { render whereto, status: :not_acceptable }
150
+ format.js { render whereto, layout: "rails_admin_next/modal", status: :not_acceptable, content_type: Mime[:html].to_s }
151
+ end
152
+ end
153
+
154
+ def check_for_cancel
155
+ cancelling = params[:_continue] || (params[:bulk_action] && !params[:bulk_ids])
156
+ return unless cancelling
157
+
158
+ redirect_to(back_or_index, notice: I18n.t("admin.flash.noaction"))
159
+ end
160
+
161
+ def get_collection(model_config, scope, pagination)
162
+ section = (@action.key == :export) ? model_config.export : model_config.list
163
+ eager_loads = section.fields.flat_map(&:eager_load_values)
164
+ options = {}
165
+ options = options.merge(page: (params[:page] || 1).to_i, per: params[:per] || model_config.list.items_per_page) if pagination
166
+ options = options.merge(include: eager_loads) unless eager_loads.blank?
167
+ options = options.merge(get_sort_hash(model_config))
168
+ options = options.merge(query: params[:query]) if params[:query].present?
169
+ options = options.merge(filters: params[:f]) if params[:f].present?
170
+ options = options.merge(bulk_ids: params[:bulk_ids]) if params[:bulk_ids]
171
+ model_config.abstract_model.all(options, scope)
172
+ end
173
+
174
+ def get_association_scope_from_params
175
+ return nil unless params[:associated_collection].present?
176
+
177
+ source_abstract_model = RailsAdminNext::AbstractModel.new(to_model_name(params[:source_abstract_model]))
178
+ source_model_config = source_abstract_model.config
179
+ source_object = source_abstract_model.get(params[:source_object_id])
180
+ action = params[:current_action].in?(%w[create update]) ? params[:current_action] : "edit"
181
+ @association = source_model_config.send(action).fields.detect { |f| f.name == params[:associated_collection].to_sym }.with(controller: self, object: source_object)
182
+ @association.associated_collection_scope
183
+ end
184
+ end
185
+ end