1
Fork 0
mirror of https://github.com/glassflame/glassflame.github.io.git synced 2024-10-16 14:37:33 +00:00

Another iteration!

This commit is contained in:
Steffo 2023-10-22 00:37:05 +02:00
parent 45c3afd7d4
commit 93b91c5b6c
Signed by: steffo
GPG key ID: 2A24051445686895
11 changed files with 481 additions and 178 deletions

View file

@ -7,7 +7,7 @@
<inspection_tool class="HtmlUnknownTag" enabled="true" level="WARNING" enabled_by_default="true">
<option name="myValues">
<value>
<list size="9">
<list size="11">
<item index="0" class="java.lang.String" itemvalue="nobr" />
<item index="1" class="java.lang.String" itemvalue="noembed" />
<item index="2" class="java.lang.String" itemvalue="comment" />
@ -17,6 +17,8 @@
<item index="6" class="java.lang.String" itemvalue="x-card" />
<item index="7" class="java.lang.String" itemvalue="x-hashtag" />
<item index="8" class="java.lang.String" itemvalue="x-node-file" />
<item index="9" class="java.lang.String" itemvalue="x-canvas" />
<item index="10" class="java.lang.String" itemvalue="x-display" />
</list>
</value>
</option>

View file

@ -8,6 +8,24 @@
@import "style/light.css";
@import "style/dark.css";
:root {
--color-background: #ffffff;
--color-foreground: #000000;
--color-accent: #ff7f00;
--color-gray: #7E7E7E;
--color-red: #FB464C;
--color-orange: #E9973F;
--color-yellow: #E0DE71;
--color-green: #44CF6E;
--color-blue: #53DFDD;
--color-purple: #A882FF;
--edge-width: 2px;
--node-group-border-width: 2px;
--node-file-border-width: 2px;
}
body {
margin: 0;
@ -17,47 +35,101 @@
</style>
<script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
<script type="module" src="src/index.mjs"></script>
<template id="template-node-file">
<template id="template-display">
<slot name="display-container"></slot>
</template>
<template id="template-node-group">
<style>
.node {
.node-group {
position: absolute;
box-sizing: border-box;
--color-node: var(--color-gray);
outline: var(--node-group-border-width) solid var(--color-node);
background-color: color-mix(in srgb, var(--color-node) 20%, var(--color-background));
border-radius: 0 8px 8px 8px;
padding: 12px;
overflow-x: visible;
overflow-y: visible;
}
.node-group-label {
position: relative;
bottom: 14px;
left: -12px;
transform: translateY(-100%);
display: inline-block;
outline: var(--node-group-border-width) solid var(--color-node);
background-color: color-mix(in srgb, var(--color-node) 20%, var(--color-background));
border-radius: 8px 8px 0 0;
padding: 12px;
}
.node-group-label h1 {
margin: 0;
}
</style>
<section class="node-group">
<aside class="node-group-label">
<h1><slot name="node-label">{Group label}</slot></h1>
</aside>
<slot name="node-contents">{Node contents}</slot>
</section>
</template>
<template id="template-node-file">
<style>
.node-file {
outline-width: 2px;
position: absolute;
box-sizing: border-box;
--color-node: var(--color-gray);
outline: var(--node-file-border-width) solid var(--color-node);
background-color: color-mix(in srgb, var(--color-node) 10%, var(--color-background));
border-radius: 8px;
padding: 12px;
overflow-x: clip;
overflow-y: scroll;
}
.node-empty {
outline-style: dashed;
}
.node-full {
outline-style: solid;
}
</style>
<article class="node node-file">
<article class="node-file">
<h1>
<slot name="node-title">Node title</slot>
<slot name="node-title">{Node title}</slot>
</h1>
<slot name="node-contents">Node contents</slot>
<slot name="node-contents">{Node contents}</slot>
</article>
</template>
<template id="template-markdown">
<slot name="markdown-contents"></slot>
</template>
<template id="template-canvas">
<template id="template-node-text">
<style>
.canvas {
.node-text {
position: absolute;
box-sizing: border-box;
--color-node: var(--color-gray);
outline: var(--node-file-border-width) solid var(--color-node);
background-color: color-mix(in srgb, var(--color-node) 10%, var(--color-background));
border-radius: 8px;
padding: 12px;
overflow-x: clip;
overflow-y: scroll;
}
</style>
<div class="canvas"></div>
<article class="node-text">
<slot name="node-contents">{Node contents}</slot>
</article>
</template>
<template id="template-edge">
<slot name="edge-svg">{Edge SVG}</slot>
</template>
<template id="template-markdown">
<slot name="markdown-contents">{Markdown text}</slot>
</template>
<template id="template-canvas">
<slot name="canvas-contents">{Canvas}</slot>
</template>
<template id="template-wikilink">
<style>
@ -67,7 +139,7 @@
cursor: pointer;
}
</style>
<a class="wikilink"><slot name="wikilink-text">Wikilink text</slot></a>
<a class="wikilink"><slot name="wikilink-text">{Wikilink text}</slot></a>
</template>
<template id="template-hashtag">
<style>
@ -76,10 +148,10 @@
color: var(--color-accent);
}
</style>
<span class="hashtag"><slot name="hashtag-text">#Hashtag</slot></span>
<span class="hashtag"><slot name="hashtag-text">{#Hashtag}</slot></span>
</template>
</head>
<body>
<x-canvas contents=""/>
<x-display root vref="https://raw.githubusercontent.com/Steffo99/appunti-magistrali/main/" wref="8 - Crittografia applicata/1 - Concetti/3 - Casualità ed entropia/★ mappa concettuale.canvas"/>
</body>
</html>

View file

@ -1,25 +1,45 @@
import { fileDetails } from "../utils/file.mjs";
export class CanvasElement extends HTMLElement {
/**
* Return the closest {@link CanvasElement} ancestor in the tree.
*
* @param initial {HTMLElement} The element to start the search from.
*/
static findFirstCanvasAncestor(initial) {
let current = initial
while(current) {
if(current instanceof ShadowRoot) {
current = current.host
}
if(current instanceof CanvasElement) {
return current
}
current = current.parentNode
}
return null
}
static getTemplate() {
return document.getElementById("template-canvas")
}
parsedJSON
canvasElement
nodeElements = []
edgeElements = []
constructor() {
super()
this.parsedJSON = JSON.parse(this.getAttribute("contents"))
}
contentsSlotted
nodeElements = {}
edgeElements = {}
// noinspection JSUnusedGlobalSymbols
connectedCallback() {
const instanceDocument = CanvasElement.getTemplate().content.cloneNode(true)
const shadow = this.attachShadow({ mode: "open" })
this.canvasElement = instanceDocument.querySelector(".canvas")
this.parsedJSON = JSON.parse(this.getAttribute("contents"))
this.contentsSlotted = document.createElement("div")
this.contentsSlotted.slot = "canvas-contents"
let minX = { x: Infinity, width: 0 }
let minY = { y: Infinity, height: 0 }
@ -27,23 +47,15 @@ export class CanvasElement extends HTMLElement {
let maxY = { y: -Infinity, height: 0 }
for(const node of this.parsedJSON["nodes"]) {
if(node["type"] === "file") {
if(node["x"] < minX["x"]) minX = node
if(node["y"] < minY["y"]) minY = node
if(node["x"] + node["width"] > maxX["x"] + node["width"]) maxX = node
if(node["y"] + node["height"] > maxY["y"] + node["height"]) maxY = node
}
else {
console.warn("Encountered node of unimplemented type: ", node["type"])
}
}
console.debug("minX:", minX, "| minY:", minY, "| maxX:", maxX, "| maxY:", maxY)
for(const node of this.parsedJSON["nodes"]) {
if(node["type"] === "file") {
const element = document.createElement("x-node-file")
element.setAttribute("file", node["file"])
const element = document.createElement(`x-node-${node["type"]}`)
element.setAttribute("id", node["id"])
element.setAttribute("x", node["x"] - minX["x"])
element.setAttribute("y", node["y"] - minY["y"])
@ -51,17 +63,79 @@ export class CanvasElement extends HTMLElement {
element.setAttribute("height", node["height"])
element.setAttribute("color", node["color"])
this.nodeElements.push(element)
this.canvasElement.appendChild(element)
}
else {
switch(node["type"]) {
case "text":
element.setAttribute("text", node["text"])
break
case "file":
element.setAttribute("file", node["file"])
element.setAttribute("fileName", fileDetails(node["file"])[0])
break
case "group":
element.setAttribute("label", node["label"])
break
default:
console.warn("Encountered node of unimplemented type: ", node["type"])
}
break
}
this.canvasElement.style["width"] = `${maxX["x"] + maxX["width"] - minX["x"]}px`
this.canvasElement.style["height"] = `${maxY["y"] + maxY["height"] - minY["y"]}px`
this.nodeElements[node["id"]] = element
this.contentsSlotted.appendChild(element)
}
for(const edge of this.parsedJSON["edges"]) {
const element = document.createElement("x-edge")
element.setAttribute("id", edge["id"])
element.setAttribute("node-from", edge["fromNode"])
element.setAttribute("node-from-side", edge["fromSide"])
element.setAttribute("node-to", edge["toNode"])
element.setAttribute("node-to-side", edge["toSide"])
element.setAttribute("color", edge["color"])
element.setAttribute("arrows", edge["toEnd"])
this.edgeElements[edge["id"]] = element
this.contentsSlotted.appendChild(element)
}
console.log(Object.values(this.nodeElements))
this.contentsSlotted.style["width"] = `${maxX["x"] + maxX["width"] - minX["x"]}px`
this.contentsSlotted.style["height"] = `${maxY["y"] + maxY["height"] - minY["y"]}px`
this.appendChild(this.contentsSlotted)
shadow.appendChild(instanceDocument)
}
}
/**
* Element representing the generic skeleton of an Obsidian Canvas item.
*/
export class CanvasItemElement extends HTMLElement {
colorToHex() {
const color = this.getAttribute("color")
if(color?.startsWith("#")) {
// This is an hex color
return color
}
else {
// TODO: Check which colors correspond to what
return {
[undefined]: "var(--color-gray)",
"undefined": "var(--color-gray)",
"0": "var(--color-gray)",
"1": "var(--color-red)",
"2": "var(--color-orange)",
"3": "var(--color-yellow)",
"4": "var(--color-green)",
"5": "var(--color-blue)",
"6": "var(--color-purple)",
}[color]
}
}
}

82
src/elements/display.mjs Normal file
View file

@ -0,0 +1,82 @@
import { fileDetails } from "../utils/file.mjs";
import { CanvasElement } from "./canvas.mjs";
import { MarkdownElement } from "./markdown.mjs";
import { FetchError } from "./node.mjs";
export class DisplayElement extends HTMLElement {
/**
* Return the closest {@link DisplayElement} ancestor in the tree.
*
* @param initial {HTMLElement} The element to start the search from.
*/
static findFirstDisplayAncestor(initial) {
let current = initial
while(current) {
if(current instanceof ShadowRoot) {
current = current.host
}
if(current instanceof DisplayElement) {
return current
}
current = current.parentNode
}
return null
}
static getTemplate() {
return document.getElementById("template-display")
}
containerSlotted
loadButton
// noinspection JSUnusedGlobalSymbols
connectedCallback() {
const instanceDocument = DisplayElement.getTemplate().content.cloneNode(true)
const shadow = this.attachShadow({ mode: "open" })
this.containerSlotted = document.createElement("div")
this.containerSlotted.slot = "display-container"
this.loadButton = document.createElement("button")
this.loadButton.innerText = "Load"
this.loadButton.addEventListener("click", this.load.bind(this))
this.containerSlotted.appendChild(this.loadButton)
this.appendChild(this.containerSlotted)
shadow.appendChild(instanceDocument)
}
data
async fetchData() {
const vref = this.getAttribute("vref")
const wref = this.getAttribute("wref")
const url = new URL(wref, vref)
const response = await fetch(url, {})
if(!response.ok) throw new FetchError(response, "Fetch response is not ok")
this.data = await response.text()
}
async load() {
this.loadButton.disabled = true
await this.fetchData()
this.containerSlotted.remove()
this.containerSlotted = null
const [, fileExtension] = fileDetails(this.getAttribute("wref"))
this.containerSlotted = document.createElement({
"md": customElements.getName(MarkdownElement),
"canvas": this.getAttribute("root") !== undefined ? customElements.getName(CanvasElement) : "div",
}[fileExtension] ?? "div")
this.containerSlotted.slot = "display-container"
this.containerSlotted.setAttribute("contents", this.data)
this.appendChild(this.containerSlotted)
}
}

54
src/elements/edge.mjs Normal file
View file

@ -0,0 +1,54 @@
import { CanvasElement, CanvasItemElement } from "./canvas.mjs";
export class EdgeElement extends CanvasItemElement {
static getTemplate() {
return document.getElementById("template-edge")
}
svgSlotted
lineElement
// noinspection JSUnusedGlobalSymbols
connectedCallback() {
const instanceDocument = EdgeElement.getTemplate().content.cloneNode(true)
const shadow = this.attachShadow({ mode: "open" })
const canvas = CanvasElement.findFirstCanvasAncestor(this)
const fromNode = canvas.nodeElements[this.getAttribute("node-from")]
const fromSide = this.getAttribute("node-from-side")
const [x1, y1] = fromNode.getCenterCoordinatesOfSide(fromSide)
const toNode = canvas.nodeElements[this.getAttribute("node-to")]
const toSide = this.getAttribute("node-to-side")
const [x2, y2] = toNode.getCenterCoordinatesOfSide(toSide)
const minX = Math.min(x1, x2)
const minY = Math.min(y1, y2)
const diffX = Math.abs(x1 - x2)
const diffY = Math.abs(y1 - y2)
const arrows = this.getAttribute("arrows") ?? "end"
this.svgSlotted = document.createElementNS("http://www.w3.org/2000/svg", "svg")
this.svgSlotted.slot = "edge-svg"
this.svgSlotted.style.setProperty("position", "absolute")
this.svgSlotted.style.setProperty("left", "0")
this.svgSlotted.style.setProperty("top", "0")
this.svgSlotted.style.setProperty("overflow", "visible")
this.lineElement = document.createElementNS("http://www.w3.org/2000/svg", "line")
this.lineElement.setAttribute("x1", x1)
this.lineElement.setAttribute("y1", y1)
this.lineElement.setAttribute("x2", x2)
this.lineElement.setAttribute("y2", y2)
this.lineElement.style.setProperty("stroke", this.colorToHex())
this.lineElement.style.setProperty("stroke-width", "var(--edge-width)")
this.svgSlotted.appendChild(this.lineElement)
this.appendChild(this.svgSlotted)
shadow.appendChild(instanceDocument)
}
}

View file

@ -1,3 +1,5 @@
export {NodeFileElement} from "./node.mjs"
export {NodeFileElement, NodeGroupElement, NodeTextElement} from "./node.mjs"
export {MarkdownElement, HashtagElement, WikilinkElement} from "./markdown.mjs"
export {CanvasElement} from "./canvas.mjs"
export {DisplayElement} from "./display.mjs"
export {EdgeElement} from "./edge.mjs"

View file

@ -1,38 +1,7 @@
import { configFromWindow } from "../config.mjs";
import { CanvasItemElement } from "./canvas.mjs";
import { DisplayElement } from "./display.mjs";
/**
* Element representing the generic skeleton of an Obsidian Canvas node.
*/
export class NodeElement extends HTMLElement {
x
y
width
height
color
// noinspection JSUnusedGlobalSymbols
connectedCallback() {
this.id = this.getAttribute("id")
this.x = this.getAttribute("x")
this.y = this.getAttribute("y")
this.width = this.getAttribute("width")
this.height = this.getAttribute("height")
this.color = this.getAttribute("color")
}
colorToHex() {
if(this?.color?.startsWith("#")) {
// This is an hex color
return this.color
}
else {
// TODO: Check which colors correspond to what
return {}[this.color]
}
}
}
/**
* Error in the fetching of a file.
*/
@ -48,114 +17,130 @@ export class FetchError extends Error {
}
}
/**
* Element representing the skeleton of an Obsidian Canvas node pointing to a file.
*
* Requires the following attributes:
* - `file`: wref to the target file
* - `id`: id unique to the node
* - `x`: horizontal translation
* - `y`: vertical translation
* - `width`: width of the card in px
* - `height`: height of the card in px
* - (optional) `color`: custom Obsidian color of the card
*/
export class NodeElement extends CanvasItemElement {
getCenterCoordinatesOfSide(side) {
switch(side) {
case "top":
return [
Number(this.getAttribute("x")) + Number(this.getAttribute("width")) / 2,
Number(this.getAttribute("y")),
]
case "bottom":
return [
Number(this.getAttribute("x")) + Number(this.getAttribute("width")) / 2,
Number(this.getAttribute("y")) + Number(this.getAttribute("height")),
]
case "left":
return [
Number(this.getAttribute("x")),
Number(this.getAttribute("y")) + Number(this.getAttribute("height")) / 2,
]
case "right":
return [
Number(this.getAttribute("x")) + Number(this.getAttribute("width")),
Number(this.getAttribute("y")) + Number(this.getAttribute("height")) / 2,
]
}
}
}
export class NodeGroupElement extends NodeElement {
static getTemplate() {
return document.getElementById("template-node-group")
}
instanceElement
labelSlotted
// noinspection JSUnusedGlobalSymbols
connectedCallback() {
const instanceDocument = NodeGroupElement.getTemplate().content.cloneNode(true)
const shadow = this.attachShadow({ mode: "open" })
this.instanceElement = instanceDocument.querySelector(".node-group")
this.instanceElement.style.setProperty("left", `${this.getAttribute("x")}px`)
this.instanceElement.style.setProperty("top", `${this.getAttribute("y")}px`)
this.instanceElement.style.setProperty("width", `${this.getAttribute("width")}px`)
this.instanceElement.style.setProperty("height", `${this.getAttribute("height")}px`)
this.instanceElement.style.setProperty("--color-node", this.colorToHex())
this.labelSlotted = document.createElement("span")
this.labelSlotted.slot = "node-label"
this.labelSlotted.innerText = this.getAttribute("label")
this.appendChild(this.labelSlotted)
shadow.appendChild(instanceDocument)
}
}
export class NodeFileElement extends NodeElement {
static type = "file"
file
fileName
fileExtension
fileLeaf() {
return this.file.split("/").at(-1)
}
fileDetails() {
const split = this.fileLeaf().split(".")
const name = split.slice(0, -1)
const extension = split.at(-1)
return [name, extension]
}
static getTemplate() {
return document.getElementById("template-node-file")
}
instanceElement
nameSlotted
placeholderSlotted
contentsSlotted
loadButton
// noinspection JSUnusedGlobalSymbols
connectedCallback() {
super.connectedCallback()
this.file = this.getAttribute("file")
const [fileName, fileExtension] = this.fileDetails()
this.fileName = fileName
this.fileExtension = fileExtension
const instanceDocument = NodeFileElement.getTemplate().content.cloneNode(true)
const shadow = this.attachShadow({ mode: "open" })
this.instanceElement = instanceDocument.querySelector(".node.node-file")
this.instanceElement = instanceDocument.querySelector(".node-file")
this.instanceElement.style["left"] = `${this.x}px`
this.instanceElement.style["top"] = `${this.y}px`
this.instanceElement.style["width"] = `${this.width}px`
this.instanceElement.style["height"] = `${this.height}px`
this.instanceElement.style["--node-color"] = this.colorToHex()
this.instanceElement.classList.add("node-empty")
this.instanceElement.classList.remove("node-full")
this.instanceElement.style.setProperty("left", `${this.getAttribute("x")}px`)
this.instanceElement.style.setProperty("top", `${this.getAttribute("y")}px`)
this.instanceElement.style.setProperty("width", `${this.getAttribute("width")}px`)
this.instanceElement.style.setProperty("height", `${this.getAttribute("height")}px`)
this.instanceElement.style.setProperty("--color-node", this.colorToHex())
this.nameSlotted = document.createElement("span")
this.nameSlotted.slot = "node-title"
this.nameSlotted.innerText = this.fileName
this.nameSlotted.innerText = this.getAttribute("fileName")
this.appendChild(this.nameSlotted)
this.placeholderSlotted = document.createElement("div")
this.placeholderSlotted.slot = "node-contents"
this.loadButton = document.createElement("button")
this.loadButton.innerText = "Load"
this.loadButton.addEventListener("click", this.fillNode.bind(this))
this.placeholderSlotted.appendChild(this.loadButton)
this.appendChild(this.placeholderSlotted)
this.contentsSlotted = document.createElement(customElements.getName(DisplayElement))
this.contentsSlotted.slot = "node-contents"
const firstDisplayAncestor = DisplayElement.findFirstDisplayAncestor(this)
this.contentsSlotted.setAttribute("vref", firstDisplayAncestor.getAttribute("vref"))
this.contentsSlotted.setAttribute("wref", this.getAttribute("file"))
this.appendChild(this.contentsSlotted)
shadow.appendChild(instanceDocument)
}
contents
async fetchContents() {
console.info("Fetching:", this.file)
const url = new URL(this.file, configFromWindow()["vault"])
const response = await fetch(url, {})
if(!response.ok) throw new FetchError(response, "Fetch response is not ok")
this.contents = await response.text()
}
async fillNode() {
this.loadButton.disabled = true
export class NodeTextElement extends NodeElement {
static getTemplate() {
return document.getElementById("template-node-text")
}
this.placeholderSlotted.remove()
this.placeholderSlotted = null
instanceElement
contentsSlotted
await this.fetchContents()
// noinspection JSUnusedGlobalSymbols
connectedCallback() {
const instanceDocument = NodeTextElement.getTemplate().content.cloneNode(true)
const shadow = this.attachShadow({ mode: "open" })
this.instanceElement.classList.remove("node-empty")
this.instanceElement.classList.add("node-full")
this.instanceElement = instanceDocument.querySelector(".node-text")
this.contentsSlotted = document.createElement({
"md": "x-markdown",
"canvas": "x-canvas",
}[this.fileExtension] ?? "div")
this.instanceElement.style.setProperty("left", `${this.getAttribute("x")}px`)
this.instanceElement.style.setProperty("top", `${this.getAttribute("y")}px`)
this.instanceElement.style.setProperty("width", `${this.getAttribute("width")}px`)
this.instanceElement.style.setProperty("height", `${this.getAttribute("height")}px`)
this.instanceElement.style.setProperty("--color-node", this.colorToHex())
this.contentsSlotted = document.createElement("x-markdown")
this.contentsSlotted.slot = "node-contents"
this.contentsSlotted.setAttribute("contents", this.contents)
this.appendChild(this.contentsSlotted)
this.contentsSlotted.setAttribute("contents", this.getAttribute("text"))
shadow.appendChild(instanceDocument)
}
}

View file

@ -1,7 +1,11 @@
import { CanvasElement, HashtagElement, MarkdownElement, NodeFileElement, WikilinkElement } from "./elements/index.mjs";
import { CanvasElement, HashtagElement, MarkdownElement, NodeFileElement, WikilinkElement, DisplayElement, EdgeElement, NodeGroupElement, NodeTextElement } from "./elements/index.mjs";
customElements.define("x-node-file", NodeFileElement)
customElements.define("x-node-text", NodeTextElement)
customElements.define("x-node-group", NodeGroupElement)
customElements.define("x-markdown", MarkdownElement)
customElements.define("x-wikilink", WikilinkElement)
customElements.define("x-hashtag", HashtagElement)
customElements.define("x-canvas", CanvasElement)
customElements.define("x-display", DisplayElement)
customElements.define("x-edge", EdgeElement)

12
src/utils/file.mjs Normal file
View file

@ -0,0 +1,12 @@
/**
* Parse a file path to get the file name and extension.
*
* @param file The file path to parse.
* @returns {[String, String]} The file's name and extension without the dot respectively.
*/
export function fileDetails(file) {
const split = file.split("/").at(-1).split(".")
const name = split.slice(0, -1)
const extension = split.at(-1)
return [name, extension]
}

View file

@ -1,7 +1,15 @@
@media (prefers-color-scheme: dark) {
:root {
@media screen and (prefers-color-scheme: dark) {
:root:root {
--color-background: #1e1e1e;
--color-foreground: #ffffff;
--color-accent: #ff7f00;
--color-gray: #7E7E7E;
--color-red: #FB464C;
--color-orange: #E9973F;
--color-yellow: #E0DE71;
--color-green: #44CF6E;
--color-blue: #53DFDD;
--color-purple: #A882FF;
}
}

View file

@ -1,7 +1,15 @@
@media (prefers-color-scheme: light) {
:root {
@media screen and (prefers-color-scheme: light) {
:root:root {
--color-background: #ffffff;
--color-foreground: #1e1e1e;
--color-accent: #ff7f00;
--color-gray: #7E7E7E;
--color-red: #FB464C;
--color-orange: #E9973F;
--color-yellow: #E0DE71;
--color-green: #44CF6E;
--color-blue: #53DFDD;
--color-purple: #A882FF;
}
}