plutonium 0.65.0 → 0.67.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 (164) hide show
  1. checksums.yaml +4 -4
  2. data/.claude/skills/plutonium/SKILL.md +43 -43
  3. data/.claude/skills/plutonium-app/SKILL.md +101 -59
  4. data/.claude/skills/plutonium-async-interactions/SKILL.md +19 -19
  5. data/.claude/skills/plutonium-auth/SKILL.md +119 -53
  6. data/.claude/skills/plutonium-behavior/SKILL.md +110 -78
  7. data/.claude/skills/plutonium-dashboard/SKILL.md +11 -4
  8. data/.claude/skills/plutonium-kanban/SKILL.md +81 -49
  9. data/.claude/skills/plutonium-resource/SKILL.md +158 -141
  10. data/.claude/skills/plutonium-tenancy/SKILL.md +104 -55
  11. data/.claude/skills/plutonium-testing/SKILL.md +130 -33
  12. data/.claude/skills/plutonium-ui/SKILL.md +161 -98
  13. data/.claude/skills/plutonium-wizard/SKILL.md +93 -82
  14. data/CHANGELOG.md +43 -0
  15. data/README.md +9 -9
  16. data/SECURITY.md +1 -1
  17. data/app/assets/plutonium.css +1 -1
  18. data/app/assets/plutonium.js +56 -3
  19. data/app/assets/plutonium.js.map +4 -4
  20. data/app/assets/plutonium.min.js +32 -32
  21. data/app/assets/plutonium.min.js.map +4 -4
  22. data/config/brakeman.ignore +1 -1
  23. data/config/locales/en/js.yml +4 -0
  24. data/config/locales/en/ui.yml +16 -0
  25. data/docs/.vitepress/sync-skills.mjs +6 -3
  26. data/docs/blog/introducing-plutonium-dashboards.md +4 -5
  27. data/docs/blog/introducing-plutonium-i18n.md +4 -5
  28. data/docs/getting-started/installation.md +5 -5
  29. data/docs/getting-started/tutorial/02-first-resource.md +3 -3
  30. data/docs/getting-started/tutorial/03-authentication.md +7 -7
  31. data/docs/getting-started/tutorial/04-authorization.md +21 -4
  32. data/docs/getting-started/tutorial/05-custom-actions.md +2 -2
  33. data/docs/getting-started/tutorial/06-nested-resources.md +5 -2
  34. data/docs/getting-started/tutorial/07-author-portal.md +2 -2
  35. data/docs/getting-started/tutorial/08-customizing-ui.md +45 -30
  36. data/docs/getting-started/tutorial/index.md +1 -1
  37. data/docs/guides/adding-resources.md +10 -7
  38. data/docs/guides/authentication.md +25 -25
  39. data/docs/guides/authorization.md +24 -24
  40. data/docs/guides/creating-packages.md +17 -17
  41. data/docs/guides/custom-actions.md +32 -32
  42. data/docs/guides/customizing-ui.md +29 -26
  43. data/docs/guides/dashboards.md +1 -1
  44. data/docs/guides/index.md +3 -3
  45. data/docs/guides/kanban.md +55 -55
  46. data/docs/guides/multi-tenancy.md +35 -22
  47. data/docs/guides/nested-resources.md +21 -21
  48. data/docs/guides/performance.md +3 -3
  49. data/docs/guides/search-filtering.md +13 -13
  50. data/docs/guides/testing.md +16 -12
  51. data/docs/guides/theming.md +32 -17
  52. data/docs/guides/troubleshooting.md +2 -2
  53. data/docs/guides/user-invites.md +17 -17
  54. data/docs/guides/user-profile.md +51 -24
  55. data/docs/guides/wizards.md +55 -55
  56. data/docs/reference/app/generators.md +22 -22
  57. data/docs/reference/app/index.md +15 -18
  58. data/docs/reference/app/packages.md +8 -8
  59. data/docs/reference/app/portals.md +75 -29
  60. data/docs/reference/auth/accounts.md +15 -15
  61. data/docs/reference/auth/index.md +12 -12
  62. data/docs/reference/auth/profile.md +67 -29
  63. data/docs/reference/behavior/async-interactions.md +24 -24
  64. data/docs/reference/behavior/controllers.md +28 -28
  65. data/docs/reference/behavior/index.md +5 -5
  66. data/docs/reference/behavior/interactions.md +44 -44
  67. data/docs/reference/behavior/policies.md +48 -28
  68. data/docs/reference/configuration.md +6 -6
  69. data/docs/reference/dashboard/dsl.md +2 -2
  70. data/docs/reference/dashboard/index.md +1 -1
  71. data/docs/reference/generators/lite.md +7 -7
  72. data/docs/reference/i18n.md +23 -0
  73. data/docs/reference/index.md +1 -1
  74. data/docs/reference/kanban/authorization.md +9 -9
  75. data/docs/reference/kanban/dsl.md +32 -32
  76. data/docs/reference/kanban/index.md +1 -1
  77. data/docs/reference/kanban/positioning.md +17 -15
  78. data/docs/reference/resource/actions.md +51 -51
  79. data/docs/reference/resource/definition.md +85 -80
  80. data/docs/reference/resource/export.md +6 -6
  81. data/docs/reference/resource/index.md +16 -16
  82. data/docs/reference/resource/model.md +24 -24
  83. data/docs/reference/resource/positioning.md +78 -76
  84. data/docs/reference/resource/query.md +24 -14
  85. data/docs/reference/tenancy/entity-scoping.md +65 -35
  86. data/docs/reference/tenancy/index.md +11 -11
  87. data/docs/reference/tenancy/invites.md +20 -20
  88. data/docs/reference/tenancy/nested-resources.md +13 -13
  89. data/docs/reference/testing/index.md +116 -22
  90. data/docs/reference/ui/assets.md +57 -25
  91. data/docs/reference/ui/components.md +20 -20
  92. data/docs/reference/ui/displays.md +32 -15
  93. data/docs/reference/ui/forms.md +89 -39
  94. data/docs/reference/ui/index.md +17 -15
  95. data/docs/reference/ui/layouts.md +21 -21
  96. data/docs/reference/ui/pages.md +22 -22
  97. data/docs/reference/ui/tables.md +7 -7
  98. data/docs/reference/wizard/anchoring-resume.md +33 -32
  99. data/docs/reference/wizard/dsl.md +44 -44
  100. data/docs/reference/wizard/index.md +6 -6
  101. data/docs/reference/wizard/one-time.md +18 -18
  102. data/docs/reference/wizard/registration-launch.md +32 -32
  103. data/docs/reference/wizard/storage-config.md +23 -23
  104. data/gemfiles/rails_7.gemfile +1 -0
  105. data/gemfiles/rails_7.gemfile.lock +13 -3
  106. data/gemfiles/rails_8.0.gemfile +1 -0
  107. data/gemfiles/rails_8.0.gemfile.lock +10 -1
  108. data/gemfiles/rails_8.1.gemfile +1 -0
  109. data/gemfiles/rails_8.1.gemfile.lock +10 -1
  110. data/lib/generators/pu/profile/conn_generator.rb +6 -0
  111. data/lib/plutonium/auth/rodauth.rb +7 -0
  112. data/lib/plutonium/core/controller.rb +0 -2
  113. data/lib/plutonium/helpers/content_helper.rb +0 -3
  114. data/lib/plutonium/query/filter.rb +2 -1
  115. data/lib/plutonium/query/filters/tags.rb +36 -0
  116. data/lib/plutonium/resource/record/associated_with.rb +23 -2
  117. data/lib/plutonium/ui/component/resolves_tags.rb +13 -0
  118. data/lib/plutonium/ui/display/base.rb +50 -2
  119. data/lib/plutonium/ui/display/components/badge.rb +1 -1
  120. data/lib/plutonium/ui/display/components/binary.rb +20 -0
  121. data/lib/plutonium/ui/display/components/boolean.rb +1 -1
  122. data/lib/plutonium/ui/display/components/code.rb +23 -0
  123. data/lib/plutonium/ui/display/components/color.rb +14 -6
  124. data/lib/plutonium/ui/display/components/duration.rb +31 -0
  125. data/lib/plutonium/ui/display/components/file_size.rb +22 -0
  126. data/lib/plutonium/ui/display/components/key_value.rb +37 -0
  127. data/lib/plutonium/ui/display/components/list.rb +33 -0
  128. data/lib/plutonium/ui/display/components/number.rb +32 -0
  129. data/lib/plutonium/ui/display/components/progress.rb +36 -0
  130. data/lib/plutonium/ui/display/components/rating.rb +36 -0
  131. data/lib/plutonium/ui/display/components/relative_time.rb +25 -0
  132. data/lib/plutonium/ui/display/components/rich_text.rb +23 -0
  133. data/lib/plutonium/ui/display/components/tags.rb +18 -0
  134. data/lib/plutonium/ui/display/options/inferred_types.rb +13 -0
  135. data/lib/plutonium/ui/display/theme.rb +24 -2
  136. data/lib/plutonium/ui/form/base.rb +30 -1
  137. data/lib/plutonium/ui/form/components/binary.rb +40 -0
  138. data/lib/plutonium/ui/form/components/key_value_store.rb +22 -71
  139. data/lib/plutonium/ui/form/components/list.rb +46 -0
  140. data/lib/plutonium/ui/form/components/rating.rb +51 -0
  141. data/lib/plutonium/ui/form/components/rich_text.rb +20 -0
  142. data/lib/plutonium/ui/form/components/tags.rb +45 -0
  143. data/lib/plutonium/ui/form/components/tags_filter.rb +40 -0
  144. data/lib/plutonium/ui/form/components/uppy.rb +1 -1
  145. data/lib/plutonium/ui/form/concerns/typeahead_attributes.rb +7 -1
  146. data/lib/plutonium/ui/form/options/inferred_types.rb +16 -15
  147. data/lib/plutonium/ui/form/theme.rb +20 -4
  148. data/lib/plutonium/ui/grid/card.rb +13 -1
  149. data/lib/plutonium/ui/layout/base.rb +10 -1
  150. data/lib/plutonium/ui/options/rich_text_field.rb +18 -0
  151. data/lib/plutonium/ui/options/secret_field.rb +32 -0
  152. data/lib/plutonium/ui/options/tags_field.rb +23 -0
  153. data/lib/plutonium/ui/table/components/toolbar.rb +2 -2
  154. data/lib/plutonium/ui/table/components/view_switcher.rb +4 -2
  155. data/lib/plutonium/ui/table/display_theme.rb +30 -2
  156. data/lib/plutonium/version.rb +1 -1
  157. data/package.json +1 -1
  158. data/src/css/components.css +53 -10
  159. data/src/css/flatpickr.css +91 -0
  160. data/src/js/controllers/easymde_controller.js +1 -0
  161. data/src/js/controllers/register_controllers.js +2 -0
  162. data/src/js/controllers/slim_select_controller.js +32 -3
  163. data/src/js/controllers/timeago_controller.js +40 -0
  164. metadata +25 -2
@@ -1,5 +1,5 @@
1
1
  module Plutonium
2
- VERSION = "0.65.0"
2
+ VERSION = "0.67.0"
3
3
  NEXT_MAJOR_VERSION = VERSION.split(".").tap { |v|
4
4
  v[1] = v[1].to_i + 1
5
5
  v[2] = 0
data/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@radioactive-labs/plutonium",
3
- "version": "0.65.0",
3
+ "version": "0.67.0",
4
4
  "description": "Build production-ready Rails apps in minutes, not days. Convention-driven, fully customizable, AI-ready.",
5
5
  "type": "module",
6
6
  "main": "src/js/core.js",
@@ -971,7 +971,7 @@ html.pu-rail-pinned .icon-rail-pin-expand {
971
971
  @apply flex items-center p-4 mb-4 rounded-lg;
972
972
  }
973
973
  .dark .pu-alert {
974
- @apply bg-stone-300;
974
+ @apply bg-gray-800;
975
975
  }
976
976
 
977
977
  .pu-alert-message {
@@ -982,7 +982,7 @@ html.pu-rail-pinned .icon-rail-pin-expand {
982
982
  @apply ms-auto -mx-1.5 -my-1.5 rounded-lg focus:ring-2 p-1.5 inline-flex items-center justify-center h-8 w-8;
983
983
  }
984
984
  .dark .pu-alert-close {
985
- @apply bg-stone-300 hover:bg-gray-700;
985
+ @apply bg-gray-800 hover:bg-gray-700;
986
986
  }
987
987
 
988
988
  /* --- Variants: success / warning / danger / info ---
@@ -995,11 +995,11 @@ html.pu-rail-pinned .icon-rail-pin-expand {
995
995
  .pu-toast-success .pu-toast-icon { @apply text-success-500 bg-success-100; }
996
996
  .dark .pu-toast-success .pu-toast-icon { @apply bg-success-800 text-success-200; }
997
997
  .pu-toast-success .pu-toast-close { @apply bg-success-50 text-success-400 hover:bg-success-200 focus:ring-success-400; }
998
- .dark .pu-toast-success .pu-toast-close { @apply text-success-400; }
998
+ .dark .pu-toast-success .pu-toast-close { @apply bg-gray-800 text-success-400 hover:bg-gray-700; }
999
999
  .pu-alert-success { @apply bg-success-50 text-success-800; }
1000
1000
  .dark .pu-alert-success { @apply text-success-400; }
1001
1001
  .pu-alert-success .pu-alert-close { @apply bg-success-50 text-success-500 hover:bg-success-200 focus:ring-success-400; }
1002
- .dark .pu-alert-success .pu-alert-close { @apply text-success-400; }
1002
+ .dark .pu-alert-success .pu-alert-close { @apply bg-gray-800 text-success-400 hover:bg-gray-700; }
1003
1003
 
1004
1004
  /* warning */
1005
1005
  .pu-toast-warning { @apply bg-warning-50; }
@@ -1007,11 +1007,11 @@ html.pu-rail-pinned .icon-rail-pin-expand {
1007
1007
  .pu-toast-warning .pu-toast-icon { @apply text-warning-500 bg-warning-100; }
1008
1008
  .dark .pu-toast-warning .pu-toast-icon { @apply bg-warning-800 text-warning-200; }
1009
1009
  .pu-toast-warning .pu-toast-close { @apply bg-warning-50 text-warning-400 hover:bg-warning-200 focus:ring-warning-400; }
1010
- .dark .pu-toast-warning .pu-toast-close { @apply text-warning-400; }
1010
+ .dark .pu-toast-warning .pu-toast-close { @apply bg-gray-800 text-warning-400 hover:bg-gray-700; }
1011
1011
  .pu-alert-warning { @apply bg-warning-50 text-warning-800; }
1012
1012
  .dark .pu-alert-warning { @apply text-warning-400; }
1013
1013
  .pu-alert-warning .pu-alert-close { @apply bg-warning-50 text-warning-500 hover:bg-warning-200 focus:ring-warning-400; }
1014
- .dark .pu-alert-warning .pu-alert-close { @apply text-warning-400; }
1014
+ .dark .pu-alert-warning .pu-alert-close { @apply bg-gray-800 text-warning-400 hover:bg-gray-700; }
1015
1015
 
1016
1016
  /* danger */
1017
1017
  .pu-toast-danger { @apply bg-danger-50; }
@@ -1019,11 +1019,11 @@ html.pu-rail-pinned .icon-rail-pin-expand {
1019
1019
  .pu-toast-danger .pu-toast-icon { @apply text-danger-500 bg-danger-100; }
1020
1020
  .dark .pu-toast-danger .pu-toast-icon { @apply bg-danger-800 text-danger-200; }
1021
1021
  .pu-toast-danger .pu-toast-close { @apply bg-danger-50 text-danger-400 hover:bg-danger-200 focus:ring-danger-400; }
1022
- .dark .pu-toast-danger .pu-toast-close { @apply text-danger-400; }
1022
+ .dark .pu-toast-danger .pu-toast-close { @apply bg-gray-800 text-danger-400 hover:bg-gray-700; }
1023
1023
  .pu-alert-danger { @apply bg-danger-50 text-danger-800; }
1024
1024
  .dark .pu-alert-danger { @apply text-danger-400; }
1025
1025
  .pu-alert-danger .pu-alert-close { @apply bg-danger-50 text-danger-500 hover:bg-danger-200 focus:ring-danger-400; }
1026
- .dark .pu-alert-danger .pu-alert-close { @apply text-danger-400; }
1026
+ .dark .pu-alert-danger .pu-alert-close { @apply bg-gray-800 text-danger-400 hover:bg-gray-700; }
1027
1027
 
1028
1028
  /* info (default / :notice) */
1029
1029
  .pu-toast-info { @apply bg-info-50; }
@@ -1031,11 +1031,11 @@ html.pu-rail-pinned .icon-rail-pin-expand {
1031
1031
  .pu-toast-info .pu-toast-icon { @apply text-info-500 bg-info-100; }
1032
1032
  .dark .pu-toast-info .pu-toast-icon { @apply bg-info-800 text-info-200; }
1033
1033
  .pu-toast-info .pu-toast-close { @apply bg-info-50 text-info-400 hover:bg-info-200 focus:ring-info-400; }
1034
- .dark .pu-toast-info .pu-toast-close { @apply text-info-400; }
1034
+ .dark .pu-toast-info .pu-toast-close { @apply bg-gray-800 text-info-400 hover:bg-gray-700; }
1035
1035
  .pu-alert-info { @apply bg-info-50 text-info-800; }
1036
1036
  .dark .pu-alert-info { @apply text-info-400; }
1037
1037
  .pu-alert-info .pu-alert-close { @apply bg-info-50 text-info-500 hover:bg-info-200 focus:ring-info-400; }
1038
- .dark .pu-alert-info .pu-alert-close { @apply text-info-400; }
1038
+ .dark .pu-alert-info .pu-alert-close { @apply bg-gray-800 text-info-400 hover:bg-gray-700; }
1039
1039
 
1040
1040
  /* ===========================================================================
1041
1041
  Wizard — horizontal "steps" stepper (numbered nodes on a connector track)
@@ -1247,3 +1247,46 @@ html.pu-rail-pinned .icon-rail-pin-expand {
1247
1247
  opacity: 0.6;
1248
1248
  }
1249
1249
  }
1250
+
1251
+ /* Star rating input (Form::Components::Rating). Radios run max..1 in a
1252
+ row-reversed box, so `~ label` reaches the stars below the checked or
1253
+ hovered one. */
1254
+ .pu-rating {
1255
+ @apply inline-flex flex-row-reverse justify-end gap-0.5 border-0 p-0 m-0;
1256
+ }
1257
+ .pu-rating label {
1258
+ @apply cursor-pointer text-[var(--pu-border)] transition-colors;
1259
+ }
1260
+ .pu-rating label svg {
1261
+ @apply w-6 h-6;
1262
+ }
1263
+ .pu-rating input:checked ~ label,
1264
+ .pu-rating label:hover,
1265
+ .pu-rating label:hover ~ label {
1266
+ @apply text-warning-500;
1267
+ }
1268
+ .pu-rating input:checked ~ label svg,
1269
+ .pu-rating label:hover svg,
1270
+ .pu-rating label:hover ~ label svg {
1271
+ fill: currentColor;
1272
+ }
1273
+ /* "Clear" comes last in the DOM; order: -1 puts it after the stars
1274
+ (row-reverse lays out from the right). `input ~` matches the star-fill
1275
+ rules' specificity, and coming later lets this win. */
1276
+ .pu-rating input ~ label.pu-rating-clear {
1277
+ @apply order-[-1] ml-2 self-center text-sm text-[var(--pu-text-muted)] hover:text-[var(--pu-text)] hover:underline;
1278
+ }
1279
+ .pu-rating input:focus-visible + label {
1280
+ @apply rounded-sm outline-2 outline-offset-2 outline-primary-500;
1281
+ }
1282
+
1283
+ /* Tailwind's preflight strips list markers. Restore them inside the rich
1284
+ text editors (Trix, Lexxy) so lists look like lists while editing. */
1285
+ trix-editor ul,
1286
+ .lexxy-content ul {
1287
+ @apply list-disc ps-6;
1288
+ }
1289
+ trix-editor ol,
1290
+ .lexxy-content ol {
1291
+ @apply list-decimal ps-6;
1292
+ }
@@ -795,3 +795,94 @@ span.flatpickr-weekday {
795
795
  [data-controller="remote-modal"] > .flatpickr-calendar {
796
796
  left: auto !important;
797
797
  }
798
+
799
+ /* Dark mode: map flatpickr's hardcoded light palette onto the design tokens. */
800
+ .dark .flatpickr-calendar {
801
+ background: var(--pu-surface);
802
+ box-shadow: 0 0 0 1px var(--pu-border), 0 3px 13px rgba(0, 0, 0, 0.4);
803
+ }
804
+ .dark .flatpickr-calendar.arrowTop:after {
805
+ border-bottom-color: var(--pu-surface);
806
+ }
807
+ .dark .flatpickr-calendar.arrowBottom:after {
808
+ border-top-color: var(--pu-surface);
809
+ }
810
+ .dark .flatpickr-calendar.arrowTop:before {
811
+ border-bottom-color: var(--pu-border);
812
+ }
813
+ .dark .flatpickr-calendar.arrowBottom:before {
814
+ border-top-color: var(--pu-border);
815
+ }
816
+ .dark .flatpickr-calendar.hasTime .flatpickr-time {
817
+ border-top-color: var(--pu-border);
818
+ }
819
+ .dark .flatpickr-months .flatpickr-month,
820
+ .dark .flatpickr-months .flatpickr-prev-month,
821
+ .dark .flatpickr-months .flatpickr-next-month,
822
+ .dark .flatpickr-current-month input.cur-year,
823
+ .dark .flatpickr-current-month .flatpickr-monthDropdown-months {
824
+ color: var(--pu-text);
825
+ fill: var(--pu-text);
826
+ }
827
+ .dark .flatpickr-months .flatpickr-prev-month:hover svg,
828
+ .dark .flatpickr-months .flatpickr-next-month:hover svg {
829
+ fill: var(--pu-text-muted);
830
+ }
831
+ .dark .flatpickr-current-month .flatpickr-monthDropdown-months,
832
+ .dark .flatpickr-current-month .flatpickr-monthDropdown-months .flatpickr-monthDropdown-month {
833
+ background: var(--pu-surface);
834
+ }
835
+ .dark .flatpickr-current-month .numInputWrapper span.arrowUp:after {
836
+ border-bottom-color: var(--pu-text);
837
+ }
838
+ .dark .flatpickr-current-month .numInputWrapper span.arrowDown:after {
839
+ border-top-color: var(--pu-text);
840
+ }
841
+ .dark span.flatpickr-weekday {
842
+ color: var(--pu-text-muted);
843
+ }
844
+ .dark .flatpickr-day {
845
+ color: var(--pu-text);
846
+ }
847
+ .dark .flatpickr-day:hover,
848
+ .dark .flatpickr-day:focus,
849
+ .dark .flatpickr-day.prevMonthDay:hover,
850
+ .dark .flatpickr-day.nextMonthDay:hover,
851
+ .dark .flatpickr-day.inRange {
852
+ background: var(--pu-surface-alt);
853
+ border-color: var(--pu-surface-alt);
854
+ }
855
+ .dark .flatpickr-day.inRange {
856
+ box-shadow: -5px 0 0 var(--pu-surface-alt), 5px 0 0 var(--pu-surface-alt);
857
+ }
858
+ .dark .flatpickr-day.today {
859
+ border-color: var(--pu-text-muted);
860
+ }
861
+ .dark .flatpickr-day.prevMonthDay,
862
+ .dark .flatpickr-day.nextMonthDay,
863
+ .dark .flatpickr-day.flatpickr-disabled,
864
+ .dark .flatpickr-day.flatpickr-disabled:hover {
865
+ color: var(--pu-text-muted);
866
+ opacity: 0.5;
867
+ }
868
+ .dark .flatpickr-weekwrapper .flatpickr-weeks,
869
+ .dark .dayContainer + .dayContainer {
870
+ box-shadow: none;
871
+ }
872
+ .dark .flatpickr-time input,
873
+ .dark .flatpickr-time .flatpickr-am-pm,
874
+ .dark .flatpickr-time .flatpickr-time-separator {
875
+ color: var(--pu-text);
876
+ }
877
+ .dark .flatpickr-time .numInputWrapper span.arrowUp:after {
878
+ border-bottom-color: var(--pu-text);
879
+ }
880
+ .dark .flatpickr-time .numInputWrapper span.arrowDown:after {
881
+ border-top-color: var(--pu-text);
882
+ }
883
+ .dark .flatpickr-time input:hover,
884
+ .dark .flatpickr-time .flatpickr-am-pm:hover,
885
+ .dark .flatpickr-time input:focus,
886
+ .dark .flatpickr-time .flatpickr-am-pm:focus {
887
+ background: var(--pu-surface-alt);
888
+ }
@@ -66,6 +66,7 @@ export default class extends Controller {
66
66
  #buildOptions() {
67
67
  let options = {
68
68
  element: this.element,
69
+ autoDownloadFontAwesome: false,
69
70
  promptURLs: true,
70
71
  spellChecker: false,
71
72
  // Override the default preview renderer
@@ -43,6 +43,7 @@ import BreadcrumbsController from "./breadcrumbs_controller.js"
43
43
  import RunProgressController from "./run_progress_controller.js"
44
44
  import ChartController from "./chart_controller.js"
45
45
  import FrameRefreshController from "./frame_refresh_controller.js"
46
+ import TimeagoController from "./timeago_controller.js"
46
47
 
47
48
  export default function (application) {
48
49
  // Register controllers here
@@ -90,4 +91,5 @@ export default function (application) {
90
91
  application.register("run-progress", RunProgressController)
91
92
  application.register("chart", ChartController)
92
93
  application.register("frame-refresh", FrameRefreshController)
94
+ application.register("timeago", TimeagoController)
93
95
  }
@@ -9,10 +9,15 @@ import { t } from "../i18n.js";
9
9
  // When present, SlimSelect's built-in client-side
10
10
  // filter is replaced by a debounced fetch through
11
11
  // this URL.
12
+ // addable — typed text can be added as a new option (chip
13
+ // inputs, see Form::Components::List).
14
+ // max-selected — caps how many options a multi-select keeps.
12
15
  export default class extends Controller {
13
16
  static values = {
14
17
  typeaheadUrl: String,
15
- typeaheadDebounceMs: { type: Number, default: 200 }
18
+ typeaheadDebounceMs: { type: Number, default: 200 },
19
+ addable: Boolean,
20
+ maxSelected: Number
16
21
  }
17
22
 
18
23
  connect() {
@@ -39,11 +44,19 @@ export default class extends Controller {
39
44
  // define placeholderText, searchText, searchPlaceholder and searchingText.
40
45
  const strings = t("plutonium.js.libraries.slim_select");
41
46
  if (strings && typeof strings === "object") {
42
- const { placeholderText, searchText, searchPlaceholder, searchingText } = strings;
43
- Object.assign(settings, { placeholderText, searchText, searchPlaceholder, searchingText });
47
+ const { placeholderText, searchText, searchPlaceholder, searchingText, addableText } = strings;
48
+ Object.assign(settings, { placeholderText, searchText, searchPlaceholder, searchingText, addableText });
44
49
  Object.keys(settings).forEach((k) => settings[k] === undefined && delete settings[k]);
45
50
  }
46
51
 
52
+ if (this.hasMaxSelectedValue) settings.maxSelected = this.maxSelectedValue;
53
+
54
+ if (this.addableValue) {
55
+ // A plain "Select Value" / "Search" hides that new values can be typed in.
56
+ settings.placeholderText = t("plutonium.js.slim_select.add_placeholder");
57
+ settings.searchPlaceholder = t("plutonium.js.slim_select.add_search_placeholder");
58
+ }
59
+
47
60
  this.modal = this.element.closest('[data-controller="remote-modal"]');
48
61
 
49
62
  if (this.modal) {
@@ -67,6 +80,20 @@ export default class extends Controller {
67
80
 
68
81
  const events = {};
69
82
 
83
+ if (this.modal) {
84
+ // A modal <dialog> gets `overflow: auto` from the UA stylesheet, so a
85
+ // dropdown that runs past the dialog's edge (a select near the bottom
86
+ // of a short modal) is clipped. The dialog never scrolls itself (its
87
+ // body owns the scroll regions), so let it overflow while open.
88
+ events.afterOpen = () => { this.modal.style.overflow = "visible"; };
89
+ events.afterClose = () => { this.modal.style.overflow = ""; };
90
+ }
91
+
92
+ if (this.addableValue) {
93
+ // Returning false rejects blank input instead of adding an empty chip.
94
+ events.addable = (value) => value.trim() || false;
95
+ }
96
+
70
97
  if (this.hasTypeaheadUrlValue && this.typeaheadUrlValue) {
71
98
  // Replace SlimSelect's client-side filter with a server fetch.
72
99
  // Returns the SlimSelect data array shape: {value, text}.
@@ -193,6 +220,8 @@ export default class extends Controller {
193
220
  this.boundModalReposition = null;
194
221
  }
195
222
 
223
+ if (this.modal) this.modal.style.overflow = "";
224
+
196
225
  if (this.slimSelect) {
197
226
  this.slimSelect.destroy();
198
227
  this.slimSelect = null;
@@ -0,0 +1,40 @@
1
+ import { Controller } from "@hotwired/stimulus"
2
+
3
+ // Renders a <time> as relative text ("3 days ago", "in 5 minutes") in the
4
+ // page's locale and keeps it current. The server-rendered absolute date stays
5
+ // in the element's title, so hovering still shows the exact time.
6
+ const UNITS = [
7
+ ["year", 60 * 60 * 24 * 365],
8
+ ["month", 60 * 60 * 24 * 30],
9
+ ["week", 60 * 60 * 24 * 7],
10
+ ["day", 60 * 60 * 24],
11
+ ["hour", 60 * 60],
12
+ ["minute", 60],
13
+ ["second", 1],
14
+ ]
15
+
16
+ export default class extends Controller {
17
+ static values = {
18
+ datetime: String,
19
+ refreshInterval: { type: Number, default: 60000 },
20
+ }
21
+
22
+ connect() {
23
+ this.formatter = new Intl.RelativeTimeFormat(document.documentElement.lang || undefined, { numeric: "auto" })
24
+ this.render()
25
+ this.timer = setInterval(() => this.render(), this.refreshIntervalValue)
26
+ }
27
+
28
+ disconnect() {
29
+ clearInterval(this.timer)
30
+ }
31
+
32
+ render() {
33
+ const date = new Date(this.datetimeValue)
34
+ if (isNaN(date)) return
35
+
36
+ const seconds = Math.round((date - Date.now()) / 1000)
37
+ const [unit, size] = UNITS.find(([, size]) => Math.abs(seconds) >= size) || UNITS[UNITS.length - 1]
38
+ this.element.textContent = this.formatter.format(Math.round(seconds / size), unit)
39
+ }
40
+ }
metadata CHANGED
@@ -1,13 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: plutonium
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.65.0
4
+ version: 0.67.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Stefan Froelich
8
8
  bindir: exe
9
9
  cert_chain: []
10
- date: 2026-10-02 00:00:00.000000000 Z
10
+ date: 2026-10-09 00:00:00.000000000 Z
11
11
  dependencies:
12
12
  - !ruby/object:Gem::Dependency
13
13
  name: zeitwerk
@@ -1234,6 +1234,7 @@ files:
1234
1234
  - lib/plutonium/query/filters/date.rb
1235
1235
  - lib/plutonium/query/filters/date_range.rb
1236
1236
  - lib/plutonium/query/filters/select.rb
1237
+ - lib/plutonium/query/filters/tags.rb
1237
1238
  - lib/plutonium/query/filters/text.rb
1238
1239
  - lib/plutonium/query/model_scope.rb
1239
1240
  - lib/plutonium/railtie.rb
@@ -1311,13 +1312,25 @@ files:
1311
1312
  - lib/plutonium/ui/display/components/association.rb
1312
1313
  - lib/plutonium/ui/display/components/attachment.rb
1313
1314
  - lib/plutonium/ui/display/components/badge.rb
1315
+ - lib/plutonium/ui/display/components/binary.rb
1314
1316
  - lib/plutonium/ui/display/components/boolean.rb
1317
+ - lib/plutonium/ui/display/components/code.rb
1315
1318
  - lib/plutonium/ui/display/components/color.rb
1316
1319
  - lib/plutonium/ui/display/components/currency.rb
1320
+ - lib/plutonium/ui/display/components/duration.rb
1321
+ - lib/plutonium/ui/display/components/file_size.rb
1317
1322
  - lib/plutonium/ui/display/components/formatted_value.rb
1323
+ - lib/plutonium/ui/display/components/key_value.rb
1324
+ - lib/plutonium/ui/display/components/list.rb
1318
1325
  - lib/plutonium/ui/display/components/markdown.rb
1326
+ - lib/plutonium/ui/display/components/number.rb
1319
1327
  - lib/plutonium/ui/display/components/phlexi_render.rb
1328
+ - lib/plutonium/ui/display/components/progress.rb
1329
+ - lib/plutonium/ui/display/components/rating.rb
1330
+ - lib/plutonium/ui/display/components/relative_time.rb
1331
+ - lib/plutonium/ui/display/components/rich_text.rb
1320
1332
  - lib/plutonium/ui/display/components/section.rb
1333
+ - lib/plutonium/ui/display/components/tags.rb
1321
1334
  - lib/plutonium/ui/display/options/inferred_types.rb
1322
1335
  - lib/plutonium/ui/display/resource.rb
1323
1336
  - lib/plutonium/ui/display/theme.rb
@@ -1326,6 +1339,7 @@ files:
1326
1339
  - lib/plutonium/ui/empty_card.rb
1327
1340
  - lib/plutonium/ui/export_button.rb
1328
1341
  - lib/plutonium/ui/form/base.rb
1342
+ - lib/plutonium/ui/form/components/binary.rb
1329
1343
  - lib/plutonium/ui/form/components/currency.rb
1330
1344
  - lib/plutonium/ui/form/components/easymde.rb
1331
1345
  - lib/plutonium/ui/form/components/flatpickr.rb
@@ -1333,12 +1347,17 @@ files:
1333
1347
  - lib/plutonium/ui/form/components/intl_tel_input.rb
1334
1348
  - lib/plutonium/ui/form/components/json.rb
1335
1349
  - lib/plutonium/ui/form/components/key_value_store.rb
1350
+ - lib/plutonium/ui/form/components/list.rb
1336
1351
  - lib/plutonium/ui/form/components/password.rb
1352
+ - lib/plutonium/ui/form/components/rating.rb
1337
1353
  - lib/plutonium/ui/form/components/resource_select.rb
1354
+ - lib/plutonium/ui/form/components/rich_text.rb
1338
1355
  - lib/plutonium/ui/form/components/section.rb
1339
1356
  - lib/plutonium/ui/form/components/secure_association.rb
1340
1357
  - lib/plutonium/ui/form/components/secure_polymorphic_association.rb
1341
1358
  - lib/plutonium/ui/form/components/sticky_footer.rb
1359
+ - lib/plutonium/ui/form/components/tags.rb
1360
+ - lib/plutonium/ui/form/components/tags_filter.rb
1342
1361
  - lib/plutonium/ui/form/components/toggle.rb
1343
1362
  - lib/plutonium/ui/form/components/uppy.rb
1344
1363
  - lib/plutonium/ui/form/concerns/renders_nested_resource_fields.rb
@@ -1376,6 +1395,9 @@ files:
1376
1395
  - lib/plutonium/ui/nav_grid_menu.rb
1377
1396
  - lib/plutonium/ui/nav_user.rb
1378
1397
  - lib/plutonium/ui/options/has_cents_field.rb
1398
+ - lib/plutonium/ui/options/rich_text_field.rb
1399
+ - lib/plutonium/ui/options/secret_field.rb
1400
+ - lib/plutonium/ui/options/tags_field.rb
1379
1401
  - lib/plutonium/ui/page/base.rb
1380
1402
  - lib/plutonium/ui/page/dashboard.rb
1381
1403
  - lib/plutonium/ui/page/edit.rb
@@ -1511,6 +1533,7 @@ files:
1511
1533
  - src/js/controllers/table_column_menu_controller.js
1512
1534
  - src/js/controllers/table_header_controller.js
1513
1535
  - src/js/controllers/textarea_autogrow_controller.js
1536
+ - src/js/controllers/timeago_controller.js
1514
1537
  - src/js/controllers/view_switcher_controller.js
1515
1538
  - src/js/controllers/wizard_controller.js
1516
1539
  - src/js/core.js