diff --git a/app/controllers/phlex_storybook/experiments_controller.rb b/app/controllers/phlex_storybook/experiments_controller.rb index 5bedc44..4dc06f0 100644 --- a/app/controllers/phlex_storybook/experiments_controller.rb +++ b/app/controllers/phlex_storybook/experiments_controller.rb @@ -4,7 +4,7 @@ module PhlexStorybook class ExperimentsController < ApplicationController before_action :reject_unless_editable! - layout -> { Layouts::ApplicationLayout } + layout -> { turbo_frame_request? ? false : Layouts::ApplicationLayout } def preview respond_to do |format| @@ -29,6 +29,21 @@ def show end end + def update + File.write(experiment, params[:ruby]) + + respond_to do |format| + format.turbo_stream do + render turbo_stream: [ + turbo_stream.replace( + "experiment_preview", + Components::Experiments::ExperimentPreview.new(name: experiment_name), + ), + ] + end + end + end + private def experiment diff --git a/app/javascript/phlex_storybook/controllers/code_editor_controller.js b/app/javascript/phlex_storybook/controllers/code_editor_controller.js index 66634a4..aa259c9 100644 --- a/app/javascript/phlex_storybook/controllers/code_editor_controller.js +++ b/app/javascript/phlex_storybook/controllers/code_editor_controller.js @@ -2,10 +2,21 @@ import { Controller } from "@hotwired/stimulus" import * as monaco from "monaco-editor" export default class extends Controller { - static targets = ["ruby"]; + static targets = ["editor", "ruby"]; connect() { - monaco.editor.create(this.element, {value: this.source(), language: "ruby", theme: "vs-dark"}); + this.editor = monaco.editor.create(this.editorTarget, { + value: this.source(), + language: "ruby", + theme: "vs-dark", + scrollBeyondLastLine: false, + fontSize: 14, + automaticLayout: true, + }); + } + + saveSource() { + this.rubyTarget.textContent = this.editor.getValue(); } source() { diff --git a/config/routes.rb b/config/routes.rb index c0a0623..bafd317 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -2,7 +2,7 @@ root to: 'stories#index' resources :components, only: [:update] - resources :experiments, only: [:show] do + resources :experiments, only: [:show, :update] do member do get :preview end diff --git a/lib/phlex_storybook/assets/phlex_storybook_application.css b/lib/phlex_storybook/assets/phlex_storybook_application.css index 4ad0642..4aa374d 100644 --- a/lib/phlex_storybook/assets/phlex_storybook_application.css +++ b/lib/phlex_storybook/assets/phlex_storybook_application.css @@ -891,6 +891,10 @@ fieldset{ right: 0px; } +.row-span-3{ + grid-row: span 3 / span 3; +} + .mb-4{ margin-bottom: 1rem; } @@ -941,6 +945,16 @@ fieldset{ height: 1.25rem; } +.size-6{ + width: 1.5rem; + height: 1.5rem; +} + +.size-8{ + width: 2rem; + height: 2rem; +} + .h-fit{ height: -moz-fit-content; height: fit-content; @@ -1006,10 +1020,18 @@ fieldset{ transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y)); } +.grid-flow-row{ + grid-auto-flow: row; +} + .grid-cols-1{ grid-template-columns: repeat(1, minmax(0, 1fr)); } +.grid-rows-6{ + grid-template-rows: repeat(6, minmax(0, 1fr)); +} + .flex-col{ flex-direction: column; } @@ -1063,6 +1085,11 @@ fieldset{ background-color: rgb(243 244 246 / var(--tw-bg-opacity)); } +.bg-slate-500{ + --tw-bg-opacity: 1; + background-color: rgb(100 116 139 / var(--tw-bg-opacity)); +} + .bg-slate-600{ --tw-bg-opacity: 1; background-color: rgb(71 85 105 / var(--tw-bg-opacity)); @@ -1103,6 +1130,16 @@ fieldset{ padding: 0.5rem; } +.px-0{ + padding-left: 0px; + padding-right: 0px; +} + +.px-1{ + padding-left: 0.25rem; + padding-right: 0.25rem; +} + .px-2{ padding-left: 0.5rem; padding-right: 0.5rem; @@ -1189,6 +1226,10 @@ fieldset{ color: rgb(255 255 255 / var(--tw-text-opacity)); } +.opacity-70{ + opacity: 0.7; +} + .story-selector { ul{ margin-bottom: 0.75rem; @@ -1235,11 +1276,19 @@ li.story-button-active{ color: rgb(75 85 99 / var(--tw-text-opacity)); } +.hover\:cursor-pointer:hover{ + cursor: pointer; +} + .hover\:bg-blue-700:hover{ --tw-bg-opacity: 1; background-color: rgb(29 78 216 / var(--tw-bg-opacity)); } +.hover\:opacity-100:hover{ + opacity: 1; +} + .hover\:ring-1:hover{ --tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color); --tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color); diff --git a/lib/phlex_storybook/components/experiments/experiment_display.rb b/lib/phlex_storybook/components/experiments/experiment_display.rb index dbd491c..1ea30cd 100644 --- a/lib/phlex_storybook/components/experiments/experiment_display.rb +++ b/lib/phlex_storybook/components/experiments/experiment_display.rb @@ -4,8 +4,6 @@ module PhlexStorybook module Components module Experiments class ExperimentDisplay < ApplicationView - # include Phlex::Rails::Helpers::IframeTag - def initialize(name:) @name = name end @@ -19,10 +17,17 @@ def view_template end turbo_frame_tag("experiment_display") do - div(class: "flex flex-col h-screen max-h-screen", data: { controller: "story-display copy" }) do - render_header @name - div(class: "px-2 flex-1 overflow-y-scroll overflow-x-hidden") do - iframe(src: helpers.preview_experiment_path(@name), class: "w-full h-full") + form(data: { turbo_streams: true }, method: "PUT", action: helpers.experiment_path(@name)) do + div(class: "flex flex-col h-screen max-h-screen w-full", data: { controller: "code-editor" }) do + render_header @name + + div(class: "grid grid-flow-row grid-rows-6 h-full w-full px-0") do + div(class: "row-span-3 overflow-x-hidden") do + render ExperimentEditor.new(name: @name) + end + + render ExperimentPreview.new(name: @name) + end end end end @@ -36,7 +41,15 @@ def blank_template end def render_header(text) - h2(class: "bg-slate-900 text-white border-x border-slate-700 p-2 flex-none") { text } + h2(class: "flex justify-between bg-slate-900 text-white border-x border-slate-700 p-2 flex-none") do + div { text.classify } + div do + button( + class: "px-1 opacity-70 hover:opacity-100 hover:cursor-pointer", + data: { action: "click->code-editor#saveSource" } + ) { render Icon.new(:Save, size: :md) } + end + end end end end diff --git a/lib/phlex_storybook/components/experiments/experiment_editor.rb b/lib/phlex_storybook/components/experiments/experiment_editor.rb new file mode 100644 index 0000000..74fb2fd --- /dev/null +++ b/lib/phlex_storybook/components/experiments/experiment_editor.rb @@ -0,0 +1,26 @@ +# frozen_string_literal: true + +module PhlexStorybook + module Components + module Experiments + class ExperimentEditor < ApplicationView + def initialize(name:) + @name = name + end + + def view_template + div id: "source", class: "h-full overflow-auto scroll", data: { code_editor_target: "editor" } + textarea(class: "hidden", data: { copy_target: "source", code_editor_target: "ruby" }, name: "ruby") do + source + end + end + + private + + def source + File.read(PhlexStorybook.configuration.experiment(@name)) + end + end + end + end +end diff --git a/lib/phlex_storybook/components/experiments/experiment_preview.rb b/lib/phlex_storybook/components/experiments/experiment_preview.rb new file mode 100644 index 0000000..dde9e73 --- /dev/null +++ b/lib/phlex_storybook/components/experiments/experiment_preview.rb @@ -0,0 +1,27 @@ +# frozen_string_literal: true + +require 'digest' + +module PhlexStorybook + module Components + module Experiments + class ExperimentPreview < ApplicationView + def initialize(name:) + @name = name + end + + def view_template + turbo_frame_tag("experiment_preview") do + div(class: "text-xs bg-slate-500 text-slate-100 w-full p-2") do + path = PhlexStorybook.configuration.experiment @name + "file: #{path}, md5: #{Digest::MD5.hexdigest(File.read(path))}" + end + div(class: "row-span-3 h-full w-full overflow-y-scroll overflow-x-hidden px-2") do + iframe(src: helpers.preview_experiment_path(@name), class: "w-full h-full") + end + end + end + end + end + end +end diff --git a/lib/phlex_storybook/components/icon.rb b/lib/phlex_storybook/components/icon.rb index e820ed4..c2fd1a3 100644 --- a/lib/phlex_storybook/components/icon.rb +++ b/lib/phlex_storybook/components/icon.rb @@ -3,13 +3,20 @@ module PhlexStorybook module Components class Icon < ApplicationView - def initialize(icon_class, inactive_class = nil, active: false) - @icon_class = inactive_class && !active ? inactive_class : icon_class + SIZES = { + sm: "size-4", + md: "size-6", + lg: "size-8", + } + + def initialize(icon_class, inactive_class = nil, active: false, size: nil) @active = active + @icon_class = inactive_class && !active ? inactive_class : icon_class + @size = size || :sm end def view_template - render Phlex::Icons::Lucide.const_get(@icon_class).new(classes: "#{icon_color} size-4 inline") + render Phlex::Icons::Lucide.const_get(@icon_class).new(classes: "#{icon_color} #{SIZES[@size]} inline") end private diff --git a/lib/phlex_storybook/layouts/application_layout.rb b/lib/phlex_storybook/layouts/application_layout.rb index e33a1c2..42f8b95 100644 --- a/lib/phlex_storybook/layouts/application_layout.rb +++ b/lib/phlex_storybook/layouts/application_layout.rb @@ -22,10 +22,11 @@ def view_template(&) stylesheet_link_tag "phlex_storybook_application", media: "all" turbo_refreshes_with method: :morph, scroll: :preserve end - span(class: "fixed bottom-0 right-0 p-2 text-xs text-indigo-200") do - "v#{PhlexStorybook::VERSION}" - end + body class: "bg-gray-100 text-gray-900 dark:bg-gray-900 dark:text-gray-100" do + span(class: "fixed bottom-0 right-0 p-2 text-xs text-indigo-200") do + "v#{PhlexStorybook::VERSION}" + end yield end end