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 */
.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) {
@ -25,7 +25,7 @@
}
.button-primary {
@apply border-0 bg-blue-400 text-white;
@apply border-transparent bg-blue-600 text-white;
}
.button-primary:not(:disabled) {
@ -35,11 +35,11 @@
/* Form fields */
.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 {
@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 {

View file

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

View file

@ -99,14 +99,14 @@
}
.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 */
background-color: #282c34;
color: #abb2bf;
}
.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 */
background-color: #282c34;
color: #abb2bf;

View file

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

View file

@ -1,4 +1,10 @@
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 { Socket } from "phoenix";

View file

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

View file

@ -6,6 +6,8 @@
"": {
"license": "MIT",
"dependencies": {
"@fontsource/inter": "^4.2.2",
"@fontsource/jetbrains-mono": "^4.2.2",
"dompurify": "^2.2.6",
"hyperlist": "^1.0.0",
"marked": "^1.2.8",
@ -14,7 +16,8 @@
"nprogress": "^0.2.0",
"phoenix": "file:../deps/phoenix",
"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": {
"@babel/core": "^7.0.0",
@ -1095,6 +1098,16 @@
"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": {
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/@fullhuman/postcss-purgecss/-/postcss-purgecss-3.1.3.tgz",
@ -13059,6 +13072,11 @@
"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": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/remove-trailing-separator/-/remove-trailing-separator-1.1.0.tgz",
@ -16690,6 +16708,16 @@
"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": {
"version": "3.1.3",
"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": {
"version": "1.1.0",
"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"
},
"dependencies": {
"@fontsource/inter": "^4.2.2",
"@fontsource/jetbrains-mono": "^4.2.2",
"dompurify": "^2.2.6",
"hyperlist": "^1.0.0",
"marked": "^1.2.8",
@ -18,7 +20,8 @@
"nprogress": "^0.2.0",
"phoenix": "file:../deps/phoenix",
"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": {
"@babel/core": "^7.0.0",

View file

@ -7,7 +7,38 @@ module.exports = {
],
darkMode: false,
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: {
extend: {},

View file

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

View file

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

View file

@ -1,5 +1,6 @@
defmodule LivebookWeb.Helpers do
import Phoenix.LiveView.Helpers
import Phoenix.HTML.Tag
@doc """
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/)
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

View file

@ -17,25 +17,25 @@ defmodule LivebookWeb.CellComponent do
def render_cell_content(%{cell: %{type: :markdown}} = assigns) do
~L"""
<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">
<%= Icons.svg(:pencil, class: "h-6") %>
<button class="text-gray-400 hover:text-current" data-element="enable-insert-mode-button">
<%= remix_icon("pencil-line", class: "text-2xl") %>
</button>
<button class="text-gray-500 hover:text-current"
<button class="text-gray-400 hover:text-current"
phx-click="delete_cell"
phx-value-cell_id="<%= @cell.id %>">
<%= Icons.svg(:trash, class: "h-6") %>
<%= remix_icon("delete-bin-line", class: "text-2xl") %>
</button>
<button class="text-gray-500 hover:text-current"
<button class="text-gray-400 hover:text-current"
phx-click="move_cell"
phx-value-cell_id="<%= @cell.id %>"
phx-value-offset="-1">
<%= Icons.svg(:chevron_up, class: "h-6") %>
<%= remix_icon("arrow-up-s-line", class: "text-2xl") %>
</button>
<button class="text-gray-500 hover:text-current"
<button class="text-gray-400 hover:text-current"
phx-click="move_cell"
phx-value-cell_id="<%= @cell.id %>"
phx-value-offset="1">
<%= Icons.svg(:chevron_down, class: "h-6") %>
<%= remix_icon("arrow-down-s-line", class: "text-2xl") %>
</button>
</div>
@ -53,37 +53,37 @@ defmodule LivebookWeb.CellComponent do
~L"""
<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 %>
<button class="text-gray-500 hover:text-current"
<button class="text-gray-400 hover:text-current"
phx-click="queue_cell_evaluation"
phx-value-cell_id="<%= @cell.id %>">
<%= Icons.svg(:play, class: "h-6") %>
<%= remix_icon("play-circle-line", class: "text-2xl") %>
</button>
<% else %>
<button class="text-gray-500 hover:text-current"
<button class="text-gray-400 hover:text-current"
phx-click="cancel_cell_evaluation"
phx-value-cell_id="<%= @cell.id %>">
<%= Icons.svg(:stop, class: "h-6") %>
<%= remix_icon("stop-circle-line", class: "text-2xl") %>
</button>
<% end %>
<button class="text-gray-500 hover:text-current"
<button class="text-gray-400 hover:text-current"
phx-click="delete_cell"
phx-value-cell_id="<%= @cell.id %>">
<%= Icons.svg(:trash, class: "h-6") %>
<%= remix_icon("delete-bin-line", class: "text-2xl") %>
</button>
<%= live_patch to: Routes.session_path(@socket, :cell_settings, @session_id, @cell.id), class: "text-gray-500 hover:text-current" do %>
<%= Icons.svg(:adjustments, class: "h-6") %>
<%= live_patch to: Routes.session_path(@socket, :cell_settings, @session_id, @cell.id), class: "text-gray-400 hover:text-current" do %>
<%= remix_icon("list-settings-line", class: "text-2xl") %>
<% end %>
<button class="text-gray-500 hover:text-current"
<button class="text-gray-400 hover:text-current"
phx-click="move_cell"
phx-value-cell_id="<%= @cell.id %>"
phx-value-offset="-1">
<%= Icons.svg(:chevron_up, class: "h-6") %>
<%= remix_icon("arrow-up-s-line", class: "text-2xl") %>
</button>
<button class="text-gray-500 hover:text-current"
<button class="text-gray-400 hover:text-current"
phx-click="move_cell"
phx-value-cell_id="<%= @cell.id %>"
phx-value-offset="1">
<%= Icons.svg(:chevron_down, class: "h-6") %>
<%= remix_icon("arrow-down-s-line", class: "text-2xl") %>
</button>
</div>
@ -102,7 +102,7 @@ defmodule LivebookWeb.CellComponent do
assigns = %{cell: cell, cell_info: cell_info, show_status: show_status}
~L"""
<div class="py-3 rounded-md overflow-hidden bg-editor relative">
<div class="py-3 rounded-lg overflow-hidden bg-editor relative">
<div
id="editor-container-<%= @cell.id %>"
data-element="editor-container"
@ -137,9 +137,9 @@ defmodule LivebookWeb.CellComponent do
~L"""
<div class="max-w-2xl w-full animate-pulse">
<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-md"></div>
<div class="h-4 bg-gray-200 rounded-md w-5/6"></div>
<div class="h-4 bg-gray-200 rounded-lg w-3/4"></div>
<div class="h-4 bg-gray-200 rounded-lg"></div>
<div class="h-4 bg-gray-200 rounded-lg w-5/6"></div>
</div>
</div>
"""
@ -159,9 +159,9 @@ defmodule LivebookWeb.CellComponent do
~L"""
<div class="px-8 max-w-2xl w-full animate-pulse">
<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-md"></div>
<div class="h-4 bg-gray-500 rounded-md w-5/6"></div>
<div class="h-4 bg-gray-500 rounded-lg w-3/4"></div>
<div class="h-4 bg-gray-500 rounded-lg"></div>
<div class="h-4 bg-gray-500 rounded-lg w-5/6"></div>
</div>
</div>
"""
@ -171,7 +171,7 @@ defmodule LivebookWeb.CellComponent do
assigns = %{outputs: outputs, cell_id: cell_id}
~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 %>
<div class="p-4">
<div class="">
@ -230,7 +230,7 @@ defmodule LivebookWeb.CellComponent do
<div class="text-xs text-gray-400">Evaluating</div>
<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="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>
</div>
"""

View file

@ -17,17 +17,14 @@ defmodule LivebookWeb.HomeLive do
@impl true
def render(assigns) do
~L"""
<header class="flex justify-center p-4 border-b">
<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="container max-w-4xl w-full mx-auto p-4 pb-8 flex flex-col items-center space-y-4">
<div class="w-full flex justify-end">
<button class="button-base button-sm"
<button class="button-base button-primary"
phx-click="new">
New notebook
New Notebook
</button>
</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,
id: "path_select",
path: @path,
@ -35,27 +32,30 @@ defmodule LivebookWeb.HomeLive do
running_paths: paths(@session_summaries),
target: nil %>
<div class="flex justify-end space-x-2">
<%= content_tag :button, "Fork",
class: "button-base button-sm",
<%= content_tag :button,
class: "button-base",
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 %>
<%= 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 %>
<%= content_tag :button, "Open",
class: "button-base button-sm button-primary",
class: "button-base button-primary",
phx_click: "open",
disabled: not path_openable?(@path, @session_summaries) %>
<% end %>
</div>
</div>
<div class="w-full pt-24">
<h3 class="text-xl font-medium text-gray-900">
Running sessions
<h3 class="text-xl font-semibold text-gray-800 mb-5">
Running Sessions
</h3>
<%= 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.
</div>
<% 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"""
<div class="<%= if(@persistent, do: "opacity-100", else: "opacity-0") %> hover:opacity-100 flex space-x-2 justify-center items-center">
<%= 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-value-type="markdown"
phx-value-section_id="<%= @section_id %>"
phx-value-index="<%= @index %>">
+ Markdown
</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-value-type="elixir"
phx-value-section_id="<%= @section_id %>"

View file

@ -19,10 +19,15 @@ defmodule LivebookWeb.ModalComponent do
phx-page-loading></div>
<!-- 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"
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 %>
</div>
</div>

View file

@ -14,7 +14,7 @@ defmodule LivebookWeb.PathSelectComponent do
def render(assigns) do
~L"""
<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"
phx-hook="FocusOnUpdate"
type="text"
@ -37,22 +37,22 @@ defmodule LivebookWeb.PathSelectComponent do
defp render_file(file, target) do
icon =
case file do
%{is_running: true} -> :play
%{is_dir: true} -> :folder
_ -> :document_text
%{is_running: true} -> "play-circle-line"
%{is_dir: true} -> "folder-fill"
_ -> "file-code-line"
end
assigns = %{file: file, icon: icon}
~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-value-path="<%= file.path %>"
<%= if target, do: "phx-target=#{target}" %>>
<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 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 %>
</span>
</button>

View file

@ -20,7 +20,7 @@ defmodule LivebookWeb.SectionComponent do
</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" tabindex="-1">
<%= Icons.svg(:trash, class: "h-6") %>
<%= remix_icon("delete-bin-line", class: "text-2xl") %>
</button>
</div>
</div>

View file

@ -91,14 +91,14 @@ defmodule LivebookWeb.SessionLive do
<% end %>
<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">
<%= Icons.svg(:plus, class: "h-6") %>
<%= remix_icon("add-line", class: "text-2xl") %>
<span>New section</span>
</div>
</button>
</div>
<%= 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">
<%= 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>
</div>
<% 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">
<%= if @data.path 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 %>
<%= Icons.svg(:check_circle, class: "h-5 text-green-400") %>
<%= remix_icon("checkbox-circle-line", class: "text-xl text-green-400") %>
<% end %>
<span>
<%= Path.basename(@data.path) %>
</span>
<% else %>
<%= Icons.svg(:document_text, class: "h-5 text-gray-400") %>
<%= remix_icon("file-code-line", class: "text-xl text-gray-400") %>
<span>
No file choosen
</span>
@ -123,10 +123,10 @@ defmodule LivebookWeb.SessionLive do
<% end %>
<div class="p-4 flex space-x-2">
<%= 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 %>
<%= 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 %>
</div>
</div>

View file

@ -13,7 +13,7 @@ defmodule LivebookWeb.SessionLive.AttachedLive do
~L"""
<div class="flex-col space-y-3">
<%= 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 %>
</div>
<% end %>
@ -35,10 +35,10 @@ defmodule LivebookWeb.SessionLive.AttachedLive do
Then enter the name of the node below:
</p>
<%= 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") %>
<%= submit "Connect", class: "mt-3 button-base button-sm" %>
<%= submit "Connect", class: "mt-3 button-base" %>
</form>
</div>
"""

View file

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

View file

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

View file

@ -33,7 +33,7 @@ defmodule LivebookWeb.SessionLive.MixStandaloneLive do
extnames: [],
running_paths: [],
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 %>
<%= if @status != :initial do %>
<div class="markdown">

View file

@ -50,7 +50,7 @@ defmodule LivebookWeb.SessionLive.PersistenceComponent do
</div>
<div class="flex justify-end">
<%= content_tag :button, "Done",
class: "button-base button-primary button-sm",
class: "button-base button-primary",
phx_click: "done",
phx_target: @myself,
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),
which you can configure yourself here.
</p>
<div class="shadow rounded-md p-2">
<div class="border border-gray-200 rounded-lg p-2">
<%= if @runtime do %>
<table class="w-full text-center text-sm">
<thead>
@ -35,7 +35,7 @@ defmodule LivebookWeb.SessionLive.RuntimeComponent do
<td><%= runtime_type_label(@runtime) %></td>
<td><%= @runtime.node %></td>
<td>
<button class="button-base text-sm button-sm button-danger"
<button class="button-base text-sm button-danger"
type="button"
phx-click="disconnect"
phx-target="<%= @myself %>">

View file

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

View file

@ -6,24 +6,23 @@ defmodule LivebookWeb.SessionsComponent do
@impl true
def render(assigns) do
~L"""
<div class="mt-3 flex flex-col space-y-2">
<div class="flex flex-col space-y-4">
<%= for summary <- @session_summaries do %>
<div class="shadow rounded-md p-2">
<div class="p-3 flex items-center">
<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) %>
<div class="text-gray-500 text-sm">
<%= summary.path || "No file" %>
</div>
<div class="p-5 flex items-center border border-gray-200 rounded-lg">
<div class="flex-grow flex flex-col space-y-1">
<%= 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-600 text-sm">
<%= summary.path || "No file" %>
</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>
<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>
<% end %>
</div>

View file

@ -1,19 +1,19 @@
<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">
<%= 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-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>
</div>
<% end %>
<%= 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-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>
</div>
<% end %>

View file

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