alembic 0.2.0 → 0.4.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 (57) hide show
  1. checksums.yaml +4 -4
  2. data/app/assets/javascripts/alembic/one_page.js +39 -0
  3. data/app/controllers/alembic/application_controller.rb +2 -0
  4. data/app/controllers/alembic/flows_controller.rb +31 -0
  5. data/app/controllers/alembic/manage/base_controller.rb +9 -0
  6. data/app/controllers/alembic/manage/flows_controller.rb +37 -3
  7. data/app/controllers/alembic/manage/page_blocks_controller.rb +1 -1
  8. data/app/controllers/alembic/manage/pages_controller.rb +7 -0
  9. data/app/controllers/concerns/alembic/summarizes.rb +35 -2
  10. data/app/helpers/alembic/application_helper.rb +17 -2
  11. data/app/helpers/alembic/content_blocks_helper.rb +31 -0
  12. data/app/helpers/alembic/pages_helper.rb +7 -6
  13. data/app/helpers/alembic/result_blocks_helper.rb +57 -0
  14. data/app/models/alembic/flow/definition_summary.rb +2 -0
  15. data/app/models/alembic/flow/output_form.rb +74 -0
  16. data/app/models/alembic/flow/preview_values.rb +47 -0
  17. data/app/models/alembic/flow/summaries.rb +39 -0
  18. data/app/models/alembic/outputs/band.rb +1 -1
  19. data/app/models/alembic/outputs/lowest.rb +5 -3
  20. data/app/models/alembic/outputs/outcome.rb +33 -0
  21. data/app/models/alembic/page/content_blocks.rb +20 -0
  22. data/app/models/alembic/page/options.rb +14 -7
  23. data/app/models/alembic/page/preview.rb +15 -0
  24. data/app/models/alembic/result_blocks.rb +33 -0
  25. data/app/models/alembic/summary/report.rb +1 -1
  26. data/app/models/alembic/summary/result.rb +1 -1
  27. data/app/views/alembic/blocks/_answers.html.erb +11 -0
  28. data/app/views/alembic/blocks/_band.html.erb +6 -0
  29. data/app/views/alembic/blocks/_categories.html.erb +11 -0
  30. data/app/views/alembic/blocks/_code.html.erb +1 -0
  31. data/app/views/alembic/blocks/_facts.html.erb +5 -0
  32. data/app/views/alembic/blocks/_hero.html.erb +9 -0
  33. data/app/views/alembic/blocks/_lead.html.erb +15 -0
  34. data/app/views/alembic/blocks/_outcome_entry.html.erb +44 -0
  35. data/app/views/alembic/blocks/_outcomes.html.erb +10 -0
  36. data/app/views/alembic/blocks/_score.html.erb +7 -0
  37. data/app/views/alembic/blocks/_table.html.erb +20 -0
  38. data/app/views/alembic/blocks/_weakest.html.erb +12 -0
  39. data/app/views/alembic/flows/_question.html.erb +24 -0
  40. data/app/views/alembic/flows/complete.html.erb +9 -3
  41. data/app/views/alembic/flows/one_page.html.erb +35 -0
  42. data/app/views/alembic/flows/show.html.erb +16 -13
  43. data/app/views/alembic/flows/step.html.erb +47 -0
  44. data/app/views/alembic/flows/summary_page.html.erb +5 -0
  45. data/app/views/alembic/manage/flows/_outputs.html.erb +61 -0
  46. data/app/views/alembic/manage/flows/edit.html.erb +30 -0
  47. data/app/views/alembic/manage/pages/_block_content.html.erb +1 -1
  48. data/app/views/alembic/manage/pages/show.html.erb +10 -1
  49. data/config/routes.rb +1 -0
  50. data/db/migrate/20260930120000_add_summary_page_to_alembic_flow_definition_summaries.rb +6 -0
  51. data/db/migrate/20260930230000_add_shows_answer_values_to_alembic_flow_definition_summaries.rb +5 -0
  52. data/db/migrate/20260930233000_add_asks_on_one_page_to_alembic_flow_definition_summaries.rb +5 -0
  53. data/db/migrate/20261001120000_add_intro_page_to_alembic_flow_definition_summaries.rb +6 -0
  54. data/lib/alembic/engine.rb +10 -0
  55. data/lib/alembic/version.rb +1 -1
  56. data/lib/alembic.rb +5 -1
  57. metadata +33 -9
@@ -0,0 +1,20 @@
1
+ module Alembic
2
+ class Page < ApplicationRecord
3
+ module ContentBlocks
4
+ def self.register
5
+ Page.block(:alembic_hero, name: "Hero", width: 12, height: 3, drawn_by: :alembic_hero_block,
6
+ fields: [ { key: :kicker, label: "Small label" }, { key: :headline, label: "Headline" }, { key: :text, label: "Text" } ])
7
+ Page.block(:alembic_facts, name: "Facts", width: 12, height: 1, drawn_by: :alembic_facts_block,
8
+ fields: [ { key: :facts, label: "Facts, one per line" } ], body: :facts,
9
+ options: { question_count: { value: "question_count" } })
10
+ Page.block(:alembic_table, name: "Table", width: 12, height: 4, drawn_by: :alembic_table_block,
11
+ fields: [ { key: :rows, label: "Rows, one per line" } ], body: :rows)
12
+ Page.block(:alembic_code, name: "Code", width: 12, height: 3, drawn_by: :alembic_code_block,
13
+ fields: [ { key: :code, label: "Code" } ], body: :code)
14
+ Page.block(:alembic_start, name: "Start button", width: 4, height: 1, drawn_by: :alembic_start_block,
15
+ fields: [ { key: :label, label: "Label" } ],
16
+ options: { start_path: { value: "start_path" }, starts_a_run: { value: "starts_a_run" } })
17
+ end
18
+ end
19
+ end
20
+ end
@@ -6,8 +6,8 @@ module Alembic
6
6
  bodies[key.to_sym] = body
7
7
  end
8
8
 
9
- def self.for(block)
10
- named(filled(block).except(bodies[block["type"].to_sym]), declared.fetch(block["type"].to_sym, {}))
9
+ def self.for(block, values: {})
10
+ named(filled(block).except(bodies[block["type"].to_sym]), declared.fetch(block["type"].to_sym, {}), values)
11
11
  end
12
12
 
13
13
  def self.body_for(block)
@@ -28,19 +28,26 @@ module Alembic
28
28
  block.fetch("content", {}).reject { |_key, value| value == "" }.symbolize_keys
29
29
  end
30
30
 
31
- def self.named(filled, options)
31
+ def self.named(filled, options, values)
32
32
  options.each_with_object(filled) do |(option, spec), built|
33
- spec.is_a?(Hash) ? fill(built, option, spec) : built[option] = spec
33
+ spec.is_a?(Hash) ? fill(built, option, spec, values) : built[option] = spec
34
34
  end
35
35
  end
36
36
 
37
- def self.fill(built, option, spec)
38
- value = built.delete(spec.fetch(:from, option))
37
+ def self.fill(built, option, spec, values)
38
+ value = handed(built, option, spec, values)
39
39
  built[option] = value.nil? ? spec[:default] : value
40
40
  built.delete(option) if built[option].nil?
41
41
  end
42
42
 
43
- private_class_method :filled, :named, :fill
43
+ def self.handed(built, option, spec, values)
44
+ return values[spec[:value]] if spec.key?(:value)
45
+ return values[built.delete(spec[:value_of]).to_s] if spec.key?(:value_of)
46
+
47
+ built.delete(spec.fetch(:from, option))
48
+ end
49
+
50
+ private_class_method :filled, :named, :fill, :handed
44
51
  end
45
52
  end
46
53
  end
@@ -0,0 +1,15 @@
1
+ module Alembic
2
+ class Page < ApplicationRecord
3
+ module Preview
4
+ mattr_accessor :provider, :chooser
5
+
6
+ def self.values_for(page, choice)
7
+ provider ? provider.call(page, choice).to_h : {}
8
+ end
9
+
10
+ def self.choices_for(page)
11
+ chooser ? chooser.call(page).to_a : []
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,33 @@
1
+ module Alembic
2
+ module ResultBlocks
3
+ OUTPUT = { key: :output, label: "Output" }.freeze
4
+ HEADING = { key: :heading, label: "Heading" }.freeze
5
+
6
+ def self.register
7
+ Page.block(:alembic_score, name: "Score", width: 12, height: 3, drawn_by: :alembic_score_block,
8
+ fields: [ OUTPUT, { key: :kicker, label: "Label above" }, { key: :caption, label: "Caption" } ],
9
+ options: { value: { value_of: :output } })
10
+ Page.block(:alembic_band, name: "Band", width: 12, height: 2, drawn_by: :alembic_band_block,
11
+ fields: [ OUTPUT, { key: :score, label: "Score output" } ],
12
+ options: { value: { value_of: :output }, score: { value_of: :score } })
13
+ Page.block(:alembic_categories, name: "Score per category", width: 12, height: 4, drawn_by: :alembic_categories_block,
14
+ fields: [ OUTPUT, HEADING ], options: { value: { value_of: :output } })
15
+ Page.block(:alembic_weakest, name: "Weakest categories", width: 12, height: 4, drawn_by: :alembic_weakest_block,
16
+ fields: [ OUTPUT, HEADING, { key: :cost_label, label: "Cost label" } ], options: { value: { value_of: :output } })
17
+ Page.block(:alembic_answers, name: "Answers given", width: 12, height: 4, drawn_by: :alembic_answers_block,
18
+ fields: [ HEADING ], options: { value: { value: "answers" } })
19
+ Page.block(:alembic_outcome, name: "Chosen outcome", width: 12, height: 4, drawn_by: :alembic_outcome_block,
20
+ fields: [ OUTPUT ], options: { value: { value_of: :output } })
21
+ Page.block(:alembic_outcomes, name: "Every outcome", width: 12, height: 6, drawn_by: :alembic_outcomes_block,
22
+ fields: [ OUTPUT, HEADING ], options: { value: { value_of: :output } })
23
+ register_lead if Alembic.lead_address
24
+ end
25
+
26
+ def self.register_lead
27
+ Page.block(:alembic_lead, name: "Lead", width: 12, height: 3, drawn_by: :alembic_lead_block,
28
+ fields: [ HEADING, { key: :blurb, label: "Text" }, { key: :button, label: "Button" }, { key: :placeholder, label: "Placeholder" } ],
29
+ options: { slug: { value: "slug" }, note: { value: "note" } })
30
+ end
31
+ private_class_method :register_lead
32
+ end
33
+ end
@@ -13,7 +13,7 @@ module Alembic
13
13
  output_type = @registry.fetch(output["type"])
14
14
  produced[output["id"]] = output_type.compute(output, run, produced)
15
15
 
16
- Result.new(id: output["id"], label: label_for(output, output_type), value: produced[output["id"]])
16
+ Result.new(id: output["id"], type: output["type"], label: label_for(output, output_type), value: produced[output["id"]])
17
17
  end
18
18
  end
19
19
 
@@ -1,5 +1,5 @@
1
1
  module Alembic
2
2
  module Summary
3
- Result = Data.define(:id, :label, :value)
3
+ Result = Data.define(:id, :type, :label, :value)
4
4
  end
5
5
  end
@@ -0,0 +1,11 @@
1
+ <div>
2
+ <h2 class="text-xs font-semibold uppercase tracking-wider text-gray-500 mb-4"><%= heading %></h2>
3
+ <ul class="divide-y divide-gray-100 border-y border-gray-100">
4
+ <% answers.each do |question, answer| %>
5
+ <li class="flex items-baseline justify-between gap-6 py-3">
6
+ <span class="text-gray-600"><%= question %></span>
7
+ <span class="shrink-0 font-semibold text-gray-900"><%= answer %></span>
8
+ </li>
9
+ <% end %>
10
+ </ul>
11
+ </div>
@@ -0,0 +1,6 @@
1
+ <div class="text-center">
2
+ <span class="inline-block mt-5 px-4 py-1.5 rounded-full text-sm font-semibold text-white <%= tone %>"><%= band["name"] %></span>
3
+ <% if band["description"].present? %>
4
+ <p class="text-gray-600 leading-relaxed max-w-lg mx-auto mt-4"><%= band["description"] %></p>
5
+ <% end %>
6
+ </div>
@@ -0,0 +1,11 @@
1
+ <div>
2
+ <h2 class="text-xs font-semibold uppercase tracking-wider text-gray-500 mb-4"><%= heading %></h2>
3
+ <% shares.each do |name, share| %>
4
+ <div class="flex items-center gap-3 mb-2.5">
5
+ <span class="text-sm text-gray-600 w-44 shrink-0"><%= name %></span>
6
+ <span class="flex-1 h-2 bg-gray-100 rounded-full overflow-hidden">
7
+ <span class="block h-full rounded-full <%= alembic_tone(share) %>" style="width:<%= [ share.to_i, 4 ].max %>%"></span>
8
+ </span>
9
+ </div>
10
+ <% end %>
11
+ </div>
@@ -0,0 +1 @@
1
+ <pre class="overflow-x-auto rounded-lg bg-gray-900 p-4 text-sm text-gray-100"><code><%= code %></code></pre>
@@ -0,0 +1,5 @@
1
+ <div class="flex flex-wrap gap-2 mb-8">
2
+ <% facts.each do |value, label| %>
3
+ <span class="text-sm text-gray-500 bg-gray-50 border border-gray-200 rounded-md px-3 py-1.5"><% if label %><b class="text-gray-900"><%= value %></b> <%= label %><% else %><%= value %><% end %></span>
4
+ <% end %>
5
+ </div>
@@ -0,0 +1,9 @@
1
+ <div>
2
+ <% if kicker.present? %>
3
+ <p class="text-xs font-semibold uppercase tracking-widest text-accent-600 mb-3"><%= kicker %></p>
4
+ <% end %>
5
+ <h1 class="text-3xl md:text-4xl font-bold tracking-tight text-gray-900 mb-3"><%= headline %></h1>
6
+ <% if text.present? %>
7
+ <p class="text-lg text-gray-600 mb-6"><%= text %></p>
8
+ <% end %>
9
+ </div>
@@ -0,0 +1,15 @@
1
+ <%= ui_panel(padding: :lg) do %>
2
+ <h2 class="text-xl font-semibold text-gray-900 mb-2"><%= heading %></h2>
3
+ <% if blurb.present? %>
4
+ <p class="text-gray-600 mb-5"><%= blurb %></p>
5
+ <% end %>
6
+ <%= form_with url: address, method: :post, class: "flex flex-col sm:flex-row gap-3" do %>
7
+ <%= render Alembic.lead_fields_partial if Alembic.lead_fields_partial %>
8
+ <%= hidden_field_tag "slug", slug %>
9
+ <%= hidden_field_tag "note", note %>
10
+ <div class="flex-1">
11
+ <%= ui_input(name: "email", type: :email, placeholder: placeholder) %>
12
+ </div>
13
+ <button type="submit" class="inline-flex items-center justify-center font-semibold rounded-lg bg-accent-600 text-white hover:bg-accent-500 px-6 py-3 cursor-pointer whitespace-nowrap"><%= button %></button>
14
+ <% end %>
15
+ <% end %>
@@ -0,0 +1,44 @@
1
+ <%= ui_panel(padding: :lg) do %>
2
+ <details id="outcome-<%= entry["key"] %>" class="group" <%= "open" if open %>>
3
+ <summary class="flex items-start gap-3 cursor-pointer list-none">
4
+ <% if entry["label"].present? %>
5
+ <span class="text-xs font-semibold uppercase tracking-wider text-accent-600 border border-accent-200 rounded px-2 py-1 shrink-0"><%= entry["label"] %></span>
6
+ <% end %>
7
+ <span class="flex-1">
8
+ <span class="block text-lg font-semibold text-gray-900"><%= entry["name"] %></span>
9
+ <span class="block text-sm text-gray-500"><%= entry["tagline"] %></span>
10
+ </span>
11
+ <span class="text-gray-400 transition group-open:rotate-90">&#9656;</span>
12
+ </summary>
13
+
14
+ <div class="mt-5 space-y-5">
15
+ <% if Array(entry["facts"]).any? %>
16
+ <dl class="grid grid-cols-1 sm:grid-cols-3 gap-px overflow-hidden rounded-lg bg-gray-200">
17
+ <% Array(entry["facts"]).each do |label, value| %>
18
+ <div class="bg-gray-50 p-3"><dt class="text-xs uppercase tracking-wide text-gray-400 mb-1"><%= label %></dt><dd class="text-sm text-gray-900"><%= value %></dd></div>
19
+ <% end %>
20
+ </dl>
21
+ <% end %>
22
+
23
+ <% Array(entry["sections"]).each do |section| %>
24
+ <div>
25
+ <h4 class="text-xs font-semibold uppercase tracking-wider <%= section["note"].present? ? "text-red-600" : "text-accent-600" %> mb-1"><%= section["heading"] %></h4>
26
+ <p class="text-gray-700"><%= section["text"] %></p>
27
+ <% if section["note"].present? %>
28
+ <p class="mt-1 text-sm text-red-700"><span class="font-semibold">Result:</span> <%= section["note"] %></p>
29
+ <% end %>
30
+ </div>
31
+ <% end %>
32
+
33
+ <% if Array(entry["steps"]).any? %>
34
+ <div>
35
+ <h4 class="text-xs font-semibold uppercase tracking-wider text-accent-600 mb-2">How to build it</h4>
36
+ <% Array(entry["steps"]).each_with_index do |step, index| %>
37
+ <p class="text-sm font-semibold text-gray-900 mt-3"><%= index + 1 %>. <%= step["title"] %></p>
38
+ <pre class="mt-1 overflow-x-auto rounded-lg bg-gray-900 p-4 text-xs leading-relaxed text-gray-100"><code><%= step["code"] %></code></pre>
39
+ <% end %>
40
+ </div>
41
+ <% end %>
42
+ </div>
43
+ </details>
44
+ <% end %>
@@ -0,0 +1,10 @@
1
+ <div>
2
+ <% if heading.present? %>
3
+ <h2 class="text-2xl font-bold tracking-tight text-gray-900 mb-6"><%= heading %></h2>
4
+ <% end %>
5
+ <div class="space-y-3">
6
+ <% entries.each do |entry| %>
7
+ <%= render "alembic/blocks/outcome_entry", entry: entry, open: false %>
8
+ <% end %>
9
+ </div>
10
+ </div>
@@ -0,0 +1,7 @@
1
+ <div class="text-center">
2
+ <p class="text-xs font-semibold uppercase tracking-widest text-accent-600 mb-4"><%= kicker %></p>
3
+ <div class="text-7xl md:text-8xl font-bold tracking-tight text-gray-900"><%= value.nil? ? "—" : "#{value}%" %></div>
4
+ <% if caption.present? %>
5
+ <p class="text-sm text-gray-500 mt-1"><%= caption %></p>
6
+ <% end %>
7
+ </div>
@@ -0,0 +1,20 @@
1
+ <div class="overflow-x-auto">
2
+ <table class="w-full text-sm text-left">
3
+ <thead>
4
+ <tr class="border-b border-gray-200">
5
+ <% heading.each do |cell| %>
6
+ <th class="py-2 pr-4 font-semibold text-gray-900"><%= cell %></th>
7
+ <% end %>
8
+ </tr>
9
+ </thead>
10
+ <tbody>
11
+ <% rows.each do |row| %>
12
+ <tr class="border-b border-gray-100">
13
+ <% row.each do |cell| %>
14
+ <td class="py-2 pr-4 text-gray-600"><%= cell %></td>
15
+ <% end %>
16
+ </tr>
17
+ <% end %>
18
+ </tbody>
19
+ </table>
20
+ </div>
@@ -0,0 +1,12 @@
1
+ <div>
2
+ <h2 class="text-xs font-semibold uppercase tracking-wider text-gray-500 mb-4"><%= heading %></h2>
3
+ <div class="space-y-4">
4
+ <% categories.each do |category| %>
5
+ <%= ui_panel(padding: :lg) do %>
6
+ <h3 class="text-lg font-semibold text-gray-900 mb-1.5"><%= category["name"] %></h3>
7
+ <p class="text-gray-700 mb-2"><%= category["miss"] %></p>
8
+ <p class="text-sm text-accent-700"><span class="font-semibold"><%= cost_label %></span> <%= category["cost"] %></p>
9
+ <% end %>
10
+ <% end %>
11
+ </div>
12
+ </div>
@@ -0,0 +1,24 @@
1
+ <fieldset>
2
+ <% if category.present? %>
3
+ <p class="text-xs font-semibold uppercase tracking-wider text-accent-600 mb-2"><%= category %></p>
4
+ <% end %>
5
+ <legend class="text-xl font-semibold text-gray-900 mb-5 leading-snug"><%= question.text %></legend>
6
+ <div class="space-y-3">
7
+ <% question.choices.each do |choice| %>
8
+ <label class="block cursor-pointer">
9
+ <%= radio_button_tag "answers[#{question.id}]", choice.value, false, class: "peer sr-only", onchange: (local_assigns.fetch(:sends_on_choice, true) ? "this.form.requestSubmit()" : nil) %>
10
+ <span class="flex items-start gap-3 rounded-lg border-2 border-gray-200 bg-white px-4 py-3 transition hover:border-gray-400 peer-checked:border-accent-500 peer-checked:bg-accent-50">
11
+ <% if shows_values %>
12
+ <span class="text-xs font-semibold uppercase tracking-wider text-accent-600 border border-accent-200 rounded px-2 py-0.5 shrink-0 mt-0.5"><%= choice.value %></span>
13
+ <% end %>
14
+ <span class="text-gray-900">
15
+ <%= choice.label.presence || choice.value %>
16
+ <% if choice.hint.present? %>
17
+ <small class="block text-gray-500"><%= choice.hint %></small>
18
+ <% end %>
19
+ </span>
20
+ </span>
21
+ </label>
22
+ <% end %>
23
+ </div>
24
+ </fieldset>
@@ -11,13 +11,19 @@
11
11
 
12
12
  <% if @outputs.any? %>
13
13
  <%= ui_section do %>
14
- <%= ui_grid(cols: { default: 1, sm: 2 }, gap: :lg) do %>
14
+ <div class="space-y-10">
15
15
  <% @outputs.each do |output| %>
16
16
  <div data-output="<%= output.id %>">
17
- <%= ui_stat_card(label: output.label, value: alembic_output_lines(output.value).join(" · ")) %>
17
+ <%= alembic_default_result(output, @outputs) %>
18
18
  </div>
19
19
  <% end %>
20
- <% end %>
20
+ </div>
21
+ <% end %>
22
+ <% end %>
23
+
24
+ <% if Alembic.lead_address %>
25
+ <%= ui_section do %>
26
+ <%= alembic_lead_block(slug: summary_values["slug"], note: summary_values["note"]) %>
21
27
  <% end %>
22
28
  <% end %>
23
29
 
@@ -0,0 +1,35 @@
1
+ <% content_for(:title, @guide.headline) %>
2
+ <% content_for :head do %>
3
+ <%= javascript_include_tag "alembic/one_page", defer: true %>
4
+ <% end %>
5
+
6
+ <section class="py-20 md:py-28">
7
+ <div class="max-w-2xl mx-auto px-6">
8
+ <%= link_to "← Start over", flow_start_path(@guide.slug), class: "text-sm text-accent-600 hover:text-accent-700" %>
9
+
10
+ <%= form_with url: flow_step_path(@guide.slug), method: :get, data: { turbo: false, one_page: true }, class: "mt-6" do %>
11
+ <% @questions.each_with_index do |question, index| %>
12
+ <div data-one-page-question>
13
+ <div class="flex items-center justify-between text-sm text-gray-500 mb-2">
14
+ <span>Question <%= index + 1 %> of <%= @questions.size %></span>
15
+ </div>
16
+ <div class="h-1.5 bg-gray-100 rounded-full overflow-hidden mb-8">
17
+ <i class="block h-full bg-accent-600 rounded-full transition-all" style="width:<%= (index * 100.0 / @questions.size).round %>%"></i>
18
+ </div>
19
+ <%= render "alembic/flows/question", question: question, category: step_category(question), shows_values: shows_answer_values?, sends_on_choice: false %>
20
+ <% if index.positive? %>
21
+ <button type="button" data-one-page-previous hidden class="mt-6 cursor-pointer text-sm text-gray-500 underline underline-offset-2 hover:text-gray-900">← Previous</button>
22
+ <% end %>
23
+ </div>
24
+ <% end %>
25
+
26
+ <div data-one-page-ready class="mt-8">
27
+ <p class="text-gray-600 mb-5">That's every question.</p>
28
+ <div class="flex items-center gap-4">
29
+ <button type="submit" class="inline-flex items-center justify-center font-semibold rounded-lg bg-accent-600 text-white hover:bg-accent-500 px-6 py-3 cursor-pointer">See my results</button>
30
+ <button type="button" data-one-page-previous hidden class="cursor-pointer text-sm text-gray-500 underline underline-offset-2 hover:text-gray-900">← Previous</button>
31
+ </div>
32
+ </div>
33
+ <% end %>
34
+ </div>
35
+ </section>
@@ -1,17 +1,20 @@
1
- <% content_for(:title, @flow.title.presence || @flow.slug) %>
1
+ <% intro_page = Alembic::Flow::Summaries.new(@flow).intro_page %>
2
+ <% if intro_page&.live_version %>
3
+ <% content_for(:title, @flow.title.presence || @flow.slug) %>
4
+ <% content_for :head do %>
5
+ <%= stylesheet_link_tag "ks_blocks" %>
6
+ <% end %>
2
7
 
3
- <%= ui_page(max_width: :md) do %>
4
- <%= ui_page_header(title: @flow.title.presence || @flow.slug, subtitle: flow_summary(@flow)) %>
8
+ <%= drawn_page(intro_page, values: intro_values) %>
9
+ <% else %>
10
+ <% content_for(:title, intro_values["title"]) %>
5
11
 
6
- <%= ui_section do %>
7
- <div class="flex flex-wrap items-center gap-3">
8
- <% if @flow.each_step? && @flow.definition.present? && !previewing? %>
9
- <%= ui_form(action: alembic.flow_runs_path(@flow.slug), method: :post) do %>
10
- <%= ui_button(label: @flow.start_label.presence || "Start", type: :submit) %>
11
- <% end %>
12
- <% else %>
13
- <%= ui_button(label: @flow.start_label.presence || "Start", href: first_step_path(@flow.slug)) %>
14
- <% end %>
12
+ <section class="py-20 md:py-28">
13
+ <div class="max-w-2xl mx-auto px-6">
14
+ <%= alembic_hero_block(headline: intro_values["title"], text: intro_values["summary"]) %>
15
+ <%= alembic_facts_block(question_count: intro_values["question_count"]) { "{question_count} | questions" } %>
16
+ <%= alembic_start_block(label: @flow.start_label.presence || "Start →", start_path: intro_values["start_path"],
17
+ starts_a_run: intro_values["starts_a_run"]) %>
15
18
  </div>
16
- <% end %>
19
+ </section>
17
20
  <% end %>
@@ -0,0 +1,47 @@
1
+ <% content_for(:title, @guide.headline) %>
2
+
3
+ <section class="py-20 md:py-28">
4
+ <div class="max-w-2xl mx-auto px-6">
5
+ <%= link_to "← Start over", flow_start_path(@guide.slug), class: "text-sm text-accent-600 hover:text-accent-700" %>
6
+
7
+ <% answered = @answers.size %>
8
+ <% questions = answered + @guide.questions_left(@answers) %>
9
+ <div class="mt-6 flex items-center justify-between text-sm text-gray-500 mb-2">
10
+ <span>Question <%= answered + 1 %> of <%= questions %></span>
11
+ </div>
12
+ <div class="h-1.5 bg-gray-100 rounded-full overflow-hidden mb-8">
13
+ <i class="block h-full bg-accent-600 rounded-full transition-all" style="width:<%= (answered * 100.0 / [ questions, 1 ].max).round %>%"></i>
14
+ </div>
15
+
16
+ <% if flash[:alert] %>
17
+ <div class="mt-6"><%= ui_alert(message: flash[:alert], type: :error) %></div>
18
+ <% end %>
19
+
20
+ <% if @waiting %>
21
+ <p class="mt-6 text-gray-700">Waiting for <%= @waiting %>.</p>
22
+ <% else %>
23
+ <%= form_with url: step_form[:url], method: step_form[:method], data: { turbo: false }, class: "mt-6" do %>
24
+ <%= hidden_field_tag :asked, @question.id %>
25
+ <% if carries_answers? %>
26
+ <% @answers.each do |key, value| %>
27
+ <%= hidden_field_tag "answers[#{key}]", value %>
28
+ <% end %>
29
+ <% end %>
30
+
31
+ <% if @question.respond_to?(:choices) %>
32
+ <%= render "alembic/flows/question", question: @question, category: step_category(@question),
33
+ shows_values: shows_answer_values? %>
34
+ <% else %>
35
+ <%= render @drawing, step: @question %>
36
+ <% end %>
37
+
38
+ <div class="mt-8 flex items-center gap-4">
39
+ <%= ui_button(label: "Next", type: :submit) %>
40
+ <% if @answers.any? %>
41
+ <button type="submit" name="back" value="1" formnovalidate class="cursor-pointer text-sm text-gray-500 underline underline-offset-2 hover:text-gray-900">← Back</button>
42
+ <% end %>
43
+ </div>
44
+ <% end %>
45
+ <% end %>
46
+ </div>
47
+ </section>
@@ -0,0 +1,5 @@
1
+ <% content_for :head do %>
2
+ <%= stylesheet_link_tag "ks_blocks" %>
3
+ <% end %>
4
+
5
+ <%= drawn_page(@summary_page, values: summary_values) %>
@@ -0,0 +1,61 @@
1
+ <%= ui_section(title: "What it works out") do %>
2
+ <% uncategorised.each do |question| %>
3
+ <div class="mb-4" data-problem>
4
+ <%= ui_alert(type: :warning, message: "#{EasyFlow::Steps::Question.asked(question)} has no category, so it counts toward no category's score.") %>
5
+ </div>
6
+ <% end %>
7
+ <% outputs.each_with_index do |output, index| %>
8
+ <% field = "flow[outputs][#{index}]" %>
9
+ <div data-output-editor="<%= output["id"] %>" class="mb-6">
10
+ <%= hidden_field_tag "#{field}[id]", output["id"], id: nil %>
11
+ <%= hidden_field_tag "#{field}[type]", output["type"], id: nil %>
12
+ <p class="mb-1 text-sm ks-tone-muted"><%= output["type"] %></p>
13
+ <%= ui_input(name: "#{field}[label]", value: output["label"]) %>
14
+ <% if output["type"] == "band" %>
15
+ <% (Array(output["bands"]) + [ {} ]).each_with_index do |band, position| %>
16
+ <div class="mt-3 grid grid-cols-1 gap-2 sm:grid-cols-3" data-band>
17
+ <%= ui_input(name: "#{field}[bands][#{position}][name]", value: band["name"], placeholder: "Name") %>
18
+ <%= ui_input(name: "#{field}[bands][#{position}][ceiling]", value: band["ceiling"], type: :number, placeholder: "Up to, below") %>
19
+ <%= ui_input(name: "#{field}[bands][#{position}][description]", value: band["description"], placeholder: "Description") %>
20
+ </div>
21
+ <% end %>
22
+ <% end %>
23
+ <% if output["type"] == "outcome" %>
24
+ <label class="ks-label mb-1 mt-3">Rules, first that holds wins</label>
25
+ <%= ui_textarea(name: "#{field}[rules_text]", value: alembic_rules_text(output["rules"]), rows: 4) %>
26
+ <% (Array(output["entries"]) + [ {} ]).each_with_index do |entry, position| %>
27
+ <% place = "#{field}[entries][#{position}]" %>
28
+ <% texts = alembic_entry_texts(entry) %>
29
+ <div class="mt-4 space-y-2 border-t border-(color:--ks-color-divider) pt-4" data-outcome-entry>
30
+ <div class="grid grid-cols-1 gap-2 sm:grid-cols-2">
31
+ <%= ui_input(name: "#{place}[key]", value: entry["key"], placeholder: "Key the rules name") %>
32
+ <%= ui_input(name: "#{place}[label]", value: entry["label"], placeholder: "Label, such as Tier 1") %>
33
+ <%= ui_input(name: "#{place}[name]", value: entry["name"], placeholder: "Name") %>
34
+ <%= ui_input(name: "#{place}[tagline]", value: entry["tagline"], placeholder: "Tagline") %>
35
+ </div>
36
+ <%= ui_textarea(name: "#{place}[facts_text]", value: texts["facts_text"], rows: 3, placeholder: "Facts, one per line: Label | value") %>
37
+ <%= ui_textarea(name: "#{place}[sections_text]", value: texts["sections_text"], rows: 4, placeholder: "Sections, one per line: Heading | text | result note") %>
38
+ <%= ui_textarea(name: "#{place}[steps_text]", value: texts["steps_text"], rows: 6, placeholder: "Steps: a title line, then code, with --- between steps") %>
39
+ </div>
40
+ <% end %>
41
+ <% end %>
42
+ <% if output["type"] == "lowest" %>
43
+ <label class="ks-label mb-1 mt-3">How many to name</label>
44
+ <%= ui_input(name: "#{field}[count]", value: output["count"], type: :number) %>
45
+ <% categories.each do |category| %>
46
+ <div class="mt-3 grid grid-cols-1 gap-2 sm:grid-cols-3" data-category-copy>
47
+ <p class="text-sm"><%= category %></p>
48
+ <%= ui_input(name: "#{field}[copy][#{category}][miss]", value: output.dig("copy", category, "miss"), placeholder: "What it misses") %>
49
+ <%= ui_input(name: "#{field}[copy][#{category}][cost]", value: output.dig("copy", category, "cost"), placeholder: "What it costs") %>
50
+ </div>
51
+ <% end %>
52
+ <% end %>
53
+ </div>
54
+ <% end %>
55
+ <% field = "flow[outputs][#{outputs.size}]" %>
56
+ <div data-new-outcome class="mt-6">
57
+ <label class="ks-label mb-1">Add an outcome</label>
58
+ <%= hidden_field_tag "#{field}[type]", "outcome", id: nil %>
59
+ <%= ui_input(name: "#{field}[id]", placeholder: "The new outcome's id") %>
60
+ </div>
61
+ <% end %>
@@ -17,6 +17,36 @@
17
17
  <%= ui_input(name: "flow[start_label]", value: @flow.start_label) %>
18
18
  <% end %>
19
19
 
20
+ <%= ui_section(title: "How each question is asked") do %>
21
+ <%= hidden_field_tag "flow[shows_answer_values]", "0", id: nil %>
22
+ <label class="flex items-center gap-2">
23
+ <%= check_box_tag "flow[shows_answer_values]", "1", @shows_answer_values %>
24
+ <span>Show each answer's value beside its label</span>
25
+ </label>
26
+
27
+ <%= hidden_field_tag "flow[asks_on_one_page]", "0", id: nil %>
28
+ <label class="mt-3 flex items-center gap-2">
29
+ <%= check_box_tag "flow[asks_on_one_page]", "1", @asks_on_one_page, disabled: @branches %>
30
+ <span>Ask every question on one page</span>
31
+ </label>
32
+ <% end %>
33
+
34
+ <%= ui_section(title: "Where this starts") do %>
35
+ <p class="mb-3"><%= link_to "Preview the intro page", alembic.manage_flow_preview_path(@flow), class: "text-sm text-(color:--ks-color-link) hover:text-(color:--ks-color-link-hover) dark:text-(color:--ks-color-link-dark) dark:hover:text-(color:--ks-color-link-hover-dark)" %></p>
36
+ <%= ui_select(name: "flow[intro_page_id]", selected: @intro_page&.id,
37
+ include_blank: "Alembic's default intro page",
38
+ options: @published_pages.map { |page| [ page.name, page.id ] }) %>
39
+ <% end %>
40
+
41
+ <%= ui_section(title: "Where this finishes") do %>
42
+ <p class="mb-3"><%= link_to "Preview the summary page with sample results", alembic.step_manage_flow_preview_path(@flow, answers: Alembic::Flow::PreviewValues.sample_state(@flow)), class: "text-sm text-(color:--ks-color-link) hover:text-(color:--ks-color-link-hover) dark:text-(color:--ks-color-link-dark) dark:hover:text-(color:--ks-color-link-hover-dark)" %></p>
43
+ <%= ui_select(name: "flow[summary_page_id]", selected: @summary_page&.id,
44
+ include_blank: "Alembic's default summary page",
45
+ options: @published_pages.map { |page| [ page.name, page.id ] }) %>
46
+ <% end %>
47
+
48
+ <%= render "alembic/manage/flows/outputs", outputs: @outputs, categories: @categories, uncategorised: @uncategorised %>
49
+
20
50
  <%= ui_section(title: "What this keeps of a run") do %>
21
51
  <%= ui_select(name: "flow[persists]", selected: @flow.persists,
22
52
  options: [ [ "Nothing", "unsaved" ],
@@ -1 +1 @@
1
- <%= shown_block(block) %>
1
+ <%= shown_block(block, values: preview_values(page)) %>
@@ -8,10 +8,19 @@
8
8
  <%= ui_button(label: "Publish", type: :submit, variant: :secondary) %>
9
9
  <% end %>
10
10
 
11
+ <% choices = Alembic::Page::Preview.choices_for(@page) %>
12
+ <% if choices.any? %>
13
+ <%= ui_form(action: alembic.preview_manage_page_path(@page), method: :patch) do %>
14
+ <label class="ks-label mb-1">Preview with</label>
15
+ <%= ui_select(name: "choice", options: choices, selected: session.dig(:alembic_preview, @page.id.to_s), include_blank: "Sample results") %>
16
+ <%= ui_button(label: "Show", type: :submit, variant: :secondary) %>
17
+ <% end %>
18
+ <% end %>
19
+
11
20
  <%= ui_panel do %>
12
21
  <%= react_ui("alembic/page-builder", @payload.merge(token: form_authenticity_token)) %>
13
22
  <% end %>
14
23
 
15
24
  <%= block_contents(@page.blocks) do |block| %>
16
- <%= render "alembic/manage/pages/block_content", block: block %>
25
+ <%= render "alembic/manage/pages/block_content", block: block, page: @page %>
17
26
  <% end %>
data/config/routes.rb CHANGED
@@ -2,6 +2,7 @@ Alembic::Engine.routes.draw do
2
2
  namespace :manage do
3
3
  resources :pages, only: [ :index, :create, :show ] do
4
4
  post "publish", on: :member
5
+ patch "preview", on: :member
5
6
  get "layout", to: "page_blocks#layout", as: :layout
6
7
  post "blocks", to: "page_blocks#add_block", as: :blocks
7
8
  patch "blocks", to: "page_blocks#place_blocks"
@@ -0,0 +1,6 @@
1
+ class AddSummaryPageToAlembicFlowDefinitionSummaries < ActiveRecord::Migration[8.1]
2
+ def change
3
+ add_reference :alembic_flow_definition_summaries, :summary_page,
4
+ foreign_key: { to_table: :alembic_pages, on_delete: :nullify }
5
+ end
6
+ end