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.
- checksums.yaml +4 -4
- data/.claude/skills/plutonium/SKILL.md +19 -1
- data/.claude/skills/plutonium-app/SKILL.md +41 -0
- data/.claude/skills/plutonium-auth/SKILL.md +40 -0
- data/.claude/skills/plutonium-behavior/SKILL.md +47 -1
- data/.claude/skills/plutonium-kanban/SKILL.md +313 -0
- data/.claude/skills/plutonium-resource/SKILL.md +40 -0
- data/.claude/skills/plutonium-tenancy/SKILL.md +43 -0
- data/.claude/skills/plutonium-testing/SKILL.md +38 -0
- data/.claude/skills/plutonium-ui/SKILL.md +51 -0
- data/.claude/skills/plutonium-wizard/SKILL.md +469 -0
- data/.cliff.toml +6 -0
- data/Appraisals +3 -0
- data/CHANGELOG.md +549 -439
- data/CLAUDE.md +15 -7
- data/app/assets/plutonium.css +1 -1
- data/app/assets/plutonium.js +895 -193
- data/app/assets/plutonium.js.map +4 -4
- data/app/assets/plutonium.min.js +53 -53
- data/app/assets/plutonium.min.js.map +4 -4
- data/app/views/layouts/basic.html.erb +7 -0
- data/app/views/plutonium/_flash_toasts.html.erb +2 -46
- data/app/views/plutonium/_toast.html.erb +52 -0
- data/app/views/resource/_resource_kanban.html.erb +1 -0
- data/db/migrate/wizard/20260615000001_create_plutonium_wizard_sessions.rb +57 -0
- data/docs/.vitepress/config.ts +24 -0
- data/docs/guides/index.md +2 -0
- data/docs/guides/kanban.md +447 -0
- data/docs/guides/wizards.md +447 -0
- data/docs/public/images/guides/kanban-after-move.png +0 -0
- data/docs/public/images/guides/kanban-board-light.png +0 -0
- data/docs/public/images/guides/kanban-board.png +0 -0
- data/docs/public/images/guides/kanban-show-centered-modal.png +0 -0
- data/docs/public/images/guides/kanban-wip-toast.png +0 -0
- data/docs/public/images/guides/wizards-chooser.png +0 -0
- data/docs/public/images/guides/wizards-completed.png +0 -0
- data/docs/public/images/guides/wizards-index-action.png +0 -0
- data/docs/public/images/guides/wizards-repeater.png +0 -0
- data/docs/public/images/guides/wizards-review.png +0 -0
- data/docs/public/images/guides/wizards-step.png +0 -0
- data/docs/reference/behavior/policies.md +1 -1
- data/docs/reference/index.md +14 -0
- data/docs/reference/kanban/authorization.md +62 -0
- data/docs/reference/kanban/dsl.md +293 -0
- data/docs/reference/kanban/index.md +40 -0
- data/docs/reference/kanban/positioning.md +162 -0
- data/docs/reference/resource/definition.md +16 -0
- data/docs/reference/ui/forms.md +36 -0
- data/docs/reference/ui/pages.md +2 -0
- data/docs/reference/wizard/anchoring-resume.md +194 -0
- data/docs/reference/wizard/dsl.md +332 -0
- data/docs/reference/wizard/index.md +33 -0
- data/docs/reference/wizard/one-time.md +129 -0
- data/docs/reference/wizard/registration-launch.md +177 -0
- data/docs/reference/wizard/storage-config.md +151 -0
- data/docs/superpowers/plans/2026-06-14-form-sectioning.md +2 -2
- data/docs/superpowers/plans/2026-06-15-wizard-dsl.md +1619 -0
- data/docs/superpowers/plans/2026-06-15-wizard-dsl.md.tasks.json +68 -0
- data/docs/superpowers/plans/2026-06-26-kanban-dsl.md +1128 -0
- data/docs/superpowers/plans/2026-06-26-kanban-dsl.md.tasks.json +24 -0
- data/docs/superpowers/specs/2026-06-15-wizard-dsl-design.md +836 -0
- data/docs/superpowers/specs/2026-06-15-wizard-dsl-examples.rb +245 -0
- data/docs/superpowers/specs/2026-06-17-wizard-relaunch-prompt-design.md +86 -0
- data/docs/superpowers/specs/2026-06-18-wizard-attachments-design.md +101 -0
- data/docs/superpowers/specs/2026-06-18-wizard-hosting-design.md +220 -0
- data/docs/superpowers/specs/2026-06-26-kanban-dsl-design.md +388 -0
- data/gemfiles/postgres.gemfile +8 -0
- data/gemfiles/postgres.gemfile.lock +321 -0
- data/gemfiles/rails_7.gemfile +1 -0
- data/gemfiles/rails_7.gemfile.lock +1 -1
- data/gemfiles/rails_8.0.gemfile +1 -0
- data/gemfiles/rails_8.0.gemfile.lock +1 -1
- data/gemfiles/rails_8.1.gemfile +1 -0
- data/gemfiles/rails_8.1.gemfile.lock +14 -1
- data/lib/generators/pu/invites/templates/packages/invites/app/controllers/invites/user_invitations_controller.rb.tt +6 -1
- data/lib/plutonium/action/base.rb +9 -0
- data/lib/plutonium/auth/rodauth.rb +1 -2
- data/lib/plutonium/configuration.rb +4 -0
- data/lib/plutonium/core/controller.rb +20 -1
- data/lib/plutonium/definition/base.rb +25 -0
- data/lib/plutonium/definition/form_layout.rb +54 -35
- data/lib/plutonium/definition/index_views.rb +54 -1
- data/lib/plutonium/definition/wizards.rb +209 -0
- data/lib/plutonium/invites/concerns/invite_token.rb +9 -0
- data/lib/plutonium/invites/concerns/invite_user.rb +9 -0
- data/lib/plutonium/invites/controller.rb +4 -1
- data/lib/plutonium/kanban/action.rb +7 -0
- data/lib/plutonium/kanban/board.rb +40 -0
- data/lib/plutonium/kanban/broadcaster.rb +54 -0
- data/lib/plutonium/kanban/column.rb +69 -0
- data/lib/plutonium/kanban/context.rb +15 -0
- data/lib/plutonium/kanban/dsl.rb +71 -0
- data/lib/plutonium/kanban/grouping.rb +51 -0
- data/lib/plutonium/kanban/positioning.rb +75 -0
- data/lib/plutonium/kanban.rb +11 -0
- data/lib/plutonium/migrations.rb +40 -0
- data/lib/plutonium/positioning.rb +146 -0
- data/lib/plutonium/railtie.rb +33 -0
- data/lib/plutonium/resource/controller.rb +2 -0
- data/lib/plutonium/resource/controllers/crud_actions.rb +1 -1
- data/lib/plutonium/resource/controllers/kanban_actions.rb +455 -0
- data/lib/plutonium/resource/controllers/wizard_actions.rb +165 -0
- data/lib/plutonium/resource/policy.rb +8 -0
- data/lib/plutonium/routing/mapper_extensions.rb +44 -0
- data/lib/plutonium/routing/wizard_registration.rb +289 -0
- data/lib/plutonium/ui/display/resource.rb +17 -12
- data/lib/plutonium/ui/form/base.rb +19 -5
- data/lib/plutonium/ui/form/components/password.rb +126 -0
- data/lib/plutonium/ui/form/components/uppy.rb +6 -3
- data/lib/plutonium/ui/form/options/inferred_types.rb +20 -0
- data/lib/plutonium/ui/form/resource.rb +1 -1
- data/lib/plutonium/ui/form/wizard.rb +63 -0
- data/lib/plutonium/ui/grid/card.rb +16 -5
- data/lib/plutonium/ui/kanban/card.rb +67 -0
- data/lib/plutonium/ui/kanban/color_dot.rb +36 -0
- data/lib/plutonium/ui/kanban/column.rb +324 -0
- data/lib/plutonium/ui/kanban/resource.rb +212 -0
- data/lib/plutonium/ui/layout/resource_layout.rb +7 -1
- data/lib/plutonium/ui/modal/base.rb +30 -3
- data/lib/plutonium/ui/modal/centered.rb +5 -2
- data/lib/plutonium/ui/page/index.rb +1 -0
- data/lib/plutonium/ui/page/show.rb +23 -0
- data/lib/plutonium/ui/page/wizard.rb +371 -0
- data/lib/plutonium/ui/page/wizard_chooser.rb +97 -0
- data/lib/plutonium/ui/page/wizard_completed.rb +86 -0
- data/lib/plutonium/ui/table/base.rb +1 -1
- data/lib/plutonium/ui/table/components/view_switcher.rb +2 -1
- data/lib/plutonium/ui/wizard/review.rb +196 -0
- data/lib/plutonium/ui/wizard/stepper.rb +122 -0
- data/lib/plutonium/ui/wizard/summary_display.rb +59 -0
- data/lib/plutonium/version.rb +1 -1
- data/lib/plutonium/wizard/attachment_data.rb +42 -0
- data/lib/plutonium/wizard/attachments.rb +226 -0
- data/lib/plutonium/wizard/base.rb +216 -0
- data/lib/plutonium/wizard/base_controller.rb +31 -0
- data/lib/plutonium/wizard/configuration.rb +42 -0
- data/lib/plutonium/wizard/controller.rb +162 -0
- data/lib/plutonium/wizard/data.rb +134 -0
- data/lib/plutonium/wizard/driving.rb +639 -0
- data/lib/plutonium/wizard/dsl.rb +336 -0
- data/lib/plutonium/wizard/errors.rb +27 -0
- data/lib/plutonium/wizard/field_capture.rb +157 -0
- data/lib/plutonium/wizard/field_importer.rb +208 -0
- data/lib/plutonium/wizard/gate.rb +171 -0
- data/lib/plutonium/wizard/instance_key.rb +97 -0
- data/lib/plutonium/wizard/lazy_persisted.rb +77 -0
- data/lib/plutonium/wizard/resume.rb +250 -0
- data/lib/plutonium/wizard/review_step.rb +48 -0
- data/lib/plutonium/wizard/route_resolution.rb +40 -0
- data/lib/plutonium/wizard/runner.rb +684 -0
- data/lib/plutonium/wizard/session.rb +53 -0
- data/lib/plutonium/wizard/state.rb +35 -0
- data/lib/plutonium/wizard/step.rb +61 -0
- data/lib/plutonium/wizard/step_adapter.rb +103 -0
- data/lib/plutonium/wizard/store/active_record.rb +174 -0
- data/lib/plutonium/wizard/store/base.rb +42 -0
- data/lib/plutonium/wizard/store/memory.rb +44 -0
- data/lib/plutonium/wizard/sweep_job.rb +76 -0
- data/lib/plutonium/wizard.rb +86 -0
- data/lib/plutonium.rb +5 -0
- data/lib/rodauth/features/case_insensitive_login.rb +1 -1
- data/lib/tasks/release.rake +144 -191
- data/package.json +3 -3
- data/src/css/components.css +132 -0
- data/src/js/controllers/attachment_input_controller.js +15 -1
- data/src/js/controllers/dirty_form_guard_controller.js +155 -27
- data/src/js/controllers/kanban_controller.js +330 -0
- data/src/js/controllers/password_sentinel_controller.js +39 -0
- data/src/js/controllers/register_controllers.js +6 -0
- data/src/js/controllers/remote_modal_controller.js +10 -0
- data/src/js/controllers/row_click_controller.js +14 -1
- data/src/js/controllers/wizard_controller.js +54 -0
- data/src/js/turbo/turbo_confirm.js +1 -1
- data/yarn.lock +271 -282
- 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
|
data/lib/tasks/release.rake
CHANGED
|
@@ -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
|
-
|
|
5
|
-
task :next_version do
|
|
6
|
-
current_version = Plutonium::VERSION
|
|
7
|
-
puts "Current version: #{current_version}"
|
|
28
|
+
# --- helpers --------------------------------------------------------------
|
|
8
29
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
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
|
-
|
|
25
|
-
|
|
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
|
-
|
|
46
|
-
|
|
47
|
-
|
|
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
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
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
|
-
|
|
58
|
-
exit 1
|
|
73
|
+
abort "Error: version must be in format X.Y.Z (got #{version.inspect})"
|
|
59
74
|
end
|
|
60
75
|
|
|
61
|
-
|
|
62
|
-
|
|
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
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
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
|
-
#
|
|
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
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
puts "
|
|
93
|
-
puts "
|
|
94
|
-
|
|
95
|
-
puts "
|
|
96
|
-
puts "
|
|
97
|
-
puts "
|
|
98
|
-
puts "
|
|
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
|
-
|
|
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 "
|
|
159
|
-
task :
|
|
160
|
-
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
|
-
|
|
163
|
-
|
|
164
|
-
|
|
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
|
-
#
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
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
|
-
#
|
|
185
|
-
|
|
186
|
-
puts "
|
|
187
|
-
|
|
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
|
-
#
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
puts "
|
|
194
|
-
|
|
195
|
-
|
|
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
|
-
#
|
|
200
|
-
|
|
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
|
-
|
|
230
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
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",
|
data/src/css/components.css
CHANGED
|
@@ -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,
|
|
123
|
+
.use(Dashboard, dashboardOptions)
|
|
110
124
|
.use(ImageEditor, { target: Dashboard })
|
|
111
125
|
|
|
112
126
|
this.#configureUploader()
|