diff --git a/assets/css/app.css b/assets/css/app.css index 20a301b09..50e984e73 100644 --- a/assets/css/app.css +++ b/assets/css/app.css @@ -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; +} diff --git a/lib/live_book_web.ex b/lib/live_book_web.ex index 23abffb4a..ed4f8603f 100644 --- a/lib/live_book_web.ex +++ b/lib/live_book_web.ex @@ -87,6 +87,9 @@ defmodule LiveBookWeb do import LiveBookWeb.ErrorHelpers alias LiveBookWeb.Router.Helpers, as: Routes + + # Custom helpers + alias LiveBookWeb.Icons end end diff --git a/lib/live_book_web/live/cell.ex b/lib/live_book_web/live/cell.ex new file mode 100644 index 000000000..5b6e0d6ae --- /dev/null +++ b/lib/live_book_web/live/cell.ex @@ -0,0 +1,52 @@ +defmodule LiveBookWeb.Cell do + use LiveBookWeb, :live_component + + def render(assigns) do + ~L""" +
+
+ + + <%= cell_type_icon(@cell.type) %> +
+
+
Editor placeholder
+
+ <%= for output <- @cell.outputs do %> +
+ <%= render_output(output) %> +
+ <% end %> +
+ """ + 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""" + + """ + end + + defp cell_type_icon(:markdown) do + ~e""" + + """ + end +end diff --git a/lib/live_book_web/live/icons.ex b/lib/live_book_web/live/icons.ex new file mode 100644 index 000000000..320b79869 --- /dev/null +++ b/lib/live_book_web/live/icons.ex @@ -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""" + + """ + |> heroicon_svg(attrs) + end + + def svg(:play, attrs) do + ~e""" + + + """ + |> heroicon_svg(attrs) + end + + def svg(:plus, attrs) do + ~e""" + + """ + |> heroicon_svg(attrs) + end + + def svg(:trash, attrs) do + ~e""" + + """ + |> 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 diff --git a/lib/live_book_web/live/insert_cell_actions.ex b/lib/live_book_web/live/insert_cell_actions.ex new file mode 100644 index 000000000..71b91d392 --- /dev/null +++ b/lib/live_book_web/live/insert_cell_actions.ex @@ -0,0 +1,34 @@ +defmodule LiveBookWeb.InsertCellActions do + use LiveBookWeb, :live_component + + def render(assigns) do + ~L""" +
+ <%= line() %> + + + <%= line() %> +
+ """ + end + + defp line() do + ~e""" +
+ """ + end +end diff --git a/lib/live_book_web/live/section.ex b/lib/live_book_web/live/section.ex new file mode 100644 index 000000000..337db6822 --- /dev/null +++ b/lib/live_book_web/live/section.ex @@ -0,0 +1,30 @@ +defmodule LiveBookWeb.Section do + use LiveBookWeb, :live_component + + def render(assigns) do + ~L""" +
"> +
+
+ <%= Icons.svg(:chevron_right, class: "h-8") %> +

<%= @section.name %>

+
+
+ +
+
+
+
+ <%= 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 %> +
+
+
+ """ + end +end diff --git a/lib/live_book_web/live/session_live.ex b/lib/live_book_web/live/session_live.ex index 99a4b2b70..8e351e1d7 100644 --- a/lib/live_book_web/live/session_live.ex +++ b/lib/live_book_web/live/session_live.ex @@ -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""" -
- Session <%= @session_id %> +
+
+

<%= @data.notebook.name %>

+
+ <%= for section <- @data.notebook.sections do %> +
+ + <%= section.name %> + +
+ <% end %> +
+
+ <%= Icons.svg(:plus, class: "h-6") %> + New section +
+
+
+
+
+
+ <%= 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 %> +
+
""" 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 diff --git a/lib/live_book_web/live/sessions_live.ex b/lib/live_book_web/live/sessions_live.ex index d535203e0..77e15eb55 100644 --- a/lib/live_book_web/live/sessions_live.ex +++ b/lib/live_book_web/live/sessions_live.ex @@ -22,14 +22,12 @@ defmodule LiveBookWeb.SessionsLive do
<%= for session_id <- Enum.sort(@session_ids) do %>
-
+
<%= live_redirect session_id, to: Routes.live_path(@socket, LiveBookWeb.SessionLive, session_id) %>
-
diff --git a/lib/live_book_web/templates/layout/live.html.leex b/lib/live_book_web/templates/layout/live.html.leex index 31d4dc7dc..33922bd71 100644 --- a/lib/live_book_web/templates/layout/live.html.leex +++ b/lib/live_book_web/templates/layout/live.html.leex @@ -1,24 +1,26 @@ - +<%#
%> + +
+
+ -
-
- + +
- -
- - <%= @inner_content %> -
+ <%= @inner_content %> + +<%#
%>