Add basic session UI

This commit is contained in:
Jonatan Kłosko 2021-01-16 17:04:47 +01:00
parent e31e14d753
commit 4b1a86527f
9 changed files with 279 additions and 30 deletions

View file

@ -4,3 +4,14 @@
@import "../node_modules/nprogress/nprogress.css";
@import "./live_view.css";
/* Remove the default outline on focused elements */
:focus,
button:focus {
outline: none;
}
/* Hide Phoenix live reload frame */
iframe[hidden] {
display: none;
}

View file

@ -87,6 +87,9 @@ defmodule LiveBookWeb do
import LiveBookWeb.ErrorHelpers
alias LiveBookWeb.Router.Helpers, as: Routes
# Custom helpers
alias LiveBookWeb.Icons
end
end

View file

@ -0,0 +1,52 @@
defmodule LiveBookWeb.Cell do
use LiveBookWeb, :live_component
def render(assigns) do
~L"""
<div class="<%= cell_class(@focused) %>" phx-click="focus_cell" phx-value-cell_id="<%= @cell.id %>">
<div class="flex flex-col items-center space-y-2 absolute right-0 top-0 -mr-10">
<button class="text-gray-500 hover:text-current">
<%= Icons.svg(:play, class: "h-6") %>
</button>
<button phx-click="delete_cell" phx-value-cell_id="<%= @cell.id %>" class="text-gray-500 hover:text-current">
<%= Icons.svg(:trash, class: "h-6") %>
</button>
<%= cell_type_icon(@cell.type) %>
</div>
<div
id="cell-<%= @cell.id %>"
phx-hook="Editor"
phx-update="ignore"
data-id="<%= @cell.id %>"
data-type="<%= @cell.type %>"
>
<div class="h-20 flex opacity-20">Editor placeholder</div>
</div>
<%= for output <- @cell.outputs do %>
<div class="p-2">
<%= render_output(output) %>
</div>
<% end %>
</div>
"""
end
defp cell_class(focused) do
base = "flex flex-col p-2 border-2 border-gray-200 rounded border-opacity-0 relative mr-10"
if focused, do: base <> " border-opacity-100", else: base
end
defp render_output(_), do: nil
defp cell_type_icon(:elixir) do
~e"""
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32"><path fill="#8251A8" d="M16.457 27.304c-4.118 0-7.457-3.971-7.457-8.87 0-4.012 2.96-8.915 5.244-11.901C15.326 5.119 17.371 4 17.371 4s-1.048 5.714 1.794 7.984c2.523 2.014 4.38 4.635 4.38 6.94 0 4.618-2.97 8.38-7.088 8.38z"/></svg>
"""
end
defp cell_type_icon(:markdown) do
~e"""
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32"><path fill="#EE9D5C" fill-rule="evenodd" d="M17.74 21.419h-3.48v-5.416l-2.631 3.463-2.631-3.463v5.416h-3.48V10.586h3.48l2.63 3.625 2.632-3.625h3.48V21.42zm5.223.874L18.63 16h2.631v-5.416h3.48V16h2.632l-4.409 6.293h-.002zM27.99 7H4.01c-.541 0-1.012.206-1.411.617-.4.411-.599.895-.599 1.453v13.86c0 .585.2 1.076.599 1.474.4.397.87.596 1.41.596H27.99c.541 0 1.012-.199 1.411-.596.4-.398.599-.89.599-1.474V9.07c0-.558-.2-1.042-.599-1.453-.4-.411-.87-.617-1.41-.617z"/></svg>
"""
end
end

View file

@ -0,0 +1,51 @@
defmodule LiveBookWeb.Icons do
import Phoenix.HTML
import Phoenix.HTML.Tag
@doc """
Returns icon svg tag.
"""
@spec svg(atom(), keyword()) :: Phoenix.HTML.safe()
def svg(name, attrs \\ [])
def svg(:chevron_right, attrs) do
~e"""
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7" />
"""
|> heroicon_svg(attrs)
end
def svg(:play, attrs) do
~e"""
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M14.752 11.168l-3.197-2.132A1 1 0 0010 9.87v4.263a1 1 0 001.555.832l3.197-2.132a1 1 0 000-1.664z" />
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
"""
|> heroicon_svg(attrs)
end
def svg(:plus, attrs) do
~e"""
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 6v6m0 0v6m0-6h6m-6 0H6" />
"""
|> heroicon_svg(attrs)
end
def svg(:trash, attrs) do
~e"""
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" />
"""
|> heroicon_svg(attrs)
end
# https://heroicons.com
defp heroicon_svg(svg_content, attrs) do
heroicon_svg_attrs = [
xmlns: "http://www.w3.org/2000/svg",
fill: "none",
viewBox: "0 0 24 24",
stroke: "currentColor"
]
content_tag(:svg, svg_content, Keyword.merge(attrs, heroicon_svg_attrs))
end
end

View file

@ -0,0 +1,34 @@
defmodule LiveBookWeb.InsertCellActions do
use LiveBookWeb, :live_component
def render(assigns) do
~L"""
<div class="opacity-0 hover:opacity-100 flex space-x-2 justify-center items-center">
<%= line() %>
<button
phx-click="insert_cell"
phx-value-type="markdown"
phx-value-section_id="<%= @section_id %>"
phx-value-index="<%= @index %>"
class="py-1 px-2 rounded text-sm hover:bg-gray-100 border border-gray-200 bg-gray-50">
+ Markdown
</button>
<button
phx-click="insert_cell"
phx-value-type="elixir"
phx-value-section_id="<%= @section_id %>"
phx-value-index="<%= @index %>"
class="py-1 px-2 rounded text-sm hover:bg-gray-100 border border-gray-200 bg-gray-50">
+ Elixir
</button>
<%= line() %>
</div>
"""
end
defp line() do
~e"""
<div class="border-t-2 border-dashed border-gray-200 flex-grow"></div>
"""
end
end

View file

@ -0,0 +1,30 @@
defmodule LiveBookWeb.Section do
use LiveBookWeb, :live_component
def render(assigns) do
~L"""
<div class="<%= if not @selected, do: "hidden" %>">
<div class="flex justify-between items-center">
<div class="flex space-x-2 items-center text-gray-600">
<%= Icons.svg(:chevron_right, class: "h-8") %>
<h2 class="text-3xl" contenteditable spellcheck="false"><%= @section.name %></h2>
</div>
<div class="flex space-x-2 items-center">
<button phx-click="delete_section" phx-value-section_id="<%= @section.id %>" class="text-gray-600 hover:text-current">
<%= Icons.svg(:trash, class: "h-6") %>
</button>
</div>
</div>
<div class="container py-4">
<div class="flex flex-col space-y-2">
<%= live_component @socket, LiveBookWeb.InsertCellActions, section_id: @section.id, index: 0, id: "insert-#{@section.id}-0" %>
<%= for {cell, index} <- Enum.with_index(@section.cells) do %>
<%= live_component @socket, LiveBookWeb.Cell, cell: cell, id: "cell-#{cell.id}", focused: cell.id == @focused_cell_id %>
<%= live_component @socket, LiveBookWeb.InsertCellActions, section_id: @section.id, index: index + 1, id: "insert-#{@section.id}-#{index + 1}" %>
<% end %>
</div>
</div>
</div>
"""
end
end

View file

@ -13,7 +13,7 @@ defmodule LiveBookWeb.SessionLive do
data = Session.get_data(session_id)
{:ok, assign(socket, session_id: session_id, data: data)}
{:ok, assign(socket, session_id: session_id, data: data, selected_section_id: nil, focused_cell_id: nil)}
else
{:ok, redirect(socket, to: Routes.live_path(socket, LiveBookWeb.SessionsLive))}
end
@ -22,12 +22,80 @@ defmodule LiveBookWeb.SessionLive do
@impl true
def render(assigns) do
~L"""
<div class="container max-w-screen-md p-4 mx-auto">
Session <%= @session_id %>
<div class="flex flex-grow max-h-full">
<div class="w-1/5 bg-gray-100 border-r-2 border-gray-200">
<h1 class="p-8 text-2xl" contenteditable spellcheck="false"><%= @data.notebook.name %></h1>
<div class="flex flex-col space-y-2 pl-4">
<%= for section <- @data.notebook.sections do %>
<div phx-click="select_section" phx-value-section_id="<%= section.id %>" class="py-2 px-4 rounded-l-md cursor-pointer text-gray-500 hover:text-current">
<span>
<%= section.name %>
</span>
</div>
<% end %>
<div phx-click="add_section" class="py-2 px-4 rounded-l-md cursor-pointer text-gray-300 hover:text-gray-400">
<div class="flex items-center space-x-2">
<%= Icons.svg(:plus, class: "h-6") %>
<span>New section</span>
</div>
</div>
</div>
</div>
<div class="flex-grow px-6 py-8 flex overflow-y-auto">
<div class="max-w-screen-lg w-full mx-auto">
<%= for section <- @data.notebook.sections do %>
<%= live_component @socket, LiveBookWeb.Section,
id: "section-#{section.id}",
section: section,
selected: section.id == @selected_section_id,
focused_cell_id: @focused_cell_id %>
<% end %>
</div>
</div>
</div>
"""
end
@impl true
def handle_event("add_section", _params, socket) do
end_index = length(socket.assigns.data.notebook.sections)
Session.insert_section(socket.assigns.session_id, end_index)
{:noreply, socket}
end
def handle_event("delete_section", %{"section_id" => section_id}, socket) do
Session.delete_section(socket.assigns.session_id, section_id)
{:noreply, socket}
end
def handle_event("select_section", %{"section_id" => section_id}, socket) do
{:noreply, assign(socket, selected_section_id: section_id)}
end
def handle_event(
"insert_cell",
%{"section_id" => section_id, "index" => index, "type" => type},
socket
) do
index = String.to_integer(index) |> max(0)
type = String.to_atom(type)
Session.insert_cell(socket.assigns.session_id, section_id, index, type)
{:noreply, socket}
end
def handle_event("delete_cell", %{"cell_id" => cell_id}, socket) do
Session.delete_cell(socket.assigns.session_id, cell_id)
{:noreply, socket}
end
def handle_event("focus_cell", %{"cell_id" => cell_id}, socket) do
{:noreply, assign(socket, focused_cell_id: cell_id)}
end
@impl true
def handle_info({:operation, operation}, socket) do
case Session.Data.apply_operation(socket.assigns.data, operation) do

View file

@ -22,14 +22,12 @@ defmodule LiveBookWeb.SessionsLive do
</div>
<%= for session_id <- Enum.sort(@session_ids) do %>
<div class="p-3 flex">
<div class="flex-grow text-lg hover:opacity-70">
<div class="flex-grow text-lg text-gray-500 hover:text-current">
<%= live_redirect session_id, to: Routes.live_path(@socket, LiveBookWeb.SessionLive, session_id) %>
</div>
<div>
<button phx-click="delete_session" phx-value-id="<%= session_id %>" aria-label="delete">
<svg class="h-6 w-6 hover:opacity-70" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" />
</svg>
<button phx-click="delete_session" phx-value-id="<%= session_id %>" aria-label="delete" class="text-gray-500 hover:text-current">
<%= Icons.svg(:trash, class: "h-6") %>
</button>
</div>
</div>

View file

@ -1,24 +1,26 @@
<nav class="flex items-center justify-between px-8 py-3 bg-purple-600">
<div class="w-full">
<%= live_redirect "LiveBook", to: "/", class: "font-bold inline-block mr-4 py-2 text-white" %>
</div>
<ul class="flex flex-row">
<li class="flex items-center">
<%= live_redirect "Sessions", to: "/sessions", class: "px-3 text-xs uppercase font-bold text-white hover:opacity-75" %>
</li>
</ul>
</nav>
<%# <div class="flex flex-col min-h-screen"> %>
<!--
<nav class="flex items-center justify-between px-8 py-3 bg-purple-600">
<div class="w-full">
<%= live_redirect "LiveBook", to: "/", class: "font-bold inline-block mr-4 py-2 text-white" %>
</div>
<ul class="flex flex-row">
<li class="flex items-center">
<%= live_redirect "Sessions", to: "/sessions", class: "px-3 text-xs uppercase font-bold text-white hover:opacity-75" %>
</li>
</ul>
</nav> -->
<main role="main" class="flex-grow flex flex-col align-center h-screen">
<div>
<p class="alert alert-info" role="alert"
phx-click="lv:clear-flash"
phx-value-key="info"><%= live_flash(@flash, :info) %></p>
<main role="main" class="container mx-auto flex flex-col align-center">
<div>
<p class="alert alert-info" role="alert"
phx-click="lv:clear-flash"
phx-value-key="info"><%= live_flash(@flash, :info) %></p>
<p class="alert alert-danger" role="alert"
phx-click="lv:clear-flash"
phx-value-key="error"><%= live_flash(@flash, :error) %></p>
</div>
<p class="alert alert-danger" role="alert"
phx-click="lv:clear-flash"
phx-value-key="error"><%= live_flash(@flash, :error) %></p>
</div>
<%= @inner_content %>
</main>
<%= @inner_content %>
</main>
<%# </div> %>