slash_migrate 0.1.0 → 0.1.1

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: e02b861e967a6d80fde6d69853c8222cbb16bee08c698d4566673f7f3baf34ec
4
- data.tar.gz: dd3e18fb6ee29dbb2882112062c87659a3d62569773d62256cdb1f7866142fcd
3
+ metadata.gz: fc38b5dc8b5c3ad04547645969779d7a6b744363a01967d974874c603ade8444
4
+ data.tar.gz: 4863978dd450993f5bef0a2be7695bdf8a64a05fb278a7e4a5822b374624fb99
5
5
  SHA512:
6
- metadata.gz: d96ec25095737a5cb6a48b8fd6abcd63635f1bd6487df203d7e910f180f8378654a95b91113f04a09fa908997e8f373c9d8eb9462346809c3ce053ef9b17a625
7
- data.tar.gz: 86548b278ba6c311b89ab96f23e887bebd57cd40d3156d0869bdd76e6ae1850f891a225b8c1e50ad556899bc4649c9cee3b250ec7a91bc9bf98eacc7a74b3293
6
+ metadata.gz: 6b2081b038e753378cacd065a390b0b789e3b200a069268ff06b973ba84e99806f0b0ad331dda23fb7ab0580a92befcde7841c6496fe8cf03902e40806db1fb9
7
+ data.tar.gz: ad18527b2b39c34c637f3e35ec56ac1e5ef37c5c2f308c531106c4dfe736b610beacdcba7c3d98f652ed86429299867e186179d22a7fc95de5241baed7ebaaf9
data/CHANGELOG.md CHANGED
@@ -4,6 +4,31 @@ All notable changes to this project are documented here. The format is based on
4
4
  [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project
5
5
  adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
6
6
 
7
+ ## [0.1.1] - 2026-05-27
8
+
9
+ ### Fixed
10
+
11
+ - Running a migration or rollback from the GUI no longer crashes the host app
12
+ with `ActionDispatch::Cookies::CookieOverflow`. The migrations page now updates
13
+ in place via a Turbo Stream rather than carrying the command output through the
14
+ 4 KB session cookie.
15
+ - Live previews and migration actions no longer return 500
16
+ (`ActionView::MissingTemplate`) in host apps that have `turbo-rails` installed;
17
+ the engine's hand-written stream templates are pinned to the HTML format.
18
+ - `db:migrate` / `db:rollback` no longer fail with "… is not yet checked out"
19
+ when the host app uses a custom `BUNDLE_PATH` (e.g. GitHub Codespaces): the task
20
+ now shells out within the host's own bundle environment.
21
+ - Restored two margins — above the "Drop this column" panel and below the
22
+ migration output — that an internal spacing-token rename had silently dropped,
23
+ and moved those values into CSS so a future rename can't break them again.
24
+
25
+ ### Changed
26
+
27
+ - Migrations run / rollback / delete now update the page in place via Turbo
28
+ Stream instead of reloading after a redirect.
29
+ - The nav brand and page titles follow the configured mount path, and the table
30
+ stats line pluralizes correctly.
31
+
7
32
  ## [0.1.0] - 2026-05-27
8
33
 
9
34
  ### Added
@@ -18,4 +43,5 @@ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
18
43
  - A live preview of the migration and model code each action will generate.
19
44
  - `SlashMigrate.configure` for `mount_path` and `enabled_environments`.
20
45
 
46
+ [0.1.1]: https://github.com/firstdraft/slash_migrate/compare/v0.1.0...v0.1.1
21
47
  [0.1.0]: https://github.com/firstdraft/slash_migrate/releases/tag/v0.1.0
@@ -11,5 +11,22 @@ module SlashMigrate
11
11
  rescue_from MigrationFileWriter::DuplicateError do |error|
12
12
  redirect_to migrations_path, alert: error.message
13
13
  end
14
+
15
+ private
16
+
17
+ # Render a hand-written <turbo-stream> template (the engine ships no
18
+ # turbo-rails). The html format is forced deliberately: when the host app
19
+ # *does* have turbo-rails, it registers the text/vnd.turbo-stream.html MIME,
20
+ # so a request asking for a Turbo Stream negotiates to the :turbo_stream
21
+ # format and a bare `render :preview` would look for a nonexistent
22
+ # preview.turbo_stream.erb and raise MissingTemplate (500). Pass content_type
23
+ # when the response is applied by native Turbo form handling (it keys off the
24
+ # header); the live previews are applied client-side by
25
+ # Turbo.renderStreamMessage and don't need it.
26
+ def render_stream(template, content_type: nil)
27
+ options = {layout: false, formats: [:html]}
28
+ options[:content_type] = content_type if content_type
29
+ render(template, **options)
30
+ end
14
31
  end
15
32
  end
@@ -10,10 +10,10 @@ module SlashMigrate
10
10
  def preview
11
11
  @migration = AddColumnsMigration.from_params(table: @table, rows: params[:attributes])
12
12
  @hint = "Add a column to see the migration it will generate." unless @migration.any?
13
- render :preview, layout: false
13
+ render_stream :preview
14
14
  rescue => e
15
15
  @error = e.message
16
- render :preview, layout: false
16
+ render_stream :preview
17
17
  end
18
18
 
19
19
  def create
@@ -50,10 +50,10 @@ module SlashMigrate
50
50
  original = find_column
51
51
  @migration = original && EditColumnMigration.new(table: @table, original: original, desired: desired_column)
52
52
  @hint = "Change a value to see the migration it will generate." unless @migration&.changed?
53
- render :update_preview, layout: false
53
+ render_stream :update_preview
54
54
  rescue => e
55
55
  @error = e.message
56
- render :update_preview, layout: false
56
+ render_stream :update_preview
57
57
  end
58
58
 
59
59
  def update
@@ -10,10 +10,10 @@ module SlashMigrate
10
10
  def preview
11
11
  @migration = build_migration
12
12
  @hint = "Pick a column to index to see the migration it will generate." unless @migration.any?
13
- render :preview, layout: false
13
+ render_stream :preview
14
14
  rescue => e
15
15
  @error = e.message
16
- render :preview, layout: false
16
+ render_stream :preview
17
17
  end
18
18
 
19
19
  def create
@@ -1,22 +1,24 @@
1
1
  module SlashMigrate
2
2
  class MigrationsController < ApplicationController
3
3
  def index
4
- @migrations = runner.status
5
- @pending = runner.pending?
4
+ load_migrations
6
5
  end
7
6
 
8
7
  def run
9
- finish(runner.migrate, "Migrated.", "rails db:migrate")
8
+ @result = runner.migrate
9
+ @command = "rails db:migrate"
10
+ stream_result
10
11
  end
11
12
 
12
13
  def rollback
13
- finish(runner.rollback, "Rolled back the last migration.", "rails db:rollback")
14
+ @result = runner.rollback
15
+ @command = "rails db:rollback"
16
+ stream_result
14
17
  end
15
18
 
16
19
  def destroy
17
- result = runner.delete(params[:version])
18
- flash[result.success? ? :notice : :alert] = result.output
19
- redirect_to migrations_path
20
+ @result = runner.delete(params[:version])
21
+ stream_result
20
22
  end
21
23
 
22
24
  private
@@ -25,17 +27,22 @@ module SlashMigrate
25
27
  @runner ||= MigrationRunner.new
26
28
  end
27
29
 
28
- # Post/redirect/get: stash the (truncated) command output in the flash so a
29
- # refresh doesn't re-run the task.
30
- def finish(result, message, command)
31
- flash[:migrate_output] = result.output.to_s.last(3000)
32
- flash[:migrate_command] = command
33
- if result.success?
34
- flash[:notice] = message
35
- else
36
- flash[:alert] = "Command failed see the output below."
37
- end
38
- redirect_to migrations_path
30
+ def load_migrations
31
+ @migrations = runner.status
32
+ @pending = runner.pending?
33
+ end
34
+
35
+ # Update the page in place with a Turbo Stream instead of redirecting and
36
+ # carrying the command output in the flash: that output can be large, and
37
+ # CookieStore (the Rails default) caps the session at 4 KB, so a redirect
38
+ # could overflow the cookie and crash the host app. The engine ships no
39
+ # turbo-rails, so the <turbo-stream> is written by hand (see stream.html.erb)
40
+ # and rendered via render_stream, which forces the html template and sets the
41
+ # turbo-stream MIME; native Turbo form handling applies it. A plain refresh
42
+ # re-issues the GET, so the task never re-runs.
43
+ def stream_result
44
+ load_migrations
45
+ render_stream :stream, content_type: "text/vnd.turbo-stream.html"
39
46
  end
40
47
  end
41
48
  end
@@ -16,11 +16,11 @@ module SlashMigrate
16
16
  @hint = "Enter a model name to see the migration it will generate."
17
17
  end
18
18
 
19
- render :preview, layout: false
19
+ render_stream :preview
20
20
  rescue => e
21
21
  # Partial/invalid input shouldn't 500 the live preview; show the problem.
22
22
  @error = e.message
23
- render :preview, layout: false
23
+ render_stream :preview
24
24
  end
25
25
 
26
26
  def create
@@ -43,6 +43,29 @@ module SlashMigrate
43
43
  end
44
44
  end
45
45
 
46
+ # Where the engine is actually mounted, as the browser sees it (e.g.
47
+ # "/rails/migrate"). Read from the request's script_name so a host that
48
+ # mounts us somewhere non-standard is reflected verbatim, rather than
49
+ # trusting the configured default. Falls back to that default when there's
50
+ # no request to read (e.g. rendering outside the engine).
51
+ def mount_path
52
+ request&.script_name.presence || SlashMigrate.config.mount_path
53
+ end
54
+
55
+ # The mount path drawn as the nav brand: each "/" muted via <em> (see
56
+ # `.nav-brand em`) with the path segments between them in ink.
57
+ def nav_brand
58
+ safe_join(mount_path.split("/", -1), tag.em("/"))
59
+ end
60
+
61
+ # The document <title>: the name the current page set via `content_for
62
+ # :title`, so browser tabs, history, and bookmarks stay distinguishable.
63
+ # Falls back to the mount path (the plain-text form of the nav brand) for
64
+ # any page that doesn't set one.
65
+ def page_title
66
+ content_for(:title).presence || mount_path
67
+ end
68
+
46
69
  # The column types worth showing first, in teaching order. Anything else the
47
70
  # database supports follows, alphabetically.
48
71
  COMMON_COLUMN_TYPES = %w[string text integer boolean references datetime date decimal float].freeze
@@ -34,9 +34,8 @@ module SlashMigrate
34
34
  # Normalize to a String, matching the form-input path. Active Record
35
35
  # casts defaults differently across versions and adapters (Rails 8.0
36
36
  # hands back "0", 8.1 casts it to 0; a boolean default arrives as
37
- # false), and the rest of this class — presence, downcase, literal
38
- # interpolation — assumes a string. Without &.to_s, presence(false)
39
- # would also silently drop a boolean's `default: false`.
37
+ # false), and the rest of this class — to_s.strip.presence, downcase,
38
+ # literal interpolation — assumes a string.
40
39
  default: ar_column.default&.to_s,
41
40
  limit: meta&.limit,
42
41
  precision: meta&.precision,
@@ -51,12 +50,12 @@ module SlashMigrate
51
50
  @name = name.to_s.strip
52
51
  @type = type.to_s
53
52
  @null = null
54
- @default = presence(default)
55
- @limit = presence(limit)
56
- @precision = presence(precision)
57
- @scale = presence(scale)
53
+ @default = default.to_s.strip.presence
54
+ @limit = limit.to_s.strip.presence
55
+ @precision = precision.to_s.strip.presence
56
+ @scale = scale.to_s.strip.presence
58
57
  @index = index.to_s
59
- @to_table = presence(to_table)
58
+ @to_table = to_table.to_s.strip.presence
60
59
  end
61
60
 
62
61
  def blank?
@@ -189,10 +188,5 @@ module SlashMigrate
189
188
  @default.inspect
190
189
  end
191
190
  end
192
-
193
- def presence(value)
194
- string = value.to_s.strip
195
- string.empty? ? nil : string
196
- end
197
191
  end
198
192
  end
@@ -62,7 +62,13 @@ module SlashMigrate
62
62
  private
63
63
 
64
64
  def run(task)
65
- output, process = Bundler.with_unbundled_env do
65
+ # db:migrate is part of the host app's OWN bundle, so reset to the env
66
+ # from before this process loaded Bundler with with_original_env — NOT
67
+ # with_unbundled_env. The latter strips the host's bundler config,
68
+ # including a custom BUNDLE_PATH (e.g. Codespaces installs the bundle under
69
+ # /home/student/.bundle); the child bin/rails then can't find gems there —
70
+ # notably a git-sourced gem — and dies with "is not yet checked out".
71
+ output, process = Bundler.with_original_env do
66
72
  Open3.capture2e({"RAILS_ENV" => Rails.env.to_s}, rails_bin, task, chdir: Rails.root.to_s)
67
73
  end
68
74
  Result.new(output: output, success: process.success?)
@@ -2,7 +2,7 @@
2
2
  <html lang="en">
3
3
  <head>
4
4
  <meta charset="utf-8">
5
- <title>slash_migrate</title>
5
+ <title><%= page_title %></title>
6
6
  <meta name="viewport" content="width=device-width, initial-scale=1">
7
7
  <%= csrf_meta_tags %>
8
8
  <%= csp_meta_tag %>
@@ -18,7 +18,7 @@
18
18
  </head>
19
19
  <body>
20
20
  <header class="nav">
21
- <%= link_to root_path, class: "nav-brand" do %><em>/</em>rails<em>/</em>migrate<% end %>
21
+ <%= link_to root_path, class: "nav-brand" do %><%= nav_brand %><% end %>
22
22
  <span class="nav-spacer"></span>
23
23
  <nav class="nav-links">
24
24
  <%= nav_link "Tables", root_path, :database, active: nav_section == :tables %>
@@ -1,10 +1,11 @@
1
1
  <% content_for :page_class, "page-form" %>
2
+ <% content_for :title, "Edit #{@column.name} on #{@table}" %>
2
3
 
3
4
  <div class="page-head">
4
5
  <div>
5
6
  <%= render "slash_migrate/shared/breadcrumbs", trail: [{label: "Tables", url: root_path}, {label: @table, url: table_path(@table)}, {label: "Edit #{@column.name}"}] %>
6
7
  <h1 class="page-title">Edit <span class="mono"><%= @column.name %></span> on <span class="mono"><%= @table %></span></h1>
7
- <p class="page-sub">Change the column's name, type, nullability, or default. The migration below updates as you edit.</p>
8
+ <p class="page-sub">Change the column's name, type, nullability, or default. The proposed migration will appear below in real-time as you edit.</p>
8
9
  </div>
9
10
  </div>
10
11
 
@@ -59,7 +60,7 @@
59
60
  </div>
60
61
  <% end %>
61
62
 
62
- <div class="panel danger-zone" style="margin-top:var(--gap-lg)">
63
+ <div class="panel danger-zone">
63
64
  <div class="panel-head">
64
65
  <div>
65
66
  <h2>Drop this column</h2>
@@ -1,10 +1,11 @@
1
1
  <% content_for :page_class, "page-form" %>
2
+ <% content_for :title, "Add columns to #{@table}" %>
2
3
 
3
4
  <div class="page-head">
4
5
  <div>
5
6
  <%= render "slash_migrate/shared/breadcrumbs", trail: [{label: "Tables", url: root_path}, {label: @table, url: table_path(@table)}, {label: "Add columns"}] %>
6
7
  <h1 class="page-title">Add columns to <span class="mono"><%= @table %></span></h1>
7
- <p class="page-sub">Append one or more columns to the existing table. A timestamped <span class="mono">add_column</span> migration will be created.</p>
8
+ <p class="page-sub">Add some columns to your existing table. The proposed <span class="mono">add_column</span> migration will appear below in real-time as you type.</p>
8
9
  </div>
9
10
  </div>
10
11
 
@@ -1,10 +1,11 @@
1
1
  <% content_for :page_class, "page-form" %>
2
+ <% content_for :title, "Add index to #{@table}" %>
2
3
 
3
4
  <div class="page-head">
4
5
  <div>
5
6
  <%= render "slash_migrate/shared/breadcrumbs", trail: [{label: "Tables", url: root_path}, {label: @table, url: table_path(@table)}, {label: "Add index"}] %>
6
7
  <h1 class="page-title">Add index to <span class="mono"><%= @table %></span></h1>
7
- <p class="page-sub">Pick one or more columns to index — useful for speeding up lookups on foreign keys, or columns you filter or sort by often.</p>
8
+ <p class="page-sub">Pick the columns you'd like to index — handy for speeding up lookups on foreign keys, or columns you filter or sort by often. The proposed migration will appear below in real-time as you type.</p>
8
9
  </div>
9
10
  </div>
10
11
 
@@ -0,0 +1,79 @@
1
+ <%
2
+ applied = @migrations.count(&:applied?)
3
+ pending = @migrations.size - applied
4
+ %>
5
+
6
+ <div class="page-head">
7
+ <div>
8
+ <h1 class="page-title">Migrations</h1>
9
+ <p class="page-sub">
10
+ <%= applied %> applied · <%= pending %> pending. This list mirrors the files in your <span class="mono">db/migrate</span> folder.
11
+ </p>
12
+ </div>
13
+ <div class="row">
14
+ <% if @migrations.any?(&:applied?) %>
15
+ <%= button_to rollback_migrations_path, class: "btn btn-ghost",
16
+ data: {turbo_confirm: "Roll back the most recent migration? This reverses its changes."} do %>
17
+ <%= sm_icon(:rotate) %> Roll back last
18
+ <% end %>
19
+ <% end %>
20
+ <% if @pending %>
21
+ <%= button_to run_migrations_path, class: "btn btn-accent" do %><%= sm_icon(:play) %> Run pending<% end %>
22
+ <% end %>
23
+ </div>
24
+ </div>
25
+
26
+ <%# Feedback from the last run / rollback / delete, shown in place. Carried in
27
+ instance variables rather than the flash, so large command output never
28
+ rides through the session cookie. %>
29
+ <% if @command %>
30
+ <div class="migrate-output">
31
+ <%= render "slash_migrate/shared/terminal",
32
+ command: @command,
33
+ output: @result.output,
34
+ meta: (@result.success? ? "output" : "exited 1"),
35
+ meta_style: (@result.success? ? nil : "color:#E8B07F") %>
36
+ </div>
37
+ <% elsif @result %>
38
+ <div class="migrate-output">
39
+ <%= render layout: "slash_migrate/shared/callout", locals: {kind: @result.success? ? "ok" : "warn"} do %>
40
+ <%= @result.output %>
41
+ <% end %>
42
+ </div>
43
+ <% end %>
44
+
45
+ <div class="panel">
46
+ <% if @migrations.empty? %>
47
+ <div class="panel-pad muted">No migration files yet. Create a model or add columns, and the migration shows up here.</div>
48
+ <% else %>
49
+ <table class="table">
50
+ <thead>
51
+ <tr><th>Status</th><th>Version</th><th>Migration</th><th></th></tr>
52
+ </thead>
53
+ <tbody>
54
+ <% @migrations.each do |migration| %>
55
+ <tr>
56
+ <td>
57
+ <span class="badge <%= migration.applied? ? "is-up" : "is-pending" %>"><%= migration.applied? ? "up" : "pending" %></span>
58
+ </td>
59
+ <td><span class="mono muted"><%= migration.version %></span></td>
60
+ <td><span class="mono"><%= migration.name %></span></td>
61
+ <td class="col-actions">
62
+ <% if migration.applied? %>
63
+ <span class="has-tip" tabindex="0">
64
+ <button type="button" class="btn btn-sm btn-disabled" disabled><%= sm_icon(:trash) %> Delete file</button>
65
+ <span class="tip">This migration has been run. Roll it back with <kbd>db:rollback</kbd> first — deleting its file would orphan the <kbd>schema_migrations</kbd> record and leave the change unreversible.</span>
66
+ </span>
67
+ <% else %>
68
+ <%= button_to migration_path(migration.version), method: :delete, class: "btn btn-sm btn-ghost",
69
+ data: {turbo_confirm: "Delete the “#{migration.name}” migration file? It hasn't been run."} do %>
70
+ <%= sm_icon(:trash) %> Delete file
71
+ <% end %>
72
+ <% end %>
73
+ </td>
74
+ </tr>
75
+ <% end %>
76
+ </tbody>
77
+ </table>
78
+ <% end %>
79
+ </div>
@@ -1,71 +1,6 @@
1
- <%
2
- applied = @migrations.count(&:applied?)
3
- pending = @migrations.size - applied
4
- %>
5
-
6
- <div class="page-head">
7
- <div>
8
- <h1 class="page-title">Migrations</h1>
9
- <p class="page-sub">
10
- <%= applied %> applied · <%= pending %> pending. This list mirrors the files in <span class="mono">db/migrate</span>.
11
- </p>
12
- </div>
13
- <div class="row">
14
- <% if @migrations.any?(&:applied?) %>
15
- <%= button_to rollback_migrations_path, class: "btn btn-ghost",
16
- data: {turbo_confirm: "Roll back the most recent migration? This reverses its changes."} do %>
17
- <%= sm_icon(:rotate) %> Roll back last
18
- <% end %>
19
- <% end %>
20
- <% if @pending %>
21
- <%= button_to run_migrations_path, class: "btn btn-accent" do %><%= sm_icon(:play) %> Run pending<% end %>
22
- <% end %>
23
- </div>
24
- </div>
25
-
26
- <% if flash[:migrate_output].present? %>
27
- <% failed = flash[:alert].present? %>
28
- <div style="margin-bottom:var(--gap-lg)">
29
- <%= render "slash_migrate/shared/terminal",
30
- command: (flash[:migrate_command] || "rails db:migrate"),
31
- output: flash[:migrate_output],
32
- meta: (failed ? "exited 1" : "output"),
33
- meta_style: (failed ? "color:#E8B07F" : nil) %>
34
- </div>
35
- <% end %>
36
-
37
- <div class="panel">
38
- <% if @migrations.empty? %>
39
- <div class="panel-pad muted">No migration files yet. Create a model or add columns, and the migration shows up here.</div>
40
- <% else %>
41
- <table class="table">
42
- <thead>
43
- <tr><th>Status</th><th>Version</th><th>Migration</th><th></th></tr>
44
- </thead>
45
- <tbody>
46
- <% @migrations.each do |migration| %>
47
- <tr>
48
- <td>
49
- <span class="badge <%= migration.applied? ? "is-up" : "is-pending" %>"><%= migration.applied? ? "up" : "pending" %></span>
50
- </td>
51
- <td><span class="mono muted"><%= migration.version %></span></td>
52
- <td><span class="mono"><%= migration.name %></span></td>
53
- <td class="col-actions">
54
- <% if migration.applied? %>
55
- <span class="has-tip" tabindex="0">
56
- <button type="button" class="btn btn-sm btn-disabled" disabled><%= sm_icon(:trash) %> Delete file</button>
57
- <span class="tip">This migration has been run. Roll it back with <kbd>db:rollback</kbd> first — deleting its file would orphan the <kbd>schema_migrations</kbd> record and leave the change unreversible.</span>
58
- </span>
59
- <% else %>
60
- <%= button_to migration_path(migration.version), method: :delete, class: "btn btn-sm btn-ghost",
61
- data: {turbo_confirm: "Delete the “#{migration.name}” migration file? It hasn't been run."} do %>
62
- <%= sm_icon(:trash) %> Delete file
63
- <% end %>
64
- <% end %>
65
- </td>
66
- </tr>
67
- <% end %>
68
- </tbody>
69
- </table>
70
- <% end %>
1
+ <% content_for :title, "Migrations" %>
2
+ <%# The whole page lives in one Turbo Stream target so run / rollback / delete
3
+ can update it in place (see stream.html.erb) without a redirect. %>
4
+ <div id="sm-migrations">
5
+ <%= render "migrations" %>
71
6
  </div>
@@ -0,0 +1,6 @@
1
+ <%# Rendered for run / rollback / delete. The controller sends this with the
2
+ text/vnd.turbo-stream.html MIME so native Turbo form handling applies it —
3
+ no turbo-rails gem, no redirect, no flash. %>
4
+ <turbo-stream action="update" target="sm-migrations">
5
+ <template><%= render "migrations" %></template>
6
+ </turbo-stream>
@@ -1,9 +1,10 @@
1
1
  <% content_for :page_class, "page-form" %>
2
+ <% content_for :title, "New table" %>
2
3
 
3
4
  <div class="page-head">
4
5
  <div>
5
6
  <h1 class="page-title">New table</h1>
6
- <p class="page-sub">Pick a name and add columns. The migration and model files appear live below as you type.</p>
7
+ <p class="page-sub">Pick a name for your new table and add some columns. The proposed migration and model files will appear below in real-time as you type.</p>
7
8
  </div>
8
9
  </div>
9
10
 
@@ -1,6 +1,11 @@
1
- <%# Animated three-chevron indicator pointing from the form down to the live preview. %>
2
- <div class="stack-link" aria-hidden="true">
3
- <span class="chev"></span>
4
- <span class="chev"></span>
5
- <span class="chev"></span>
1
+ <%# Animated three-chevron indicators pointing from the form down to the live
2
+ preview. Three identical stacks, spread across the width (see .flow-arrows). %>
3
+ <div class="flow-arrows" aria-hidden="true">
4
+ <% 3.times do %>
5
+ <span class="stack-link">
6
+ <span class="chev"></span>
7
+ <span class="chev"></span>
8
+ <span class="chev"></span>
9
+ </span>
10
+ <% end %>
6
11
  </div>
@@ -1,7 +1,9 @@
1
+ <% content_for :title, "Tables" %>
2
+
1
3
  <div class="page-head">
2
4
  <div>
3
5
  <h1 class="page-title">Tables</h1>
4
- <p class="page-sub">The tables in this app's database. Open any one to inspect its columns, indexes, and foreign keys.</p>
6
+ <p class="page-sub">The tables in your app's database. Open any one to inspect its columns, indexes, and foreign keys.</p>
5
7
  </div>
6
8
  <%= link_to new_model_path, class: "btn btn-accent" do %><%= sm_icon(:plus) %> New table<% end %>
7
9
  </div>
@@ -5,15 +5,16 @@
5
5
  primary_key = @table.primary_key
6
6
  fk_targets = foreign_keys.to_h { |fk| [fk.column, fk.to_table] }
7
7
  %>
8
+ <% content_for :title, @table.name %>
8
9
 
9
10
  <div class="page-head">
10
11
  <div>
11
12
  <%= render "slash_migrate/shared/breadcrumbs", trail: [{label: "Tables", url: root_path}, {label: @table.name}] %>
12
13
  <h1 class="page-title"><span class="mono"><%= @table.name %></span></h1>
13
14
  <p class="page-sub">
14
- <%= columns.size %> column<%= "s" unless columns.size == 1 %> ·
15
- <%= indexes.size %> index<%= "es" unless indexes.size == 1 %> ·
16
- <%= foreign_keys.size %> foreign key<%= "s" unless foreign_keys.size == 1 %>
15
+ <%= pluralize(columns.size, "column") %> ·
16
+ <%= pluralize(indexes.size, "index", "indexes") %> ·
17
+ <%= pluralize(foreign_keys.size, "foreign key") %>
17
18
  </p>
18
19
  </div>
19
20
  <%= link_to new_table_column_path(@table.name), class: "btn btn-ghost" do %><%= sm_icon(:plus) %> Add columns<% end %>
@@ -348,7 +348,9 @@ form.button_to { display:inline; margin:0; }
348
348
  .stack { display:flex; flex-direction:column; gap:var(--space-md); }
349
349
  .stack > .callout { margin:0; }
350
350
  /* Three downward chevrons that shimmer toward the live preview — an affordance,
351
- not a button (no fill, no circle). */
351
+ not a button (no fill, no circle). Repeated across the width so the eye is
352
+ drawn down to the preview from wherever it's resting. */
353
+ .flow-arrows { display:flex; justify-content:space-around; }
352
354
  .stack-link { display:flex; flex-direction:column; align-items:center; gap:1px; padding:4px 0; }
353
355
  .stack-link .chev {
354
356
  width:11px; height:11px;
@@ -376,6 +378,9 @@ form.button_to { display:inline; margin:0; }
376
378
  /* ── Misc ────────────────────────────────────────────── */
377
379
  .chip-soft { font-family:var(--font-mono); font-size:0.92em; background:var(--surface-2); padding:1px 6px; border-radius:4px; color:var(--ink-2); }
378
380
  .row { display:flex; align-items:center; gap:var(--space-sm); }
379
- .danger-zone { border-color:var(--err-border); background:linear-gradient(180deg, #F9E7E1 0%, var(--surface) 60%); }
381
+ .danger-zone { margin-top:var(--space-xl); border-color:var(--err-border); background:linear-gradient(180deg, #F9E7E1 0%, var(--surface) 60%); }
380
382
  .danger-zone .panel-head { border-bottom-color:#ECD0C5; }
381
383
  .danger-zone .panel-head h2 { color:var(--err); }
384
+ /* Spacing tokens belong in the stylesheet, not in view inline styles, so a
385
+ future token rename can't silently drop them (see the --gap-* history). */
386
+ .migrate-output { margin-bottom:var(--space-xl); }
@@ -1,3 +1,3 @@
1
1
  module SlashMigrate
2
- VERSION = "0.1.0"
2
+ VERSION = "0.1.1"
3
3
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: slash_migrate
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.1.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Raghu Betina
@@ -68,7 +68,9 @@ files:
68
68
  - app/views/slash_migrate/indexes/_preview.html.erb
69
69
  - app/views/slash_migrate/indexes/new.html.erb
70
70
  - app/views/slash_migrate/indexes/preview.html.erb
71
+ - app/views/slash_migrate/migrations/_migrations.html.erb
71
72
  - app/views/slash_migrate/migrations/index.html.erb
73
+ - app/views/slash_migrate/migrations/stream.html.erb
72
74
  - app/views/slash_migrate/models/_name_help.html.erb
73
75
  - app/views/slash_migrate/models/_preview.html.erb
74
76
  - app/views/slash_migrate/models/_row.html.erb