Move live query and template rendering outside of iframe
This commit is contained in:
parent
a205178ff0
commit
48e147d0b2
@ -3,6 +3,7 @@ functions:
|
|||||||
queryWidget:
|
queryWidget:
|
||||||
path: query.ts:widget
|
path: query.ts:widget
|
||||||
codeWidget: query
|
codeWidget: query
|
||||||
|
renderMode: markdown
|
||||||
|
|
||||||
lintQuery:
|
lintQuery:
|
||||||
path: query.ts:lintQuery
|
path: query.ts:lintQuery
|
||||||
@ -12,6 +13,7 @@ functions:
|
|||||||
templateWidget:
|
templateWidget:
|
||||||
path: template.ts:widget
|
path: template.ts:widget
|
||||||
codeWidget: template
|
codeWidget: template
|
||||||
|
renderMode: markdown
|
||||||
|
|
||||||
queryComplete:
|
queryComplete:
|
||||||
path: complete.ts:queryComplete
|
path: complete.ts:queryComplete
|
||||||
|
@ -46,6 +46,7 @@ import { DataStoreSpacePrimitives } from "../common/spaces/datastore_space_primi
|
|||||||
import {
|
import {
|
||||||
EncryptedSpacePrimitives,
|
EncryptedSpacePrimitives,
|
||||||
} from "../common/spaces/encrypted_space_primitives.ts";
|
} from "../common/spaces/encrypted_space_primitives.ts";
|
||||||
|
import { LimitedMap } from "../common/limited_map.ts";
|
||||||
const frontMatterRegex = /^---\n(([^\n]|\n)*?)---\n/;
|
const frontMatterRegex = /^---\n(([^\n]|\n)*?)---\n/;
|
||||||
|
|
||||||
const autoSaveInterval = 1000;
|
const autoSaveInterval = 1000;
|
||||||
@ -187,6 +188,13 @@ export class Client {
|
|||||||
// Load settings
|
// Load settings
|
||||||
this.settings = await ensureSettingsAndIndex(localSpacePrimitives);
|
this.settings = await ensureSettingsAndIndex(localSpacePrimitives);
|
||||||
|
|
||||||
|
// Load widget cache
|
||||||
|
this.widgetCache = new LimitedMap(
|
||||||
|
100,
|
||||||
|
await this.stateDataStore.get(["cache", "widgets"]) ||
|
||||||
|
{},
|
||||||
|
);
|
||||||
|
|
||||||
// Pinging a remote space to ensure we're authenticated properly, if not will result in a redirect to auth page
|
// Pinging a remote space to ensure we're authenticated properly, if not will result in a redirect to auth page
|
||||||
try {
|
try {
|
||||||
await this.httpSpacePrimitives.ping();
|
await this.httpSpacePrimitives.ping();
|
||||||
@ -1004,4 +1012,28 @@ export class Client {
|
|||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private widgetCache = new LimitedMap<WidgetCacheItem>(100);
|
||||||
|
|
||||||
|
debouncedWidgetCacheFlush = throttle(() => {
|
||||||
|
this.stateDataStore.set(["cache", "widgets"], this.widgetCache.toJSON())
|
||||||
|
.catch(
|
||||||
|
console.error,
|
||||||
|
);
|
||||||
|
console.log("Flushed widget cache to store");
|
||||||
|
}, 5000);
|
||||||
|
|
||||||
|
setWidgetCache(key: string, height: number, html: string) {
|
||||||
|
this.widgetCache.set(key, { height, html });
|
||||||
|
this.debouncedWidgetCacheFlush();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getWidgetCache(key: string): WidgetCacheItem | undefined {
|
||||||
|
return this.widgetCache.get(key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type WidgetCacheItem = {
|
||||||
|
height: number;
|
||||||
|
html: string;
|
||||||
|
};
|
||||||
|
@ -1,79 +1,12 @@
|
|||||||
import { WidgetContent } from "../../plug-api/app_event.ts";
|
import { Decoration, EditorState, syntaxTree } from "../deps.ts";
|
||||||
import { Decoration, EditorState, syntaxTree, WidgetType } from "../deps.ts";
|
|
||||||
import type { Client } from "../client.ts";
|
import type { Client } from "../client.ts";
|
||||||
import {
|
import {
|
||||||
decoratorStateField,
|
decoratorStateField,
|
||||||
invisibleDecoration,
|
invisibleDecoration,
|
||||||
isCursorInRange,
|
isCursorInRange,
|
||||||
} from "./util.ts";
|
} from "./util.ts";
|
||||||
import { createWidgetSandboxIFrame } from "../components/widget_sandbox_iframe.ts";
|
import { MarkdownWidget } from "./markdown_widget.ts";
|
||||||
import type { CodeWidgetCallback } from "$sb/types.ts";
|
import { IFrameWidget } from "./iframe_widget.ts";
|
||||||
|
|
||||||
class IFrameWidget extends WidgetType {
|
|
||||||
iframe?: HTMLIFrameElement;
|
|
||||||
|
|
||||||
constructor(
|
|
||||||
readonly from: number,
|
|
||||||
readonly to: number,
|
|
||||||
readonly client: Client,
|
|
||||||
readonly bodyText: string,
|
|
||||||
readonly codeWidgetCallback: CodeWidgetCallback,
|
|
||||||
) {
|
|
||||||
super();
|
|
||||||
}
|
|
||||||
|
|
||||||
toDOM(): HTMLElement {
|
|
||||||
const from = this.from;
|
|
||||||
const iframe = createWidgetSandboxIFrame(
|
|
||||||
this.client,
|
|
||||||
this.bodyText,
|
|
||||||
this.codeWidgetCallback(this.bodyText, this.client.currentPage!),
|
|
||||||
(message) => {
|
|
||||||
switch (message.type) {
|
|
||||||
case "blur":
|
|
||||||
this.client.editorView.dispatch({
|
|
||||||
selection: { anchor: from },
|
|
||||||
});
|
|
||||||
this.client.focus();
|
|
||||||
|
|
||||||
break;
|
|
||||||
case "reload":
|
|
||||||
this.codeWidgetCallback(this.bodyText, this.client.currentPage!)
|
|
||||||
.then(
|
|
||||||
(widgetContent: WidgetContent) => {
|
|
||||||
iframe.contentWindow!.postMessage({
|
|
||||||
type: "html",
|
|
||||||
html: widgetContent.html,
|
|
||||||
script: widgetContent.script,
|
|
||||||
theme:
|
|
||||||
document.getElementsByTagName("html")[0].dataset.theme,
|
|
||||||
});
|
|
||||||
},
|
|
||||||
);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
const estimatedHeight = this.estimatedHeight;
|
|
||||||
iframe.height = `${estimatedHeight}px`;
|
|
||||||
|
|
||||||
return iframe;
|
|
||||||
}
|
|
||||||
|
|
||||||
get estimatedHeight(): number {
|
|
||||||
const cachedHeight = this.client.space.getCachedWidgetHeight(this.bodyText);
|
|
||||||
// console.log("Calling estimated height", cachedHeight);
|
|
||||||
return cachedHeight > 0 ? cachedHeight : 150;
|
|
||||||
}
|
|
||||||
|
|
||||||
eq(other: WidgetType): boolean {
|
|
||||||
return (
|
|
||||||
other instanceof IFrameWidget &&
|
|
||||||
other.bodyText === this.bodyText
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function fencedCodePlugin(editor: Client) {
|
export function fencedCodePlugin(editor: Client) {
|
||||||
return decoratorStateField((state: EditorState) => {
|
return decoratorStateField((state: EditorState) => {
|
||||||
@ -87,6 +20,9 @@ export function fencedCodePlugin(editor: Client) {
|
|||||||
const codeWidgetCallback = editor.system.codeWidgetHook
|
const codeWidgetCallback = editor.system.codeWidgetHook
|
||||||
.codeWidgetCallbacks
|
.codeWidgetCallbacks
|
||||||
.get(lang);
|
.get(lang);
|
||||||
|
const renderMode = editor.system.codeWidgetHook.codeWidgetModes.get(
|
||||||
|
lang,
|
||||||
|
);
|
||||||
if (codeWidgetCallback) {
|
if (codeWidgetCallback) {
|
||||||
// We got a custom renderer!
|
// We got a custom renderer!
|
||||||
const lineStrings = text.split("\n");
|
const lineStrings = text.split("\n");
|
||||||
@ -131,15 +67,24 @@ export function fencedCodePlugin(editor: Client) {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
widgets.push(
|
const widget = renderMode === "markdown"
|
||||||
Decoration.widget({
|
? new MarkdownWidget(
|
||||||
widget: new IFrameWidget(
|
|
||||||
from + lineStrings[0].length + 1,
|
from + lineStrings[0].length + 1,
|
||||||
to - lineStrings[lineStrings.length - 1].length - 1,
|
to - lineStrings[lineStrings.length - 1].length - 1,
|
||||||
editor,
|
editor,
|
||||||
lineStrings.slice(1, lineStrings.length - 1).join("\n"),
|
lineStrings.slice(1, lineStrings.length - 1).join("\n"),
|
||||||
codeWidgetCallback,
|
codeWidgetCallback,
|
||||||
),
|
)
|
||||||
|
: new IFrameWidget(
|
||||||
|
from + lineStrings[0].length + 1,
|
||||||
|
to - lineStrings[lineStrings.length - 1].length - 1,
|
||||||
|
editor,
|
||||||
|
lineStrings.slice(1, lineStrings.length - 1).join("\n"),
|
||||||
|
codeWidgetCallback,
|
||||||
|
);
|
||||||
|
widgets.push(
|
||||||
|
Decoration.widget({
|
||||||
|
widget: widget,
|
||||||
}).range(from),
|
}).range(from),
|
||||||
);
|
);
|
||||||
return false;
|
return false;
|
||||||
|
71
web/cm_plugins/iframe_widget.ts
Normal file
71
web/cm_plugins/iframe_widget.ts
Normal file
@ -0,0 +1,71 @@
|
|||||||
|
import { WidgetContent } from "../../plug-api/app_event.ts";
|
||||||
|
import { WidgetType } from "../deps.ts";
|
||||||
|
import type { Client } from "../client.ts";
|
||||||
|
import { createWidgetSandboxIFrame } from "../components/widget_sandbox_iframe.ts";
|
||||||
|
import type { CodeWidgetCallback } from "$sb/types.ts";
|
||||||
|
|
||||||
|
export class IFrameWidget extends WidgetType {
|
||||||
|
iframe?: HTMLIFrameElement;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
readonly from: number,
|
||||||
|
readonly to: number,
|
||||||
|
readonly client: Client,
|
||||||
|
readonly bodyText: string,
|
||||||
|
readonly codeWidgetCallback: CodeWidgetCallback,
|
||||||
|
) {
|
||||||
|
super();
|
||||||
|
}
|
||||||
|
|
||||||
|
toDOM(): HTMLElement {
|
||||||
|
const from = this.from;
|
||||||
|
const iframe = createWidgetSandboxIFrame(
|
||||||
|
this.client,
|
||||||
|
this.bodyText,
|
||||||
|
this.codeWidgetCallback(this.bodyText, this.client.currentPage!),
|
||||||
|
(message) => {
|
||||||
|
switch (message.type) {
|
||||||
|
case "blur":
|
||||||
|
this.client.editorView.dispatch({
|
||||||
|
selection: { anchor: from },
|
||||||
|
});
|
||||||
|
this.client.focus();
|
||||||
|
|
||||||
|
break;
|
||||||
|
case "reload":
|
||||||
|
this.codeWidgetCallback(this.bodyText, this.client.currentPage!)
|
||||||
|
.then(
|
||||||
|
(widgetContent: WidgetContent) => {
|
||||||
|
iframe.contentWindow!.postMessage({
|
||||||
|
type: "html",
|
||||||
|
html: widgetContent.html,
|
||||||
|
script: widgetContent.script,
|
||||||
|
theme:
|
||||||
|
document.getElementsByTagName("html")[0].dataset.theme,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
const estimatedHeight = this.estimatedHeight;
|
||||||
|
iframe.height = `${estimatedHeight}px`;
|
||||||
|
|
||||||
|
return iframe;
|
||||||
|
}
|
||||||
|
|
||||||
|
get estimatedHeight(): number {
|
||||||
|
const cachedHeight = this.client.space.getCachedWidgetHeight(this.bodyText);
|
||||||
|
// console.log("Calling estimated height", cachedHeight);
|
||||||
|
return cachedHeight > 0 ? cachedHeight : 150;
|
||||||
|
}
|
||||||
|
|
||||||
|
eq(other: WidgetType): boolean {
|
||||||
|
return (
|
||||||
|
other instanceof IFrameWidget &&
|
||||||
|
other.bodyText === this.bodyText
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
163
web/cm_plugins/markdown_widget.ts
Normal file
163
web/cm_plugins/markdown_widget.ts
Normal file
@ -0,0 +1,163 @@
|
|||||||
|
import { WidgetType } from "../deps.ts";
|
||||||
|
import type { Client } from "../client.ts";
|
||||||
|
import type { CodeWidgetCallback } from "$sb/types.ts";
|
||||||
|
import { renderMarkdownToHtml } from "../../plugs/markdown/markdown_render.ts";
|
||||||
|
import { resolveAttachmentPath } from "$sb/lib/resolve.ts";
|
||||||
|
import { parse } from "../../common/markdown_parser/parse_tree.ts";
|
||||||
|
import buildMarkdown from "../../common/markdown_parser/parser.ts";
|
||||||
|
import { renderToText } from "$sb/lib/tree.ts";
|
||||||
|
|
||||||
|
export class MarkdownWidget extends WidgetType {
|
||||||
|
renderedMarkdown?: string;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
readonly from: number,
|
||||||
|
readonly to: number,
|
||||||
|
readonly client: Client,
|
||||||
|
readonly bodyText: string,
|
||||||
|
readonly codeWidgetCallback: CodeWidgetCallback,
|
||||||
|
) {
|
||||||
|
super();
|
||||||
|
}
|
||||||
|
|
||||||
|
toDOM(): HTMLElement {
|
||||||
|
const div = document.createElement("div");
|
||||||
|
div.className = "sb-markdown-widget";
|
||||||
|
const cacheItem = this.client.getWidgetCache(this.bodyText);
|
||||||
|
if (cacheItem) {
|
||||||
|
div.innerHTML = this.wrapHtml(cacheItem.html);
|
||||||
|
this.attachListeners(div);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Async kick-off of content renderer
|
||||||
|
this.renderContent(div, cacheItem?.html).catch(console.error);
|
||||||
|
|
||||||
|
return div;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async renderContent(
|
||||||
|
div: HTMLElement,
|
||||||
|
cachedHtml: string | undefined,
|
||||||
|
) {
|
||||||
|
const widgetContent = await this.codeWidgetCallback(
|
||||||
|
this.bodyText,
|
||||||
|
this.client.currentPage!,
|
||||||
|
);
|
||||||
|
const lang = buildMarkdown(this.client.system.mdExtensions);
|
||||||
|
let mdTree = parse(
|
||||||
|
lang,
|
||||||
|
widgetContent.markdown!,
|
||||||
|
);
|
||||||
|
mdTree = await this.client.system.localSyscall(
|
||||||
|
"system.invokeFunction",
|
||||||
|
[
|
||||||
|
"markdown.expandCodeWidgets",
|
||||||
|
mdTree,
|
||||||
|
this.client.currentPage,
|
||||||
|
],
|
||||||
|
);
|
||||||
|
// Used for the source button
|
||||||
|
this.renderedMarkdown = renderToText(mdTree);
|
||||||
|
|
||||||
|
const html = renderMarkdownToHtml(mdTree, {
|
||||||
|
// Annotate every element with its position so we can use it to put
|
||||||
|
// the cursor there when the user clicks on the table.
|
||||||
|
annotationPositions: true,
|
||||||
|
translateUrls: (url) => {
|
||||||
|
if (!url.includes("://")) {
|
||||||
|
url = resolveAttachmentPath(
|
||||||
|
this.client.currentPage!,
|
||||||
|
decodeURI(url),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return url;
|
||||||
|
},
|
||||||
|
preserveAttributes: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (cachedHtml === html) {
|
||||||
|
// HTML still same as in cache, no need to re-render
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
div.innerHTML = this.wrapHtml(html);
|
||||||
|
this.attachListeners(div);
|
||||||
|
|
||||||
|
// Let's give it a tick, then measure and cache
|
||||||
|
setTimeout(() => {
|
||||||
|
this.client.setWidgetCache(
|
||||||
|
this.bodyText,
|
||||||
|
div.clientHeight,
|
||||||
|
html,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private wrapHtml(html: string) {
|
||||||
|
return `
|
||||||
|
<div class="button-bar">
|
||||||
|
<button class="source-button" title="Show Markdown source"><svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-code"><polyline points="16 18 22 12 16 6"></polyline><polyline points="8 6 2 12 8 18"></polyline></svg></button>
|
||||||
|
<button class="reload-button" title="Reload"><svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="23 4 23 10 17 10"></polyline><polyline points="1 20 1 14 7 14"></polyline><path d="M3.51 9a9 9 0 0 1 14.85-3.36L23 10M1 14l4.64 4.36A9 9 0 0 0 20.49 15"></path></svg></button>
|
||||||
|
<button class="edit-button" title="Edit"><svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-edit"><path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"></path><path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"></path></svg></button>
|
||||||
|
</div>
|
||||||
|
${html}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
private attachListeners(div: HTMLElement) {
|
||||||
|
div.querySelectorAll("a[data-ref]").forEach((el_) => {
|
||||||
|
const el = el_ as HTMLElement;
|
||||||
|
// Override default click behavior with a local navigate (faster)
|
||||||
|
el.addEventListener("click", (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
this.client.navigate(el.dataset.ref!);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Implement task toggling
|
||||||
|
div.querySelectorAll("span[data-external-task-ref]").forEach((el: any) => {
|
||||||
|
const taskRef = el.dataset.externalTaskRef;
|
||||||
|
el.querySelector("input[type=checkbox]").addEventListener(
|
||||||
|
"change",
|
||||||
|
(e: any) => {
|
||||||
|
const oldState = e.target.dataset.state;
|
||||||
|
const newState = oldState === " " ? "x" : " ";
|
||||||
|
// Update state in DOM as well for future toggles
|
||||||
|
e.target.dataset.state = newState;
|
||||||
|
console.log("Toggling task", taskRef);
|
||||||
|
this.client.system.localSyscall(
|
||||||
|
"system.invokeFunction",
|
||||||
|
["tasks.updateTaskState", taskRef, oldState, newState],
|
||||||
|
).catch(
|
||||||
|
console.error,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
div.querySelector(".edit-button")!.addEventListener("click", () => {
|
||||||
|
this.client.editorView.dispatch({
|
||||||
|
selection: { anchor: this.from },
|
||||||
|
});
|
||||||
|
this.client.focus();
|
||||||
|
});
|
||||||
|
div.querySelector(".reload-button")!.addEventListener("click", () => {
|
||||||
|
this.renderContent(div, undefined).catch(console.error);
|
||||||
|
});
|
||||||
|
div.querySelector(".source-button")!.addEventListener("click", () => {
|
||||||
|
div.innerText = this.renderedMarkdown!;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
get estimatedHeight(): number {
|
||||||
|
const cacheItem = this.client.getWidgetCache(this.bodyText);
|
||||||
|
// console.log("Calling estimated height", cacheItem);
|
||||||
|
return cacheItem ? cacheItem.height : -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
eq(other: WidgetType): boolean {
|
||||||
|
return (
|
||||||
|
other instanceof MarkdownWidget &&
|
||||||
|
other.bodyText === this.bodyText
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -4,10 +4,12 @@ import { CodeWidgetCallback } from "$sb/types.ts";
|
|||||||
|
|
||||||
export type CodeWidgetT = {
|
export type CodeWidgetT = {
|
||||||
codeWidget?: string;
|
codeWidget?: string;
|
||||||
|
renderMode?: "markdown" | "iframe";
|
||||||
};
|
};
|
||||||
|
|
||||||
export class CodeWidgetHook implements Hook<CodeWidgetT> {
|
export class CodeWidgetHook implements Hook<CodeWidgetT> {
|
||||||
codeWidgetCallbacks = new Map<string, CodeWidgetCallback>();
|
codeWidgetCallbacks = new Map<string, CodeWidgetCallback>();
|
||||||
|
codeWidgetModes = new Map<string, "markdown" | "iframe">();
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
}
|
}
|
||||||
@ -23,6 +25,10 @@ export class CodeWidgetHook implements Hook<CodeWidgetT> {
|
|||||||
if (!functionDef.codeWidget) {
|
if (!functionDef.codeWidget) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
this.codeWidgetModes.set(
|
||||||
|
functionDef.codeWidget,
|
||||||
|
functionDef.renderMode || "iframe",
|
||||||
|
);
|
||||||
this.codeWidgetCallbacks.set(
|
this.codeWidgetCallbacks.set(
|
||||||
functionDef.codeWidget,
|
functionDef.codeWidget,
|
||||||
(bodyText, pageName) => {
|
(bodyText, pageName) => {
|
||||||
|
@ -443,6 +443,91 @@
|
|||||||
line-height: 0;
|
line-height: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.sb-markdown-widget {
|
||||||
|
overflow-y: scroll;
|
||||||
|
margin: 0 0 -4ch 0;
|
||||||
|
border: 1px solid var(--editor-directive-background-color);
|
||||||
|
border-radius: 5px;
|
||||||
|
white-space: nowrap;
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
ul,
|
||||||
|
ol {
|
||||||
|
margin-top: 0;
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
ul {
|
||||||
|
list-style: none;
|
||||||
|
padding-left: 1ch;
|
||||||
|
}
|
||||||
|
|
||||||
|
ul li::before {
|
||||||
|
content: "\2022";
|
||||||
|
/* Add content: \2022 is the CSS Code/unicode for a bullet */
|
||||||
|
color: var(--editor-list-bullet-color);
|
||||||
|
display: inline-block;
|
||||||
|
/* Needed to add space between the bullet and the text */
|
||||||
|
width: 1em;
|
||||||
|
/* Also needed for space (tweak if needed) */
|
||||||
|
// margin-left: -1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1,
|
||||||
|
h2,
|
||||||
|
h3,
|
||||||
|
h4,
|
||||||
|
h5 {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
a.wiki-link {
|
||||||
|
border-radius: 5px;
|
||||||
|
padding: 0 5px;
|
||||||
|
color: var(--editor-wiki-link-page-color);
|
||||||
|
background-color: var(--editor-wiki-link-page-background-color);
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
span.task-deadline {
|
||||||
|
background-color: rgba(22, 22, 22, 0.07);
|
||||||
|
}
|
||||||
|
|
||||||
|
tt {
|
||||||
|
background-color: var(--editor-code-background-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Button bar
|
||||||
|
&:hover .button-bar,
|
||||||
|
&:active .button-bar {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button-bar {
|
||||||
|
position: absolute;
|
||||||
|
right: 6px;
|
||||||
|
top: 6px;
|
||||||
|
display: none;
|
||||||
|
background: var(--editor-directive-background-color);
|
||||||
|
padding-inline: 3px;
|
||||||
|
padding-bottom: 1px;
|
||||||
|
border-radius: 5px;
|
||||||
|
|
||||||
|
button {
|
||||||
|
border: none;
|
||||||
|
background: none;
|
||||||
|
cursor: pointer;
|
||||||
|
color: var(--root-color);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.edit-button,
|
||||||
|
.reload-button {
|
||||||
|
margin-left: -10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
.sb-fenced-code-iframe {
|
.sb-fenced-code-iframe {
|
||||||
background-color: transparent;
|
background-color: transparent;
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user