Something went wrong. Try again.
Monorepo for the nibling.run platform
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257import { sheet } from "@nibling/web/internals";import "@nibling/web/toolbar";import type { CaretMove } from "../../views/editing.ts";import { LANGUAGE_NAMES, typingKeys, type Language } from "../../views/keys.ts";
/** * Styles for the compact (phone) presentation. Panel bodies still live in the shell's body layer; * the editor area and the sheet's body are transparent so the positioned bodies show through. */export const compactSheet = sheet(` :host(:state(compact)) { --nib-size-tab-bar: var(--nib-size-touch-target); --nib-size-row: var(--nib-size-touch-target); } /* The keyboard insets are set from the visual viewport, so nothing hides behind an on-screen keyboard. */ [part="compact"] { position: absolute; inset: var(--keyboard-top, 0px) 0 var(--keyboard-bottom, 0px) 0; display: grid; grid-template-rows: auto minmax(0, 1fr) auto auto; pointer-events: none; } [part="keys"] { overflow-x: auto; overscroll-behavior-x: contain; scrollbar-width: none; background: var(--nib-color-surface); border-top: var(--nib-size-border) solid var(--nib-color-border); pointer-events: auto; } [part="keys"][hidden] { display: none; } button[part~="key"] { flex: none; font-family: var(--nib-font-family-code); touch-action: manipulation; } button[part~="key"]:active { background: var(--nib-color-hover); } /* Hide keyboard stays reachable at the end of the row however far it has scrolled. */ [part="keys"] button[part~="dismiss"] { position: sticky; inset-inline-end: 0; margin-inline-start: auto; background: var(--nib-color-surface); border-inline-start: var(--nib-size-border) solid var(--nib-color-border); } [part="header"] { display: flex; align-items: center; gap: var(--nib-space-xs); min-height: var(--nib-size-touch-target); padding-inline: var(--nib-space-xs); background: var(--nib-color-surface); border-bottom: var(--nib-size-border) solid var(--nib-color-border); pointer-events: auto; } [part="title"] { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; text-align: center; font-weight: var(--nib-font-weight-strong); } button[part] { min-width: var(--nib-size-touch-target); min-height: var(--nib-size-touch-target); padding-inline: var(--nib-space-sm); border: 0; border-radius: var(--nib-radius-md); background: none; color: inherit; font: inherit; } button[part]:focus-visible { outline: var(--nib-size-focus-ring) solid var(--nib-color-focus-ring); outline-offset: calc(-1 * var(--nib-size-focus-ring)); } [part="header"] button[aria-pressed="true"] { background: var(--nib-color-selected); } [part="main"] { display: flex; min-height: 0; } [part="sheet"] { display: flex; flex-direction: column; border-top: var(--nib-size-border) solid var(--nib-color-border); } [part="sheet-handle"] { width: 100%; text-align: start; background: var(--nib-color-surface); pointer-events: auto; } /* Empty alt text keeps the glyph out of the accessible name. */ [part="sheet-handle"]::before { content: "▴" / ""; margin-inline-end: var(--nib-space-sm); color: var(--nib-color-text-muted); } [part="sheet-handle"][aria-expanded="true"]::before { content: "▾" / ""; } [part="sheet-body"] { display: flex; height: var(--nib-size-sheet); } [part="sheet-body"][hidden], [part="sheet"][hidden] { display: none; } [part="drawer"] { width: var(--nib-size-drawer); max-width: none; height: 100%; max-height: none; margin: 0; padding: 0; border: 0; border-inline-end: var(--nib-size-border) solid var(--nib-color-border); background: var(--nib-color-canvas); color: var(--nib-color-text); font: var(--nib-font-ui); /* The compact root lets pointer events through; the drawer inherits from it even in the top layer. */ pointer-events: auto; } [part="drawer"][open] { display: flex; flex-direction: column; } [part="drawer"]::backdrop { background: var(--nib-color-scrim); } [part="drawer-header"] { display: flex; align-items: center; justify-content: space-between; padding-inline-start: var(--nib-space-sm); min-height: var(--nib-size-touch-target); background: var(--nib-color-surface); border-bottom: var(--nib-size-border) solid var(--nib-color-border); } [part="drawer-header"] h2 { margin: 0; font: inherit; font-weight: var(--nib-font-weight-strong); } [part="drawer-body"] { display: flex; flex: 1; min-height: 0; } [part="drawer-body"] > nib-tabpanel { position: static; flex: 1; }`);
/** What a key bar key does to the focused editing target. */export type KeyAction = | { readonly type: "insert"; readonly text: string; readonly caret?: number } | { readonly type: "move"; readonly direction: CaretMove } | { readonly type: "indent" | "undo" | "redo" | "dismiss" };
/** Keys every language gets: indent and caret movement lead the row; history follows the typing keys. */const LEAD: readonly { readonly label: string; readonly name: string; readonly action: KeyAction }[] = [ { label: "⇥", name: "Tab", action: { type: "indent" } }, { label: "←", name: "Left", action: { type: "move", direction: "left" } }, { label: "↑", name: "Up", action: { type: "move", direction: "up" } }, { label: "↓", name: "Down", action: { type: "move", direction: "down" } }, { label: "→", name: "Right", action: { type: "move", direction: "right" } },];const TAIL: typeof LEAD = [ { label: "↶", name: "Undo", action: { type: "undo" } }, { label: "↷", name: "Redo", action: { type: "redo" } },];
export interface CompactActions { /** The header's Files button. */ readonly files: () => void; /** The header's Preview toggle. */ readonly preview: () => void; /** The sheet handle, which opens and closes the sheet. */ readonly sheet: () => void; /** A leaf was activated in the drawer, such as a file opened from the explorer. */ readonly chose: () => void; /** The drawer closed, by any means. */ readonly closed: () => void; /** A key bar key was pressed. */ readonly key: (action: KeyAction) => void;}
/** * The compact presentation's chrome, built once per shell: a header with a Files button, the * current title, and a Preview toggle; the editor area; a bottom sheet behind a handle; and a modal * drawer for the explorer. The shell decides what each region shows. */export class CompactChrome { readonly root = part("div", "compact"); readonly files = part("button", "files"); readonly title = part("span", "title"); readonly preview = part("button", "preview"); readonly main = part("div", "main"); readonly sheet = part("section", "sheet"); readonly handle = part("button", "sheet-handle"); readonly sheetBody = part("div", "sheet-body"); readonly drawer = part("dialog", "drawer"); readonly drawerBody = part("div", "drawer-body"); /** The sticky row above the on-screen keyboard. */ readonly keys = part("nib-toolbar", "keys"); #keyAction: (action: KeyAction) => void = () => {}; #language: Language | undefined;
constructor(actions: CompactActions) { const header = part("header", "header"); this.files.textContent = "☰"; this.files.ariaLabel = "Files"; this.files.ariaHasPopup = "dialog"; this.files.ariaExpanded = "false"; this.files.addEventListener("click", actions.files); this.title.ariaLive = "polite"; this.preview.textContent = "Preview"; this.preview.ariaPressed = "false"; this.preview.addEventListener("click", actions.preview); header.append(this.files, this.title, this.preview);
this.sheet.ariaLabel = "Panel"; this.handle.ariaExpanded = "false"; this.handle.ariaControlsElements = [this.sheetBody]; this.handle.addEventListener("click", actions.sheet); this.sheetBody.hidden = true; this.sheet.append(this.handle, this.sheetBody);
const drawerHeader = part("div", "drawer-header"); const heading = document.createElement("h2"); heading.textContent = "Files"; const close = part("button", "drawer-close"); close.textContent = "×"; close.ariaLabel = "Close files"; close.addEventListener("click", () => this.drawer.close()); drawerHeader.append(heading, close); this.drawer.ariaLabelledByElements = [heading]; // A tap on the backdrop lands on the dialog itself, outside its content. this.drawer.addEventListener("click", (event) => event.target === this.drawer && this.drawer.close()); this.drawer.addEventListener("activate", actions.chose); this.drawer.addEventListener("close", actions.closed); this.drawer.append(drawerHeader, this.drawerBody);
this.keys.hidden = true; this.#keyAction = actions.key; this.setLanguage("text"); // A tap must not move focus off the field, or the on-screen keyboard closes under the finger. this.keys.addEventListener("pointerdown", (event) => event.preventDefault()); this.keys.addEventListener("mousedown", (event) => event.preventDefault());
this.root.append(header, this.main, this.sheet, this.keys, this.drawer); }
/** Fills the key bar with `language`'s typing keys between the fixed ones. */ setLanguage(language: Language): void { if (language === this.#language) return; this.#language = language; this.keys.label = `${LANGUAGE_NAMES[language]} keys`; const fixed = (key: (typeof LEAD)[number]) => keyButton(key.label, key.name, () => this.#keyAction(key.action)); const typing = typingKeys(language).map((key) => keyButton(key.label, key.name, () => this.#keyAction({ type: "insert", text: key.text, caret: key.caret })), ); const dismiss = keyButton("⌄", "Hide keyboard", () => this.#keyAction({ type: "dismiss" })); dismiss.part.add("dismiss"); this.keys.replaceChildren(...LEAD.map(fixed), ...typing, ...TAIL.map(fixed), dismiss); this.keys.scrollLeft = 0; }}
function keyButton(label: string, name: string | undefined, onPress: () => void): HTMLButtonElement { const button = part("button", "key"); button.textContent = label; if (name) button.ariaLabel = name; button.addEventListener("click", onPress); return button;}
function part<K extends keyof HTMLElementTagNameMap>(tag: K, name: string): HTMLElementTagNameMap[K] { const el = document.createElement(tag); el.part = name; return el;}