plutonium 0.60.5 → 0.61.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 (175) hide show
  1. checksums.yaml +4 -4
  2. data/.claude/skills/plutonium/SKILL.md +19 -1
  3. data/.claude/skills/plutonium-app/SKILL.md +41 -0
  4. data/.claude/skills/plutonium-auth/SKILL.md +40 -0
  5. data/.claude/skills/plutonium-behavior/SKILL.md +47 -1
  6. data/.claude/skills/plutonium-kanban/SKILL.md +313 -0
  7. data/.claude/skills/plutonium-resource/SKILL.md +40 -0
  8. data/.claude/skills/plutonium-tenancy/SKILL.md +43 -0
  9. data/.claude/skills/plutonium-testing/SKILL.md +38 -0
  10. data/.claude/skills/plutonium-ui/SKILL.md +51 -0
  11. data/.claude/skills/plutonium-wizard/SKILL.md +469 -0
  12. data/.cliff.toml +6 -0
  13. data/Appraisals +3 -0
  14. data/CHANGELOG.md +549 -439
  15. data/CLAUDE.md +15 -7
  16. data/app/assets/plutonium.css +1 -1
  17. data/app/assets/plutonium.js +895 -193
  18. data/app/assets/plutonium.js.map +4 -4
  19. data/app/assets/plutonium.min.js +53 -53
  20. data/app/assets/plutonium.min.js.map +4 -4
  21. data/app/views/layouts/basic.html.erb +7 -0
  22. data/app/views/plutonium/_flash_toasts.html.erb +2 -46
  23. data/app/views/plutonium/_toast.html.erb +52 -0
  24. data/app/views/resource/_resource_kanban.html.erb +1 -0
  25. data/db/migrate/wizard/20260615000001_create_plutonium_wizard_sessions.rb +57 -0
  26. data/docs/.vitepress/config.ts +24 -0
  27. data/docs/guides/index.md +2 -0
  28. data/docs/guides/kanban.md +447 -0
  29. data/docs/guides/wizards.md +447 -0
  30. data/docs/public/images/guides/kanban-after-move.png +0 -0
  31. data/docs/public/images/guides/kanban-board-light.png +0 -0
  32. data/docs/public/images/guides/kanban-board.png +0 -0
  33. data/docs/public/images/guides/kanban-show-centered-modal.png +0 -0
  34. data/docs/public/images/guides/kanban-wip-toast.png +0 -0
  35. data/docs/public/images/guides/wizards-chooser.png +0 -0
  36. data/docs/public/images/guides/wizards-completed.png +0 -0
  37. data/docs/public/images/guides/wizards-index-action.png +0 -0
  38. data/docs/public/images/guides/wizards-repeater.png +0 -0
  39. data/docs/public/images/guides/wizards-review.png +0 -0
  40. data/docs/public/images/guides/wizards-step.png +0 -0
  41. data/docs/reference/behavior/policies.md +1 -1
  42. data/docs/reference/index.md +14 -0
  43. data/docs/reference/kanban/authorization.md +62 -0
  44. data/docs/reference/kanban/dsl.md +293 -0
  45. data/docs/reference/kanban/index.md +40 -0
  46. data/docs/reference/kanban/positioning.md +162 -0
  47. data/docs/reference/resource/definition.md +16 -0
  48. data/docs/reference/ui/forms.md +36 -0
  49. data/docs/reference/ui/pages.md +2 -0
  50. data/docs/reference/wizard/anchoring-resume.md +194 -0
  51. data/docs/reference/wizard/dsl.md +332 -0
  52. data/docs/reference/wizard/index.md +33 -0
  53. data/docs/reference/wizard/one-time.md +129 -0
  54. data/docs/reference/wizard/registration-launch.md +177 -0
  55. data/docs/reference/wizard/storage-config.md +151 -0
  56. data/docs/superpowers/plans/2026-06-14-form-sectioning.md +2 -2
  57. data/docs/superpowers/plans/2026-06-15-wizard-dsl.md +1619 -0
  58. data/docs/superpowers/plans/2026-06-15-wizard-dsl.md.tasks.json +68 -0
  59. data/docs/superpowers/plans/2026-06-26-kanban-dsl.md +1128 -0
  60. data/docs/superpowers/plans/2026-06-26-kanban-dsl.md.tasks.json +24 -0
  61. data/docs/superpowers/specs/2026-06-15-wizard-dsl-design.md +836 -0
  62. data/docs/superpowers/specs/2026-06-15-wizard-dsl-examples.rb +245 -0
  63. data/docs/superpowers/specs/2026-06-17-wizard-relaunch-prompt-design.md +86 -0
  64. data/docs/superpowers/specs/2026-06-18-wizard-attachments-design.md +101 -0
  65. data/docs/superpowers/specs/2026-06-18-wizard-hosting-design.md +220 -0
  66. data/docs/superpowers/specs/2026-06-26-kanban-dsl-design.md +388 -0
  67. data/gemfiles/postgres.gemfile +8 -0
  68. data/gemfiles/postgres.gemfile.lock +321 -0
  69. data/gemfiles/rails_7.gemfile +1 -0
  70. data/gemfiles/rails_7.gemfile.lock +1 -1
  71. data/gemfiles/rails_8.0.gemfile +1 -0
  72. data/gemfiles/rails_8.0.gemfile.lock +1 -1
  73. data/gemfiles/rails_8.1.gemfile +1 -0
  74. data/gemfiles/rails_8.1.gemfile.lock +14 -1
  75. data/lib/generators/pu/invites/templates/packages/invites/app/controllers/invites/user_invitations_controller.rb.tt +6 -1
  76. data/lib/plutonium/action/base.rb +9 -0
  77. data/lib/plutonium/auth/rodauth.rb +1 -2
  78. data/lib/plutonium/configuration.rb +4 -0
  79. data/lib/plutonium/core/controller.rb +20 -1
  80. data/lib/plutonium/definition/base.rb +25 -0
  81. data/lib/plutonium/definition/form_layout.rb +54 -35
  82. data/lib/plutonium/definition/index_views.rb +54 -1
  83. data/lib/plutonium/definition/wizards.rb +209 -0
  84. data/lib/plutonium/invites/concerns/invite_token.rb +9 -0
  85. data/lib/plutonium/invites/concerns/invite_user.rb +9 -0
  86. data/lib/plutonium/invites/controller.rb +4 -1
  87. data/lib/plutonium/kanban/action.rb +7 -0
  88. data/lib/plutonium/kanban/board.rb +40 -0
  89. data/lib/plutonium/kanban/broadcaster.rb +54 -0
  90. data/lib/plutonium/kanban/column.rb +69 -0
  91. data/lib/plutonium/kanban/context.rb +15 -0
  92. data/lib/plutonium/kanban/dsl.rb +71 -0
  93. data/lib/plutonium/kanban/grouping.rb +51 -0
  94. data/lib/plutonium/kanban/positioning.rb +75 -0
  95. data/lib/plutonium/kanban.rb +11 -0
  96. data/lib/plutonium/migrations.rb +40 -0
  97. data/lib/plutonium/positioning.rb +146 -0
  98. data/lib/plutonium/railtie.rb +33 -0
  99. data/lib/plutonium/resource/controller.rb +2 -0
  100. data/lib/plutonium/resource/controllers/crud_actions.rb +1 -1
  101. data/lib/plutonium/resource/controllers/kanban_actions.rb +455 -0
  102. data/lib/plutonium/resource/controllers/wizard_actions.rb +165 -0
  103. data/lib/plutonium/resource/policy.rb +8 -0
  104. data/lib/plutonium/routing/mapper_extensions.rb +44 -0
  105. data/lib/plutonium/routing/wizard_registration.rb +289 -0
  106. data/lib/plutonium/ui/display/resource.rb +17 -12
  107. data/lib/plutonium/ui/form/base.rb +19 -5
  108. data/lib/plutonium/ui/form/components/password.rb +126 -0
  109. data/lib/plutonium/ui/form/components/uppy.rb +6 -3
  110. data/lib/plutonium/ui/form/options/inferred_types.rb +20 -0
  111. data/lib/plutonium/ui/form/resource.rb +1 -1
  112. data/lib/plutonium/ui/form/wizard.rb +63 -0
  113. data/lib/plutonium/ui/grid/card.rb +16 -5
  114. data/lib/plutonium/ui/kanban/card.rb +67 -0
  115. data/lib/plutonium/ui/kanban/color_dot.rb +36 -0
  116. data/lib/plutonium/ui/kanban/column.rb +324 -0
  117. data/lib/plutonium/ui/kanban/resource.rb +212 -0
  118. data/lib/plutonium/ui/layout/resource_layout.rb +7 -1
  119. data/lib/plutonium/ui/modal/base.rb +30 -3
  120. data/lib/plutonium/ui/modal/centered.rb +5 -2
  121. data/lib/plutonium/ui/page/index.rb +1 -0
  122. data/lib/plutonium/ui/page/show.rb +23 -0
  123. data/lib/plutonium/ui/page/wizard.rb +371 -0
  124. data/lib/plutonium/ui/page/wizard_chooser.rb +97 -0
  125. data/lib/plutonium/ui/page/wizard_completed.rb +86 -0
  126. data/lib/plutonium/ui/table/base.rb +1 -1
  127. data/lib/plutonium/ui/table/components/view_switcher.rb +2 -1
  128. data/lib/plutonium/ui/wizard/review.rb +196 -0
  129. data/lib/plutonium/ui/wizard/stepper.rb +122 -0
  130. data/lib/plutonium/ui/wizard/summary_display.rb +59 -0
  131. data/lib/plutonium/version.rb +1 -1
  132. data/lib/plutonium/wizard/attachment_data.rb +42 -0
  133. data/lib/plutonium/wizard/attachments.rb +226 -0
  134. data/lib/plutonium/wizard/base.rb +216 -0
  135. data/lib/plutonium/wizard/base_controller.rb +31 -0
  136. data/lib/plutonium/wizard/configuration.rb +42 -0
  137. data/lib/plutonium/wizard/controller.rb +162 -0
  138. data/lib/plutonium/wizard/data.rb +134 -0
  139. data/lib/plutonium/wizard/driving.rb +639 -0
  140. data/lib/plutonium/wizard/dsl.rb +336 -0
  141. data/lib/plutonium/wizard/errors.rb +27 -0
  142. data/lib/plutonium/wizard/field_capture.rb +157 -0
  143. data/lib/plutonium/wizard/field_importer.rb +208 -0
  144. data/lib/plutonium/wizard/gate.rb +171 -0
  145. data/lib/plutonium/wizard/instance_key.rb +97 -0
  146. data/lib/plutonium/wizard/lazy_persisted.rb +77 -0
  147. data/lib/plutonium/wizard/resume.rb +250 -0
  148. data/lib/plutonium/wizard/review_step.rb +48 -0
  149. data/lib/plutonium/wizard/route_resolution.rb +40 -0
  150. data/lib/plutonium/wizard/runner.rb +684 -0
  151. data/lib/plutonium/wizard/session.rb +53 -0
  152. data/lib/plutonium/wizard/state.rb +35 -0
  153. data/lib/plutonium/wizard/step.rb +61 -0
  154. data/lib/plutonium/wizard/step_adapter.rb +103 -0
  155. data/lib/plutonium/wizard/store/active_record.rb +174 -0
  156. data/lib/plutonium/wizard/store/base.rb +42 -0
  157. data/lib/plutonium/wizard/store/memory.rb +44 -0
  158. data/lib/plutonium/wizard/sweep_job.rb +76 -0
  159. data/lib/plutonium/wizard.rb +86 -0
  160. data/lib/plutonium.rb +5 -0
  161. data/lib/rodauth/features/case_insensitive_login.rb +1 -1
  162. data/lib/tasks/release.rake +144 -191
  163. data/package.json +3 -3
  164. data/src/css/components.css +132 -0
  165. data/src/js/controllers/attachment_input_controller.js +15 -1
  166. data/src/js/controllers/dirty_form_guard_controller.js +155 -27
  167. data/src/js/controllers/kanban_controller.js +330 -0
  168. data/src/js/controllers/password_sentinel_controller.js +39 -0
  169. data/src/js/controllers/register_controllers.js +6 -0
  170. data/src/js/controllers/remote_modal_controller.js +10 -0
  171. data/src/js/controllers/row_click_controller.js +14 -1
  172. data/src/js/controllers/wizard_controller.js +54 -0
  173. data/src/js/turbo/turbo_confirm.js +1 -1
  174. data/yarn.lock +271 -282
  175. metadata +100 -1
data/lib/plutonium.rb CHANGED
@@ -10,7 +10,11 @@ require "phlexi-display"
10
10
  require "phlexi-form"
11
11
  require "phlexi-table"
12
12
 
13
+ require_relative "plutonium/wizard"
14
+ require_relative "plutonium/migrations"
13
15
  require_relative "plutonium/configuration"
16
+ require_relative "plutonium/positioning"
17
+ require_relative "plutonium/kanban"
14
18
  require_relative "rodauth/plugins" if defined?(Rodauth)
15
19
 
16
20
  # Plutonium module
@@ -46,6 +50,7 @@ module Plutonium
46
50
  loader.ignore("#{__dir__}/rodauth")
47
51
  loader.inflector.inflect("ui" => "UI")
48
52
  loader.inflector.inflect("workflow_dsl" => "WorkflowDSL")
53
+ loader.inflector.inflect("dsl" => "DSL")
49
54
  loader.enable_reloading if defined?(Rails.env) && Rails.env.development?
50
55
  loader.setup
51
56
  end
@@ -10,7 +10,7 @@ module Rodauth
10
10
  logins = [login_param]
11
11
  logins << login_confirm_param if respond_to?(:login_confirm_param)
12
12
 
13
- if [logins].include?(key)
13
+ if logins.include?(key)
14
14
  super.downcase
15
15
  else
16
16
  super
@@ -1,230 +1,183 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ # Release flow
4
+ # ------------
5
+ # Publishing happens from a laptop. CI does NOT push to any registry — it only
6
+ # cuts the GitHub Release (with notes + the built gem) when the tag lands.
7
+ #
8
+ # 1. rake release:prepare # auto-computes next version via git-cliff
9
+ # rake release:prepare[1.2.3] # ...or pass one explicitly
10
+ # # → bumps + regenerates changelog + builds assets,
11
+ # # STAGES them, and shows the diff. Nothing is committed.
12
+ # 2. git diff --cached # review the staged changes
13
+ # 3. rake release:publish # commits, publishes gem + npm, then tags + pushes
14
+ # # → CI cuts the Release from the tag
15
+ #
16
+ # To abort after prepare: git reset --hard (discards the staged changes).
17
+ # release:publish is idempotent and resumable: it skips a gem/npm already live
18
+ # and only tags if the tag is missing, so a partial failure can just be re-run.
19
+
20
+ require "json"
21
+
22
+ RELEASE_CLIFF_CONFIG = ".cliff.toml"
23
+ RELEASE_VERSION_FILE = "lib/plutonium/version.rb"
24
+ RELEASE_PACKAGE_JSON = "package.json"
25
+ RELEASE_NPM_PACKAGE = "@radioactive-labs/plutonium"
26
+
3
27
  namespace :release do
4
- desc "Display next version based on conventional commits"
5
- task :next_version do
6
- current_version = Plutonium::VERSION
7
- puts "Current version: #{current_version}"
28
+ # --- helpers --------------------------------------------------------------
8
29
 
9
- # Find the tag to compare against
10
- version_tag = "v#{current_version}"
11
- tag_exists = system("git rev-parse #{version_tag} >/dev/null 2>&1")
12
-
13
- unless tag_exists
14
- # Fall back to most recent tag
15
- version_tag = `git describe --tags --abbrev=0 2>/dev/null`.strip
16
- if version_tag.empty?
17
- puts "No tags found, comparing against initial commit"
18
- version_tag = `git rev-list --max-parents=0 HEAD`.strip
19
- else
20
- puts "Tag v#{current_version} not found, comparing against #{version_tag}"
21
- end
22
- end
30
+ def current_version
31
+ File.read(RELEASE_VERSION_FILE)[/VERSION = "([\d.]+)"/, 1] ||
32
+ abort("Could not read VERSION from #{RELEASE_VERSION_FILE}")
33
+ end
23
34
 
24
- # Check for breaking changes, features, or fixes since last tag
25
- breaking = `git log #{version_tag}..HEAD --oneline | grep -i "BREAKING CHANGE"`.strip
26
- features = `git log #{version_tag}..HEAD --oneline | grep "^[a-f0-9]* feat"`.strip
27
- fixes = `git log #{version_tag}..HEAD --oneline | grep "^[a-f0-9]* fix"`.strip
28
-
29
- major, minor, patch = current_version.split(".").map(&:to_i)
30
-
31
- if !breaking.empty?
32
- next_version = "#{major + 1}.0.0"
33
- puts "Next version (breaking changes): #{next_version}"
34
- elsif !features.empty?
35
- next_version = "#{major}.#{minor + 1}.0"
36
- puts "Next version (new features): #{next_version}"
37
- elsif !fixes.empty?
38
- next_version = "#{major}.#{minor}.#{patch + 1}"
39
- puts "Next version (bug fixes): #{next_version}"
40
- else
41
- puts "No changes detected"
42
- end
35
+ def git_cliff?
36
+ system("which git-cliff > /dev/null 2>&1")
43
37
  end
44
38
 
45
- desc "Prepare a new release"
46
- task :prepare, [:version] do |_t, args|
47
- version = args[:version]
39
+ # Next version per conventional commits. git-cliff owns the semver math
40
+ # (including the pre-1.0 rules configured under [bump] in .cliff.toml).
41
+ def computed_next_version
42
+ abort "git-cliff not found. Install with: brew install git-cliff" unless git_cliff?
43
+ bumped = `git-cliff --config #{RELEASE_CLIFF_CONFIG} --bumped-version 2>/dev/null`.strip
44
+ abort "git-cliff could not compute a version (no conventional commits since last tag?)" if bumped.empty?
45
+ bumped.delete_prefix("v")
46
+ end
48
47
 
49
- unless version
50
- puts "Usage: rake release:prepare[VERSION]"
51
- puts "Example: rake release:prepare[0.27.0]"
52
- exit 1
53
- end
48
+ def gem_published?(version)
49
+ out = `gem list --remote --exact --all plutonium 2>/dev/null`
50
+ out.include?("#{version},") || out.include?("#{version})") || out.include?(" #{version} ")
51
+ end
52
+
53
+ def npm_published?(version)
54
+ published = `npm view #{RELEASE_NPM_PACKAGE}@#{version} version 2>/dev/null`.strip
55
+ published == version
56
+ end
57
+
58
+ # --- version --------------------------------------------------------------
59
+
60
+ desc "Show the next version computed from conventional commits"
61
+ task :version do
62
+ puts "Current version: #{current_version}"
63
+ puts "Next version: #{computed_next_version}"
64
+ end
65
+
66
+ # --- prepare --------------------------------------------------------------
67
+
68
+ desc "Stage a release (bump + changelog + assets) for review. Version optional; git-cliff computes it."
69
+ task :prepare, [:version] do |_t, args|
70
+ version = args[:version] || computed_next_version
54
71
 
55
- # Validate version format
56
72
  unless version.match?(/^\d+\.\d+\.\d+$/)
57
- puts "Error: Version must be in format X.Y.Z"
58
- exit 1
73
+ abort "Error: version must be in format X.Y.Z (got #{version.inspect})"
59
74
  end
60
75
 
61
- # Update version.rb
62
- version_file = "lib/plutonium/version.rb"
63
- content = File.read(version_file)
64
- updated_content = content.gsub(/VERSION = "[\d.]+"/, %(VERSION = "#{version}"))
65
- File.write(version_file, updated_content)
66
- puts "✓ Updated #{version_file}"
67
-
68
- # Update package.json version
69
- package_json_file = "package.json"
70
- if File.exist?(package_json_file)
71
- package_content = File.read(package_json_file)
72
- updated_package = package_content.gsub(/"version":\s*"[\d.]+"/, %("version": "#{version}"))
73
- File.write(package_json_file, updated_package)
74
- puts "✓ Updated #{package_json_file}"
76
+ unless `git status --porcelain`.strip.empty?
77
+ abort "Error: working tree is dirty. Commit or stash first."
75
78
  end
76
79
 
77
- # Generate changelog using git-cliff
78
- if system("which git-cliff > /dev/null 2>&1")
79
- system("git-cliff --tag v#{version} -o CHANGELOG.md")
80
- puts "✓ Generated CHANGELOG.md"
81
- else
82
- puts " git-cliff not found. Install with: brew install git-cliff"
83
- puts " Skipping changelog generation"
84
- end
80
+ puts "Preparing release v#{version}..."
81
+
82
+ # Bump version.rb
83
+ content = File.read(RELEASE_VERSION_FILE)
84
+ File.write(RELEASE_VERSION_FILE, content.gsub(/VERSION = "[\d.]+"/, %(VERSION = "#{version}")))
85
+ puts " #{RELEASE_VERSION_FILE}"
86
+
87
+ # Bump package.json
88
+ pkg = File.read(RELEASE_PACKAGE_JSON)
89
+ File.write(RELEASE_PACKAGE_JSON, pkg.gsub(/"version":\s*"[\d.]+"/, %("version": "#{version}")))
90
+ puts "✓ #{RELEASE_PACKAGE_JSON}"
85
91
 
86
- # Build front-end assets
92
+ # Changelog same config CI uses for release notes, so they agree.
93
+ abort "git-cliff not found. Install with: brew install git-cliff" unless git_cliff?
94
+ system("git-cliff", "--config", RELEASE_CLIFF_CONFIG, "--tag", "v#{version}", "-o", "CHANGELOG.md") ||
95
+ abort("Changelog generation failed")
96
+ puts "✓ CHANGELOG.md"
97
+
98
+ # Rebuild committed frontend assets so the tagged tree ships current JS/CSS.
87
99
  Rake::Task["release:build_frontend"].invoke
88
100
 
89
- puts "\nNext steps:"
90
- puts "1. Review the changes:"
91
- puts " git diff"
92
- puts "2. Commit the version bump:"
93
- puts " git add -A"
94
- puts " git commit -m 'chore(release): prepare for v#{version}'"
95
- puts "3. Create and push the tag:"
96
- puts " git tag v#{version}"
97
- puts " git push origin main --tags"
98
- puts "4. Build and release:"
99
- puts " rake release:publish"
101
+ # Stage everything and show it — review happens BEFORE anything is committed.
102
+ system("git", "add", "-A") || abort("git add failed")
103
+
104
+ puts "\n✓ Staged release v#{version} (nothing committed yet)."
105
+ puts "\nStaged changes:"
106
+ system("git", "--no-pager", "diff", "--cached", "--stat")
107
+ puts "\nNext:"
108
+ puts " git diff --cached # review the full diff"
109
+ puts " rake release:publish # commit, publish gem + npm, tag + push"
110
+ puts " git reset --hard # abort and discard the staged changes"
100
111
  end
101
112
 
102
113
  desc "Build front-end assets"
103
114
  task :build_frontend do
104
115
  puts "Building front-end assets..."
105
- # in: File::NULL — yarn 4 puts the terminal in raw mode for its
106
- # progress UI and doesn't always restore it on exit. Without this,
107
- # subsequent `$stdin.gets` prompts read one keystroke at a time and
108
- # never see a newline, so Enter never terminates the line.
109
116
  system("yarn build", in: File::NULL) || abort("Front-end build failed")
110
117
  puts "✓ Built front-end assets"
111
118
  end
112
119
 
113
- desc "Publish the gem to RubyGems"
114
- task :publish_gem do
115
- # Reload version constant in case it was updated
116
- load "lib/plutonium/version.rb"
117
- version = Plutonium::VERSION
118
-
119
- # Build the gem
120
- puts "Building gem..."
121
- system("gem build plutonium.gemspec") || abort("Gem build failed")
122
-
123
- # Push to RubyGems
124
- puts "Publishing to RubyGems..."
125
- gem_file = "plutonium-#{version}.gem"
126
- system("gem push #{gem_file}") || abort("Gem push failed")
127
-
128
- puts "✓ Published plutonium #{version} to RubyGems"
129
-
130
- # Clean up
131
- File.delete(gem_file) if File.exist?(gem_file)
132
- end
133
-
134
- desc "Publish the npm package"
135
- task :publish_npm do
136
- puts "Publishing npm package..."
137
-
138
- # Check if user is logged in to npm, login if needed
139
- unless system("npm whoami > /dev/null 2>&1")
140
- puts "Not logged in to npm. Opening login..."
141
- system("npm login") || abort("npm login failed")
142
- end
143
-
144
- # Publish to npm
145
- system("npm publish --access public") || abort("npm publish failed")
146
-
147
- # Get version from package.json
148
- require "json"
149
- package_json = JSON.parse(File.read("package.json"))
150
- version = package_json["version"]
151
-
152
- puts "✓ Published @radioactive-labs/plutonium #{version} to npm"
153
- end
154
-
155
- desc "Publish both gem and npm package"
156
- task publish: [:build_frontend, :publish_gem, :publish_npm]
120
+ # --- publish (primary; idempotent + resumable) ----------------------------
157
121
 
158
- desc "Full release workflow"
159
- task :full, [:version] do |_t, args|
160
- version = args[:version]
122
+ desc "Commit the prepared release, publish gem + npm, then tag + push (fires the Release workflow)"
123
+ task publish: [:build_frontend] do
124
+ version = current_version
125
+ tag = "v#{version}"
161
126
 
162
- unless version
163
- puts "Usage: rake release:full[VERSION]"
164
- exit 1
127
+ # Commit the changes prepare left staged for review. If the tree is already
128
+ # clean (e.g. re-running after a partial failure), there's nothing to commit.
129
+ if `git status --porcelain`.strip.empty?
130
+ puts "• working tree clean — nothing to commit"
131
+ else
132
+ system("git", "add", "-A") || abort("git add failed")
133
+ system("git", "commit", "-m", "chore(release): prepare for v#{version}") || abort("git commit failed")
134
+ puts "✓ Committed release v#{version}"
165
135
  end
166
136
 
167
- # Snapshot the terminal mode up front. yarn 4 and git-cliff both put
168
- # the TTY in raw mode for progress UIs and don't always restore it,
169
- # which breaks every subsequent `$stdin.gets` (Enter arrives as a
170
- # bare \r and gets() never returns). Restore the snapshot before each
171
- # prompt so the user can actually answer.
172
- tty_state = `stty -g 2>/dev/null`.strip
173
- restore_tty = -> { system("stty #{tty_state} 2>/dev/null") if tty_state != "" }
174
-
175
- puts "Starting release workflow for v#{version}..."
176
-
177
- # Check npm authentication early, login if needed
178
- unless system("npm whoami > /dev/null 2>&1")
179
- puts "Not logged in to npm. Opening login..."
180
- system("npm login") || abort("npm login failed")
137
+ # Gem (skip if this version is already on RubyGems)
138
+ if gem_published?(version)
139
+ puts "• gem plutonium #{version} already on RubyGems skipping"
140
+ else
141
+ puts "Building + pushing gem..."
142
+ system("gem build plutonium.gemspec") || abort("Gem build failed")
143
+ gem_file = "plutonium-#{version}.gem"
144
+ system("gem push #{gem_file}") || abort("Gem push failed")
145
+ File.delete(gem_file) if File.exist?(gem_file)
146
+ puts "✓ Published plutonium #{version} to RubyGems"
181
147
  end
182
- puts "✓ npm authenticated as: #{`npm whoami`.strip}"
183
148
 
184
- # Check for uncommitted changes
185
- unless `git status --porcelain`.strip.empty?
186
- puts "Error: You have uncommitted changes. Please commit or stash them first."
187
- exit 1
149
+ # npm (skip if this version is already published)
150
+ if npm_published?(version)
151
+ puts " npm #{RELEASE_NPM_PACKAGE}@#{version} already published skipping"
152
+ else
153
+ unless system("npm whoami > /dev/null 2>&1")
154
+ puts "Not logged in to npm. Opening login..."
155
+ system("npm login") || abort("npm login failed")
156
+ end
157
+ system("npm publish --access public") || abort("npm publish failed")
158
+ puts "✓ Published #{RELEASE_NPM_PACKAGE} #{version} to npm"
188
159
  end
189
160
 
190
- # Check we're on main branch
191
- current_branch = `git branch --show-current`.strip
192
- unless current_branch == "main" || current_branch == "master"
193
- puts "Warning: You're not on main/master branch (current: #{current_branch})"
194
- restore_tty.call
195
- print "Continue anyway? [y/N] "
196
- exit 1 unless $stdin.gets.strip.downcase == "y"
161
+ # Tag + push last, so CI cuts the Release only once the packages are live.
162
+ branch = `git branch --show-current`.strip
163
+ if system("git rev-parse #{tag} >/dev/null 2>&1")
164
+ puts " tag #{tag} already exists skipping tag"
165
+ else
166
+ system("git", "tag", tag) || abort("git tag failed")
197
167
  end
168
+ system("git", "push", "origin", branch) || abort("git push branch failed")
169
+ system("git", "push", "origin", tag) || abort("git push tag failed")
198
170
 
199
- # Prepare release
200
- Rake::Task["release:prepare"].invoke(version)
201
-
202
- # Confirm before proceeding
203
- restore_tty.call
204
- puts "\nReady to commit, tag, and publish?"
205
- print "Continue? [y/N] "
206
- exit 0 unless $stdin.gets.strip.downcase == "y"
207
-
208
- # Commit
209
- system("git add -A")
210
- system("git commit -m 'chore(release): prepare for v#{version}'")
211
-
212
- # Push commit (without tags yet)
213
- system("git push origin #{current_branch}")
214
-
215
- # Build and publish (do this BEFORE tagging)
216
- puts "\nBuilding and publishing gem and npm package..."
217
- Rake::Task["release:publish"].invoke
218
-
219
- # Only tag and push tag if publish succeeded
220
- puts "\nCreating and pushing tag..."
221
- system("git tag v#{version}")
222
- system("git push origin v#{version}")
223
-
224
- puts "\n✓ Release complete!"
225
- puts "GitHub Actions will create the release shortly."
171
+ puts "\n✓ Released #{tag}. GitHub Actions will cut the Release from the tag."
172
+ puts " Watch: https://github.com/radioactive-labs/plutonium-core/actions"
226
173
  end
227
174
  end
228
175
 
229
- desc "Release tasks"
230
- task release: ["release:next_version"]
176
+ # Neutralize the dangerous bare `rake release` that bundler/gem_tasks defines
177
+ # (it would tag + gem push directly). Point people at the real flow instead.
178
+ if Rake::Task.task_defined?("release")
179
+ Rake::Task["release"].clear
180
+ task :release do
181
+ warn "Use `rake release:prepare` then `rake release:publish`. See lib/tasks/release.rake."
182
+ end
183
+ end
data/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@radioactive-labs/plutonium",
3
- "version": "0.60.5",
3
+ "version": "0.61.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,7 +25,7 @@
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.4.3",
28
+ "dompurify": "^3.4.11",
29
29
  "lodash.debounce": "^4.0.8",
30
30
  "marked": "^15.0.3"
31
31
  },
@@ -38,7 +38,7 @@
38
38
  "chokidar-cli": "^3.0.0",
39
39
  "concurrently": "^8.2.2",
40
40
  "cssnano": "^7.0.2",
41
- "esbuild": "^0.28.0",
41
+ "esbuild": "^0.28.1",
42
42
  "esbuild-plugin-manifest": "^1.0.3",
43
43
  "flowbite-typography": "^1.0.5",
44
44
  "medium-zoom": "^1.1.0",
@@ -931,3 +931,135 @@ html.pu-rail-pinned .icon-rail-pin-expand {
931
931
  .dark .pu-alert-info { @apply text-info-400; }
932
932
  .pu-alert-info .pu-alert-close { @apply bg-info-50 text-info-500 hover:bg-info-200 focus:ring-info-400; }
933
933
  .dark .pu-alert-info .pu-alert-close { @apply text-info-400; }
934
+
935
+ /* ===========================================================================
936
+ Wizard — horizontal "steps" stepper (numbered nodes on a connector track)
937
+ =========================================================================== */
938
+ .pu-wizard-steps { @apply flex list-none m-0 p-0; }
939
+ .pu-wizard-steps > li { @apply relative flex-1 min-w-0 text-center; }
940
+
941
+ /* the connector track: two halves behind each node, so first/last don't overhang */
942
+ .pu-wizard-steps > li::before,
943
+ .pu-wizard-steps > li::after {
944
+ content: ""; position: absolute; top: 17px; height: 2px; z-index: 0;
945
+ background: var(--pu-border);
946
+ }
947
+ .pu-wizard-steps > li::before { left: 0; right: 50%; }
948
+ .pu-wizard-steps > li::after { left: 50%; right: 0; }
949
+ .pu-wizard-steps > li:first-child::before,
950
+ .pu-wizard-steps > li:last-child::after { display: none; }
951
+
952
+ .pu-wizard-steps .pu-step-link { @apply block no-underline; }
953
+ .pu-wizard-steps .pu-step-node {
954
+ @apply relative z-10 mx-auto grid place-items-center font-semibold;
955
+ width: 34px; height: 34px; border-radius: 9999px; font-size: .85rem;
956
+ background: var(--pu-surface); border: 2px solid var(--pu-border-strong);
957
+ color: var(--pu-text-subtle); transition: all .2s;
958
+ }
959
+ .pu-wizard-steps .pu-step-label {
960
+ @apply block mt-2 font-bold; font-size: .8rem; color: var(--pu-text-muted);
961
+ }
962
+ .pu-wizard-steps a.pu-step-link:hover .pu-step-label { @apply underline; }
963
+
964
+ /* current */
965
+ .pu-wizard-steps > li[data-state="current"] .pu-step-node {
966
+ @apply bg-primary-600 text-white; border-color: theme(colors.primary.600);
967
+ box-shadow: 0 0 0 5px color-mix(in srgb, theme(colors.primary.600) 16%, transparent);
968
+ }
969
+ .dark .pu-wizard-steps > li[data-state="current"] .pu-step-node {
970
+ @apply bg-primary-400 text-primary-950; border-color: theme(colors.primary.400);
971
+ box-shadow: 0 0 0 5px color-mix(in srgb, theme(colors.primary.400) 20%, transparent);
972
+ }
973
+ .pu-wizard-steps > li[data-state="current"] .pu-step-label { color: var(--pu-text); }
974
+
975
+ /* done — opaque tint (mixed with the surface, not transparent) so the connector
976
+ track behind the node never shows through and "cuts" the circle */
977
+ .pu-wizard-steps > li[data-state="completed"] .pu-step-node {
978
+ @apply text-primary-600; border-color: theme(colors.primary.600);
979
+ background: color-mix(in srgb, theme(colors.primary.600) 12%, var(--pu-surface));
980
+ }
981
+ .dark .pu-wizard-steps > li[data-state="completed"] .pu-step-node {
982
+ @apply text-primary-400; border-color: theme(colors.primary.400);
983
+ background: color-mix(in srgb, theme(colors.primary.400) 16%, var(--pu-surface));
984
+ }
985
+ .pu-wizard-steps > li[data-state="completed"] .pu-step-number { display: none; }
986
+ .pu-wizard-steps > li[data-state="completed"] .pu-step-node::after { content: "\2713"; }
987
+
988
+ /* reached but NOT complete — a revealed branch step the cursor landed on that was
989
+ never submitted (or is now invalid). Show the number in a warning tint (matching
990
+ the review's "needs attention" banner), never the done-check. */
991
+ .pu-wizard-steps > li[data-state="incomplete"] .pu-step-node {
992
+ @apply text-warning-600; border-color: theme(colors.warning.500);
993
+ background: color-mix(in srgb, theme(colors.warning.500) 12%, var(--pu-surface));
994
+ }
995
+ .dark .pu-wizard-steps > li[data-state="incomplete"] .pu-step-node {
996
+ @apply text-warning-400; border-color: theme(colors.warning.400);
997
+ background: color-mix(in srgb, theme(colors.warning.400) 16%, var(--pu-surface));
998
+ }
999
+
1000
+ /* the terminal (review) node shows a finish flag, never the done-check — even
1001
+ once it's been reached (`completed` state) */
1002
+ .pu-wizard-steps > li[data-terminal] .pu-step-node::after { content: none; }
1003
+
1004
+ /* filled connectors: a node's trailing half (::after) colors only when the NEXT
1005
+ node is reached (completed/current), and its leading half (::before) colors
1006
+ when this node itself is reached. So navigating BACK to a step still shows a
1007
+ filled line into the reached step that follows it — no stray gray segment. */
1008
+ .pu-wizard-steps > li[data-state="completed"]::before,
1009
+ .pu-wizard-steps > li[data-state="current"]::before,
1010
+ .pu-wizard-steps > li[data-state="incomplete"]::before,
1011
+ .pu-wizard-steps > li:has(+ li[data-state="completed"])::after,
1012
+ .pu-wizard-steps > li:has(+ li[data-state="current"])::after,
1013
+ .pu-wizard-steps > li:has(+ li[data-state="incomplete"])::after { background: theme(colors.primary.500); }
1014
+ .dark .pu-wizard-steps > li[data-state="completed"]::before,
1015
+ .dark .pu-wizard-steps > li[data-state="current"]::before,
1016
+ .dark .pu-wizard-steps > li[data-state="incomplete"]::before,
1017
+ .dark .pu-wizard-steps > li:has(+ li[data-state="completed"])::after,
1018
+ .dark .pu-wizard-steps > li:has(+ li[data-state="current"])::after,
1019
+ .dark .pu-wizard-steps > li:has(+ li[data-state="incomplete"])::after { background: theme(colors.primary.400); }
1020
+
1021
+ /* compact on small screens: hide labels, nodes-only track */
1022
+ @media (max-width: 640px) {
1023
+ .pu-wizard-steps .pu-step-label { @apply hidden; }
1024
+ }
1025
+
1026
+ /* ===================
1027
+ KANBAN BOARD
1028
+ =================== */
1029
+
1030
+ /*
1031
+ * Column wrapper: by default show the expanded body and hide the strip.
1032
+ * The Stimulus controller adds/removes `pu-kanban-column-collapsed` on the
1033
+ * wrapper to toggle between the two halves without a server round-trip.
1034
+ */
1035
+ .pu-kanban-column-wrapper > .pu-kanban-strip {
1036
+ display: none;
1037
+ }
1038
+
1039
+ /* Collapsed state: show strip, hide body */
1040
+ .pu-kanban-column-wrapper.pu-kanban-column-collapsed > .pu-kanban-strip {
1041
+ display: flex;
1042
+ flex-direction: column;
1043
+ align-items: center;
1044
+ }
1045
+
1046
+ .pu-kanban-column-wrapper.pu-kanban-column-collapsed > .pu-kanban-body {
1047
+ display: none;
1048
+ }
1049
+
1050
+ /* Card being dragged: ghost effect so the origin position is visible */
1051
+ .pu-kanban-dragging {
1052
+ opacity: 0.3;
1053
+ }
1054
+
1055
+ /* Column highlighted as the current drop target */
1056
+ .pu-kanban-drop-target {
1057
+ outline: 2px solid var(--color-primary-500, #3b82f6);
1058
+ outline-offset: -2px;
1059
+ border-radius: var(--pu-radius-md, 0.5rem);
1060
+ }
1061
+
1062
+ /* Column that would reject a drop: dimmed so the user sees valid targets */
1063
+ .pu-kanban-no-drop {
1064
+ opacity: 0.45;
1065
+ }
@@ -95,6 +95,20 @@ export default class extends Controller {
95
95
  //======= Config
96
96
 
97
97
  configureUppy() {
98
+ // A modal <dialog> opened with showModal() lives in the browser top
99
+ // layer, which paints above everything else regardless of z-index.
100
+ // uppy's Dashboard overlay (inline: false) mounts to <body> by default,
101
+ // so it renders BEHIND the modal. Targeting the enclosing dialog makes
102
+ // the overlay a descendant of the top-layer dialog, so it paints above
103
+ // the modal instead. (The dialog carries an open/close transform, so the
104
+ // overlay's `position: fixed` resolves against the dialog's box rather
105
+ // than the viewport — it renders within the modal bounds, above it.)
106
+ // Outside a modal, closest() is null and the overlay mounts to <body>
107
+ // (uppy's default) as before.
108
+ const dashboardOptions = { inline: false, closeAfterFinish: true }
109
+ const dialog = this.element.closest("dialog")
110
+ if (dialog) dashboardOptions.target = dialog
111
+
98
112
  this.uppy = new Uppy({
99
113
  restrictions: {
100
114
  maxFileSize: this.maxFileSizeValue,
@@ -106,7 +120,7 @@ export default class extends Controller {
106
120
  requiredMetaFields: this.requiredMetaFieldsValue,
107
121
  }
108
122
  })
109
- .use(Dashboard, { inline: false, closeAfterFinish: true })
123
+ .use(Dashboard, dashboardOptions)
110
124
  .use(ImageEditor, { target: Dashboard })
111
125
 
112
126
  this.#configureUploader()