mirror of
https://github.com/zsviczian/obsidian-excalidraw-plugin.git
synced 2025-08-06 05:46:28 +00:00
Compare commits
9 Commits
2.0.1-beta
...
2.0.3
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e72c1676c2 | ||
|
|
5a17eb7054 | ||
|
|
75d52c07b8 | ||
|
|
4dc6c17486 | ||
|
|
e780930799 | ||
|
|
49cd6a36a1 | ||
|
|
d4830983e2 | ||
|
|
a69fefffdc | ||
|
|
1d0466dae7 |
203
ea-scripts/GPT-Draw-a-UI.md
Normal file
203
ea-scripts/GPT-Draw-a-UI.md
Normal file
@@ -0,0 +1,203 @@
|
||||
/*
|
||||
Based on https://github.com/SawyerHood/draw-a-ui
|
||||
|
||||
<iframe width="560" height="315" src="https://www.youtube.com/embed/y3kHl_6Ll4w" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe>
|
||||
|
||||

|
||||
```js*/
|
||||
|
||||
if(!ea.verifyMinimumPluginVersion || !ea.verifyMinimumPluginVersion("2.0.2")) {
|
||||
new Notice("This script requires a newer version of Excalidraw. Please install the latest version.");
|
||||
return;
|
||||
}
|
||||
|
||||
settings = ea.getScriptSettings();
|
||||
//set default values on first run
|
||||
if(!settings["OPENAI_API_KEY"]) {
|
||||
settings = {
|
||||
"OPENAI_API_KEY" : {
|
||||
value: "",
|
||||
description: `Get your api key at <a href="https://platform.openai.com/">https://platform.openai.com/</a><br>⚠️ Note that the gpt-4-vision-preview
|
||||
requires a minimum of $5 credit on your account.`
|
||||
},
|
||||
"FOLDER" : {
|
||||
value: "GPTPlayground",
|
||||
description: `The folder in your vault where you want to store generated html pages`
|
||||
},
|
||||
"FILENAME": {
|
||||
value: "page",
|
||||
description: `The base name of the html file that will be created. Each time you run the script
|
||||
a new file will be created using the following pattern "filename_i" where i is a counter`
|
||||
}
|
||||
};
|
||||
await ea.setScriptSettings(settings);
|
||||
}
|
||||
|
||||
const OPENAI_API_KEY = settings["OPENAI_API_KEY"].value;
|
||||
const FOLDER = settings["FOLDER"].value;
|
||||
const FILENAME = settings["FILENAME"].value;
|
||||
|
||||
if(OPENAI_API_KEY==="") {
|
||||
new Notice("Please set an OpenAI API key in plugin settings");
|
||||
return;
|
||||
}
|
||||
|
||||
const systemPrompt = `You are an expert tailwind developer. A user will provide you with a
|
||||
low-fidelity wireframe of an application and you will return
|
||||
a single html file that uses tailwind to create the website.
|
||||
Use creative license to make the application more fleshed out. Write the necessary javascript code.
|
||||
If you need to insert an image, use placehold.co to create a placeholder image.
|
||||
Respond only with the html file.`;
|
||||
|
||||
const post = async (request) => {
|
||||
const { image } = await request.json();
|
||||
const body = {
|
||||
model: "gpt-4-vision-preview",
|
||||
max_tokens: 4096,
|
||||
messages: [
|
||||
{
|
||||
role: "system",
|
||||
content: systemPrompt,
|
||||
},
|
||||
{
|
||||
role: "user",
|
||||
content: [
|
||||
{
|
||||
type: "image_url",
|
||||
image_url: image,
|
||||
},
|
||||
"Turn this into a single html file using tailwind.",
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
let json = null;
|
||||
try {
|
||||
const resp = await requestUrl ({
|
||||
url: "https://api.openai.com/v1/chat/completions",
|
||||
method: "post",
|
||||
contentType: "application/json",
|
||||
body: JSON.stringify(body),
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Authorization: `Bearer ${OPENAI_API_KEY}`,
|
||||
},
|
||||
throw: false
|
||||
});
|
||||
json = resp.json;
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
}
|
||||
|
||||
return json;
|
||||
}
|
||||
|
||||
const blobToBase64 = async (blob) => {
|
||||
const arrayBuffer = await blob.arrayBuffer()
|
||||
const bytes = new Uint8Array(arrayBuffer)
|
||||
var binary = '';
|
||||
var len = bytes.byteLength;
|
||||
for (var i = 0; i < len; i++) {
|
||||
binary += String.fromCharCode(bytes[i]);
|
||||
}
|
||||
return btoa(binary);
|
||||
}
|
||||
|
||||
const getRequestObjFromSelectedElements = async (view) => {
|
||||
await view.forceSave(true);
|
||||
const viewElements = ea.getViewSelectedElements();
|
||||
if(viewElements.length === 0) {
|
||||
new Notice ("Aborting because there is nothing selected.",4000);
|
||||
return;
|
||||
}
|
||||
const bb = ea.getBoundingBox(viewElements);
|
||||
const size = (bb.width*bb.height);
|
||||
const minRatio = Math.sqrt(360000/size);
|
||||
const maxRatio = Math.sqrt(size/16000000);
|
||||
const scale = minRatio > 1
|
||||
? minRatio
|
||||
: (
|
||||
maxRatio > 1
|
||||
? 1/maxRatio
|
||||
: 1
|
||||
);
|
||||
|
||||
const loader = ea.getEmbeddedFilesLoader(false);
|
||||
const exportSettings = {
|
||||
withBackground: true,
|
||||
withTheme: true,
|
||||
};
|
||||
|
||||
const img =
|
||||
await ea.createPNG(
|
||||
view.file.path,
|
||||
scale,
|
||||
exportSettings,
|
||||
loader,
|
||||
"light",
|
||||
);
|
||||
const dataURL = `data:image/png;base64,${await blobToBase64(img)}`;
|
||||
return { json: async () => ({ image: dataURL }) }
|
||||
}
|
||||
|
||||
const extractHTMLFromString = (result) => {
|
||||
if(!result) return null;
|
||||
const start = result.indexOf('```html\n');
|
||||
const end = result.lastIndexOf('```');
|
||||
if (start !== -1 && end !== -1) {
|
||||
const htmlString = result.substring(start + 8, end);
|
||||
return htmlString.trim();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
const checkAndCreateFolder = async (folderpath) => {
|
||||
const vault = app.vault;
|
||||
folderpath = ea.obsidian.normalizePath(folderpath);
|
||||
const folder = vault.getAbstractFileByPathInsensitive(folderpath);
|
||||
if (folder) {
|
||||
return folder;
|
||||
}
|
||||
return await vault.createFolder(folderpath);
|
||||
}
|
||||
|
||||
const getNewUniqueFilepath = (filename, folderpath) => {
|
||||
let fname = ea.obsidian.normalizePath(`${folderpath}/${filename}.html`);
|
||||
let file = app.vault.getAbstractFileByPath(fname);
|
||||
let i = 0;
|
||||
while (file) {
|
||||
fname = ea.obsidian.normalizePath(`${folderpath}/${filename}_${i++}.html`);
|
||||
file = app.vault.getAbstractFileByPath(fname);
|
||||
}
|
||||
return fname;
|
||||
}
|
||||
|
||||
const requestObject = await getRequestObjFromSelectedElements(ea.targetView);
|
||||
const result = await post(requestObject);
|
||||
|
||||
const errorMessage = () => {
|
||||
new Notice ("Something went wrong! Check developer console for more.");
|
||||
console.log(result);
|
||||
}
|
||||
|
||||
if(!result?.hasOwnProperty("choices")) {
|
||||
errorMessage();
|
||||
return;
|
||||
}
|
||||
|
||||
const htmlContent = extractHTMLFromString(result.choices[0]?.message?.content);
|
||||
|
||||
if(!htmlContent) {
|
||||
errorMessage();
|
||||
return;
|
||||
}
|
||||
|
||||
const folder = await checkAndCreateFolder(FOLDER);
|
||||
const filepath = getNewUniqueFilepath(FILENAME,folder.path);
|
||||
const file = await app.vault.create(filepath,htmlContent);
|
||||
const url = app.vault.adapter.getFilePath(file.path).toString();
|
||||
const bb = ea.getBoundingBox(ea.getViewSelectedElements());
|
||||
ea.addEmbeddable(bb.topX+bb.width+40,bb.topY,600,800,url);
|
||||
await ea.addElementsToView(false,true);
|
||||
ea.viewZoomToElements([]);
|
||||
1
ea-scripts/GPT-Draw-a-UI.svg
Normal file
1
ea-scripts/GPT-Draw-a-UI.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 512"><path d="M320 0c17.7 0 32 14.3 32 32V96H472c39.8 0 72 32.2 72 72V440c0 39.8-32.2 72-72 72H168c-39.8 0-72-32.2-72-72V168c0-39.8 32.2-72 72-72H288V32c0-17.7 14.3-32 32-32zM208 384c-8.8 0-16 7.2-16 16s7.2 16 16 16h32c8.8 0 16-7.2 16-16s-7.2-16-16-16H208zm96 0c-8.8 0-16 7.2-16 16s7.2 16 16 16h32c8.8 0 16-7.2 16-16s-7.2-16-16-16H304zm96 0c-8.8 0-16 7.2-16 16s7.2 16 16 16h32c8.8 0 16-7.2 16-16s-7.2-16-16-16H400zM264 256a40 40 0 1 0 -80 0 40 40 0 1 0 80 0zm152 40a40 40 0 1 0 0-80 40 40 0 1 0 0 80zM48 224H64V416H48c-26.5 0-48-21.5-48-48V272c0-26.5 21.5-48 48-48zm544 0c26.5 0 48 21.5 48 48v96c0 26.5-21.5 48-48 48H576V224h16z"/></svg>
|
||||
|
After Width: | Height: | Size: 694 B |
File diff suppressed because one or more lines are too long
@@ -115,6 +115,7 @@ I would love to include your contribution in the script library. If you have a s
|
||||
|<div><img src="https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Auto%20Draw%20for%20Pen.svg"/></div>|[[#Auto Draw for Pen]]|
|
||||
|<div><img src="https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Boolean%20Operations.svg"/></div>|[[#Boolean Operations]]|
|
||||
|<div><img src="https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Copy%20Selected%20Element%20Styles%20to%20Global.svg"/></div>|[[#Copy Selected Element Styles to Global]]|
|
||||
|<div><img src="https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/GPT-Draw-a-UI.svg"/></div>|[[#GPT Draw-a-UI]]|
|
||||
|<div><img src="https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Hardware%20Eraser%20Support.svg"/></div>|[[#Hardware Eraser Support]]|
|
||||
|<div><img src="https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Palette%20loader.svg"/></div>|[[#Palette Loader]]|
|
||||
|<div><img src="https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/PDF%20Page%20Text%20to%20Clipboard.svg"/></div>|[[#PDF Page Text to Clipboard]]|
|
||||
@@ -350,6 +351,13 @@ https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea
|
||||
```
|
||||
<table><tr valign='top'><td class="label">Author</td><td class="data"><a href='https://github.com/7flash'>@7flash</a></td></tr><tr valign='top'><td class="label">Source</td><td class="data"><a href='https://github.com/zsviczian/obsidian-excalidraw-plugin/blob/master/ea-scripts/Grid%20Selected%20Images.md'>File on GitHub</a></td></tr><tr valign='top'><td class="label">Description</td><td class="data">This script arranges selected images into compact grid view, removing gaps in-between, resizing when necessary and breaking into multiple rows/columns.<br><img src='https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/images/scripts-grid-selected-images.png'></td></tr></table>
|
||||
|
||||
## GPT Draw-a-UI
|
||||
```excalidraw-script-install
|
||||
https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/GPT-Draw-a-UI.md
|
||||
```
|
||||
<table><tr valign='top'><td class="label">Author</td><td class="data"><a href='https://github.com/zsviczian'>@zsviczian</a></td></tr><tr valign='top'><td class="label">Source</td><td class="data"><a href='https://github.com/zsviczian/obsidian-excalidraw-plugin/blob/master/ea-scripts/GPT-Draw-a-UI.md'>File on GitHub</a></td></tr><tr valign='top'><td class="label">Description</td><td class="data">Draw a UI and let GPT create the code for you.<br><iframe width="400" height="225" src="https://www.youtube.com/embed/y3kHl_6Ll4w" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe><img src='https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/images/scripts-draw-a-ui.jpg'></td></tr></table>
|
||||
|
||||
|
||||
## Hardware Eraser Support
|
||||
```excalidraw-script-install
|
||||
https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Hardware%20Eraser%20Support.md
|
||||
|
||||
BIN
images/scripts-draw-a-ui.jpg
Normal file
BIN
images/scripts-draw-a-ui.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 67 KiB |
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"id": "obsidian-excalidraw-plugin",
|
||||
"name": "Excalidraw",
|
||||
"version": "2.0.1-beta.1",
|
||||
"version": "2.0.1-beta-2",
|
||||
"minAppVersion": "1.1.6",
|
||||
"description": "An Obsidian plugin to edit and view Excalidraw drawings",
|
||||
"author": "Zsolt Viczian",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"id": "obsidian-excalidraw-plugin",
|
||||
"name": "Excalidraw",
|
||||
"version": "2.0.1",
|
||||
"version": "2.0.3",
|
||||
"minAppVersion": "1.1.6",
|
||||
"description": "An Obsidian plugin to edit and view Excalidraw drawings",
|
||||
"author": "Zsolt Viczian",
|
||||
|
||||
@@ -1235,11 +1235,8 @@ export class ExcalidrawData {
|
||||
const scene = this.scene as SceneDataWithFiles;
|
||||
|
||||
//remove files and equations that no longer have a corresponding image element
|
||||
const fileIds = (
|
||||
scene.elements.filter(
|
||||
(e) => e.type === "image",
|
||||
) as ExcalidrawImageElement[]
|
||||
).map((e) => e.fileId);
|
||||
const images = scene.elements.filter((e) => e.type === "image") as ExcalidrawImageElement[];
|
||||
const fileIds = (images).map((e) => e.fileId);
|
||||
this.files.forEach((value, key) => {
|
||||
if (!fileIds.contains(key)) {
|
||||
this.files.delete(key);
|
||||
@@ -1261,22 +1258,26 @@ export class ExcalidrawData {
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
//check if there are any images that need to be processed in the new scene
|
||||
if (!scene.files || Object.keys(scene.files).length === 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
//assing new fileId to duplicate equation and markdown files
|
||||
//https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/601
|
||||
//https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/593
|
||||
//https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/297
|
||||
const processedIds = new Set<string>();
|
||||
fileIds.forEach(fileId=>{
|
||||
fileIds.forEach((fileId,idx)=>{
|
||||
if(processedIds.has(fileId)) {
|
||||
const file = this.getFile(fileId);
|
||||
const equation = this.getEquation(fileId);
|
||||
const mermaid = this.getMermaid(fileId);
|
||||
|
||||
|
||||
|
||||
//images should have a single reference, but equations, and markdown embeds should have as many as instances of the file in the scene
|
||||
if(file && (file.isHyperLink || file.isLocalLink || (file.file && (file.file.extension !== "md" || this.plugin.isExcalidrawFile(file.file))))) {
|
||||
return;
|
||||
@@ -1284,6 +1285,12 @@ export class ExcalidrawData {
|
||||
if(mermaid) {
|
||||
return;
|
||||
}
|
||||
|
||||
if(getMermaidText(images[idx])) {
|
||||
this.setMermaid(fileId, {mermaid: getMermaidText(images[idx]), isLoaded: true});
|
||||
return;
|
||||
}
|
||||
|
||||
const newId = fileid();
|
||||
(scene
|
||||
.elements
|
||||
|
||||
@@ -127,7 +127,7 @@ import { CanvasNodeFactory, ObsidianCanvasNode } from "./utils/CanvasNodeFactory
|
||||
import { EmbeddableMenu } from "./menu/EmbeddableActionsMenu";
|
||||
import { useDefaultExcalidrawFrame } from "./utils/CustomEmbeddableUtils";
|
||||
import { UniversalInsertFileModal } from "./dialogs/UniversalInsertFileModal";
|
||||
import { shouldRenderMermaid } from "./utils/MermaidUtils";
|
||||
import { getMermaidText, shouldRenderMermaid } from "./utils/MermaidUtils";
|
||||
import { nanoid } from "nanoid";
|
||||
|
||||
declare const PLUGIN_VERSION:string;
|
||||
@@ -1024,13 +1024,14 @@ export default class ExcalidrawView extends TextFileView {
|
||||
})();
|
||||
return;
|
||||
}
|
||||
if (this.excalidrawData.hasMermaid(selectedImage.fileId)) {
|
||||
if (this.excalidrawData.hasMermaid(selectedImage.fileId) || getMermaidText(imageElement)) {
|
||||
if(shouldRenderMermaid) {
|
||||
const api = this.excalidrawAPI as ExcalidrawImperativeAPI;
|
||||
api.setActiveTool({type: "mermaid"});
|
||||
api.updateScene({appState: { openDialog: "mermaid" }});
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
await this.save(false); //in case pasted images haven't been saved yet
|
||||
if (this.excalidrawData.hasFile(selectedImage.fileId)) {
|
||||
const ef = this.excalidrawData.getFile(selectedImage.fileId);
|
||||
|
||||
@@ -228,7 +228,7 @@ function RenderObsidianView(
|
||||
return () => {}; //cleanup on unmount
|
||||
}, [linkText, subpath, containerRef]);
|
||||
|
||||
const setColors = (canvasNode: HTMLDivElement, element: NonDeletedExcalidrawElement, mdProps: EmbeddableMDCustomProps, canvas: string) => {
|
||||
const setColors = (canvasNode: HTMLDivElement, element: NonDeletedExcalidrawElement, mdProps: EmbeddableMDCustomProps, canvasColor: string) => {
|
||||
if(!mdProps) return;
|
||||
if (!leafRef.current?.hasOwnProperty("node")) return;
|
||||
|
||||
@@ -246,28 +246,43 @@ function RenderObsidianView(
|
||||
if(mdProps.backgroundMatchElement) {
|
||||
const opacity = (mdProps?.backgroundOpacity ?? 50)/100;
|
||||
const color = element?.backgroundColor
|
||||
? ea.getCM(element.backgroundColor).alphaTo(opacity).stringHEX()
|
||||
? (element.backgroundColor.toLowerCase() === "transparent"
|
||||
? "transparent"
|
||||
: ea.getCM(element.backgroundColor).alphaTo(opacity).stringHEX())
|
||||
: "transparent";
|
||||
|
||||
color === "transparent" ? canvasNode?.addClass("transparent") : canvasNode?.removeClass("transparent");
|
||||
canvasNode?.style.setProperty("--canvas-background", color);
|
||||
canvasNode?.style.setProperty("--background-primary", color);
|
||||
canvasNodeContainer?.style.setProperty("background-color", color);
|
||||
} else if (!(mdProps?.backgroundMatchElement ?? true )) {
|
||||
const opacity = (mdProps.backgroundOpacity??100)/100;
|
||||
const color = mdProps.backgroundMatchCanvas
|
||||
? ea.getCM(canvasColor).alphaTo((mdProps.backgroundOpacity??100)/100).stringHEX()
|
||||
? (canvasColor.toLowerCase() === "transparent"
|
||||
? "transparent"
|
||||
: ea.getCM(canvasColor).alphaTo(opacity).stringHEX())
|
||||
: ea.getCM(mdProps.backgroundColor).alphaTo((mdProps.backgroundOpacity??100)/100).stringHEX();
|
||||
containerRef.current?.style.setProperty("--canvas-background", color);
|
||||
|
||||
color === "transparent" ? canvasNode?.addClass("transparent") : canvasNode?.removeClass("transparent");
|
||||
canvasNode?.style.setProperty("--canvas-background", color);
|
||||
canvasNode?.style.setProperty("--background-primary", color);
|
||||
canvasNodeContainer?.style.setProperty("background-color", color);
|
||||
}
|
||||
|
||||
if(mdProps.borderMatchElement) {
|
||||
const opacity = (mdProps?.borderOpacity ?? 50)/100;
|
||||
const color = element?.strokeColor
|
||||
? ea.getCM(element?.strokeColor).alphaTo(opacity).stringHEX()
|
||||
: "transparent";
|
||||
? (element.strokeColor.toLowerCase() === "transparent"
|
||||
? "transparent"
|
||||
: ea.getCM(element.strokeColor).alphaTo(opacity).stringHEX())
|
||||
: "transparent";
|
||||
canvasNode?.style.setProperty("--canvas-border", color);
|
||||
canvasNode?.style.setProperty("--canvas-color", color);
|
||||
canvasNodeContainer?.style.setProperty("border-color", color);
|
||||
} else if(!(mdProps?.borderMatchElement ?? true)) {
|
||||
const color = ea.getCM(mdProps.borderColor).alphaTo((mdProps.borderOpacity??100)/100).stringHEX();
|
||||
canvasNode?.style.setProperty("--canvas-border", color);
|
||||
canvasNode?.style.setProperty("--canvas-color", color);
|
||||
canvasNodeContainer?.style.setProperty("border-color", color);
|
||||
}
|
||||
}
|
||||
@@ -398,7 +413,7 @@ export const CustomEmbeddable: React.FC<{element: NonDeletedExcalidrawElement; v
|
||||
color: `var(--text-normal)`,
|
||||
}}
|
||||
className={`${theme} canvas-node ${
|
||||
mdProps?.filenameVisible ? "" : "excalidraw-mdEmbed-hideFilename"}`}
|
||||
mdProps?.filenameVisible && !mdProps.useObsidianDefaults ? "" : "excalidraw-mdEmbed-hideFilename"}`}
|
||||
>
|
||||
<RenderObsidianView
|
||||
mdProps={mdProps}
|
||||
|
||||
@@ -109,7 +109,7 @@ export class EmbeddalbeMDFileCustomDataSettingsComponent {
|
||||
|
||||
bgSetting.settingEl.style.display = (this.mdCustomData.backgroundMatchElement || this.mdCustomData.backgroundMatchCanvas) ? "none" : "";
|
||||
const opacity = (value:number):DocumentFragment => {
|
||||
return fragWithHTML(`Current transparency is <b>${value}%</b>`);
|
||||
return fragWithHTML(`Current opacity is <b>${value}%</b>`);
|
||||
}
|
||||
|
||||
const bgOpacitySetting = new Setting(contentEl)
|
||||
|
||||
@@ -10,6 +10,8 @@ import { getNewUniqueFilepath, getPathWithoutExtension, splitFolderAndFilename }
|
||||
import { addAppendUpdateCustomData, fragWithHTML } from "src/utils/Utils";
|
||||
import { getYouTubeStartAt, isValidYouTubeStart, isYouTube, updateYouTubeStartTime } from "src/utils/YoutTubeUtils";
|
||||
import { EmbeddalbeMDFileCustomDataSettingsComponent } from "./EmbeddableMDFileCustomDataSettingsComponent";
|
||||
import { isCTRL } from "src/utils/ModifierkeyHelper";
|
||||
import { ExcalidrawImperativeAPI } from "@zsviczian/excalidraw/types/types";
|
||||
|
||||
export type EmbeddableMDCustomProps = {
|
||||
useObsidianDefaults: boolean;
|
||||
@@ -31,6 +33,7 @@ export class EmbeddableSettings extends Modal {
|
||||
private youtubeStart: string = null;
|
||||
private isMDFile: boolean;
|
||||
private mdCustomData: EmbeddableMDCustomProps;
|
||||
private onKeyDown: (ev: KeyboardEvent) => void;
|
||||
|
||||
constructor(
|
||||
private plugin: ExcalidrawPlugin,
|
||||
@@ -60,12 +63,12 @@ export class EmbeddableSettings extends Modal {
|
||||
|
||||
onOpen(): void {
|
||||
this.containerEl.classList.add("excalidraw-release");
|
||||
this.titleEl.setText(t("ES_TITLE"));
|
||||
//this.titleEl.setText(t("ES_TITLE"));
|
||||
this.createForm();
|
||||
}
|
||||
|
||||
onClose() {
|
||||
|
||||
this.containerEl.removeEventListener("keydown",this.onKeyDown);
|
||||
}
|
||||
|
||||
async createForm() {
|
||||
@@ -85,11 +88,21 @@ export class EmbeddableSettings extends Modal {
|
||||
}
|
||||
|
||||
const zoomValue = ():DocumentFragment => {
|
||||
return fragWithHTML(`Current zoom is <b>${Math.round(this.zoomValue*100)}%</b>`);
|
||||
}
|
||||
return fragWithHTML(`${t("ES_ZOOM_100_RELATIVE_DESC")}<br>Current zoom is <b>${Math.round(this.zoomValue*100)}%</b>`);
|
||||
}
|
||||
|
||||
const zoomSetting = new Setting(this.contentEl)
|
||||
.setName(t("ES_ZOOM"))
|
||||
.setDesc(zoomValue())
|
||||
.addButton(button =>
|
||||
button
|
||||
.setButtonText(t("ES_ZOOM_100"))
|
||||
.onClick(() => {
|
||||
const api = this.view.excalidrawAPI as ExcalidrawImperativeAPI;
|
||||
this.zoomValue = 1/api.getAppState().zoom.value;
|
||||
zoomSetting.setDesc(zoomValue());
|
||||
})
|
||||
)
|
||||
.addSlider(slider =>
|
||||
slider
|
||||
.setLimits(10,400,5)
|
||||
@@ -117,64 +130,87 @@ export class EmbeddableSettings extends Modal {
|
||||
this.contentEl.createEl("h3",{text: t("ES_EMBEDDABLE_SETTINGS")});
|
||||
new EmbeddalbeMDFileCustomDataSettingsComponent(this.contentEl,this.mdCustomData).render();
|
||||
}
|
||||
|
||||
new Setting(this.contentEl)
|
||||
.addButton(button =>
|
||||
button
|
||||
.setButtonText(t("PROMPT_BUTTON_CANCEL"))
|
||||
.setTooltip("ESC")
|
||||
.onClick(() => {
|
||||
this.close();
|
||||
})
|
||||
)
|
||||
.addButton(button =>
|
||||
button
|
||||
.setButtonText(t("PROMPT_BUTTON_OK"))
|
||||
.setTooltip("CTRL/Opt+Enter")
|
||||
.setCta()
|
||||
.onClick(()=>this.applySettings())
|
||||
)
|
||||
|
||||
const div = this.contentEl.createDiv({cls: "excalidraw-prompt-buttons-div"});
|
||||
const bOk = div.createEl("button", { text: t("PROMPT_BUTTON_OK"), cls: "excalidraw-prompt-button"});
|
||||
bOk.onclick = async () => {
|
||||
let dirty = false;
|
||||
const el = this.ea.getElement(this.element.id) as Mutable<ExcalidrawEmbeddableElement>;
|
||||
if(this.updatedFilepath) {
|
||||
const newPathWithExt = `${this.updatedFilepath}.${this.file.extension}`;
|
||||
if(newPathWithExt !== this.file.path) {
|
||||
const fnparts = splitFolderAndFilename(newPathWithExt);
|
||||
const newPath = getNewUniqueFilepath(
|
||||
this.app.vault,
|
||||
fnparts.folderpath,
|
||||
fnparts.filename,
|
||||
);
|
||||
await this.app.vault.rename(this.file,newPath);
|
||||
el.link = this.element.link.replace(
|
||||
/(\[\[)([^#\]]*)([^\]]*]])/,`$1${
|
||||
this.plugin.app.metadataCache.fileToLinktext(
|
||||
this.file,this.view.file.path,true)
|
||||
}$3`);
|
||||
dirty = true;
|
||||
}
|
||||
}
|
||||
if(this.isYouTube && this.youtubeStart !== getYouTubeStartAt(this.element.link)) {
|
||||
dirty = true;
|
||||
if(isValidYouTubeStart(this.youtubeStart)) {
|
||||
el.link = updateYouTubeStartTime(el.link,this.youtubeStart);
|
||||
} else {
|
||||
new Notice(t("ES_YOUTUBE_START_INVALID"));
|
||||
}
|
||||
}
|
||||
if(
|
||||
this.isMDFile && (
|
||||
this.mdCustomData.backgroundColor !== this.element.customData?.backgroundColor ||
|
||||
this.mdCustomData.borderColor !== this.element.customData?.borderColor ||
|
||||
this.mdCustomData.backgroundOpacity !== this.element.customData?.backgroundOpacity ||
|
||||
this.mdCustomData.borderOpacity !== this.element.customData?.borderOpacity ||
|
||||
this.mdCustomData.filenameVisible !== this.element.customData?.filenameVisible)
|
||||
) {
|
||||
addAppendUpdateCustomData(el,{mdProps: this.mdCustomData});
|
||||
dirty = true;
|
||||
}
|
||||
|
||||
if(this.zoomValue !== this.element.scale[0]) {
|
||||
dirty = true;
|
||||
|
||||
el.scale = [this.zoomValue,this.zoomValue];
|
||||
const onKeyDown = (ev: KeyboardEvent) => {
|
||||
if(isCTRL(ev) && ev.key === "Enter") {
|
||||
this.applySettings();
|
||||
}
|
||||
if(dirty) {
|
||||
this.ea.addElementsToView();
|
||||
}
|
||||
this.close();
|
||||
};
|
||||
const bCancel = div.createEl("button", { text: t("PROMPT_BUTTON_CANCEL"), cls: "excalidraw-prompt-button" });
|
||||
bCancel.onclick = () => {
|
||||
this.close();
|
||||
};
|
||||
}
|
||||
|
||||
this.onKeyDown = onKeyDown;
|
||||
this.containerEl.ownerDocument.addEventListener("keydown",onKeyDown);
|
||||
}
|
||||
|
||||
|
||||
|
||||
private async applySettings() {
|
||||
let dirty = false;
|
||||
const el = this.ea.getElement(this.element.id) as Mutable<ExcalidrawEmbeddableElement>;
|
||||
if(this.updatedFilepath) {
|
||||
const newPathWithExt = `${this.updatedFilepath}.${this.file.extension}`;
|
||||
if(newPathWithExt !== this.file.path) {
|
||||
const fnparts = splitFolderAndFilename(newPathWithExt);
|
||||
const newPath = getNewUniqueFilepath(
|
||||
this.app.vault,
|
||||
fnparts.folderpath,
|
||||
fnparts.filename,
|
||||
);
|
||||
await this.app.vault.rename(this.file,newPath);
|
||||
el.link = this.element.link.replace(
|
||||
/(\[\[)([^#\]]*)([^\]]*]])/,`$1${
|
||||
this.plugin.app.metadataCache.fileToLinktext(
|
||||
this.file,this.view.file.path,true)
|
||||
}$3`);
|
||||
dirty = true;
|
||||
}
|
||||
}
|
||||
if(this.isYouTube && this.youtubeStart !== getYouTubeStartAt(this.element.link)) {
|
||||
dirty = true;
|
||||
if(this.youtubeStart === "" || isValidYouTubeStart(this.youtubeStart)) {
|
||||
el.link = updateYouTubeStartTime(el.link,this.youtubeStart);
|
||||
} else {
|
||||
new Notice(t("ES_YOUTUBE_START_INVALID"));
|
||||
}
|
||||
}
|
||||
if(
|
||||
this.isMDFile && (
|
||||
this.mdCustomData.backgroundColor !== this.element.customData?.backgroundColor ||
|
||||
this.mdCustomData.borderColor !== this.element.customData?.borderColor ||
|
||||
this.mdCustomData.backgroundOpacity !== this.element.customData?.backgroundOpacity ||
|
||||
this.mdCustomData.borderOpacity !== this.element.customData?.borderOpacity ||
|
||||
this.mdCustomData.filenameVisible !== this.element.customData?.filenameVisible)
|
||||
) {
|
||||
addAppendUpdateCustomData(el,{mdProps: this.mdCustomData});
|
||||
dirty = true;
|
||||
}
|
||||
|
||||
if(this.zoomValue !== this.element.scale[0]) {
|
||||
dirty = true;
|
||||
|
||||
el.scale = [this.zoomValue,this.zoomValue];
|
||||
}
|
||||
if(dirty) {
|
||||
this.ea.addElementsToView();
|
||||
}
|
||||
this.close();
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -17,6 +17,35 @@ I develop this plugin as a hobby, spending my free time doing this. If you find
|
||||
|
||||
<div class="ex-coffee-div"><a href="https://ko-fi.com/zsolt"><img src="https://cdn.ko-fi.com/cdn/kofi3.png?v=3" height=45></a></div>
|
||||
`,
|
||||
"2.0.3":`
|
||||
## Fixed
|
||||
- Mermaid to Excalidraw stopped working after installing the Obsidian 1.5.0 insider build. [#1450](https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/1450)
|
||||
- CTRL+Click on a Mermaid diagram did not open the Mermaid editor.
|
||||
- Embed color settings were not honored when the embedded markdown was focused on a section or block.
|
||||
- Scrollbars were visible when the embeddable was set to transparent (set background color to match element background, and set element background color to "transparent").
|
||||
`,
|
||||
"2.0.2":`
|
||||
<div class="excalidraw-videoWrapper"><div>
|
||||
<iframe src="https://www.youtube.com/embed/502swdqvZ2A" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe>
|
||||
</div></div>
|
||||
|
||||
## Fixed
|
||||
- Resolved an issue where the Command Palette's "Toggle between Excalidraw and Markdown mode" failed to uncompress the Excalidraw JSON for editing.
|
||||
|
||||
## New
|
||||
- Scaling feature for embedded objects (markdown documents, pdfs, YouTube, etc.): Hold down the SHIFT key while resizing elements to adjust their size.
|
||||
- Expanded support for Canvas Candy. Regardless of Canvas Candy, you can apply CSS classes to embedded markdown documents for transparency, shape adjustments, text orientation, and more.
|
||||
- Added new functionalities to the active embeddable top-left menu:
|
||||
- Document Properties (cog icon)
|
||||
- File renaming
|
||||
- Basic styling options for embedded markdown documents
|
||||
- Setting YouTube start time
|
||||
- Zoom to full screen for PDFs
|
||||
- Improved immersive embedding of Excalidraw into Obsidian Canvas.
|
||||
- Introduced new Command Palette Actions:
|
||||
- Embeddable Properties
|
||||
- Scaling selected embeddable elements to 100% relative to the current canvas zoom.
|
||||
`,
|
||||
"2.0.1":`
|
||||
<div class="excalidraw-videoWrapper"><div>
|
||||
<iframe src="https://www.youtube.com/embed/xmqiBTrlbEM" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe>
|
||||
|
||||
@@ -182,7 +182,7 @@ export const EXCALIDRAW_AUTOMATE_INFO: SuggesterInfo[] = [
|
||||
},
|
||||
{
|
||||
field: "createPNG",
|
||||
code: "createPNG(templatePath?: string, scale?: number, exportSettings?: ExportSettings, loader?: EmbeddedFilesLoader, theme?: string,): Promise<any>;",
|
||||
code: "createPNG(templatePath?: string, scale?: number, exportSettings?: ExportSettings, loader?: EmbeddedFilesLoader, theme?: string,padding?: number): Promise<any>;",
|
||||
desc: "Use ExcalidrawAutomate.getExportSettings(boolean,boolean) to create an ExportSettings object.\nUse ExcalidrawAutomate.getEmbeddedFilesLoader(boolean?) to create an EmbeddedFilesLoader object.",
|
||||
after: "",
|
||||
},
|
||||
|
||||
@@ -11,6 +11,7 @@ export default {
|
||||
// main.ts
|
||||
PUBLISH_SVG_CHECK: "Obsidian Publish: Find SVG and PNG exports that are out of date",
|
||||
EMBEDDABLE_PROPERTIES: "Embeddable Properties",
|
||||
EMBEDDABLE_RELATIVE_ZOOM: "Scale selected embeddable elements to 100% relative to the current canvas zoom",
|
||||
OPEN_IMAGE_SOURCE: "Open Excalidraw drawing",
|
||||
INSTALL_SCRIPT: "Install the script",
|
||||
UPDATE_SCRIPT: "Update available - Click to install",
|
||||
@@ -84,7 +85,7 @@ export default {
|
||||
OPEN_LINK: "Open selected text as link\n(SHIFT+CLICK to open in a new pane)",
|
||||
EXPORT_EXCALIDRAW: "Export to an .Excalidraw file",
|
||||
LINK_BUTTON_CLICK_NO_TEXT:
|
||||
"Select a ImageElement, or select a TextElement that contains an internal or external link.\n",
|
||||
"Select an ImageElement, or select a TextElement that contains an internal or external link.\n",
|
||||
FILENAME_INVALID_CHARS:
|
||||
'File name cannot contain any of the following characters: * " \\ < > : | ? #',
|
||||
FORCE_SAVE:
|
||||
@@ -575,9 +576,9 @@ FILENAME_HEAD: "Filename",
|
||||
PROPERTIES: "Properties",
|
||||
|
||||
//EmbeddableSettings.tsx
|
||||
ES_TITLE: "Element Settings",
|
||||
ES_TITLE: "Embeddable Element Settings",
|
||||
ES_RENAME: "Rename File",
|
||||
ES_ZOOM: "Embedded Content Zoom",
|
||||
ES_ZOOM: "Embedded Content Scaling",
|
||||
ES_YOUTUBE_START: "YouTube Start Time",
|
||||
ES_YOUTUBE_START_DESC: "ss, mm:ss, hh:mm:ss",
|
||||
ES_YOUTUBE_START_INVALID: "The YouTube Start Time is invalid. Please check the format and try again",
|
||||
@@ -589,10 +590,12 @@ FILENAME_HEAD: "Filename",
|
||||
ES_BORDER_HEAD: "Embedded note border color",
|
||||
ES_BORDER_COLOR: "Border Color",
|
||||
ES_BORDER_MATCH_ELEMENT: "Match Element Border Color",
|
||||
ES_BACKGROUND_OPACITY: "Background Transparency",
|
||||
ES_BORDER_OPACITY: "Border Transparency",
|
||||
ES_BACKGROUND_OPACITY: "Background Opacity",
|
||||
ES_BORDER_OPACITY: "Border Opacity",
|
||||
ES_EMBEDDABLE_SETTINGS: "Embeddable Markdown Settings",
|
||||
ES_USE_OBSIDIAN_DEFAULTS: "Use Obsidian Defaults",
|
||||
ES_ZOOM_100_RELATIVE_DESC: "The button will adjust the element scale so it will show the content at 100% relative to the current zoom level of your canvas",
|
||||
ES_ZOOM_100: "Relative 100%",
|
||||
|
||||
//Prompts.ts
|
||||
PROMPT_FILE_DOES_NOT_EXIST: "File does not exist. Do you want to create it?",
|
||||
|
||||
52
src/main.ts
52
src/main.ts
@@ -50,7 +50,8 @@ import ExcalidrawView, { TextMode, getTextMode } from "./ExcalidrawView";
|
||||
import {
|
||||
changeThemeOfExcalidrawMD,
|
||||
getMarkdownDrawingSection,
|
||||
ExcalidrawData
|
||||
ExcalidrawData,
|
||||
REGEX_LINK
|
||||
} from "./ExcalidrawData";
|
||||
import {
|
||||
ExcalidrawSettings,
|
||||
@@ -118,6 +119,10 @@ import { StylesManager } from "./utils/StylesManager";
|
||||
import { MATHJAX_SOURCE_LZCOMPRESSED } from "./constMathJaxSource";
|
||||
import { PublishOutOfDateFilesDialog } from "./dialogs/PublishOutOfDateFiles";
|
||||
import { EmbeddableSettings } from "./dialogs/EmbeddableSettings";
|
||||
import { processLinkText } from "./utils/CustomEmbeddableUtils";
|
||||
import { getEA } from "src";
|
||||
import { ExcalidrawImperativeAPI } from "@zsviczian/excalidraw/types/types";
|
||||
import { Mutable } from "@zsviczian/excalidraw/types/utility-types";
|
||||
|
||||
declare const EXCALIDRAW_PACKAGES:string;
|
||||
declare const react:any;
|
||||
@@ -868,7 +873,41 @@ export default class ExcalidrawPlugin extends Plugin {
|
||||
return false;
|
||||
}
|
||||
if(checking) return true;
|
||||
new EmbeddableSettings(view.plugin,view,null,els[0]).open();
|
||||
const getFile = (el:ExcalidrawEmbeddableElement):TFile => {
|
||||
const res = REGEX_LINK.getRes(el.link).next();
|
||||
if(!res || (!res.value && res.done)) {
|
||||
return null;
|
||||
}
|
||||
const link = REGEX_LINK.getLink(res);
|
||||
const { file } = processLinkText(link, view);
|
||||
return file;
|
||||
}
|
||||
new EmbeddableSettings(view.plugin,view,getFile(els[0]),els[0]).open();
|
||||
}
|
||||
})
|
||||
|
||||
this.addCommand({
|
||||
id: "excalidraw-embeddables-relative-scale",
|
||||
name: t("EMBEDDABLE_RELATIVE_ZOOM"),
|
||||
checkCallback: (checking: boolean) => {
|
||||
const view = this.app.workspace.getActiveViewOfType(ExcalidrawView);
|
||||
if(!view) return false;
|
||||
if(!view.excalidrawAPI) return false;
|
||||
const els = view.getViewSelectedElements().filter(el=>el.type==="embeddable") as ExcalidrawEmbeddableElement[];
|
||||
if(els.length === 0) {
|
||||
if(checking) return false;
|
||||
new Notice("Select at least one embeddable element and try again");
|
||||
return false;
|
||||
}
|
||||
if(checking) return true;
|
||||
const ea = getEA(view) as ExcalidrawAutomate;
|
||||
const api = ea.getExcalidrawAPI() as ExcalidrawImperativeAPI;
|
||||
ea.copyViewElementsToEAforEditing(els);
|
||||
const scale = 1/api.getAppState().zoom.value;
|
||||
ea.getElements().forEach((el: Mutable<ExcalidrawEmbeddableElement>)=>{
|
||||
el.scale = [scale,scale];
|
||||
})
|
||||
ea.addElementsToView();
|
||||
}
|
||||
})
|
||||
|
||||
@@ -1644,10 +1683,7 @@ export default class ExcalidrawPlugin extends Plugin {
|
||||
|
||||
const excalidrawView = this.app.workspace.getActiveViewOfType(ExcalidrawView)
|
||||
if (excalidrawView) {
|
||||
const activeLeaf = excalidrawView.leaf;
|
||||
this.excalidrawFileModes[(activeLeaf as any).id || activeFile.path] =
|
||||
"markdown";
|
||||
this.setMarkdownView(activeLeaf);
|
||||
excalidrawView.openAsMarkdown();
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -2088,14 +2124,14 @@ export default class ExcalidrawPlugin extends Plugin {
|
||||
|
||||
//!Temporary hack
|
||||
//https://discord.com/channels/686053708261228577/817515900349448202/1031101635784613968
|
||||
if (app.isMobile && newActiveviewEV && !previouslyActiveEV) {
|
||||
if (this.app.isMobile && newActiveviewEV && !previouslyActiveEV) {
|
||||
const navbar = document.querySelector("body>.app-container>.mobile-navbar");
|
||||
if(navbar && navbar instanceof HTMLDivElement) {
|
||||
navbar.style.position="relative";
|
||||
}
|
||||
}
|
||||
|
||||
if (app.isMobile && !newActiveviewEV && previouslyActiveEV) {
|
||||
if (this.app.isMobile && !newActiveviewEV && previouslyActiveEV) {
|
||||
const navbar = document.querySelector("body>.app-container>.mobile-navbar");
|
||||
if(navbar && navbar instanceof HTMLDivElement) {
|
||||
navbar.style.position="";
|
||||
|
||||
@@ -190,7 +190,7 @@ export class EmbeddableMenu {
|
||||
title={t("ZOOM_TO_FIT")}
|
||||
action={() => {
|
||||
if(!element) return;
|
||||
api.zoomToFit([element], view.plugin.settings.zoomToFitMaxLevel, 0.1);
|
||||
api.zoomToFit([element], 30, 0.1);
|
||||
}}
|
||||
icon={ICONS.ZoomToSelectedElement}
|
||||
view={view}
|
||||
|
||||
@@ -12,9 +12,12 @@ export const getYouTubeStartAt = (url: string): string => {
|
||||
const hours = Math.floor(time / 3600);
|
||||
const minutes = Math.floor((time - hours * 3600) / 60);
|
||||
const seconds = time - hours * 3600 - minutes * 60;
|
||||
if(hours === 0 && minutes === 0 && seconds === 0) return "";
|
||||
if(hours === 0 && minutes === 0) return `${String(seconds).padStart(2, '0')}`;
|
||||
if(hours === 0) return `${String(minutes).padStart(2, '0')}:${String(seconds).padStart(2, '0')}`;
|
||||
return `${String(hours).padStart(2, '0')}:${String(minutes).padStart(2, '0')}:${String(seconds).padStart(2, '0')}`;
|
||||
}
|
||||
return "00:00:00";
|
||||
return "";
|
||||
};
|
||||
|
||||
export const isValidYouTubeStart = (value: string): boolean => {
|
||||
@@ -26,7 +29,9 @@ export const isValidYouTubeStart = (value: string): boolean => {
|
||||
export const updateYouTubeStartTime = (link: string, startTime: string): string => {
|
||||
const match = link.match(REG_YOUTUBE);
|
||||
if (match?.[2]) {
|
||||
const startTimeParam = `t=${timeStringToSeconds(startTime)}`;
|
||||
const startTimeParam = startTime === ""
|
||||
? ``
|
||||
: `t=${timeStringToSeconds(startTime)}`;
|
||||
let updatedLink = link;
|
||||
if (match[3]) {
|
||||
// If start time already exists, update it
|
||||
|
||||
10
styles.css
10
styles.css
@@ -477,6 +477,13 @@ hr.excalidraw-setting-hr {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.excalidraw__embeddable-container .canvas-node:not(.is-editing).transparent {
|
||||
::-webkit-scrollbar,
|
||||
::-webkit-scrollbar-horizontal {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.canvas-node:not(.is-editing):has(.excalidraw-canvas-immersive) {
|
||||
::-webkit-scrollbar,
|
||||
::-webkit-scrollbar-horizontal {
|
||||
@@ -488,4 +495,5 @@ hr.excalidraw-setting-hr {
|
||||
.canvas-node:not(.is-editing) .canvas-node-container:has(.excalidraw-canvas-immersive) {
|
||||
border: unset;
|
||||
box-shadow: unset;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user