mirror of
https://github.com/livebook-dev/livebook.git
synced 2025-10-20 18:36:31 +08:00
Introduce code zen (#1115)
* Introduce code focus mode * Update aria labels * Update wording * Show shortcut in code zen toggle * Update shortcut
This commit is contained in:
parent
d3ebf42b32
commit
2b8d732a23
12 changed files with 160 additions and 18 deletions
|
@ -213,16 +213,24 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.menu__content {
|
.menu__content {
|
||||||
@apply absolute z-[100] rounded-lg bg-white flex flex-col py-2 mt-1;
|
@apply absolute z-[100] rounded-lg bg-white flex flex-col py-2;
|
||||||
box-shadow: 0px 15px 99px rgba(13, 24, 41, 0.15);
|
box-shadow: 0px 15px 99px rgba(13, 24, 41, 0.15);
|
||||||
}
|
}
|
||||||
|
|
||||||
.menu__content--right {
|
.menu__content--top-right {
|
||||||
@apply right-0;
|
@apply top-0 right-0 transform -translate-y-full -mt-1;
|
||||||
}
|
}
|
||||||
|
|
||||||
.menu__content--left {
|
.menu__content--top-left {
|
||||||
@apply left-0;
|
@apply top-0 left-0 transform -translate-y-full -mt-1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu__content--bottom-right {
|
||||||
|
@apply bottom-0 right-0 transform translate-y-full -mb-1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu__content--bottom-left {
|
||||||
|
@apply bottom-0 left-0 transform translate-y-full -mb-1;
|
||||||
}
|
}
|
||||||
|
|
||||||
.menu:not(.menu--open) > .menu__overlay,
|
.menu:not(.menu--open) > .menu__overlay,
|
||||||
|
|
|
@ -259,3 +259,28 @@ solely client-side operations.
|
||||||
[phx-hook="VirtualizedLines"]:not(:hover) [data-el-clipcopy] {
|
[phx-hook="VirtualizedLines"]:not(:hover) [data-el-clipcopy] {
|
||||||
@apply hidden;
|
@apply hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[data-el-session][data-js-code-zen] [data-el-section-headline],
|
||||||
|
[data-el-session][data-js-code-zen] [data-el-section-subheadline],
|
||||||
|
[data-el-session][data-js-code-zen] [data-el-cell][data-type="markdown"],
|
||||||
|
[data-el-session][data-js-code-zen] [data-el-actions],
|
||||||
|
[data-el-session][data-js-code-zen] [data-el-insert-buttons] {
|
||||||
|
@apply hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-el-session][data-js-code-zen] [data-el-sections-container] {
|
||||||
|
@apply space-y-0 mt-0;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-el-session][data-js-code-zen][data-js-no-outputs]
|
||||||
|
[data-el-outputs-container] {
|
||||||
|
@apply hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-el-session][data-js-code-zen] [data-el-code-zen-enable] {
|
||||||
|
@apply hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-el-session]:not([data-js-code-zen]) [data-el-focus-mode-options] {
|
||||||
|
@apply hidden;
|
||||||
|
}
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import { getAttributeOrThrow, parseInteger } from "../lib/attribute";
|
import { getAttributeOrThrow, parseInteger } from "../lib/attribute";
|
||||||
import { randomId, randomToken } from "../lib/utils";
|
import { isElementHidden, randomId, randomToken } from "../lib/utils";
|
||||||
import {
|
import {
|
||||||
getChannel,
|
getChannel,
|
||||||
transportDecode,
|
transportDecode,
|
||||||
|
@ -227,7 +227,7 @@ const JSView = {
|
||||||
const { iframe, iframePlaceholder } = this;
|
const { iframe, iframePlaceholder } = this;
|
||||||
const notebookEl = document.querySelector(`[data-el-notebook]`);
|
const notebookEl = document.querySelector(`[data-el-notebook]`);
|
||||||
|
|
||||||
if (iframePlaceholder.offsetParent === null) {
|
if (isElementHidden(iframePlaceholder)) {
|
||||||
// When the placeholder is hidden, we hide the iframe as well
|
// When the placeholder is hidden, we hide the iframe as well
|
||||||
iframe.classList.add("hidden");
|
iframe.classList.add("hidden");
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -7,6 +7,7 @@ import {
|
||||||
setFavicon,
|
setFavicon,
|
||||||
cancelEvent,
|
cancelEvent,
|
||||||
isElementInViewport,
|
isElementInViewport,
|
||||||
|
isElementHidden,
|
||||||
} from "../lib/utils";
|
} from "../lib/utils";
|
||||||
import { getAttributeOrDefault } from "../lib/attribute";
|
import { getAttributeOrDefault } from "../lib/attribute";
|
||||||
import KeyBuffer from "../lib/key_buffer";
|
import KeyBuffer from "../lib/key_buffer";
|
||||||
|
@ -68,6 +69,7 @@ const Session = {
|
||||||
|
|
||||||
this.focusedId = null;
|
this.focusedId = null;
|
||||||
this.insertMode = false;
|
this.insertMode = false;
|
||||||
|
this.codeZen = false;
|
||||||
this.keyBuffer = new KeyBuffer();
|
this.keyBuffer = new KeyBuffer();
|
||||||
this.clientsMap = {};
|
this.clientsMap = {};
|
||||||
this.lastLocationReportByClientPid = {};
|
this.lastLocationReportByClientPid = {};
|
||||||
|
@ -121,6 +123,21 @@ const Session = {
|
||||||
this.handleCellIndicatorsClick(event)
|
this.handleCellIndicatorsClick(event)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
this.getElement("code-zen-enable-button").addEventListener(
|
||||||
|
"click",
|
||||||
|
(event) => this.setCodeZen(true)
|
||||||
|
);
|
||||||
|
|
||||||
|
this.getElement("code-zen-disable-button").addEventListener(
|
||||||
|
"click",
|
||||||
|
(event) => this.setCodeZen(false)
|
||||||
|
);
|
||||||
|
|
||||||
|
this.getElement("code-zen-outputs-toggle").addEventListener(
|
||||||
|
"click",
|
||||||
|
(event) => this.el.toggleAttribute("data-js-no-outputs")
|
||||||
|
);
|
||||||
|
|
||||||
window.addEventListener(
|
window.addEventListener(
|
||||||
"phx:page-loading-stop",
|
"phx:page-loading-stop",
|
||||||
() => {
|
() => {
|
||||||
|
@ -359,9 +376,11 @@ const Session = {
|
||||||
} else if (keyBuffer.tryMatch(["N"])) {
|
} else if (keyBuffer.tryMatch(["N"])) {
|
||||||
this.insertCellAboveFocused("code");
|
this.insertCellAboveFocused("code");
|
||||||
} else if (keyBuffer.tryMatch(["m"])) {
|
} else if (keyBuffer.tryMatch(["m"])) {
|
||||||
this.insertCellBelowFocused("markdown");
|
!this.codeZen && this.insertCellBelowFocused("markdown");
|
||||||
} else if (keyBuffer.tryMatch(["M"])) {
|
} else if (keyBuffer.tryMatch(["M"])) {
|
||||||
this.insertCellAboveFocused("markdown");
|
!this.codeZen && this.insertCellAboveFocused("markdown");
|
||||||
|
} else if (keyBuffer.tryMatch(["z"])) {
|
||||||
|
this.setCodeZen(!this.codeZen);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -852,6 +871,28 @@ const Session = {
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
setCodeZen(enabled) {
|
||||||
|
this.codeZen = enabled;
|
||||||
|
|
||||||
|
if (enabled) {
|
||||||
|
this.el.setAttribute("data-js-code-zen", "");
|
||||||
|
} else {
|
||||||
|
this.el.removeAttribute("data-js-code-zen");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.focusedId) {
|
||||||
|
const visibleId = this.ensureVisibleFocusableEl(this.focusedId);
|
||||||
|
|
||||||
|
if (visibleId !== this.focused) {
|
||||||
|
this.setFocusedEl(visibleId, { scroll: false });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (visibleId) {
|
||||||
|
this.getFocusableEl(visibleId).scrollIntoView({ block: "center" });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
// Server event handlers
|
// Server event handlers
|
||||||
|
|
||||||
handleCellInserted(cellId) {
|
handleCellInserted(cellId) {
|
||||||
|
@ -863,7 +904,12 @@ const Session = {
|
||||||
|
|
||||||
handleCellDeleted(cellId, siblingCellId) {
|
handleCellDeleted(cellId, siblingCellId) {
|
||||||
if (this.focusedId === cellId) {
|
if (this.focusedId === cellId) {
|
||||||
this.setFocusedEl(siblingCellId);
|
if (this.codeZen) {
|
||||||
|
const visibleSiblingId = this.ensureVisibleFocusableEl(siblingCellId);
|
||||||
|
this.setFocusedEl(visibleSiblingId);
|
||||||
|
} else {
|
||||||
|
this.setFocusedEl(siblingCellId);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -1061,6 +1107,20 @@ const Session = {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
ensureVisibleFocusableEl(cellId) {
|
||||||
|
const focusableEl = this.getFocusableEl(cellId);
|
||||||
|
const allFocusableEls = Array.from(
|
||||||
|
this.el.querySelectorAll(`[data-focusable-id]`)
|
||||||
|
);
|
||||||
|
const idx = allFocusableEls.indexOf(focusableEl);
|
||||||
|
const visibleSibling = [
|
||||||
|
...allFocusableEls.slice(idx, -1),
|
||||||
|
...allFocusableEls.slice(0, idx).reverse(),
|
||||||
|
].find((el) => !isElementHidden(el));
|
||||||
|
|
||||||
|
return visibleSibling && visibleSibling.getAttribute("data-focusable-id");
|
||||||
|
},
|
||||||
|
|
||||||
isCell(focusableId) {
|
isCell(focusableId) {
|
||||||
const el = this.getFocusableEl(focusableId);
|
const el = this.getFocusableEl(focusableId);
|
||||||
return el.hasAttribute("data-el-cell");
|
return el.hasAttribute("data-el-cell");
|
||||||
|
@ -1081,7 +1141,9 @@ const Session = {
|
||||||
},
|
},
|
||||||
|
|
||||||
getFocusableEls() {
|
getFocusableEls() {
|
||||||
return Array.from(this.el.querySelectorAll(`[data-focusable-id]`));
|
return Array.from(this.el.querySelectorAll(`[data-focusable-id]`)).filter(
|
||||||
|
(el) => !isElementHidden(el)
|
||||||
|
);
|
||||||
},
|
},
|
||||||
|
|
||||||
getFocusableIds() {
|
getFocusableIds() {
|
||||||
|
|
|
@ -17,6 +17,10 @@ export function isElementInViewport(element) {
|
||||||
return box.bottom >= 0 && box.top <= window.innerHeight;
|
return box.bottom >= 0 && box.top <= window.innerHeight;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function isElementHidden(element) {
|
||||||
|
return element.offsetParent === null;
|
||||||
|
}
|
||||||
|
|
||||||
export function clamp(n, x, y) {
|
export function clamp(n, x, y) {
|
||||||
return Math.min(Math.max(n, x), y);
|
return Math.min(Math.max(n, x), y);
|
||||||
}
|
}
|
||||||
|
|
|
@ -193,7 +193,7 @@ defmodule LivebookWeb.FileSelectComponent do
|
||||||
|
|
||||||
defp file_system_menu_button(assigns) do
|
defp file_system_menu_button(assigns) do
|
||||||
~H"""
|
~H"""
|
||||||
<.menu id="file-system-menu" disabled={@file_system_select_disabled} position="left">
|
<.menu id="file-system-menu" disabled={@file_system_select_disabled} position="bottom-left">
|
||||||
<:toggle>
|
<:toggle>
|
||||||
<button type="button" class="button-base button-gray button-square-icon"
|
<button type="button" class="button-base button-gray button-square-icon"
|
||||||
aria-label="switch file system"
|
aria-label="switch file system"
|
||||||
|
|
|
@ -438,7 +438,7 @@ defmodule LivebookWeb.LiveHelpers do
|
||||||
assigns =
|
assigns =
|
||||||
assigns
|
assigns
|
||||||
|> assign_new(:disabled, fn -> false end)
|
|> assign_new(:disabled, fn -> false end)
|
||||||
|> assign_new(:position, fn -> "right" end)
|
|> assign_new(:position, fn -> "bottom-right" end)
|
||||||
|> assign_new(:secondary_click, fn -> false end)
|
|> assign_new(:secondary_click, fn -> false end)
|
||||||
|
|
||||||
~H"""
|
~H"""
|
||||||
|
@ -452,7 +452,7 @@ defmodule LivebookWeb.LiveHelpers do
|
||||||
</div>
|
</div>
|
||||||
<div class="menu__overlay"
|
<div class="menu__overlay"
|
||||||
phx-click-away={JS.remove_class("menu--open", to: "##{@id}")}></div>
|
phx-click-away={JS.remove_class("menu--open", to: "##{@id}")}></div>
|
||||||
<menu class={"menu__content menu__content--#{@position}"}
|
<menu class={"menu__content #{menu_content_class(@position)}"}
|
||||||
role="menu"
|
role="menu"
|
||||||
phx-click-away={JS.remove_class("menu--open", to: "##{@id}")}}>
|
phx-click-away={JS.remove_class("menu--open", to: "##{@id}")}}>
|
||||||
<%= render_slot(@content) %>
|
<%= render_slot(@content) %>
|
||||||
|
@ -461,6 +461,11 @@ defmodule LivebookWeb.LiveHelpers do
|
||||||
"""
|
"""
|
||||||
end
|
end
|
||||||
|
|
||||||
|
defp menu_content_class("top-left"), do: "menu__content--top-left"
|
||||||
|
defp menu_content_class("top-right"), do: "menu__content--top-right"
|
||||||
|
defp menu_content_class("bottom-left"), do: "menu__content--bottom-left"
|
||||||
|
defp menu_content_class("bottom-right"), do: "menu__content--bottom-right"
|
||||||
|
|
||||||
@doc """
|
@doc """
|
||||||
A menu item that shows a submenu on hover.
|
A menu item that shows a submenu on hover.
|
||||||
|
|
||||||
|
|
|
@ -198,7 +198,7 @@ defmodule LivebookWeb.SessionLive do
|
||||||
runtime={@data_view.runtime}
|
runtime={@data_view.runtime}
|
||||||
cell_view={@data_view.setup_cell_view} />
|
cell_view={@data_view.setup_cell_view} />
|
||||||
</div>
|
</div>
|
||||||
<div class="mt-8 flex flex-col w-full space-y-16">
|
<div class="mt-8 flex flex-col w-full space-y-16" data-el-sections-container>
|
||||||
<%= if @data_view.section_views == [] do %>
|
<%= if @data_view.section_views == [] do %>
|
||||||
<div class="flex justify-center">
|
<div class="flex justify-center">
|
||||||
<button class="button-base button-small"
|
<button class="button-base button-small"
|
||||||
|
|
|
@ -5,6 +5,7 @@ defmodule LivebookWeb.SessionLive.IndicatorsComponent do
|
||||||
def render(assigns) do
|
def render(assigns) do
|
||||||
~H"""
|
~H"""
|
||||||
<div class="flex flex-col items-center space-y-2" data-el-notebook-indicators>
|
<div class="flex flex-col items-center space-y-2" data-el-notebook-indicators>
|
||||||
|
<.code_zen_indicator />
|
||||||
<%= if @file do %>
|
<%= if @file do %>
|
||||||
<%= if @dirty do %>
|
<%= if @dirty do %>
|
||||||
<%= if @autosave_interval_s do %>
|
<%= if @autosave_interval_s do %>
|
||||||
|
@ -67,6 +68,42 @@ defmodule LivebookWeb.SessionLive.IndicatorsComponent do
|
||||||
"""
|
"""
|
||||||
end
|
end
|
||||||
|
|
||||||
|
defp code_zen_indicator(assigns) do
|
||||||
|
~H"""
|
||||||
|
<span class="tooltip left" data-tooltip="Enter code zen (z)" data-el-code-zen-enable>
|
||||||
|
<button class="icon-button icon-outlined-button border-gray-200 hover:bg-gray-100 focus:bg-gray-100"
|
||||||
|
aria-label="enter code zen"
|
||||||
|
data-el-code-zen-enable-button>
|
||||||
|
<.remix_icon icon="code-line" class="text-xl text-gray-400" />
|
||||||
|
</button>
|
||||||
|
</span>
|
||||||
|
<div data-el-focus-mode-options>
|
||||||
|
<.menu id="focus-mode-menu" position="top-right">
|
||||||
|
<:toggle>
|
||||||
|
<button class="icon-button icon-outlined-button border-green-bright-300 hover:bg-green-bright-50 focus:bg-green-bright-50"
|
||||||
|
aria-label="code zen options">
|
||||||
|
<.remix_icon icon="code-line" class="text-xl text-green-bright-400" />
|
||||||
|
</button>
|
||||||
|
</:toggle>
|
||||||
|
<:content>
|
||||||
|
<button class="menu-item text-gray-500"
|
||||||
|
role="menuitem"
|
||||||
|
data-el-code-zen-outputs-toggle>
|
||||||
|
<.remix_icon icon="layout-bottom-2-line" />
|
||||||
|
<span class="font-medium">Toggle outputs</span>
|
||||||
|
</button>
|
||||||
|
<button class="menu-item text-gray-500"
|
||||||
|
role="menuitem"
|
||||||
|
data-el-code-zen-disable-button>
|
||||||
|
<.remix_icon icon="close-line" />
|
||||||
|
<span class="font-medium">Exit code zen</span>
|
||||||
|
</button>
|
||||||
|
</:content>
|
||||||
|
</.menu>
|
||||||
|
</div>
|
||||||
|
"""
|
||||||
|
end
|
||||||
|
|
||||||
defp global_status(%{status: :evaluating} = assigns) do
|
defp global_status(%{status: :evaluating} = assigns) do
|
||||||
~H"""
|
~H"""
|
||||||
<span class="tooltip left" data-tooltip="Go to evaluating cell">
|
<span class="tooltip left" data-tooltip="Go to evaluating cell">
|
||||||
|
|
|
@ -14,7 +14,7 @@ defmodule LivebookWeb.SessionLive.InsertButtonsComponent do
|
||||||
phx-value-section_id={@section_id}
|
phx-value-section_id={@section_id}
|
||||||
phx-value-cell_id={@cell_id}
|
phx-value-cell_id={@cell_id}
|
||||||
>+ Code</button>
|
>+ Code</button>
|
||||||
<.menu id={"#{@id}-block-menu"} position="left">
|
<.menu id={"#{@id}-block-menu"} position="bottom-left">
|
||||||
<:toggle>
|
<:toggle>
|
||||||
<button class="button-base button-small">+ Block</button>
|
<button class="button-base button-small">+ Block</button>
|
||||||
</:toggle>
|
</:toggle>
|
||||||
|
@ -61,7 +61,7 @@ defmodule LivebookWeb.SessionLive.InsertButtonsComponent do
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
<% true -> %>
|
<% true -> %>
|
||||||
<.menu id={"#{@id}-smart-menu"} position="left">
|
<.menu id={"#{@id}-smart-menu"} position="bottom-left">
|
||||||
<:toggle>
|
<:toggle>
|
||||||
<button class="button-base button-small">+ Smart</button>
|
<button class="button-base button-small">+ Smart</button>
|
||||||
</:toggle>
|
</:toggle>
|
||||||
|
|
|
@ -89,7 +89,7 @@ defmodule LivebookWeb.SessionLive.SectionComponent do
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<%= if @section_view.parent do %>
|
<%= if @section_view.parent do %>
|
||||||
<h3 class="mt-1 flex items-end space-x-1 text-sm font-semibold text-gray-800">
|
<h3 class="mt-1 flex items-end space-x-1 text-sm font-semibold text-gray-800" data-el-section-subheadline>
|
||||||
<span class="tooltip bottom" data-tooltip={"This section branches out from the main flow\nand can be evaluated in parallel"}>
|
<span class="tooltip bottom" data-tooltip={"This section branches out from the main flow\nand can be evaluated in parallel"}>
|
||||||
<.remix_icon icon="git-branch-line" class="text-lg font-normal flip-horizontally leading-none" />
|
<.remix_icon icon="git-branch-line" class="text-lg font-normal flip-horizontally leading-none" />
|
||||||
</span>
|
</span>
|
||||||
|
|
|
@ -91,6 +91,7 @@ defmodule LivebookWeb.SessionLive.ShortcutsComponent do
|
||||||
%{seq: ["m"], desc: "Insert Markdown cell below", basic: true},
|
%{seq: ["m"], desc: "Insert Markdown cell below", basic: true},
|
||||||
%{seq: ["N"], desc: "Insert Code cell above"},
|
%{seq: ["N"], desc: "Insert Code cell above"},
|
||||||
%{seq: ["M"], desc: "Insert Markdown cell above"},
|
%{seq: ["M"], desc: "Insert Markdown cell above"},
|
||||||
|
%{seq: ["z"], desc: "Toggle code zen"},
|
||||||
%{seq: ["d", "d"], desc: "Delete cell", basic: true},
|
%{seq: ["d", "d"], desc: "Delete cell", basic: true},
|
||||||
%{seq: ["e", "e"], desc: "Evaluate cell"},
|
%{seq: ["e", "e"], desc: "Evaluate cell"},
|
||||||
%{seq: ["e", "s"], desc: "Evaluate section"},
|
%{seq: ["e", "s"], desc: "Evaluate section"},
|
||||||
|
|
Loading…
Add table
Reference in a new issue