plutonium 0.50.0 → 0.51.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 (132) hide show
  1. checksums.yaml +4 -4
  2. data/.claude/skills/plutonium/SKILL.md +85 -102
  3. data/.claude/skills/plutonium-app/SKILL.md +572 -0
  4. data/.claude/skills/plutonium-auth/SKILL.md +163 -300
  5. data/.claude/skills/plutonium-behavior/SKILL.md +838 -0
  6. data/.claude/skills/plutonium-resource/SKILL.md +1176 -0
  7. data/.claude/skills/plutonium-tenancy/SKILL.md +655 -0
  8. data/.claude/skills/plutonium-testing/SKILL.md +6 -5
  9. data/.claude/skills/plutonium-ui/SKILL.md +900 -0
  10. data/CHANGELOG.md +27 -2
  11. data/Rakefile +2 -1
  12. data/app/assets/plutonium.css +1 -11
  13. data/app/assets/plutonium.js +1009 -1214
  14. data/app/assets/plutonium.js.map +3 -3
  15. data/app/assets/plutonium.min.js +52 -51
  16. data/app/assets/plutonium.min.js.map +3 -3
  17. data/docs/.vitepress/config.ts +37 -27
  18. data/docs/getting-started/index.md +22 -29
  19. data/docs/getting-started/installation.md +37 -80
  20. data/docs/getting-started/tutorial/index.md +4 -5
  21. data/docs/guides/adding-resources.md +66 -377
  22. data/docs/guides/authentication.md +94 -463
  23. data/docs/guides/authorization.md +124 -370
  24. data/docs/guides/creating-packages.md +94 -296
  25. data/docs/guides/custom-actions.md +121 -441
  26. data/docs/guides/index.md +22 -42
  27. data/docs/guides/multi-tenancy.md +116 -187
  28. data/docs/guides/nested-resources.md +103 -431
  29. data/docs/guides/search-filtering.md +123 -240
  30. data/docs/guides/testing.md +5 -4
  31. data/docs/guides/theming.md +157 -407
  32. data/docs/guides/troubleshooting.md +5 -3
  33. data/docs/guides/user-invites.md +106 -425
  34. data/docs/guides/user-profile.md +76 -243
  35. data/docs/index.md +1 -1
  36. data/docs/reference/app/generators.md +517 -0
  37. data/docs/reference/app/index.md +158 -0
  38. data/docs/reference/app/packages.md +146 -0
  39. data/docs/reference/app/portals.md +377 -0
  40. data/docs/reference/auth/accounts.md +230 -0
  41. data/docs/reference/auth/index.md +88 -0
  42. data/docs/reference/auth/profile.md +185 -0
  43. data/docs/reference/behavior/controllers.md +395 -0
  44. data/docs/reference/behavior/index.md +22 -0
  45. data/docs/reference/behavior/interactions.md +341 -0
  46. data/docs/reference/behavior/policies.md +417 -0
  47. data/docs/reference/index.md +56 -49
  48. data/docs/reference/resource/actions.md +423 -0
  49. data/docs/reference/resource/definition.md +508 -0
  50. data/docs/reference/resource/index.md +50 -0
  51. data/docs/reference/resource/model.md +348 -0
  52. data/docs/reference/resource/query.md +305 -0
  53. data/docs/reference/tenancy/entity-scoping.md +361 -0
  54. data/docs/reference/tenancy/index.md +36 -0
  55. data/docs/reference/tenancy/invites.md +393 -0
  56. data/docs/reference/tenancy/nested-resources.md +267 -0
  57. data/docs/reference/testing/index.md +287 -0
  58. data/docs/reference/ui/assets.md +400 -0
  59. data/docs/reference/ui/components.md +165 -0
  60. data/docs/reference/ui/displays.md +104 -0
  61. data/docs/reference/ui/forms.md +284 -0
  62. data/docs/reference/ui/index.md +30 -0
  63. data/docs/reference/ui/layouts.md +106 -0
  64. data/docs/reference/ui/pages.md +189 -0
  65. data/docs/reference/ui/tables.md +117 -0
  66. data/docs/superpowers/specs/2026-05-09-typeahead-endpoint-design.md +203 -0
  67. data/docs/superpowers/specs/2026-05-12-skill-compaction-design.md +99 -0
  68. data/docs/superpowers/specs/2026-05-13-docs-restructure-design.md +186 -0
  69. data/gemfiles/rails_7.gemfile.lock +1 -1
  70. data/gemfiles/rails_8.0.gemfile.lock +1 -1
  71. data/gemfiles/rails_8.1.gemfile.lock +1 -1
  72. data/lib/generators/pu/core/update/update_generator.rb +0 -20
  73. data/lib/generators/pu/invites/install_generator.rb +1 -0
  74. data/lib/plutonium/definition/base.rb +1 -1
  75. data/lib/plutonium/definition/{views.rb → index_views.rb} +21 -20
  76. data/lib/plutonium/helpers/turbo_helper.rb +11 -0
  77. data/lib/plutonium/helpers/turbo_stream_actions_helper.rb +14 -0
  78. data/lib/plutonium/resource/controller.rb +1 -0
  79. data/lib/plutonium/resource/controllers/crud_actions.rb +19 -1
  80. data/lib/plutonium/resource/controllers/typeahead.rb +180 -0
  81. data/lib/plutonium/resource/policy.rb +7 -0
  82. data/lib/plutonium/routing/mapper_extensions.rb +15 -0
  83. data/lib/plutonium/ui/component/methods.rb +4 -0
  84. data/lib/plutonium/ui/form/base.rb +6 -2
  85. data/lib/plutonium/ui/form/components/json.rb +58 -0
  86. data/lib/plutonium/ui/form/components/resource_select.rb +62 -8
  87. data/lib/plutonium/ui/form/components/secure_association.rb +98 -22
  88. data/lib/plutonium/ui/form/concerns/typeahead_attributes.rb +83 -0
  89. data/lib/plutonium/ui/form/resource.rb +0 -4
  90. data/lib/plutonium/ui/grid/resource.rb +1 -1
  91. data/lib/plutonium/ui/layout/base.rb +1 -0
  92. data/lib/plutonium/ui/page/base.rb +0 -7
  93. data/lib/plutonium/ui/page/index.rb +4 -4
  94. data/lib/plutonium/ui/table/resource.rb +1 -1
  95. data/lib/plutonium/version.rb +1 -1
  96. data/lib/plutonium.rb +8 -0
  97. data/lib/tasks/release.rake +15 -1
  98. data/package.json +10 -10
  99. data/src/css/slim_select.css +4 -0
  100. data/src/js/controllers/slim_select_controller.js +61 -0
  101. data/src/js/turbo/turbo_actions.js +33 -0
  102. data/yarn.lock +553 -543
  103. metadata +44 -33
  104. data/.claude/skills/plutonium-assets/SKILL.md +0 -512
  105. data/.claude/skills/plutonium-controller/SKILL.md +0 -396
  106. data/.claude/skills/plutonium-create-resource/SKILL.md +0 -303
  107. data/.claude/skills/plutonium-definition/SKILL.md +0 -1223
  108. data/.claude/skills/plutonium-entity-scoping/SKILL.md +0 -317
  109. data/.claude/skills/plutonium-forms/SKILL.md +0 -465
  110. data/.claude/skills/plutonium-installation/SKILL.md +0 -331
  111. data/.claude/skills/plutonium-interaction/SKILL.md +0 -413
  112. data/.claude/skills/plutonium-invites/SKILL.md +0 -408
  113. data/.claude/skills/plutonium-model/SKILL.md +0 -440
  114. data/.claude/skills/plutonium-nested-resources/SKILL.md +0 -360
  115. data/.claude/skills/plutonium-package/SKILL.md +0 -198
  116. data/.claude/skills/plutonium-policy/SKILL.md +0 -456
  117. data/.claude/skills/plutonium-portal/SKILL.md +0 -410
  118. data/.claude/skills/plutonium-views/SKILL.md +0 -651
  119. data/docs/reference/assets/index.md +0 -496
  120. data/docs/reference/controller/index.md +0 -412
  121. data/docs/reference/definition/actions.md +0 -462
  122. data/docs/reference/definition/fields.md +0 -383
  123. data/docs/reference/definition/index.md +0 -326
  124. data/docs/reference/definition/query.md +0 -351
  125. data/docs/reference/generators/index.md +0 -648
  126. data/docs/reference/interaction/index.md +0 -449
  127. data/docs/reference/model/features.md +0 -248
  128. data/docs/reference/model/index.md +0 -218
  129. data/docs/reference/policy/index.md +0 -456
  130. data/docs/reference/portal/index.md +0 -379
  131. data/docs/reference/views/forms.md +0 -411
  132. data/docs/reference/views/index.md +0 -544
data/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@radioactive-labs/plutonium",
3
- "version": "0.50.0",
3
+ "version": "0.51.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",
@@ -25,27 +25,27 @@
25
25
  "@uppy/dashboard": "^4.1.3",
26
26
  "@uppy/image-editor": "^3.2.1",
27
27
  "@uppy/xhr-upload": "^4.2.3",
28
- "dompurify": "^3.2.2",
28
+ "dompurify": "^3.4.3",
29
29
  "lodash.debounce": "^4.0.8",
30
30
  "marked": "^15.0.3"
31
31
  },
32
32
  "devDependencies": {
33
33
  "@tailwindcss/forms": "^0.5.10",
34
- "@tailwindcss/postcss": "^4.0.9",
34
+ "@tailwindcss/postcss": "^4.3.0",
35
35
  "@tailwindcss/typography": "^0.5.16",
36
36
  "chokidar-cli": "^3.0.0",
37
37
  "concurrently": "^8.2.2",
38
38
  "cssnano": "^7.0.2",
39
- "esbuild": "^0.20.1",
39
+ "esbuild": "^0.28.0",
40
40
  "esbuild-plugin-manifest": "^1.0.3",
41
41
  "flowbite-typography": "^1.0.5",
42
- "mermaid": "^11.4.0",
43
- "postcss": "^8.4.35",
44
- "postcss-cli": "^11.0.0",
42
+ "mermaid": "^11.15.0",
43
+ "postcss": "^8.5.14",
44
+ "postcss-cli": "^11.0.1",
45
45
  "postcss-hash": "^3.0.0",
46
- "postcss-import": "^16.1.0",
47
- "tailwindcss": "^4.0.9",
48
- "vitepress": "^1.4.1",
46
+ "postcss-import": "^16.1.1",
47
+ "tailwindcss": "^4.3.0",
48
+ "vitepress": "^1.6.4",
49
49
  "vitepress-plugin-mermaid": "^2.0.17"
50
50
  },
51
51
  "scripts": {
@@ -151,6 +151,10 @@
151
151
  @apply absolute flex h-auto flex-col w-auto max-h-72 border transition-all duration-200 opacity-0 z-[10000] overflow-hidden;
152
152
  background-color: var(--pu-surface);
153
153
  border-color: var(--pu-border);
154
+ /* Default text color for everything inside the panel — covers the
155
+ "No Results" text and any other slim-select chrome that doesn't
156
+ declare its own color rule. Specific rules below still win. */
157
+ color: var(--pu-text);
154
158
  box-shadow: var(--pu-shadow-md);
155
159
  transform: scaleY(0);
156
160
  transform-origin: top;
@@ -1,7 +1,19 @@
1
1
  import { Controller } from "@hotwired/stimulus";
2
2
 
3
3
  // Connects to data-controller="slim-select"
4
+ //
5
+ // Optional values (used by the typeahead-capable ResourceSelect):
6
+ // typeahead-url — backend endpoint that returns
7
+ // {results: [{value, label}, ...], has_more: bool}.
8
+ // When present, SlimSelect's built-in client-side
9
+ // filter is replaced by a debounced fetch through
10
+ // this URL.
4
11
  export default class extends Controller {
12
+ static values = {
13
+ typeaheadUrl: String,
14
+ typeaheadDebounceMs: { type: Number, default: 200 }
15
+ }
16
+
5
17
  connect() {
6
18
  if (this.slimSelect) return;
7
19
 
@@ -47,9 +59,18 @@ export default class extends Controller {
47
59
  settings.openPosition = "auto";
48
60
  }
49
61
 
62
+ const events = {};
63
+
64
+ if (this.hasTypeaheadUrlValue && this.typeaheadUrlValue) {
65
+ // Replace SlimSelect's client-side filter with a server fetch.
66
+ // Returns the SlimSelect data array shape: {value, text}.
67
+ events.search = (search, currentData) => this.#typeaheadFetch(search, currentData);
68
+ }
69
+
50
70
  this.slimSelect = new SlimSelect({
51
71
  select: this.element,
52
72
  settings: settings,
73
+ events: events,
53
74
  });
54
75
 
55
76
  // Add event listeners for better positioning
@@ -177,6 +198,46 @@ export default class extends Controller {
177
198
  this.#cleanupSlimSelect();
178
199
  }
179
200
 
201
+ // Server-driven search. SlimSelect calls events.search on each
202
+ // keystroke; we debounce so that rapid typing produces a single
203
+ // request, and abort any in-flight fetch when a newer one starts.
204
+ // Returns a Promise resolving to either a DataArray (rendered as
205
+ // options) or a string (rendered as the no-results label).
206
+ #typeaheadFetch(search, _currentData) {
207
+ if (this._typeaheadDebounce) clearTimeout(this._typeaheadDebounce);
208
+ if (this._typeaheadAbort) this._typeaheadAbort.abort();
209
+
210
+ return new Promise((resolve) => {
211
+ this._typeaheadDebounce = setTimeout(() => {
212
+ this._typeaheadAbort = new AbortController();
213
+ this.#performTypeaheadFetch(search, this._typeaheadAbort.signal).then(resolve);
214
+ }, this.typeaheadDebounceMsValue);
215
+ });
216
+ }
217
+
218
+ async #performTypeaheadFetch(search, signal) {
219
+ const url = new URL(this.typeaheadUrlValue, window.location.origin);
220
+ url.searchParams.set("q", search || "");
221
+
222
+ try {
223
+ const res = await fetch(url.toString(), {
224
+ headers: { Accept: "application/json" },
225
+ signal: signal,
226
+ });
227
+ if (!res.ok) return "Search failed";
228
+ const json = await res.json();
229
+ const results = Array.isArray(json.results) ? json.results : [];
230
+ return results.map((row) => ({
231
+ value: String(row.value ?? ""),
232
+ text: String(row.label ?? ""),
233
+ }));
234
+ } catch (e) {
235
+ if (e.name === "AbortError") return [];
236
+ console.warn("[slim-select] typeahead error", e);
237
+ return "Search failed";
238
+ }
239
+ }
240
+
180
241
  #handleMorph() {
181
242
  if (!this.element.isConnected) return;
182
243
 
@@ -6,3 +6,36 @@ Turbo.StreamActions.redirect = function () {
6
6
  const url = this.getAttribute("url")
7
7
  Turbo.visit(url)
8
8
  }
9
+
10
+ // Closes the <dialog> rendered inside the targeted turbo-frame and
11
+ // empties the frame so the dialog can be re-opened later. Used by the
12
+ // stacked-modal flow: after a successful create inside the secondary
13
+ // modal, the server tells the browser to dismiss it.
14
+ Turbo.StreamActions.close_frame = function () {
15
+ const frameId = this.getAttribute("target")
16
+ if (!frameId) return
17
+
18
+ const frame = document.getElementById(frameId)
19
+ if (!frame) return
20
+
21
+ const dialog = frame.querySelector("dialog")
22
+ if (dialog && typeof dialog.close === "function") dialog.close()
23
+
24
+ // Clearing the frame's content keeps a future visit to the same URL
25
+ // re-fetching (turbo would otherwise treat the frame as cached).
26
+ frame.innerHTML = ""
27
+ frame.removeAttribute("src")
28
+ }
29
+
30
+ // Reloads the targeted turbo-frame from its current src. Used after a
31
+ // secondary-modal action mutates data the primary modal depends on
32
+ // (e.g. a newly created association option) so the primary re-renders.
33
+ Turbo.StreamActions.reload_frame = function () {
34
+ const frameId = this.getAttribute("target")
35
+ if (!frameId) return
36
+
37
+ const frame = document.getElementById(frameId)
38
+ if (!frame || typeof frame.reload !== "function") return
39
+
40
+ frame.reload()
41
+ }