Tons of work
This commit is contained in:
+1
-1
@@ -18,7 +18,7 @@
|
||||
"@parcel/transformer-webmanifest": "2.3.2",
|
||||
"@parcel/validator-typescript": "^2.3.2",
|
||||
"@types/events": "^3.0.0",
|
||||
"@types/lodash": "^4.14.179",
|
||||
"@types/jest": "^27.4.1",
|
||||
"@types/node": "^17.0.21",
|
||||
"@types/react": "^17.0.39",
|
||||
"@types/react-dom": "^17.0.11",
|
||||
|
||||
+30
-34
@@ -1,17 +1,12 @@
|
||||
// TODO:
|
||||
// Send state to client
|
||||
// Shape of editor.editorView.state.toJSON({"cursors": cursorField})
|
||||
// From there import it
|
||||
// EditorState.fromJSON(js, {extensions: cursorField}, {cursors: cursorField})
|
||||
|
||||
import {
|
||||
collab,
|
||||
getSyncedVersion,
|
||||
receiveUpdates,
|
||||
sendableUpdates,
|
||||
Update,
|
||||
} from "@codemirror/collab";
|
||||
import { RangeSetBuilder } from "@codemirror/rangeset";
|
||||
import { Text } from "@codemirror/state";
|
||||
import { Text, Transaction } from "@codemirror/state";
|
||||
import {
|
||||
Decoration,
|
||||
DecorationSet,
|
||||
@@ -20,27 +15,13 @@ import {
|
||||
ViewUpdate,
|
||||
WidgetType,
|
||||
} from "@codemirror/view";
|
||||
import { throttle } from "./util";
|
||||
import { Cursor, cursorEffect } from "./cursorEffect";
|
||||
import { RealtimeSpace, SpaceEventHandlers } from "./space";
|
||||
import { EventEmitter } from "./event";
|
||||
|
||||
const throttleInterval = 250;
|
||||
|
||||
const throttle = (func: () => void, limit: number) => {
|
||||
let timer: any = null;
|
||||
return function () {
|
||||
if (!timer) {
|
||||
timer = setTimeout(() => {
|
||||
func();
|
||||
timer = null;
|
||||
}, limit);
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
//@ts-ignore
|
||||
window.throttle = throttle;
|
||||
|
||||
export class Document {
|
||||
export class CollabDocument {
|
||||
text: Text;
|
||||
version: number;
|
||||
cursors: Map<string, Cursor>;
|
||||
@@ -81,24 +62,39 @@ class CursorWidget extends WidgetType {
|
||||
}
|
||||
}
|
||||
|
||||
export type CollabEvents = {
|
||||
cursorSnapshot: (pageName: string, cursors: Map<string, Cursor>) => void;
|
||||
};
|
||||
|
||||
export function collabExtension(
|
||||
pageName: string,
|
||||
clientID: string,
|
||||
doc: Document,
|
||||
space: RealtimeSpace,
|
||||
reloadCallback: () => void
|
||||
doc: CollabDocument,
|
||||
collabEmitter: EventEmitter<CollabEvents>,
|
||||
callbacks: {
|
||||
pushUpdates: (
|
||||
pageName: string,
|
||||
version: number,
|
||||
updates: readonly (Update & { origin: Transaction })[]
|
||||
) => Promise<boolean>;
|
||||
pullUpdates: (
|
||||
pageName: string,
|
||||
version: number
|
||||
) => Promise<readonly Update[]>;
|
||||
reload: () => void;
|
||||
}
|
||||
) {
|
||||
let plugin = ViewPlugin.fromClass(
|
||||
class {
|
||||
private pushing = false;
|
||||
private done = false;
|
||||
private failedPushes = 0;
|
||||
decorations: DecorationSet;
|
||||
private cursorPositions: Map<string, Cursor> = doc.cursors;
|
||||
decorations: DecorationSet;
|
||||
|
||||
throttledPush = throttle(() => this.push(), throttleInterval);
|
||||
|
||||
eventHandlers: Partial<SpaceEventHandlers> = {
|
||||
eventHandlers: Partial<CollabEvents> = {
|
||||
cursorSnapshot: (pageName, cursors) => {
|
||||
console.log("Received new cursor snapshot", cursors);
|
||||
this.cursorPositions = new Map(Object.entries(cursors));
|
||||
@@ -136,7 +132,7 @@ export function collabExtension(
|
||||
this.pull();
|
||||
}
|
||||
this.decorations = this.buildDecorations(view);
|
||||
space.on(this.eventHandlers);
|
||||
collabEmitter.on(this.eventHandlers);
|
||||
}
|
||||
|
||||
update(update: ViewUpdate) {
|
||||
@@ -184,7 +180,7 @@ export function collabExtension(
|
||||
console.log("Updates", updates);
|
||||
this.pushing = true;
|
||||
let version = getSyncedVersion(this.view.state);
|
||||
let success = await space.pushUpdates(pageName, version, updates);
|
||||
let success = await callbacks.pushUpdates(pageName, version, updates);
|
||||
this.pushing = false;
|
||||
|
||||
if (!success && !this.done) {
|
||||
@@ -192,7 +188,7 @@ export function collabExtension(
|
||||
if (this.failedPushes > 10) {
|
||||
// Not sure if 10 is a good number, but YOLO
|
||||
console.log("10 pushes failed, reloading");
|
||||
reloadCallback();
|
||||
callbacks.reload();
|
||||
return this.destroy();
|
||||
}
|
||||
console.log(
|
||||
@@ -213,7 +209,7 @@ export function collabExtension(
|
||||
async pull() {
|
||||
while (!this.done) {
|
||||
let version = getSyncedVersion(this.view.state);
|
||||
let updates = await space.pullUpdates(pageName, version);
|
||||
let updates = await callbacks.pullUpdates(pageName, version);
|
||||
let d = receiveUpdates(this.view.state, updates);
|
||||
// Pull out cursor updates and update local state
|
||||
for (let update of updates) {
|
||||
@@ -235,7 +231,7 @@ export function collabExtension(
|
||||
|
||||
destroy() {
|
||||
this.done = true;
|
||||
space.off(this.eventHandlers);
|
||||
collabEmitter.off(this.eventHandlers);
|
||||
}
|
||||
},
|
||||
{
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { PageMeta } from "../types";
|
||||
import { AppViewState, PageMeta } from "../types";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { faFileLines } from "@fortawesome/free-solid-svg-icons";
|
||||
import { Notification } from "../types";
|
||||
|
||||
function prettyName(s: string | undefined): string {
|
||||
if (!s) {
|
||||
@@ -10,10 +11,14 @@ function prettyName(s: string | undefined): string {
|
||||
}
|
||||
|
||||
export function TopBar({
|
||||
currentPage,
|
||||
pageName,
|
||||
status,
|
||||
notifications,
|
||||
onClick,
|
||||
}: {
|
||||
currentPage?: string;
|
||||
pageName?: string;
|
||||
status?: string;
|
||||
notifications: Notification[];
|
||||
onClick: () => void;
|
||||
}) {
|
||||
return (
|
||||
@@ -22,7 +27,12 @@ export function TopBar({
|
||||
<span className="icon">
|
||||
<FontAwesomeIcon icon={faFileLines} />
|
||||
</span>
|
||||
<span className="current-page">{prettyName(currentPage)}</span>
|
||||
<span className="current-page">{prettyName(pageName)}</span>
|
||||
<div className="status">
|
||||
{notifications.map((notification) => (
|
||||
<div key={notification.id}>{notification.message}</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
+72
-60
@@ -10,7 +10,13 @@ import { indentWithTab, standardKeymap } from "@codemirror/commands";
|
||||
import { history, historyKeymap } from "@codemirror/history";
|
||||
import { bracketMatching } from "@codemirror/matchbrackets";
|
||||
import { searchKeymap } from "@codemirror/search";
|
||||
import { EditorState, StateField, Transaction, Text } from "@codemirror/state";
|
||||
import {
|
||||
EditorSelection,
|
||||
EditorState,
|
||||
StateField,
|
||||
Text,
|
||||
Transaction,
|
||||
} from "@codemirror/state";
|
||||
import {
|
||||
drawSelection,
|
||||
dropCursor,
|
||||
@@ -19,30 +25,25 @@ import {
|
||||
KeyBinding,
|
||||
keymap,
|
||||
} from "@codemirror/view";
|
||||
|
||||
import { debounce } from "lodash";
|
||||
|
||||
// import { debounce } from "lodash";
|
||||
import React, { useEffect, useReducer } from "react";
|
||||
import ReactDOM from "react-dom";
|
||||
import coreManifest from "./generated/core.plug.json";
|
||||
|
||||
// @ts-ignore
|
||||
window.coreManifest = coreManifest;
|
||||
import { Plug, System } from "../../plugbox/src/runtime";
|
||||
import { WebworkerSandbox } from "../../plugbox/src/worker_sandbox";
|
||||
import { AppEvent, AppEventDispatcher, ClickEvent } from "./app_event";
|
||||
import { collabExtension, CollabDocument } from "./collab";
|
||||
import * as commands from "./commands";
|
||||
import { CommandPalette } from "./components/command_palette";
|
||||
import { PageNavigator } from "./components/page_navigator";
|
||||
import { StatusBar } from "./components/status_bar";
|
||||
import { TopBar } from "./components/top_bar";
|
||||
import { Cursor } from "./cursorEffect";
|
||||
import coreManifest from "./generated/core.plug.json";
|
||||
import { Indexer } from "./indexer";
|
||||
import { lineWrapper } from "./lineWrapper";
|
||||
import { markdown } from "./markdown";
|
||||
import { IPageNavigator, PathPageNavigator } from "./navigator";
|
||||
import customMarkDown from "./parser";
|
||||
import { System } from "../../plugbox/src/runtime";
|
||||
import { Plug } from "../../plugbox/src/runtime";
|
||||
import { slashCommandRegexp } from "./types";
|
||||
|
||||
import reducer from "./reducer";
|
||||
import { smartQuoteKeymap } from "./smart_quotes";
|
||||
import { RealtimeSpace } from "./space";
|
||||
@@ -57,15 +58,9 @@ import {
|
||||
AppViewState,
|
||||
initialViewState,
|
||||
NuggetHook,
|
||||
PageMeta,
|
||||
slashCommandRegexp,
|
||||
} from "./types";
|
||||
import { safeRun } from "./util";
|
||||
|
||||
import { collabExtension } from "./collab";
|
||||
|
||||
import { Document } from "./collab";
|
||||
import { EditorSelection } from "@codemirror/state";
|
||||
import { Cursor } from "./cursorEffect";
|
||||
import { safeRun, throttle } from "./util";
|
||||
|
||||
class PageState {
|
||||
scrollTop: number;
|
||||
@@ -77,8 +72,6 @@ class PageState {
|
||||
}
|
||||
}
|
||||
|
||||
const watchInterval = 5000;
|
||||
|
||||
export class Editor implements AppEventDispatcher {
|
||||
editorView?: EditorView;
|
||||
viewState: AppViewState;
|
||||
@@ -103,18 +96,20 @@ export class Editor implements AppEventDispatcher {
|
||||
this.editorView = new EditorView({
|
||||
state: this.createEditorState(
|
||||
"",
|
||||
new Document(Text.of([""]), 0, new Map<string, Cursor>())
|
||||
new CollabDocument(Text.of([""]), 0, new Map<string, Cursor>())
|
||||
),
|
||||
parent: document.getElementById("editor")!,
|
||||
});
|
||||
this.pageNavigator = new PathPageNavigator();
|
||||
this.indexer = new Indexer("page-index", space);
|
||||
|
||||
this.indexCurrentPageDebounced = debounce(this.indexCurrentPage, 2000);
|
||||
this.indexCurrentPageDebounced = throttle(
|
||||
this.indexCurrentPage.bind(this),
|
||||
2000
|
||||
);
|
||||
}
|
||||
|
||||
async init() {
|
||||
// await this.loadPageList();
|
||||
await this.loadPlugs();
|
||||
this.focus();
|
||||
|
||||
@@ -125,16 +120,6 @@ export class Editor implements AppEventDispatcher {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.currentPage) {
|
||||
let pageState = this.openPages.get(this.currentPage)!;
|
||||
if (pageState) {
|
||||
pageState.selection = this.editorView!.state.selection;
|
||||
pageState.scrollTop = this.editorView!.scrollDOM.scrollTop;
|
||||
}
|
||||
|
||||
this.space.closePage(this.currentPage);
|
||||
}
|
||||
|
||||
await this.loadPage(pageName);
|
||||
});
|
||||
|
||||
@@ -142,12 +127,14 @@ export class Editor implements AppEventDispatcher {
|
||||
connect: () => {
|
||||
if (this.currentPage) {
|
||||
console.log("Connected to socket, fetch fresh?");
|
||||
this.flashNotification("Reconnected, reloading page");
|
||||
this.reloadPage();
|
||||
}
|
||||
},
|
||||
pageChanged: (meta) => {
|
||||
if (this.currentPage === meta.name) {
|
||||
console.log("page changed on disk, reloading");
|
||||
console.log("Page changed on disk, reloading");
|
||||
this.flashNotification("Page changed on disk, reloading");
|
||||
this.reloadPage();
|
||||
}
|
||||
},
|
||||
@@ -164,6 +151,24 @@ export class Editor implements AppEventDispatcher {
|
||||
}
|
||||
}
|
||||
|
||||
flashNotification(message: string) {
|
||||
let id = Math.floor(Math.random() * 1000000);
|
||||
this.viewDispatch({
|
||||
type: "show-notification",
|
||||
notification: {
|
||||
id: id,
|
||||
message: message,
|
||||
date: new Date(),
|
||||
},
|
||||
});
|
||||
setTimeout(() => {
|
||||
this.viewDispatch({
|
||||
type: "dismiss-notification",
|
||||
id: id,
|
||||
});
|
||||
}, 2000);
|
||||
}
|
||||
|
||||
async loadPlugs() {
|
||||
const system = new System<NuggetHook>();
|
||||
system.registerSyscalls(
|
||||
@@ -174,7 +179,11 @@ export class Editor implements AppEventDispatcher {
|
||||
);
|
||||
|
||||
console.log("Now loading core plug");
|
||||
let mainPlug = await system.load("core", coreManifest);
|
||||
let mainPlug = await system.load(
|
||||
"core",
|
||||
coreManifest,
|
||||
new WebworkerSandbox(system)
|
||||
);
|
||||
this.plugs.push(mainPlug);
|
||||
this.editorCommands = new Map<string, AppCommand>();
|
||||
for (let plug of this.plugs) {
|
||||
@@ -217,7 +226,7 @@ export class Editor implements AppEventDispatcher {
|
||||
return this.viewState.currentPage;
|
||||
}
|
||||
|
||||
createEditorState(pageName: string, doc: Document): EditorState {
|
||||
createEditorState(pageName: string, doc: CollabDocument): EditorState {
|
||||
const editor = this;
|
||||
let commandKeyBindings: KeyBinding[] = [];
|
||||
for (let def of this.editorCommands.values()) {
|
||||
@@ -243,17 +252,14 @@ export class Editor implements AppEventDispatcher {
|
||||
history(),
|
||||
drawSelection(),
|
||||
dropCursor(),
|
||||
// indentOnInput(),
|
||||
customMarkdownStyle,
|
||||
bracketMatching(),
|
||||
closeBrackets(),
|
||||
collabExtension(
|
||||
pageName,
|
||||
this.space.socket.id,
|
||||
doc,
|
||||
this.space,
|
||||
this.reloadPage.bind(this)
|
||||
),
|
||||
collabExtension(pageName, this.space.socket.id, doc, this.space, {
|
||||
pushUpdates: this.space.pushUpdates.bind(this.space),
|
||||
pullUpdates: this.space.pullUpdates.bind(this.space),
|
||||
reload: this.reloadPage.bind(this),
|
||||
}),
|
||||
autocompletion({
|
||||
override: [
|
||||
this.plugCompleter.bind(this),
|
||||
@@ -428,13 +434,26 @@ export class Editor implements AppEventDispatcher {
|
||||
}
|
||||
|
||||
async loadPage(pageName: string) {
|
||||
let doc = await this.space.openPage(pageName);
|
||||
let editorState = this.createEditorState(pageName, doc);
|
||||
let pageState = this.openPages.get(pageName);
|
||||
const editorView = this.editorView;
|
||||
if (!editorView) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Persist current page state and nicely close page
|
||||
if (this.currentPage) {
|
||||
let pageState = this.openPages.get(this.currentPage)!;
|
||||
if (pageState) {
|
||||
pageState.selection = this.editorView!.state.selection;
|
||||
pageState.scrollTop = this.editorView!.scrollDOM.scrollTop;
|
||||
}
|
||||
|
||||
this.space.closePage(this.currentPage);
|
||||
}
|
||||
|
||||
// Fetch next page to open
|
||||
let doc = await this.space.openPage(pageName);
|
||||
let editorState = this.createEditorState(pageName, doc);
|
||||
let pageState = this.openPages.get(pageName);
|
||||
editorView.setState(editorState);
|
||||
if (!pageState) {
|
||||
pageState = new PageState(0, editorState.selection);
|
||||
@@ -444,7 +463,7 @@ export class Editor implements AppEventDispatcher {
|
||||
});
|
||||
} else {
|
||||
// Restore state
|
||||
console.log("Restoring selection state");
|
||||
console.log("Restoring selection state", pageState.selection);
|
||||
editorView.dispatch({
|
||||
selection: pageState.selection,
|
||||
});
|
||||
@@ -456,15 +475,8 @@ export class Editor implements AppEventDispatcher {
|
||||
name: pageName,
|
||||
});
|
||||
|
||||
// let indexerPageMeta = await this.indexer.getPageIndexPageMeta(pageName);
|
||||
// if (
|
||||
// (indexerPageMeta &&
|
||||
// doc.meta.lastModified.getTime() !==
|
||||
// indexerPageMeta.lastModified.getTime()) ||
|
||||
// !indexerPageMeta
|
||||
// ) {
|
||||
// TODO: Check if indexing is required?
|
||||
await this.indexCurrentPage();
|
||||
// }
|
||||
}
|
||||
|
||||
ViewComponent(): React.ReactElement {
|
||||
@@ -513,13 +525,13 @@ export class Editor implements AppEventDispatcher {
|
||||
/>
|
||||
)}
|
||||
<TopBar
|
||||
currentPage={viewState.currentPage}
|
||||
pageName={viewState.currentPage}
|
||||
notifications={viewState.notifications}
|
||||
onClick={() => {
|
||||
dispatch({ type: "start-navigate" });
|
||||
}}
|
||||
/>
|
||||
<div id="editor"></div>
|
||||
<StatusBar editorView={this.editorView} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
export abstract class EventEmitter<HandlerT> {
|
||||
private handlers: Partial<HandlerT>[] = [];
|
||||
|
||||
on(handlers: Partial<HandlerT>) {
|
||||
this.handlers.push(handlers);
|
||||
}
|
||||
|
||||
off(handlers: Partial<HandlerT>) {
|
||||
this.handlers = this.handlers.filter((h) => h !== handlers);
|
||||
}
|
||||
|
||||
emit(eventName: keyof HandlerT, ...args: any[]) {
|
||||
for (let handler of this.handlers) {
|
||||
let fn: any = handler[eventName];
|
||||
if (fn) {
|
||||
fn(...args);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -48,6 +48,16 @@ export default function reducer(
|
||||
...state,
|
||||
commands: action.commands,
|
||||
};
|
||||
case "show-notification":
|
||||
return {
|
||||
...state,
|
||||
notifications: [action.notification, ...state.notifications],
|
||||
};
|
||||
case "dismiss-notification":
|
||||
return {
|
||||
...state,
|
||||
notifications: state.notifications.filter((n) => n.id !== action.id),
|
||||
};
|
||||
}
|
||||
return state;
|
||||
}
|
||||
|
||||
+14
-40
@@ -3,8 +3,9 @@ import { Socket } from "socket.io-client";
|
||||
import { Update } from "@codemirror/collab";
|
||||
import { Transaction, Text, ChangeSet } from "@codemirror/state";
|
||||
|
||||
import { Document } from "./collab";
|
||||
import { CollabEvents, CollabDocument } from "./collab";
|
||||
import { Cursor, cursorEffect } from "./cursorEffect";
|
||||
import { EventEmitter } from "./event";
|
||||
|
||||
export interface Space {
|
||||
listPages(): Promise<PageMeta[]>;
|
||||
@@ -14,43 +15,15 @@ export interface Space {
|
||||
getPageMeta(name: string): Promise<PageMeta>;
|
||||
}
|
||||
|
||||
export type SpaceEventHandlers = {
|
||||
export type SpaceEvents = {
|
||||
connect: () => void;
|
||||
cursorSnapshot: (
|
||||
pageName: string,
|
||||
cursors: { [key: string]: Cursor }
|
||||
) => void;
|
||||
pageCreated: (meta: PageMeta) => void;
|
||||
pageChanged: (meta: PageMeta) => void;
|
||||
pageDeleted: (name: string) => void;
|
||||
pageListUpdated: (pages: Set<PageMeta>) => void;
|
||||
};
|
||||
} & CollabEvents;
|
||||
|
||||
abstract class EventEmitter<HandlerT> {
|
||||
private handlers: Partial<HandlerT>[] = [];
|
||||
|
||||
on(handlers: Partial<HandlerT>) {
|
||||
this.handlers.push(handlers);
|
||||
}
|
||||
|
||||
off(handlers: Partial<HandlerT>) {
|
||||
this.handlers = this.handlers.filter((h) => h !== handlers);
|
||||
}
|
||||
|
||||
emit(eventName: keyof HandlerT, ...args: any[]) {
|
||||
for (let handler of this.handlers) {
|
||||
let fn: any = handler[eventName];
|
||||
if (fn) {
|
||||
fn(...args);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export class RealtimeSpace
|
||||
extends EventEmitter<SpaceEventHandlers>
|
||||
implements Space
|
||||
{
|
||||
export class RealtimeSpace extends EventEmitter<SpaceEvents> implements Space {
|
||||
socket: Socket;
|
||||
reqId = 0;
|
||||
allPages = new Set<PageMeta>();
|
||||
@@ -67,7 +40,7 @@ export class RealtimeSpace
|
||||
"pageDeleted",
|
||||
].forEach((eventName) => {
|
||||
socket.on(eventName, (...args) => {
|
||||
this.emit(eventName as keyof SpaceEventHandlers, ...args);
|
||||
this.emit(eventName as keyof SpaceEvents, ...args);
|
||||
});
|
||||
});
|
||||
this.wsCall("listPages").then((pages) => {
|
||||
@@ -133,18 +106,19 @@ export class RealtimeSpace
|
||||
return Array.from(this.allPages);
|
||||
}
|
||||
|
||||
async openPage(name: string): Promise<Document> {
|
||||
async openPage(name: string): Promise<CollabDocument> {
|
||||
this.reqId++;
|
||||
let pageJSON = await this.wsCall("openPage", name);
|
||||
let cursors = new Map<string, Cursor>();
|
||||
for (let p in pageJSON.cursors) {
|
||||
cursors.set(p, pageJSON.cursors[p]);
|
||||
}
|
||||
return new Document(Text.of(pageJSON.text), pageJSON.version, cursors);
|
||||
|
||||
return new CollabDocument(
|
||||
Text.of(pageJSON.text),
|
||||
pageJSON.version,
|
||||
new Map(Object.entries(pageJSON.cursors))
|
||||
);
|
||||
}
|
||||
|
||||
async closePage(name: string): Promise<void> {
|
||||
this.socket!.emit("closePage", name);
|
||||
this.socket.emit("closePage", name);
|
||||
}
|
||||
|
||||
async readPage(name: string): Promise<{ text: string; meta: PageMeta }> {
|
||||
|
||||
+31
-24
@@ -32,34 +32,41 @@ body {
|
||||
max-width: 800px;
|
||||
font-size: 28px;
|
||||
margin: auto;
|
||||
}
|
||||
|
||||
.current-page {
|
||||
font-family: var(--ui-font);
|
||||
font-weight: bold;
|
||||
}
|
||||
.status {
|
||||
float: right;
|
||||
border: rgb(41, 41, 41) 1px solid;
|
||||
border-radius: 5px;
|
||||
padding: 3px;
|
||||
font-size: 14px;
|
||||
}
|
||||
.current-page {
|
||||
font-family: var(--ui-font);
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.icon {
|
||||
padding-left: 5px;
|
||||
padding-right: 10px;
|
||||
.icon {
|
||||
padding-left: 5px;
|
||||
padding-right: 10px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#bottom {
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 20px;
|
||||
background-color: rgb(232, 232, 232);
|
||||
color: rgb(79, 78, 78);
|
||||
border-top: rgb(186, 186, 186) 1px solid;
|
||||
margin: 0;
|
||||
padding: 5px 10px;
|
||||
font-family: var(--ui-font);
|
||||
font-size: 0.9em;
|
||||
text-align: right;
|
||||
}
|
||||
// #bottom {
|
||||
// position: fixed;
|
||||
// bottom: 0;
|
||||
// left: 0;
|
||||
// right: 0;
|
||||
// height: 20px;
|
||||
// background-color: rgb(232, 232, 232);
|
||||
// color: rgb(79, 78, 78);
|
||||
// border-top: rgb(186, 186, 186) 1px solid;
|
||||
// margin: 0;
|
||||
// padding: 5px 10px;
|
||||
// font-family: var(--ui-font);
|
||||
// font-size: 0.9em;
|
||||
// text-align: right;
|
||||
// }
|
||||
|
||||
// body.keyboard #bottom {
|
||||
// bottom: 250px;
|
||||
@@ -68,7 +75,7 @@ body {
|
||||
#editor {
|
||||
position: absolute;
|
||||
top: 55px;
|
||||
bottom: 30px;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
overflow-y: hidden;
|
||||
|
||||
+11
-1
@@ -35,12 +35,19 @@ export interface CommandDef {
|
||||
slashCommand?: string;
|
||||
}
|
||||
|
||||
export type Notification = {
|
||||
id: number;
|
||||
message: string;
|
||||
date: Date;
|
||||
};
|
||||
|
||||
export type AppViewState = {
|
||||
currentPage?: string;
|
||||
showPageNavigator: boolean;
|
||||
showCommandPalette: boolean;
|
||||
allPages: Set<PageMeta>;
|
||||
commands: Map<string, AppCommand>;
|
||||
notifications: Notification[];
|
||||
};
|
||||
|
||||
export const initialViewState: AppViewState = {
|
||||
@@ -48,6 +55,7 @@ export const initialViewState: AppViewState = {
|
||||
showCommandPalette: false,
|
||||
allPages: new Set(),
|
||||
commands: new Map(),
|
||||
notifications: [],
|
||||
};
|
||||
|
||||
export type Action =
|
||||
@@ -57,4 +65,6 @@ export type Action =
|
||||
| { type: "stop-navigate" }
|
||||
| { type: "update-commands"; commands: Map<string, AppCommand> }
|
||||
| { type: "show-palette" }
|
||||
| { type: "hide-palette" };
|
||||
| { type: "hide-palette" }
|
||||
| { type: "show-notification"; notification: Notification }
|
||||
| { type: "dismiss-notification"; id: number };
|
||||
|
||||
@@ -17,3 +17,15 @@ export function safeRun(fn: () => Promise<void>) {
|
||||
export function isMacLike() {
|
||||
return /(Mac|iPhone|iPod|iPad)/i.test(navigator.platform);
|
||||
}
|
||||
|
||||
export function throttle(func: () => void, limit: number) {
|
||||
let timer: any = null;
|
||||
return function () {
|
||||
if (!timer) {
|
||||
timer = setTimeout(() => {
|
||||
func();
|
||||
timer = null;
|
||||
}, limit);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
+62
-5
@@ -979,10 +979,13 @@
|
||||
resolved "https://registry.yarnpkg.com/@types/events/-/events-3.0.0.tgz#2862f3f58a9a7f7c3e78d79f130dd4d71c25c2a7"
|
||||
integrity sha512-EaObqwIvayI5a8dCzhFrjKzVwKLxjoG9T6Ppd5CEo07LRKfQ8Yokw54r5+Wq7FaBQ+yXRvQAYPrHwya1/UFt9g==
|
||||
|
||||
"@types/lodash@^4.14.179":
|
||||
version "4.14.179"
|
||||
resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.179.tgz#490ec3288088c91295780237d2497a3aa9dfb5c5"
|
||||
integrity sha512-uwc1x90yCKqGcIOAT6DwOSuxnrAbpkdPsUOZtwrXb4D/6wZs+6qG7QnIawDuZWg0sWpxl+ltIKCaLoMlna678w==
|
||||
"@types/jest@^27.4.1":
|
||||
version "27.4.1"
|
||||
resolved "https://registry.yarnpkg.com/@types/jest/-/jest-27.4.1.tgz#185cbe2926eaaf9662d340cc02e548ce9e11ab6d"
|
||||
integrity sha512-23iPJADSmicDVrWk+HT58LMJtzLAnB2AgIzplQuq/bSrGaxCrlvRFjGbXmamnnk/mAmCdLStiGqggu28ocUyiw==
|
||||
dependencies:
|
||||
jest-matcher-utils "^27.0.0"
|
||||
pretty-format "^27.0.0"
|
||||
|
||||
"@types/node@^17.0.21":
|
||||
version "17.0.21"
|
||||
@@ -1025,6 +1028,11 @@ abortcontroller-polyfill@^1.1.9:
|
||||
resolved "https://registry.yarnpkg.com/abortcontroller-polyfill/-/abortcontroller-polyfill-1.7.3.tgz#1b5b487bd6436b5b764fd52a612509702c3144b5"
|
||||
integrity sha512-zetDJxd89y3X99Kvo4qFx8GKlt6GsvN3UcRZHwU6iFA/0KiOmhkTVhe8oRoTBiTVPZu09x3vCra47+w8Yz1+2Q==
|
||||
|
||||
ansi-regex@^5.0.1:
|
||||
version "5.0.1"
|
||||
resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-5.0.1.tgz#082cb2c89c9fe8659a311a53bd6a4dc5301db304"
|
||||
integrity sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==
|
||||
|
||||
ansi-styles@^3.2.1:
|
||||
version "3.2.1"
|
||||
resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-3.2.1.tgz#41fbb20243e50b12be0f04b8dedbf07520ce841d"
|
||||
@@ -1039,6 +1047,11 @@ ansi-styles@^4.1.0:
|
||||
dependencies:
|
||||
color-convert "^2.0.1"
|
||||
|
||||
ansi-styles@^5.0.0:
|
||||
version "5.2.0"
|
||||
resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-5.2.0.tgz#07449690ad45777d1924ac2abb2fc8895dba836b"
|
||||
integrity sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==
|
||||
|
||||
anymatch@~3.1.2:
|
||||
version "3.1.2"
|
||||
resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-3.1.2.tgz#c0557c096af32f106198f4f4e2a383537e378716"
|
||||
@@ -1241,7 +1254,7 @@ chalk@^2.0.0:
|
||||
escape-string-regexp "^1.0.5"
|
||||
supports-color "^5.3.0"
|
||||
|
||||
chalk@^4.1.0:
|
||||
chalk@^4.0.0, chalk@^4.1.0:
|
||||
version "4.1.2"
|
||||
resolved "https://registry.yarnpkg.com/chalk/-/chalk-4.1.2.tgz#aac4e2b7734a740867aeb16bf02aad556a1e7a01"
|
||||
integrity sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==
|
||||
@@ -1514,6 +1527,11 @@ dexie@^3.2.1:
|
||||
resolved "https://registry.yarnpkg.com/dexie/-/dexie-3.2.1.tgz#ef21456d725e700c1ab7ac4307896e4fdabaf753"
|
||||
integrity sha512-Y8oz3t2XC9hvjkP35B5I8rUkKKwM36GGRjWQCMjzIYScg7W+GHKDXobSYswkisW7CxL1/tKQtggMDsiWqDUc1g==
|
||||
|
||||
diff-sequences@^27.5.1:
|
||||
version "27.5.1"
|
||||
resolved "https://registry.yarnpkg.com/diff-sequences/-/diff-sequences-27.5.1.tgz#eaecc0d327fd68c8d9672a1e64ab8dccb2ef5327"
|
||||
integrity sha512-k1gCAXAsNgLwEL+Y8Wvl+M6oEFj5bgazfZULpS5CneoPPXRaCCW7dm+q21Ky2VEE5X+VeRDBVg1Pcvvsr4TtNQ==
|
||||
|
||||
diffie-hellman@^5.0.0:
|
||||
version "5.0.3"
|
||||
resolved "https://registry.yarnpkg.com/diffie-hellman/-/diffie-hellman-5.0.3.tgz#40e8ee98f55a2149607146921c63e1ae5f3d2875"
|
||||
@@ -2003,6 +2021,31 @@ is-weakref@^1.0.1:
|
||||
dependencies:
|
||||
call-bind "^1.0.2"
|
||||
|
||||
jest-diff@^27.5.1:
|
||||
version "27.5.1"
|
||||
resolved "https://registry.yarnpkg.com/jest-diff/-/jest-diff-27.5.1.tgz#a07f5011ac9e6643cf8a95a462b7b1ecf6680def"
|
||||
integrity sha512-m0NvkX55LDt9T4mctTEgnZk3fmEg3NRYutvMPWM/0iPnkFj2wIeF45O1718cMSOFO1vINkqmxqD8vE37uTEbqw==
|
||||
dependencies:
|
||||
chalk "^4.0.0"
|
||||
diff-sequences "^27.5.1"
|
||||
jest-get-type "^27.5.1"
|
||||
pretty-format "^27.5.1"
|
||||
|
||||
jest-get-type@^27.5.1:
|
||||
version "27.5.1"
|
||||
resolved "https://registry.yarnpkg.com/jest-get-type/-/jest-get-type-27.5.1.tgz#3cd613c507b0f7ace013df407a1c1cd578bcb4f1"
|
||||
integrity sha512-2KY95ksYSaK7DMBWQn6dQz3kqAf3BB64y2udeG+hv4KfSOb9qwcYQstTJc1KCbsix+wLZWZYN8t7nwX3GOBLRw==
|
||||
|
||||
jest-matcher-utils@^27.0.0:
|
||||
version "27.5.1"
|
||||
resolved "https://registry.yarnpkg.com/jest-matcher-utils/-/jest-matcher-utils-27.5.1.tgz#9c0cdbda8245bc22d2331729d1091308b40cf8ab"
|
||||
integrity sha512-z2uTx/T6LBaCoNWNFWwChLBKYxTMcGBRjAt+2SbP929/Fflb9aa5LGma654Rz8z9HLxsrUaYzxE9T/EFIL/PAw==
|
||||
dependencies:
|
||||
chalk "^4.0.0"
|
||||
jest-diff "^27.5.1"
|
||||
jest-get-type "^27.5.1"
|
||||
pretty-format "^27.5.1"
|
||||
|
||||
"js-tokens@^3.0.0 || ^4.0.0", js-tokens@^4.0.0:
|
||||
version "4.0.0"
|
||||
resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499"
|
||||
@@ -2544,6 +2587,15 @@ posthtml@^0.16.4, posthtml@^0.16.5:
|
||||
posthtml-parser "^0.10.0"
|
||||
posthtml-render "^3.0.0"
|
||||
|
||||
pretty-format@^27.0.0, pretty-format@^27.5.1:
|
||||
version "27.5.1"
|
||||
resolved "https://registry.yarnpkg.com/pretty-format/-/pretty-format-27.5.1.tgz#2181879fdea51a7a5851fb39d920faa63f01d88e"
|
||||
integrity sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==
|
||||
dependencies:
|
||||
ansi-regex "^5.0.1"
|
||||
ansi-styles "^5.0.0"
|
||||
react-is "^17.0.1"
|
||||
|
||||
prop-types@^15.8.1:
|
||||
version "15.8.1"
|
||||
resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.8.1.tgz#67d87bf1a694f48435cf332c24af10214a3140b5"
|
||||
@@ -2609,6 +2661,11 @@ react-is@^16.13.1:
|
||||
resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4"
|
||||
integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==
|
||||
|
||||
react-is@^17.0.1:
|
||||
version "17.0.2"
|
||||
resolved "https://registry.yarnpkg.com/react-is/-/react-is-17.0.2.tgz#e691d4a8e9c789365655539ab372762b0efb54f0"
|
||||
integrity sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==
|
||||
|
||||
react-refresh@^0.9.0:
|
||||
version "0.9.0"
|
||||
resolved "https://registry.yarnpkg.com/react-refresh/-/react-refresh-0.9.0.tgz#71863337adc3e5c2f8a6bfddd12ae3bfe32aafbf"
|
||||
|
||||
Reference in New Issue
Block a user