diff --git a/wco/crypto/crypto.js b/wco/crypto/crypto.js index 7f52ffb..baecb49 100644 --- a/wco/crypto/crypto.js +++ b/wco/crypto/crypto.js @@ -58,4 +58,53 @@ apx.crypto.decryptMessage = async (encryptedMessage, privateKey) => { ); }; +apx.crypto.sign = async (message, privateKey) => { + privateKey = await openpgp.readPrivateKey( + { + armoredKey: privateKey + } + ); + + return await openpgp.sign( + { + message: await openpgp.createMessage( + { + text: message + } + ), + signingKeys: privateKey + } + ); +}; + +apx.crypto.verifySignature = async (message, signature, publicKey) => { + publicKey = await openpgp.readKey( + { + armoredKey: publicKey + } + ); + + const verified = await openpgp.verify( + { + message: await openpgp.createMessage( + { + text: message + } + ), + signature: await openpgp.readSignature( + { + armoredSignature: signature + } + ), + verificationKeys: publicKey + } + ); + + if (await verified.signatures[0].verified) { + return true; + } else { + return false; + }; +}; + export default apx; \ No newline at end of file diff --git a/wco/privatri/alias.mustache b/wco/privatri/alias.mustache new file mode 100644 index 0000000..bfabfaa --- /dev/null +++ b/wco/privatri/alias.mustache @@ -0,0 +1,6 @@ + \ No newline at end of file diff --git a/wco/privatri/apx.js b/wco/privatri/apx.js new file mode 100644 index 0000000..a64f712 --- /dev/null +++ b/wco/privatri/apx.js @@ -0,0 +1,194 @@ +var apx = apx || {}; + +apx.crypto = apx.crypto || {}; +apx.indexedDB = apx.indexedDB || {}; + +apx.crypto.genKey = async (uuid) => { + return await openpgp.generateKey( + { + type: "ecc", + curve: "curve25519", + userIDs: [ + { + alias: uuid + } + ], + passphrase: "", + format: "armored", + } + ); +}; + +apx.crypto.encryptMessage = async (message, publicKey) => { + publicKey = await openpgp.readKey( + { + armoredKey: publicKey + } + ); + + return await openpgp.encrypt( + { + message: await openpgp.createMessage( + { + text: message + } + ), + encryptionKeys: publicKey + } + ); +}; + +apx.crypto.decryptMessage = async (encryptedMessage, privateKey) => { + privateKey = await openpgp.readPrivateKey( + { + armoredKey: privateKey + } + ); + + const message = await openpgp.readMessage( + { + armoredMessage: encryptedMessage + } + ); + + return await openpgp.decrypt( + { + message, + decryptionKeys: privateKey + } + ); +}; + +apx.crypto.sign = async (message, privateKey) => { + privateKey = await openpgp.readPrivateKey( + { + armoredKey: privateKey + } + ); + + return await openpgp.sign( + { + message: await openpgp.createMessage( + { + text: message + } + ), + signingKeys: privateKey + } + ); +}; + +apx.crypto.verifySignature = async (message, signature, publicKey) => { + publicKey = await openpgp.readKey( + { + armoredKey: publicKey + } + ); + + const verified = await openpgp.verify( + { + message: await openpgp.createMessage( + { + text: message + } + ), + signature: await openpgp.readSignature( + { + armoredSignature: signature + } + ), + verificationKeys: publicKey + } + ); + + if (await verified.signatures[0].verified) { + return true; + } else { + return false; + }; +}; + +apx.indexedDB.set = async (db, storeName, value) => { + return new Promise((resolve, reject) => { + const request = indexedDB.open(db, 1); + + request.onupgradeneeded = (event) => { + const db = event.target.result; + + if (!db.objectStoreNames.contains("threads")) { + db.createObjectStore("threads", { keyPath: "uuid" }); + }; + + if (!db.objectStoreNames.contains("messages")) { + db.createObjectStore("messages", { keyPath: "privatriid" }); + }; + }; + + request.onsuccess = (event) => { + const db = event.target.result; + + if (!db.objectStoreNames.contains(storeName)) { + return resolve(); + }; + + const transaction = db.transaction(storeName, "readwrite"); + const store = transaction.objectStore(storeName); + + const putRequest = store.put(value); + putRequest.onsuccess = () => resolve(); + putRequest.onerror = (error) => reject(error); + }; + + request.onerror = (error) => reject(error); + }); +}; + +apx.indexedDB.get = async (db, storeName, key) => { + return new Promise((resolve, reject) => { + const request = indexedDB.open(db, 1); + + request.onsuccess = (event) => { + const db = event.target.result; + if (!db.objectStoreNames.contains(storeName)) { + return resolve(null); + } + const transaction = db.transaction(storeName, "readonly"); + const store = transaction.objectStore(storeName); + + const getRequest = store.get(key); + + getRequest.onsuccess = () => { + resolve(getRequest.result || null); + }; + + getRequest.onerror = () => resolve(null); + }; + + request.onerror = (error) => reject(error); + }); +}; + +apx.indexedDB.del = async (db, storeName, key) => { + return new Promise((resolve, reject) => { + const request = indexedDB.open(db, 1); + + request.onsuccess = (event) => { + const db = event.target.result; + + if (!db.objectStoreNames.contains(storeName)) { + return resolve(); + }; + + const transaction = db.transaction(storeName, "readwrite"); + const store = transaction.objectStore(storeName); + + const deleteRequest = store.delete(key); + deleteRequest.onsuccess = () => resolve(); + deleteRequest.onerror = (error) => reject(error); + }; + + request.onerror = (error) => reject(error); + }); +}; + +export default apx; \ No newline at end of file diff --git a/wco/privatri/assets/icon.png b/wco/privatri/assets/icon.png new file mode 100644 index 0000000..c64489c Binary files /dev/null and b/wco/privatri/assets/icon.png differ diff --git a/wco/privatri/createThread.js b/wco/privatri/createThread.js new file mode 100644 index 0000000..b29119f --- /dev/null +++ b/wco/privatri/createThread.js @@ -0,0 +1,44 @@ +import apx from "./apx.js"; + +const autoDeletionBtnElArray = document.querySelectorAll("li.autoDeletionBtn"); + +autoDeletionBtnElArray.forEach(btn => { + btn.addEventListener("click", () => { + autoDeletionBtnElArray.forEach(btn => btn.classList.remove("bg-base-200")); + btn.classList.add("bg-base-200"); + }); +}); + +document.querySelector("#createThreadBtn").addEventListener("click", async () => { + const { publicKey, privateKey } = await apx.crypto.genKey(); + + const messageObj = await (async (publicKey) => { + const uuid = crypto.randomUUID(); + const timestamp = Date.now(); + const alias = await apx.crypto.encryptMessage(JSON.parse(localStorage.getItem("apx")).data.headers.xalias, publicKey); + + return { + privatriid: `${uuid}_${timestamp}`, + thread: uuid, + timestamp: timestamp, + owner: alias, + title: await apx.crypto.encryptMessage(document.querySelector("#threadNameInput").value, publicKey), + sender_alias: alias, + publicKey: publicKey, + aliases: [ + alias + ], + message: await apx.crypto.encryptMessage(document.querySelector("#threadDescriptionInput").value, publicKey), + dt_autodestruction: (() => { + const selectedBtn = Array.from(autoDeletionBtnElArray).find(btn => btn.classList.contains("bg-base-200")); + return parseInt(selectedBtn ? selectedBtn.getAttribute("data-auto-deletion") : 0, 10); + })(), + urgencydeletion: document.querySelector('input[type="checkbox"].toggle').checked + }; + })(publicKey); + + // Faire un post sur l'endpoint /privatri + + await apx.indexedDB.set("privatri", "threads", { uuid: messageObj.thread, privateKey: privateKey }); + await apx.indexedDB.set("privatri", "messages", messageObj); +}); \ No newline at end of file diff --git a/wco/privatri/createThread.mustache b/wco/privatri/createThread.mustache new file mode 100644 index 0000000..cfbf198 --- /dev/null +++ b/wco/privatri/createThread.mustache @@ -0,0 +1,34 @@ +
+

New thread

+
+ + + + +
+ + +
+ +

Urgency deletion

+
+ +
\ No newline at end of file diff --git a/wco/privatri/editMessage.mustache b/wco/privatri/editMessage.mustache new file mode 100644 index 0000000..6358d23 --- /dev/null +++ b/wco/privatri/editMessage.mustache @@ -0,0 +1,16 @@ + +
+ {{date}} +
+ + +
+
\ No newline at end of file diff --git a/wco/privatri/inviteAlias.js b/wco/privatri/inviteAlias.js new file mode 100644 index 0000000..29c3247 --- /dev/null +++ b/wco/privatri/inviteAlias.js @@ -0,0 +1,52 @@ +const url = "https://www.facebook.com/"; + +const bodyEl = document.querySelector("body"); +const qrCodeCanvasEl = document.querySelector("#qrCodeCanvas"); +const invitationLinkInputEl = document.querySelector("#invitationLinkInput"); + +const displayToastAlert = async (message) => { + let toastAlertTemplate = ""; + + await fetch("./toastAlert.mustache") + .then(res => res.text()) + .then(template => { + toastAlertTemplate = template; + }); + + return Mustache.render(toastAlertTemplate, { message }); +}; + +(async () => { + const qrCode = new QRCodeStyling({ + width: 425, + height: 425, + type: "svg", + data: url, + image: "./assets/icon.png", + dotsOptions: { + color: "#ffffff", + type: "rounded" + }, + backgroundOptions: { + color: getComputedStyle(bodyEl).backgroundColor || "#000000", + }, + imageOptions: { + crossOrigin: "anonymous", + margin: 20 + } + }); + + qrCode.append(qrCodeCanvasEl); + + invitationLinkInputEl.value = url; + + copyBtn.addEventListener("click", async () => { + navigator.clipboard.writeText(invitationLinkInputEl.value); + + bodyEl.insertAdjacentHTML("beforeend", await displayToastAlert("Invitation link copied to clipboard.")); + + setTimeout(() => { + bodyEl.lastElementChild.remove(); + }, 3000); + }); +})(); \ No newline at end of file diff --git a/wco/privatri/inviteAlias.mustache b/wco/privatri/inviteAlias.mustache new file mode 100644 index 0000000..ce01b6d --- /dev/null +++ b/wco/privatri/inviteAlias.mustache @@ -0,0 +1,12 @@ +
+

Invite an alias into this thread

+
+
+ + +
+
\ No newline at end of file diff --git a/wco/privatri/main_fr.html b/wco/privatri/main_fr.html new file mode 100644 index 0000000..d663c77 --- /dev/null +++ b/wco/privatri/main_fr.html @@ -0,0 +1,108 @@ + + + + + + + + + + + Document + + +
+
+ + + +
+
+ + + + + +
+
+
+
+ {threadName} +
+ +
+
+
+ +
+ + +
+
+ + \ No newline at end of file diff --git a/wco/privatri/main_fr.mustache b/wco/privatri/main_fr.mustache index 1a6a152..fc6c1a7 100644 --- a/wco/privatri/main_fr.mustache +++ b/wco/privatri/main_fr.mustache @@ -1,8 +1,93 @@ -
- -

Messages Privés (privatri)

-

Ce composant gère le stockage sécurisé des messages dans le navigateur.

-
- -
-
+
+
+ + + +
+
+ + + + + +
+
+
+
+ {threadName} +
+ +
+
+
+ +
+ + +
+
\ No newline at end of file diff --git a/wco/privatri/message.mustache b/wco/privatri/message.mustache new file mode 100644 index 0000000..9aa563f --- /dev/null +++ b/wco/privatri/message.mustache @@ -0,0 +1,23 @@ +
+
+
+ {{message}} +
+
+
+
+ {{date}} + +
+
\ No newline at end of file diff --git a/wco/privatri/output.css b/wco/privatri/output.css new file mode 100644 index 0000000..e25d2bc --- /dev/null +++ b/wco/privatri/output.css @@ -0,0 +1,2733 @@ +/*! tailwindcss v4.1.11 | MIT License | https://tailwindcss.com */ +@layer properties; +@layer theme, base, components, utilities; +@layer theme { + :root, :host { + --font-sans: ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", + "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"; + --font-mono: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", + "Courier New", monospace; + --spacing: 0.25rem; + --container-xs: 20rem; + --text-sm: 0.875rem; + --text-sm--line-height: calc(1.25 / 0.875); + --text-base: 1rem; + --text-base--line-height: calc(1.5 / 1); + --text-lg: 1.125rem; + --text-lg--line-height: calc(1.75 / 1.125); + --text-xl: 1.25rem; + --text-xl--line-height: calc(1.75 / 1.25); + --text-4xl: 2.25rem; + --text-4xl--line-height: calc(2.5 / 2.25); + --font-weight-medium: 500; + --font-weight-semibold: 600; + --radius-lg: 0.5rem; + --ease-out: cubic-bezier(0, 0, 0.2, 1); + --ease-in-out: cubic-bezier(0.4, 0, 0.2, 1); + --default-transition-duration: 150ms; + --default-transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); + --default-font-family: var(--font-sans); + --default-mono-font-family: var(--font-mono); + } +} +@layer base { + *, ::after, ::before, ::backdrop, ::file-selector-button { + box-sizing: border-box; + margin: 0; + padding: 0; + border: 0 solid; + } + html, :host { + line-height: 1.5; + -webkit-text-size-adjust: 100%; + tab-size: 4; + font-family: var(--default-font-family, ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"); + font-feature-settings: var(--default-font-feature-settings, normal); + font-variation-settings: var(--default-font-variation-settings, normal); + -webkit-tap-highlight-color: transparent; + } + hr { + height: 0; + color: inherit; + border-top-width: 1px; + } + abbr:where([title]) { + -webkit-text-decoration: underline dotted; + text-decoration: underline dotted; + } + h1, h2, h3, h4, h5, h6 { + font-size: inherit; + font-weight: inherit; + } + a { + color: inherit; + -webkit-text-decoration: inherit; + text-decoration: inherit; + } + b, strong { + font-weight: bolder; + } + code, kbd, samp, pre { + font-family: var(--default-mono-font-family, ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace); + font-feature-settings: var(--default-mono-font-feature-settings, normal); + font-variation-settings: var(--default-mono-font-variation-settings, normal); + font-size: 1em; + } + small { + font-size: 80%; + } + sub, sup { + font-size: 75%; + line-height: 0; + position: relative; + vertical-align: baseline; + } + sub { + bottom: -0.25em; + } + sup { + top: -0.5em; + } + table { + text-indent: 0; + border-color: inherit; + border-collapse: collapse; + } + :-moz-focusring { + outline: auto; + } + progress { + vertical-align: baseline; + } + summary { + display: list-item; + } + ol, ul, menu { + list-style: none; + } + img, svg, video, canvas, audio, iframe, embed, object { + display: block; + vertical-align: middle; + } + img, video { + max-width: 100%; + height: auto; + } + button, input, select, optgroup, textarea, ::file-selector-button { + font: inherit; + font-feature-settings: inherit; + font-variation-settings: inherit; + letter-spacing: inherit; + color: inherit; + border-radius: 0; + background-color: transparent; + opacity: 1; + } + :where(select:is([multiple], [size])) optgroup { + font-weight: bolder; + } + :where(select:is([multiple], [size])) optgroup option { + padding-inline-start: 20px; + } + ::file-selector-button { + margin-inline-end: 4px; + } + ::placeholder { + opacity: 1; + } + @supports (not (-webkit-appearance: -apple-pay-button)) or (contain-intrinsic-size: 1px) { + ::placeholder { + color: currentcolor; + @supports (color: color-mix(in lab, red, red)) { + color: color-mix(in oklab, currentcolor 50%, transparent); + } + } + } + textarea { + resize: vertical; + } + ::-webkit-search-decoration { + -webkit-appearance: none; + } + ::-webkit-date-and-time-value { + min-height: 1lh; + text-align: inherit; + } + ::-webkit-datetime-edit { + display: inline-flex; + } + ::-webkit-datetime-edit-fields-wrapper { + padding: 0; + } + ::-webkit-datetime-edit, ::-webkit-datetime-edit-year-field, ::-webkit-datetime-edit-month-field, ::-webkit-datetime-edit-day-field, ::-webkit-datetime-edit-hour-field, ::-webkit-datetime-edit-minute-field, ::-webkit-datetime-edit-second-field, ::-webkit-datetime-edit-millisecond-field, ::-webkit-datetime-edit-meridiem-field { + padding-block: 0; + } + :-moz-ui-invalid { + box-shadow: none; + } + button, input:where([type="button"], [type="reset"], [type="submit"]), ::file-selector-button { + appearance: button; + } + ::-webkit-inner-spin-button, ::-webkit-outer-spin-button { + height: auto; + } + [hidden]:where(:not([hidden="until-found"])) { + display: none !important; + } +} +@layer utilities { + .drawer-side { + pointer-events: none; + visibility: hidden; + position: fixed; + inset-inline-start: calc(0.25rem * 0); + top: calc(0.25rem * 0); + z-index: 1; + grid-column-start: 1; + grid-row-start: 1; + display: grid; + width: 100%; + grid-template-columns: repeat(1, minmax(0, 1fr)); + grid-template-rows: repeat(1, minmax(0, 1fr)); + align-items: flex-start; + justify-items: start; + overflow-x: hidden; + overflow-y: hidden; + overscroll-behavior: contain; + opacity: 0%; + transition: opacity 0.2s ease-out 0.1s allow-discrete, visibility 0.3s ease-out 0.1s allow-discrete; + height: 100vh; + height: 100dvh; + > .drawer-overlay { + position: sticky; + top: calc(0.25rem * 0); + cursor: pointer; + place-self: stretch; + background-color: oklch(0% 0 0 / 40%); + } + > * { + grid-column-start: 1; + grid-row-start: 1; + } + > *:not(.drawer-overlay) { + will-change: transform; + transition: translate 0.3s ease-out; + translate: -100%; + [dir="rtl"] & { + translate: 100%; + } + } + } + .drawer-open { + > .drawer-side { + overflow-y: auto; + } + > .drawer-toggle { + display: none; + & ~ .drawer-side { + pointer-events: auto; + visibility: visible; + position: sticky; + display: block; + width: auto; + overscroll-behavior: auto; + opacity: 100%; + & > .drawer-overlay { + cursor: default; + background-color: transparent; + } + & > *:not(.drawer-overlay) { + translate: 0%; + [dir="rtl"] & { + translate: 0%; + } + } + } + &:checked ~ .drawer-side { + pointer-events: auto; + visibility: visible; + } + } + } + .drawer-toggle { + position: fixed; + height: calc(0.25rem * 0); + width: calc(0.25rem * 0); + appearance: none; + opacity: 0%; + &:checked { + & ~ .drawer-side { + pointer-events: auto; + visibility: visible; + overflow-y: auto; + opacity: 100%; + & > *:not(.drawer-overlay) { + translate: 0%; + } + } + } + &:focus-visible ~ .drawer-content label.drawer-button { + outline: 2px solid; + outline-offset: 2px; + } + } + .menu { + display: flex; + width: fit-content; + flex-direction: column; + flex-wrap: wrap; + padding: calc(0.25rem * 2); + --menu-active-fg: var(--color-neutral-content); + --menu-active-bg: var(--color-neutral); + font-size: 0.875rem; + :where(li ul) { + position: relative; + margin-inline-start: calc(0.25rem * 4); + padding-inline-start: calc(0.25rem * 2); + white-space: nowrap; + &:before { + position: absolute; + inset-inline-start: calc(0.25rem * 0); + top: calc(0.25rem * 3); + bottom: calc(0.25rem * 3); + background-color: var(--color-base-content); + opacity: 10%; + width: var(--border); + content: ""; + } + } + :where(li > .menu-dropdown:not(.menu-dropdown-show)) { + display: none; + } + :where(li:not(.menu-title) > *:not(ul, details, .menu-title, .btn)), :where(li:not(.menu-title) > details > summary:not(.menu-title)) { + display: grid; + grid-auto-flow: column; + align-content: flex-start; + align-items: center; + gap: calc(0.25rem * 2); + border-radius: var(--radius-field); + padding-inline: calc(0.25rem * 3); + padding-block: calc(0.25rem * 1.5); + text-align: start; + transition-property: color, background-color, box-shadow; + transition-duration: 0.2s; + transition-timing-function: cubic-bezier(0, 0, 0.2, 1); + grid-auto-columns: minmax(auto, max-content) auto max-content; + text-wrap: balance; + user-select: none; + } + :where(li > details > summary) { + --tw-outline-style: none; + outline-style: none; + @media (forced-colors: active) { + outline: 2px solid transparent; + outline-offset: 2px; + } + &::-webkit-details-marker { + display: none; + } + } + :where(li > details > summary), :where(li > .menu-dropdown-toggle) { + &:after { + justify-self: flex-end; + display: block; + height: 0.375rem; + width: 0.375rem; + rotate: -135deg; + translate: 0 -1px; + transition-property: rotate, translate; + transition-duration: 0.2s; + content: ""; + transform-origin: 50% 50%; + box-shadow: 2px 2px inset; + pointer-events: none; + } + } + :where(li > details[open] > summary):after, :where(li > .menu-dropdown-toggle.menu-dropdown-show):after { + rotate: 45deg; + translate: 0 1px; + } + :where( li:not(.menu-title, .disabled) > *:not(ul, details, .menu-title), li:not(.menu-title, .disabled) > details > summary:not(.menu-title) ):not(.menu-active, :active, .btn) { + &.menu-focus, &:focus-visible { + cursor: pointer; + background-color: var(--color-base-content); + @supports (color: color-mix(in lab, red, red)) { + background-color: color-mix(in oklab, var(--color-base-content) 10%, transparent); + } + color: var(--color-base-content); + --tw-outline-style: none; + outline-style: none; + @media (forced-colors: active) { + outline: 2px solid transparent; + outline-offset: 2px; + } + } + } + :where( li:not(.menu-title, .disabled) > *:not(ul, details, .menu-title):not(.menu-active, :active, .btn):hover, li:not(.menu-title, .disabled) > details > summary:not(.menu-title):not(.menu-active, :active, .btn):hover ) { + cursor: pointer; + background-color: var(--color-base-content); + @supports (color: color-mix(in lab, red, red)) { + background-color: color-mix(in oklab, var(--color-base-content) 10%, transparent); + } + --tw-outline-style: none; + outline-style: none; + @media (forced-colors: active) { + outline: 2px solid transparent; + outline-offset: 2px; + } + box-shadow: 0 1px oklch(0% 0 0 / 0.01) inset, 0 -1px oklch(100% 0 0 / 0.01) inset; + } + :where(li:empty) { + background-color: var(--color-base-content); + opacity: 10%; + margin: 0.5rem 1rem; + height: 1px; + } + :where(li) { + position: relative; + display: flex; + flex-shrink: 0; + flex-direction: column; + flex-wrap: wrap; + align-items: stretch; + .badge { + justify-self: flex-end; + } + & > *:not(ul, .menu-title, details, .btn):active, & > *:not(ul, .menu-title, details, .btn).menu-active, & > details > summary:active { + --tw-outline-style: none; + outline-style: none; + @media (forced-colors: active) { + outline: 2px solid transparent; + outline-offset: 2px; + } + color: var(--menu-active-fg); + background-color: var(--menu-active-bg); + background-size: auto, calc(var(--noise) * 100%); + background-image: none, var(--fx-noise); + &:not(&:active) { + box-shadow: 0 2px calc(var(--depth) * 3px) -2px var(--menu-active-bg); + } + } + &.menu-disabled { + pointer-events: none; + color: var(--color-base-content); + @supports (color: color-mix(in lab, red, red)) { + color: color-mix(in oklab, var(--color-base-content) 20%, transparent); + } + } + } + .dropdown:focus-within { + .menu-dropdown-toggle:after { + rotate: 45deg; + translate: 0 1px; + } + } + .dropdown-content { + margin-top: calc(0.25rem * 2); + padding: calc(0.25rem * 2); + &:before { + display: none; + } + } + } + .dropdown { + position: relative; + display: inline-block; + position-area: var(--anchor-v, bottom) var(--anchor-h, span-right); + & > *:not(summary):focus { + --tw-outline-style: none; + outline-style: none; + @media (forced-colors: active) { + outline: 2px solid transparent; + outline-offset: 2px; + } + } + .dropdown-content { + position: absolute; + } + &:not(details, .dropdown-open, .dropdown-hover:hover, :focus-within) { + .dropdown-content { + display: none; + transform-origin: top; + opacity: 0%; + scale: 95%; + } + } + &[popover], .dropdown-content { + z-index: 999; + animation: dropdown 0.2s; + transition-property: opacity, scale, display; + transition-behavior: allow-discrete; + transition-duration: 0.2s; + transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); + } + @starting-style { + &[popover], .dropdown-content { + scale: 95%; + opacity: 0; + } + } + &.dropdown-open, &:not(.dropdown-hover):focus, &:focus-within { + > [tabindex]:first-child { + pointer-events: none; + } + .dropdown-content { + opacity: 100%; + } + } + &.dropdown-hover:hover { + .dropdown-content { + opacity: 100%; + scale: 100%; + } + } + &:is(details) { + summary { + &::-webkit-details-marker { + display: none; + } + } + } + &.dropdown-open, &:focus, &:focus-within { + .dropdown-content { + scale: 100%; + } + } + &:where([popover]) { + background: #0000; + } + &[popover] { + position: fixed; + color: inherit; + @supports not (position-area: bottom) { + margin: auto; + &.dropdown-open:not(:popover-open) { + display: none; + transform-origin: top; + opacity: 0%; + scale: 95%; + } + &::backdrop { + background-color: color-mix(in oklab, #000 30%, #0000); + } + } + &:not(.dropdown-open, :popover-open) { + display: none; + transform-origin: top; + opacity: 0%; + scale: 95%; + } + } + } + .btn { + :where(&) { + width: unset; + } + display: inline-flex; + flex-shrink: 0; + cursor: pointer; + flex-wrap: nowrap; + align-items: center; + justify-content: center; + gap: calc(0.25rem * 1.5); + text-align: center; + vertical-align: middle; + outline-offset: 2px; + webkit-user-select: none; + user-select: none; + padding-inline: var(--btn-p); + color: var(--btn-fg); + --tw-prose-links: var(--btn-fg); + height: var(--size); + font-size: var(--fontsize, 0.875rem); + font-weight: 600; + outline-color: var(--btn-color, var(--color-base-content)); + transition-property: color, background-color, border-color, box-shadow; + transition-timing-function: cubic-bezier(0, 0, 0.2, 1); + transition-duration: 0.2s; + border-start-start-radius: var(--join-ss, var(--radius-field)); + border-start-end-radius: var(--join-se, var(--radius-field)); + border-end-start-radius: var(--join-es, var(--radius-field)); + border-end-end-radius: var(--join-ee, var(--radius-field)); + background-color: var(--btn-bg); + background-size: auto, calc(var(--noise) * 100%); + background-image: none, var(--btn-noise); + border-width: var(--border); + border-style: solid; + border-color: var(--btn-border); + text-shadow: 0 0.5px oklch(100% 0 0 / calc(var(--depth) * 0.15)); + touch-action: manipulation; + box-shadow: 0 0.5px 0 0.5px oklch(100% 0 0 / calc(var(--depth) * 6%)) inset, var(--btn-shadow); + --size: calc(var(--size-field, 0.25rem) * 10); + --btn-bg: var(--btn-color, var(--color-base-200)); + --btn-fg: var(--color-base-content); + --btn-p: 1rem; + --btn-border: var(--btn-bg); + @supports (color: color-mix(in lab, red, red)) { + --btn-border: color-mix(in oklab, var(--btn-bg), #000 calc(var(--depth) * 5%)); + } + --btn-shadow: 0 3px 2px -2px var(--btn-bg), + 0 4px 3px -2px var(--btn-bg); + @supports (color: color-mix(in lab, red, red)) { + --btn-shadow: 0 3px 2px -2px color-mix(in oklab, var(--btn-bg) calc(var(--depth) * 30%), #0000), + 0 4px 3px -2px color-mix(in oklab, var(--btn-bg) calc(var(--depth) * 30%), #0000); + } + --btn-noise: var(--fx-noise); + .prose & { + text-decoration-line: none; + } + @media (hover: hover) { + &:hover { + --btn-bg: var(--btn-color, var(--color-base-200)); + @supports (color: color-mix(in lab, red, red)) { + --btn-bg: color-mix(in oklab, var(--btn-color, var(--color-base-200)), #000 7%); + } + } + } + &:focus-visible { + outline-width: 2px; + outline-style: solid; + isolation: isolate; + } + &:active:not(.btn-active) { + translate: 0 0.5px; + --btn-bg: var(--btn-color, var(--color-base-200)); + @supports (color: color-mix(in lab, red, red)) { + --btn-bg: color-mix(in oklab, var(--btn-color, var(--color-base-200)), #000 5%); + } + --btn-border: var(--btn-color, var(--color-base-200)); + @supports (color: color-mix(in lab, red, red)) { + --btn-border: color-mix(in oklab, var(--btn-color, var(--color-base-200)), #000 7%); + } + --btn-shadow: 0 0 0 0 oklch(0% 0 0/0), 0 0 0 0 oklch(0% 0 0/0); + } + &:is(:disabled, [disabled], .btn-disabled) { + &:not(.btn-link, .btn-ghost) { + background-color: var(--color-base-content); + @supports (color: color-mix(in lab, red, red)) { + background-color: color-mix(in oklab, var(--color-base-content) 10%, transparent); + } + box-shadow: none; + } + pointer-events: none; + --btn-border: #0000; + --btn-noise: none; + --btn-fg: var(--color-base-content); + @supports (color: color-mix(in lab, red, red)) { + --btn-fg: color-mix(in oklch, var(--color-base-content) 20%, #0000); + } + @media (hover: hover) { + &:hover { + pointer-events: none; + background-color: var(--color-neutral); + @supports (color: color-mix(in lab, red, red)) { + background-color: color-mix(in oklab, var(--color-neutral) 20%, transparent); + } + --btn-border: #0000; + --btn-fg: var(--color-base-content); + @supports (color: color-mix(in lab, red, red)) { + --btn-fg: color-mix(in oklch, var(--color-base-content) 20%, #0000); + } + } + } + } + &:is(input[type="checkbox"], input[type="radio"]) { + appearance: none; + &::after { + content: attr(aria-label); + } + } + &:where(input:checked:not(.filter .btn)) { + --btn-color: var(--color-primary); + --btn-fg: var(--color-primary-content); + isolation: isolate; + } + } + .list { + display: flex; + flex-direction: column; + font-size: 0.875rem; + :where(.list-row) { + --list-grid-cols: minmax(0, auto) 1fr; + position: relative; + display: grid; + grid-auto-flow: column; + gap: calc(0.25rem * 4); + border-radius: var(--radius-box); + padding: calc(0.25rem * 4); + word-break: break-word; + grid-template-columns: var(--list-grid-cols); + &:has(.list-col-grow:nth-child(1)) { + --list-grid-cols: 1fr; + } + &:has(.list-col-grow:nth-child(2)) { + --list-grid-cols: minmax(0, auto) 1fr; + } + &:has(.list-col-grow:nth-child(3)) { + --list-grid-cols: minmax(0, auto) minmax(0, auto) 1fr; + } + &:has(.list-col-grow:nth-child(4)) { + --list-grid-cols: minmax(0, auto) minmax(0, auto) minmax(0, auto) 1fr; + } + &:has(.list-col-grow:nth-child(5)) { + --list-grid-cols: minmax(0, auto) minmax(0, auto) minmax(0, auto) minmax(0, auto) 1fr; + } + &:has(.list-col-grow:nth-child(6)) { + --list-grid-cols: minmax(0, auto) minmax(0, auto) minmax(0, auto) minmax(0, auto) + minmax(0, auto) 1fr; + } + :not(.list-col-wrap) { + grid-row-start: 1; + } + } + & > :not(:last-child) { + &.list-row, .list-row { + &:after { + content: ""; + border-bottom: var(--border) solid; + inset-inline: var(--radius-box); + position: absolute; + bottom: calc(0.25rem * 0); + border-color: var(--color-base-content); + @supports (color: color-mix(in lab, red, red)) { + border-color: color-mix(in oklab, var(--color-base-content) 5%, transparent); + } + } + } + } + } + .toast { + position: fixed; + inset-inline-start: auto; + inset-inline-end: calc(0.25rem * 4); + top: auto; + bottom: calc(0.25rem * 4); + display: flex; + flex-direction: column; + gap: calc(0.25rem * 2); + background-color: transparent; + translate: var(--toast-x, 0) var(--toast-y, 0); + width: max-content; + max-width: calc(100vw - 2rem); + & > * { + animation: toast 0.25s ease-out; + } + &:where(.toast-start) { + inset-inline-start: calc(0.25rem * 4); + inset-inline-end: auto; + --toast-x: 0; + } + &:where(.toast-center) { + inset-inline-start: calc(1/2 * 100%); + inset-inline-end: calc(1/2 * 100%); + --toast-x: -50%; + } + &:where(.toast-end) { + inset-inline-start: auto; + inset-inline-end: calc(0.25rem * 4); + --toast-x: 0; + } + &:where(.toast-bottom) { + top: auto; + bottom: calc(0.25rem * 4); + --toast-y: 0; + } + &:where(.toast-middle) { + top: calc(1/2 * 100%); + bottom: auto; + --toast-y: -50%; + } + &:where(.toast-top) { + top: calc(0.25rem * 4); + bottom: auto; + --toast-y: 0; + } + } + .toggle { + border: var(--border) solid currentColor; + color: var(--input-color); + position: relative; + display: inline-grid; + flex-shrink: 0; + cursor: pointer; + appearance: none; + place-content: center; + vertical-align: middle; + webkit-user-select: none; + user-select: none; + grid-template-columns: 0fr 1fr 1fr; + --radius-selector-max: calc( + var(--radius-selector) + var(--radius-selector) + var(--radius-selector) + ); + border-radius: calc( var(--radius-selector) + min(var(--toggle-p), var(--radius-selector-max)) + min(var(--border), var(--radius-selector-max)) ); + padding: var(--toggle-p); + box-shadow: 0 1px currentColor inset; + @supports (color: color-mix(in lab, red, red)) { + box-shadow: 0 1px color-mix(in oklab, currentColor calc(var(--depth) * 10%), #0000) inset; + } + transition: color 0.3s, grid-template-columns 0.2s; + --input-color: var(--color-base-content); + @supports (color: color-mix(in lab, red, red)) { + --input-color: color-mix(in oklab, var(--color-base-content) 50%, #0000); + } + --toggle-p: calc(var(--size) * 0.125); + --size: calc(var(--size-selector, 0.25rem) * 6); + width: calc((var(--size) * 2) - (var(--border) + var(--toggle-p)) * 2); + height: var(--size); + > * { + z-index: 1; + grid-column: span 1 / span 1; + grid-column-start: 2; + grid-row-start: 1; + height: 100%; + cursor: pointer; + appearance: none; + background-color: transparent; + padding: calc(0.25rem * 0.5); + transition: opacity 0.2s, rotate 0.4s; + border: none; + &:focus { + --tw-outline-style: none; + outline-style: none; + @media (forced-colors: active) { + outline: 2px solid transparent; + outline-offset: 2px; + } + } + &:nth-child(2) { + color: var(--color-base-100); + rotate: 0deg; + } + &:nth-child(3) { + color: var(--color-base-100); + opacity: 0%; + rotate: -15deg; + } + } + &:has(:checked) { + > :nth-child(2) { + opacity: 0%; + rotate: 15deg; + } + > :nth-child(3) { + opacity: 100%; + rotate: 0deg; + } + } + &:before { + position: relative; + inset-inline-start: calc(0.25rem * 0); + grid-column-start: 2; + grid-row-start: 1; + aspect-ratio: 1 / 1; + height: 100%; + border-radius: var(--radius-selector); + background-color: currentColor; + translate: 0; + --tw-content: ""; + content: var(--tw-content); + transition: background-color 0.1s, translate 0.2s, inset-inline-start 0.2s; + box-shadow: 0 -1px oklch(0% 0 0 / calc(var(--depth) * 0.1)) inset, 0 8px 0 -4px oklch(100% 0 0 / calc(var(--depth) * 0.1)) inset, 0 1px currentColor; + @supports (color: color-mix(in lab, red, red)) { + box-shadow: 0 -1px oklch(0% 0 0 / calc(var(--depth) * 0.1)) inset, 0 8px 0 -4px oklch(100% 0 0 / calc(var(--depth) * 0.1)) inset, 0 1px color-mix(in oklab, currentColor calc(var(--depth) * 10%), #0000); + } + background-size: auto, calc(var(--noise) * 100%); + background-image: none, var(--fx-noise); + } + @media (forced-colors: active) { + &:before { + outline-style: var(--tw-outline-style); + outline-width: 1px; + outline-offset: calc(1px * -1); + } + } + @media print { + &:before { + outline: 0.25rem solid; + outline-offset: -1rem; + } + } + &:focus-visible, &:has(:focus-visible) { + outline: 2px solid currentColor; + outline-offset: 2px; + } + &:checked, &[aria-checked="true"], &:has(> input:checked) { + grid-template-columns: 1fr 1fr 0fr; + background-color: var(--color-base-100); + --input-color: var(--color-base-content); + &:before { + background-color: currentColor; + } + @starting-style { + &:before { + opacity: 0; + } + } + } + &:indeterminate { + grid-template-columns: 0.5fr 1fr 0.5fr; + } + &:disabled { + cursor: not-allowed; + opacity: 30%; + &:before { + background-color: transparent; + border: var(--border) solid currentColor; + } + } + } + .input { + cursor: text; + border: var(--border) solid #0000; + position: relative; + display: inline-flex; + flex-shrink: 1; + appearance: none; + align-items: center; + gap: calc(0.25rem * 2); + background-color: var(--color-base-100); + padding-inline: calc(0.25rem * 3); + vertical-align: middle; + white-space: nowrap; + width: clamp(3rem, 20rem, 100%); + height: var(--size); + font-size: 0.875rem; + touch-action: manipulation; + border-start-start-radius: var(--join-ss, var(--radius-field)); + border-start-end-radius: var(--join-se, var(--radius-field)); + border-end-start-radius: var(--join-es, var(--radius-field)); + border-end-end-radius: var(--join-ee, var(--radius-field)); + border-color: var(--input-color); + box-shadow: 0 1px var(--input-color) inset, 0 -1px oklch(100% 0 0 / calc(var(--depth) * 0.1)) inset; + @supports (color: color-mix(in lab, red, red)) { + box-shadow: 0 1px color-mix(in oklab, var(--input-color) calc(var(--depth) * 10%), #0000) inset, 0 -1px oklch(100% 0 0 / calc(var(--depth) * 0.1)) inset; + } + --size: calc(var(--size-field, 0.25rem) * 10); + --input-color: var(--color-base-content); + @supports (color: color-mix(in lab, red, red)) { + --input-color: color-mix(in oklab, var(--color-base-content) 20%, #0000); + } + &:where(input) { + display: inline-flex; + } + :where(input) { + display: inline-flex; + height: 100%; + width: 100%; + appearance: none; + background-color: transparent; + border: none; + &:focus, &:focus-within { + --tw-outline-style: none; + outline-style: none; + @media (forced-colors: active) { + outline: 2px solid transparent; + outline-offset: 2px; + } + } + } + :where(input[type="url"]), :where(input[type="email"]) { + direction: ltr; + } + :where(input[type="date"]) { + display: inline-block; + } + &:focus, &:focus-within { + --input-color: var(--color-base-content); + box-shadow: 0 1px var(--input-color); + @supports (color: color-mix(in lab, red, red)) { + box-shadow: 0 1px color-mix(in oklab, var(--input-color) calc(var(--depth) * 10%), #0000); + } + outline: 2px solid var(--input-color); + outline-offset: 2px; + isolation: isolate; + z-index: 1; + } + &:has(> input[disabled]), &:is(:disabled, [disabled]) { + cursor: not-allowed; + border-color: var(--color-base-200); + background-color: var(--color-base-200); + color: var(--color-base-content); + @supports (color: color-mix(in lab, red, red)) { + color: color-mix(in oklab, var(--color-base-content) 40%, transparent); + } + &::placeholder { + color: var(--color-base-content); + @supports (color: color-mix(in lab, red, red)) { + color: color-mix(in oklab, var(--color-base-content) 20%, transparent); + } + } + box-shadow: none; + } + &:has(> input[disabled]) > input[disabled] { + cursor: not-allowed; + } + &::-webkit-date-and-time-value { + text-align: inherit; + } + &[type="number"] { + &::-webkit-inner-spin-button { + margin-block: calc(0.25rem * -3); + margin-inline-end: calc(0.25rem * -3); + } + } + &::-webkit-calendar-picker-indicator { + position: absolute; + inset-inline-end: 0.75em; + } + } + .table { + font-size: 0.875rem; + position: relative; + width: 100%; + border-radius: var(--radius-box); + text-align: left; + &:where(:dir(rtl), [dir="rtl"], [dir="rtl"] *) { + text-align: right; + } + tr.row-hover { + &, &:nth-child(even) { + &:hover { + @media (hover: hover) { + background-color: var(--color-base-200); + } + } + } + } + :where(th, td) { + padding-inline: calc(0.25rem * 4); + padding-block: calc(0.25rem * 3); + vertical-align: middle; + } + :where(thead, tfoot) { + white-space: nowrap; + color: var(--color-base-content); + @supports (color: color-mix(in lab, red, red)) { + color: color-mix(in oklab, var(--color-base-content) 60%, transparent); + } + font-size: 0.875rem; + font-weight: 600; + } + :where(tfoot) { + border-top: var(--border) solid var(--color-base-content); + @supports (color: color-mix(in lab, red, red)) { + border-top: var(--border) solid color-mix(in oklch, var(--color-base-content) 5%, #0000); + } + } + :where(.table-pin-rows thead tr) { + position: sticky; + top: calc(0.25rem * 0); + z-index: 1; + background-color: var(--color-base-100); + } + :where(.table-pin-rows tfoot tr) { + position: sticky; + bottom: calc(0.25rem * 0); + z-index: 1; + background-color: var(--color-base-100); + } + :where(.table-pin-cols tr th) { + position: sticky; + right: calc(0.25rem * 0); + left: calc(0.25rem * 0); + background-color: var(--color-base-100); + } + :where(thead tr, tbody tr:not(:last-child)) { + border-bottom: var(--border) solid var(--color-base-content); + @supports (color: color-mix(in lab, red, red)) { + border-bottom: var(--border) solid color-mix(in oklch, var(--color-base-content) 5%, #0000); + } + } + } + .chat-bubble { + position: relative; + display: block; + width: fit-content; + border-radius: var(--radius-field); + background-color: var(--color-base-300); + padding-inline: calc(0.25rem * 4); + padding-block: calc(0.25rem * 2); + color: var(--color-base-content); + grid-row-end: 3; + min-height: 2rem; + min-width: 2.5rem; + max-width: 90%; + &:before { + position: absolute; + bottom: calc(0.25rem * 0); + height: calc(0.25rem * 3); + width: calc(0.25rem * 3); + background-color: inherit; + content: ""; + mask-repeat: no-repeat; + mask-image: var(--mask-chat); + mask-position: 0px -1px; + mask-size: 13px; + } + } + .checkbox { + border: var(--border) solid var(--input-color, var(--color-base-content)); + @supports (color: color-mix(in lab, red, red)) { + border: var(--border) solid var(--input-color, color-mix(in oklab, var(--color-base-content) 20%, #0000)); + } + position: relative; + display: inline-block; + flex-shrink: 0; + cursor: pointer; + appearance: none; + border-radius: var(--radius-selector); + padding: calc(0.25rem * 1); + vertical-align: middle; + color: var(--color-base-content); + box-shadow: 0 1px oklch(0% 0 0 / calc(var(--depth) * 0.1)) inset, 0 0 #0000 inset, 0 0 #0000; + transition: background-color 0.2s, box-shadow 0.2s; + --size: calc(var(--size-selector, 0.25rem) * 6); + width: var(--size); + height: var(--size); + background-size: auto, calc(var(--noise) * 100%); + background-image: none, var(--fx-noise); + &:before { + --tw-content: ""; + content: var(--tw-content); + display: block; + width: 100%; + height: 100%; + rotate: 45deg; + background-color: currentColor; + opacity: 0%; + transition: clip-path 0.3s, opacity 0.1s, rotate 0.3s, translate 0.3s; + transition-delay: 0.1s; + clip-path: polygon(20% 100%, 20% 80%, 50% 80%, 50% 80%, 70% 80%, 70% 100%); + box-shadow: 0px 3px 0 0px oklch(100% 0 0 / calc(var(--depth) * 0.1)) inset; + font-size: 1rem; + line-height: 0.75; + } + &:focus-visible { + outline: 2px solid var(--input-color, currentColor); + outline-offset: 2px; + } + &:checked, &[aria-checked="true"] { + background-color: var(--input-color, #0000); + box-shadow: 0 0 #0000 inset, 0 8px 0 -4px oklch(100% 0 0 / calc(var(--depth) * 0.1)) inset, 0 1px oklch(0% 0 0 / calc(var(--depth) * 0.1)); + &:before { + clip-path: polygon(20% 100%, 20% 80%, 50% 80%, 50% 0%, 70% 0%, 70% 100%); + opacity: 100%; + } + @media (forced-colors: active) { + &:before { + rotate: 0deg; + background-color: transparent; + --tw-content: "✔︎"; + clip-path: none; + } + } + @media print { + &:before { + rotate: 0deg; + background-color: transparent; + --tw-content: "✔︎"; + clip-path: none; + } + } + } + &:indeterminate { + &:before { + rotate: 0deg; + opacity: 100%; + translate: 0 -35%; + clip-path: polygon(20% 100%, 20% 80%, 50% 80%, 50% 80%, 80% 80%, 80% 100%); + } + } + &:disabled { + cursor: not-allowed; + opacity: 20%; + } + } + .radio { + position: relative; + display: inline-block; + flex-shrink: 0; + cursor: pointer; + appearance: none; + border-radius: calc(infinity * 1px); + padding: calc(0.25rem * 1); + vertical-align: middle; + border: var(--border) solid var(--input-color, currentColor); + @supports (color: color-mix(in lab, red, red)) { + border: var(--border) solid var(--input-color, color-mix(in srgb, currentColor 20%, #0000)); + } + box-shadow: 0 1px oklch(0% 0 0 / calc(var(--depth) * 0.1)) inset; + --size: calc(var(--size-selector, 0.25rem) * 6); + width: var(--size); + height: var(--size); + color: var(--input-color, currentColor); + &:before { + display: block; + width: 100%; + height: 100%; + border-radius: calc(infinity * 1px); + --tw-content: ""; + content: var(--tw-content); + background-size: auto, calc(var(--noise) * 100%); + background-image: none, var(--fx-noise); + } + &:focus-visible { + outline: 2px solid currentColor; + } + &:checked, &[aria-checked="true"] { + animation: radio 0.2s ease-out; + border-color: currentColor; + background-color: var(--color-base-100); + &:before { + background-color: currentColor; + box-shadow: 0 -1px oklch(0% 0 0 / calc(var(--depth) * 0.1)) inset, 0 8px 0 -4px oklch(100% 0 0 / calc(var(--depth) * 0.1)) inset, 0 1px oklch(0% 0 0 / calc(var(--depth) * 0.1)); + } + @media (forced-colors: active) { + &:before { + outline-style: var(--tw-outline-style); + outline-width: 1px; + outline-offset: calc(1px * -1); + } + } + @media print { + &:before { + outline: 0.25rem solid; + outline-offset: -1rem; + } + } + } + &:disabled { + cursor: not-allowed; + opacity: 20%; + } + } + .rating { + position: relative; + display: inline-flex; + vertical-align: middle; + & input { + border: none; + appearance: none; + } + :where(*) { + animation: rating 0.25s ease-out; + height: calc(0.25rem * 6); + width: calc(0.25rem * 6); + border-radius: 0; + background-color: var(--color-base-content); + opacity: 20%; + &:is(input) { + cursor: pointer; + } + } + & .rating-hidden { + width: calc(0.25rem * 2); + background-color: transparent; + } + input[type="radio"]:checked { + background-image: none; + } + * { + &:checked, &[aria-checked="true"], &[aria-current="true"], &:has(~ *:checked, ~ *[aria-checked="true"], ~ *[aria-current="true"]) { + opacity: 100%; + } + &:focus-visible { + transition: scale 0.2s ease-out; + scale: 1.1; + } + } + & *:active:focus { + animation: none; + scale: 1.1; + } + &.rating-xs :where(*:not(.rating-hidden)) { + width: calc(0.25rem * 4); + height: calc(0.25rem * 4); + } + &.rating-sm :where(*:not(.rating-hidden)) { + width: calc(0.25rem * 5); + height: calc(0.25rem * 5); + } + &.rating-md :where(*:not(.rating-hidden)) { + width: calc(0.25rem * 6); + height: calc(0.25rem * 6); + } + &.rating-lg :where(*:not(.rating-hidden)) { + width: calc(0.25rem * 7); + height: calc(0.25rem * 7); + } + &.rating-xl :where(*:not(.rating-hidden)) { + width: calc(0.25rem * 8); + height: calc(0.25rem * 8); + } + } + .progress { + position: relative; + height: calc(0.25rem * 2); + width: 100%; + appearance: none; + overflow: hidden; + border-radius: var(--radius-box); + background-color: currentColor; + @supports (color: color-mix(in lab, red, red)) { + background-color: color-mix(in oklab, currentColor 20%, transparent); + } + color: var(--color-base-content); + &:indeterminate { + background-image: repeating-linear-gradient( 90deg, currentColor -1%, currentColor 10%, #0000 10%, #0000 90% ); + background-size: 200%; + background-position-x: 15%; + animation: progress 5s ease-in-out infinite; + @supports (-moz-appearance: none) { + &::-moz-progress-bar { + background-color: transparent; + background-image: repeating-linear-gradient( 90deg, currentColor -1%, currentColor 10%, #0000 10%, #0000 90% ); + background-size: 200%; + background-position-x: 15%; + animation: progress 5s ease-in-out infinite; + } + } + } + @supports (-moz-appearance: none) { + &::-moz-progress-bar { + border-radius: var(--radius-box); + background-color: currentColor; + } + } + @supports (-webkit-appearance: none) { + &::-webkit-progress-bar { + border-radius: var(--radius-box); + background-color: transparent; + } + &::-webkit-progress-value { + border-radius: var(--radius-box); + background-color: currentColor; + } + } + } + .absolute { + position: absolute; + } + .fixed { + position: fixed; + } + .relative { + position: relative; + } + .dropdown-right { + --anchor-h: right; + --anchor-v: span-bottom; + .dropdown-content { + inset-inline-start: 100%; + top: calc(0.25rem * 0); + bottom: auto; + transform-origin: left; + } + } + .chat-start { + place-items: start; + grid-template-columns: auto 1fr; + .chat-header { + grid-column-start: 2; + } + .chat-footer { + grid-column-start: 2; + } + .chat-image { + grid-column-start: 1; + } + .chat-bubble { + grid-column-start: 2; + border-end-start-radius: 0; + &:before { + transform: rotateY(0deg); + inset-inline-start: -0.75rem; + } + [dir="rtl"] &:before { + transform: rotateY(180deg); + } + } + } + .dropdown-left { + --anchor-h: left; + --anchor-v: span-bottom; + .dropdown-content { + inset-inline-end: 100%; + top: calc(0.25rem * 0); + bottom: auto; + transform-origin: right; + } + } + .dropdown-center { + --anchor-h: center; + :where(.dropdown-content) { + inset-inline-end: calc(1/2 * 100%); + translate: 50% 0; + [dir="rtl"] & { + translate: -50% 0; + } + } + &.dropdown-left { + --anchor-h: left; + --anchor-v: center; + .dropdown-content { + top: auto; + bottom: calc(1/2 * 100%); + translate: 0 50%; + } + } + &.dropdown-right { + --anchor-h: right; + --anchor-v: center; + .dropdown-content { + top: auto; + bottom: calc(1/2 * 100%); + translate: 0 50%; + } + } + } + .dropdown-end { + --anchor-h: span-left; + :where(.dropdown-content) { + inset-inline-end: calc(0.25rem * 0); + translate: 0 0; + [dir="rtl"] & { + translate: 0 0; + } + } + &.dropdown-left { + --anchor-h: left; + --anchor-v: span-top; + .dropdown-content { + top: auto; + bottom: calc(0.25rem * 0); + } + } + &.dropdown-right { + --anchor-h: right; + --anchor-v: span-top; + .dropdown-content { + top: auto; + bottom: calc(0.25rem * 0); + } + } + } + .right-8 { + right: calc(var(--spacing) * 8); + } + .bottom-8 { + bottom: calc(var(--spacing) * 8); + } + .textarea { + border: var(--border) solid #0000; + min-height: calc(0.25rem * 20); + flex-shrink: 1; + appearance: none; + border-radius: var(--radius-field); + background-color: var(--color-base-100); + padding-block: calc(0.25rem * 2); + vertical-align: middle; + width: clamp(3rem, 20rem, 100%); + padding-inline-start: 0.75rem; + padding-inline-end: 0.75rem; + font-size: 0.875rem; + touch-action: manipulation; + border-color: var(--input-color); + box-shadow: 0 1px var(--input-color) inset, 0 -1px oklch(100% 0 0 / calc(var(--depth) * 0.1)) inset; + @supports (color: color-mix(in lab, red, red)) { + box-shadow: 0 1px color-mix(in oklab, var(--input-color) calc(var(--depth) * 10%), #0000) inset, 0 -1px oklch(100% 0 0 / calc(var(--depth) * 0.1)) inset; + } + --input-color: var(--color-base-content); + @supports (color: color-mix(in lab, red, red)) { + --input-color: color-mix(in oklab, var(--color-base-content) 20%, #0000); + } + textarea { + appearance: none; + background-color: transparent; + border: none; + &:focus, &:focus-within { + --tw-outline-style: none; + outline-style: none; + @media (forced-colors: active) { + outline: 2px solid transparent; + outline-offset: 2px; + } + } + } + &:focus, &:focus-within { + --input-color: var(--color-base-content); + box-shadow: 0 1px var(--input-color); + @supports (color: color-mix(in lab, red, red)) { + box-shadow: 0 1px color-mix(in oklab, var(--input-color) calc(var(--depth) * 10%), #0000); + } + outline: 2px solid var(--input-color); + outline-offset: 2px; + isolation: isolate; + } + &:has(> textarea[disabled]), &:is(:disabled, [disabled]) { + cursor: not-allowed; + border-color: var(--color-base-200); + background-color: var(--color-base-200); + color: var(--color-base-content); + @supports (color: color-mix(in lab, red, red)) { + color: color-mix(in oklab, var(--color-base-content) 40%, transparent); + } + &::placeholder { + color: var(--color-base-content); + @supports (color: color-mix(in lab, red, red)) { + color: color-mix(in oklab, var(--color-base-content) 20%, transparent); + } + } + box-shadow: none; + } + &:has(> textarea[disabled]) > textarea[disabled] { + cursor: not-allowed; + } + } + .z-1 { + z-index: 1; + } + .drawer-content { + grid-column-start: 2; + grid-row-start: 1; + min-width: calc(0.25rem * 0); + } + .chat-image { + grid-row: span 2 / span 2; + align-self: flex-end; + } + .chat-footer { + grid-row-start: 3; + display: flex; + gap: calc(0.25rem * 1); + font-size: 0.6875rem; + } + .chat-header { + grid-row-start: 1; + display: flex; + gap: calc(0.25rem * 1); + font-size: 0.6875rem; + } + .m-1 { + margin: calc(var(--spacing) * 1); + } + .filter { + display: flex; + flex-wrap: wrap; + input[type="radio"] { + width: auto; + } + input { + overflow: hidden; + opacity: 100%; + scale: 1; + transition: margin 0.1s, opacity 0.3s, padding 0.3s, border-width 0.1s; + &:not(:last-child) { + margin-inline-end: calc(0.25rem * 1); + } + &.filter-reset { + aspect-ratio: 1 / 1; + &::after { + content: "×"; + } + } + } + &:not(:has(input:checked:not(.filter-reset))) { + .filter-reset, input[type="reset"] { + scale: 0; + border-width: 0; + margin-inline: calc(0.25rem * 0); + width: calc(0.25rem * 0); + padding-inline: calc(0.25rem * 0); + opacity: 0%; + } + } + &:has(input:checked:not(.filter-reset)) { + input:not(:checked, .filter-reset, input[type="reset"]) { + scale: 0; + border-width: 0; + margin-inline: calc(0.25rem * 0); + width: calc(0.25rem * 0); + padding-inline: calc(0.25rem * 0); + opacity: 0%; + } + } + } + .mt-2 { + margin-top: calc(var(--spacing) * 2); + } + .mt-4 { + margin-top: calc(var(--spacing) * 4); + } + .mt-8 { + margin-top: calc(var(--spacing) * 8); + } + .mt-16 { + margin-top: calc(var(--spacing) * 16); + } + .mb-4 { + margin-bottom: calc(var(--spacing) * 4); + } + .mb-8 { + margin-bottom: calc(var(--spacing) * 8); + } + .ml-2 { + margin-left: calc(var(--spacing) * 2); + } + .ml-4 { + margin-left: calc(var(--spacing) * 4); + } + .ml-auto { + margin-left: auto; + } + .badge { + display: inline-flex; + align-items: center; + justify-content: center; + gap: calc(0.25rem * 2); + border-radius: var(--radius-selector); + vertical-align: middle; + color: var(--badge-fg); + border: var(--border) solid var(--badge-color, var(--color-base-200)); + font-size: 0.875rem; + width: fit-content; + padding-inline: calc(0.25rem * 3 - var(--border)); + background-size: auto, calc(var(--noise) * 100%); + background-image: none, var(--fx-noise); + background-color: var(--badge-bg); + --badge-bg: var(--badge-color, var(--color-base-100)); + --badge-fg: var(--color-base-content); + --size: calc(var(--size-selector, 0.25rem) * 6); + height: var(--size); + } + .alert { + display: grid; + align-items: center; + gap: calc(0.25rem * 4); + border-radius: var(--radius-box); + padding-inline: calc(0.25rem * 4); + padding-block: calc(0.25rem * 3); + color: var(--color-base-content); + background-color: var(--alert-color, var(--color-base-200)); + justify-content: start; + justify-items: start; + grid-auto-flow: column; + grid-template-columns: auto; + text-align: start; + border: var(--border) solid var(--color-base-200); + font-size: 0.875rem; + line-height: 1.25rem; + background-size: auto, calc(var(--noise) * 100%); + background-image: none, var(--fx-noise); + box-shadow: 0 3px 0 -2px oklch(100% 0 0 / calc(var(--depth) * 0.08)) inset, 0 1px #000, 0 4px 3px -2px oklch(0% 0 0 / calc(var(--depth) * 0.08)); + @supports (color: color-mix(in lab, red, red)) { + box-shadow: 0 3px 0 -2px oklch(100% 0 0 / calc(var(--depth) * 0.08)) inset, 0 1px color-mix( in oklab, color-mix(in oklab, #000 20%, var(--alert-color, var(--color-base-200))) calc(var(--depth) * 20%), #0000 ), 0 4px 3px -2px oklch(0% 0 0 / calc(var(--depth) * 0.08)); + } + &:has(:nth-child(2)) { + grid-template-columns: auto minmax(auto, 1fr); + } + &.alert-outline { + background-color: transparent; + color: var(--alert-color); + box-shadow: none; + background-image: none; + } + &.alert-dash { + background-color: transparent; + color: var(--alert-color); + border-style: dashed; + box-shadow: none; + background-image: none; + } + &.alert-soft { + color: var(--alert-color, var(--color-base-content)); + background: var(--alert-color, var(--color-base-content)); + @supports (color: color-mix(in lab, red, red)) { + background: color-mix( in oklab, var(--alert-color, var(--color-base-content)) 8%, var(--color-base-100) ); + } + border-color: var(--alert-color, var(--color-base-content)); + @supports (color: color-mix(in lab, red, red)) { + border-color: color-mix( in oklab, var(--alert-color, var(--color-base-content)) 10%, var(--color-base-100) ); + } + box-shadow: none; + background-image: none; + } + } + .join { + display: inline-flex; + align-items: stretch; + --join-ss: 0; + --join-se: 0; + --join-es: 0; + --join-ee: 0; + :where(.join-item) { + border-start-start-radius: var(--join-ss, 0); + border-start-end-radius: var(--join-se, 0); + border-end-start-radius: var(--join-es, 0); + border-end-end-radius: var(--join-ee, 0); + * { + --join-ss: var(--radius-field); + --join-se: var(--radius-field); + --join-es: var(--radius-field); + --join-ee: var(--radius-field); + } + } + > .join-item:where(:first-child) { + --join-ss: var(--radius-field); + --join-se: 0; + --join-es: var(--radius-field); + --join-ee: 0; + } + :first-child:not(:last-child) { + :where(.join-item) { + --join-ss: var(--radius-field); + --join-se: 0; + --join-es: var(--radius-field); + --join-ee: 0; + } + } + > .join-item:where(:last-child) { + --join-ss: 0; + --join-se: var(--radius-field); + --join-es: 0; + --join-ee: var(--radius-field); + } + :last-child:not(:first-child) { + :where(.join-item) { + --join-ss: 0; + --join-se: var(--radius-field); + --join-es: 0; + --join-ee: var(--radius-field); + } + } + > .join-item:where(:only-child) { + --join-ss: var(--radius-field); + --join-se: var(--radius-field); + --join-es: var(--radius-field); + --join-ee: var(--radius-field); + } + :only-child { + :where(.join-item) { + --join-ss: var(--radius-field); + --join-se: var(--radius-field); + --join-es: var(--radius-field); + --join-ee: var(--radius-field); + } + } + } + .chat { + display: grid; + column-gap: calc(0.25rem * 3); + padding-block: calc(0.25rem * 1); + --mask-chat: url("data:image/svg+xml,%3csvg width='13' height='13' xmlns='http://www.w3.org/2000/svg'%3e%3cpath fill='black' d='M0 11.5004C0 13.0004 2 13.0004 2 13.0004H12H13V0.00036329L12.5 0C12.5 0 11.977 2.09572 11.8581 2.50033C11.6075 3.35237 10.9149 4.22374 9 5.50036C6 7.50036 0 10.0004 0 11.5004Z'/%3e%3c/svg%3e"); + } + .prose { + :root & { + --tw-prose-body: var(--color-base-content); + @supports (color: color-mix(in lab, red, red)) { + --tw-prose-body: color-mix(in oklab, var(--color-base-content) 80%, #0000); + } + --tw-prose-headings: var(--color-base-content); + --tw-prose-lead: var(--color-base-content); + --tw-prose-links: var(--color-base-content); + --tw-prose-bold: var(--color-base-content); + --tw-prose-counters: var(--color-base-content); + --tw-prose-bullets: var(--color-base-content); + @supports (color: color-mix(in lab, red, red)) { + --tw-prose-bullets: color-mix(in oklab, var(--color-base-content) 50%, #0000); + } + --tw-prose-hr: var(--color-base-content); + @supports (color: color-mix(in lab, red, red)) { + --tw-prose-hr: color-mix(in oklab, var(--color-base-content) 20%, #0000); + } + --tw-prose-quotes: var(--color-base-content); + --tw-prose-quote-borders: var(--color-base-content); + @supports (color: color-mix(in lab, red, red)) { + --tw-prose-quote-borders: color-mix(in oklab, var(--color-base-content) 20%, #0000); + } + --tw-prose-captions: var(--color-base-content); + @supports (color: color-mix(in lab, red, red)) { + --tw-prose-captions: color-mix(in oklab, var(--color-base-content) 50%, #0000); + } + --tw-prose-code: var(--color-base-content); + --tw-prose-pre-code: var(--color-neutral-content); + --tw-prose-pre-bg: var(--color-neutral); + --tw-prose-th-borders: var(--color-base-content); + @supports (color: color-mix(in lab, red, red)) { + --tw-prose-th-borders: color-mix(in oklab, var(--color-base-content) 50%, #0000); + } + --tw-prose-td-borders: var(--color-base-content); + @supports (color: color-mix(in lab, red, red)) { + --tw-prose-td-borders: color-mix(in oklab, var(--color-base-content) 20%, #0000); + } + --tw-prose-kbd: var(--color-base-content); + @supports (color: color-mix(in lab, red, red)) { + --tw-prose-kbd: color-mix(in oklab, var(--color-base-content) 80%, #0000); + } + :where(code):not(pre > code) { + background-color: var(--color-base-200); + border-radius: var(--radius-selector); + border: var(--border) solid var(--color-base-300); + padding-inline: 0.5em; + font-weight: inherit; + &:before, &:after { + display: none; + } + } + } + } + .block { + display: block; + } + .flex { + display: flex; + } + .grid { + display: grid; + } + .hidden { + display: none; + } + .table { + display: table; + } + .size-12 { + width: calc(var(--spacing) * 12); + height: calc(var(--spacing) * 12); + } + .h-4 { + height: calc(var(--spacing) * 4); + } + .h-8 { + height: calc(var(--spacing) * 8); + } + .h-10 { + height: calc(var(--spacing) * 10); + } + .h-12 { + height: calc(var(--spacing) * 12); + } + .h-16 { + height: calc(var(--spacing) * 16); + } + .h-20 { + height: calc(var(--spacing) * 20); + } + .h-fit { + height: fit-content; + } + .h-full { + height: 100%; + } + .h-screen { + height: 100vh; + } + .max-h-12 { + max-height: calc(var(--spacing) * 12); + } + .max-h-screen { + max-height: 100vh; + } + .min-h-0 { + min-height: calc(var(--spacing) * 0); + } + .btn-wide { + width: 100%; + max-width: calc(0.25rem * 64); + } + .w-4 { + width: calc(var(--spacing) * 4); + } + .w-16 { + width: calc(var(--spacing) * 16); + } + .w-20 { + width: calc(var(--spacing) * 20); + } + .w-52 { + width: calc(var(--spacing) * 52); + } + .w-100 { + width: calc(var(--spacing) * 100); + } + .w-fit { + width: fit-content; + } + .w-full { + width: 100%; + } + .w-screen { + width: 100vw; + } + .max-w-20 { + max-width: calc(var(--spacing) * 20); + } + .max-w-\[20rem\] { + max-width: 20rem; + } + .min-w-110 { + min-width: calc(var(--spacing) * 110); + } + .min-w-xs { + min-width: var(--container-xs); + } + .flex-1 { + flex: 1; + } + .flex-shrink { + flex-shrink: 1; + } + .flex-shrink-0 { + flex-shrink: 0; + } + .border-collapse { + border-collapse: collapse; + } + .scale-50 { + --tw-scale-x: 50%; + --tw-scale-y: 50%; + --tw-scale-z: 50%; + scale: var(--tw-scale-x) var(--tw-scale-y); + } + .transform { + transform: var(--tw-rotate-x,) var(--tw-rotate-y,) var(--tw-rotate-z,) var(--tw-skew-x,) var(--tw-skew-y,); + } + .skeleton { + border-radius: var(--radius-box); + background-color: var(--color-base-300); + @media (prefers-reduced-motion: reduce) { + transition-duration: 15s; + } + will-change: background-position; + animation: skeleton 1.8s ease-in-out infinite; + background-image: linear-gradient( 105deg, #0000 0% 40%, var(--color-base-100) 50%, #0000 60% 100% ); + background-size: 200% auto; + background-repeat: no-repeat; + background-position-x: -50%; + } + .link { + cursor: pointer; + text-decoration-line: underline; + &:focus { + --tw-outline-style: none; + outline-style: none; + @media (forced-colors: active) { + outline: 2px solid transparent; + outline-offset: 2px; + } + } + &:focus-visible { + outline: 2px solid currentColor; + outline-offset: 2px; + } + } + .resize { + resize: both; + } + .grid-cols-4 { + grid-template-columns: repeat(4, minmax(0, 1fr)); + } + .grid-cols-\[repeat\(auto-fit\,minmax\(5rem\,1fr\)\)\] { + grid-template-columns: repeat(auto-fit,minmax(5rem,1fr)); + } + .flex-col { + flex-direction: column; + } + .flex-wrap { + flex-wrap: wrap; + } + .items-center { + align-items: center; + } + .justify-between { + justify-content: space-between; + } + .justify-center { + justify-content: center; + } + .justify-evenly { + justify-content: space-evenly; + } + .gap-0 { + gap: calc(var(--spacing) * 0); + } + .gap-2 { + gap: calc(var(--spacing) * 2); + } + .gap-x-2 { + column-gap: calc(var(--spacing) * 2); + } + .gap-x-4 { + column-gap: calc(var(--spacing) * 4); + } + .gap-x-8 { + column-gap: calc(var(--spacing) * 8); + } + .gap-y-6 { + row-gap: calc(var(--spacing) * 6); + } + .gap-y-8 { + row-gap: calc(var(--spacing) * 8); + } + .overflow-y-auto { + overflow-y: auto; + } + .rounded { + border-radius: 0.25rem; + } + .rounded-box { + border-radius: var(--radius-box); + } + .rounded-box { + border-radius: var(--radius-box); + } + .rounded-full { + border-radius: calc(infinity * 1px); + } + .rounded-lg { + border-radius: var(--radius-lg); + } + .rounded-tl-lg { + border-top-left-radius: var(--radius-lg); + } + .rounded-tr-lg { + border-top-right-radius: var(--radius-lg); + } + .rounded-br-lg { + border-bottom-right-radius: var(--radius-lg); + } + .rounded-bl-lg { + border-bottom-left-radius: var(--radius-lg); + } + .border { + border-style: var(--tw-border-style); + border-width: 1px; + } + .border-r { + border-right-style: var(--tw-border-style); + border-right-width: 1px; + } + .border-none { + --tw-border-style: none; + border-style: none; + } + .border-solid { + --tw-border-style: solid; + border-style: solid; + } + .alert-info { + border-color: var(--color-info); + color: var(--color-info-content); + --alert-color: var(--color-info); + } + .border-transparent { + border-color: transparent; + } + .border-r-base-300 { + border-right-color: var(--color-base-300); + } + .bg-base-100 { + background-color: var(--color-base-100); + } + .bg-base-200 { + background-color: var(--color-base-200); + } + .bg-base-300 { + background-color: var(--color-base-300); + } + .bg-base-content { + background-color: var(--color-base-content); + } + .bg-info { + background-color: var(--color-info); + } + .bg-transparent { + background-color: transparent; + } + .mask-repeat { + mask-repeat: repeat; + } + .fill-current { + fill: currentcolor; + } + .object-cover { + object-fit: cover; + } + .p-0 { + padding: calc(var(--spacing) * 0); + } + .p-2 { + padding: calc(var(--spacing) * 2); + } + .p-4 { + padding: calc(var(--spacing) * 4); + } + .px-4 { + padding-inline: calc(var(--spacing) * 4); + } + .pt-1 { + padding-top: calc(var(--spacing) * 1); + } + .pt-4 { + padding-top: calc(var(--spacing) * 4); + } + .pr-0 { + padding-right: calc(var(--spacing) * 0); + } + .pr-8 { + padding-right: calc(var(--spacing) * 8); + } + .pb-1 { + padding-bottom: calc(var(--spacing) * 1); + } + .pb-4 { + padding-bottom: calc(var(--spacing) * 4); + } + .pl-8 { + padding-left: calc(var(--spacing) * 8); + } + .text-center { + text-align: center; + } + .text-4xl { + font-size: var(--text-4xl); + line-height: var(--tw-leading, var(--text-4xl--line-height)); + } + .text-base { + font-size: var(--text-base); + line-height: var(--tw-leading, var(--text-base--line-height)); + } + .text-lg { + font-size: var(--text-lg); + line-height: var(--tw-leading, var(--text-lg--line-height)); + } + .text-sm { + font-size: var(--text-sm); + line-height: var(--tw-leading, var(--text-sm--line-height)); + } + .text-xl { + font-size: var(--text-xl); + line-height: var(--tw-leading, var(--text-xl--line-height)); + } + .font-medium { + --tw-font-weight: var(--font-weight-medium); + font-weight: var(--font-weight-medium); + } + .font-semibold { + --tw-font-weight: var(--font-weight-semibold); + font-weight: var(--font-weight-semibold); + } + .text-wrap { + text-wrap: wrap; + } + .text-base-100 { + color: var(--color-base-100); + } + .text-base-content { + color: var(--color-base-content); + } + .text-info-content { + color: var(--color-info-content); + } + .underline { + text-decoration-line: underline; + } + .opacity-50 { + opacity: 50%; + } + .opacity-75 { + opacity: 75%; + } + .shadow-lg { + --tw-shadow: 0 10px 15px -3px var(--tw-shadow-color, rgb(0 0 0 / 0.1)), 0 4px 6px -4px var(--tw-shadow-color, rgb(0 0 0 / 0.1)); + box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow); + } + .shadow-none { + --tw-shadow: 0 0 #0000; + box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow); + } + .shadow-sm { + --tw-shadow: 0 1px 3px 0 var(--tw-shadow-color, rgb(0 0 0 / 0.1)), 0 1px 2px -1px var(--tw-shadow-color, rgb(0 0 0 / 0.1)); + box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow); + } + .outline { + outline-style: var(--tw-outline-style); + outline-width: 1px; + } + .filter { + filter: var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,); + } + .transition { + transition-property: color, background-color, border-color, outline-color, text-decoration-color, fill, stroke, --tw-gradient-from, --tw-gradient-via, --tw-gradient-to, opacity, box-shadow, transform, translate, scale, rotate, filter, -webkit-backdrop-filter, backdrop-filter, display, visibility, content-visibility, overlay, pointer-events; + transition-timing-function: var(--tw-ease, var(--default-transition-timing-function)); + transition-duration: var(--tw-duration, var(--default-transition-duration)); + } + .ease-in-out { + --tw-ease: var(--ease-in-out); + transition-timing-function: var(--ease-in-out); + } + .ease-out { + --tw-ease: var(--ease-out); + transition-timing-function: var(--ease-out); + } + .btn-neutral { + --btn-color: var(--color-neutral); + --btn-fg: var(--color-neutral-content); + } + .outline-none { + --tw-outline-style: none; + outline-style: none; + } + .group-hover\:flex { + &:is(:where(.group):hover *) { + @media (hover: hover) { + display: flex; + } + } + } + .placeholder\:text-base-content { + &::placeholder { + color: var(--color-base-content); + } + } + .placeholder\:opacity-75 { + &::placeholder { + opacity: 75%; + } + } + .focus-within\:outline { + &:focus-within { + outline-style: var(--tw-outline-style); + outline-width: 1px; + } + } + .focus-within\:outline-2 { + &:focus-within { + outline-style: var(--tw-outline-style); + outline-width: 2px; + } + } + .focus-within\:outline-primary { + &:focus-within { + outline-color: var(--color-primary); + } + } + .hover\:bg-base-200 { + &:hover { + @media (hover: hover) { + background-color: var(--color-base-200); + } + } + } + .hover\:bg-base-300 { + &:hover { + @media (hover: hover) { + background-color: var(--color-base-300); + } + } + } +} +@layer base { + :where(:root),:root:has(input.theme-controller[value=light]:checked),[data-theme=light] { + color-scheme: light; + --color-base-100: oklch(100% 0 0); + --color-base-200: oklch(98% 0 0); + --color-base-300: oklch(95% 0 0); + --color-base-content: oklch(21% 0.006 285.885); + --color-primary: oklch(45% 0.24 277.023); + --color-primary-content: oklch(93% 0.034 272.788); + --color-secondary: oklch(65% 0.241 354.308); + --color-secondary-content: oklch(94% 0.028 342.258); + --color-accent: oklch(77% 0.152 181.912); + --color-accent-content: oklch(38% 0.063 188.416); + --color-neutral: oklch(14% 0.005 285.823); + --color-neutral-content: oklch(92% 0.004 286.32); + --color-info: oklch(74% 0.16 232.661); + --color-info-content: oklch(29% 0.066 243.157); + --color-success: oklch(76% 0.177 163.223); + --color-success-content: oklch(37% 0.077 168.94); + --color-warning: oklch(82% 0.189 84.429); + --color-warning-content: oklch(41% 0.112 45.904); + --color-error: oklch(71% 0.194 13.428); + --color-error-content: oklch(27% 0.105 12.094); + --radius-selector: 0.5rem; + --radius-field: 0.25rem; + --radius-box: 0.5rem; + --size-selector: 0.25rem; + --size-field: 0.25rem; + --border: 1px; + --depth: 1; + --noise: 0; + } +} +@layer base { + @media (prefers-color-scheme: dark) { + :root { + color-scheme: dark; + --color-base-100: oklch(25.33% 0.016 252.42); + --color-base-200: oklch(23.26% 0.014 253.1); + --color-base-300: oklch(21.15% 0.012 254.09); + --color-base-content: oklch(97.807% 0.029 256.847); + --color-primary: oklch(58% 0.233 277.117); + --color-primary-content: oklch(96% 0.018 272.314); + --color-secondary: oklch(65% 0.241 354.308); + --color-secondary-content: oklch(94% 0.028 342.258); + --color-accent: oklch(77% 0.152 181.912); + --color-accent-content: oklch(38% 0.063 188.416); + --color-neutral: oklch(14% 0.005 285.823); + --color-neutral-content: oklch(92% 0.004 286.32); + --color-info: oklch(74% 0.16 232.661); + --color-info-content: oklch(29% 0.066 243.157); + --color-success: oklch(76% 0.177 163.223); + --color-success-content: oklch(37% 0.077 168.94); + --color-warning: oklch(82% 0.189 84.429); + --color-warning-content: oklch(41% 0.112 45.904); + --color-error: oklch(71% 0.194 13.428); + --color-error-content: oklch(27% 0.105 12.094); + --radius-selector: 0.5rem; + --radius-field: 0.25rem; + --radius-box: 0.5rem; + --size-selector: 0.25rem; + --size-field: 0.25rem; + --border: 1px; + --depth: 1; + --noise: 0; + } + } +} +@layer base { + :root:has(input.theme-controller[value=light]:checked),[data-theme=light] { + color-scheme: light; + --color-base-100: oklch(100% 0 0); + --color-base-200: oklch(98% 0 0); + --color-base-300: oklch(95% 0 0); + --color-base-content: oklch(21% 0.006 285.885); + --color-primary: oklch(45% 0.24 277.023); + --color-primary-content: oklch(93% 0.034 272.788); + --color-secondary: oklch(65% 0.241 354.308); + --color-secondary-content: oklch(94% 0.028 342.258); + --color-accent: oklch(77% 0.152 181.912); + --color-accent-content: oklch(38% 0.063 188.416); + --color-neutral: oklch(14% 0.005 285.823); + --color-neutral-content: oklch(92% 0.004 286.32); + --color-info: oklch(74% 0.16 232.661); + --color-info-content: oklch(29% 0.066 243.157); + --color-success: oklch(76% 0.177 163.223); + --color-success-content: oklch(37% 0.077 168.94); + --color-warning: oklch(82% 0.189 84.429); + --color-warning-content: oklch(41% 0.112 45.904); + --color-error: oklch(71% 0.194 13.428); + --color-error-content: oklch(27% 0.105 12.094); + --radius-selector: 0.5rem; + --radius-field: 0.25rem; + --radius-box: 0.5rem; + --size-selector: 0.25rem; + --size-field: 0.25rem; + --border: 1px; + --depth: 1; + --noise: 0; + } +} +@layer base { + :root:has(input.theme-controller[value=dark]:checked),[data-theme=dark] { + color-scheme: dark; + --color-base-100: oklch(25.33% 0.016 252.42); + --color-base-200: oklch(23.26% 0.014 253.1); + --color-base-300: oklch(21.15% 0.012 254.09); + --color-base-content: oklch(97.807% 0.029 256.847); + --color-primary: oklch(58% 0.233 277.117); + --color-primary-content: oklch(96% 0.018 272.314); + --color-secondary: oklch(65% 0.241 354.308); + --color-secondary-content: oklch(94% 0.028 342.258); + --color-accent: oklch(77% 0.152 181.912); + --color-accent-content: oklch(38% 0.063 188.416); + --color-neutral: oklch(14% 0.005 285.823); + --color-neutral-content: oklch(92% 0.004 286.32); + --color-info: oklch(74% 0.16 232.661); + --color-info-content: oklch(29% 0.066 243.157); + --color-success: oklch(76% 0.177 163.223); + --color-success-content: oklch(37% 0.077 168.94); + --color-warning: oklch(82% 0.189 84.429); + --color-warning-content: oklch(41% 0.112 45.904); + --color-error: oklch(71% 0.194 13.428); + --color-error-content: oklch(27% 0.105 12.094); + --radius-selector: 0.5rem; + --radius-field: 0.25rem; + --radius-box: 0.5rem; + --size-selector: 0.25rem; + --size-field: 0.25rem; + --border: 1px; + --depth: 1; + --noise: 0; + } +} +@layer base { + :root { + --fx-noise: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='a'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='1.34' numOctaves='4' stitchTiles='stitch'%3E%3C/feTurbulence%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23a)' opacity='0.2'%3E%3C/rect%3E%3C/svg%3E"); + } +} +@layer base { + :root, [data-theme] { + background-color: var(--root-bg, var(--color-base-100)); + color: var(--color-base-content); + } +} +@layer base { + :root:has( .modal-open, .modal[open], .modal:target, .modal-toggle:checked, .drawer:not([class*="drawer-open"]) > .drawer-toggle:checked ) { + overflow: hidden; + } +} +@layer base { + @property --radialprogress { + syntax: ""; + inherits: true; + initial-value: 0%; + } +} +@layer base { + :where( :root:has( .modal-open, .modal[open], .modal:target, .modal-toggle:checked, .drawer:not(.drawer-open) > .drawer-toggle:checked ) ) { + scrollbar-gutter: stable; + background-image: linear-gradient(var(--color-base-100), var(--color-base-100)); + --root-bg: var(--color-base-100); + @supports (color: color-mix(in lab, red, red)) { + --root-bg: color-mix(in srgb, var(--color-base-100), oklch(0% 0 0) 40%); + } + } + :where(.modal[open], .modal-open, .modal-toggle:checked + .modal):not(.modal-start, .modal-end) { + scrollbar-gutter: stable; + } +} +@layer base { + :root { + scrollbar-color: currentColor #0000; + @supports (color: color-mix(in lab, red, red)) { + scrollbar-color: color-mix(in oklch, currentColor 35%, #0000) #0000; + } + } +} +@keyframes progress { + 50% { + background-position-x: -115%; + } +} +@keyframes rating { + 0%, 40% { + scale: 1.1; + filter: brightness(1.05) contrast(1.05); + } +} +@keyframes skeleton { + 0% { + background-position: 150%; + } + 100% { + background-position: -50%; + } +} +@keyframes dropdown { + 0% { + opacity: 0; + } +} +@keyframes radio { + 0% { + padding: 5px; + } + 50% { + padding: 3px; + } +} +@keyframes toast { + 0% { + scale: 0.9; + opacity: 0; + } + 100% { + scale: 1; + opacity: 1; + } +} +@layer base { + @media (prefers-color-scheme: dark) { + :root { + color-scheme: dark; + --color-base-100: oklch(14% 0 0); + --color-base-200: oklch(20% 0 0); + --color-base-300: oklch(26% 0 0); + --color-base-content: oklch(97% 0 0); + --color-primary: oklch(55% 0.013 58.071); + --color-primary-content: oklch(98% 0.001 106.423); + --color-secondary: oklch(60% 0.25 292.717); + --color-secondary-content: oklch(96% 0.016 293.756); + --color-accent: oklch(58% 0.233 277.117); + --color-accent-content: oklch(96% 0.018 272.314); + --color-neutral: oklch(20% 0 0); + --color-neutral-content: oklch(98% 0 0); + --color-info: oklch(74% 0.16 232.661); + --color-info-content: oklch(29% 0.066 243.157); + --color-success: oklch(76% 0.177 163.223); + --color-success-content: oklch(26% 0.051 172.552); + --color-warning: oklch(82% 0.189 84.429); + --color-warning-content: oklch(27% 0.077 45.635); + --color-error: oklch(70% 0.191 22.216); + --color-error-content: oklch(25% 0.092 26.042); + --radius-selector: 1rem; + --radius-field: 2rem; + --radius-box: 0.5rem; + --size-selector: 0.25rem; + --size-field: 0.25rem; + --border: 1px; + --depth: 1; + --noise: 0; + } + } +} +@layer base { + :where(:root),:root:has(input.theme-controller[value=synthwave]:checked),[data-theme="synthwave"] { + color-scheme: dark; + --color-base-100: oklch(14% 0 0); + --color-base-200: oklch(20% 0 0); + --color-base-300: oklch(26% 0 0); + --color-base-content: oklch(97% 0 0); + --color-primary: oklch(55% 0.013 58.071); + --color-primary-content: oklch(98% 0.001 106.423); + --color-secondary: oklch(60% 0.25 292.717); + --color-secondary-content: oklch(96% 0.016 293.756); + --color-accent: oklch(58% 0.233 277.117); + --color-accent-content: oklch(96% 0.018 272.314); + --color-neutral: oklch(20% 0 0); + --color-neutral-content: oklch(98% 0 0); + --color-info: oklch(74% 0.16 232.661); + --color-info-content: oklch(29% 0.066 243.157); + --color-success: oklch(76% 0.177 163.223); + --color-success-content: oklch(26% 0.051 172.552); + --color-warning: oklch(82% 0.189 84.429); + --color-warning-content: oklch(27% 0.077 45.635); + --color-error: oklch(70% 0.191 22.216); + --color-error-content: oklch(25% 0.092 26.042); + --radius-selector: 1rem; + --radius-field: 2rem; + --radius-box: 0.5rem; + --size-selector: 0.25rem; + --size-field: 0.25rem; + --border: 1px; + --depth: 1; + --noise: 0; + } +} +@property --tw-scale-x { + syntax: "*"; + inherits: false; + initial-value: 1; +} +@property --tw-scale-y { + syntax: "*"; + inherits: false; + initial-value: 1; +} +@property --tw-scale-z { + syntax: "*"; + inherits: false; + initial-value: 1; +} +@property --tw-rotate-x { + syntax: "*"; + inherits: false; +} +@property --tw-rotate-y { + syntax: "*"; + inherits: false; +} +@property --tw-rotate-z { + syntax: "*"; + inherits: false; +} +@property --tw-skew-x { + syntax: "*"; + inherits: false; +} +@property --tw-skew-y { + syntax: "*"; + inherits: false; +} +@property --tw-border-style { + syntax: "*"; + inherits: false; + initial-value: solid; +} +@property --tw-font-weight { + syntax: "*"; + inherits: false; +} +@property --tw-shadow { + syntax: "*"; + inherits: false; + initial-value: 0 0 #0000; +} +@property --tw-shadow-color { + syntax: "*"; + inherits: false; +} +@property --tw-shadow-alpha { + syntax: ""; + inherits: false; + initial-value: 100%; +} +@property --tw-inset-shadow { + syntax: "*"; + inherits: false; + initial-value: 0 0 #0000; +} +@property --tw-inset-shadow-color { + syntax: "*"; + inherits: false; +} +@property --tw-inset-shadow-alpha { + syntax: ""; + inherits: false; + initial-value: 100%; +} +@property --tw-ring-color { + syntax: "*"; + inherits: false; +} +@property --tw-ring-shadow { + syntax: "*"; + inherits: false; + initial-value: 0 0 #0000; +} +@property --tw-inset-ring-color { + syntax: "*"; + inherits: false; +} +@property --tw-inset-ring-shadow { + syntax: "*"; + inherits: false; + initial-value: 0 0 #0000; +} +@property --tw-ring-inset { + syntax: "*"; + inherits: false; +} +@property --tw-ring-offset-width { + syntax: ""; + inherits: false; + initial-value: 0px; +} +@property --tw-ring-offset-color { + syntax: "*"; + inherits: false; + initial-value: #fff; +} +@property --tw-ring-offset-shadow { + syntax: "*"; + inherits: false; + initial-value: 0 0 #0000; +} +@property --tw-outline-style { + syntax: "*"; + inherits: false; + initial-value: solid; +} +@property --tw-blur { + syntax: "*"; + inherits: false; +} +@property --tw-brightness { + syntax: "*"; + inherits: false; +} +@property --tw-contrast { + syntax: "*"; + inherits: false; +} +@property --tw-grayscale { + syntax: "*"; + inherits: false; +} +@property --tw-hue-rotate { + syntax: "*"; + inherits: false; +} +@property --tw-invert { + syntax: "*"; + inherits: false; +} +@property --tw-opacity { + syntax: "*"; + inherits: false; +} +@property --tw-saturate { + syntax: "*"; + inherits: false; +} +@property --tw-sepia { + syntax: "*"; + inherits: false; +} +@property --tw-drop-shadow { + syntax: "*"; + inherits: false; +} +@property --tw-drop-shadow-color { + syntax: "*"; + inherits: false; +} +@property --tw-drop-shadow-alpha { + syntax: ""; + inherits: false; + initial-value: 100%; +} +@property --tw-drop-shadow-size { + syntax: "*"; + inherits: false; +} +@property --tw-ease { + syntax: "*"; + inherits: false; +} +@layer properties { + @supports ((-webkit-hyphens: none) and (not (margin-trim: inline))) or ((-moz-orient: inline) and (not (color:rgb(from red r g b)))) { + *, ::before, ::after, ::backdrop { + --tw-scale-x: 1; + --tw-scale-y: 1; + --tw-scale-z: 1; + --tw-rotate-x: initial; + --tw-rotate-y: initial; + --tw-rotate-z: initial; + --tw-skew-x: initial; + --tw-skew-y: initial; + --tw-border-style: solid; + --tw-font-weight: initial; + --tw-shadow: 0 0 #0000; + --tw-shadow-color: initial; + --tw-shadow-alpha: 100%; + --tw-inset-shadow: 0 0 #0000; + --tw-inset-shadow-color: initial; + --tw-inset-shadow-alpha: 100%; + --tw-ring-color: initial; + --tw-ring-shadow: 0 0 #0000; + --tw-inset-ring-color: initial; + --tw-inset-ring-shadow: 0 0 #0000; + --tw-ring-inset: initial; + --tw-ring-offset-width: 0px; + --tw-ring-offset-color: #fff; + --tw-ring-offset-shadow: 0 0 #0000; + --tw-outline-style: solid; + --tw-blur: initial; + --tw-brightness: initial; + --tw-contrast: initial; + --tw-grayscale: initial; + --tw-hue-rotate: initial; + --tw-invert: initial; + --tw-opacity: initial; + --tw-saturate: initial; + --tw-sepia: initial; + --tw-drop-shadow: initial; + --tw-drop-shadow-color: initial; + --tw-drop-shadow-alpha: 100%; + --tw-drop-shadow-size: initial; + --tw-ease: initial; + } + } +} diff --git a/wco/privatri/privatri.js b/wco/privatri/privatri.js index f5fadd3..f1d28b5 100644 --- a/wco/privatri/privatri.js +++ b/wco/privatri/privatri.js @@ -1,194 +1,506 @@ -/** - * @file privatri.js - * @description WCO component for managing private, thread-based messages in IndexedDB. - * @version 1.0 - */ +import apx from "./apx.js"; +import { getOldestPrivatriids, syncronizeBackend } from "./utils.js"; -((window) => { - 'use strict'; +const bodyEl = document.querySelector("body"); +const searchInputEl = document.querySelector("#threadSearchBar"); +const threadFilterOptionsElArray = document.querySelector("#threadFilterOptions").querySelectorAll("li"); +const threadsContainerEl = document.querySelector("#threadsContainer"); +const threadPageEl = document.querySelector("#threadPage"); +const messageInputEl = document.getElementById("messageInput"); +const messagesContainerEl = document.querySelector("#messagesContainer"); +const attachmentsInputEl = document.querySelector("#attachmentsInput"); - // --- Component Definition --- - const privatri = {}; +function formatDate(timestamp) { + const date = new Date(timestamp); + const day = String(date.getDate()).padStart(2, "0"); + const month = String(date.getMonth() + 1).padStart(2, "0"); + const year = date.getFullYear(); + const hours = String(date.getHours()).padStart(2, "0"); + const minutes = String(date.getMinutes()).padStart(2, "0"); - // --- Private State --- - let _db = null; // Holds the single, persistent IndexedDB connection - const DB_NAME = 'privatriDB'; - const DB_VERSION = 1; + return `${day}/${month}/${year} ${hours}:${minutes}`; +}; - /** - * Logs messages to the console with a consistent prefix. - * @param {string} level - The log level (e.g., 'log', 'error', 'warn'). - * @param {...any} args - The messages to log. - */ - const _log = (level, ...args) => { - console[level]('[privatri]', ...args); - }; +searchInputEl.addEventListener("input", () => { + const value = searchInputEl.value.toLowerCase(); - /** - * Opens and initializes the IndexedDB database. - * This function is called by init() and handles the connection and schema upgrades. - * @param {string[]} initialStoreNames - An array of store names (thread IDs) to ensure exist. - * @returns {Promise} A promise that resolves with the database connection. - */ - const _openDatabase = (initialStoreNames = []) => { - return new Promise((resolve, reject) => { - _log('log', `Opening database "${DB_NAME}" with version ${DB_VERSION}.`); - - const request = indexedDB.open(DB_NAME, DB_VERSION); - - // This event is only triggered for new databases or version changes. - request.onupgradeneeded = (event) => { - const db = event.target.result; - _log('log', 'Database upgrade needed. Current stores:', [...db.objectStoreNames]); - - initialStoreNames.forEach(storeName => { - if (!db.objectStoreNames.contains(storeName)) { - _log('log', `Creating new object store: "${storeName}"`); - db.createObjectStore(storeName, { keyPath: 'key' }); - } - }); - }; - - request.onsuccess = (event) => { - _log('log', 'Database opened successfully.'); - _db = event.target.result; - - // Generic error handler for the connection - _db.onerror = (event) => { - _log('error', 'Database error:', event.target.error); + Array.from(threadsContainerEl.children).forEach(child => { + if (child.querySelector("li > div").textContent.toLowerCase().includes(value)) { + child.style.display = "block"; + } else { + child.style.display = "none"; }; + }); +}); + +threadFilterOptionsElArray.forEach(option => { + option.addEventListener("click", async () => { + const filter = option.getAttribute("data-filter"); + + if (filter === "date") { + Array.from(threadsContainerEl.children).sort((a, b) => { + return a.querySelector("li > span").getAttribute("data-timestamp") - b.querySelector("li > span").getAttribute("data-timestamp"); + }).forEach(child => { + threadsContainerEl.appendChild(child); + }); + } else if (filter === "name") { + Array.from(threadsContainerEl.children).sort((a, b) => { + return a.querySelector("li > div").textContent.localeCompare(b.querySelector("li > div").textContent); + }).forEach(child => { + threadsContainerEl.appendChild(child); + }); + }; + }); +}); + +const sendNewMessage = async (timestamp, message, attachmentsArray) => { + let messageTemplate = ""; + + await fetch("./message.mustache") + .then(res => res.text()) + .then(template => { + messageTemplate = template; + }); + + messageTemplate = Mustache.render(messageTemplate, { timestamp, message, date: formatDate(timestamp) }); + + const tempNode = document.createElement("div"); + tempNode.innerHTML = messageTemplate; + const messageNode = tempNode.firstElementChild; + + attachmentsArray.forEach(attachment => { + messageNode.querySelector("div.attachmentsContainer").insertAdjacentHTML("beforeend", `${attachment.filename}`); + }); + + return messageNode.outerHTML; +}; + +const editMessage = async (timestamp, message) => { + let editMessageTemplate = ""; + + await fetch("./editMessage.mustache") + .then(res => res.text()) + .then(template => { + editMessageTemplate = template; + }); + + return Mustache.render(editMessageTemplate, { message, date: formatDate(timestamp) }); +}; + +const displayToastAlert = async (message) => { + let toastAlertTemplate = ""; + + await fetch("./toastAlert.mustache") + .then(res => res.text()) + .then(template => { + toastAlertTemplate = template; + }); + + return Mustache.render(toastAlertTemplate, { message }); +}; + +sendMessageBtn.addEventListener("click", async () => { + const message = messageInputEl.value.trim(); + + if (message !== "") { + const messageObj = await (async (publicKey) => { + const timestamp = Date.now(); + const alias = await apx.crypto.encryptMessage(JSON.parse(localStorage.getItem("apx")).data.headers.xalias, publicKey); + + return { + privatriid: `${threadPageEl.getAttribute("data-uuid")}_${timestamp}`, + timestamp: timestamp, + sender_alias: alias, + message: await apx.crypto.encryptMessage(message, publicKey), + attachments: await (async () => { + const attachmentsArray = []; + const localAttachmentsArray = JSON.parse(sessionStorage.getItem(`attachmentsArray_${threadPageEl.getAttribute("data-uuid")}`)) || []; + + if (localAttachmentsArray.length > 0) { + for (const attachment of localAttachmentsArray) { + attachmentsArray.push( + { + fileType: attachment.fileType, + filename: attachment.filename, + content: await apx.crypto.encryptMessage(attachment.content, publicKey) + } + ); + }; + }; + + return attachmentsArray; + })() + }; + })((await apx.indexedDB.get("privatri", "messages", `${threadPageEl.getAttribute("data-uuid")}_${messagesContainerEl.firstElementChild.getAttribute("data-timestamp")}`)).publicKey); - resolve(_db); - }; + const newMessageHTML = await sendNewMessage(messageObj.timestamp, message, JSON.parse(sessionStorage.getItem(`attachmentsArray_${threadPageEl.getAttribute("data-uuid")}`)) || []); - request.onerror = (event) => { - _log('error', 'Failed to open database:', event.target.error); - reject(event.target.error); - }; + sessionStorage.removeItem(`attachmentsArray_${threadPageEl.getAttribute("data-uuid")}`); + + messagesContainerEl.insertAdjacentHTML("beforeend", newMessageHTML); + + messageInputEl.value = ""; + + // Faire un post sur l'endpoint /privatri + + await apx.indexedDB.set("privatri", "messages", messageObj); + }; +}); + +document.addEventListener("DOMContentLoaded", async () => { + const lastConnection = JSON.parse(localStorage.getItem("lastConnection")) || Date.now(); + + await syncronizeBackend(lastConnection); + + const privatriidArray = await getOldestPrivatriids("privatri", "messages"); + + const thread = async (name, uuid) => { + let threadTemplate = ""; + + await fetch("./thread.mustache") + .then(res => res.text()) + .then(template => { + threadTemplate = template; + }); + + return Mustache.render(threadTemplate, { uuid, name }); + }; + + for (const privatriid of privatriidArray) { + const obj = await apx.indexedDB.get("privatri", "messages", privatriid) + + const privateKey = (await apx.indexedDB.get("privatri", "threads", obj.thread)).privateKey; + const name = (await apx.crypto.decryptMessage(obj.title, privateKey)).data; + + threadsContainerEl.insertAdjacentHTML("beforeend", await thread(name, obj.thread)); + }; + + Array.from(threadsContainerEl.children).forEach(child => { + child.addEventListener("click", async () => { + const uuid = child.getAttribute("data-uuid"); + + const messagesObj = await new Promise((resolve, reject) => { + const request = indexedDB.open("privatri", 1); + + request.onsuccess = (event) => { + const db = event.target.result; + + const transaction = db.transaction("messages", "readonly"); + const store = transaction.objectStore("messages"); + const cursorRequest = store.openCursor(); + + const result = []; + cursorRequest.onsuccess = (event) => { + const cursor = event.target.result; + + if (cursor) { + const obj = cursor.value; + const keyUuid = obj.privatriid.split("_")[0]; + + if (keyUuid === uuid) { + result.push(obj); + }; + cursor.continue(); + } else { + resolve(result); + }; + }; + + cursorRequest.onerror = (event) => reject(event); + }; + + request.onerror = (event) => reject(event); + }); + + threadPageEl.setAttribute("data-uuid", uuid); + threadPageEl.querySelector("#threadName").innerText = (await apx.crypto.decryptMessage(messagesObj[0].title, (await apx.indexedDB.get("privatri", "threads", uuid)).privateKey)).data; + + messagesContainerEl.innerHTML = ""; + + let privateKey = ""; + + for (const message of messagesObj) { + if (privateKey === "") { + privateKey = (await apx.indexedDB.get("privatri", "threads", message.thread)).privateKey; + }; + + const decryptedMessage = (await apx.crypto.decryptMessage(message.message, privateKey)).data; + + let decryptedAttachments = []; + + if (message.attachments !== undefined) { + decryptedAttachments = await (async () => { + const attachmentsArray = []; + + if (message.attachments.length > 0) { + for (const attachment of message.attachments) { + attachmentsArray.push({ + fileType: attachment.fileType, + filename: attachment.filename, + content: (await apx.crypto.decryptMessage(attachment.content, privateKey)).data + }); + }; + }; + + return attachmentsArray; + })(); + }; + + messagesContainerEl.insertAdjacentHTML("beforeend", await sendNewMessage(message.timestamp, decryptedMessage, decryptedAttachments)); + }; + }); }); - }; - // --- Public API --- + document.querySelectorAll("a").forEach(link => { + link.addEventListener("click", async (event) => { + event.preventDefault(); - /** - * Initializes the privatri component. - * Opens the database connection and ensures initial object stores are created. - * @param {object} config - Configuration object. - * @param {string[]} [config.threads=[]] - An array of initial thread IDs (store names) to create. - * @returns {Promise} - */ - privatri.init = async (config = {}) => { - if (_db) { - _log('warn', 'privatri component already initialized.'); - return; - } - const threads = config.threads || []; - await _openDatabase(threads); - }; + window.history.replaceState({}, document.title, window.location.pathname); - /** - * Retrieves a value from a specific store (thread). - * @param {string} storeName - The name of the store (thread ID). - * @param {string} key - The key of the item to retrieve. - * @returns {Promise} A promise that resolves with the value or null if not found. - */ - privatri.getValue = (storeName, key) => { - return new Promise((resolve, reject) => { - if (!_db || !_db.objectStoreNames.contains(storeName)) { - _log('warn', `Store "${storeName}" does not exist.`); - return resolve(null); - } - const transaction = _db.transaction(storeName, 'readonly'); - const store = transaction.objectStore(storeName); - const request = store.get(key); + const templateName = link.getAttribute("data-template"); - request.onsuccess = () => resolve(request.result ? request.result.value : null); - request.onerror = (e) => { - _log('error', `Error getting value for key "${key}" from store "${storeName}":`, e.target.error); - reject(e.target.error); - }; + if (templateName === "threadAliasList" || templateName === "threadSettings") { + window.history.pushState({}, "", `${window.location.pathname}?uuid=${threadPageEl.getAttribute("data-uuid")}`); + }; + + await fetch(`./${templateName}.mustache`) + .then(res => res.text()) + .then(template => { + template = Mustache.render(template, {}); + + bodyEl.innerHTML = template; + }); + + const script = document.createElement("script"); + + script.type = "module"; + script.src = `./${templateName}.js`; + + bodyEl.appendChild(script); + }); }); - }; +}); - /** - * Adds or updates a key-value pair in a specific store (thread). - * @param {string} storeName - The name of the store (thread ID). - * @param {string} key - The key of the item to set. - * @param {any} value - The value to store. - * @returns {Promise} - */ - privatri.setValue = (storeName, key, value) => { - return new Promise((resolve, reject) => { - if (!_db || !_db.objectStoreNames.contains(storeName)) { - _log('error', `Cannot set value. Store "${storeName}" does not exist.`); - return reject(new Error(`Store "${storeName}" not found.`)); - } - const transaction = _db.transaction(storeName, 'readwrite'); - const store = transaction.objectStore(storeName); - const request = store.put({ key: key, value: value }); +window.addEventListener("beforeunload", () => { + if (apx) + localStorage.setItem("lastConnection", JSON.stringify(Date.now())); +}); - request.onsuccess = () => resolve(); - request.onerror = (e) => { - _log('error', `Error setting value for key "${key}" in store "${storeName}":`, e.target.error); - reject(e.target.error); - }; - }); - }; +window.attachDeleteMessageEvent = async function attachDeleteMessageEvent(btn) { + const messageEl = btn.parentElement.parentElement.parentElement; - /** - * Removes a key-value pair from a specific store (thread). - * @param {string} storeName - The name of the store (thread ID). - * @param {string} key - The key of the item to remove. - * @returns {Promise} - */ - privatri.removeKey = (storeName, key) => { - return new Promise((resolve, reject) => { - if (!_db || !_db.objectStoreNames.contains(storeName)) { - _log('warn', `Cannot remove key. Store "${storeName}" does not exist.`); - return resolve(); // Resolve peacefully if store doesn't exist - } - const transaction = _db.transaction(storeName, 'readwrite'); - const store = transaction.objectStore(storeName); - const request = store.delete(key); + const privateKey = (await apx.indexedDB.get("privatri", "threads", threadPageEl.getAttribute("data-uuid"))).privateKey; - request.onsuccess = () => resolve(); - request.onerror = (e) => { - _log('error', `Error removing key "${key}" from store "${storeName}":`, e.target.error); - reject(e.target.error); - }; - }); - }; + const signatureMessage = `${JSON.parse(localStorage.getItem("apx")).data.headers.xalias}_${threadPageEl.getAttribute("data-uuid")}_${messageEl.getAttribute("data-timestamp")}`; + const signature = await apx.crypto.sign(signatureMessage, privateKey); + + let verified = false; - /** - * A utility function to save a batch of messages, demonstrating how to use the component. - * @param {object} threadsObj - An object where keys are thread IDs and values are message objects. - * @example - * const messages = { - * "thread-123": { "1678886400": { text: "Hello" } }, - * "thread-456": { "1678886401": { text: "Hi there" } } - * }; - * await privatri.storeMessages(messages); - */ - privatri.storeMessages = async (threadsObj = {}) => { - if (!_db) { - _log('error', 'Database not initialized. Please call privatri.init() first.'); - return; - } - _log('log', 'Storing messages in IndexedDB...'); - for (const [uuid, threadObj] of Object.entries(threadsObj)) { - // Ensure the object store exists before trying to write to it. - if (!_db.objectStoreNames.contains(uuid)) { - _log('warn', `Store "${uuid}" not found during message storage. You may need to re-init with this thread.`); - continue; - } - for (const [timestamp, messageObj] of Object.entries(threadObj)) { - await privatri.setValue(uuid, timestamp, messageObj); - } - } - _log('log', 'Finished storing messages.'); - }; + try { + verified = await fetch("", { + method: "GET", + body: JSON.stringify({ + message: signatureMessage, + signature: signature, + uuid: threadPageEl.getAttribute("data-uuid"), + timestamp: messageEl.getAttribute("data-timestamp"), + }) + }); + } catch (error) { + console.error("Error while verifying signature:", error); + }; - // Expose the component to the global window object - window.privatri = privatri; + const authorAlias = await apx.crypto.decryptMessage((await apx.indexedDB.get("privatri", "messages", `${threadPageEl.getAttribute("data-uuid")}_${messageEl.getAttribute("data-timestamp")}`)).sender_alias, privateKey).data; -})(window); + if ((JSON.parse(localStorage.getItem("apx")).data.headers.xalias === authorAlias) && (verified === true)) { + await apx.indexedDB.del("privatri", "messages", `${threadPageEl.getAttribute("data-uuid")}_${messageEl.getAttribute("data-timestamp")}`); + + messageEl.remove(); + } else { + bodyEl.insertAdjacentHTML("beforeend", await displayToastAlert("You don't have the permissions to delete this message.")); + + setTimeout(() => { + bodyEl.lastElementChild.remove(); + }, 3000); + }; +}; + +window.attachEditMessageEvent = async function attachEditMessageEvent(btn) { + const messageEl = btn.parentElement.parentElement.parentElement; + + const authorAlias = (await apx.crypto.decryptMessage((await apx.indexedDB.get("privatri", "messages", `${threadPageEl.getAttribute("data-uuid")}_${messageEl.getAttribute("data-timestamp")}`)).sender_alias, (await apx.indexedDB.get("privatri", "threads", threadPageEl.getAttribute("data-uuid"))).privateKey)).data; + + if (JSON.parse(localStorage.getItem("apx")).data.headers.xalias === authorAlias) { + const messageValue = messageEl.querySelector("div.message").innerText; + + const attachmentsArray = (() => { + const attachmentsArray = []; + + messageEl.querySelector("div.attachmentsContainer").querySelectorAll("img").forEach(img => { + attachmentsArray.push( + { + fileType: img.getAttribute("src").match(/^data:(.*);base64,/)[1], + filename: img.getAttribute("alt"), + content: img.getAttribute("src").split(",")[1] + } + ); + }); + + return attachmentsArray; + })(); + + messageEl.innerHTML = await editMessage(parseInt(messageEl.getAttribute("data-timestamp"), 10), messageEl.querySelector("div.message").innerText); + + messageEl.querySelector("button.cancelEditBtn").addEventListener("click", async () => { + const messageObj = await apx.indexedDB.get("privatri", "messages", `${threadPageEl.getAttribute("data-uuid")}_${messageEl.getAttribute("data-timestamp")}`); + + messageEl.innerHTML = await sendNewMessage(messageObj.timestamp, messageValue, attachmentsArray); + }); + + messageEl.querySelector("button.saveEditBtn").addEventListener("click", async () => { + const newMessageValue = messageEl.querySelector("textarea").value.trim(); + + if (newMessageValue !== "") { + const privateKey = (await apx.indexedDB.get("privatri", "threads", threadPageEl.getAttribute("data-uuid"))).privateKey; + + const messageObj = await apx.indexedDB.get("privatri", "messages", `${threadPageEl.getAttribute("data-uuid")}_${messageEl.getAttribute("data-timestamp")}`); + + messageObj.message = await apx.crypto.encryptMessage(newMessageValue, privateKey), + + await apx.indexedDB.set("privatri", "messages", messageObj); + + messageEl.innerHTML = await sendNewMessage(messageObj.timestamp, newMessageValue, attachmentsArray); + }; + }); + } else { + bodyEl.insertAdjacentHTML("beforeend", await displayToastAlert("You don't have the permissions to edit this message.")); + + setTimeout(() => { + bodyEl.lastElementChild.remove(); + }, 3000); + }; +}; + +window.attachEventDisplayAttachment = function(img) { + let overlay = document.getElementById("imageOverlay"); + + if (overlay === null) { + overlay = document.createElement('div'); + + overlay.id = "imageOverlay"; + overlay.style.position = "fixed"; + overlay.style.top = 0; + overlay.style.left = 0; + overlay.style.width = "100vw"; + overlay.style.height = "100vh"; + overlay.style.background = "rgba(0, 0, 0, 0.75)"; + overlay.style.display = "flex"; + overlay.style.alignItems = "center"; + overlay.style.justifyContent = "center"; + overlay.style.zIndex = 999; + + overlay.onclick = () => { + overlay.remove(); + }; + + document.body.appendChild(overlay); + } else { + overlay.innerHTML = ""; + overlay.style.display = "flex"; + }; + + const fullScreenImage = document.createElement("img"); + + fullScreenImage.src = img.src; + fullScreenImage.alt = img.alt; + fullScreenImage.style.maxWidth = "90vw"; + fullScreenImage.style.maxHeight = "90vh"; + overlay.appendChild(fullScreenImage); +}; + +document.querySelector("#attachmentsBtn").addEventListener("click", () => { + attachmentsInputEl.click(); +}); + +attachmentsInputEl.addEventListener("change", async () => { + const filesArray = Array.from(attachmentsInputEl.files); + const maxFileSize = 5 * 1024 * 1024; + const maxSize = 512; + + for (const file of filesArray) { + if (file.size <= maxFileSize) { + const attachmentObj = await new Promise((resolve, reject) => { + const attachmentObj = { + fileType: file.type, + filename: file.name, + content: "" + }; + + const img = new Image(); + + img.onload = () => { + if (img.width > maxSize || img.height > maxSize) { + let width = img.width; + let height = img.height; + + if (width > maxSize) { + height *= maxSize / width; + width = maxSize; + }; + + if (height > maxSize) { + width *= maxSize / height; + height = maxSize; + }; + + const canvas = document.createElement("canvas"); + canvas.width = width; + canvas.height = height; + + const ctx = canvas.getContext("2d"); + + ctx.drawImage(img, 0, 0, width, height); + + canvas.toBlob(blob => { + const reader = new FileReader(); + + reader.onload = (event) => { + attachmentObj.content = event.target.result.split(",")[1]; + + resolve(attachmentObj); + }; + + reader.onerror = reject; + reader.readAsDataURL(blob); + }, "image/jpeg", 0.8); + } else { + const reader = new FileReader(); + + reader.onload = (event) => { + attachmentObj.content = event.target.result.split(",")[1]; + + resolve(attachmentObj); + }; + + reader.onerror = reject; + reader.readAsDataURL(file); + }; + }; + + img.onerror = reject; + img.src = URL.createObjectURL(file); + + return attachmentObj; + }); + + if (sessionStorage.getItem(`attachmentsArray_${threadPageEl.getAttribute("data-uuid")}`) === null) { + sessionStorage.setItem(`attachmentsArray_${threadPageEl.getAttribute("data-uuid")}`, JSON.stringify([attachmentObj])); + } else { + const attachmentsArray = JSON.parse(sessionStorage.getItem(`attachmentsArray_${threadPageEl.getAttribute("data-uuid")}`)); + attachmentsArray.push(attachmentObj); + + sessionStorage.setItem(`attachmentsArray_${threadPageEl.getAttribute("data-uuid")}`, JSON.stringify(attachmentsArray)); + }; + }; + }; +}); \ No newline at end of file diff --git a/wco/privatri/thread.mustache b/wco/privatri/thread.mustache new file mode 100644 index 0000000..2103725 --- /dev/null +++ b/wco/privatri/thread.mustache @@ -0,0 +1,7 @@ +
    +
  • + +
    {{name}}
    + 17 +
  • +
\ No newline at end of file diff --git a/wco/privatri/threadAliasList.js b/wco/privatri/threadAliasList.js new file mode 100644 index 0000000..9456e0a --- /dev/null +++ b/wco/privatri/threadAliasList.js @@ -0,0 +1,88 @@ +import apx from "./apx.js"; +import { getOldestPrivatriids } from "./utils.js"; + +const bodyEl = document.querySelector("body"); +const aliasListContainerEl = document.querySelector("#aliasListContainer"); + +let aliasesArray = []; + +const newAlias = async (alias) => { + let aliasTemplate = ""; + + await fetch("./alias.mustache") + .then(res => res.text()) + .then(template => { + aliasTemplate = template; + }); + + return Mustache.render(aliasTemplate, { alias }); +}; + +const displayToastAlert = async (message) => { + let toastAlertTemplate = ""; + + await fetch("./toastAlert.mustache") + .then(res => res.text()) + .then(template => { + toastAlertTemplate = template; + }); + + return Mustache.render(toastAlertTemplate, { message }); +}; + +(async () => { + const params = new URLSearchParams(window.location.search); + const uuid = params.get("uuid"); + + const privateKey = (await apx.indexedDB.get("privatri", "threads", uuid)).privateKey; + + const privatriidArray = await getOldestPrivatriids("privatri", "messages"); + + privatriidArray.forEach(async privatriid => { + if (privatriid.split("_")[0] === uuid) { + aliasesArray = (await apx.indexedDB.get("privatri", "messages", privatriid)).aliases; + const ownerAlias = (await apx.indexedDB.get("privatri", "messages", privatriid)).owner; + + for (const alias of aliasesArray) { + aliasListContainerEl.insertAdjacentHTML("beforeend", await newAlias({ + decrypted: (await apx.crypto.decryptMessage(alias, privateKey)).data, + crypted: alias + })); + }; + + document.querySelectorAll("button.removeAliasBtn").forEach(btn => { + btn.addEventListener("click", async () => { + if (JSON.parse(localStorage.getItem("apx")).data.headers.xalias === (await apx.crypto.decryptMessage(ownerAlias, privateKey)).data) { + if (btn.getAttribute("data-alias") !== ownerAlias) { + const alias = btn.getAttribute("data-alias"); + aliasesArray = aliasesArray.filter(a => a !== alias); + btn.parentElement.parentElement.remove(); + + privatriidArray.forEach(async privatriid => { + if (privatriid.split("_")[0] === uuid) { + const messageObj = await apx.indexedDB.get("privatri", "messages", privatriid); + messageObj.aliases = aliasesArray; + + await apx.indexedDB.set("privatri", "messages", messageObj); + }; + }); + } else { + bodyEl.insertAdjacentHTML("beforeend", await displayToastAlert("You cannot remove the owner of the thread.")); + + setTimeout(() => { + bodyEl.lastElementChild.remove(); + }, 3000); + }; + } else { + bodyEl.insertAdjacentHTML("beforeend", await displayToastAlert("You don't have the permissions to remove this alias.")); + + setTimeout(() => { + bodyEl.lastElementChild.remove(); + }, 3000); + }; + }); + }); + }; + }); +})(); + diff --git a/wco/privatri/threadAliasList.mustache b/wco/privatri/threadAliasList.mustache new file mode 100644 index 0000000..a42e90a --- /dev/null +++ b/wco/privatri/threadAliasList.mustache @@ -0,0 +1,6 @@ +
+

Alias list

+
+ +
+
\ No newline at end of file diff --git a/wco/privatri/threadSettings.js b/wco/privatri/threadSettings.js new file mode 100644 index 0000000..22ac9c9 --- /dev/null +++ b/wco/privatri/threadSettings.js @@ -0,0 +1,72 @@ +import apx from "./apx.js"; +import { getOldestPrivatriids } from "./utils.js"; + +const bodyEl = document.querySelector("body"); +const threadNameInputEl = document.querySelector("#threadNameInput"); +const autoDeletionBtnElArray = document.querySelectorAll("li.autoDeletionBtn"); +const applyModificationsBtnEl = document.querySelector("#applyModificationsBtn"); + +const displayToastAlert = async (message) => { + let toastAlertTemplate = ""; + + await fetch("./toastAlert.mustache") + .then(res => res.text()) + .then(template => { + toastAlertTemplate = template; + }); + + return Mustache.render(toastAlertTemplate, { message }); +}; + +let messageObj = {}; + +(async () => { + const params = new URLSearchParams(window.location.search); + const uuid = params.get("uuid"); + + const privateKey = (await apx.indexedDB.get("privatri", "threads", uuid)).privateKey; + + const privatriidArray = await getOldestPrivatriids("privatri", "messages"); + let ownerAlias = ""; + + privatriidArray.forEach(async privatriid => { + if (privatriid.split("_")[0] === uuid) { + messageObj = await apx.indexedDB.get("privatri", "messages", privatriid); + ownerAlias = messageObj.owner; + + threadNameInputEl.value = (await apx.crypto.decryptMessage(messageObj.title, privateKey)).data; + + (Array.from(autoDeletionBtnElArray).find(el => el.getAttribute("data-auto-deletion") === String(messageObj.dt_autodestruction))).classList.add("bg-base-200"); + + if (messageObj.urgencydeletion === true) { + document.querySelector('input[type="checkbox"].toggle').checked = true; + }; + }; + }); + + applyModificationsBtnEl.addEventListener("click", async () => { + if (JSON.parse(localStorage.getItem("apx")).data.headers.xalias === (await apx.crypto.decryptMessage(ownerAlias, privateKey)).data) { + messageObj.title = await apx.crypto.encryptMessage(threadNameInputEl.value, privateKey); + messageObj.dt_autodestruction = (() => { + const selectedBtn = Array.from(autoDeletionBtnElArray).find(btn => btn.classList.contains("bg-base-200")); + return parseInt(selectedBtn ? selectedBtn.getAttribute("data-auto-deletion") : 0, 10); + })(); + messageObj.urgencydeletion = document.querySelector('input[type="checkbox"].toggle').checked; + + await apx.indexedDB.set("privatri", "messages", messageObj); + } else { + bodyEl.insertAdjacentHTML("beforeend", await displayToastAlert("You don't have the permissions to edit this thread.")); + + setTimeout(() => { + bodyEl.lastElementChild.remove(); + }, 3000); + }; + }); +})(); + +autoDeletionBtnElArray.forEach(btn => { + btn.addEventListener("click", () => { + autoDeletionBtnElArray.forEach(btn => btn.classList.remove("bg-base-200")); + btn.classList.add("bg-base-200"); + }); +}); diff --git a/wco/privatri/threadSettings.mustache b/wco/privatri/threadSettings.mustache new file mode 100644 index 0000000..3ea3a91 --- /dev/null +++ b/wco/privatri/threadSettings.mustache @@ -0,0 +1,33 @@ +
+

Thread settings

+
+ + + + +
+ +
+ +

Urgency deletion

+
+ +
\ No newline at end of file diff --git a/wco/privatri/toastAlert.mustache b/wco/privatri/toastAlert.mustache new file mode 100644 index 0000000..00a4393 --- /dev/null +++ b/wco/privatri/toastAlert.mustache @@ -0,0 +1,5 @@ +
+
+ {{message}} +
+
\ No newline at end of file diff --git a/wco/privatri/utils.js b/wco/privatri/utils.js new file mode 100644 index 0000000..3eb0d02 --- /dev/null +++ b/wco/privatri/utils.js @@ -0,0 +1,46 @@ +async function getOldestPrivatriids(dbName, storeName) { + return new Promise((resolve, reject) => { + const request = indexedDB.open(dbName, 1); + + request.onsuccess = (event) => { + const db = event.target.result; + + const transaction = db.transaction(storeName, "readonly"); + const store = transaction.objectStore(storeName); + const cursorRequest = store.openCursor(); + const uuidMap = {}; + + cursorRequest.onsuccess = (event) => { + const cursor = event.target.result; + if (cursor) { + const obj = cursor.value; + + const [uuid, timestamp] = obj.privatriid.split("_"); + + if (!uuidMap[uuid] || Number(timestamp) < uuidMap[uuid].timestamp) { + uuidMap[uuid] = { privatriid: obj.privatriid, timestamp: Number(timestamp) }; + }; + + cursor.continue(); + } else { + const result = Object.values(uuidMap).map(event => event.privatriid); + + resolve(result); + }; + }; + + cursorRequest.onerror = (event) => reject(event); + }; + + request.onerror = (event) => reject(event); + }); +}; + +async function syncronizeBackend(lastConnection) { + +}; + +export { + getOldestPrivatriids, + syncronizeBackend +}; \ No newline at end of file