Initial redesign (#75)

* Add Remix icons

* Replace existing icons with Remix icons

* Update fonts

* Redesign homepage

* Redesign shortcuts modal

* Fix tests
This commit is contained in:
Jonatan Kłosko 2021-03-12 11:57:01 +01:00 committed by GitHub
parent 266bf35bd0
commit a2d1e2f934
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
30 changed files with 252 additions and 326 deletions

View file

@ -1,7 +1,7 @@
/* Buttons */ /* Buttons */
.button-base { .button-base {
@apply px-4 py-2 bg-white rounded-md border border-gray-300 font-medium text-gray-700; @apply px-5 py-2 bg-white rounded-lg border border-gray-200 font-medium text-sm text-gray-600;
} }
.button-base:not(:disabled) { .button-base:not(:disabled) {
@ -25,7 +25,7 @@
} }
.button-primary { .button-primary {
@apply border-0 bg-blue-400 text-white; @apply border-transparent bg-blue-600 text-white;
} }
.button-primary:not(:disabled) { .button-primary:not(:disabled) {
@ -35,11 +35,11 @@
/* Form fields */ /* Form fields */
.input-base { .input-base {
@apply w-full px-3 py-3 bg-white rounded-md placeholder-gray-400 text-gray-700; @apply w-full p-3 bg-white border border-gray-200 rounded-lg placeholder-gray-400 text-gray-600;
} }
.checkbox-base { .checkbox-base {
@apply h-5 w-5 appearance-none border border-gray-300 rounded text-blue-400 cursor-pointer; @apply h-5 w-5 appearance-none border border-gray-300 rounded text-blue-600 cursor-pointer;
} }
.checkbox-base:checked { .checkbox-base:checked {

View file

@ -3,3 +3,7 @@
button:focus { button:focus {
outline: none; outline: none;
} }
body {
font-family: "Inter";
}

View file

@ -99,14 +99,14 @@
} }
.markdown code { .markdown code {
@apply py-1 px-2 rounded-md text-sm align-middle; @apply py-1 px-2 rounded-lg text-sm align-middle font-mono;
/* Match the editor colors */ /* Match the editor colors */
background-color: #282c34; background-color: #282c34;
color: #abb2bf; color: #abb2bf;
} }
.markdown pre > code { .markdown pre > code {
@apply block p-4 rounded-md text-sm align-middle; @apply block p-4 rounded-lg text-sm align-middle font-mono;
/* Match the editor colors */ /* Match the editor colors */
background-color: #282c34; background-color: #282c34;
color: #abb2bf; color: #abb2bf;

View file

@ -9,6 +9,6 @@
} }
.font-editor { .font-editor {
font-family: "Droid Sans Mono", monospace, monospace, "Droid Sans Fallback"; font-family: "JetBrains Mono";
font-size: 14px; font-size: 14px;
} }

View file

@ -1,4 +1,10 @@
import "../css/app.css"; import "../css/app.css";
import 'remixicon/fonts/remixicon.css'
import "@fontsource/inter";
import "@fontsource/inter/500.css";
import "@fontsource/inter/600.css";
import "@fontsource/jetbrains-mono";
import "phoenix_html"; import "phoenix_html";
import { Socket } from "phoenix"; import { Socket } from "phoenix";

View file

@ -77,6 +77,7 @@ class LiveEditor {
occurrencesHighlight: false, occurrencesHighlight: false,
renderLineHighlight: "none", renderLineHighlight: "none",
theme: "custom", theme: "custom",
fontFamily: "JetBrains Mono"
}); });
this.editor.getModel().updateOptions({ this.editor.getModel().updateOptions({

View file

@ -6,6 +6,8 @@
"": { "": {
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@fontsource/inter": "^4.2.2",
"@fontsource/jetbrains-mono": "^4.2.2",
"dompurify": "^2.2.6", "dompurify": "^2.2.6",
"hyperlist": "^1.0.0", "hyperlist": "^1.0.0",
"marked": "^1.2.8", "marked": "^1.2.8",
@ -14,7 +16,8 @@
"nprogress": "^0.2.0", "nprogress": "^0.2.0",
"phoenix": "file:../deps/phoenix", "phoenix": "file:../deps/phoenix",
"phoenix_html": "file:../deps/phoenix_html", "phoenix_html": "file:../deps/phoenix_html",
"phoenix_live_view": "file:../deps/phoenix_live_view" "phoenix_live_view": "file:../deps/phoenix_live_view",
"remixicon": "^2.5.0"
}, },
"devDependencies": { "devDependencies": {
"@babel/core": "^7.0.0", "@babel/core": "^7.0.0",
@ -1095,6 +1098,16 @@
"node": ">=0.1.95" "node": ">=0.1.95"
} }
}, },
"node_modules/@fontsource/inter": {
"version": "4.2.2",
"resolved": "https://registry.npmjs.org/@fontsource/inter/-/inter-4.2.2.tgz",
"integrity": "sha512-lvR1PQe+8FTTd3YRW84KGcgUR8leZ7S3aY+51MQ90MQHI0VQe3cDH6T6jjs1qTm+wPmWfdSVjN8ugvNZpGUnvA=="
},
"node_modules/@fontsource/jetbrains-mono": {
"version": "4.2.2",
"resolved": "https://registry.npmjs.org/@fontsource/jetbrains-mono/-/jetbrains-mono-4.2.2.tgz",
"integrity": "sha512-aCwLIqfZZrZjy+cIx/9hSzxyOcz8YzMbd3VQ8QRkS2MB3+XsXAnMLEkELXLCfKYUkEysRECaq5s7+Qhi1hgZAA=="
},
"node_modules/@fullhuman/postcss-purgecss": { "node_modules/@fullhuman/postcss-purgecss": {
"version": "3.1.3", "version": "3.1.3",
"resolved": "https://registry.npmjs.org/@fullhuman/postcss-purgecss/-/postcss-purgecss-3.1.3.tgz", "resolved": "https://registry.npmjs.org/@fullhuman/postcss-purgecss/-/postcss-purgecss-3.1.3.tgz",
@ -13059,6 +13072,11 @@
"jsesc": "bin/jsesc" "jsesc": "bin/jsesc"
} }
}, },
"node_modules/remixicon": {
"version": "2.5.0",
"resolved": "https://registry.npmjs.org/remixicon/-/remixicon-2.5.0.tgz",
"integrity": "sha512-q54ra2QutYDZpuSnFjmeagmEiN9IMo56/zz5dDNitzKD23oFRw77cWo4TsrAdmdkPiEn8mxlrTqxnkujDbEGww=="
},
"node_modules/remove-trailing-separator": { "node_modules/remove-trailing-separator": {
"version": "1.1.0", "version": "1.1.0",
"resolved": "https://registry.npmjs.org/remove-trailing-separator/-/remove-trailing-separator-1.1.0.tgz", "resolved": "https://registry.npmjs.org/remove-trailing-separator/-/remove-trailing-separator-1.1.0.tgz",
@ -16690,6 +16708,16 @@
"minimist": "^1.2.0" "minimist": "^1.2.0"
} }
}, },
"@fontsource/inter": {
"version": "4.2.2",
"resolved": "https://registry.npmjs.org/@fontsource/inter/-/inter-4.2.2.tgz",
"integrity": "sha512-lvR1PQe+8FTTd3YRW84KGcgUR8leZ7S3aY+51MQ90MQHI0VQe3cDH6T6jjs1qTm+wPmWfdSVjN8ugvNZpGUnvA=="
},
"@fontsource/jetbrains-mono": {
"version": "4.2.2",
"resolved": "https://registry.npmjs.org/@fontsource/jetbrains-mono/-/jetbrains-mono-4.2.2.tgz",
"integrity": "sha512-aCwLIqfZZrZjy+cIx/9hSzxyOcz8YzMbd3VQ8QRkS2MB3+XsXAnMLEkELXLCfKYUkEysRECaq5s7+Qhi1hgZAA=="
},
"@fullhuman/postcss-purgecss": { "@fullhuman/postcss-purgecss": {
"version": "3.1.3", "version": "3.1.3",
"resolved": "https://registry.npmjs.org/@fullhuman/postcss-purgecss/-/postcss-purgecss-3.1.3.tgz", "resolved": "https://registry.npmjs.org/@fullhuman/postcss-purgecss/-/postcss-purgecss-3.1.3.tgz",
@ -26530,6 +26558,11 @@
} }
} }
}, },
"remixicon": {
"version": "2.5.0",
"resolved": "https://registry.npmjs.org/remixicon/-/remixicon-2.5.0.tgz",
"integrity": "sha512-q54ra2QutYDZpuSnFjmeagmEiN9IMo56/zz5dDNitzKD23oFRw77cWo4TsrAdmdkPiEn8mxlrTqxnkujDbEGww=="
},
"remove-trailing-separator": { "remove-trailing-separator": {
"version": "1.1.0", "version": "1.1.0",
"resolved": "https://registry.npmjs.org/remove-trailing-separator/-/remove-trailing-separator-1.1.0.tgz", "resolved": "https://registry.npmjs.org/remove-trailing-separator/-/remove-trailing-separator-1.1.0.tgz",

View file

@ -10,6 +10,8 @@
"test:watch": "jest --watch" "test:watch": "jest --watch"
}, },
"dependencies": { "dependencies": {
"@fontsource/inter": "^4.2.2",
"@fontsource/jetbrains-mono": "^4.2.2",
"dompurify": "^2.2.6", "dompurify": "^2.2.6",
"hyperlist": "^1.0.0", "hyperlist": "^1.0.0",
"marked": "^1.2.8", "marked": "^1.2.8",
@ -18,7 +20,8 @@
"nprogress": "^0.2.0", "nprogress": "^0.2.0",
"phoenix": "file:../deps/phoenix", "phoenix": "file:../deps/phoenix",
"phoenix_html": "file:../deps/phoenix_html", "phoenix_html": "file:../deps/phoenix_html",
"phoenix_live_view": "file:../deps/phoenix_live_view" "phoenix_live_view": "file:../deps/phoenix_live_view",
"remixicon": "^2.5.0"
}, },
"devDependencies": { "devDependencies": {
"@babel/core": "^7.0.0", "@babel/core": "^7.0.0",

View file

@ -7,7 +7,38 @@ module.exports = {
], ],
darkMode: false, darkMode: false,
theme: { theme: {
extend: {}, fontFamily: {
'sans': ['Inter'],
'mono': ['JetBrains Mono'],
},
extend: {
colors: {
blue: {
50: '#F5F7FF',
100: '#ECF0FF',
200: '#D8E0FF',
300: '#B2C1FF',
400: '#8BA2FF',
500: '#6583FF',
600: '#3E64FF',
700: '#2D4CDB',
800: '#1F37B7',
900: '#132593',
},
gray: {
50: '#F8FAFC',
100: '#F0F5F9',
200: '#E1E8F0',
300: '#CAD5E0',
400: '#91A4B7',
500: '#61758A',
600: '#445668',
700: '#304254',
800: '#1C2A3A',
900: '#0D1829',
},
},
},
}, },
variants: { variants: {
extend: {}, extend: {},

View file

@ -44,7 +44,7 @@ module.exports = (env, options) => {
], ],
}, },
{ {
test: /\.ttf$/, test: /\.(ttf|woff|woff2|eot|svg)$/,
use: ['file-loader'], use: ['file-loader'],
}, },
] ]

View file

@ -66,7 +66,6 @@ defmodule LivebookWeb do
# Custom helpers # Custom helpers
import LivebookWeb.Helpers import LivebookWeb.Helpers
alias LivebookWeb.Icons
end end
end end

View file

@ -1,5 +1,6 @@
defmodule LivebookWeb.Helpers do defmodule LivebookWeb.Helpers do
import Phoenix.LiveView.Helpers import Phoenix.LiveView.Helpers
import Phoenix.HTML.Tag
@doc """ @doc """
Renders a component inside the `Livebook.ModalComponent` component. Renders a component inside the `Livebook.ModalComponent` component.
@ -31,4 +32,13 @@ defmodule LivebookWeb.Helpers do
defp windows?(user_agent), do: String.match?(user_agent, ~r/Windows/) defp windows?(user_agent), do: String.match?(user_agent, ~r/Windows/)
defdelegate ansi_string_to_html(string), to: LivebookWeb.ANSI defdelegate ansi_string_to_html(string), to: LivebookWeb.ANSI
@doc """
Returns [Remix](https://remixicon.com) icon tag.
"""
def remix_icon(name, attrs \\ []) do
icon_class = "ri-#{name}"
attrs = Keyword.update(attrs, :class, icon_class, fn class -> "#{icon_class} #{class}" end)
content_tag(:i, "", attrs)
end
end end

View file

@ -17,25 +17,25 @@ defmodule LivebookWeb.CellComponent do
def render_cell_content(%{cell: %{type: :markdown}} = assigns) do def render_cell_content(%{cell: %{type: :markdown}} = assigns) do
~L""" ~L"""
<div class="flex flex-col items-center space-y-2 absolute z-50 right-0 top-0 -mr-10" data-element="actions"> <div class="flex flex-col items-center space-y-2 absolute z-50 right-0 top-0 -mr-10" data-element="actions">
<button class="text-gray-500 hover:text-current" data-element="enable-insert-mode-button"> <button class="text-gray-400 hover:text-current" data-element="enable-insert-mode-button">
<%= Icons.svg(:pencil, class: "h-6") %> <%= remix_icon("pencil-line", class: "text-2xl") %>
</button> </button>
<button class="text-gray-500 hover:text-current" <button class="text-gray-400 hover:text-current"
phx-click="delete_cell" phx-click="delete_cell"
phx-value-cell_id="<%= @cell.id %>"> phx-value-cell_id="<%= @cell.id %>">
<%= Icons.svg(:trash, class: "h-6") %> <%= remix_icon("delete-bin-line", class: "text-2xl") %>
</button> </button>
<button class="text-gray-500 hover:text-current" <button class="text-gray-400 hover:text-current"
phx-click="move_cell" phx-click="move_cell"
phx-value-cell_id="<%= @cell.id %>" phx-value-cell_id="<%= @cell.id %>"
phx-value-offset="-1"> phx-value-offset="-1">
<%= Icons.svg(:chevron_up, class: "h-6") %> <%= remix_icon("arrow-up-s-line", class: "text-2xl") %>
</button> </button>
<button class="text-gray-500 hover:text-current" <button class="text-gray-400 hover:text-current"
phx-click="move_cell" phx-click="move_cell"
phx-value-cell_id="<%= @cell.id %>" phx-value-cell_id="<%= @cell.id %>"
phx-value-offset="1"> phx-value-offset="1">
<%= Icons.svg(:chevron_down, class: "h-6") %> <%= remix_icon("arrow-down-s-line", class: "text-2xl") %>
</button> </button>
</div> </div>
@ -53,37 +53,37 @@ defmodule LivebookWeb.CellComponent do
~L""" ~L"""
<div class="flex flex-col items-center space-y-2 absolute z-50 right-0 top-0 -mr-10" data-element="actions"> <div class="flex flex-col items-center space-y-2 absolute z-50 right-0 top-0 -mr-10" data-element="actions">
<%= if @cell_info.evaluation_status == :ready do %> <%= if @cell_info.evaluation_status == :ready do %>
<button class="text-gray-500 hover:text-current" <button class="text-gray-400 hover:text-current"
phx-click="queue_cell_evaluation" phx-click="queue_cell_evaluation"
phx-value-cell_id="<%= @cell.id %>"> phx-value-cell_id="<%= @cell.id %>">
<%= Icons.svg(:play, class: "h-6") %> <%= remix_icon("play-circle-line", class: "text-2xl") %>
</button> </button>
<% else %> <% else %>
<button class="text-gray-500 hover:text-current" <button class="text-gray-400 hover:text-current"
phx-click="cancel_cell_evaluation" phx-click="cancel_cell_evaluation"
phx-value-cell_id="<%= @cell.id %>"> phx-value-cell_id="<%= @cell.id %>">
<%= Icons.svg(:stop, class: "h-6") %> <%= remix_icon("stop-circle-line", class: "text-2xl") %>
</button> </button>
<% end %> <% end %>
<button class="text-gray-500 hover:text-current" <button class="text-gray-400 hover:text-current"
phx-click="delete_cell" phx-click="delete_cell"
phx-value-cell_id="<%= @cell.id %>"> phx-value-cell_id="<%= @cell.id %>">
<%= Icons.svg(:trash, class: "h-6") %> <%= remix_icon("delete-bin-line", class: "text-2xl") %>
</button> </button>
<%= live_patch to: Routes.session_path(@socket, :cell_settings, @session_id, @cell.id), class: "text-gray-500 hover:text-current" do %> <%= live_patch to: Routes.session_path(@socket, :cell_settings, @session_id, @cell.id), class: "text-gray-400 hover:text-current" do %>
<%= Icons.svg(:adjustments, class: "h-6") %> <%= remix_icon("list-settings-line", class: "text-2xl") %>
<% end %> <% end %>
<button class="text-gray-500 hover:text-current" <button class="text-gray-400 hover:text-current"
phx-click="move_cell" phx-click="move_cell"
phx-value-cell_id="<%= @cell.id %>" phx-value-cell_id="<%= @cell.id %>"
phx-value-offset="-1"> phx-value-offset="-1">
<%= Icons.svg(:chevron_up, class: "h-6") %> <%= remix_icon("arrow-up-s-line", class: "text-2xl") %>
</button> </button>
<button class="text-gray-500 hover:text-current" <button class="text-gray-400 hover:text-current"
phx-click="move_cell" phx-click="move_cell"
phx-value-cell_id="<%= @cell.id %>" phx-value-cell_id="<%= @cell.id %>"
phx-value-offset="1"> phx-value-offset="1">
<%= Icons.svg(:chevron_down, class: "h-6") %> <%= remix_icon("arrow-down-s-line", class: "text-2xl") %>
</button> </button>
</div> </div>
@ -102,7 +102,7 @@ defmodule LivebookWeb.CellComponent do
assigns = %{cell: cell, cell_info: cell_info, show_status: show_status} assigns = %{cell: cell, cell_info: cell_info, show_status: show_status}
~L""" ~L"""
<div class="py-3 rounded-md overflow-hidden bg-editor relative"> <div class="py-3 rounded-lg overflow-hidden bg-editor relative">
<div <div
id="editor-container-<%= @cell.id %>" id="editor-container-<%= @cell.id %>"
data-element="editor-container" data-element="editor-container"
@ -137,9 +137,9 @@ defmodule LivebookWeb.CellComponent do
~L""" ~L"""
<div class="max-w-2xl w-full animate-pulse"> <div class="max-w-2xl w-full animate-pulse">
<div class="flex-1 space-y-4"> <div class="flex-1 space-y-4">
<div class="h-4 bg-gray-200 rounded-md w-3/4"></div> <div class="h-4 bg-gray-200 rounded-lg w-3/4"></div>
<div class="h-4 bg-gray-200 rounded-md"></div> <div class="h-4 bg-gray-200 rounded-lg"></div>
<div class="h-4 bg-gray-200 rounded-md w-5/6"></div> <div class="h-4 bg-gray-200 rounded-lg w-5/6"></div>
</div> </div>
</div> </div>
""" """
@ -159,9 +159,9 @@ defmodule LivebookWeb.CellComponent do
~L""" ~L"""
<div class="px-8 max-w-2xl w-full animate-pulse"> <div class="px-8 max-w-2xl w-full animate-pulse">
<div class="flex-1 space-y-4 py-1"> <div class="flex-1 space-y-4 py-1">
<div class="h-4 bg-gray-500 rounded-md w-3/4"></div> <div class="h-4 bg-gray-500 rounded-lg w-3/4"></div>
<div class="h-4 bg-gray-500 rounded-md"></div> <div class="h-4 bg-gray-500 rounded-lg"></div>
<div class="h-4 bg-gray-500 rounded-md w-5/6"></div> <div class="h-4 bg-gray-500 rounded-lg w-5/6"></div>
</div> </div>
</div> </div>
""" """
@ -171,7 +171,7 @@ defmodule LivebookWeb.CellComponent do
assigns = %{outputs: outputs, cell_id: cell_id} assigns = %{outputs: outputs, cell_id: cell_id}
~L""" ~L"""
<div class="flex flex-col rounded-md border border-gray-200 divide-y divide-gray-200 font-editor"> <div class="flex flex-col rounded-lg border border-gray-200 divide-y divide-gray-200 font-editor">
<%= for {output, index} <- @outputs |> Enum.reverse() |> Enum.with_index() do %> <%= for {output, index} <- @outputs |> Enum.reverse() |> Enum.with_index() do %>
<div class="p-4"> <div class="p-4">
<div class=""> <div class="">
@ -230,7 +230,7 @@ defmodule LivebookWeb.CellComponent do
<div class="text-xs text-gray-400">Evaluating</div> <div class="text-xs text-gray-400">Evaluating</div>
<span class="flex relative h-3 w-3"> <span class="flex relative h-3 w-3">
<span class="animate-ping absolute inline-flex h-3 w-3 rounded-full bg-blue-300 opacity-75"></span> <span class="animate-ping absolute inline-flex h-3 w-3 rounded-full bg-blue-300 opacity-75"></span>
<span class="relative inline-flex rounded-full h-3 w-3 bg-blue-400"></span> <span class="relative inline-flex rounded-full h-3 w-3 bg-blue-600"></span>
</span> </span>
</div> </div>
""" """

View file

@ -17,17 +17,14 @@ defmodule LivebookWeb.HomeLive do
@impl true @impl true
def render(assigns) do def render(assigns) do
~L""" ~L"""
<header class="flex justify-center p-4 border-b"> <div class="container max-w-4xl w-full mx-auto p-4 pb-8 flex flex-col items-center space-y-4">
<h1 class="text-2xl font-medium">Livebook</h1>
</header>
<div class="container max-w-5xl w-full mx-auto p-4 pb-8 flex flex-col items-center space-y-4">
<div class="w-full flex justify-end"> <div class="w-full flex justify-end">
<button class="button-base button-sm" <button class="button-base button-primary"
phx-click="new"> phx-click="new">
New notebook New Notebook
</button> </button>
</div> </div>
<div class="container flex flex-col space-y-4"> <div class="w-full flex flex-col space-y-4">
<%= live_component @socket, LivebookWeb.PathSelectComponent, <%= live_component @socket, LivebookWeb.PathSelectComponent,
id: "path_select", id: "path_select",
path: @path, path: @path,
@ -35,27 +32,30 @@ defmodule LivebookWeb.HomeLive do
running_paths: paths(@session_summaries), running_paths: paths(@session_summaries),
target: nil %> target: nil %>
<div class="flex justify-end space-x-2"> <div class="flex justify-end space-x-2">
<%= content_tag :button, "Fork", <%= content_tag :button,
class: "button-base button-sm", class: "button-base",
phx_click: "fork", phx_click: "fork",
disabled: not path_forkable?(@path) %> disabled: not path_forkable?(@path) do %>
<%= remix_icon("git-branch-line", class: "align-middle mr-1") %>
<span>Fork</span>
<% end %>
<%= if path_running?(@path, @session_summaries) do %> <%= if path_running?(@path, @session_summaries) do %>
<%= live_patch "Join session", to: Routes.session_path(@socket, :page, session_id_by_path(@path, @session_summaries)), <%= live_patch "Join session", to: Routes.session_path(@socket, :page, session_id_by_path(@path, @session_summaries)),
class: "button-base button-sm button-primary" %> class: "button-base button-primary" %>
<% else %> <% else %>
<%= content_tag :button, "Open", <%= content_tag :button, "Open",
class: "button-base button-sm button-primary", class: "button-base button-primary",
phx_click: "open", phx_click: "open",
disabled: not path_openable?(@path, @session_summaries) %> disabled: not path_openable?(@path, @session_summaries) %>
<% end %> <% end %>
</div> </div>
</div> </div>
<div class="w-full pt-24"> <div class="w-full pt-24">
<h3 class="text-xl font-medium text-gray-900"> <h3 class="text-xl font-semibold text-gray-800 mb-5">
Running sessions Running Sessions
</h3> </h3>
<%= if @session_summaries == [] do %> <%= if @session_summaries == [] do %>
<div class="mt-3 text-gray-500 text-medium"> <div class="text-gray-500 text-medium">
No sessions currently running, you can create one above. No sessions currently running, you can create one above.
</div> </div>
<% else %> <% else %>

View file

@ -1,193 +0,0 @@
defmodule LivebookWeb.Icons do
import Phoenix.HTML.Tag
import Phoenix.LiveView.Helpers
@doc """
Returns icon svg tag.
"""
def svg(name, attrs \\ [])
def svg(:play, attrs) do
assigns = %{attrs: heroicon_svg_attrs(attrs)}
~L"""
<%= tag(:svg, @attrs) %>
<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" />
</svg>
"""
end
def svg(:plus, attrs) do
assigns = %{attrs: heroicon_svg_attrs(attrs)}
~L"""
<%= tag(:svg, @attrs) %>
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 6v6m0 0v6m0-6h6m-6 0H6" />
</svg>
"""
end
def svg(:trash, attrs) do
assigns = %{attrs: heroicon_svg_attrs(attrs)}
~L"""
<%= tag(:svg, @attrs) %>
<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" />
</svg>
"""
end
def svg(:chip, attrs) do
assigns = %{attrs: heroicon_svg_attrs(attrs)}
~L"""
<%= tag(:svg, @attrs) %>
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 3v2m6-2v2M9 19v2m6-2v2M5 9H3m2 6H3m18-6h-2m2 6h-2M7 19h10a2 2 0 002-2V7a2 2 0 00-2-2H7a2 2 0 00-2 2v10a2 2 0 002 2zM9 9h6v6H9V9z" />
</svg>
"""
end
def svg(:information_circle, attrs) do
assigns = %{attrs: heroicon_svg_attrs(attrs)}
~L"""
<%= tag(:svg, @attrs) %>
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
"""
end
def svg(:exclamation_circle, attrs) do
assigns = %{attrs: heroicon_svg_attrs(attrs)}
~L"""
<%= tag(:svg, @attrs) %>
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
"""
end
def svg(:question_mark_circle, attrs) do
assigns = %{attrs: heroicon_svg_attrs(attrs)}
~L"""
<%= tag(:svg, @attrs) %>
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8.228 9c.549-1.165 2.03-2 3.772-2 2.21 0 4 1.343 4 3 0 1.4-1.278 2.575-3.006 2.907-.542.104-.994.54-.994 1.093m0 3h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
"""
end
def svg(:pencil, attrs) do
assigns = %{attrs: heroicon_svg_attrs(attrs)}
~L"""
<%= tag(:svg, @attrs) %>
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15.232 5.232l3.536 3.536m-2.036-5.036a2.5 2.5 0 113.536 3.536L6.5 21.036H3v-3.572L16.732 3.732z" />
</svg>
"""
end
def svg(:folder, attrs) do
assigns = %{attrs: heroicon_svg_attrs(attrs)}
~L"""
<%= tag(:svg, @attrs) %>
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 7v10a2 2 0 002 2h14a2 2 0 002-2V9a2 2 0 00-2-2h-6l-2-2H5a2 2 0 00-2 2z" />
</svg>
"""
end
def svg(:document_text, attrs) do
assigns = %{attrs: heroicon_svg_attrs(attrs)}
~L"""
<%= tag(:svg, @attrs) %>
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
</svg>
"""
end
def svg(:check_circle, attrs) do
assigns = %{attrs: heroicon_svg_attrs(attrs)}
~L"""
<%= tag(:svg, @attrs) %>
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
"""
end
def svg(:dots_circle_horizontal, attrs) do
assigns = %{attrs: heroicon_svg_attrs(attrs)}
~L"""
<%= tag(:svg, @attrs) %>
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 12h.01M12 12h.01M16 12h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
"""
end
def svg(:home, attrs) do
assigns = %{attrs: heroicon_svg_attrs(attrs)}
~L"""
<%= tag(:svg, @attrs) %>
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 12l2-2m0 0l7-7 7 7M5 10v10a1 1 0 001 1h3m10-11l2 2m-2-2v10a1 1 0 01-1 1h-3m-6 0a1 1 0 001-1v-4a1 1 0 011-1h2a1 1 0 011 1v4a1 1 0 001 1m-6 0h6" />
</svg>
"""
end
def svg(:stop, attrs) do
assigns = %{attrs: heroicon_svg_attrs(attrs)}
~L"""
<%= tag(:svg, @attrs) %>
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 10a1 1 0 011-1h4a1 1 0 011 1v4a1 1 0 01-1 1h-4a1 1 0 01-1-1v-4z" />
</svg>
"""
end
def svg(:chevron_up, attrs) do
assigns = %{attrs: heroicon_svg_attrs(attrs)}
~L"""
<%= tag(:svg, @attrs) %>
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 15l7-7 7 7" />
</svg>
"""
end
def svg(:chevron_down, attrs) do
assigns = %{attrs: heroicon_svg_attrs(attrs)}
~L"""
<%= tag(:svg, @attrs) %>
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7" />
</svg>
"""
end
def svg(:adjustments, attrs) do
assigns = %{attrs: heroicon_svg_attrs(attrs)}
~L"""
<%= tag(:svg, @attrs) %>
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 6V4m0 2a2 2 0 100 4m0-4a2 2 0 110 4m-6 8a2 2 0 100-4m0 4a2 2 0 110-4m0 4v2m0-6V4m6 6v10m6-2a2 2 0 100-4m0 4a2 2 0 110-4m0 4v2m0-6V4" />
</svg>
"""
end
# https://heroicons.com
defp heroicon_svg_attrs(attrs) do
heroicon_svg_attrs = [
xmlns: "http://www.w3.org/2000/svg",
fill: "none",
viewBox: "0 0 24 24",
stroke: "currentColor"
]
Keyword.merge(attrs, heroicon_svg_attrs)
end
end

View file

@ -5,14 +5,14 @@ defmodule LivebookWeb.InsertCellComponent do
~L""" ~L"""
<div class="<%= if(@persistent, do: "opacity-100", else: "opacity-0") %> hover:opacity-100 flex space-x-2 justify-center items-center"> <div class="<%= if(@persistent, do: "opacity-100", else: "opacity-0") %> hover:opacity-100 flex space-x-2 justify-center items-center">
<%= line() %> <%= line() %>
<button class="py-1 px-2 rounded-md text-sm hover:bg-gray-50 border border-gray-200" <button class="py-1 px-2 rounded-lg text-sm hover:bg-gray-50 border border-gray-200"
phx-click="insert_cell" phx-click="insert_cell"
phx-value-type="markdown" phx-value-type="markdown"
phx-value-section_id="<%= @section_id %>" phx-value-section_id="<%= @section_id %>"
phx-value-index="<%= @index %>"> phx-value-index="<%= @index %>">
+ Markdown + Markdown
</button> </button>
<button class="py-1 px-2 rounded-md text-sm hover:bg-gray-50 border border-gray-200" <button class="py-1 px-2 rounded-lg text-sm hover:bg-gray-50 border border-gray-200"
phx-click="insert_cell" phx-click="insert_cell"
phx-value-type="elixir" phx-value-type="elixir"
phx-value-section_id="<%= @section_id %>" phx-value-section_id="<%= @section_id %>"

View file

@ -19,10 +19,15 @@ defmodule LivebookWeb.ModalComponent do
phx-page-loading></div> phx-page-loading></div>
<!-- Modal box --> <!-- Modal box -->
<div class="relative max-h-full overflow-y-auto bg-white rounded-md shadow-xl" <div class="relative max-h-full overflow-y-auto bg-white rounded-lg shadow-xl"
role="dialog" role="dialog"
aria-modal="true"> aria-modal="true">
<%= live_patch to: @return_to, class: "absolute top-6 right-6 text-gray-400 flex space-x-1 items-center" do %>
<span class="text-sm">(esc)</span>
<%= remix_icon("close-line", class: "text-2xl") %>
<% end %>
<%= live_component @socket, @component, @opts %> <%= live_component @socket, @component, @opts %>
</div> </div>
</div> </div>

View file

@ -14,7 +14,7 @@ defmodule LivebookWeb.PathSelectComponent do
def render(assigns) do def render(assigns) do
~L""" ~L"""
<form phx-change="set_path" phx-submit="set_path" <%= if @target, do: "phx-target=#{@target}" %>> <form phx-change="set_path" phx-submit="set_path" <%= if @target, do: "phx-target=#{@target}" %>>
<input class="input-base shadow" <input class="input-base"
id="input-path" id="input-path"
phx-hook="FocusOnUpdate" phx-hook="FocusOnUpdate"
type="text" type="text"
@ -37,22 +37,22 @@ defmodule LivebookWeb.PathSelectComponent do
defp render_file(file, target) do defp render_file(file, target) do
icon = icon =
case file do case file do
%{is_running: true} -> :play %{is_running: true} -> "play-circle-line"
%{is_dir: true} -> :folder %{is_dir: true} -> "folder-fill"
_ -> :document_text _ -> "file-code-line"
end end
assigns = %{file: file, icon: icon} assigns = %{file: file, icon: icon}
~L""" ~L"""
<button class="flex space-x-2 items-center p-2 rounded-md hover:bg-gray-100 focus:ring-1 focus:ring-blue-400 <%= if(@file.is_running, do: "text-green-400 opacity-75", else: "text-gray-700") %>" <button class="flex space-x-2 items-center p-2 rounded-lg hover:bg-gray-100 focus:ring-1 focus:ring-gray-400"
phx-click="set_path" phx-click="set_path"
phx-value-path="<%= file.path %>" phx-value-path="<%= file.path %>"
<%= if target, do: "phx-target=#{target}" %>> <%= if target, do: "phx-target=#{target}" %>>
<span class="block"> <span class="block">
<%= Icons.svg(@icon, class: "h-5") %> <%= remix_icon(@icon, class: "text-xl align-middle #{if(@file.is_running, do: "text-green-300", else: "text-gray-400")}") %>
</span> </span>
<span class="block overflow-hidden overflow-ellipsis whitespace-nowrap"> <span class="block font-medium overflow-hidden overflow-ellipsis whitespace-nowrap <%= if(@file.is_running, do: "text-green-300", else: "text-gray-500") %>">
<%= file.name %> <%= file.name %>
</span> </span>
</button> </button>

View file

@ -20,7 +20,7 @@ defmodule LivebookWeb.SectionComponent do
</div> </div>
<div class="flex space-x-2 items-center"> <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" tabindex="-1"> <button phx-click="delete_section" phx-value-section_id="<%= @section.id %>" class="text-gray-600 hover:text-current" tabindex="-1">
<%= Icons.svg(:trash, class: "h-6") %> <%= remix_icon("delete-bin-line", class: "text-2xl") %>
</button> </button>
</div> </div>
</div> </div>

View file

@ -91,14 +91,14 @@ defmodule LivebookWeb.SessionLive do
<% end %> <% end %>
<button phx-click="add_section" class="py-2 px-4 rounded-l-md cursor-pointer text-gray-300 hover:text-gray-400"> <button 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"> <div class="flex items-center space-x-2">
<%= Icons.svg(:plus, class: "h-6") %> <%= remix_icon("add-line", class: "text-2xl") %>
<span>New section</span> <span>New section</span>
</div> </div>
</button> </button>
</div> </div>
<%= live_patch to: Routes.session_path(@socket, :runtime, @session_id) do %> <%= live_patch to: Routes.session_path(@socket, :runtime, @session_id) do %>
<div class="text-sm text-gray-500 text-medium px-4 py-2 border-b border-gray-200 flex space-x-2 items-center hover:bg-gray-200"> <div class="text-sm text-gray-500 text-medium px-4 py-2 border-b border-gray-200 flex space-x-2 items-center hover:bg-gray-200">
<%= Icons.svg(:chip, class: "h-5 text-gray-400") %> <%= remix_icon("cpu-line", class: "text-xl text-gray-400") %>
<span><%= runtime_description(@data.runtime) %></span> <span><%= runtime_description(@data.runtime) %></span>
</div> </div>
<% end %> <% end %>
@ -106,15 +106,15 @@ defmodule LivebookWeb.SessionLive do
<div class="text-sm text-gray-500 text-medium px-4 py-2 border-b border-gray-200 flex space-x-2 items-center hover:bg-gray-200"> <div class="text-sm text-gray-500 text-medium px-4 py-2 border-b border-gray-200 flex space-x-2 items-center hover:bg-gray-200">
<%= if @data.path do %> <%= if @data.path do %>
<%= if @data.dirty do %> <%= if @data.dirty do %>
<%= Icons.svg(:dots_circle_horizontal, class: "h-5 text-blue-400") %> <%= remix_icon("refresh-line", class: "text-xl text-blue-600") %>
<% else %> <% else %>
<%= Icons.svg(:check_circle, class: "h-5 text-green-400") %> <%= remix_icon("checkbox-circle-line", class: "text-xl text-green-400") %>
<% end %> <% end %>
<span> <span>
<%= Path.basename(@data.path) %> <%= Path.basename(@data.path) %>
</span> </span>
<% else %> <% else %>
<%= Icons.svg(:document_text, class: "h-5 text-gray-400") %> <%= remix_icon("file-code-line", class: "text-xl text-gray-400") %>
<span> <span>
No file choosen No file choosen
</span> </span>
@ -123,10 +123,10 @@ defmodule LivebookWeb.SessionLive do
<% end %> <% end %>
<div class="p-4 flex space-x-2"> <div class="p-4 flex space-x-2">
<%= live_patch to: Routes.home_path(@socket, :page) do %> <%= live_patch to: Routes.home_path(@socket, :page) do %>
<%= Icons.svg(:home, class: "h-6 w-6 text-gray-600 hover:text-current") %> <%= remix_icon("home-2-line", class: "text-2xl text-gray-600 hover:text-current") %>
<% end %> <% end %>
<%= live_patch to: Routes.session_path(@socket, :shortcuts, @session_id) do %> <%= live_patch to: Routes.session_path(@socket, :shortcuts, @session_id) do %>
<%= Icons.svg(:question_mark_circle, class: "h-6 w-6 text-gray-600 hover:text-current") %> <%= remix_icon("question-line", class: "text-2xl text-gray-600 hover:text-current") %>
<% end %> <% end %>
</div> </div>
</div> </div>

View file

@ -13,7 +13,7 @@ defmodule LivebookWeb.SessionLive.AttachedLive do
~L""" ~L"""
<div class="flex-col space-y-3"> <div class="flex-col space-y-3">
<%= if @error_message do %> <%= if @error_message do %>
<div class="mb-3 rounded-md px-4 py-2 bg-red-100 text-red-400 font-medium"> <div class="mb-3 rounded-lg px-4 py-2 bg-red-100 text-red-400 font-medium">
<%= @error_message %> <%= @error_message %>
</div> </div>
<% end %> <% end %>
@ -35,10 +35,10 @@ defmodule LivebookWeb.SessionLive.AttachedLive do
Then enter the name of the node below: Then enter the name of the node below:
</p> </p>
<%= f = form_for :node, "#", phx_submit: "init" %> <%= f = form_for :node, "#", phx_submit: "init" %>
<%= text_input f, :name, class: "input-base shadow", <%= text_input f, :name, class: "input-base",
placeholder: if(Livebook.Config.shortnames?, do: "test", else: "test@127.0.0.1") %> placeholder: if(Livebook.Config.shortnames?, do: "test", else: "test@127.0.0.1") %>
<%= submit "Connect", class: "mt-3 button-base button-sm" %> <%= submit "Connect", class: "mt-3 button-base" %>
</form> </form>
</div> </div>
""" """

View file

@ -28,8 +28,8 @@ defmodule LivebookWeb.SessionLive.CellSettingsComponent do
</label> </label>
</div> </div>
<div class="mt-6 flex justify-end space-x-2"> <div class="mt-6 flex justify-end space-x-2">
<%= live_patch "Cancel", to: @return_to, class: "button-base button-sm" %> <%= live_patch "Cancel", to: @return_to, class: "button-base" %>
<button class="button-base button-primary button-sm" type="submit"> <button class="button-base button-primary" type="submit">
Save Save
</button> </button>
</div> </div>

View file

@ -17,7 +17,7 @@ defmodule LivebookWeb.SessionLive.ElixirStandaloneLive do
This is the default runtime and is started automatically This is the default runtime and is started automatically
as soon as you evaluate the first cell. as soon as you evaluate the first cell.
</p> </p>
<button class="button-base button-sm" phx-click="init"> <button class="button-base" phx-click="init">
Connect Connect
</button> </button>
<%= if @output do %> <%= if @output do %>

View file

@ -33,7 +33,7 @@ defmodule LivebookWeb.SessionLive.MixStandaloneLive do
extnames: [], extnames: [],
running_paths: [], running_paths: [],
target: nil %> target: nil %>
<%= content_tag :button, "Connect", class: "button-base button-sm", phx_click: "init", disabled: not mix_project_root?(@path) %> <%= content_tag :button, "Connect", class: "button-base", phx_click: "init", disabled: not mix_project_root?(@path) %>
<% end %> <% end %>
<%= if @status != :initial do %> <%= if @status != :initial do %>
<div class="markdown"> <div class="markdown">

View file

@ -50,7 +50,7 @@ defmodule LivebookWeb.SessionLive.PersistenceComponent do
</div> </div>
<div class="flex justify-end"> <div class="flex justify-end">
<%= content_tag :button, "Done", <%= content_tag :button, "Done",
class: "button-base button-primary button-sm", class: "button-base button-primary",
phx_click: "done", phx_click: "done",
phx_target: @myself, phx_target: @myself,
disabled: not path_savable?(normalize_path(@path), @session_summaries) %> disabled: not path_savable?(normalize_path(@path), @session_summaries) %>

View file

@ -20,7 +20,7 @@ defmodule LivebookWeb.SessionLive.RuntimeComponent do
The code is evaluated in a separate Elixir runtime (node), The code is evaluated in a separate Elixir runtime (node),
which you can configure yourself here. which you can configure yourself here.
</p> </p>
<div class="shadow rounded-md p-2"> <div class="border border-gray-200 rounded-lg p-2">
<%= if @runtime do %> <%= if @runtime do %>
<table class="w-full text-center text-sm"> <table class="w-full text-center text-sm">
<thead> <thead>
@ -35,7 +35,7 @@ defmodule LivebookWeb.SessionLive.RuntimeComponent do
<td><%= runtime_type_label(@runtime) %></td> <td><%= runtime_type_label(@runtime) %></td>
<td><%= @runtime.node %></td> <td><%= @runtime.node %></td>
<td> <td>
<button class="button-base text-sm button-sm button-danger" <button class="button-base text-sm button-danger"
type="button" type="button"
phx-click="disconnect" phx-click="disconnect"
phx-target="<%= @myself %>"> phx-target="<%= @myself %>">

View file

@ -3,26 +3,26 @@ defmodule LivebookWeb.SessionLive.ShortcutsComponent do
@shortcuts %{ @shortcuts %{
insert_mode: [ insert_mode: [
%{seq: "esc", desc: "Switch back to navigation mode"}, %{seq: ["esc"], desc: "Switch back to navigation mode"},
%{seq: "ctrl + enter", desc: "Evaluate cell and stay in insert mode"} %{seq: ["ctrl", ""], desc: "Evaluate cell and stay in insert mode"}
], ],
navigation_mode: [ navigation_mode: [
%{seq: "?", desc: "Open this help modal"}, %{seq: ["?"], desc: "Open this help modal"},
%{seq: "j", desc: "Focus next cell"}, %{seq: ["j"], desc: "Focus next cell"},
%{seq: "k", desc: "Focus previous cell"}, %{seq: ["k"], desc: "Focus previous cell"},
%{seq: "J", desc: "Move cell down"}, %{seq: ["J"], desc: "Move cell down"},
%{seq: "K", desc: "Move cell up"}, %{seq: ["K"], desc: "Move cell up"},
%{seq: "i", desc: "Switch to insert mode"}, %{seq: ["i"], desc: "Switch to insert mode"},
%{seq: "n", desc: "Insert Elixir cell below"}, %{seq: ["n"], desc: "Insert Elixir cell below"},
%{seq: "m", desc: "Insert Markdown cell below"}, %{seq: ["m"], desc: "Insert Markdown cell below"},
%{seq: "N", desc: "Insert Elixir cell above"}, %{seq: ["N"], desc: "Insert Elixir cell above"},
%{seq: "M", desc: "Insert Markdown cell above"}, %{seq: ["M"], desc: "Insert Markdown cell above"},
%{seq: "dd", desc: "Delete cell"}, %{seq: ["dd"], desc: "Delete cell"},
%{seq: "ee", desc: "Evaluate cell"}, %{seq: ["ee"], desc: "Evaluate cell"},
%{seq: "es", desc: "Evaluate section"}, %{seq: ["es"], desc: "Evaluate section"},
%{seq: "ea", desc: "Evaluate all stale/new cells"}, %{seq: ["ea"], desc: "Evaluate all stale/new cells"},
%{seq: "ej", desc: "Evaluate cells below"}, %{seq: ["ej"], desc: "Evaluate cells below"},
%{seq: "ex", desc: "Cancel cell evaluation"} %{seq: ["ex"], desc: "Cancel cell evaluation"}
] ]
} }
@ -35,10 +35,10 @@ defmodule LivebookWeb.SessionLive.ShortcutsComponent do
def render(assigns) do def render(assigns) do
~L""" ~L"""
<div class="p-6 sm:max-w-4xl sm:w-full flex flex-col space-y-3"> <div class="p-6 sm:max-w-4xl sm:w-full flex flex-col space-y-3">
<h3 class="text-lg font-medium text-gray-900"> <h3 class="text-2xl font-semibold text-gray-800">
Keyboard shortcuts Keyboard shortcuts
</h3> </h3>
<p class="text-gray-500"> <p class="text-gray-700">
Livebook highly embraces keyboard navigation to improve your productivity. Livebook highly embraces keyboard navigation to improve your productivity.
It operates in one of two modes similarly to the Vim text editor. It operates in one of two modes similarly to the Vim text editor.
In <span class="font-semibold">navigation mode</span> you move around In <span class="font-semibold">navigation mode</span> you move around
@ -78,10 +78,8 @@ defmodule LivebookWeb.SessionLive.ShortcutsComponent do
<tbody> <tbody>
<%= for shortcut <- @shortcuts do %> <%= for shortcut <- @shortcuts do %>
<tr> <tr>
<td class="py-1 pr-4"> <td class="py-2 pr-3">
<span class="bg-editor text-editor py-0.5 px-2 rounded-md inline-flex items-center"> <%= render_seq(shortcut.seq, @platform) %>
<%= if(@platform == :mac, do: seq_for_mac(shortcut.seq), else: shortcut.seq) %>
</span>
</td> </td>
<td> <td>
<%= shortcut.desc %> <%= shortcut.desc %>
@ -93,10 +91,40 @@ defmodule LivebookWeb.SessionLive.ShortcutsComponent do
""" """
end end
defp render_seq(seq, platform) do
seq = if(platform == :mac, do: seq_for_mac(seq), else: seq)
joiner = remix_icon("add-line", class: "text-xl text-gray-600")
elements =
seq
|> Enum.map(&render_key/1)
|> Enum.intersperse(joiner)
assigns = %{elements: elements}
~L"""
<div class="flex space-x-1 items-center">
<%= for element <- @elements do %>
<%= element %>
<% end %>
</div>
"""
end
defp render_key(key) do
content_tag("span", key,
class:
"bg-editor text-gray-200 text-sm font-semibold h-8 w-8 flex items-center justify-center rounded-lg inline-flex items-center"
)
end
defp seq_for_mac(seq) do defp seq_for_mac(seq) do
seq Enum.map(seq, fn
|> String.replace("ctrl", "cmd") "ctrl" -> ""
|> String.replace("alt", "option") "alt" -> ""
key -> key
end)
end end
defp split_in_half(list) do defp split_in_half(list) do

View file

@ -6,24 +6,23 @@ defmodule LivebookWeb.SessionsComponent do
@impl true @impl true
def render(assigns) do def render(assigns) do
~L""" ~L"""
<div class="mt-3 flex flex-col space-y-2"> <div class="flex flex-col space-y-4">
<%= for summary <- @session_summaries do %> <%= for summary <- @session_summaries do %>
<div class="shadow rounded-md p-2"> <div class="p-5 flex items-center border border-gray-200 rounded-lg">
<div class="p-3 flex items-center"> <div class="flex-grow flex flex-col space-y-1">
<div class="flex-grow flex flex-col space-y-1 text-gray-700 text-lg hover:text-gray-900"> <%= live_redirect summary.notebook_name, to: Routes.session_path(@socket, :page, summary.session_id),
<%= live_redirect summary.notebook_name, to: Routes.session_path(@socket, :page, summary.session_id) %> class: "font-semibold text-gray-800 hover:text-gray-900" %>
<div class="text-gray-500 text-sm"> <div class="text-gray-600 text-sm">
<%= summary.path || "No file" %> <%= summary.path || "No file" %>
</div>
</div> </div>
<button class="text-gray-500 hover:text-current"
phx-click="delete_session"
phx-value-id="<%= summary.session_id %>"
phx-target="<%= @myself %>"
aria-label="delete">
<%= Icons.svg(:trash, class: "h-6") %>
</button>
</div> </div>
<button class="text-gray-400 hover:text-current"
phx-click="delete_session"
phx-value-id="<%= summary.session_id %>"
phx-target="<%= @myself %>"
aria-label="delete">
<%= remix_icon("delete-bin-line", class: "text-xl") %>
</button>
</div> </div>
<% end %> <% end %>
</div> </div>

View file

@ -1,19 +1,19 @@
<main role="main" class="flex-grow flex flex-col h-screen"> <main role="main" class="flex-grow flex flex-col h-screen">
<div class="fixed right-5 bottom-5 z-50 flex flex-col space-y-3"> <div class="fixed right-5 bottom-5 z-50 flex flex-col space-y-3">
<%= if live_flash(@flash, :info) do %> <%= if live_flash(@flash, :info) do %>
<div class="flex items-center space-x-2 rounded-md px-4 py-2 bg-blue-100 text-blue-400 hover:opacity-75 cursor-pointer" role="alert" <div class="flex items-center space-x-2 rounded-lg px-4 py-2 bg-blue-100 text-blue-600 hover:opacity-75 cursor-pointer" role="alert"
phx-click="lv:clear-flash" phx-click="lv:clear-flash"
phx-value-key="info"> phx-value-key="info">
<%= Icons.svg(:information_circle, class: "h-6 w-6") %> <%= remix_icon("information-line", class: "text-2xl") %>
<span class="whitespace-pre"><%= live_flash(@flash, :info) %></span> <span class="whitespace-pre"><%= live_flash(@flash, :info) %></span>
</div> </div>
<% end %> <% end %>
<%= if live_flash(@flash, :error) do %> <%= if live_flash(@flash, :error) do %>
<div class="flex items-center space-x-2 rounded-md px-4 py-2 bg-red-100 text-red-400 hover:opacity-75 cursor-pointer" role="alert" <div class="flex items-center space-x-2 rounded-lg px-4 py-2 bg-red-100 text-red-400 hover:opacity-75 cursor-pointer" role="alert"
phx-click="lv:clear-flash" phx-click="lv:clear-flash"
phx-value-key="error"> phx-value-key="error">
<%= Icons.svg(:exclamation_circle, class: "h-6 w-6") %> <%= remix_icon("error-warning-line", class: "text-2xl") %>
<span class="whitespace-pre"><%= live_flash(@flash, :error) %></span> <span class="whitespace-pre"><%= live_flash(@flash, :error) %></span>
</div> </div>
<% end %> <% end %>

View file

@ -7,8 +7,8 @@ defmodule LivebookWeb.HomeLiveTest do
test "disconnected and connected render", %{conn: conn} do test "disconnected and connected render", %{conn: conn} do
{:ok, view, disconnected_html} = live(conn, "/") {:ok, view, disconnected_html} = live(conn, "/")
assert disconnected_html =~ "Livebook" assert disconnected_html =~ "Running Sessions"
assert render(view) =~ "Livebook" assert render(view) =~ "Running Sessions"
end end
test "redirects to session upon creation", %{conn: conn} do test "redirects to session upon creation", %{conn: conn} do
@ -16,7 +16,7 @@ defmodule LivebookWeb.HomeLiveTest do
assert {:error, {:live_redirect, %{to: to}}} = assert {:error, {:live_redirect, %{to: to}}} =
view view
|> element("button", "New notebook") |> element("button", "New Notebook")
|> render_click() |> render_click()
assert to =~ "/sessions/" assert to =~ "/sessions/"