mirror of
https://github.com/livebook-dev/livebook.git
synced 2025-10-19 18:06:16 +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 {
|
||||
@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);
|
||||
}
|
||||
|
||||
.menu__content--right {
|
||||
@apply right-0;
|
||||
.menu__content--top-right {
|
||||
@apply top-0 right-0 transform -translate-y-full -mt-1;
|
||||
}
|
||||
|
||||
.menu__content--left {
|
||||
@apply left-0;
|
||||
.menu__content--top-left {
|
||||
@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,
|
||||
|
|
|
@ -259,3 +259,28 @@ solely client-side operations.
|
|||
[phx-hook="VirtualizedLines"]:not(:hover) [data-el-clipcopy] {
|
||||
@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 { randomId, randomToken } from "../lib/utils";
|
||||
import { isElementHidden, randomId, randomToken } from "../lib/utils";
|
||||
import {
|
||||
getChannel,
|
||||
transportDecode,
|
||||
|
@ -227,7 +227,7 @@ const JSView = {
|
|||
const { iframe, iframePlaceholder } = this;
|
||||
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
|
||||
iframe.classList.add("hidden");
|
||||
} else {
|
||||
|
|
|
@ -7,6 +7,7 @@ import {
|
|||
setFavicon,
|
||||
cancelEvent,
|
||||
isElementInViewport,
|
||||
isElementHidden,
|
||||
} from "../lib/utils";
|
||||
import { getAttributeOrDefault } from "../lib/attribute";
|
||||
import KeyBuffer from "../lib/key_buffer";
|
||||
|
@ -68,6 +69,7 @@ const Session = {
|
|||
|
||||
this.focusedId = null;
|
||||
this.insertMode = false;
|
||||
this.codeZen = false;
|
||||
this.keyBuffer = new KeyBuffer();
|
||||
this.clientsMap = {};
|
||||
this.lastLocationReportByClientPid = {};
|
||||
|
@ -121,6 +123,21 @@ const Session = {
|
|||
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(
|
||||
"phx:page-loading-stop",
|
||||
() => {
|
||||
|
@ -359,9 +376,11 @@ const Session = {
|
|||
} else if (keyBuffer.tryMatch(["N"])) {
|
||||
this.insertCellAboveFocused("code");
|
||||
} else if (keyBuffer.tryMatch(["m"])) {
|
||||
this.insertCellBelowFocused("markdown");
|
||||
!this.codeZen && this.insertCellBelowFocused("markdown");
|
||||
} 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
|
||||
|
||||
handleCellInserted(cellId) {
|
||||
|
@ -863,7 +904,12 @@ const Session = {
|
|||
|
||||
handleCellDeleted(cellId, siblingCellId) {
|
||||
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) {
|
||||
const el = this.getFocusableEl(focusableId);
|
||||
return el.hasAttribute("data-el-cell");
|
||||
|
@ -1081,7 +1141,9 @@ const Session = {
|
|||
},
|
||||
|
||||
getFocusableEls() {
|
||||
return Array.from(this.el.querySelectorAll(`[data-focusable-id]`));
|
||||
return Array.from(this.el.querySelectorAll(`[data-focusable-id]`)).filter(
|
||||
(el) => !isElementHidden(el)
|
||||
);
|
||||
},
|
||||
|
||||
getFocusableIds() {
|
||||
|
|
|
@ -17,6 +17,10 @@ export function isElementInViewport(element) {
|
|||
return box.bottom >= 0 && box.top <= window.innerHeight;
|
||||
}
|
||||
|
||||
export function isElementHidden(element) {
|
||||
return element.offsetParent === null;
|
||||
}
|
||||
|
||||
export function clamp(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
|
||||
~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>
|
||||
<button type="button" class="button-base button-gray button-square-icon"
|
||||
aria-label="switch file system"
|
||||
|
|
|
@ -438,7 +438,7 @@ defmodule LivebookWeb.LiveHelpers do
|
|||
assigns =
|
||||
assigns
|
||||
|> 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)
|
||||
|
||||
~H"""
|
||||
|
@ -452,7 +452,7 @@ defmodule LivebookWeb.LiveHelpers do
|
|||
</div>
|
||||
<div class="menu__overlay"
|
||||
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"
|
||||
phx-click-away={JS.remove_class("menu--open", to: "##{@id}")}}>
|
||||
<%= render_slot(@content) %>
|
||||
|
@ -461,6 +461,11 @@ defmodule LivebookWeb.LiveHelpers do
|
|||
"""
|
||||
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 """
|
||||
A menu item that shows a submenu on hover.
|
||||
|
||||
|
|
|
@ -198,7 +198,7 @@ defmodule LivebookWeb.SessionLive do
|
|||
runtime={@data_view.runtime}
|
||||
cell_view={@data_view.setup_cell_view} />
|
||||
</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 %>
|
||||
<div class="flex justify-center">
|
||||
<button class="button-base button-small"
|
||||
|
|
|
@ -5,6 +5,7 @@ defmodule LivebookWeb.SessionLive.IndicatorsComponent do
|
|||
def render(assigns) do
|
||||
~H"""
|
||||
<div class="flex flex-col items-center space-y-2" data-el-notebook-indicators>
|
||||
<.code_zen_indicator />
|
||||
<%= if @file do %>
|
||||
<%= if @dirty do %>
|
||||
<%= if @autosave_interval_s do %>
|
||||
|
@ -67,6 +68,42 @@ defmodule LivebookWeb.SessionLive.IndicatorsComponent do
|
|||
"""
|
||||
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
|
||||
~H"""
|
||||
<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-cell_id={@cell_id}
|
||||
>+ Code</button>
|
||||
<.menu id={"#{@id}-block-menu"} position="left">
|
||||
<.menu id={"#{@id}-block-menu"} position="bottom-left">
|
||||
<:toggle>
|
||||
<button class="button-base button-small">+ Block</button>
|
||||
</:toggle>
|
||||
|
@ -61,7 +61,7 @@ defmodule LivebookWeb.SessionLive.InsertButtonsComponent do
|
|||
</span>
|
||||
|
||||
<% true -> %>
|
||||
<.menu id={"#{@id}-smart-menu"} position="left">
|
||||
<.menu id={"#{@id}-smart-menu"} position="bottom-left">
|
||||
<:toggle>
|
||||
<button class="button-base button-small">+ Smart</button>
|
||||
</:toggle>
|
||||
|
|
|
@ -89,7 +89,7 @@ defmodule LivebookWeb.SessionLive.SectionComponent do
|
|||
</div>
|
||||
</div>
|
||||
<%= 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"}>
|
||||
<.remix_icon icon="git-branch-line" class="text-lg font-normal flip-horizontally leading-none" />
|
||||
</span>
|
||||
|
|
|
@ -91,6 +91,7 @@ defmodule LivebookWeb.SessionLive.ShortcutsComponent do
|
|||
%{seq: ["m"], desc: "Insert Markdown cell below", basic: true},
|
||||
%{seq: ["N"], desc: "Insert Code cell above"},
|
||||
%{seq: ["M"], desc: "Insert Markdown cell above"},
|
||||
%{seq: ["z"], desc: "Toggle code zen"},
|
||||
%{seq: ["d", "d"], desc: "Delete cell", basic: true},
|
||||
%{seq: ["e", "e"], desc: "Evaluate cell"},
|
||||
%{seq: ["e", "s"], desc: "Evaluate section"},
|
||||
|
|
Loading…
Add table
Reference in a new issue