personal web client for Bluesky
typescript solidjs bluesky atcute

initial commit

Changed files
+14586
.vscode
patches
src
api
assets
components
globals
lib
styles
views
+24
.gitignore
··· 1 + # Logs 2 + logs 3 + *.log 4 + npm-debug.log* 5 + yarn-debug.log* 6 + yarn-error.log* 7 + pnpm-debug.log* 8 + lerna-debug.log* 9 + 10 + node_modules 11 + dist 12 + dist-ssr 13 + *.local 14 + 15 + # Editor directories and files 16 + .vscode/* 17 + !.vscode/extensions.json 18 + .idea 19 + .DS_Store 20 + *.suo 21 + *.ntvs* 22 + *.njsproj 23 + *.sln 24 + *.sw?
+1
.npmrc
··· 1 + @jsr:registry=https://npm.jsr.io
+1
.prettierignore
··· 1 + pnpm-lock.yaml
+19
.prettierrc
··· 1 + { 2 + "trailingComma": "all", 3 + "useTabs": true, 4 + "tabWidth": 2, 5 + "printWidth": 110, 6 + "semi": true, 7 + "singleQuote": true, 8 + "bracketSpacing": true, 9 + "plugins": ["prettier-plugin-tailwindcss"], 10 + "tailwindFunctions": ["tw"], 11 + "overrides": [ 12 + { 13 + "files": ["tsconfig.json", "jsconfig.json"], 14 + "options": { 15 + "parser": "jsonc" 16 + } 17 + } 18 + ] 19 + }
+3
.vscode/extensions.json
··· 1 + { 2 + "recommendations": ["esbenp.prettier-vscode", "bradlc.vscode-tailwindcss"] 3 + }
+10
index.html
··· 1 + <!doctype html> 2 + <html lang="en"> 3 + <head> 4 + <meta charset="utf-8" /> 5 + <meta name="viewport" content="width=device-width, initial-scale=1.0" /> 6 + <title>Aglais</title> 7 + <script type="module" src="./src/main"></script> 8 + </head> 9 + <body></body> 10 + </html>
+47
package.json
··· 1 + { 2 + "packageManager": "pnpm@9.1.0", 3 + "type": "module", 4 + "private": true, 5 + "scripts": { 6 + "dev": "vite", 7 + "build": "vite build", 8 + "preview": "vite preview", 9 + "fmt": "prettier --cache --write ." 10 + }, 11 + "dependencies": { 12 + "@floating-ui/dom": "^1.6.5", 13 + "@floating-ui/utils": "^0.2.2", 14 + "@mary/bluesky-client": "npm:@jsr/mary__bluesky-client@^0.5.23", 15 + "@mary/events": "npm:@jsr/mary__events@^0.1.0", 16 + "@mary/solid-freeze": "npm:@externdefs/solid-freeze@^0.1.1", 17 + "@mary/solid-query": "npm:@externdefs/solid-query@^0.1.4", 18 + "cborg": "4.0.7", 19 + "nanoid": "^5.0.7", 20 + "solid-floating-ui": "~0.2.1", 21 + "solid-js": "^1.8.17", 22 + "solid-textarea-autosize": "^0.0.5" 23 + }, 24 + "devDependencies": { 25 + "@types/dom-close-watcher": "^1.0.0", 26 + "autoprefixer": "^10.4.19", 27 + "prettier": "^3.3.2", 28 + "prettier-plugin-tailwindcss": "^0.6.3", 29 + "tailwindcss": "^3.4.4", 30 + "terser": "^5.31.1", 31 + "typescript": "^5.4.5", 32 + "vite": "^5.2.11", 33 + "vite-plugin-pwa": "0.17.4", 34 + "vite-plugin-solid": "^2.10.2" 35 + }, 36 + "pnpm": { 37 + "patchedDependencies": { 38 + "@tanstack/query-core@5.17.19": "patches/@tanstack__query-core@5.17.19.patch", 39 + "cborg@4.0.7": "patches/cborg@4.0.7.patch", 40 + "solid-textarea-autosize@0.0.5": "patches/solid-textarea-autosize@0.0.5.patch", 41 + "vite-plugin-pwa@0.17.4": "patches/vite-plugin-pwa@0.17.4.patch", 42 + "solid-js@1.8.17": "patches/solid-js@1.8.17.patch", 43 + "workbox-precaching@7.1.0": "patches/workbox-precaching@7.1.0.patch", 44 + "vite@5.2.11": "patches/vite@5.2.11.patch" 45 + } 46 + } 47 + }
+60
patches/@tanstack__query-core@5.17.19.patch
··· 1 + diff --git a/build/modern/utils.cjs b/build/modern/utils.cjs 2 + index 6ef9599fd446b7fa98a292f85160b46be88c0295..c9e3957446a81f4fee1da9ac9da779d5f0e90a5d 100644 3 + --- a/build/modern/utils.cjs 4 + +++ b/build/modern/utils.cjs 5 + @@ -182,21 +182,12 @@ function isPlainArray(value) { 6 + return Array.isArray(value) && value.length === Object.keys(value).length; 7 + } 8 + function isPlainObject(o) { 9 + - if (!hasObjectPrototype(o)) { 10 + + if (typeof o !== "object" || o === null) { 11 + return false; 12 + } 13 + - const ctor = o.constructor; 14 + - if (typeof ctor === "undefined") { 15 + - return true; 16 + - } 17 + - const prot = ctor.prototype; 18 + - if (!hasObjectPrototype(prot)) { 19 + - return false; 20 + - } 21 + - if (!prot.hasOwnProperty("isPrototypeOf")) { 22 + - return false; 23 + - } 24 + - return true; 25 + + 26 + + const proto = Object.getPrototypeOf(o); 27 + + return (proto === Object.prototype || proto === null) && Object.isExtensible(o); 28 + } 29 + function hasObjectPrototype(o) { 30 + return Object.prototype.toString.call(o) === "[object Object]"; 31 + diff --git a/build/modern/utils.js b/build/modern/utils.js 32 + index 0e93a35b45cfc2d53fc17120c741a91ec8a8c533..82b51475e4e12860552f4a0f5f3af23f416740fa 100644 33 + --- a/build/modern/utils.js 34 + +++ b/build/modern/utils.js 35 + @@ -140,21 +140,12 @@ function isPlainArray(value) { 36 + return Array.isArray(value) && value.length === Object.keys(value).length; 37 + } 38 + function isPlainObject(o) { 39 + - if (!hasObjectPrototype(o)) { 40 + + if (typeof o !== "object" || o === null) { 41 + return false; 42 + } 43 + - const ctor = o.constructor; 44 + - if (typeof ctor === "undefined") { 45 + - return true; 46 + - } 47 + - const prot = ctor.prototype; 48 + - if (!hasObjectPrototype(prot)) { 49 + - return false; 50 + - } 51 + - if (!prot.hasOwnProperty("isPrototypeOf")) { 52 + - return false; 53 + - } 54 + - return true; 55 + + 56 + + const proto = Object.getPrototypeOf(o); 57 + + return (proto === null || proto === Object) && Object.isExtensible(o); 58 + } 59 + function hasObjectPrototype(o) { 60 + return Object.prototype.toString.call(o) === "[object Object]";
+98
patches/cborg@4.0.7.patch
··· 1 + diff --git a/cborg.js b/cborg.js 2 + index 90bde204e5f14e14d6a98a95413bb64fb8c807c2..b1bfe5bd9054c362a8c9b7216403b10d6f7d901b 100644 3 + --- a/cborg.js 4 + +++ b/cborg.js 5 + @@ -4,11 +4,11 @@ import { Token, Type } from './lib/token.js' 6 + 7 + /** 8 + * Export the types that were present in the original manual cborg.d.ts 9 + - * @typedef {import('./interface').TagDecoder} TagDecoder 10 + + * @typedef {import('./interface.js').TagDecoder} TagDecoder 11 + * There was originally just `TypeEncoder` so don't break types by renaming or not exporting 12 + - * @typedef {import('./interface').OptionalTypeEncoder} TypeEncoder 13 + - * @typedef {import('./interface').DecodeOptions} DecodeOptions 14 + - * @typedef {import('./interface').EncodeOptions} EncodeOptions 15 + + * @typedef {import('./interface.js').OptionalTypeEncoder} TypeEncoder 16 + + * @typedef {import('./interface.js').DecodeOptions} DecodeOptions 17 + + * @typedef {import('./interface.js').EncodeOptions} EncodeOptions 18 + */ 19 + 20 + export { 21 + diff --git a/interface.ts b/interface.d.ts 22 + similarity index 95% 23 + rename from interface.ts 24 + rename to interface.d.ts 25 + index 020264da98b4ab5d184d5ef471e8c957c32b4da1..b26c48daab83440ab51773c922fe92d2af111acd 100644 26 + --- a/interface.ts 27 + +++ b/interface.d.ts 28 + @@ -1,5 +1,5 @@ 29 + -import { Token } from './lib/token' 30 + -import { Bl } from './lib/bl' 31 + +import { Token } from './lib/token.js' 32 + +import { Bl } from './lib/bl.js' 33 + 34 + export type TokenOrNestedTokens = Token | Token[] | TokenOrNestedTokens[] 35 + 36 + diff --git a/lib/encode.js b/lib/encode.js 37 + index acd7bac1f580d27e8c0052c493e891136d0e8c47..3ae4185291dea9885d5a769f6e4818890ea1f87f 100644 38 + --- a/lib/encode.js 39 + +++ b/lib/encode.js 40 + @@ -15,12 +15,12 @@ import { encodeTag } from './6tag.js' 41 + import { encodeFloat } from './7float.js' 42 + 43 + /** 44 + - * @typedef {import('../interface').EncodeOptions} EncodeOptions 45 + - * @typedef {import('../interface').OptionalTypeEncoder} OptionalTypeEncoder 46 + - * @typedef {import('../interface').Reference} Reference 47 + - * @typedef {import('../interface').StrictTypeEncoder} StrictTypeEncoder 48 + - * @typedef {import('../interface').TokenTypeEncoder} TokenTypeEncoder 49 + - * @typedef {import('../interface').TokenOrNestedTokens} TokenOrNestedTokens 50 + + * @typedef {import('../interface.js').EncodeOptions} EncodeOptions 51 + + * @typedef {import('../interface.js').OptionalTypeEncoder} OptionalTypeEncoder 52 + + * @typedef {import('../interface.js').Reference} Reference 53 + + * @typedef {import('../interface.js').StrictTypeEncoder} StrictTypeEncoder 54 + + * @typedef {import('../interface.js').TokenTypeEncoder} TokenTypeEncoder 55 + + * @typedef {import('../interface.js').TokenOrNestedTokens} TokenOrNestedTokens 56 + */ 57 + 58 + /** @type {EncodeOptions} */ 59 + diff --git a/types/cborg.d.ts b/types/cborg.d.ts 60 + index 1f8711ab5dcc7cf16f3682c538544379094841dd..0b53251a245b295cff0daf02d025df8a9abf411a 100644 61 + --- a/types/cborg.d.ts 62 + +++ b/types/cborg.d.ts 63 + @@ -1,19 +1,19 @@ 64 + /** 65 + * There was originally just `TypeEncoder` so don't break types by renaming or not exporting 66 + */ 67 + -export type TagDecoder = import('./interface').TagDecoder; 68 + +export type TagDecoder = import('./interface.js').TagDecoder; 69 + /** 70 + * Export the types that were present in the original manual cborg.d.ts 71 + */ 72 + -export type TypeEncoder = import('./interface').OptionalTypeEncoder; 73 + +export type TypeEncoder = import('./interface.js').OptionalTypeEncoder; 74 + /** 75 + * Export the types that were present in the original manual cborg.d.ts 76 + */ 77 + -export type DecodeOptions = import('./interface').DecodeOptions; 78 + +export type DecodeOptions = import('./interface.js').DecodeOptions; 79 + /** 80 + * Export the types that were present in the original manual cborg.d.ts 81 + */ 82 + -export type EncodeOptions = import('./interface').EncodeOptions; 83 + +export type EncodeOptions = import('./interface.js').EncodeOptions; 84 + import { decode } from './lib/decode.js'; 85 + import { decodeFirst } from './lib/decode.js'; 86 + import { encode } from './lib/encode.js'; 87 + diff --git a/types/interface.d.ts b/types/interface.d.ts 88 + index 40b734f397df16956bd5e99c396f31027fbb2231..6692903b3fb3390c92b534258e421fc977697136 100644 89 + --- a/types/interface.d.ts 90 + +++ b/types/interface.d.ts 91 + @@ -1,5 +1,5 @@ 92 + -import { Token } from './lib/token'; 93 + -import { Bl } from './lib/bl'; 94 + +import { Token } from './lib/token.js'; 95 + +import { Bl } from './lib/bl.js'; 96 + export type TokenOrNestedTokens = Token | Token[] | TokenOrNestedTokens[]; 97 + export interface Reference { 98 + parent: Reference | undefined;
+20
patches/solid-js@1.8.17.patch
··· 1 + diff --git a/dist/solid.js b/dist/solid.js 2 + index 7350fc223921ca774966b22cb77e8542e28b0e25..bfa0e6c2a0056e256fe31b4c8aebade1cdfcce8b 100644 3 + --- a/dist/solid.js 4 + +++ b/dist/solid.js 5 + @@ -1573,7 +1573,6 @@ function Show(props) { 6 + keyed 7 + ? c 8 + : () => { 9 + - if (!untrack(condition)) throw narrowedError("Show"); 10 + return props.when; 11 + } 12 + ) 13 + @@ -1620,7 +1619,6 @@ function Switch(props) { 14 + keyed 15 + ? when 16 + : () => { 17 + - if (untrack(evalConditions)[0] !== index) throw narrowedError("Match"); 18 + return cond.when; 19 + } 20 + )
+14
patches/solid-textarea-autosize@0.0.5.patch
··· 1 + diff --git a/package.json b/package.json 2 + index 663b11c362a6ffe995ca92663b9840ee57e405ba..da43af39a66d40495987f18448a70a0b96e39e7c 100644 3 + --- a/package.json 4 + +++ b/package.json 5 + @@ -34,7 +34,8 @@ 6 + "require": "./dist/cjs/index.js" 7 + }, 8 + "require": "./dist/cjs/index.js", 9 + - "node": "./dist/cjs/index.js" 10 + + "node": "./dist/cjs/index.js", 11 + + "types": "./dist/types/index.d.ts" 12 + } 13 + }, 14 + "scripts": {
+37
patches/vite-plugin-pwa@0.17.4.patch
··· 1 + diff --git a/dist/client/build/register.js b/dist/client/build/register.js 2 + index 0dc588160b1a58778ebc02757d9757e45cb212e3..aef9ecfc0eee73fd8406bfa3025e38ba2468064c 100644 3 + --- a/dist/client/build/register.js 4 + +++ b/dist/client/build/register.js 5 + @@ -7,6 +7,7 @@ function registerSW(options = {}) { 6 + const { 7 + immediate = false, 8 + onNeedRefresh, 9 + + onBeginUpdate, 10 + onOfflineReady, 11 + onRegistered, 12 + onRegisteredSW, 13 + @@ -71,6 +72,12 @@ function registerSW(options = {}) { 14 + } 15 + } 16 + wb.register({ immediate }).then((r) => { 17 + + if (onBeginUpdate) { 18 + + r?.addEventListener('updatefound', () => { 19 + + onBeginUpdate(); 20 + + }); 21 + + } 22 + + 23 + if (onRegisteredSW) 24 + onRegisteredSW("__SW__", r); 25 + else 26 + diff --git a/types/index.d.ts b/types/index.d.ts 27 + index c2553517a12c98f4f7d1b0ef10a2dd203842d45e..694f29ec1ca485c3d620d3cd47517abdef7e17c1 100644 28 + --- a/types/index.d.ts 29 + +++ b/types/index.d.ts 30 + @@ -1,6 +1,7 @@ 31 + export interface RegisterSWOptions { 32 + immediate?: boolean 33 + onNeedRefresh?: () => void 34 + + onBeginUpdate?: () => void 35 + onOfflineReady?: () => void 36 + /** 37 + * Called only if `onRegisteredSW` is not provided.
+13
patches/vite@5.2.11.patch
··· 1 + diff --git a/dist/node/chunks/dep-cNe07EU9.js b/dist/node/chunks/dep-cNe07EU9.js 2 + index 477583eaa3b1ececd28abc98d5171fd85254f10d..b3109c1c648f2c108c1a13b044a5e4a239ac97b6 100644 3 + --- a/dist/node/chunks/dep-cNe07EU9.js 4 + +++ b/dist/node/chunks/dep-cNe07EU9.js 5 + @@ -67286,7 +67286,7 @@ async function resolveBuildPlugins(config) { 6 + ...(config.isWorker ? [webWorkerPostPlugin()] : []), 7 + ], 8 + post: [ 9 + - buildImportAnalysisPlugin(config), 10 + + ...(config.build.modulePreload !== false ? [buildImportAnalysisPlugin(config)] : []), 11 + ...(config.esbuild !== false ? [buildEsbuildPlugin(config)] : []), 12 + ...(options.minify ? [terserPlugin(config)] : []), 13 + ...(!config.isWorker
+89
patches/workbox-precaching@7.1.0.patch
··· 1 + diff --git a/PrecacheController.js b/PrecacheController.js 2 + index e00975e3762dc6382c39bebee04a89a651aae3d0..5d830222ffbd8e5ed841bbf3fe2560d354ca87ca 100644 3 + --- a/PrecacheController.js 4 + +++ b/PrecacheController.js 5 + @@ -150,9 +150,7 @@ class PrecacheController { 6 + return waitUntil(event, async () => { 7 + const installReportPlugin = new PrecacheInstallReportPlugin(); 8 + this.strategy.plugins.push(installReportPlugin); 9 + - // Cache entries one at a time. 10 + - // See https://github.com/GoogleChrome/workbox/issues/2528 11 + - for (const [url, cacheKey] of this._urlsToCacheKeys) { 12 + + await eachLimit(Array.from(this._urlsToCacheKeys), 6, async ([url, cacheKey]) => { 13 + const integrity = this._cacheKeysToIntegrities.get(cacheKey); 14 + const cacheMode = this._urlsToCacheModes.get(url); 15 + const request = new Request(url, { 16 + @@ -165,7 +163,7 @@ class PrecacheController { 17 + request, 18 + event, 19 + })); 20 + - } 21 + + }); 22 + const { updatedURLs, notUpdatedURLs } = installReportPlugin; 23 + if (process.env.NODE_ENV !== 'production') { 24 + printInstallDetails(updatedURLs, notUpdatedURLs); 25 + @@ -290,3 +288,64 @@ class PrecacheController { 26 + } 27 + } 28 + export { PrecacheController }; 29 + + 30 + +const eachLimit = (values, limit, iterator) => { 31 + + return new Promise((res, rej) => { 32 + + let active = 0; 33 + + let current = 0; 34 + + 35 + + let fulfilled = false; 36 + + 37 + + const resolve = () => { 38 + + if (fulfilled || active > 0) { 39 + + return; 40 + + } 41 + + 42 + + fulfilled = true; 43 + + res(); 44 + + }; 45 + + 46 + + const reject = (err) => { 47 + + if (fulfilled) { 48 + + return; 49 + + } 50 + + 51 + + rej(err); 52 + + }; 53 + + 54 + + const run = () => { 55 + + const c = current++; 56 + + 57 + + if (fulfilled) { 58 + + return; 59 + + } 60 + + if (c >= values.length) { 61 + + return resolve(); 62 + + } 63 + + 64 + + const value = values[c]; 65 + + 66 + + active++; 67 + + 68 + + try { 69 + + const ret = iterator(value, c); 70 + + 71 + + if (ret && 'then' in ret) { 72 + + ret.then(() => { 73 + + active--; 74 + + run(); 75 + + }, rej); 76 + + } else { 77 + + active--; 78 + + run(); 79 + + } 80 + + } catch (err) { 81 + + reject(err); 82 + + } 83 + + }; 84 + + 85 + + for (let i = 0; i < limit; i++) { 86 + + run(); 87 + + } 88 + + }); 89 + +};
+4780
pnpm-lock.yaml
··· 1 + lockfileVersion: '9.0' 2 + 3 + settings: 4 + autoInstallPeers: false 5 + excludeLinksFromLockfile: false 6 + 7 + patchedDependencies: 8 + '@tanstack/query-core@5.17.19': 9 + hash: v3r5daycwz67li5mxwjnajy7bm 10 + path: patches/@tanstack__query-core@5.17.19.patch 11 + cborg@4.0.7: 12 + hash: dz6b6r6dc4jadjfng7vtgi53hy 13 + path: patches/cborg@4.0.7.patch 14 + solid-js@1.8.17: 15 + hash: wunpcbjxb5h4ujg4psj63uuluq 16 + path: patches/solid-js@1.8.17.patch 17 + solid-textarea-autosize@0.0.5: 18 + hash: xoixqosplh7bmfbnvr4hschede 19 + path: patches/solid-textarea-autosize@0.0.5.patch 20 + vite-plugin-pwa@0.17.4: 21 + hash: ve5hypcrajivuvoyst6zln6qyq 22 + path: patches/vite-plugin-pwa@0.17.4.patch 23 + vite@5.2.11: 24 + hash: rtipi3fkkgeet3kqyzne4ksswy 25 + path: patches/vite@5.2.11.patch 26 + workbox-precaching@7.1.0: 27 + hash: uwqzx25dqx6gokakqgp7nxcupi 28 + path: patches/workbox-precaching@7.1.0.patch 29 + 30 + importers: 31 + 32 + .: 33 + dependencies: 34 + '@floating-ui/dom': 35 + specifier: ^1.6.5 36 + version: 1.6.5 37 + '@floating-ui/utils': 38 + specifier: ^0.2.2 39 + version: 0.2.2 40 + '@mary/bluesky-client': 41 + specifier: npm:@jsr/mary__bluesky-client@^0.5.23 42 + version: '@jsr/mary__bluesky-client@0.5.23' 43 + '@mary/events': 44 + specifier: npm:@jsr/mary__events@^0.1.0 45 + version: '@jsr/mary__events@0.1.0' 46 + '@mary/solid-freeze': 47 + specifier: npm:@externdefs/solid-freeze@^0.1.1 48 + version: '@externdefs/solid-freeze@0.1.1(solid-js@1.8.17(patch_hash=wunpcbjxb5h4ujg4psj63uuluq))' 49 + '@mary/solid-query': 50 + specifier: npm:@externdefs/solid-query@^0.1.4 51 + version: '@externdefs/solid-query@0.1.4(solid-js@1.8.17(patch_hash=wunpcbjxb5h4ujg4psj63uuluq))' 52 + cborg: 53 + specifier: 4.0.7 54 + version: 4.0.7(patch_hash=dz6b6r6dc4jadjfng7vtgi53hy) 55 + nanoid: 56 + specifier: ^5.0.7 57 + version: 5.0.7 58 + solid-floating-ui: 59 + specifier: ~0.2.1 60 + version: 0.2.1(@floating-ui/dom@1.6.5)(solid-js@1.8.17(patch_hash=wunpcbjxb5h4ujg4psj63uuluq)) 61 + solid-js: 62 + specifier: ^1.8.17 63 + version: 1.8.17(patch_hash=wunpcbjxb5h4ujg4psj63uuluq) 64 + solid-textarea-autosize: 65 + specifier: ^0.0.5 66 + version: 0.0.5(patch_hash=xoixqosplh7bmfbnvr4hschede)(solid-js@1.8.17(patch_hash=wunpcbjxb5h4ujg4psj63uuluq)) 67 + devDependencies: 68 + '@types/dom-close-watcher': 69 + specifier: ^1.0.0 70 + version: 1.0.0 71 + autoprefixer: 72 + specifier: ^10.4.19 73 + version: 10.4.19(postcss@8.4.38) 74 + prettier: 75 + specifier: ^3.3.2 76 + version: 3.3.2 77 + prettier-plugin-tailwindcss: 78 + specifier: ^0.6.3 79 + version: 0.6.3(prettier@3.3.2) 80 + tailwindcss: 81 + specifier: ^3.4.4 82 + version: 3.4.4 83 + terser: 84 + specifier: ^5.31.1 85 + version: 5.31.1 86 + typescript: 87 + specifier: ^5.4.5 88 + version: 5.4.5 89 + vite: 90 + specifier: ^5.2.11 91 + version: 5.2.11(patch_hash=rtipi3fkkgeet3kqyzne4ksswy)(terser@5.31.1) 92 + vite-plugin-pwa: 93 + specifier: 0.17.4 94 + version: 0.17.4(patch_hash=ve5hypcrajivuvoyst6zln6qyq)(@types/babel__core@7.20.5)(vite@5.2.11(patch_hash=rtipi3fkkgeet3kqyzne4ksswy)(terser@5.31.1)) 95 + vite-plugin-solid: 96 + specifier: ^2.10.2 97 + version: 2.10.2(solid-js@1.8.17(patch_hash=wunpcbjxb5h4ujg4psj63uuluq))(vite@5.2.11(patch_hash=rtipi3fkkgeet3kqyzne4ksswy)(terser@5.31.1)) 98 + 99 + packages: 100 + 101 + '@alloc/quick-lru@5.2.0': 102 + resolution: {integrity: sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==} 103 + engines: {node: '>=10'} 104 + 105 + '@ampproject/remapping@2.3.0': 106 + resolution: {integrity: sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==} 107 + engines: {node: '>=6.0.0'} 108 + 109 + '@apideck/better-ajv-errors@0.3.6': 110 + resolution: {integrity: sha512-P+ZygBLZtkp0qqOAJJVX4oX/sFo5JR3eBWwwuqHHhK0GIgQOKWrAfiAaWX0aArHkRWHMuggFEgAZNxVPwPZYaA==} 111 + engines: {node: '>=10'} 112 + peerDependencies: 113 + ajv: '>=8' 114 + 115 + '@babel/code-frame@7.24.6': 116 + resolution: {integrity: sha512-ZJhac6FkEd1yhG2AHOmfcXG4ceoLltoCVJjN5XsWN9BifBQr+cHJbWi0h68HZuSORq+3WtJ2z0hwF2NG1b5kcA==} 117 + engines: {node: '>=6.9.0'} 118 + 119 + '@babel/compat-data@7.24.6': 120 + resolution: {integrity: sha512-aC2DGhBq5eEdyXWqrDInSqQjO0k8xtPRf5YylULqx8MCd6jBtzqfta/3ETMRpuKIc5hyswfO80ObyA1MvkCcUQ==} 121 + engines: {node: '>=6.9.0'} 122 + 123 + '@babel/core@7.24.6': 124 + resolution: {integrity: sha512-qAHSfAdVyFmIvl0VHELib8xar7ONuSHrE2hLnsaWkYNTI68dmi1x8GYDhJjMI/e7XWal9QBlZkwbOnkcw7Z8gQ==} 125 + engines: {node: '>=6.9.0'} 126 + 127 + '@babel/generator@7.24.6': 128 + resolution: {integrity: sha512-S7m4eNa6YAPJRHmKsLHIDJhNAGNKoWNiWefz1MBbpnt8g9lvMDl1hir4P9bo/57bQEmuwEhnRU/AMWsD0G/Fbg==} 129 + engines: {node: '>=6.9.0'} 130 + 131 + '@babel/helper-annotate-as-pure@7.24.6': 132 + resolution: {integrity: sha512-DitEzDfOMnd13kZnDqns1ccmftwJTS9DMkyn9pYTxulS7bZxUxpMly3Nf23QQ6NwA4UB8lAqjbqWtyvElEMAkg==} 133 + engines: {node: '>=6.9.0'} 134 + 135 + '@babel/helper-builder-binary-assignment-operator-visitor@7.24.6': 136 + resolution: {integrity: sha512-+wnfqc5uHiMYtvRX7qu80Toef8BXeh4HHR1SPeonGb1SKPniNEd4a/nlaJJMv/OIEYvIVavvo0yR7u10Gqz0Iw==} 137 + engines: {node: '>=6.9.0'} 138 + 139 + '@babel/helper-compilation-targets@7.24.6': 140 + resolution: {integrity: sha512-VZQ57UsDGlX/5fFA7GkVPplZhHsVc+vuErWgdOiysI9Ksnw0Pbbd6pnPiR/mmJyKHgyIW0c7KT32gmhiF+cirg==} 141 + engines: {node: '>=6.9.0'} 142 + 143 + '@babel/helper-create-class-features-plugin@7.24.6': 144 + resolution: {integrity: sha512-djsosdPJVZE6Vsw3kk7IPRWethP94WHGOhQTc67SNXE0ZzMhHgALw8iGmYS0TD1bbMM0VDROy43od7/hN6WYcA==} 145 + engines: {node: '>=6.9.0'} 146 + peerDependencies: 147 + '@babel/core': ^7.0.0 148 + 149 + '@babel/helper-create-regexp-features-plugin@7.24.6': 150 + resolution: {integrity: sha512-C875lFBIWWwyv6MHZUG9HmRrlTDgOsLWZfYR0nW69gaKJNe0/Mpxx5r0EID2ZdHQkdUmQo2t0uNckTL08/1BgA==} 151 + engines: {node: '>=6.9.0'} 152 + peerDependencies: 153 + '@babel/core': ^7.0.0 154 + 155 + '@babel/helper-define-polyfill-provider@0.6.2': 156 + resolution: {integrity: sha512-LV76g+C502biUK6AyZ3LK10vDpDyCzZnhZFXkH1L75zHPj68+qc8Zfpx2th+gzwA2MzyK+1g/3EPl62yFnVttQ==} 157 + peerDependencies: 158 + '@babel/core': ^7.4.0 || ^8.0.0-0 <8.0.0 159 + 160 + '@babel/helper-environment-visitor@7.24.6': 161 + resolution: {integrity: sha512-Y50Cg3k0LKLMjxdPjIl40SdJgMB85iXn27Vk/qbHZCFx/o5XO3PSnpi675h1KEmmDb6OFArfd5SCQEQ5Q4H88g==} 162 + engines: {node: '>=6.9.0'} 163 + 164 + '@babel/helper-function-name@7.24.6': 165 + resolution: {integrity: sha512-xpeLqeeRkbxhnYimfr2PC+iA0Q7ljX/d1eZ9/inYbmfG2jpl8Lu3DyXvpOAnrS5kxkfOWJjioIMQsaMBXFI05w==} 166 + engines: {node: '>=6.9.0'} 167 + 168 + '@babel/helper-hoist-variables@7.24.6': 169 + resolution: {integrity: sha512-SF/EMrC3OD7dSta1bLJIlrsVxwtd0UpjRJqLno6125epQMJ/kyFmpTT4pbvPbdQHzCHg+biQ7Syo8lnDtbR+uA==} 170 + engines: {node: '>=6.9.0'} 171 + 172 + '@babel/helper-member-expression-to-functions@7.24.6': 173 + resolution: {integrity: sha512-OTsCufZTxDUsv2/eDXanw/mUZHWOxSbEmC3pP8cgjcy5rgeVPWWMStnv274DV60JtHxTk0adT0QrCzC4M9NWGg==} 174 + engines: {node: '>=6.9.0'} 175 + 176 + '@babel/helper-module-imports@7.18.6': 177 + resolution: {integrity: sha512-0NFvs3VkuSYbFi1x2Vd6tKrywq+z/cLeYC/RJNFrIX/30Bf5aiGYbtvGXolEktzJH8o5E5KJ3tT+nkxuuZFVlA==} 178 + engines: {node: '>=6.9.0'} 179 + 180 + '@babel/helper-module-imports@7.24.6': 181 + resolution: {integrity: sha512-a26dmxFJBF62rRO9mmpgrfTLsAuyHk4e1hKTUkD/fcMfynt8gvEKwQPQDVxWhca8dHoDck+55DFt42zV0QMw5g==} 182 + engines: {node: '>=6.9.0'} 183 + 184 + '@babel/helper-module-transforms@7.24.6': 185 + resolution: {integrity: sha512-Y/YMPm83mV2HJTbX1Qh2sjgjqcacvOlhbzdCCsSlblOKjSYmQqEbO6rUniWQyRo9ncyfjT8hnUjlG06RXDEmcA==} 186 + engines: {node: '>=6.9.0'} 187 + peerDependencies: 188 + '@babel/core': ^7.0.0 189 + 190 + '@babel/helper-optimise-call-expression@7.24.6': 191 + resolution: {integrity: sha512-3SFDJRbx7KuPRl8XDUr8O7GAEB8iGyWPjLKJh/ywP/Iy9WOmEfMrsWbaZpvBu2HSYn4KQygIsz0O7m8y10ncMA==} 192 + engines: {node: '>=6.9.0'} 193 + 194 + '@babel/helper-plugin-utils@7.24.6': 195 + resolution: {integrity: sha512-MZG/JcWfxybKwsA9N9PmtF2lOSFSEMVCpIRrbxccZFLJPrJciJdG/UhSh5W96GEteJI2ARqm5UAHxISwRDLSNg==} 196 + engines: {node: '>=6.9.0'} 197 + 198 + '@babel/helper-remap-async-to-generator@7.24.6': 199 + resolution: {integrity: sha512-1Qursq9ArRZPAMOZf/nuzVW8HgJLkTB9y9LfP4lW2MVp4e9WkLJDovfKBxoDcCk6VuzIxyqWHyBoaCtSRP10yg==} 200 + engines: {node: '>=6.9.0'} 201 + peerDependencies: 202 + '@babel/core': ^7.0.0 203 + 204 + '@babel/helper-replace-supers@7.24.6': 205 + resolution: {integrity: sha512-mRhfPwDqDpba8o1F8ESxsEkJMQkUF8ZIWrAc0FtWhxnjfextxMWxr22RtFizxxSYLjVHDeMgVsRq8BBZR2ikJQ==} 206 + engines: {node: '>=6.9.0'} 207 + peerDependencies: 208 + '@babel/core': ^7.0.0 209 + 210 + '@babel/helper-simple-access@7.24.6': 211 + resolution: {integrity: sha512-nZzcMMD4ZhmB35MOOzQuiGO5RzL6tJbsT37Zx8M5L/i9KSrukGXWTjLe1knIbb/RmxoJE9GON9soq0c0VEMM5g==} 212 + engines: {node: '>=6.9.0'} 213 + 214 + '@babel/helper-skip-transparent-expression-wrappers@7.24.6': 215 + resolution: {integrity: sha512-jhbbkK3IUKc4T43WadP96a27oYti9gEf1LdyGSP2rHGH77kwLwfhO7TgwnWvxxQVmke0ImmCSS47vcuxEMGD3Q==} 216 + engines: {node: '>=6.9.0'} 217 + 218 + '@babel/helper-split-export-declaration@7.24.6': 219 + resolution: {integrity: sha512-CvLSkwXGWnYlF9+J3iZUvwgAxKiYzK3BWuo+mLzD/MDGOZDj7Gq8+hqaOkMxmJwmlv0iu86uH5fdADd9Hxkymw==} 220 + engines: {node: '>=6.9.0'} 221 + 222 + '@babel/helper-string-parser@7.24.6': 223 + resolution: {integrity: sha512-WdJjwMEkmBicq5T9fm/cHND3+UlFa2Yj8ALLgmoSQAJZysYbBjw+azChSGPN4DSPLXOcooGRvDwZWMcF/mLO2Q==} 224 + engines: {node: '>=6.9.0'} 225 + 226 + '@babel/helper-validator-identifier@7.24.6': 227 + resolution: {integrity: sha512-4yA7s865JHaqUdRbnaxarZREuPTHrjpDT+pXoAZ1yhyo6uFnIEpS8VMu16siFOHDpZNKYv5BObhsB//ycbICyw==} 228 + engines: {node: '>=6.9.0'} 229 + 230 + '@babel/helper-validator-option@7.24.6': 231 + resolution: {integrity: sha512-Jktc8KkF3zIkePb48QO+IapbXlSapOW9S+ogZZkcO6bABgYAxtZcjZ/O005111YLf+j4M84uEgwYoidDkXbCkQ==} 232 + engines: {node: '>=6.9.0'} 233 + 234 + '@babel/helper-wrap-function@7.24.6': 235 + resolution: {integrity: sha512-f1JLrlw/jbiNfxvdrfBgio/gRBk3yTAEJWirpAkiJG2Hb22E7cEYKHWo0dFPTv/niPovzIdPdEDetrv6tC6gPQ==} 236 + engines: {node: '>=6.9.0'} 237 + 238 + '@babel/helpers@7.24.6': 239 + resolution: {integrity: sha512-V2PI+NqnyFu1i0GyTd/O/cTpxzQCYioSkUIRmgo7gFEHKKCg5w46+r/A6WeUR1+P3TeQ49dspGPNd/E3n9AnnA==} 240 + engines: {node: '>=6.9.0'} 241 + 242 + '@babel/highlight@7.24.6': 243 + resolution: {integrity: sha512-2YnuOp4HAk2BsBrJJvYCbItHx0zWscI1C3zgWkz+wDyD9I7GIVrfnLyrR4Y1VR+7p+chAEcrgRQYZAGIKMV7vQ==} 244 + engines: {node: '>=6.9.0'} 245 + 246 + '@babel/parser@7.24.6': 247 + resolution: {integrity: sha512-eNZXdfU35nJC2h24RznROuOpO94h6x8sg9ju0tT9biNtLZ2vuP8SduLqqV+/8+cebSLV9SJEAN5Z3zQbJG/M+Q==} 248 + engines: {node: '>=6.0.0'} 249 + hasBin: true 250 + 251 + '@babel/plugin-bugfix-firefox-class-in-computed-class-key@7.24.6': 252 + resolution: {integrity: sha512-bYndrJ6Ph6Ar+GaB5VAc0JPoP80bQCm4qon6JEzXfRl5QZyQ8Ur1K6k7htxWmPA5z+k7JQvaMUrtXlqclWYzKw==} 253 + engines: {node: '>=6.9.0'} 254 + peerDependencies: 255 + '@babel/core': ^7.0.0 256 + 257 + '@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression@7.24.6': 258 + resolution: {integrity: sha512-iVuhb6poq5ikqRq2XWU6OQ+R5o9wF+r/or9CeUyovgptz0UlnK4/seOQ1Istu/XybYjAhQv1FRSSfHHufIku5Q==} 259 + engines: {node: '>=6.9.0'} 260 + peerDependencies: 261 + '@babel/core': ^7.0.0 262 + 263 + '@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining@7.24.6': 264 + resolution: {integrity: sha512-c8TER5xMDYzzFcGqOEp9l4hvB7dcbhcGjcLVwxWfe4P5DOafdwjsBJZKsmv+o3aXh7NhopvayQIovHrh2zSRUQ==} 265 + engines: {node: '>=6.9.0'} 266 + peerDependencies: 267 + '@babel/core': ^7.13.0 268 + 269 + '@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly@7.24.6': 270 + resolution: {integrity: sha512-z8zEjYmwBUHN/pCF3NuWBhHQjJCrd33qAi8MgANfMrAvn72k2cImT8VjK9LJFu4ysOLJqhfkYYb3MvwANRUNZQ==} 271 + engines: {node: '>=6.9.0'} 272 + peerDependencies: 273 + '@babel/core': ^7.0.0 274 + 275 + '@babel/plugin-proposal-private-property-in-object@7.21.0-placeholder-for-preset-env.2': 276 + resolution: {integrity: sha512-SOSkfJDddaM7mak6cPEpswyTRnuRltl429hMraQEglW+OkovnCzsiszTmsrlY//qLFjCpQDFRvjdm2wA5pPm9w==} 277 + engines: {node: '>=6.9.0'} 278 + peerDependencies: 279 + '@babel/core': ^7.0.0-0 280 + 281 + '@babel/plugin-syntax-async-generators@7.8.4': 282 + resolution: {integrity: sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==} 283 + peerDependencies: 284 + '@babel/core': ^7.0.0-0 285 + 286 + '@babel/plugin-syntax-class-properties@7.12.13': 287 + resolution: {integrity: sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA==} 288 + peerDependencies: 289 + '@babel/core': ^7.0.0-0 290 + 291 + '@babel/plugin-syntax-class-static-block@7.14.5': 292 + resolution: {integrity: sha512-b+YyPmr6ldyNnM6sqYeMWE+bgJcJpO6yS4QD7ymxgH34GBPNDM/THBh8iunyvKIZztiwLH4CJZ0RxTk9emgpjw==} 293 + engines: {node: '>=6.9.0'} 294 + peerDependencies: 295 + '@babel/core': ^7.0.0-0 296 + 297 + '@babel/plugin-syntax-dynamic-import@7.8.3': 298 + resolution: {integrity: sha512-5gdGbFon+PszYzqs83S3E5mpi7/y/8M9eC90MRTZfduQOYW76ig6SOSPNe41IG5LoP3FGBn2N0RjVDSQiS94kQ==} 299 + peerDependencies: 300 + '@babel/core': ^7.0.0-0 301 + 302 + '@babel/plugin-syntax-export-namespace-from@7.8.3': 303 + resolution: {integrity: sha512-MXf5laXo6c1IbEbegDmzGPwGNTsHZmEy6QGznu5Sh2UCWvueywb2ee+CCE4zQiZstxU9BMoQO9i6zUFSY0Kj0Q==} 304 + peerDependencies: 305 + '@babel/core': ^7.0.0-0 306 + 307 + '@babel/plugin-syntax-import-assertions@7.24.6': 308 + resolution: {integrity: sha512-BE6o2BogJKJImTmGpkmOic4V0hlRRxVtzqxiSPa8TIFxyhi4EFjHm08nq1M4STK4RytuLMgnSz0/wfflvGFNOg==} 309 + engines: {node: '>=6.9.0'} 310 + peerDependencies: 311 + '@babel/core': ^7.0.0-0 312 + 313 + '@babel/plugin-syntax-import-attributes@7.24.6': 314 + resolution: {integrity: sha512-D+CfsVZousPXIdudSII7RGy52+dYRtbyKAZcvtQKq/NpsivyMVduepzcLqG5pMBugtMdedxdC8Ramdpcne9ZWQ==} 315 + engines: {node: '>=6.9.0'} 316 + peerDependencies: 317 + '@babel/core': ^7.0.0-0 318 + 319 + '@babel/plugin-syntax-import-meta@7.10.4': 320 + resolution: {integrity: sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g==} 321 + peerDependencies: 322 + '@babel/core': ^7.0.0-0 323 + 324 + '@babel/plugin-syntax-json-strings@7.8.3': 325 + resolution: {integrity: sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==} 326 + peerDependencies: 327 + '@babel/core': ^7.0.0-0 328 + 329 + '@babel/plugin-syntax-jsx@7.24.6': 330 + resolution: {integrity: sha512-lWfvAIFNWMlCsU0DRUun2GpFwZdGTukLaHJqRh1JRb80NdAP5Sb1HDHB5X9P9OtgZHQl089UzQkpYlBq2VTPRw==} 331 + engines: {node: '>=6.9.0'} 332 + peerDependencies: 333 + '@babel/core': ^7.0.0-0 334 + 335 + '@babel/plugin-syntax-logical-assignment-operators@7.10.4': 336 + resolution: {integrity: sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig==} 337 + peerDependencies: 338 + '@babel/core': ^7.0.0-0 339 + 340 + '@babel/plugin-syntax-nullish-coalescing-operator@7.8.3': 341 + resolution: {integrity: sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ==} 342 + peerDependencies: 343 + '@babel/core': ^7.0.0-0 344 + 345 + '@babel/plugin-syntax-numeric-separator@7.10.4': 346 + resolution: {integrity: sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug==} 347 + peerDependencies: 348 + '@babel/core': ^7.0.0-0 349 + 350 + '@babel/plugin-syntax-object-rest-spread@7.8.3': 351 + resolution: {integrity: sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==} 352 + peerDependencies: 353 + '@babel/core': ^7.0.0-0 354 + 355 + '@babel/plugin-syntax-optional-catch-binding@7.8.3': 356 + resolution: {integrity: sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q==} 357 + peerDependencies: 358 + '@babel/core': ^7.0.0-0 359 + 360 + '@babel/plugin-syntax-optional-chaining@7.8.3': 361 + resolution: {integrity: sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==} 362 + peerDependencies: 363 + '@babel/core': ^7.0.0-0 364 + 365 + '@babel/plugin-syntax-private-property-in-object@7.14.5': 366 + resolution: {integrity: sha512-0wVnp9dxJ72ZUJDV27ZfbSj6iHLoytYZmh3rFcxNnvsJF3ktkzLDZPy/mA17HGsaQT3/DQsWYX1f1QGWkCoVUg==} 367 + engines: {node: '>=6.9.0'} 368 + peerDependencies: 369 + '@babel/core': ^7.0.0-0 370 + 371 + '@babel/plugin-syntax-top-level-await@7.14.5': 372 + resolution: {integrity: sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw==} 373 + engines: {node: '>=6.9.0'} 374 + peerDependencies: 375 + '@babel/core': ^7.0.0-0 376 + 377 + '@babel/plugin-syntax-unicode-sets-regex@7.18.6': 378 + resolution: {integrity: sha512-727YkEAPwSIQTv5im8QHz3upqp92JTWhidIC81Tdx4VJYIte/VndKf1qKrfnnhPLiPghStWfvC/iFaMCQu7Nqg==} 379 + engines: {node: '>=6.9.0'} 380 + peerDependencies: 381 + '@babel/core': ^7.0.0 382 + 383 + '@babel/plugin-transform-arrow-functions@7.24.6': 384 + resolution: {integrity: sha512-jSSSDt4ZidNMggcLx8SaKsbGNEfIl0PHx/4mFEulorE7bpYLbN0d3pDW3eJ7Y5Z3yPhy3L3NaPCYyTUY7TuugQ==} 385 + engines: {node: '>=6.9.0'} 386 + peerDependencies: 387 + '@babel/core': ^7.0.0-0 388 + 389 + '@babel/plugin-transform-async-generator-functions@7.24.6': 390 + resolution: {integrity: sha512-VEP2o4iR2DqQU6KPgizTW2mnMx6BG5b5O9iQdrW9HesLkv8GIA8x2daXBQxw1MrsIkFQGA/iJ204CKoQ8UcnAA==} 391 + engines: {node: '>=6.9.0'} 392 + peerDependencies: 393 + '@babel/core': ^7.0.0-0 394 + 395 + '@babel/plugin-transform-async-to-generator@7.24.6': 396 + resolution: {integrity: sha512-NTBA2SioI3OsHeIn6sQmhvXleSl9T70YY/hostQLveWs0ic+qvbA3fa0kwAwQ0OA/XGaAerNZRQGJyRfhbJK4g==} 397 + engines: {node: '>=6.9.0'} 398 + peerDependencies: 399 + '@babel/core': ^7.0.0-0 400 + 401 + '@babel/plugin-transform-block-scoped-functions@7.24.6': 402 + resolution: {integrity: sha512-XNW7jolYHW9CwORrZgA/97tL/k05qe/HL0z/qqJq1mdWhwwCM6D4BJBV7wAz9HgFziN5dTOG31znkVIzwxv+vw==} 403 + engines: {node: '>=6.9.0'} 404 + peerDependencies: 405 + '@babel/core': ^7.0.0-0 406 + 407 + '@babel/plugin-transform-block-scoping@7.24.6': 408 + resolution: {integrity: sha512-S/t1Xh4ehW7sGA7c1j/hiOBLnEYCp/c2sEG4ZkL8kI1xX9tW2pqJTCHKtdhe/jHKt8nG0pFCrDHUXd4DvjHS9w==} 409 + engines: {node: '>=6.9.0'} 410 + peerDependencies: 411 + '@babel/core': ^7.0.0-0 412 + 413 + '@babel/plugin-transform-class-properties@7.24.6': 414 + resolution: {integrity: sha512-j6dZ0Z2Z2slWLR3kt9aOmSIrBvnntWjMDN/TVcMPxhXMLmJVqX605CBRlcGI4b32GMbfifTEsdEjGjiE+j/c3A==} 415 + engines: {node: '>=6.9.0'} 416 + peerDependencies: 417 + '@babel/core': ^7.0.0-0 418 + 419 + '@babel/plugin-transform-class-static-block@7.24.6': 420 + resolution: {integrity: sha512-1QSRfoPI9RoLRa8Mnakc6v3e0gJxiZQTYrMfLn+mD0sz5+ndSzwymp2hDcYJTyT0MOn0yuWzj8phlIvO72gTHA==} 421 + engines: {node: '>=6.9.0'} 422 + peerDependencies: 423 + '@babel/core': ^7.12.0 424 + 425 + '@babel/plugin-transform-classes@7.24.6': 426 + resolution: {integrity: sha512-+fN+NO2gh8JtRmDSOB6gaCVo36ha8kfCW1nMq2Gc0DABln0VcHN4PrALDvF5/diLzIRKptC7z/d7Lp64zk92Fg==} 427 + engines: {node: '>=6.9.0'} 428 + peerDependencies: 429 + '@babel/core': ^7.0.0-0 430 + 431 + '@babel/plugin-transform-computed-properties@7.24.6': 432 + resolution: {integrity: sha512-cRzPobcfRP0ZtuIEkA8QzghoUpSB3X3qSH5W2+FzG+VjWbJXExtx0nbRqwumdBN1x/ot2SlTNQLfBCnPdzp6kg==} 433 + engines: {node: '>=6.9.0'} 434 + peerDependencies: 435 + '@babel/core': ^7.0.0-0 436 + 437 + '@babel/plugin-transform-destructuring@7.24.6': 438 + resolution: {integrity: sha512-YLW6AE5LQpk5npNXL7i/O+U9CE4XsBCuRPgyjl1EICZYKmcitV+ayuuUGMJm2lC1WWjXYszeTnIxF/dq/GhIZQ==} 439 + engines: {node: '>=6.9.0'} 440 + peerDependencies: 441 + '@babel/core': ^7.0.0-0 442 + 443 + '@babel/plugin-transform-dotall-regex@7.24.6': 444 + resolution: {integrity: sha512-rCXPnSEKvkm/EjzOtLoGvKseK+dS4kZwx1HexO3BtRtgL0fQ34awHn34aeSHuXtZY2F8a1X8xqBBPRtOxDVmcA==} 445 + engines: {node: '>=6.9.0'} 446 + peerDependencies: 447 + '@babel/core': ^7.0.0-0 448 + 449 + '@babel/plugin-transform-duplicate-keys@7.24.6': 450 + resolution: {integrity: sha512-/8Odwp/aVkZwPFJMllSbawhDAO3UJi65foB00HYnK/uXvvCPm0TAXSByjz1mpRmp0q6oX2SIxpkUOpPFHk7FLA==} 451 + engines: {node: '>=6.9.0'} 452 + peerDependencies: 453 + '@babel/core': ^7.0.0-0 454 + 455 + '@babel/plugin-transform-dynamic-import@7.24.6': 456 + resolution: {integrity: sha512-vpq8SSLRTBLOHUZHSnBqVo0AKX3PBaoPs2vVzYVWslXDTDIpwAcCDtfhUcHSQQoYoUvcFPTdC8TZYXu9ZnLT/w==} 457 + engines: {node: '>=6.9.0'} 458 + peerDependencies: 459 + '@babel/core': ^7.0.0-0 460 + 461 + '@babel/plugin-transform-exponentiation-operator@7.24.6': 462 + resolution: {integrity: sha512-EemYpHtmz0lHE7hxxxYEuTYOOBZ43WkDgZ4arQ4r+VX9QHuNZC+WH3wUWmRNvR8ECpTRne29aZV6XO22qpOtdA==} 463 + engines: {node: '>=6.9.0'} 464 + peerDependencies: 465 + '@babel/core': ^7.0.0-0 466 + 467 + '@babel/plugin-transform-export-namespace-from@7.24.6': 468 + resolution: {integrity: sha512-inXaTM1SVrIxCkIJ5gqWiozHfFMStuGbGJAxZFBoHcRRdDP0ySLb3jH6JOwmfiinPwyMZqMBX+7NBDCO4z0NSA==} 469 + engines: {node: '>=6.9.0'} 470 + peerDependencies: 471 + '@babel/core': ^7.0.0-0 472 + 473 + '@babel/plugin-transform-for-of@7.24.6': 474 + resolution: {integrity: sha512-n3Sf72TnqK4nw/jziSqEl1qaWPbCRw2CziHH+jdRYvw4J6yeCzsj4jdw8hIntOEeDGTmHVe2w4MVL44PN0GMzg==} 475 + engines: {node: '>=6.9.0'} 476 + peerDependencies: 477 + '@babel/core': ^7.0.0-0 478 + 479 + '@babel/plugin-transform-function-name@7.24.6': 480 + resolution: {integrity: sha512-sOajCu6V0P1KPljWHKiDq6ymgqB+vfo3isUS4McqW1DZtvSVU2v/wuMhmRmkg3sFoq6GMaUUf8W4WtoSLkOV/Q==} 481 + engines: {node: '>=6.9.0'} 482 + peerDependencies: 483 + '@babel/core': ^7.0.0-0 484 + 485 + '@babel/plugin-transform-json-strings@7.24.6': 486 + resolution: {integrity: sha512-Uvgd9p2gUnzYJxVdBLcU0KurF8aVhkmVyMKW4MIY1/BByvs3EBpv45q01o7pRTVmTvtQq5zDlytP3dcUgm7v9w==} 487 + engines: {node: '>=6.9.0'} 488 + peerDependencies: 489 + '@babel/core': ^7.0.0-0 490 + 491 + '@babel/plugin-transform-literals@7.24.6': 492 + resolution: {integrity: sha512-f2wHfR2HF6yMj+y+/y07+SLqnOSwRp8KYLpQKOzS58XLVlULhXbiYcygfXQxJlMbhII9+yXDwOUFLf60/TL5tw==} 493 + engines: {node: '>=6.9.0'} 494 + peerDependencies: 495 + '@babel/core': ^7.0.0-0 496 + 497 + '@babel/plugin-transform-logical-assignment-operators@7.24.6': 498 + resolution: {integrity: sha512-EKaWvnezBCMkRIHxMJSIIylzhqK09YpiJtDbr2wsXTwnO0TxyjMUkaw4RlFIZMIS0iDj0KyIg7H7XCguHu/YDA==} 499 + engines: {node: '>=6.9.0'} 500 + peerDependencies: 501 + '@babel/core': ^7.0.0-0 502 + 503 + '@babel/plugin-transform-member-expression-literals@7.24.6': 504 + resolution: {integrity: sha512-9g8iV146szUo5GWgXpRbq/GALTnY+WnNuRTuRHWWFfWGbP9ukRL0aO/jpu9dmOPikclkxnNsjY8/gsWl6bmZJQ==} 505 + engines: {node: '>=6.9.0'} 506 + peerDependencies: 507 + '@babel/core': ^7.0.0-0 508 + 509 + '@babel/plugin-transform-modules-amd@7.24.6': 510 + resolution: {integrity: sha512-eAGogjZgcwqAxhyFgqghvoHRr+EYRQPFjUXrTYKBRb5qPnAVxOOglaxc4/byHqjvq/bqO2F3/CGwTHsgKJYHhQ==} 511 + engines: {node: '>=6.9.0'} 512 + peerDependencies: 513 + '@babel/core': ^7.0.0-0 514 + 515 + '@babel/plugin-transform-modules-commonjs@7.24.6': 516 + resolution: {integrity: sha512-JEV8l3MHdmmdb7S7Cmx6rbNEjRCgTQMZxllveHO0mx6uiclB0NflCawlQQ6+o5ZrwjUBYPzHm2XoK4wqGVUFuw==} 517 + engines: {node: '>=6.9.0'} 518 + peerDependencies: 519 + '@babel/core': ^7.0.0-0 520 + 521 + '@babel/plugin-transform-modules-systemjs@7.24.6': 522 + resolution: {integrity: sha512-xg1Z0J5JVYxtpX954XqaaAT6NpAY6LtZXvYFCJmGFJWwtlz2EmJoR8LycFRGNE8dBKizGWkGQZGegtkV8y8s+w==} 523 + engines: {node: '>=6.9.0'} 524 + peerDependencies: 525 + '@babel/core': ^7.0.0-0 526 + 527 + '@babel/plugin-transform-modules-umd@7.24.6': 528 + resolution: {integrity: sha512-esRCC/KsSEUvrSjv5rFYnjZI6qv4R1e/iHQrqwbZIoRJqk7xCvEUiN7L1XrmW5QSmQe3n1XD88wbgDTWLbVSyg==} 529 + engines: {node: '>=6.9.0'} 530 + peerDependencies: 531 + '@babel/core': ^7.0.0-0 532 + 533 + '@babel/plugin-transform-named-capturing-groups-regex@7.24.6': 534 + resolution: {integrity: sha512-6DneiCiu91wm3YiNIGDWZsl6GfTTbspuj/toTEqLh9d4cx50UIzSdg+T96p8DuT7aJOBRhFyaE9ZvTHkXrXr6Q==} 535 + engines: {node: '>=6.9.0'} 536 + peerDependencies: 537 + '@babel/core': ^7.0.0 538 + 539 + '@babel/plugin-transform-new-target@7.24.6': 540 + resolution: {integrity: sha512-f8liz9JG2Va8A4J5ZBuaSdwfPqN6axfWRK+y66fjKYbwf9VBLuq4WxtinhJhvp1w6lamKUwLG0slK2RxqFgvHA==} 541 + engines: {node: '>=6.9.0'} 542 + peerDependencies: 543 + '@babel/core': ^7.0.0-0 544 + 545 + '@babel/plugin-transform-nullish-coalescing-operator@7.24.6': 546 + resolution: {integrity: sha512-+QlAiZBMsBK5NqrBWFXCYeXyiU1y7BQ/OYaiPAcQJMomn5Tyg+r5WuVtyEuvTbpV7L25ZSLfE+2E9ywj4FD48A==} 547 + engines: {node: '>=6.9.0'} 548 + peerDependencies: 549 + '@babel/core': ^7.0.0-0 550 + 551 + '@babel/plugin-transform-numeric-separator@7.24.6': 552 + resolution: {integrity: sha512-6voawq8T25Jvvnc4/rXcWZQKKxUNZcKMS8ZNrjxQqoRFernJJKjE3s18Qo6VFaatG5aiX5JV1oPD7DbJhn0a4Q==} 553 + engines: {node: '>=6.9.0'} 554 + peerDependencies: 555 + '@babel/core': ^7.0.0-0 556 + 557 + '@babel/plugin-transform-object-rest-spread@7.24.6': 558 + resolution: {integrity: sha512-OKmi5wiMoRW5Smttne7BwHM8s/fb5JFs+bVGNSeHWzwZkWXWValR1M30jyXo1s/RaqgwwhEC62u4rFH/FBcBPg==} 559 + engines: {node: '>=6.9.0'} 560 + peerDependencies: 561 + '@babel/core': ^7.0.0-0 562 + 563 + '@babel/plugin-transform-object-super@7.24.6': 564 + resolution: {integrity: sha512-N/C76ihFKlZgKfdkEYKtaRUtXZAgK7sOY4h2qrbVbVTXPrKGIi8aww5WGe/+Wmg8onn8sr2ut6FXlsbu/j6JHg==} 565 + engines: {node: '>=6.9.0'} 566 + peerDependencies: 567 + '@babel/core': ^7.0.0-0 568 + 569 + '@babel/plugin-transform-optional-catch-binding@7.24.6': 570 + resolution: {integrity: sha512-L5pZ+b3O1mSzJ71HmxSCmTVd03VOT2GXOigug6vDYJzE5awLI7P1g0wFcdmGuwSDSrQ0L2rDOe/hHws8J1rv3w==} 571 + engines: {node: '>=6.9.0'} 572 + peerDependencies: 573 + '@babel/core': ^7.0.0-0 574 + 575 + '@babel/plugin-transform-optional-chaining@7.24.6': 576 + resolution: {integrity: sha512-cHbqF6l1QP11OkYTYQ+hhVx1E017O5ZcSPXk9oODpqhcAD1htsWG2NpHrrhthEO2qZomLK0FXS+u7NfrkF5aOQ==} 577 + engines: {node: '>=6.9.0'} 578 + peerDependencies: 579 + '@babel/core': ^7.0.0-0 580 + 581 + '@babel/plugin-transform-parameters@7.24.6': 582 + resolution: {integrity: sha512-ST7guE8vLV+vI70wmAxuZpIKzVjvFX9Qs8bl5w6tN/6gOypPWUmMQL2p7LJz5E63vEGrDhAiYetniJFyBH1RkA==} 583 + engines: {node: '>=6.9.0'} 584 + peerDependencies: 585 + '@babel/core': ^7.0.0-0 586 + 587 + '@babel/plugin-transform-private-methods@7.24.6': 588 + resolution: {integrity: sha512-T9LtDI0BgwXOzyXrvgLTT8DFjCC/XgWLjflczTLXyvxbnSR/gpv0hbmzlHE/kmh9nOvlygbamLKRo6Op4yB6aw==} 589 + engines: {node: '>=6.9.0'} 590 + peerDependencies: 591 + '@babel/core': ^7.0.0-0 592 + 593 + '@babel/plugin-transform-private-property-in-object@7.24.6': 594 + resolution: {integrity: sha512-Qu/ypFxCY5NkAnEhCF86Mvg3NSabKsh/TPpBVswEdkGl7+FbsYHy1ziRqJpwGH4thBdQHh8zx+z7vMYmcJ7iaQ==} 595 + engines: {node: '>=6.9.0'} 596 + peerDependencies: 597 + '@babel/core': ^7.0.0-0 598 + 599 + '@babel/plugin-transform-property-literals@7.24.6': 600 + resolution: {integrity: sha512-oARaglxhRsN18OYsnPTpb8TcKQWDYNsPNmTnx5++WOAsUJ0cSC/FZVlIJCKvPbU4yn/UXsS0551CFKJhN0CaMw==} 601 + engines: {node: '>=6.9.0'} 602 + peerDependencies: 603 + '@babel/core': ^7.0.0-0 604 + 605 + '@babel/plugin-transform-regenerator@7.24.6': 606 + resolution: {integrity: sha512-SMDxO95I8WXRtXhTAc8t/NFQUT7VYbIWwJCJgEli9ml4MhqUMh4S6hxgH6SmAC3eAQNWCDJFxcFeEt9w2sDdXg==} 607 + engines: {node: '>=6.9.0'} 608 + peerDependencies: 609 + '@babel/core': ^7.0.0-0 610 + 611 + '@babel/plugin-transform-reserved-words@7.24.6': 612 + resolution: {integrity: sha512-DcrgFXRRlK64dGE0ZFBPD5egM2uM8mgfrvTMOSB2yKzOtjpGegVYkzh3s1zZg1bBck3nkXiaOamJUqK3Syk+4A==} 613 + engines: {node: '>=6.9.0'} 614 + peerDependencies: 615 + '@babel/core': ^7.0.0-0 616 + 617 + '@babel/plugin-transform-shorthand-properties@7.24.6': 618 + resolution: {integrity: sha512-xnEUvHSMr9eOWS5Al2YPfc32ten7CXdH7Zwyyk7IqITg4nX61oHj+GxpNvl+y5JHjfN3KXE2IV55wAWowBYMVw==} 619 + engines: {node: '>=6.9.0'} 620 + peerDependencies: 621 + '@babel/core': ^7.0.0-0 622 + 623 + '@babel/plugin-transform-spread@7.24.6': 624 + resolution: {integrity: sha512-h/2j7oIUDjS+ULsIrNZ6/TKG97FgmEk1PXryk/HQq6op4XUUUwif2f69fJrzK0wza2zjCS1xhXmouACaWV5uPA==} 625 + engines: {node: '>=6.9.0'} 626 + peerDependencies: 627 + '@babel/core': ^7.0.0-0 628 + 629 + '@babel/plugin-transform-sticky-regex@7.24.6': 630 + resolution: {integrity: sha512-fN8OcTLfGmYv7FnDrsjodYBo1DhPL3Pze/9mIIE2MGCT1KgADYIOD7rEglpLHZj8PZlC/JFX5WcD+85FLAQusw==} 631 + engines: {node: '>=6.9.0'} 632 + peerDependencies: 633 + '@babel/core': ^7.0.0-0 634 + 635 + '@babel/plugin-transform-template-literals@7.24.6': 636 + resolution: {integrity: sha512-BJbEqJIcKwrqUP+KfUIkxz3q8VzXe2R8Wv8TaNgO1cx+nNavxn/2+H8kp9tgFSOL6wYPPEgFvU6IKS4qoGqhmg==} 637 + engines: {node: '>=6.9.0'} 638 + peerDependencies: 639 + '@babel/core': ^7.0.0-0 640 + 641 + '@babel/plugin-transform-typeof-symbol@7.24.6': 642 + resolution: {integrity: sha512-IshCXQ+G9JIFJI7bUpxTE/oA2lgVLAIK8q1KdJNoPXOpvRaNjMySGuvLfBw/Xi2/1lLo953uE8hyYSDW3TSYig==} 643 + engines: {node: '>=6.9.0'} 644 + peerDependencies: 645 + '@babel/core': ^7.0.0-0 646 + 647 + '@babel/plugin-transform-unicode-escapes@7.24.6': 648 + resolution: {integrity: sha512-bKl3xxcPbkQQo5eX9LjjDpU2xYHeEeNQbOhj0iPvetSzA+Tu9q/o5lujF4Sek60CM6MgYvOS/DJuwGbiEYAnLw==} 649 + engines: {node: '>=6.9.0'} 650 + peerDependencies: 651 + '@babel/core': ^7.0.0-0 652 + 653 + '@babel/plugin-transform-unicode-property-regex@7.24.6': 654 + resolution: {integrity: sha512-8EIgImzVUxy15cZiPii9GvLZwsy7Vxc+8meSlR3cXFmBIl5W5Tn9LGBf7CDKkHj4uVfNXCJB8RsVfnmY61iedA==} 655 + engines: {node: '>=6.9.0'} 656 + peerDependencies: 657 + '@babel/core': ^7.0.0-0 658 + 659 + '@babel/plugin-transform-unicode-regex@7.24.6': 660 + resolution: {integrity: sha512-pssN6ExsvxaKU638qcWb81RrvvgZom3jDgU/r5xFZ7TONkZGFf4MhI2ltMb8OcQWhHyxgIavEU+hgqtbKOmsPA==} 661 + engines: {node: '>=6.9.0'} 662 + peerDependencies: 663 + '@babel/core': ^7.0.0-0 664 + 665 + '@babel/plugin-transform-unicode-sets-regex@7.24.6': 666 + resolution: {integrity: sha512-quiMsb28oXWIDK0gXLALOJRXLgICLiulqdZGOaPPd0vRT7fQp74NtdADAVu+D8s00C+0Xs0MxVP0VKF/sZEUgw==} 667 + engines: {node: '>=6.9.0'} 668 + peerDependencies: 669 + '@babel/core': ^7.0.0 670 + 671 + '@babel/preset-env@7.24.6': 672 + resolution: {integrity: sha512-CrxEAvN7VxfjOG8JNF2Y/eMqMJbZPZ185amwGUBp8D9USK90xQmv7dLdFSa+VbD7fdIqcy/Mfv7WtzG8+/qxKg==} 673 + engines: {node: '>=6.9.0'} 674 + peerDependencies: 675 + '@babel/core': ^7.0.0-0 676 + 677 + '@babel/preset-modules@0.1.6-no-external-plugins': 678 + resolution: {integrity: sha512-HrcgcIESLm9aIR842yhJ5RWan/gebQUJ6E/E5+rf0y9o6oj7w0Br+sWuL6kEQ/o/AdfvR1Je9jG18/gnpwjEyA==} 679 + peerDependencies: 680 + '@babel/core': ^7.0.0-0 || ^8.0.0-0 <8.0.0 681 + 682 + '@babel/regjsgen@0.8.0': 683 + resolution: {integrity: sha512-x/rqGMdzj+fWZvCOYForTghzbtqPDZ5gPwaoNGHdgDfF2QA/XZbCBp4Moo5scrkAMPhB7z26XM/AaHuIJdgauA==} 684 + 685 + '@babel/runtime@7.24.6': 686 + resolution: {integrity: sha512-Ja18XcETdEl5mzzACGd+DKgaGJzPTCow7EglgwTmHdwokzDFYh/MHua6lU6DV/hjF2IaOJ4oX2nqnjG7RElKOw==} 687 + engines: {node: '>=6.9.0'} 688 + 689 + '@babel/template@7.24.6': 690 + resolution: {integrity: sha512-3vgazJlLwNXi9jhrR1ef8qiB65L1RK90+lEQwv4OxveHnqC3BfmnHdgySwRLzf6akhlOYenT+b7AfWq+a//AHw==} 691 + engines: {node: '>=6.9.0'} 692 + 693 + '@babel/traverse@7.24.6': 694 + resolution: {integrity: sha512-OsNjaJwT9Zn8ozxcfoBc+RaHdj3gFmCmYoQLUII1o6ZrUwku0BMg80FoOTPx+Gi6XhcQxAYE4xyjPTo4SxEQqw==} 695 + engines: {node: '>=6.9.0'} 696 + 697 + '@babel/types@7.24.6': 698 + resolution: {integrity: sha512-WaMsgi6Q8zMgMth93GvWPXkhAIEobfsIkLTacoVZoK1J0CevIPGYY2Vo5YvJGqyHqXM6P4ppOYGsIRU8MM9pFQ==} 699 + engines: {node: '>=6.9.0'} 700 + 701 + '@esbuild/aix-ppc64@0.20.2': 702 + resolution: {integrity: sha512-D+EBOJHXdNZcLJRBkhENNG8Wji2kgc9AZ9KiPr1JuZjsNtyHzrsfLRrY0tk2H2aoFu6RANO1y1iPPUCDYWkb5g==} 703 + engines: {node: '>=12'} 704 + cpu: [ppc64] 705 + os: [aix] 706 + 707 + '@esbuild/android-arm64@0.20.2': 708 + resolution: {integrity: sha512-mRzjLacRtl/tWU0SvD8lUEwb61yP9cqQo6noDZP/O8VkwafSYwZ4yWy24kan8jE/IMERpYncRt2dw438LP3Xmg==} 709 + engines: {node: '>=12'} 710 + cpu: [arm64] 711 + os: [android] 712 + 713 + '@esbuild/android-arm@0.20.2': 714 + resolution: {integrity: sha512-t98Ra6pw2VaDhqNWO2Oph2LXbz/EJcnLmKLGBJwEwXX/JAN83Fym1rU8l0JUWK6HkIbWONCSSatf4sf2NBRx/w==} 715 + engines: {node: '>=12'} 716 + cpu: [arm] 717 + os: [android] 718 + 719 + '@esbuild/android-x64@0.20.2': 720 + resolution: {integrity: sha512-btzExgV+/lMGDDa194CcUQm53ncxzeBrWJcncOBxuC6ndBkKxnHdFJn86mCIgTELsooUmwUm9FkhSp5HYu00Rg==} 721 + engines: {node: '>=12'} 722 + cpu: [x64] 723 + os: [android] 724 + 725 + '@esbuild/darwin-arm64@0.20.2': 726 + resolution: {integrity: sha512-4J6IRT+10J3aJH3l1yzEg9y3wkTDgDk7TSDFX+wKFiWjqWp/iCfLIYzGyasx9l0SAFPT1HwSCR+0w/h1ES/MjA==} 727 + engines: {node: '>=12'} 728 + cpu: [arm64] 729 + os: [darwin] 730 + 731 + '@esbuild/darwin-x64@0.20.2': 732 + resolution: {integrity: sha512-tBcXp9KNphnNH0dfhv8KYkZhjc+H3XBkF5DKtswJblV7KlT9EI2+jeA8DgBjp908WEuYll6pF+UStUCfEpdysA==} 733 + engines: {node: '>=12'} 734 + cpu: [x64] 735 + os: [darwin] 736 + 737 + '@esbuild/freebsd-arm64@0.20.2': 738 + resolution: {integrity: sha512-d3qI41G4SuLiCGCFGUrKsSeTXyWG6yem1KcGZVS+3FYlYhtNoNgYrWcvkOoaqMhwXSMrZRl69ArHsGJ9mYdbbw==} 739 + engines: {node: '>=12'} 740 + cpu: [arm64] 741 + os: [freebsd] 742 + 743 + '@esbuild/freebsd-x64@0.20.2': 744 + resolution: {integrity: sha512-d+DipyvHRuqEeM5zDivKV1KuXn9WeRX6vqSqIDgwIfPQtwMP4jaDsQsDncjTDDsExT4lR/91OLjRo8bmC1e+Cw==} 745 + engines: {node: '>=12'} 746 + cpu: [x64] 747 + os: [freebsd] 748 + 749 + '@esbuild/linux-arm64@0.20.2': 750 + resolution: {integrity: sha512-9pb6rBjGvTFNira2FLIWqDk/uaf42sSyLE8j1rnUpuzsODBq7FvpwHYZxQ/It/8b+QOS1RYfqgGFNLRI+qlq2A==} 751 + engines: {node: '>=12'} 752 + cpu: [arm64] 753 + os: [linux] 754 + 755 + '@esbuild/linux-arm@0.20.2': 756 + resolution: {integrity: sha512-VhLPeR8HTMPccbuWWcEUD1Az68TqaTYyj6nfE4QByZIQEQVWBB8vup8PpR7y1QHL3CpcF6xd5WVBU/+SBEvGTg==} 757 + engines: {node: '>=12'} 758 + cpu: [arm] 759 + os: [linux] 760 + 761 + '@esbuild/linux-ia32@0.20.2': 762 + resolution: {integrity: sha512-o10utieEkNPFDZFQm9CoP7Tvb33UutoJqg3qKf1PWVeeJhJw0Q347PxMvBgVVFgouYLGIhFYG0UGdBumROyiig==} 763 + engines: {node: '>=12'} 764 + cpu: [ia32] 765 + os: [linux] 766 + 767 + '@esbuild/linux-loong64@0.20.2': 768 + resolution: {integrity: sha512-PR7sp6R/UC4CFVomVINKJ80pMFlfDfMQMYynX7t1tNTeivQ6XdX5r2XovMmha/VjR1YN/HgHWsVcTRIMkymrgQ==} 769 + engines: {node: '>=12'} 770 + cpu: [loong64] 771 + os: [linux] 772 + 773 + '@esbuild/linux-mips64el@0.20.2': 774 + resolution: {integrity: sha512-4BlTqeutE/KnOiTG5Y6Sb/Hw6hsBOZapOVF6njAESHInhlQAghVVZL1ZpIctBOoTFbQyGW+LsVYZ8lSSB3wkjA==} 775 + engines: {node: '>=12'} 776 + cpu: [mips64el] 777 + os: [linux] 778 + 779 + '@esbuild/linux-ppc64@0.20.2': 780 + resolution: {integrity: sha512-rD3KsaDprDcfajSKdn25ooz5J5/fWBylaaXkuotBDGnMnDP1Uv5DLAN/45qfnf3JDYyJv/ytGHQaziHUdyzaAg==} 781 + engines: {node: '>=12'} 782 + cpu: [ppc64] 783 + os: [linux] 784 + 785 + '@esbuild/linux-riscv64@0.20.2': 786 + resolution: {integrity: sha512-snwmBKacKmwTMmhLlz/3aH1Q9T8v45bKYGE3j26TsaOVtjIag4wLfWSiZykXzXuE1kbCE+zJRmwp+ZbIHinnVg==} 787 + engines: {node: '>=12'} 788 + cpu: [riscv64] 789 + os: [linux] 790 + 791 + '@esbuild/linux-s390x@0.20.2': 792 + resolution: {integrity: sha512-wcWISOobRWNm3cezm5HOZcYz1sKoHLd8VL1dl309DiixxVFoFe/o8HnwuIwn6sXre88Nwj+VwZUvJf4AFxkyrQ==} 793 + engines: {node: '>=12'} 794 + cpu: [s390x] 795 + os: [linux] 796 + 797 + '@esbuild/linux-x64@0.20.2': 798 + resolution: {integrity: sha512-1MdwI6OOTsfQfek8sLwgyjOXAu+wKhLEoaOLTjbijk6E2WONYpH9ZU2mNtR+lZ2B4uwr+usqGuVfFT9tMtGvGw==} 799 + engines: {node: '>=12'} 800 + cpu: [x64] 801 + os: [linux] 802 + 803 + '@esbuild/netbsd-x64@0.20.2': 804 + resolution: {integrity: sha512-K8/DhBxcVQkzYc43yJXDSyjlFeHQJBiowJ0uVL6Tor3jGQfSGHNNJcWxNbOI8v5k82prYqzPuwkzHt3J1T1iZQ==} 805 + engines: {node: '>=12'} 806 + cpu: [x64] 807 + os: [netbsd] 808 + 809 + '@esbuild/openbsd-x64@0.20.2': 810 + resolution: {integrity: sha512-eMpKlV0SThJmmJgiVyN9jTPJ2VBPquf6Kt/nAoo6DgHAoN57K15ZghiHaMvqjCye/uU4X5u3YSMgVBI1h3vKrQ==} 811 + engines: {node: '>=12'} 812 + cpu: [x64] 813 + os: [openbsd] 814 + 815 + '@esbuild/sunos-x64@0.20.2': 816 + resolution: {integrity: sha512-2UyFtRC6cXLyejf/YEld4Hajo7UHILetzE1vsRcGL3earZEW77JxrFjH4Ez2qaTiEfMgAXxfAZCm1fvM/G/o8w==} 817 + engines: {node: '>=12'} 818 + cpu: [x64] 819 + os: [sunos] 820 + 821 + '@esbuild/win32-arm64@0.20.2': 822 + resolution: {integrity: sha512-GRibxoawM9ZCnDxnP3usoUDO9vUkpAxIIZ6GQI+IlVmr5kP3zUq+l17xELTHMWTWzjxa2guPNyrpq1GWmPvcGQ==} 823 + engines: {node: '>=12'} 824 + cpu: [arm64] 825 + os: [win32] 826 + 827 + '@esbuild/win32-ia32@0.20.2': 828 + resolution: {integrity: sha512-HfLOfn9YWmkSKRQqovpnITazdtquEW8/SoHW7pWpuEeguaZI4QnCRW6b+oZTztdBnZOS2hqJ6im/D5cPzBTTlQ==} 829 + engines: {node: '>=12'} 830 + cpu: [ia32] 831 + os: [win32] 832 + 833 + '@esbuild/win32-x64@0.20.2': 834 + resolution: {integrity: sha512-N49X4lJX27+l9jbLKSqZ6bKNjzQvHaT8IIFUy+YIqmXQdjYCToGWwOItDrfby14c78aDd5NHQl29xingXfCdLQ==} 835 + engines: {node: '>=12'} 836 + cpu: [x64] 837 + os: [win32] 838 + 839 + '@externdefs/solid-freeze@0.1.1': 840 + resolution: {integrity: sha512-duvZBfJB9oOLphx04ckKF534hP186xIBFaw4GHJ5fGeZY5syZs59UeumV5NC6aiEU9hVhAFMOnDDGkQrFqHrnQ==} 841 + peerDependencies: 842 + solid-js: ^1.8.5 843 + 844 + '@externdefs/solid-query@0.1.4': 845 + resolution: {integrity: sha512-dK5xls7YRkA+XQ8XgMFyASf8hIcz47KMQ/dlfRZhpzFLSKnWd29HQcLBGts9bQrajXai+x4x5u2uY2RKlz3+dw==} 846 + peerDependencies: 847 + solid-js: ^1.8.5 848 + 849 + '@floating-ui/core@1.6.2': 850 + resolution: {integrity: sha512-+2XpQV9LLZeanU4ZevzRnGFg2neDeKHgFLjP6YLW+tly0IvrhqT4u8enLGjLH3qeh85g19xY5rsAusfwTdn5lg==} 851 + 852 + '@floating-ui/dom@1.6.5': 853 + resolution: {integrity: sha512-Nsdud2X65Dz+1RHjAIP0t8z5e2ff/IRbei6BqFrl1urT8sDVzM1HMQ+R0XcU5ceRfyO3I6ayeqIfh+6Wb8LGTw==} 854 + 855 + '@floating-ui/utils@0.2.2': 856 + resolution: {integrity: sha512-J4yDIIthosAsRZ5CPYP/jQvUAQtlZTTD/4suA08/FEnlxqW3sKS9iAhgsa9VYLZ6vDHn/ixJgIqRQPotoBjxIw==} 857 + 858 + '@isaacs/cliui@8.0.2': 859 + resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==} 860 + engines: {node: '>=12'} 861 + 862 + '@jridgewell/gen-mapping@0.3.5': 863 + resolution: {integrity: sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg==} 864 + engines: {node: '>=6.0.0'} 865 + 866 + '@jridgewell/resolve-uri@3.1.2': 867 + resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} 868 + engines: {node: '>=6.0.0'} 869 + 870 + '@jridgewell/set-array@1.2.1': 871 + resolution: {integrity: sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==} 872 + engines: {node: '>=6.0.0'} 873 + 874 + '@jridgewell/source-map@0.3.6': 875 + resolution: {integrity: sha512-1ZJTZebgqllO79ue2bm3rIGud/bOe0pP5BjSRCRxxYkEZS8STV7zN84UBbiYu7jy+eCKSnVIUgoWWE/tt+shMQ==} 876 + 877 + '@jridgewell/sourcemap-codec@1.4.15': 878 + resolution: {integrity: sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==} 879 + 880 + '@jridgewell/trace-mapping@0.3.25': 881 + resolution: {integrity: sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==} 882 + 883 + '@jsr/mary__bluesky-client@0.5.23': 884 + resolution: {integrity: sha512-k1sZVbRvZ//kx3Fq/tEd6W5pAf3IrdE1cUSXl6lqem4cYGv67NrRHMdz51zVIud00BfwVLxHbb97cWeDHDm16Q==, tarball: https://npm.jsr.io/~/11/@jsr/mary__bluesky-client/0.5.23.tgz} 885 + 886 + '@jsr/mary__events@0.1.0': 887 + resolution: {integrity: sha512-oS6jVOaXTaNEa6avRncwrEtUYaBKrq/HEybPa9Z3aoeMs+RSly0vn0KcOj/fy2H6iTBkeh3wa8+/9nFjhKyKIg==, tarball: https://npm.jsr.io/~/11/@jsr/mary__events/0.1.0.tgz} 888 + 889 + '@nodelib/fs.scandir@2.1.5': 890 + resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} 891 + engines: {node: '>= 8'} 892 + 893 + '@nodelib/fs.stat@2.0.5': 894 + resolution: {integrity: sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==} 895 + engines: {node: '>= 8'} 896 + 897 + '@nodelib/fs.walk@1.2.8': 898 + resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==} 899 + engines: {node: '>= 8'} 900 + 901 + '@pkgjs/parseargs@0.11.0': 902 + resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==} 903 + engines: {node: '>=14'} 904 + 905 + '@rollup/plugin-babel@5.3.1': 906 + resolution: {integrity: sha512-WFfdLWU/xVWKeRQnKmIAQULUI7Il0gZnBIH/ZFO069wYIfPu+8zrfp/KMW0atmELoRDq8FbiP3VCss9MhCut7Q==} 907 + engines: {node: '>= 10.0.0'} 908 + peerDependencies: 909 + '@babel/core': ^7.0.0 910 + '@types/babel__core': ^7.1.9 911 + rollup: ^1.20.0||^2.0.0 912 + peerDependenciesMeta: 913 + '@types/babel__core': 914 + optional: true 915 + 916 + '@rollup/plugin-node-resolve@15.2.3': 917 + resolution: {integrity: sha512-j/lym8nf5E21LwBT4Df1VD6hRO2L2iwUeUmP7litikRsVp1H6NWx20NEp0Y7su+7XGc476GnXXc4kFeZNGmaSQ==} 918 + engines: {node: '>=14.0.0'} 919 + peerDependencies: 920 + rollup: ^2.78.0||^3.0.0||^4.0.0 921 + peerDependenciesMeta: 922 + rollup: 923 + optional: true 924 + 925 + '@rollup/plugin-replace@2.4.2': 926 + resolution: {integrity: sha512-IGcu+cydlUMZ5En85jxHH4qj2hta/11BHq95iHEyb2sbgiN0eCdzvUcHw5gt9pBL5lTi4JDYJ1acCoMGpTvEZg==} 927 + peerDependencies: 928 + rollup: ^1.20.0 || ^2.0.0 929 + 930 + '@rollup/plugin-terser@0.4.4': 931 + resolution: {integrity: sha512-XHeJC5Bgvs8LfukDwWZp7yeqin6ns8RTl2B9avbejt6tZqsqvVoWI7ZTQrcNsfKEDWBTnTxM8nMDkO2IFFbd0A==} 932 + engines: {node: '>=14.0.0'} 933 + peerDependencies: 934 + rollup: ^2.0.0||^3.0.0||^4.0.0 935 + peerDependenciesMeta: 936 + rollup: 937 + optional: true 938 + 939 + '@rollup/pluginutils@3.1.0': 940 + resolution: {integrity: sha512-GksZ6pr6TpIjHm8h9lSQ8pi8BE9VeubNT0OMJ3B5uZJ8pz73NPiqOtCog/x2/QzM1ENChPKxMDhiQuRHsqc+lg==} 941 + engines: {node: '>= 8.0.0'} 942 + peerDependencies: 943 + rollup: ^1.20.0||^2.0.0 944 + 945 + '@rollup/pluginutils@5.1.0': 946 + resolution: {integrity: sha512-XTIWOPPcpvyKI6L1NHo0lFlCyznUEyPmPY1mc3KpPVDYulHSTvyeLNVW00QTLIAFNhR3kYnJTQHeGqU4M3n09g==} 947 + engines: {node: '>=14.0.0'} 948 + peerDependencies: 949 + rollup: ^1.20.0||^2.0.0||^3.0.0||^4.0.0 950 + peerDependenciesMeta: 951 + rollup: 952 + optional: true 953 + 954 + '@rollup/rollup-android-arm-eabi@4.18.0': 955 + resolution: {integrity: sha512-Tya6xypR10giZV1XzxmH5wr25VcZSncG0pZIjfePT0OVBvqNEurzValetGNarVrGiq66EBVAFn15iYX4w6FKgQ==} 956 + cpu: [arm] 957 + os: [android] 958 + 959 + '@rollup/rollup-android-arm64@4.18.0': 960 + resolution: {integrity: sha512-avCea0RAP03lTsDhEyfy+hpfr85KfyTctMADqHVhLAF3MlIkq83CP8UfAHUssgXTYd+6er6PaAhx/QGv4L1EiA==} 961 + cpu: [arm64] 962 + os: [android] 963 + 964 + '@rollup/rollup-darwin-arm64@4.18.0': 965 + resolution: {integrity: sha512-IWfdwU7KDSm07Ty0PuA/W2JYoZ4iTj3TUQjkVsO/6U+4I1jN5lcR71ZEvRh52sDOERdnNhhHU57UITXz5jC1/w==} 966 + cpu: [arm64] 967 + os: [darwin] 968 + 969 + '@rollup/rollup-darwin-x64@4.18.0': 970 + resolution: {integrity: sha512-n2LMsUz7Ynu7DoQrSQkBf8iNrjOGyPLrdSg802vk6XT3FtsgX6JbE8IHRvposskFm9SNxzkLYGSq9QdpLYpRNA==} 971 + cpu: [x64] 972 + os: [darwin] 973 + 974 + '@rollup/rollup-linux-arm-gnueabihf@4.18.0': 975 + resolution: {integrity: sha512-C/zbRYRXFjWvz9Z4haRxcTdnkPt1BtCkz+7RtBSuNmKzMzp3ZxdM28Mpccn6pt28/UWUCTXa+b0Mx1k3g6NOMA==} 976 + cpu: [arm] 977 + os: [linux] 978 + 979 + '@rollup/rollup-linux-arm-musleabihf@4.18.0': 980 + resolution: {integrity: sha512-l3m9ewPgjQSXrUMHg93vt0hYCGnrMOcUpTz6FLtbwljo2HluS4zTXFy2571YQbisTnfTKPZ01u/ukJdQTLGh9A==} 981 + cpu: [arm] 982 + os: [linux] 983 + 984 + '@rollup/rollup-linux-arm64-gnu@4.18.0': 985 + resolution: {integrity: sha512-rJ5D47d8WD7J+7STKdCUAgmQk49xuFrRi9pZkWoRD1UeSMakbcepWXPF8ycChBoAqs1pb2wzvbY6Q33WmN2ftw==} 986 + cpu: [arm64] 987 + os: [linux] 988 + 989 + '@rollup/rollup-linux-arm64-musl@4.18.0': 990 + resolution: {integrity: sha512-be6Yx37b24ZwxQ+wOQXXLZqpq4jTckJhtGlWGZs68TgdKXJgw54lUUoFYrg6Zs/kjzAQwEwYbp8JxZVzZLRepQ==} 991 + cpu: [arm64] 992 + os: [linux] 993 + 994 + '@rollup/rollup-linux-powerpc64le-gnu@4.18.0': 995 + resolution: {integrity: sha512-hNVMQK+qrA9Todu9+wqrXOHxFiD5YmdEi3paj6vP02Kx1hjd2LLYR2eaN7DsEshg09+9uzWi2W18MJDlG0cxJA==} 996 + cpu: [ppc64] 997 + os: [linux] 998 + 999 + '@rollup/rollup-linux-riscv64-gnu@4.18.0': 1000 + resolution: {integrity: sha512-ROCM7i+m1NfdrsmvwSzoxp9HFtmKGHEqu5NNDiZWQtXLA8S5HBCkVvKAxJ8U+CVctHwV2Gb5VUaK7UAkzhDjlg==} 1001 + cpu: [riscv64] 1002 + os: [linux] 1003 + 1004 + '@rollup/rollup-linux-s390x-gnu@4.18.0': 1005 + resolution: {integrity: sha512-0UyyRHyDN42QL+NbqevXIIUnKA47A+45WyasO+y2bGJ1mhQrfrtXUpTxCOrfxCR4esV3/RLYyucGVPiUsO8xjg==} 1006 + cpu: [s390x] 1007 + os: [linux] 1008 + 1009 + '@rollup/rollup-linux-x64-gnu@4.18.0': 1010 + resolution: {integrity: sha512-xuglR2rBVHA5UsI8h8UbX4VJ470PtGCf5Vpswh7p2ukaqBGFTnsfzxUBetoWBWymHMxbIG0Cmx7Y9qDZzr648w==} 1011 + cpu: [x64] 1012 + os: [linux] 1013 + 1014 + '@rollup/rollup-linux-x64-musl@4.18.0': 1015 + resolution: {integrity: sha512-LKaqQL9osY/ir2geuLVvRRs+utWUNilzdE90TpyoX0eNqPzWjRm14oMEE+YLve4k/NAqCdPkGYDaDF5Sw+xBfg==} 1016 + cpu: [x64] 1017 + os: [linux] 1018 + 1019 + '@rollup/rollup-win32-arm64-msvc@4.18.0': 1020 + resolution: {integrity: sha512-7J6TkZQFGo9qBKH0pk2cEVSRhJbL6MtfWxth7Y5YmZs57Pi+4x6c2dStAUvaQkHQLnEQv1jzBUW43GvZW8OFqA==} 1021 + cpu: [arm64] 1022 + os: [win32] 1023 + 1024 + '@rollup/rollup-win32-ia32-msvc@4.18.0': 1025 + resolution: {integrity: sha512-Txjh+IxBPbkUB9+SXZMpv+b/vnTEtFyfWZgJ6iyCmt2tdx0OF5WhFowLmnh8ENGNpfUlUZkdI//4IEmhwPieNg==} 1026 + cpu: [ia32] 1027 + os: [win32] 1028 + 1029 + '@rollup/rollup-win32-x64-msvc@4.18.0': 1030 + resolution: {integrity: sha512-UOo5FdvOL0+eIVTgS4tIdbW+TtnBLWg1YBCcU2KWM7nuNwRz9bksDX1bekJJCpu25N1DVWaCwnT39dVQxzqS8g==} 1031 + cpu: [x64] 1032 + os: [win32] 1033 + 1034 + '@surma/rollup-plugin-off-main-thread@2.2.3': 1035 + resolution: {integrity: sha512-lR8q/9W7hZpMWweNiAKU7NQerBnzQQLvi8qnTDU/fxItPhtZVMbPV3lbCwjhIlNBe9Bbr5V+KHshvWmVSG9cxQ==} 1036 + 1037 + '@tanstack/query-core@5.17.19': 1038 + resolution: {integrity: sha512-Lzw8FUtnLCc9Jwz0sw9xOjZB+/mCCmJev38v2wHMUl/ioXNIhnNWeMxu0NKUjIhAd62IRB3eAtvxAGDJ55UkyA==} 1039 + 1040 + '@types/babel__core@7.20.5': 1041 + resolution: {integrity: sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==} 1042 + 1043 + '@types/babel__generator@7.6.8': 1044 + resolution: {integrity: sha512-ASsj+tpEDsEiFr1arWrlN6V3mdfjRMZt6LtK/Vp/kreFLnr5QH5+DhvD5nINYZXzwJvXeGq+05iUXcAzVrqWtw==} 1045 + 1046 + '@types/babel__template@7.4.4': 1047 + resolution: {integrity: sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==} 1048 + 1049 + '@types/babel__traverse@7.20.6': 1050 + resolution: {integrity: sha512-r1bzfrm0tomOI8g1SzvCaQHo6Lcv6zu0EA+W2kHrt8dyrHQxGzBBL4kdkzIS+jBMV+EYcMAEAqXqYaLJq5rOZg==} 1051 + 1052 + '@types/dom-close-watcher@1.0.0': 1053 + resolution: {integrity: sha512-7pL0By56sVVGMSJ3HdSY+u08Id0ljStCaf1VnGFxwfpuNdA0HMz0sl2J24eSi9M6ptl9ySkVK35jF75Fn8trUg==} 1054 + 1055 + '@types/estree@0.0.39': 1056 + resolution: {integrity: sha512-EYNwp3bU+98cpU4lAWYYL7Zz+2gryWH1qbdDTidVd6hkiR6weksdbMadyXKXNPEkQFhXM+hVO9ZygomHXp+AIw==} 1057 + 1058 + '@types/estree@1.0.5': 1059 + resolution: {integrity: sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==} 1060 + 1061 + '@types/resolve@1.20.2': 1062 + resolution: {integrity: sha512-60BCwRFOZCQhDncwQdxxeOEEkbc5dIMccYLwbxsS4TUNeVECQ/pBJ0j09mrHOl/JJvpRPGwO9SvE4nR2Nb/a4Q==} 1063 + 1064 + '@types/trusted-types@2.0.7': 1065 + resolution: {integrity: sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==} 1066 + 1067 + acorn@8.11.3: 1068 + resolution: {integrity: sha512-Y9rRfJG5jcKOE0CLisYbojUjIrIEE7AGMzA/Sm4BslANhbS+cDMpgBdcPT91oJ7OuJ9hYJBx59RjbhxVnrF8Xg==} 1069 + engines: {node: '>=0.4.0'} 1070 + hasBin: true 1071 + 1072 + ajv@8.14.0: 1073 + resolution: {integrity: sha512-oYs1UUtO97ZO2lJ4bwnWeQW8/zvOIQLGKcvPTsWmvc2SYgBb+upuNS5NxoLaMU4h8Ju3Nbj6Cq8mD2LQoqVKFA==} 1074 + 1075 + ansi-regex@5.0.1: 1076 + resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} 1077 + engines: {node: '>=8'} 1078 + 1079 + ansi-regex@6.0.1: 1080 + resolution: {integrity: sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==} 1081 + engines: {node: '>=12'} 1082 + 1083 + ansi-styles@3.2.1: 1084 + resolution: {integrity: sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==} 1085 + engines: {node: '>=4'} 1086 + 1087 + ansi-styles@4.3.0: 1088 + resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} 1089 + engines: {node: '>=8'} 1090 + 1091 + ansi-styles@6.2.1: 1092 + resolution: {integrity: sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==} 1093 + engines: {node: '>=12'} 1094 + 1095 + any-promise@1.3.0: 1096 + resolution: {integrity: sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==} 1097 + 1098 + anymatch@3.1.3: 1099 + resolution: {integrity: sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==} 1100 + engines: {node: '>= 8'} 1101 + 1102 + arg@5.0.2: 1103 + resolution: {integrity: sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==} 1104 + 1105 + array-buffer-byte-length@1.0.1: 1106 + resolution: {integrity: sha512-ahC5W1xgou+KTXix4sAO8Ki12Q+jf4i0+tmk3sC+zgcynshkHxzpXdImBehiUYKKKDwvfFiJl1tZt6ewscS1Mg==} 1107 + engines: {node: '>= 0.4'} 1108 + 1109 + arraybuffer.prototype.slice@1.0.3: 1110 + resolution: {integrity: sha512-bMxMKAjg13EBSVscxTaYA4mRc5t1UAXa2kXiGTNfZ079HIWXEkKmkgFrh/nJqamaLSrXO5H4WFFkPEaLJWbs3A==} 1111 + engines: {node: '>= 0.4'} 1112 + 1113 + async@3.2.5: 1114 + resolution: {integrity: sha512-baNZyqaaLhyLVKm/DlvdW051MSgO6b8eVfIezl9E5PqWxFgzLm/wQntEW4zOytVburDEr0JlALEpdOFwvErLsg==} 1115 + 1116 + at-least-node@1.0.0: 1117 + resolution: {integrity: sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg==} 1118 + engines: {node: '>= 4.0.0'} 1119 + 1120 + autoprefixer@10.4.19: 1121 + resolution: {integrity: sha512-BaENR2+zBZ8xXhM4pUaKUxlVdxZ0EZhjvbopwnXmxRUfqDmwSpC2lAi/QXvx7NRdPCo1WKEcEF6mV64si1z4Ew==} 1122 + engines: {node: ^10 || ^12 || >=14} 1123 + hasBin: true 1124 + peerDependencies: 1125 + postcss: ^8.1.0 1126 + 1127 + available-typed-arrays@1.0.7: 1128 + resolution: {integrity: sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==} 1129 + engines: {node: '>= 0.4'} 1130 + 1131 + babel-plugin-jsx-dom-expressions@0.37.21: 1132 + resolution: {integrity: sha512-WbQo1NQ241oki8bYasVzkMXOTSIri5GO/K47rYJb2ZBh8GaPUEWiWbMV3KwXz+96eU2i54N6ThzjQG/f5n8Azw==} 1133 + peerDependencies: 1134 + '@babel/core': ^7.20.12 1135 + 1136 + babel-plugin-polyfill-corejs2@0.4.11: 1137 + resolution: {integrity: sha512-sMEJ27L0gRHShOh5G54uAAPaiCOygY/5ratXuiyb2G46FmlSpc9eFCzYVyDiPxfNbwzA7mYahmjQc5q+CZQ09Q==} 1138 + peerDependencies: 1139 + '@babel/core': ^7.4.0 || ^8.0.0-0 <8.0.0 1140 + 1141 + babel-plugin-polyfill-corejs3@0.10.4: 1142 + resolution: {integrity: sha512-25J6I8NGfa5YkCDogHRID3fVCadIR8/pGl1/spvCkzb6lVn6SR3ojpx9nOn9iEBcUsjY24AmdKm5khcfKdylcg==} 1143 + peerDependencies: 1144 + '@babel/core': ^7.4.0 || ^8.0.0-0 <8.0.0 1145 + 1146 + babel-plugin-polyfill-regenerator@0.6.2: 1147 + resolution: {integrity: sha512-2R25rQZWP63nGwaAswvDazbPXfrM3HwVoBXK6HcqeKrSrL/JqcC/rDcf95l4r7LXLyxDXc8uQDa064GubtCABg==} 1148 + peerDependencies: 1149 + '@babel/core': ^7.4.0 || ^8.0.0-0 <8.0.0 1150 + 1151 + babel-preset-solid@1.8.17: 1152 + resolution: {integrity: sha512-s/FfTZOeds0hYxYqce90Jb+0ycN2lrzC7VP1k1JIn3wBqcaexDKdYi6xjB+hMNkL+Q6HobKbwsriqPloasR9LA==} 1153 + peerDependencies: 1154 + '@babel/core': ^7.0.0 1155 + 1156 + balanced-match@1.0.2: 1157 + resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} 1158 + 1159 + binary-extensions@2.3.0: 1160 + resolution: {integrity: sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==} 1161 + engines: {node: '>=8'} 1162 + 1163 + brace-expansion@1.1.11: 1164 + resolution: {integrity: sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==} 1165 + 1166 + brace-expansion@2.0.1: 1167 + resolution: {integrity: sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==} 1168 + 1169 + braces@3.0.3: 1170 + resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} 1171 + engines: {node: '>=8'} 1172 + 1173 + browserslist@4.23.0: 1174 + resolution: {integrity: sha512-QW8HiM1shhT2GuzkvklfjcKDiWFXHOeFCIA/huJPwHsslwcydgk7X+z2zXpEijP98UCY7HbubZt5J2Zgvf0CaQ==} 1175 + engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} 1176 + hasBin: true 1177 + 1178 + buffer-from@1.1.2: 1179 + resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==} 1180 + 1181 + builtin-modules@3.3.0: 1182 + resolution: {integrity: sha512-zhaCDicdLuWN5UbN5IMnFqNMhNfo919sH85y2/ea+5Yg9TsTkeZxpL+JLbp6cgYFS4sRLp3YV4S6yDuqVWHYOw==} 1183 + engines: {node: '>=6'} 1184 + 1185 + call-bind@1.0.7: 1186 + resolution: {integrity: sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w==} 1187 + engines: {node: '>= 0.4'} 1188 + 1189 + camelcase-css@2.0.1: 1190 + resolution: {integrity: sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==} 1191 + engines: {node: '>= 6'} 1192 + 1193 + caniuse-lite@1.0.30001623: 1194 + resolution: {integrity: sha512-X/XhAVKlpIxWPpgRTnlgZssJrF0m6YtRA0QDWgsBNT12uZM6LPRydR7ip405Y3t1LamD8cP2TZFEDZFBf5ApcA==} 1195 + 1196 + cborg@4.0.7: 1197 + resolution: {integrity: sha512-5h2n7973T4dkY2XLfHpwYR9IjeDSfolZibYtb8clW53BvvgTyq+X2EtwrjfhTAETrwaQOX4+lRua14/XjbZHaQ==} 1198 + hasBin: true 1199 + 1200 + chalk@2.4.2: 1201 + resolution: {integrity: sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==} 1202 + engines: {node: '>=4'} 1203 + 1204 + chalk@4.1.2: 1205 + resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} 1206 + engines: {node: '>=10'} 1207 + 1208 + chokidar@3.6.0: 1209 + resolution: {integrity: sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==} 1210 + engines: {node: '>= 8.10.0'} 1211 + 1212 + color-convert@1.9.3: 1213 + resolution: {integrity: sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==} 1214 + 1215 + color-convert@2.0.1: 1216 + resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} 1217 + engines: {node: '>=7.0.0'} 1218 + 1219 + color-name@1.1.3: 1220 + resolution: {integrity: sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==} 1221 + 1222 + color-name@1.1.4: 1223 + resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} 1224 + 1225 + commander@2.20.3: 1226 + resolution: {integrity: sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==} 1227 + 1228 + commander@4.1.1: 1229 + resolution: {integrity: sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==} 1230 + engines: {node: '>= 6'} 1231 + 1232 + common-tags@1.8.2: 1233 + resolution: {integrity: sha512-gk/Z852D2Wtb//0I+kRFNKKE9dIIVirjoqPoA1wJU+XePVXZfGeBpk45+A1rKO4Q43prqWBNY/MiIeRLbPWUaA==} 1234 + engines: {node: '>=4.0.0'} 1235 + 1236 + concat-map@0.0.1: 1237 + resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} 1238 + 1239 + convert-source-map@2.0.0: 1240 + resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} 1241 + 1242 + core-js-compat@3.37.1: 1243 + resolution: {integrity: sha512-9TNiImhKvQqSUkOvk/mMRZzOANTiEVC7WaBNhHcKM7x+/5E1l5NvsysR19zuDQScE8k+kfQXWRN3AtS/eOSHpg==} 1244 + 1245 + cross-spawn@7.0.3: 1246 + resolution: {integrity: sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==} 1247 + engines: {node: '>= 8'} 1248 + 1249 + crypto-random-string@2.0.0: 1250 + resolution: {integrity: sha512-v1plID3y9r/lPhviJ1wrXpLeyUIGAZ2SHNYTEapm7/8A9nLPoyvVp3RK/EPFqn5kEznyWgYZNsRtYYIWbuG8KA==} 1251 + engines: {node: '>=8'} 1252 + 1253 + cssesc@3.0.0: 1254 + resolution: {integrity: sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==} 1255 + engines: {node: '>=4'} 1256 + hasBin: true 1257 + 1258 + csstype@3.1.3: 1259 + resolution: {integrity: sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==} 1260 + 1261 + data-view-buffer@1.0.1: 1262 + resolution: {integrity: sha512-0lht7OugA5x3iJLOWFhWK/5ehONdprk0ISXqVFn/NFrDu+cuc8iADFrGQz5BnRK7LLU3JmkbXSxaqX+/mXYtUA==} 1263 + engines: {node: '>= 0.4'} 1264 + 1265 + data-view-byte-length@1.0.1: 1266 + resolution: {integrity: sha512-4J7wRJD3ABAzr8wP+OcIcqq2dlUKp4DVflx++hs5h5ZKydWMI6/D/fAot+yh6g2tHh8fLFTvNOaVN357NvSrOQ==} 1267 + engines: {node: '>= 0.4'} 1268 + 1269 + data-view-byte-offset@1.0.0: 1270 + resolution: {integrity: sha512-t/Ygsytq+R995EJ5PZlD4Cu56sWa8InXySaViRzw9apusqsOO2bQP+SbYzAhR0pFKoB+43lYy8rWban9JSuXnA==} 1271 + engines: {node: '>= 0.4'} 1272 + 1273 + debug@4.3.4: 1274 + resolution: {integrity: sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==} 1275 + engines: {node: '>=6.0'} 1276 + peerDependencies: 1277 + supports-color: '*' 1278 + peerDependenciesMeta: 1279 + supports-color: 1280 + optional: true 1281 + 1282 + deepmerge@4.3.1: 1283 + resolution: {integrity: sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==} 1284 + engines: {node: '>=0.10.0'} 1285 + 1286 + define-data-property@1.1.4: 1287 + resolution: {integrity: sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==} 1288 + engines: {node: '>= 0.4'} 1289 + 1290 + define-properties@1.2.1: 1291 + resolution: {integrity: sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==} 1292 + engines: {node: '>= 0.4'} 1293 + 1294 + didyoumean@1.2.2: 1295 + resolution: {integrity: sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==} 1296 + 1297 + dlv@1.1.3: 1298 + resolution: {integrity: sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==} 1299 + 1300 + eastasianwidth@0.2.0: 1301 + resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==} 1302 + 1303 + ejs@3.1.10: 1304 + resolution: {integrity: sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA==} 1305 + engines: {node: '>=0.10.0'} 1306 + hasBin: true 1307 + 1308 + electron-to-chromium@1.4.783: 1309 + resolution: {integrity: sha512-bT0jEz/Xz1fahQpbZ1D7LgmPYZ3iHVY39NcWWro1+hA2IvjiPeaXtfSqrQ+nXjApMvQRE2ASt1itSLRrebHMRQ==} 1310 + 1311 + emoji-regex@8.0.0: 1312 + resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} 1313 + 1314 + emoji-regex@9.2.2: 1315 + resolution: {integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==} 1316 + 1317 + es-abstract@1.23.3: 1318 + resolution: {integrity: sha512-e+HfNH61Bj1X9/jLc5v1owaLYuHdeHHSQlkhCBiTK8rBvKaULl/beGMxwrMXjpYrv4pz22BlY570vVePA2ho4A==} 1319 + engines: {node: '>= 0.4'} 1320 + 1321 + es-define-property@1.0.0: 1322 + resolution: {integrity: sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ==} 1323 + engines: {node: '>= 0.4'} 1324 + 1325 + es-errors@1.3.0: 1326 + resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==} 1327 + engines: {node: '>= 0.4'} 1328 + 1329 + es-object-atoms@1.0.0: 1330 + resolution: {integrity: sha512-MZ4iQ6JwHOBQjahnjwaC1ZtIBH+2ohjamzAO3oaHcXYup7qxjF2fixyH+Q71voWHeOkI2q/TnJao/KfXYIZWbw==} 1331 + engines: {node: '>= 0.4'} 1332 + 1333 + es-set-tostringtag@2.0.3: 1334 + resolution: {integrity: sha512-3T8uNMC3OQTHkFUsFq8r/BwAXLHvU/9O9mE0fBc/MY5iq/8H7ncvO947LmYA6ldWw9Uh8Yhf25zu6n7nML5QWQ==} 1335 + engines: {node: '>= 0.4'} 1336 + 1337 + es-to-primitive@1.2.1: 1338 + resolution: {integrity: sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA==} 1339 + engines: {node: '>= 0.4'} 1340 + 1341 + esbuild@0.20.2: 1342 + resolution: {integrity: sha512-WdOOppmUNU+IbZ0PaDiTst80zjnrOkyJNHoKupIcVyU8Lvla3Ugx94VzkQ32Ijqd7UhHJy75gNWDMUekcrSJ6g==} 1343 + engines: {node: '>=12'} 1344 + hasBin: true 1345 + 1346 + escalade@3.1.2: 1347 + resolution: {integrity: sha512-ErCHMCae19vR8vQGe50xIsVomy19rg6gFu3+r3jkEO46suLMWBksvVyoGgQV+jOfl84ZSOSlmv6Gxa89PmTGmA==} 1348 + engines: {node: '>=6'} 1349 + 1350 + escape-string-regexp@1.0.5: 1351 + resolution: {integrity: sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==} 1352 + engines: {node: '>=0.8.0'} 1353 + 1354 + estree-walker@1.0.1: 1355 + resolution: {integrity: sha512-1fMXF3YP4pZZVozF8j/ZLfvnR8NSIljt56UhbZ5PeeDmmGHpgpdwQt7ITlGvYaQukCvuBRMLEiKiYC+oeIg4cg==} 1356 + 1357 + estree-walker@2.0.2: 1358 + resolution: {integrity: sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==} 1359 + 1360 + esutils@2.0.3: 1361 + resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} 1362 + engines: {node: '>=0.10.0'} 1363 + 1364 + fast-deep-equal@3.1.3: 1365 + resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} 1366 + 1367 + fast-glob@3.3.2: 1368 + resolution: {integrity: sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==} 1369 + engines: {node: '>=8.6.0'} 1370 + 1371 + fast-json-stable-stringify@2.1.0: 1372 + resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==} 1373 + 1374 + fastq@1.17.1: 1375 + resolution: {integrity: sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w==} 1376 + 1377 + filelist@1.0.4: 1378 + resolution: {integrity: sha512-w1cEuf3S+DrLCQL7ET6kz+gmlJdbq9J7yXCSjK/OZCPA+qEN1WyF4ZAf0YYJa4/shHJra2t/d/r8SV4Ji+x+8Q==} 1379 + 1380 + fill-range@7.1.1: 1381 + resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==} 1382 + engines: {node: '>=8'} 1383 + 1384 + for-each@0.3.3: 1385 + resolution: {integrity: sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw==} 1386 + 1387 + foreground-child@3.1.1: 1388 + resolution: {integrity: sha512-TMKDUnIte6bfb5nWv7V/caI169OHgvwjb7V4WkeUvbQQdjr5rWKqHFiKWb/fcOwB+CzBT+qbWjvj+DVwRskpIg==} 1389 + engines: {node: '>=14'} 1390 + 1391 + fraction.js@4.3.7: 1392 + resolution: {integrity: sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==} 1393 + 1394 + fs-extra@9.1.0: 1395 + resolution: {integrity: sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==} 1396 + engines: {node: '>=10'} 1397 + 1398 + fs.realpath@1.0.0: 1399 + resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==} 1400 + 1401 + fsevents@2.3.3: 1402 + resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} 1403 + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} 1404 + os: [darwin] 1405 + 1406 + function-bind@1.1.2: 1407 + resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} 1408 + 1409 + function.prototype.name@1.1.6: 1410 + resolution: {integrity: sha512-Z5kx79swU5P27WEayXM1tBi5Ze/lbIyiNgU3qyXUOf9b2rgXYyF9Dy9Cx+IQv/Lc8WCG6L82zwUPpSS9hGehIg==} 1411 + engines: {node: '>= 0.4'} 1412 + 1413 + functions-have-names@1.2.3: 1414 + resolution: {integrity: sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==} 1415 + 1416 + gensync@1.0.0-beta.2: 1417 + resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==} 1418 + engines: {node: '>=6.9.0'} 1419 + 1420 + get-intrinsic@1.2.4: 1421 + resolution: {integrity: sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==} 1422 + engines: {node: '>= 0.4'} 1423 + 1424 + get-own-enumerable-property-symbols@3.0.2: 1425 + resolution: {integrity: sha512-I0UBV/XOz1XkIJHEUDMZAbzCThU/H8DxmSfmdGcKPnVhu2VfFqr34jr9777IyaTYvxjedWhqVIilEDsCdP5G6g==} 1426 + 1427 + get-symbol-description@1.0.2: 1428 + resolution: {integrity: sha512-g0QYk1dZBxGwk+Ngc+ltRH2IBp2f7zBkBMBJZCDerh6EhlhSR6+9irMCuT/09zD6qkarHUSn529sK/yL4S27mg==} 1429 + engines: {node: '>= 0.4'} 1430 + 1431 + glob-parent@5.1.2: 1432 + resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} 1433 + engines: {node: '>= 6'} 1434 + 1435 + glob-parent@6.0.2: 1436 + resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==} 1437 + engines: {node: '>=10.13.0'} 1438 + 1439 + glob@10.4.1: 1440 + resolution: {integrity: sha512-2jelhlq3E4ho74ZyVLN03oKdAZVUa6UDZzFLVH1H7dnoax+y9qyaq8zBkfDIggjniU19z0wU18y16jMB2eyVIw==} 1441 + engines: {node: '>=16 || 14 >=14.18'} 1442 + hasBin: true 1443 + 1444 + glob@7.2.3: 1445 + resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==} 1446 + deprecated: Glob versions prior to v9 are no longer supported 1447 + 1448 + globals@11.12.0: 1449 + resolution: {integrity: sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==} 1450 + engines: {node: '>=4'} 1451 + 1452 + globalthis@1.0.4: 1453 + resolution: {integrity: sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==} 1454 + engines: {node: '>= 0.4'} 1455 + 1456 + gopd@1.0.1: 1457 + resolution: {integrity: sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==} 1458 + 1459 + graceful-fs@4.2.11: 1460 + resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} 1461 + 1462 + has-bigints@1.0.2: 1463 + resolution: {integrity: sha512-tSvCKtBr9lkF0Ex0aQiP9N+OpV4zi2r/Nee5VkRDbaqv35RLYMzbwQfFSZZH0kR+Rd6302UJZ2p/bJCEoR3VoQ==} 1464 + 1465 + has-flag@3.0.0: 1466 + resolution: {integrity: sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==} 1467 + engines: {node: '>=4'} 1468 + 1469 + has-flag@4.0.0: 1470 + resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} 1471 + engines: {node: '>=8'} 1472 + 1473 + has-property-descriptors@1.0.2: 1474 + resolution: {integrity: sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==} 1475 + 1476 + has-proto@1.0.3: 1477 + resolution: {integrity: sha512-SJ1amZAJUiZS+PhsVLf5tGydlaVB8EdFpaSO4gmiUKUOxk8qzn5AIy4ZeJUmh22znIdk/uMAUT2pl3FxzVUH+Q==} 1478 + engines: {node: '>= 0.4'} 1479 + 1480 + has-symbols@1.0.3: 1481 + resolution: {integrity: sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==} 1482 + engines: {node: '>= 0.4'} 1483 + 1484 + has-tostringtag@1.0.2: 1485 + resolution: {integrity: sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==} 1486 + engines: {node: '>= 0.4'} 1487 + 1488 + hasown@2.0.2: 1489 + resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} 1490 + engines: {node: '>= 0.4'} 1491 + 1492 + html-entities@2.3.3: 1493 + resolution: {integrity: sha512-DV5Ln36z34NNTDgnz0EWGBLZENelNAtkiFA4kyNOG2tDI6Mz1uSWiq1wAKdyjnJwyDiDO7Fa2SO1CTxPXL8VxA==} 1494 + 1495 + idb@7.1.1: 1496 + resolution: {integrity: sha512-gchesWBzyvGHRO9W8tzUWFDycow5gwjvFKfyV9FF32Y7F50yZMp7mP+T2mJIWFx49zicqyC4uefHM17o6xKIVQ==} 1497 + 1498 + inflight@1.0.6: 1499 + resolution: {integrity: sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==} 1500 + deprecated: This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful. 1501 + 1502 + inherits@2.0.4: 1503 + resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} 1504 + 1505 + internal-slot@1.0.7: 1506 + resolution: {integrity: sha512-NGnrKwXzSms2qUUih/ILZ5JBqNTSa1+ZmP6flaIp6KmSElgE9qdndzS3cqjrDovwFdmwsGsLdeFgB6suw+1e9g==} 1507 + engines: {node: '>= 0.4'} 1508 + 1509 + is-array-buffer@3.0.4: 1510 + resolution: {integrity: sha512-wcjaerHw0ydZwfhiKbXJWLDY8A7yV7KhjQOpb83hGgGfId/aQa4TOvwyzn2PuswW2gPCYEL/nEAiSVpdOj1lXw==} 1511 + engines: {node: '>= 0.4'} 1512 + 1513 + is-bigint@1.0.4: 1514 + resolution: {integrity: sha512-zB9CruMamjym81i2JZ3UMn54PKGsQzsJeo6xvN3HJJ4CAsQNB6iRutp2To77OfCNuoxspsIhzaPoO1zyCEhFOg==} 1515 + 1516 + is-binary-path@2.1.0: 1517 + resolution: {integrity: sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==} 1518 + engines: {node: '>=8'} 1519 + 1520 + is-boolean-object@1.1.2: 1521 + resolution: {integrity: sha512-gDYaKHJmnj4aWxyj6YHyXVpdQawtVLHU5cb+eztPGczf6cjuTdwve5ZIEfgXqH4e57An1D1AKf8CZ3kYrQRqYA==} 1522 + engines: {node: '>= 0.4'} 1523 + 1524 + is-builtin-module@3.2.1: 1525 + resolution: {integrity: sha512-BSLE3HnV2syZ0FK0iMA/yUGplUeMmNz4AW5fnTunbCIqZi4vG3WjJT9FHMy5D69xmAYBHXQhJdALdpwVxV501A==} 1526 + engines: {node: '>=6'} 1527 + 1528 + is-callable@1.2.7: 1529 + resolution: {integrity: sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==} 1530 + engines: {node: '>= 0.4'} 1531 + 1532 + is-core-module@2.13.1: 1533 + resolution: {integrity: sha512-hHrIjvZsftOsvKSn2TRYl63zvxsgE0K+0mYMoH6gD4omR5IWB2KynivBQczo3+wF1cCkjzvptnI9Q0sPU66ilw==} 1534 + 1535 + is-data-view@1.0.1: 1536 + resolution: {integrity: sha512-AHkaJrsUVW6wq6JS8y3JnM/GJF/9cf+k20+iDzlSaJrinEo5+7vRiteOSwBhHRiAyQATN1AmY4hwzxJKPmYf+w==} 1537 + engines: {node: '>= 0.4'} 1538 + 1539 + is-date-object@1.0.5: 1540 + resolution: {integrity: sha512-9YQaSxsAiSwcvS33MBk3wTCVnWK+HhF8VZR2jRxehM16QcVOdHqPn4VPHmRK4lSr38n9JriurInLcP90xsYNfQ==} 1541 + engines: {node: '>= 0.4'} 1542 + 1543 + is-extglob@2.1.1: 1544 + resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} 1545 + engines: {node: '>=0.10.0'} 1546 + 1547 + is-fullwidth-code-point@3.0.0: 1548 + resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==} 1549 + engines: {node: '>=8'} 1550 + 1551 + is-glob@4.0.3: 1552 + resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} 1553 + engines: {node: '>=0.10.0'} 1554 + 1555 + is-module@1.0.0: 1556 + resolution: {integrity: sha512-51ypPSPCoTEIN9dy5Oy+h4pShgJmPCygKfyRCISBI+JoWT/2oJvK8QPxmwv7b/p239jXrm9M1mlQbyKJ5A152g==} 1557 + 1558 + is-negative-zero@2.0.3: 1559 + resolution: {integrity: sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw==} 1560 + engines: {node: '>= 0.4'} 1561 + 1562 + is-number-object@1.0.7: 1563 + resolution: {integrity: sha512-k1U0IRzLMo7ZlYIfzRu23Oh6MiIFasgpb9X76eqfFZAqwH44UI4KTBvBYIZ1dSL9ZzChTB9ShHfLkR4pdW5krQ==} 1564 + engines: {node: '>= 0.4'} 1565 + 1566 + is-number@7.0.0: 1567 + resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} 1568 + engines: {node: '>=0.12.0'} 1569 + 1570 + is-obj@1.0.1: 1571 + resolution: {integrity: sha512-l4RyHgRqGN4Y3+9JHVrNqO+tN0rV5My76uW5/nuO4K1b6vw5G8d/cmFjP9tRfEsdhZNt0IFdZuK/c2Vr4Nb+Qg==} 1572 + engines: {node: '>=0.10.0'} 1573 + 1574 + is-regex@1.1.4: 1575 + resolution: {integrity: sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg==} 1576 + engines: {node: '>= 0.4'} 1577 + 1578 + is-regexp@1.0.0: 1579 + resolution: {integrity: sha512-7zjFAPO4/gwyQAAgRRmqeEeyIICSdmCqa3tsVHMdBzaXXRiqopZL4Cyghg/XulGWrtABTpbnYYzzIRffLkP4oA==} 1580 + engines: {node: '>=0.10.0'} 1581 + 1582 + is-shared-array-buffer@1.0.3: 1583 + resolution: {integrity: sha512-nA2hv5XIhLR3uVzDDfCIknerhx8XUKnstuOERPNNIinXG7v9u+ohXF67vxm4TPTEPU6lm61ZkwP3c9PCB97rhg==} 1584 + engines: {node: '>= 0.4'} 1585 + 1586 + is-stream@2.0.1: 1587 + resolution: {integrity: sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==} 1588 + engines: {node: '>=8'} 1589 + 1590 + is-string@1.0.7: 1591 + resolution: {integrity: sha512-tE2UXzivje6ofPW7l23cjDOMa09gb7xlAqG6jG5ej6uPV32TlWP3NKPigtaGeHNu9fohccRYvIiZMfOOnOYUtg==} 1592 + engines: {node: '>= 0.4'} 1593 + 1594 + is-symbol@1.0.4: 1595 + resolution: {integrity: sha512-C/CPBqKWnvdcxqIARxyOh4v1UUEOCHpgDa0WYgpKDFMszcrPcffg5uhwSgPCLD2WWxmq6isisz87tzT01tuGhg==} 1596 + engines: {node: '>= 0.4'} 1597 + 1598 + is-typed-array@1.1.13: 1599 + resolution: {integrity: sha512-uZ25/bUAlUY5fR4OKT4rZQEBrzQWYV9ZJYGGsUmEJ6thodVJ1HX64ePQ6Z0qPWP+m+Uq6e9UugrE38jeYsDSMw==} 1600 + engines: {node: '>= 0.4'} 1601 + 1602 + is-weakref@1.0.2: 1603 + resolution: {integrity: sha512-qctsuLZmIQ0+vSSMfoVvyFe2+GSEvnmZ2ezTup1SBse9+twCCeial6EEi3Nc2KFcf6+qz2FBPnjXsk8xhKSaPQ==} 1604 + 1605 + is-what@4.1.16: 1606 + resolution: {integrity: sha512-ZhMwEosbFJkA0YhFnNDgTM4ZxDRsS6HqTo7qsZM08fehyRYIYa0yHu5R6mgo1n/8MgaPBXiPimPD77baVFYg+A==} 1607 + engines: {node: '>=12.13'} 1608 + 1609 + isarray@2.0.5: 1610 + resolution: {integrity: sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==} 1611 + 1612 + isexe@2.0.0: 1613 + resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} 1614 + 1615 + jackspeak@3.4.0: 1616 + resolution: {integrity: sha512-JVYhQnN59LVPFCEcVa2C3CrEKYacvjRfqIQl+h8oi91aLYQVWRYbxjPcv1bUiUy/kLmQaANrYfNMCO3kuEDHfw==} 1617 + engines: {node: '>=14'} 1618 + 1619 + jake@10.9.1: 1620 + resolution: {integrity: sha512-61btcOHNnLnsOdtLgA5efqQWjnSi/vow5HbI7HMdKKWqvrKR1bLK3BPlJn9gcSaP2ewuamUSMB5XEy76KUIS2w==} 1621 + engines: {node: '>=10'} 1622 + hasBin: true 1623 + 1624 + jiti@1.21.3: 1625 + resolution: {integrity: sha512-uy2bNX5zQ+tESe+TiC7ilGRz8AtRGmnJH55NC5S0nSUjvvvM2hJHmefHErugGXN4pNv4Qx7vLsnNw9qJ9mtIsw==} 1626 + hasBin: true 1627 + 1628 + js-tokens@4.0.0: 1629 + resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} 1630 + 1631 + jsesc@0.5.0: 1632 + resolution: {integrity: sha512-uZz5UnB7u4T9LvwmFqXii7pZSouaRPorGs5who1Ip7VO0wxanFvBL7GkM6dTHlgX+jhBApRetaWpnDabOeTcnA==} 1633 + hasBin: true 1634 + 1635 + jsesc@2.5.2: 1636 + resolution: {integrity: sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==} 1637 + engines: {node: '>=4'} 1638 + hasBin: true 1639 + 1640 + json-schema-traverse@1.0.0: 1641 + resolution: {integrity: sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==} 1642 + 1643 + json-schema@0.4.0: 1644 + resolution: {integrity: sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA==} 1645 + 1646 + json5@2.2.3: 1647 + resolution: {integrity: sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==} 1648 + engines: {node: '>=6'} 1649 + hasBin: true 1650 + 1651 + jsonfile@6.1.0: 1652 + resolution: {integrity: sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==} 1653 + 1654 + jsonpointer@5.0.1: 1655 + resolution: {integrity: sha512-p/nXbhSEcu3pZRdkW1OfJhpsVtW1gd4Wa1fnQc9YLiTfAjn0312eMKimbdIQzuZl9aa9xUGaRlP9T/CJE/ditQ==} 1656 + engines: {node: '>=0.10.0'} 1657 + 1658 + leven@3.1.0: 1659 + resolution: {integrity: sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==} 1660 + engines: {node: '>=6'} 1661 + 1662 + lilconfig@2.1.0: 1663 + resolution: {integrity: sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ==} 1664 + engines: {node: '>=10'} 1665 + 1666 + lilconfig@3.1.1: 1667 + resolution: {integrity: sha512-O18pf7nyvHTckunPWCV1XUNXU1piu01y2b7ATJ0ppkUkk8ocqVWBrYjJBCwHDjD/ZWcfyrA0P4gKhzWGi5EINQ==} 1668 + engines: {node: '>=14'} 1669 + 1670 + lines-and-columns@1.2.4: 1671 + resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==} 1672 + 1673 + lodash.debounce@4.0.8: 1674 + resolution: {integrity: sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==} 1675 + 1676 + lodash.sortby@4.7.0: 1677 + resolution: {integrity: sha512-HDWXG8isMntAyRF5vZ7xKuEvOhT4AhlRt/3czTSjvGUxjYCBVRQY48ViDHyfYz9VIoBkW4TMGQNapx+l3RUwdA==} 1678 + 1679 + lodash@4.17.21: 1680 + resolution: {integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==} 1681 + 1682 + lru-cache@10.2.2: 1683 + resolution: {integrity: sha512-9hp3Vp2/hFQUiIwKo8XCeFVnrg8Pk3TYNPIR7tJADKi5YfcF7vEaK7avFHTlSy3kOKYaJQaalfEo6YuXdceBOQ==} 1684 + engines: {node: 14 || >=16.14} 1685 + 1686 + lru-cache@5.1.1: 1687 + resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} 1688 + 1689 + magic-string@0.25.9: 1690 + resolution: {integrity: sha512-RmF0AsMzgt25qzqqLc1+MbHmhdx0ojF2Fvs4XnOqz2ZOBXzzkEwc/dJQZCYHAn7v1jbVOjAZfK8msRn4BxO4VQ==} 1691 + 1692 + merge-anything@5.1.7: 1693 + resolution: {integrity: sha512-eRtbOb1N5iyH0tkQDAoQ4Ipsp/5qSR79Dzrz8hEPxRX10RWWR/iQXdoKmBSRCThY1Fh5EhISDtpSc93fpxUniQ==} 1694 + engines: {node: '>=12.13'} 1695 + 1696 + merge2@1.4.1: 1697 + resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} 1698 + engines: {node: '>= 8'} 1699 + 1700 + micromatch@4.0.7: 1701 + resolution: {integrity: sha512-LPP/3KorzCwBxfeUuZmaR6bG2kdeHSbe0P2tY3FLRU4vYrjYz5hI4QZwV0njUx3jeuKe67YukQ1LSPZBKDqO/Q==} 1702 + engines: {node: '>=8.6'} 1703 + 1704 + minimatch@3.1.2: 1705 + resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==} 1706 + 1707 + minimatch@5.1.6: 1708 + resolution: {integrity: sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==} 1709 + engines: {node: '>=10'} 1710 + 1711 + minimatch@9.0.4: 1712 + resolution: {integrity: sha512-KqWh+VchfxcMNRAJjj2tnsSJdNbHsVgnkBhTNrW7AjVo6OvLtxw8zfT9oLw1JSohlFzJ8jCoTgaoXvJ+kHt6fw==} 1713 + engines: {node: '>=16 || 14 >=14.17'} 1714 + 1715 + minipass@7.1.2: 1716 + resolution: {integrity: sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==} 1717 + engines: {node: '>=16 || 14 >=14.17'} 1718 + 1719 + ms@2.1.2: 1720 + resolution: {integrity: sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==} 1721 + 1722 + mz@2.7.0: 1723 + resolution: {integrity: sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==} 1724 + 1725 + nanoid@3.3.7: 1726 + resolution: {integrity: sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==} 1727 + engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} 1728 + hasBin: true 1729 + 1730 + nanoid@5.0.7: 1731 + resolution: {integrity: sha512-oLxFY2gd2IqnjcYyOXD8XGCftpGtZP2AbHbOkthDkvRywH5ayNtPVy9YlOPcHckXzbLTCHpkb7FB+yuxKV13pQ==} 1732 + engines: {node: ^18 || >=20} 1733 + hasBin: true 1734 + 1735 + node-releases@2.0.14: 1736 + resolution: {integrity: sha512-y10wOWt8yZpqXmOgRo77WaHEmhYQYGNA6y421PKsKYWEK8aW+cqAphborZDhqfyKrbZEN92CN1X2KbafY2s7Yw==} 1737 + 1738 + normalize-path@3.0.0: 1739 + resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==} 1740 + engines: {node: '>=0.10.0'} 1741 + 1742 + normalize-range@0.1.2: 1743 + resolution: {integrity: sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA==} 1744 + engines: {node: '>=0.10.0'} 1745 + 1746 + object-assign@4.1.1: 1747 + resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} 1748 + engines: {node: '>=0.10.0'} 1749 + 1750 + object-hash@3.0.0: 1751 + resolution: {integrity: sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==} 1752 + engines: {node: '>= 6'} 1753 + 1754 + object-inspect@1.13.1: 1755 + resolution: {integrity: sha512-5qoj1RUiKOMsCCNLV1CBiPYE10sziTsnmNxkAI/rZhiD63CF7IqdFGC/XzjWjpSgLf0LxXX3bDFIh0E18f6UhQ==} 1756 + 1757 + object-keys@1.1.1: 1758 + resolution: {integrity: sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==} 1759 + engines: {node: '>= 0.4'} 1760 + 1761 + object.assign@4.1.5: 1762 + resolution: {integrity: sha512-byy+U7gp+FVwmyzKPYhW2h5l3crpmGsxl7X2s8y43IgxvG4g3QZ6CffDtsNQy1WsmZpQbO+ybo0AlW7TY6DcBQ==} 1763 + engines: {node: '>= 0.4'} 1764 + 1765 + once@1.4.0: 1766 + resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} 1767 + 1768 + path-is-absolute@1.0.1: 1769 + resolution: {integrity: sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==} 1770 + engines: {node: '>=0.10.0'} 1771 + 1772 + path-key@3.1.1: 1773 + resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} 1774 + engines: {node: '>=8'} 1775 + 1776 + path-parse@1.0.7: 1777 + resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==} 1778 + 1779 + path-scurry@1.11.1: 1780 + resolution: {integrity: sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==} 1781 + engines: {node: '>=16 || 14 >=14.18'} 1782 + 1783 + picocolors@1.0.1: 1784 + resolution: {integrity: sha512-anP1Z8qwhkbmu7MFP5iTt+wQKXgwzf7zTyGlcdzabySa9vd0Xt392U0rVmz9poOaBj0uHJKyyo9/upk0HrEQew==} 1785 + 1786 + picomatch@2.3.1: 1787 + resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==} 1788 + engines: {node: '>=8.6'} 1789 + 1790 + pify@2.3.0: 1791 + resolution: {integrity: sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==} 1792 + engines: {node: '>=0.10.0'} 1793 + 1794 + pirates@4.0.6: 1795 + resolution: {integrity: sha512-saLsH7WeYYPiD25LDuLRRY/i+6HaPYr6G1OUlN39otzkSTxKnubR9RTxS3/Kk50s1g2JTgFwWQDQyplC5/SHZg==} 1796 + engines: {node: '>= 6'} 1797 + 1798 + possible-typed-array-names@1.0.0: 1799 + resolution: {integrity: sha512-d7Uw+eZoloe0EHDIYoe+bQ5WXnGMOpmiZFTuMWCwpjzzkL2nTjcKiAk4hh8TjnGye2TwWOk3UXucZ+3rbmBa8Q==} 1800 + engines: {node: '>= 0.4'} 1801 + 1802 + postcss-import@15.1.0: 1803 + resolution: {integrity: sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==} 1804 + engines: {node: '>=14.0.0'} 1805 + peerDependencies: 1806 + postcss: ^8.0.0 1807 + 1808 + postcss-js@4.0.1: 1809 + resolution: {integrity: sha512-dDLF8pEO191hJMtlHFPRa8xsizHaM82MLfNkUHdUtVEV3tgTp5oj+8qbEqYM57SLfc74KSbw//4SeJma2LRVIw==} 1810 + engines: {node: ^12 || ^14 || >= 16} 1811 + peerDependencies: 1812 + postcss: ^8.4.21 1813 + 1814 + postcss-load-config@4.0.2: 1815 + resolution: {integrity: sha512-bSVhyJGL00wMVoPUzAVAnbEoWyqRxkjv64tUl427SKnPrENtq6hJwUojroMz2VB+Q1edmi4IfrAPpami5VVgMQ==} 1816 + engines: {node: '>= 14'} 1817 + peerDependencies: 1818 + postcss: '>=8.0.9' 1819 + ts-node: '>=9.0.0' 1820 + peerDependenciesMeta: 1821 + postcss: 1822 + optional: true 1823 + ts-node: 1824 + optional: true 1825 + 1826 + postcss-nested@6.0.1: 1827 + resolution: {integrity: sha512-mEp4xPMi5bSWiMbsgoPfcP74lsWLHkQbZc3sY+jWYd65CUwXrUaTp0fmNpa01ZcETKlIgUdFN/MpS2xZtqL9dQ==} 1828 + engines: {node: '>=12.0'} 1829 + peerDependencies: 1830 + postcss: ^8.2.14 1831 + 1832 + postcss-selector-parser@6.1.0: 1833 + resolution: {integrity: sha512-UMz42UD0UY0EApS0ZL9o1XnLhSTtvvvLe5Dc2H2O56fvRZi+KulDyf5ctDhhtYJBGKStV2FL1fy6253cmLgqVQ==} 1834 + engines: {node: '>=4'} 1835 + 1836 + postcss-value-parser@4.2.0: 1837 + resolution: {integrity: sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==} 1838 + 1839 + postcss@8.4.38: 1840 + resolution: {integrity: sha512-Wglpdk03BSfXkHoQa3b/oulrotAkwrlLDRSOb9D0bN86FdRyE9lppSp33aHNPgBa0JKCoB+drFLZkQoRRYae5A==} 1841 + engines: {node: ^10 || ^12 || >=14} 1842 + 1843 + prettier-plugin-tailwindcss@0.6.3: 1844 + resolution: {integrity: sha512-GeJ9bqXN4APAP0V5T2a1J/o6a50MWevEUCPWxijpdXFDQkBCoAfz4pQfv+YMXSqZ5GXLMDYio0mUOfrYL7gf4w==} 1845 + engines: {node: '>=14.21.3'} 1846 + peerDependencies: 1847 + '@ianvs/prettier-plugin-sort-imports': '*' 1848 + '@prettier/plugin-pug': '*' 1849 + '@shopify/prettier-plugin-liquid': '*' 1850 + '@trivago/prettier-plugin-sort-imports': '*' 1851 + '@zackad/prettier-plugin-twig-melody': '*' 1852 + prettier: ^3.0 1853 + prettier-plugin-astro: '*' 1854 + prettier-plugin-css-order: '*' 1855 + prettier-plugin-import-sort: '*' 1856 + prettier-plugin-jsdoc: '*' 1857 + prettier-plugin-marko: '*' 1858 + prettier-plugin-organize-attributes: '*' 1859 + prettier-plugin-organize-imports: '*' 1860 + prettier-plugin-sort-imports: '*' 1861 + prettier-plugin-style-order: '*' 1862 + prettier-plugin-svelte: '*' 1863 + peerDependenciesMeta: 1864 + '@ianvs/prettier-plugin-sort-imports': 1865 + optional: true 1866 + '@prettier/plugin-pug': 1867 + optional: true 1868 + '@shopify/prettier-plugin-liquid': 1869 + optional: true 1870 + '@trivago/prettier-plugin-sort-imports': 1871 + optional: true 1872 + '@zackad/prettier-plugin-twig-melody': 1873 + optional: true 1874 + prettier-plugin-astro: 1875 + optional: true 1876 + prettier-plugin-css-order: 1877 + optional: true 1878 + prettier-plugin-import-sort: 1879 + optional: true 1880 + prettier-plugin-jsdoc: 1881 + optional: true 1882 + prettier-plugin-marko: 1883 + optional: true 1884 + prettier-plugin-organize-attributes: 1885 + optional: true 1886 + prettier-plugin-organize-imports: 1887 + optional: true 1888 + prettier-plugin-sort-imports: 1889 + optional: true 1890 + prettier-plugin-style-order: 1891 + optional: true 1892 + prettier-plugin-svelte: 1893 + optional: true 1894 + 1895 + prettier@3.3.2: 1896 + resolution: {integrity: sha512-rAVeHYMcv8ATV5d508CFdn+8/pHPpXeIid1DdrPwXnaAdH7cqjVbpJaT5eq4yRAFU/lsbwYwSF/n5iNrdJHPQA==} 1897 + engines: {node: '>=14'} 1898 + hasBin: true 1899 + 1900 + pretty-bytes@5.6.0: 1901 + resolution: {integrity: sha512-FFw039TmrBqFK8ma/7OL3sDz/VytdtJr044/QUJtH0wK9lb9jLq9tJyIxUwtQJHwar2BqtiA4iCWSwo9JLkzFg==} 1902 + engines: {node: '>=6'} 1903 + 1904 + pretty-bytes@6.1.1: 1905 + resolution: {integrity: sha512-mQUvGU6aUFQ+rNvTIAcZuWGRT9a6f6Yrg9bHs4ImKF+HZCEK+plBvnAZYSIQztknZF2qnzNtr6F8s0+IuptdlQ==} 1906 + engines: {node: ^14.13.1 || >=16.0.0} 1907 + 1908 + punycode@2.3.1: 1909 + resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} 1910 + engines: {node: '>=6'} 1911 + 1912 + queue-microtask@1.2.3: 1913 + resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} 1914 + 1915 + randombytes@2.1.0: 1916 + resolution: {integrity: sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==} 1917 + 1918 + read-cache@1.0.0: 1919 + resolution: {integrity: sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==} 1920 + 1921 + readdirp@3.6.0: 1922 + resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==} 1923 + engines: {node: '>=8.10.0'} 1924 + 1925 + regenerate-unicode-properties@10.1.1: 1926 + resolution: {integrity: sha512-X007RyZLsCJVVrjgEFVpLUTZwyOZk3oiL75ZcuYjlIWd6rNJtOjkBwQc5AsRrpbKVkxN6sklw/k/9m2jJYOf8Q==} 1927 + engines: {node: '>=4'} 1928 + 1929 + regenerate@1.4.2: 1930 + resolution: {integrity: sha512-zrceR/XhGYU/d/opr2EKO7aRHUeiBI8qjtfHqADTwZd6Szfy16la6kqD0MIUs5z5hx6AaKa+PixpPrR289+I0A==} 1931 + 1932 + regenerator-runtime@0.14.1: 1933 + resolution: {integrity: sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==} 1934 + 1935 + regenerator-transform@0.15.2: 1936 + resolution: {integrity: sha512-hfMp2BoF0qOk3uc5V20ALGDS2ddjQaLrdl7xrGXvAIow7qeWRM2VA2HuCHkUKk9slq3VwEwLNK3DFBqDfPGYtg==} 1937 + 1938 + regexp.prototype.flags@1.5.2: 1939 + resolution: {integrity: sha512-NcDiDkTLuPR+++OCKB0nWafEmhg/Da8aUPLPMQbK+bxKKCm1/S5he+AqYa4PlMCVBalb4/yxIRub6qkEx5yJbw==} 1940 + engines: {node: '>= 0.4'} 1941 + 1942 + regexpu-core@5.3.2: 1943 + resolution: {integrity: sha512-RAM5FlZz+Lhmo7db9L298p2vHP5ZywrVXmVXpmAD9GuL5MPH6t9ROw1iA/wfHkQ76Qe7AaPF0nGuim96/IrQMQ==} 1944 + engines: {node: '>=4'} 1945 + 1946 + regjsparser@0.9.1: 1947 + resolution: {integrity: sha512-dQUtn90WanSNl+7mQKcXAgZxvUe7Z0SqXlgzv0za4LwiUhyzBC58yQO3liFoUgu8GiJVInAhJjkj1N0EtQ5nkQ==} 1948 + hasBin: true 1949 + 1950 + require-from-string@2.0.2: 1951 + resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==} 1952 + engines: {node: '>=0.10.0'} 1953 + 1954 + resolve@1.22.8: 1955 + resolution: {integrity: sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==} 1956 + hasBin: true 1957 + 1958 + reusify@1.0.4: 1959 + resolution: {integrity: sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==} 1960 + engines: {iojs: '>=1.0.0', node: '>=0.10.0'} 1961 + 1962 + rollup@2.79.1: 1963 + resolution: {integrity: sha512-uKxbd0IhMZOhjAiD5oAFp7BqvkA4Dv47qpOCtaNvng4HBwdbWtdOh8f5nZNuk2rp51PMGk3bzfWu5oayNEuYnw==} 1964 + engines: {node: '>=10.0.0'} 1965 + hasBin: true 1966 + 1967 + rollup@4.18.0: 1968 + resolution: {integrity: sha512-QmJz14PX3rzbJCN1SG4Xe/bAAX2a6NpCP8ab2vfu2GiUr8AQcr2nCV/oEO3yneFarB67zk8ShlIyWb2LGTb3Sg==} 1969 + engines: {node: '>=18.0.0', npm: '>=8.0.0'} 1970 + hasBin: true 1971 + 1972 + run-parallel@1.2.0: 1973 + resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} 1974 + 1975 + safe-array-concat@1.1.2: 1976 + resolution: {integrity: sha512-vj6RsCsWBCf19jIeHEfkRMw8DPiBb+DMXklQ/1SGDHOMlHdPUkZXFQ2YdplS23zESTijAcurb1aSgJA3AgMu1Q==} 1977 + engines: {node: '>=0.4'} 1978 + 1979 + safe-buffer@5.2.1: 1980 + resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==} 1981 + 1982 + safe-regex-test@1.0.3: 1983 + resolution: {integrity: sha512-CdASjNJPvRa7roO6Ra/gLYBTzYzzPyyBXxIMdGW3USQLyjWEls2RgW5UBTXaQVp+OrpeCK3bLem8smtmheoRuw==} 1984 + engines: {node: '>= 0.4'} 1985 + 1986 + semver@6.3.1: 1987 + resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} 1988 + hasBin: true 1989 + 1990 + serialize-javascript@6.0.2: 1991 + resolution: {integrity: sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==} 1992 + 1993 + seroval-plugins@1.0.7: 1994 + resolution: {integrity: sha512-GO7TkWvodGp6buMEX9p7tNyIkbwlyuAWbI6G9Ec5bhcm7mQdu3JOK1IXbEUwb3FVzSc363GraG/wLW23NSavIw==} 1995 + engines: {node: '>=10'} 1996 + peerDependencies: 1997 + seroval: ^1.0 1998 + 1999 + seroval@1.0.7: 2000 + resolution: {integrity: sha512-n6ZMQX5q0Vn19Zq7CIKNIo7E75gPkGCFUEqDpa8jgwpYr/vScjqnQ6H09t1uIiZ0ZSK0ypEGvrYK2bhBGWsGdw==} 2001 + engines: {node: '>=10'} 2002 + 2003 + set-function-length@1.2.2: 2004 + resolution: {integrity: sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==} 2005 + engines: {node: '>= 0.4'} 2006 + 2007 + set-function-name@2.0.2: 2008 + resolution: {integrity: sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ==} 2009 + engines: {node: '>= 0.4'} 2010 + 2011 + shebang-command@2.0.0: 2012 + resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} 2013 + engines: {node: '>=8'} 2014 + 2015 + shebang-regex@3.0.0: 2016 + resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} 2017 + engines: {node: '>=8'} 2018 + 2019 + side-channel@1.0.6: 2020 + resolution: {integrity: sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA==} 2021 + engines: {node: '>= 0.4'} 2022 + 2023 + signal-exit@4.1.0: 2024 + resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==} 2025 + engines: {node: '>=14'} 2026 + 2027 + smob@1.5.0: 2028 + resolution: {integrity: sha512-g6T+p7QO8npa+/hNx9ohv1E5pVCmWrVCUzUXJyLdMmftX6ER0oiWY/w9knEonLpnOp6b6FenKnMfR8gqwWdwig==} 2029 + 2030 + solid-floating-ui@0.2.1: 2031 + resolution: {integrity: sha512-ZWVigPZjisSY7GkAKzZpDoY6Eb0NWSakaF7tut5jkW0ls9BB/bk7Jhsxd8l0JoU3KaxYH15GYcpuM0fU06kc/A==} 2032 + engines: {node: '>=10'} 2033 + peerDependencies: 2034 + '@floating-ui/dom': ^1.0 2035 + solid-js: ^1.3 2036 + 2037 + solid-js@1.8.17: 2038 + resolution: {integrity: sha512-E0FkUgv9sG/gEBWkHr/2XkBluHb1fkrHywUgA6o6XolPDCJ4g1HaLmQufcBBhiF36ee40q+HpG/vCZu7fLpI3Q==} 2039 + 2040 + solid-refresh@0.6.3: 2041 + resolution: {integrity: sha512-F3aPsX6hVw9ttm5LYlth8Q15x6MlI/J3Dn+o3EQyRTtTxidepSTwAYdozt01/YA+7ObcciagGEyXIopGZzQtbA==} 2042 + peerDependencies: 2043 + solid-js: ^1.3 2044 + 2045 + solid-textarea-autosize@0.0.5: 2046 + resolution: {integrity: sha512-le/X+VLr4G3PEE/gZuKBDNqh5e9OoNiIYIN0fSebAuOABxJIY/Eh/9rjDEMB8DbPdeD/wHIVk4Wd9zBvFFIt1A==} 2047 + peerDependencies: 2048 + solid-js: '>=1.0.0' 2049 + 2050 + source-map-js@1.2.0: 2051 + resolution: {integrity: sha512-itJW8lvSA0TXEphiRoawsCksnlf8SyvmFzIhltqAHluXd88pkCd+cXJVHTDwdCr0IzwptSm035IHQktUu1QUMg==} 2052 + engines: {node: '>=0.10.0'} 2053 + 2054 + source-map-support@0.5.21: 2055 + resolution: {integrity: sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==} 2056 + 2057 + source-map@0.6.1: 2058 + resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==} 2059 + engines: {node: '>=0.10.0'} 2060 + 2061 + source-map@0.8.0-beta.0: 2062 + resolution: {integrity: sha512-2ymg6oRBpebeZi9UUNsgQ89bhx01TcTkmNTGnNO88imTmbSgy4nfujrgVEFKWpMTEGA11EDkTt7mqObTPdigIA==} 2063 + engines: {node: '>= 8'} 2064 + 2065 + sourcemap-codec@1.4.8: 2066 + resolution: {integrity: sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA==} 2067 + deprecated: Please use @jridgewell/sourcemap-codec instead 2068 + 2069 + string-width@4.2.3: 2070 + resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} 2071 + engines: {node: '>=8'} 2072 + 2073 + string-width@5.1.2: 2074 + resolution: {integrity: sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==} 2075 + engines: {node: '>=12'} 2076 + 2077 + string.prototype.matchall@4.0.11: 2078 + resolution: {integrity: sha512-NUdh0aDavY2og7IbBPenWqR9exH+E26Sv8e0/eTe1tltDGZL+GtBkDAnnyBtmekfK6/Dq3MkcGtzXFEd1LQrtg==} 2079 + engines: {node: '>= 0.4'} 2080 + 2081 + string.prototype.trim@1.2.9: 2082 + resolution: {integrity: sha512-klHuCNxiMZ8MlsOihJhJEBJAiMVqU3Z2nEXWfWnIqjN0gEFS9J9+IxKozWWtQGcgoa1WUZzLjKPTr4ZHNFTFxw==} 2083 + engines: {node: '>= 0.4'} 2084 + 2085 + string.prototype.trimend@1.0.8: 2086 + resolution: {integrity: sha512-p73uL5VCHCO2BZZ6krwwQE3kCzM7NKmis8S//xEC6fQonchbum4eP6kR4DLEjQFO3Wnj3Fuo8NM0kOSjVdHjZQ==} 2087 + 2088 + string.prototype.trimstart@1.0.8: 2089 + resolution: {integrity: sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg==} 2090 + engines: {node: '>= 0.4'} 2091 + 2092 + stringify-object@3.3.0: 2093 + resolution: {integrity: sha512-rHqiFh1elqCQ9WPLIC8I0Q/g/wj5J1eMkyoiD6eoQApWHP0FtlK7rqnhmabL5VUY9JQCcqwwvlOaSuutekgyrw==} 2094 + engines: {node: '>=4'} 2095 + 2096 + strip-ansi@6.0.1: 2097 + resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} 2098 + engines: {node: '>=8'} 2099 + 2100 + strip-ansi@7.1.0: 2101 + resolution: {integrity: sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==} 2102 + engines: {node: '>=12'} 2103 + 2104 + strip-comments@2.0.1: 2105 + resolution: {integrity: sha512-ZprKx+bBLXv067WTCALv8SSz5l2+XhpYCsVtSqlMnkAXMWDq+/ekVbl1ghqP9rUHTzv6sm/DwCOiYutU/yp1fw==} 2106 + engines: {node: '>=10'} 2107 + 2108 + sucrase@3.35.0: 2109 + resolution: {integrity: sha512-8EbVDiu9iN/nESwxeSxDKe0dunta1GOlHufmSSXxMD2z2/tMZpDMpvXQGsc+ajGo8y2uYUmixaSRUc/QPoQ0GA==} 2110 + engines: {node: '>=16 || 14 >=14.17'} 2111 + hasBin: true 2112 + 2113 + supports-color@5.5.0: 2114 + resolution: {integrity: sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==} 2115 + engines: {node: '>=4'} 2116 + 2117 + supports-color@7.2.0: 2118 + resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} 2119 + engines: {node: '>=8'} 2120 + 2121 + supports-preserve-symlinks-flag@1.0.0: 2122 + resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} 2123 + engines: {node: '>= 0.4'} 2124 + 2125 + tailwindcss@3.4.4: 2126 + resolution: {integrity: sha512-ZoyXOdJjISB7/BcLTR6SEsLgKtDStYyYZVLsUtWChO4Ps20CBad7lfJKVDiejocV4ME1hLmyY0WJE3hSDcmQ2A==} 2127 + engines: {node: '>=14.0.0'} 2128 + hasBin: true 2129 + 2130 + temp-dir@2.0.0: 2131 + resolution: {integrity: sha512-aoBAniQmmwtcKp/7BzsH8Cxzv8OL736p7v1ihGb5e9DJ9kTwGWHrQrVB5+lfVDzfGrdRzXch+ig7LHaY1JTOrg==} 2132 + engines: {node: '>=8'} 2133 + 2134 + tempy@0.6.0: 2135 + resolution: {integrity: sha512-G13vtMYPT/J8A4X2SjdtBTphZlrp1gKv6hZiOjw14RCWg6GbHuQBGtjlx75xLbYV/wEc0D7G5K4rxKP/cXk8Bw==} 2136 + engines: {node: '>=10'} 2137 + 2138 + terser@5.31.1: 2139 + resolution: {integrity: sha512-37upzU1+viGvuFtBo9NPufCb9dwM0+l9hMxYyWfBA+fbwrPqNJAhbZ6W47bBFnZHKHTUBnMvi87434qq+qnxOg==} 2140 + engines: {node: '>=10'} 2141 + hasBin: true 2142 + 2143 + thenify-all@1.6.0: 2144 + resolution: {integrity: sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==} 2145 + engines: {node: '>=0.8'} 2146 + 2147 + thenify@3.3.1: 2148 + resolution: {integrity: sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==} 2149 + 2150 + to-fast-properties@2.0.0: 2151 + resolution: {integrity: sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==} 2152 + engines: {node: '>=4'} 2153 + 2154 + to-regex-range@5.0.1: 2155 + resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} 2156 + engines: {node: '>=8.0'} 2157 + 2158 + tr46@1.0.1: 2159 + resolution: {integrity: sha512-dTpowEjclQ7Kgx5SdBkqRzVhERQXov8/l9Ft9dVM9fmg0W0KQSVaXX9T4i6twCPNtYiZM53lpSSUAwJbFPOHxA==} 2160 + 2161 + ts-interface-checker@0.1.13: 2162 + resolution: {integrity: sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==} 2163 + 2164 + type-fest@0.16.0: 2165 + resolution: {integrity: sha512-eaBzG6MxNzEn9kiwvtre90cXaNLkmadMWa1zQMs3XORCXNbsH/OewwbxC5ia9dCxIxnTAsSxXJaa/p5y8DlvJg==} 2166 + engines: {node: '>=10'} 2167 + 2168 + typed-array-buffer@1.0.2: 2169 + resolution: {integrity: sha512-gEymJYKZtKXzzBzM4jqa9w6Q1Jjm7x2d+sh19AdsD4wqnMPDYyvwpsIc2Q/835kHuo3BEQ7CjelGhfTsoBb2MQ==} 2170 + engines: {node: '>= 0.4'} 2171 + 2172 + typed-array-byte-length@1.0.1: 2173 + resolution: {integrity: sha512-3iMJ9q0ao7WE9tWcaYKIptkNBuOIcZCCT0d4MRvuuH88fEoEH62IuQe0OtraD3ebQEoTRk8XCBoknUNc1Y67pw==} 2174 + engines: {node: '>= 0.4'} 2175 + 2176 + typed-array-byte-offset@1.0.2: 2177 + resolution: {integrity: sha512-Ous0vodHa56FviZucS2E63zkgtgrACj7omjwd/8lTEMEPFFyjfixMZ1ZXenpgCFBBt4EC1J2XsyVS2gkG0eTFA==} 2178 + engines: {node: '>= 0.4'} 2179 + 2180 + typed-array-length@1.0.6: 2181 + resolution: {integrity: sha512-/OxDN6OtAk5KBpGb28T+HZc2M+ADtvRxXrKKbUwtsLgdoxgX13hyy7ek6bFRl5+aBs2yZzB0c4CnQfAtVypW/g==} 2182 + engines: {node: '>= 0.4'} 2183 + 2184 + typescript@5.4.5: 2185 + resolution: {integrity: sha512-vcI4UpRgg81oIRUFwR0WSIHKt11nJ7SAVlYNIu+QpqeyXP+gpQJy/Z4+F0aGxSE4MqwjyXvW/TzgkLAx2AGHwQ==} 2186 + engines: {node: '>=14.17'} 2187 + hasBin: true 2188 + 2189 + unbox-primitive@1.0.2: 2190 + resolution: {integrity: sha512-61pPlCD9h51VoreyJ0BReideM3MDKMKnh6+V9L08331ipq6Q8OFXZYiqP6n/tbHx4s5I9uRhcye6BrbkizkBDw==} 2191 + 2192 + unicode-canonical-property-names-ecmascript@2.0.0: 2193 + resolution: {integrity: sha512-yY5PpDlfVIU5+y/BSCxAJRBIS1Zc2dDG3Ujq+sR0U+JjUevW2JhocOF+soROYDSaAezOzOKuyyixhD6mBknSmQ==} 2194 + engines: {node: '>=4'} 2195 + 2196 + unicode-match-property-ecmascript@2.0.0: 2197 + resolution: {integrity: sha512-5kaZCrbp5mmbz5ulBkDkbY0SsPOjKqVS35VpL9ulMPfSl0J0Xsm+9Evphv9CoIZFwre7aJoa94AY6seMKGVN5Q==} 2198 + engines: {node: '>=4'} 2199 + 2200 + unicode-match-property-value-ecmascript@2.1.0: 2201 + resolution: {integrity: sha512-qxkjQt6qjg/mYscYMC0XKRn3Rh0wFPlfxB0xkt9CfyTvpX1Ra0+rAmdX2QyAobptSEvuy4RtpPRui6XkV+8wjA==} 2202 + engines: {node: '>=4'} 2203 + 2204 + unicode-property-aliases-ecmascript@2.1.0: 2205 + resolution: {integrity: sha512-6t3foTQI9qne+OZoVQB/8x8rk2k1eVy1gRXhV3oFQ5T6R1dqQ1xtin3XqSlx3+ATBkliTaR/hHyJBm+LVPNM8w==} 2206 + engines: {node: '>=4'} 2207 + 2208 + unique-string@2.0.0: 2209 + resolution: {integrity: sha512-uNaeirEPvpZWSgzwsPGtU2zVSTrn/8L5q/IexZmH0eH6SA73CmAA5U4GwORTxQAZs95TAXLNqeLoPPNO5gZfWg==} 2210 + engines: {node: '>=8'} 2211 + 2212 + universalify@2.0.1: 2213 + resolution: {integrity: sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==} 2214 + engines: {node: '>= 10.0.0'} 2215 + 2216 + upath@1.2.0: 2217 + resolution: {integrity: sha512-aZwGpamFO61g3OlfT7OQCHqhGnW43ieH9WZeP7QxN/G/jS4jfqUkZxoryvJgVPEcrl5NL/ggHsSmLMHuH64Lhg==} 2218 + engines: {node: '>=4'} 2219 + 2220 + update-browserslist-db@1.0.16: 2221 + resolution: {integrity: sha512-KVbTxlBYlckhF5wgfyZXTWnMn7MMZjMu9XG8bPlliUOP9ThaF4QnhP8qrjrH7DRzHfSk0oQv1wToW+iA5GajEQ==} 2222 + hasBin: true 2223 + peerDependencies: 2224 + browserslist: '>= 4.21.0' 2225 + 2226 + uri-js@4.4.1: 2227 + resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} 2228 + 2229 + util-deprecate@1.0.2: 2230 + resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} 2231 + 2232 + validate-html-nesting@1.2.2: 2233 + resolution: {integrity: sha512-hGdgQozCsQJMyfK5urgFcWEqsSSrK63Awe0t/IMR0bZ0QMtnuaiHzThW81guu3qx9abLi99NEuiaN6P9gVYsNg==} 2234 + 2235 + vite-plugin-pwa@0.17.4: 2236 + resolution: {integrity: sha512-j9iiyinFOYyof4Zk3Q+DtmYyDVBDAi6PuMGNGq6uGI0pw7E+LNm9e+nQ2ep9obMP/kjdWwzilqUrlfVRj9OobA==} 2237 + engines: {node: '>=16.0.0'} 2238 + peerDependencies: 2239 + vite: ^3.1.0 || ^4.0.0 || ^5.0.0 2240 + 2241 + vite-plugin-solid@2.10.2: 2242 + resolution: {integrity: sha512-AOEtwMe2baBSXMXdo+BUwECC8IFHcKS6WQV/1NEd+Q7vHPap5fmIhLcAzr+DUJ04/KHx/1UBU0l1/GWP+rMAPQ==} 2243 + peerDependencies: 2244 + '@testing-library/jest-dom': ^5.16.6 || ^5.17.0 || ^6.* 2245 + solid-js: ^1.7.2 2246 + vite: ^3.0.0 || ^4.0.0 || ^5.0.0 2247 + peerDependenciesMeta: 2248 + '@testing-library/jest-dom': 2249 + optional: true 2250 + 2251 + vite@5.2.11: 2252 + resolution: {integrity: sha512-HndV31LWW05i1BLPMUCE1B9E9GFbOu1MbenhS58FuK6owSO5qHm7GiCotrNY1YE5rMeQSFBGmT5ZaLEjFizgiQ==} 2253 + engines: {node: ^18.0.0 || >=20.0.0} 2254 + hasBin: true 2255 + peerDependencies: 2256 + '@types/node': ^18.0.0 || >=20.0.0 2257 + less: '*' 2258 + lightningcss: ^1.21.0 2259 + sass: '*' 2260 + stylus: '*' 2261 + sugarss: '*' 2262 + terser: ^5.4.0 2263 + peerDependenciesMeta: 2264 + '@types/node': 2265 + optional: true 2266 + less: 2267 + optional: true 2268 + lightningcss: 2269 + optional: true 2270 + sass: 2271 + optional: true 2272 + stylus: 2273 + optional: true 2274 + sugarss: 2275 + optional: true 2276 + terser: 2277 + optional: true 2278 + 2279 + vitefu@0.2.5: 2280 + resolution: {integrity: sha512-SgHtMLoqaeeGnd2evZ849ZbACbnwQCIwRH57t18FxcXoZop0uQu0uzlIhJBlF/eWVzuce0sHeqPcDo+evVcg8Q==} 2281 + peerDependencies: 2282 + vite: ^3.0.0 || ^4.0.0 || ^5.0.0 2283 + peerDependenciesMeta: 2284 + vite: 2285 + optional: true 2286 + 2287 + webidl-conversions@4.0.2: 2288 + resolution: {integrity: sha512-YQ+BmxuTgd6UXZW3+ICGfyqRyHXVlD5GtQr5+qjiNW7bF0cqrzX500HVXPBOvgXb5YnzDd+h0zqyv61KUD7+Sg==} 2289 + 2290 + whatwg-url@7.1.0: 2291 + resolution: {integrity: sha512-WUu7Rg1DroM7oQvGWfOiAK21n74Gg+T4elXEQYkOhtyLeWiJFoOGLXPKI/9gzIie9CtwVLm8wtw6YJdKyxSjeg==} 2292 + 2293 + which-boxed-primitive@1.0.2: 2294 + resolution: {integrity: sha512-bwZdv0AKLpplFY2KZRX6TvyuN7ojjr7lwkg6ml0roIy9YeuSr7JS372qlNW18UQYzgYK9ziGcerWqZOmEn9VNg==} 2295 + 2296 + which-typed-array@1.1.15: 2297 + resolution: {integrity: sha512-oV0jmFtUky6CXfkqehVvBP/LSWJ2sy4vWMioiENyJLePrBO/yKyV9OyJySfAKosh+RYkIl5zJCNZ8/4JncrpdA==} 2298 + engines: {node: '>= 0.4'} 2299 + 2300 + which@2.0.2: 2301 + resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} 2302 + engines: {node: '>= 8'} 2303 + hasBin: true 2304 + 2305 + workbox-background-sync@7.1.0: 2306 + resolution: {integrity: sha512-rMbgrzueVWDFcEq1610YyDW71z0oAXLfdRHRQcKw4SGihkfOK0JUEvqWHFwA6rJ+6TClnMIn7KQI5PNN1XQXwQ==} 2307 + 2308 + workbox-broadcast-update@7.1.0: 2309 + resolution: {integrity: sha512-O36hIfhjej/c5ar95pO67k1GQw0/bw5tKP7CERNgK+JdxBANQhDmIuOXZTNvwb2IHBx9hj2kxvcDyRIh5nzOgQ==} 2310 + 2311 + workbox-build@7.1.0: 2312 + resolution: {integrity: sha512-F6R94XAxjB2j4ETMkP1EXKfjECOtDmyvt0vz3BzgWJMI68TNSXIVNkgatwUKBlPGOfy9n2F/4voYRNAhEvPJNg==} 2313 + engines: {node: '>=16.0.0'} 2314 + 2315 + workbox-cacheable-response@7.1.0: 2316 + resolution: {integrity: sha512-iwsLBll8Hvua3xCuBB9h92+/e0wdsmSVgR2ZlvcfjepZWwhd3osumQB3x9o7flj+FehtWM2VHbZn8UJeBXXo6Q==} 2317 + 2318 + workbox-core@7.1.0: 2319 + resolution: {integrity: sha512-5KB4KOY8rtL31nEF7BfvU7FMzKT4B5TkbYa2tzkS+Peqj0gayMT9SytSFtNzlrvMaWgv6y/yvP9C0IbpFjV30Q==} 2320 + 2321 + workbox-expiration@7.1.0: 2322 + resolution: {integrity: sha512-m5DcMY+A63rJlPTbbBNtpJ20i3enkyOtSgYfv/l8h+D6YbbNiA0zKEkCUaMsdDlxggla1oOfRkyqTvl5Ni5KQQ==} 2323 + 2324 + workbox-google-analytics@7.1.0: 2325 + resolution: {integrity: sha512-FvE53kBQHfVTcZyczeBVRexhh7JTkyQ8HAvbVY6mXd2n2A7Oyz/9fIwnY406ZcDhvE4NFfKGjW56N4gBiqkrew==} 2326 + 2327 + workbox-navigation-preload@7.1.0: 2328 + resolution: {integrity: sha512-4wyAbo0vNI/X0uWNJhCMKxnPanNyhybsReMGN9QUpaePLTiDpKxPqFxl4oUmBNddPwIXug01eTSLVIFXimRG/A==} 2329 + 2330 + workbox-precaching@7.1.0: 2331 + resolution: {integrity: sha512-LyxzQts+UEpgtmfnolo0hHdNjoB7EoRWcF7EDslt+lQGd0lW4iTvvSe3v5JiIckQSB5KTW5xiCqjFviRKPj1zA==} 2332 + 2333 + workbox-range-requests@7.1.0: 2334 + resolution: {integrity: sha512-m7+O4EHolNs5yb/79CrnwPR/g/PRzMFYEdo01LqwixVnc/sbzNSvKz0d04OE3aMRel1CwAAZQheRsqGDwATgPQ==} 2335 + 2336 + workbox-recipes@7.1.0: 2337 + resolution: {integrity: sha512-NRrk4ycFN9BHXJB6WrKiRX3W3w75YNrNrzSX9cEZgFB5ubeGoO8s/SDmOYVrFYp9HMw6sh1Pm3eAY/1gVS8YLg==} 2338 + 2339 + workbox-routing@7.1.0: 2340 + resolution: {integrity: sha512-oOYk+kLriUY2QyHkIilxUlVcFqwduLJB7oRZIENbqPGeBP/3TWHYNNdmGNhz1dvKuw7aqvJ7CQxn27/jprlTdg==} 2341 + 2342 + workbox-strategies@7.1.0: 2343 + resolution: {integrity: sha512-/UracPiGhUNehGjRm/tLUQ+9PtWmCbRufWtV0tNrALuf+HZ4F7cmObSEK+E4/Bx1p8Syx2tM+pkIrvtyetdlew==} 2344 + 2345 + workbox-streams@7.1.0: 2346 + resolution: {integrity: sha512-WyHAVxRXBMfysM8ORwiZnI98wvGWTVAq/lOyBjf00pXFvG0mNaVz4Ji+u+fKa/mf1i2SnTfikoYKto4ihHeS6w==} 2347 + 2348 + workbox-sw@7.1.0: 2349 + resolution: {integrity: sha512-Hml/9+/njUXBglv3dtZ9WBKHI235AQJyLBV1G7EFmh4/mUdSQuXui80RtjDeVRrXnm/6QWgRUEHG3/YBVbxtsA==} 2350 + 2351 + workbox-window@7.1.0: 2352 + resolution: {integrity: sha512-ZHeROyqR+AS5UPzholQRDttLFqGMwP0Np8MKWAdyxsDETxq3qOAyXvqessc3GniohG6e0mAqSQyKOHmT8zPF7g==} 2353 + 2354 + wrap-ansi@7.0.0: 2355 + resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==} 2356 + engines: {node: '>=10'} 2357 + 2358 + wrap-ansi@8.1.0: 2359 + resolution: {integrity: sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==} 2360 + engines: {node: '>=12'} 2361 + 2362 + wrappy@1.0.2: 2363 + resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} 2364 + 2365 + yallist@3.1.1: 2366 + resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==} 2367 + 2368 + yaml@2.4.3: 2369 + resolution: {integrity: sha512-sntgmxj8o7DE7g/Qi60cqpLBA3HG3STcDA0kO+WfB05jEKhZMbY7umNm2rBpQvsmZ16/lPXCJGW2672dgOUkrg==} 2370 + engines: {node: '>= 14'} 2371 + hasBin: true 2372 + 2373 + snapshots: 2374 + 2375 + '@alloc/quick-lru@5.2.0': {} 2376 + 2377 + '@ampproject/remapping@2.3.0': 2378 + dependencies: 2379 + '@jridgewell/gen-mapping': 0.3.5 2380 + '@jridgewell/trace-mapping': 0.3.25 2381 + 2382 + '@apideck/better-ajv-errors@0.3.6(ajv@8.14.0)': 2383 + dependencies: 2384 + ajv: 8.14.0 2385 + json-schema: 0.4.0 2386 + jsonpointer: 5.0.1 2387 + leven: 3.1.0 2388 + 2389 + '@babel/code-frame@7.24.6': 2390 + dependencies: 2391 + '@babel/highlight': 7.24.6 2392 + picocolors: 1.0.1 2393 + 2394 + '@babel/compat-data@7.24.6': {} 2395 + 2396 + '@babel/core@7.24.6': 2397 + dependencies: 2398 + '@ampproject/remapping': 2.3.0 2399 + '@babel/code-frame': 7.24.6 2400 + '@babel/generator': 7.24.6 2401 + '@babel/helper-compilation-targets': 7.24.6 2402 + '@babel/helper-module-transforms': 7.24.6(@babel/core@7.24.6) 2403 + '@babel/helpers': 7.24.6 2404 + '@babel/parser': 7.24.6 2405 + '@babel/template': 7.24.6 2406 + '@babel/traverse': 7.24.6 2407 + '@babel/types': 7.24.6 2408 + convert-source-map: 2.0.0 2409 + debug: 4.3.4 2410 + gensync: 1.0.0-beta.2 2411 + json5: 2.2.3 2412 + semver: 6.3.1 2413 + transitivePeerDependencies: 2414 + - supports-color 2415 + 2416 + '@babel/generator@7.24.6': 2417 + dependencies: 2418 + '@babel/types': 7.24.6 2419 + '@jridgewell/gen-mapping': 0.3.5 2420 + '@jridgewell/trace-mapping': 0.3.25 2421 + jsesc: 2.5.2 2422 + 2423 + '@babel/helper-annotate-as-pure@7.24.6': 2424 + dependencies: 2425 + '@babel/types': 7.24.6 2426 + 2427 + '@babel/helper-builder-binary-assignment-operator-visitor@7.24.6': 2428 + dependencies: 2429 + '@babel/types': 7.24.6 2430 + 2431 + '@babel/helper-compilation-targets@7.24.6': 2432 + dependencies: 2433 + '@babel/compat-data': 7.24.6 2434 + '@babel/helper-validator-option': 7.24.6 2435 + browserslist: 4.23.0 2436 + lru-cache: 5.1.1 2437 + semver: 6.3.1 2438 + 2439 + '@babel/helper-create-class-features-plugin@7.24.6(@babel/core@7.24.6)': 2440 + dependencies: 2441 + '@babel/core': 7.24.6 2442 + '@babel/helper-annotate-as-pure': 7.24.6 2443 + '@babel/helper-environment-visitor': 7.24.6 2444 + '@babel/helper-function-name': 7.24.6 2445 + '@babel/helper-member-expression-to-functions': 7.24.6 2446 + '@babel/helper-optimise-call-expression': 7.24.6 2447 + '@babel/helper-replace-supers': 7.24.6(@babel/core@7.24.6) 2448 + '@babel/helper-skip-transparent-expression-wrappers': 7.24.6 2449 + '@babel/helper-split-export-declaration': 7.24.6 2450 + semver: 6.3.1 2451 + 2452 + '@babel/helper-create-regexp-features-plugin@7.24.6(@babel/core@7.24.6)': 2453 + dependencies: 2454 + '@babel/core': 7.24.6 2455 + '@babel/helper-annotate-as-pure': 7.24.6 2456 + regexpu-core: 5.3.2 2457 + semver: 6.3.1 2458 + 2459 + '@babel/helper-define-polyfill-provider@0.6.2(@babel/core@7.24.6)': 2460 + dependencies: 2461 + '@babel/core': 7.24.6 2462 + '@babel/helper-compilation-targets': 7.24.6 2463 + '@babel/helper-plugin-utils': 7.24.6 2464 + debug: 4.3.4 2465 + lodash.debounce: 4.0.8 2466 + resolve: 1.22.8 2467 + transitivePeerDependencies: 2468 + - supports-color 2469 + 2470 + '@babel/helper-environment-visitor@7.24.6': {} 2471 + 2472 + '@babel/helper-function-name@7.24.6': 2473 + dependencies: 2474 + '@babel/template': 7.24.6 2475 + '@babel/types': 7.24.6 2476 + 2477 + '@babel/helper-hoist-variables@7.24.6': 2478 + dependencies: 2479 + '@babel/types': 7.24.6 2480 + 2481 + '@babel/helper-member-expression-to-functions@7.24.6': 2482 + dependencies: 2483 + '@babel/types': 7.24.6 2484 + 2485 + '@babel/helper-module-imports@7.18.6': 2486 + dependencies: 2487 + '@babel/types': 7.24.6 2488 + 2489 + '@babel/helper-module-imports@7.24.6': 2490 + dependencies: 2491 + '@babel/types': 7.24.6 2492 + 2493 + '@babel/helper-module-transforms@7.24.6(@babel/core@7.24.6)': 2494 + dependencies: 2495 + '@babel/core': 7.24.6 2496 + '@babel/helper-environment-visitor': 7.24.6 2497 + '@babel/helper-module-imports': 7.24.6 2498 + '@babel/helper-simple-access': 7.24.6 2499 + '@babel/helper-split-export-declaration': 7.24.6 2500 + '@babel/helper-validator-identifier': 7.24.6 2501 + 2502 + '@babel/helper-optimise-call-expression@7.24.6': 2503 + dependencies: 2504 + '@babel/types': 7.24.6 2505 + 2506 + '@babel/helper-plugin-utils@7.24.6': {} 2507 + 2508 + '@babel/helper-remap-async-to-generator@7.24.6(@babel/core@7.24.6)': 2509 + dependencies: 2510 + '@babel/core': 7.24.6 2511 + '@babel/helper-annotate-as-pure': 7.24.6 2512 + '@babel/helper-environment-visitor': 7.24.6 2513 + '@babel/helper-wrap-function': 7.24.6 2514 + 2515 + '@babel/helper-replace-supers@7.24.6(@babel/core@7.24.6)': 2516 + dependencies: 2517 + '@babel/core': 7.24.6 2518 + '@babel/helper-environment-visitor': 7.24.6 2519 + '@babel/helper-member-expression-to-functions': 7.24.6 2520 + '@babel/helper-optimise-call-expression': 7.24.6 2521 + 2522 + '@babel/helper-simple-access@7.24.6': 2523 + dependencies: 2524 + '@babel/types': 7.24.6 2525 + 2526 + '@babel/helper-skip-transparent-expression-wrappers@7.24.6': 2527 + dependencies: 2528 + '@babel/types': 7.24.6 2529 + 2530 + '@babel/helper-split-export-declaration@7.24.6': 2531 + dependencies: 2532 + '@babel/types': 7.24.6 2533 + 2534 + '@babel/helper-string-parser@7.24.6': {} 2535 + 2536 + '@babel/helper-validator-identifier@7.24.6': {} 2537 + 2538 + '@babel/helper-validator-option@7.24.6': {} 2539 + 2540 + '@babel/helper-wrap-function@7.24.6': 2541 + dependencies: 2542 + '@babel/helper-function-name': 7.24.6 2543 + '@babel/template': 7.24.6 2544 + '@babel/types': 7.24.6 2545 + 2546 + '@babel/helpers@7.24.6': 2547 + dependencies: 2548 + '@babel/template': 7.24.6 2549 + '@babel/types': 7.24.6 2550 + 2551 + '@babel/highlight@7.24.6': 2552 + dependencies: 2553 + '@babel/helper-validator-identifier': 7.24.6 2554 + chalk: 2.4.2 2555 + js-tokens: 4.0.0 2556 + picocolors: 1.0.1 2557 + 2558 + '@babel/parser@7.24.6': 2559 + dependencies: 2560 + '@babel/types': 7.24.6 2561 + 2562 + '@babel/plugin-bugfix-firefox-class-in-computed-class-key@7.24.6(@babel/core@7.24.6)': 2563 + dependencies: 2564 + '@babel/core': 7.24.6 2565 + '@babel/helper-environment-visitor': 7.24.6 2566 + '@babel/helper-plugin-utils': 7.24.6 2567 + 2568 + '@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression@7.24.6(@babel/core@7.24.6)': 2569 + dependencies: 2570 + '@babel/core': 7.24.6 2571 + '@babel/helper-plugin-utils': 7.24.6 2572 + 2573 + '@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining@7.24.6(@babel/core@7.24.6)': 2574 + dependencies: 2575 + '@babel/core': 7.24.6 2576 + '@babel/helper-plugin-utils': 7.24.6 2577 + '@babel/helper-skip-transparent-expression-wrappers': 7.24.6 2578 + '@babel/plugin-transform-optional-chaining': 7.24.6(@babel/core@7.24.6) 2579 + 2580 + '@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly@7.24.6(@babel/core@7.24.6)': 2581 + dependencies: 2582 + '@babel/core': 7.24.6 2583 + '@babel/helper-environment-visitor': 7.24.6 2584 + '@babel/helper-plugin-utils': 7.24.6 2585 + 2586 + '@babel/plugin-proposal-private-property-in-object@7.21.0-placeholder-for-preset-env.2(@babel/core@7.24.6)': 2587 + dependencies: 2588 + '@babel/core': 7.24.6 2589 + 2590 + '@babel/plugin-syntax-async-generators@7.8.4(@babel/core@7.24.6)': 2591 + dependencies: 2592 + '@babel/core': 7.24.6 2593 + '@babel/helper-plugin-utils': 7.24.6 2594 + 2595 + '@babel/plugin-syntax-class-properties@7.12.13(@babel/core@7.24.6)': 2596 + dependencies: 2597 + '@babel/core': 7.24.6 2598 + '@babel/helper-plugin-utils': 7.24.6 2599 + 2600 + '@babel/plugin-syntax-class-static-block@7.14.5(@babel/core@7.24.6)': 2601 + dependencies: 2602 + '@babel/core': 7.24.6 2603 + '@babel/helper-plugin-utils': 7.24.6 2604 + 2605 + '@babel/plugin-syntax-dynamic-import@7.8.3(@babel/core@7.24.6)': 2606 + dependencies: 2607 + '@babel/core': 7.24.6 2608 + '@babel/helper-plugin-utils': 7.24.6 2609 + 2610 + '@babel/plugin-syntax-export-namespace-from@7.8.3(@babel/core@7.24.6)': 2611 + dependencies: 2612 + '@babel/core': 7.24.6 2613 + '@babel/helper-plugin-utils': 7.24.6 2614 + 2615 + '@babel/plugin-syntax-import-assertions@7.24.6(@babel/core@7.24.6)': 2616 + dependencies: 2617 + '@babel/core': 7.24.6 2618 + '@babel/helper-plugin-utils': 7.24.6 2619 + 2620 + '@babel/plugin-syntax-import-attributes@7.24.6(@babel/core@7.24.6)': 2621 + dependencies: 2622 + '@babel/core': 7.24.6 2623 + '@babel/helper-plugin-utils': 7.24.6 2624 + 2625 + '@babel/plugin-syntax-import-meta@7.10.4(@babel/core@7.24.6)': 2626 + dependencies: 2627 + '@babel/core': 7.24.6 2628 + '@babel/helper-plugin-utils': 7.24.6 2629 + 2630 + '@babel/plugin-syntax-json-strings@7.8.3(@babel/core@7.24.6)': 2631 + dependencies: 2632 + '@babel/core': 7.24.6 2633 + '@babel/helper-plugin-utils': 7.24.6 2634 + 2635 + '@babel/plugin-syntax-jsx@7.24.6(@babel/core@7.24.6)': 2636 + dependencies: 2637 + '@babel/core': 7.24.6 2638 + '@babel/helper-plugin-utils': 7.24.6 2639 + 2640 + '@babel/plugin-syntax-logical-assignment-operators@7.10.4(@babel/core@7.24.6)': 2641 + dependencies: 2642 + '@babel/core': 7.24.6 2643 + '@babel/helper-plugin-utils': 7.24.6 2644 + 2645 + '@babel/plugin-syntax-nullish-coalescing-operator@7.8.3(@babel/core@7.24.6)': 2646 + dependencies: 2647 + '@babel/core': 7.24.6 2648 + '@babel/helper-plugin-utils': 7.24.6 2649 + 2650 + '@babel/plugin-syntax-numeric-separator@7.10.4(@babel/core@7.24.6)': 2651 + dependencies: 2652 + '@babel/core': 7.24.6 2653 + '@babel/helper-plugin-utils': 7.24.6 2654 + 2655 + '@babel/plugin-syntax-object-rest-spread@7.8.3(@babel/core@7.24.6)': 2656 + dependencies: 2657 + '@babel/core': 7.24.6 2658 + '@babel/helper-plugin-utils': 7.24.6 2659 + 2660 + '@babel/plugin-syntax-optional-catch-binding@7.8.3(@babel/core@7.24.6)': 2661 + dependencies: 2662 + '@babel/core': 7.24.6 2663 + '@babel/helper-plugin-utils': 7.24.6 2664 + 2665 + '@babel/plugin-syntax-optional-chaining@7.8.3(@babel/core@7.24.6)': 2666 + dependencies: 2667 + '@babel/core': 7.24.6 2668 + '@babel/helper-plugin-utils': 7.24.6 2669 + 2670 + '@babel/plugin-syntax-private-property-in-object@7.14.5(@babel/core@7.24.6)': 2671 + dependencies: 2672 + '@babel/core': 7.24.6 2673 + '@babel/helper-plugin-utils': 7.24.6 2674 + 2675 + '@babel/plugin-syntax-top-level-await@7.14.5(@babel/core@7.24.6)': 2676 + dependencies: 2677 + '@babel/core': 7.24.6 2678 + '@babel/helper-plugin-utils': 7.24.6 2679 + 2680 + '@babel/plugin-syntax-unicode-sets-regex@7.18.6(@babel/core@7.24.6)': 2681 + dependencies: 2682 + '@babel/core': 7.24.6 2683 + '@babel/helper-create-regexp-features-plugin': 7.24.6(@babel/core@7.24.6) 2684 + '@babel/helper-plugin-utils': 7.24.6 2685 + 2686 + '@babel/plugin-transform-arrow-functions@7.24.6(@babel/core@7.24.6)': 2687 + dependencies: 2688 + '@babel/core': 7.24.6 2689 + '@babel/helper-plugin-utils': 7.24.6 2690 + 2691 + '@babel/plugin-transform-async-generator-functions@7.24.6(@babel/core@7.24.6)': 2692 + dependencies: 2693 + '@babel/core': 7.24.6 2694 + '@babel/helper-environment-visitor': 7.24.6 2695 + '@babel/helper-plugin-utils': 7.24.6 2696 + '@babel/helper-remap-async-to-generator': 7.24.6(@babel/core@7.24.6) 2697 + '@babel/plugin-syntax-async-generators': 7.8.4(@babel/core@7.24.6) 2698 + 2699 + '@babel/plugin-transform-async-to-generator@7.24.6(@babel/core@7.24.6)': 2700 + dependencies: 2701 + '@babel/core': 7.24.6 2702 + '@babel/helper-module-imports': 7.24.6 2703 + '@babel/helper-plugin-utils': 7.24.6 2704 + '@babel/helper-remap-async-to-generator': 7.24.6(@babel/core@7.24.6) 2705 + 2706 + '@babel/plugin-transform-block-scoped-functions@7.24.6(@babel/core@7.24.6)': 2707 + dependencies: 2708 + '@babel/core': 7.24.6 2709 + '@babel/helper-plugin-utils': 7.24.6 2710 + 2711 + '@babel/plugin-transform-block-scoping@7.24.6(@babel/core@7.24.6)': 2712 + dependencies: 2713 + '@babel/core': 7.24.6 2714 + '@babel/helper-plugin-utils': 7.24.6 2715 + 2716 + '@babel/plugin-transform-class-properties@7.24.6(@babel/core@7.24.6)': 2717 + dependencies: 2718 + '@babel/core': 7.24.6 2719 + '@babel/helper-create-class-features-plugin': 7.24.6(@babel/core@7.24.6) 2720 + '@babel/helper-plugin-utils': 7.24.6 2721 + 2722 + '@babel/plugin-transform-class-static-block@7.24.6(@babel/core@7.24.6)': 2723 + dependencies: 2724 + '@babel/core': 7.24.6 2725 + '@babel/helper-create-class-features-plugin': 7.24.6(@babel/core@7.24.6) 2726 + '@babel/helper-plugin-utils': 7.24.6 2727 + '@babel/plugin-syntax-class-static-block': 7.14.5(@babel/core@7.24.6) 2728 + 2729 + '@babel/plugin-transform-classes@7.24.6(@babel/core@7.24.6)': 2730 + dependencies: 2731 + '@babel/core': 7.24.6 2732 + '@babel/helper-annotate-as-pure': 7.24.6 2733 + '@babel/helper-compilation-targets': 7.24.6 2734 + '@babel/helper-environment-visitor': 7.24.6 2735 + '@babel/helper-function-name': 7.24.6 2736 + '@babel/helper-plugin-utils': 7.24.6 2737 + '@babel/helper-replace-supers': 7.24.6(@babel/core@7.24.6) 2738 + '@babel/helper-split-export-declaration': 7.24.6 2739 + globals: 11.12.0 2740 + 2741 + '@babel/plugin-transform-computed-properties@7.24.6(@babel/core@7.24.6)': 2742 + dependencies: 2743 + '@babel/core': 7.24.6 2744 + '@babel/helper-plugin-utils': 7.24.6 2745 + '@babel/template': 7.24.6 2746 + 2747 + '@babel/plugin-transform-destructuring@7.24.6(@babel/core@7.24.6)': 2748 + dependencies: 2749 + '@babel/core': 7.24.6 2750 + '@babel/helper-plugin-utils': 7.24.6 2751 + 2752 + '@babel/plugin-transform-dotall-regex@7.24.6(@babel/core@7.24.6)': 2753 + dependencies: 2754 + '@babel/core': 7.24.6 2755 + '@babel/helper-create-regexp-features-plugin': 7.24.6(@babel/core@7.24.6) 2756 + '@babel/helper-plugin-utils': 7.24.6 2757 + 2758 + '@babel/plugin-transform-duplicate-keys@7.24.6(@babel/core@7.24.6)': 2759 + dependencies: 2760 + '@babel/core': 7.24.6 2761 + '@babel/helper-plugin-utils': 7.24.6 2762 + 2763 + '@babel/plugin-transform-dynamic-import@7.24.6(@babel/core@7.24.6)': 2764 + dependencies: 2765 + '@babel/core': 7.24.6 2766 + '@babel/helper-plugin-utils': 7.24.6 2767 + '@babel/plugin-syntax-dynamic-import': 7.8.3(@babel/core@7.24.6) 2768 + 2769 + '@babel/plugin-transform-exponentiation-operator@7.24.6(@babel/core@7.24.6)': 2770 + dependencies: 2771 + '@babel/core': 7.24.6 2772 + '@babel/helper-builder-binary-assignment-operator-visitor': 7.24.6 2773 + '@babel/helper-plugin-utils': 7.24.6 2774 + 2775 + '@babel/plugin-transform-export-namespace-from@7.24.6(@babel/core@7.24.6)': 2776 + dependencies: 2777 + '@babel/core': 7.24.6 2778 + '@babel/helper-plugin-utils': 7.24.6 2779 + '@babel/plugin-syntax-export-namespace-from': 7.8.3(@babel/core@7.24.6) 2780 + 2781 + '@babel/plugin-transform-for-of@7.24.6(@babel/core@7.24.6)': 2782 + dependencies: 2783 + '@babel/core': 7.24.6 2784 + '@babel/helper-plugin-utils': 7.24.6 2785 + '@babel/helper-skip-transparent-expression-wrappers': 7.24.6 2786 + 2787 + '@babel/plugin-transform-function-name@7.24.6(@babel/core@7.24.6)': 2788 + dependencies: 2789 + '@babel/core': 7.24.6 2790 + '@babel/helper-compilation-targets': 7.24.6 2791 + '@babel/helper-function-name': 7.24.6 2792 + '@babel/helper-plugin-utils': 7.24.6 2793 + 2794 + '@babel/plugin-transform-json-strings@7.24.6(@babel/core@7.24.6)': 2795 + dependencies: 2796 + '@babel/core': 7.24.6 2797 + '@babel/helper-plugin-utils': 7.24.6 2798 + '@babel/plugin-syntax-json-strings': 7.8.3(@babel/core@7.24.6) 2799 + 2800 + '@babel/plugin-transform-literals@7.24.6(@babel/core@7.24.6)': 2801 + dependencies: 2802 + '@babel/core': 7.24.6 2803 + '@babel/helper-plugin-utils': 7.24.6 2804 + 2805 + '@babel/plugin-transform-logical-assignment-operators@7.24.6(@babel/core@7.24.6)': 2806 + dependencies: 2807 + '@babel/core': 7.24.6 2808 + '@babel/helper-plugin-utils': 7.24.6 2809 + '@babel/plugin-syntax-logical-assignment-operators': 7.10.4(@babel/core@7.24.6) 2810 + 2811 + '@babel/plugin-transform-member-expression-literals@7.24.6(@babel/core@7.24.6)': 2812 + dependencies: 2813 + '@babel/core': 7.24.6 2814 + '@babel/helper-plugin-utils': 7.24.6 2815 + 2816 + '@babel/plugin-transform-modules-amd@7.24.6(@babel/core@7.24.6)': 2817 + dependencies: 2818 + '@babel/core': 7.24.6 2819 + '@babel/helper-module-transforms': 7.24.6(@babel/core@7.24.6) 2820 + '@babel/helper-plugin-utils': 7.24.6 2821 + 2822 + '@babel/plugin-transform-modules-commonjs@7.24.6(@babel/core@7.24.6)': 2823 + dependencies: 2824 + '@babel/core': 7.24.6 2825 + '@babel/helper-module-transforms': 7.24.6(@babel/core@7.24.6) 2826 + '@babel/helper-plugin-utils': 7.24.6 2827 + '@babel/helper-simple-access': 7.24.6 2828 + 2829 + '@babel/plugin-transform-modules-systemjs@7.24.6(@babel/core@7.24.6)': 2830 + dependencies: 2831 + '@babel/core': 7.24.6 2832 + '@babel/helper-hoist-variables': 7.24.6 2833 + '@babel/helper-module-transforms': 7.24.6(@babel/core@7.24.6) 2834 + '@babel/helper-plugin-utils': 7.24.6 2835 + '@babel/helper-validator-identifier': 7.24.6 2836 + 2837 + '@babel/plugin-transform-modules-umd@7.24.6(@babel/core@7.24.6)': 2838 + dependencies: 2839 + '@babel/core': 7.24.6 2840 + '@babel/helper-module-transforms': 7.24.6(@babel/core@7.24.6) 2841 + '@babel/helper-plugin-utils': 7.24.6 2842 + 2843 + '@babel/plugin-transform-named-capturing-groups-regex@7.24.6(@babel/core@7.24.6)': 2844 + dependencies: 2845 + '@babel/core': 7.24.6 2846 + '@babel/helper-create-regexp-features-plugin': 7.24.6(@babel/core@7.24.6) 2847 + '@babel/helper-plugin-utils': 7.24.6 2848 + 2849 + '@babel/plugin-transform-new-target@7.24.6(@babel/core@7.24.6)': 2850 + dependencies: 2851 + '@babel/core': 7.24.6 2852 + '@babel/helper-plugin-utils': 7.24.6 2853 + 2854 + '@babel/plugin-transform-nullish-coalescing-operator@7.24.6(@babel/core@7.24.6)': 2855 + dependencies: 2856 + '@babel/core': 7.24.6 2857 + '@babel/helper-plugin-utils': 7.24.6 2858 + '@babel/plugin-syntax-nullish-coalescing-operator': 7.8.3(@babel/core@7.24.6) 2859 + 2860 + '@babel/plugin-transform-numeric-separator@7.24.6(@babel/core@7.24.6)': 2861 + dependencies: 2862 + '@babel/core': 7.24.6 2863 + '@babel/helper-plugin-utils': 7.24.6 2864 + '@babel/plugin-syntax-numeric-separator': 7.10.4(@babel/core@7.24.6) 2865 + 2866 + '@babel/plugin-transform-object-rest-spread@7.24.6(@babel/core@7.24.6)': 2867 + dependencies: 2868 + '@babel/core': 7.24.6 2869 + '@babel/helper-compilation-targets': 7.24.6 2870 + '@babel/helper-plugin-utils': 7.24.6 2871 + '@babel/plugin-syntax-object-rest-spread': 7.8.3(@babel/core@7.24.6) 2872 + '@babel/plugin-transform-parameters': 7.24.6(@babel/core@7.24.6) 2873 + 2874 + '@babel/plugin-transform-object-super@7.24.6(@babel/core@7.24.6)': 2875 + dependencies: 2876 + '@babel/core': 7.24.6 2877 + '@babel/helper-plugin-utils': 7.24.6 2878 + '@babel/helper-replace-supers': 7.24.6(@babel/core@7.24.6) 2879 + 2880 + '@babel/plugin-transform-optional-catch-binding@7.24.6(@babel/core@7.24.6)': 2881 + dependencies: 2882 + '@babel/core': 7.24.6 2883 + '@babel/helper-plugin-utils': 7.24.6 2884 + '@babel/plugin-syntax-optional-catch-binding': 7.8.3(@babel/core@7.24.6) 2885 + 2886 + '@babel/plugin-transform-optional-chaining@7.24.6(@babel/core@7.24.6)': 2887 + dependencies: 2888 + '@babel/core': 7.24.6 2889 + '@babel/helper-plugin-utils': 7.24.6 2890 + '@babel/helper-skip-transparent-expression-wrappers': 7.24.6 2891 + '@babel/plugin-syntax-optional-chaining': 7.8.3(@babel/core@7.24.6) 2892 + 2893 + '@babel/plugin-transform-parameters@7.24.6(@babel/core@7.24.6)': 2894 + dependencies: 2895 + '@babel/core': 7.24.6 2896 + '@babel/helper-plugin-utils': 7.24.6 2897 + 2898 + '@babel/plugin-transform-private-methods@7.24.6(@babel/core@7.24.6)': 2899 + dependencies: 2900 + '@babel/core': 7.24.6 2901 + '@babel/helper-create-class-features-plugin': 7.24.6(@babel/core@7.24.6) 2902 + '@babel/helper-plugin-utils': 7.24.6 2903 + 2904 + '@babel/plugin-transform-private-property-in-object@7.24.6(@babel/core@7.24.6)': 2905 + dependencies: 2906 + '@babel/core': 7.24.6 2907 + '@babel/helper-annotate-as-pure': 7.24.6 2908 + '@babel/helper-create-class-features-plugin': 7.24.6(@babel/core@7.24.6) 2909 + '@babel/helper-plugin-utils': 7.24.6 2910 + '@babel/plugin-syntax-private-property-in-object': 7.14.5(@babel/core@7.24.6) 2911 + 2912 + '@babel/plugin-transform-property-literals@7.24.6(@babel/core@7.24.6)': 2913 + dependencies: 2914 + '@babel/core': 7.24.6 2915 + '@babel/helper-plugin-utils': 7.24.6 2916 + 2917 + '@babel/plugin-transform-regenerator@7.24.6(@babel/core@7.24.6)': 2918 + dependencies: 2919 + '@babel/core': 7.24.6 2920 + '@babel/helper-plugin-utils': 7.24.6 2921 + regenerator-transform: 0.15.2 2922 + 2923 + '@babel/plugin-transform-reserved-words@7.24.6(@babel/core@7.24.6)': 2924 + dependencies: 2925 + '@babel/core': 7.24.6 2926 + '@babel/helper-plugin-utils': 7.24.6 2927 + 2928 + '@babel/plugin-transform-shorthand-properties@7.24.6(@babel/core@7.24.6)': 2929 + dependencies: 2930 + '@babel/core': 7.24.6 2931 + '@babel/helper-plugin-utils': 7.24.6 2932 + 2933 + '@babel/plugin-transform-spread@7.24.6(@babel/core@7.24.6)': 2934 + dependencies: 2935 + '@babel/core': 7.24.6 2936 + '@babel/helper-plugin-utils': 7.24.6 2937 + '@babel/helper-skip-transparent-expression-wrappers': 7.24.6 2938 + 2939 + '@babel/plugin-transform-sticky-regex@7.24.6(@babel/core@7.24.6)': 2940 + dependencies: 2941 + '@babel/core': 7.24.6 2942 + '@babel/helper-plugin-utils': 7.24.6 2943 + 2944 + '@babel/plugin-transform-template-literals@7.24.6(@babel/core@7.24.6)': 2945 + dependencies: 2946 + '@babel/core': 7.24.6 2947 + '@babel/helper-plugin-utils': 7.24.6 2948 + 2949 + '@babel/plugin-transform-typeof-symbol@7.24.6(@babel/core@7.24.6)': 2950 + dependencies: 2951 + '@babel/core': 7.24.6 2952 + '@babel/helper-plugin-utils': 7.24.6 2953 + 2954 + '@babel/plugin-transform-unicode-escapes@7.24.6(@babel/core@7.24.6)': 2955 + dependencies: 2956 + '@babel/core': 7.24.6 2957 + '@babel/helper-plugin-utils': 7.24.6 2958 + 2959 + '@babel/plugin-transform-unicode-property-regex@7.24.6(@babel/core@7.24.6)': 2960 + dependencies: 2961 + '@babel/core': 7.24.6 2962 + '@babel/helper-create-regexp-features-plugin': 7.24.6(@babel/core@7.24.6) 2963 + '@babel/helper-plugin-utils': 7.24.6 2964 + 2965 + '@babel/plugin-transform-unicode-regex@7.24.6(@babel/core@7.24.6)': 2966 + dependencies: 2967 + '@babel/core': 7.24.6 2968 + '@babel/helper-create-regexp-features-plugin': 7.24.6(@babel/core@7.24.6) 2969 + '@babel/helper-plugin-utils': 7.24.6 2970 + 2971 + '@babel/plugin-transform-unicode-sets-regex@7.24.6(@babel/core@7.24.6)': 2972 + dependencies: 2973 + '@babel/core': 7.24.6 2974 + '@babel/helper-create-regexp-features-plugin': 7.24.6(@babel/core@7.24.6) 2975 + '@babel/helper-plugin-utils': 7.24.6 2976 + 2977 + '@babel/preset-env@7.24.6(@babel/core@7.24.6)': 2978 + dependencies: 2979 + '@babel/compat-data': 7.24.6 2980 + '@babel/core': 7.24.6 2981 + '@babel/helper-compilation-targets': 7.24.6 2982 + '@babel/helper-plugin-utils': 7.24.6 2983 + '@babel/helper-validator-option': 7.24.6 2984 + '@babel/plugin-bugfix-firefox-class-in-computed-class-key': 7.24.6(@babel/core@7.24.6) 2985 + '@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression': 7.24.6(@babel/core@7.24.6) 2986 + '@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining': 7.24.6(@babel/core@7.24.6) 2987 + '@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly': 7.24.6(@babel/core@7.24.6) 2988 + '@babel/plugin-proposal-private-property-in-object': 7.21.0-placeholder-for-preset-env.2(@babel/core@7.24.6) 2989 + '@babel/plugin-syntax-async-generators': 7.8.4(@babel/core@7.24.6) 2990 + '@babel/plugin-syntax-class-properties': 7.12.13(@babel/core@7.24.6) 2991 + '@babel/plugin-syntax-class-static-block': 7.14.5(@babel/core@7.24.6) 2992 + '@babel/plugin-syntax-dynamic-import': 7.8.3(@babel/core@7.24.6) 2993 + '@babel/plugin-syntax-export-namespace-from': 7.8.3(@babel/core@7.24.6) 2994 + '@babel/plugin-syntax-import-assertions': 7.24.6(@babel/core@7.24.6) 2995 + '@babel/plugin-syntax-import-attributes': 7.24.6(@babel/core@7.24.6) 2996 + '@babel/plugin-syntax-import-meta': 7.10.4(@babel/core@7.24.6) 2997 + '@babel/plugin-syntax-json-strings': 7.8.3(@babel/core@7.24.6) 2998 + '@babel/plugin-syntax-logical-assignment-operators': 7.10.4(@babel/core@7.24.6) 2999 + '@babel/plugin-syntax-nullish-coalescing-operator': 7.8.3(@babel/core@7.24.6) 3000 + '@babel/plugin-syntax-numeric-separator': 7.10.4(@babel/core@7.24.6) 3001 + '@babel/plugin-syntax-object-rest-spread': 7.8.3(@babel/core@7.24.6) 3002 + '@babel/plugin-syntax-optional-catch-binding': 7.8.3(@babel/core@7.24.6) 3003 + '@babel/plugin-syntax-optional-chaining': 7.8.3(@babel/core@7.24.6) 3004 + '@babel/plugin-syntax-private-property-in-object': 7.14.5(@babel/core@7.24.6) 3005 + '@babel/plugin-syntax-top-level-await': 7.14.5(@babel/core@7.24.6) 3006 + '@babel/plugin-syntax-unicode-sets-regex': 7.18.6(@babel/core@7.24.6) 3007 + '@babel/plugin-transform-arrow-functions': 7.24.6(@babel/core@7.24.6) 3008 + '@babel/plugin-transform-async-generator-functions': 7.24.6(@babel/core@7.24.6) 3009 + '@babel/plugin-transform-async-to-generator': 7.24.6(@babel/core@7.24.6) 3010 + '@babel/plugin-transform-block-scoped-functions': 7.24.6(@babel/core@7.24.6) 3011 + '@babel/plugin-transform-block-scoping': 7.24.6(@babel/core@7.24.6) 3012 + '@babel/plugin-transform-class-properties': 7.24.6(@babel/core@7.24.6) 3013 + '@babel/plugin-transform-class-static-block': 7.24.6(@babel/core@7.24.6) 3014 + '@babel/plugin-transform-classes': 7.24.6(@babel/core@7.24.6) 3015 + '@babel/plugin-transform-computed-properties': 7.24.6(@babel/core@7.24.6) 3016 + '@babel/plugin-transform-destructuring': 7.24.6(@babel/core@7.24.6) 3017 + '@babel/plugin-transform-dotall-regex': 7.24.6(@babel/core@7.24.6) 3018 + '@babel/plugin-transform-duplicate-keys': 7.24.6(@babel/core@7.24.6) 3019 + '@babel/plugin-transform-dynamic-import': 7.24.6(@babel/core@7.24.6) 3020 + '@babel/plugin-transform-exponentiation-operator': 7.24.6(@babel/core@7.24.6) 3021 + '@babel/plugin-transform-export-namespace-from': 7.24.6(@babel/core@7.24.6) 3022 + '@babel/plugin-transform-for-of': 7.24.6(@babel/core@7.24.6) 3023 + '@babel/plugin-transform-function-name': 7.24.6(@babel/core@7.24.6) 3024 + '@babel/plugin-transform-json-strings': 7.24.6(@babel/core@7.24.6) 3025 + '@babel/plugin-transform-literals': 7.24.6(@babel/core@7.24.6) 3026 + '@babel/plugin-transform-logical-assignment-operators': 7.24.6(@babel/core@7.24.6) 3027 + '@babel/plugin-transform-member-expression-literals': 7.24.6(@babel/core@7.24.6) 3028 + '@babel/plugin-transform-modules-amd': 7.24.6(@babel/core@7.24.6) 3029 + '@babel/plugin-transform-modules-commonjs': 7.24.6(@babel/core@7.24.6) 3030 + '@babel/plugin-transform-modules-systemjs': 7.24.6(@babel/core@7.24.6) 3031 + '@babel/plugin-transform-modules-umd': 7.24.6(@babel/core@7.24.6) 3032 + '@babel/plugin-transform-named-capturing-groups-regex': 7.24.6(@babel/core@7.24.6) 3033 + '@babel/plugin-transform-new-target': 7.24.6(@babel/core@7.24.6) 3034 + '@babel/plugin-transform-nullish-coalescing-operator': 7.24.6(@babel/core@7.24.6) 3035 + '@babel/plugin-transform-numeric-separator': 7.24.6(@babel/core@7.24.6) 3036 + '@babel/plugin-transform-object-rest-spread': 7.24.6(@babel/core@7.24.6) 3037 + '@babel/plugin-transform-object-super': 7.24.6(@babel/core@7.24.6) 3038 + '@babel/plugin-transform-optional-catch-binding': 7.24.6(@babel/core@7.24.6) 3039 + '@babel/plugin-transform-optional-chaining': 7.24.6(@babel/core@7.24.6) 3040 + '@babel/plugin-transform-parameters': 7.24.6(@babel/core@7.24.6) 3041 + '@babel/plugin-transform-private-methods': 7.24.6(@babel/core@7.24.6) 3042 + '@babel/plugin-transform-private-property-in-object': 7.24.6(@babel/core@7.24.6) 3043 + '@babel/plugin-transform-property-literals': 7.24.6(@babel/core@7.24.6) 3044 + '@babel/plugin-transform-regenerator': 7.24.6(@babel/core@7.24.6) 3045 + '@babel/plugin-transform-reserved-words': 7.24.6(@babel/core@7.24.6) 3046 + '@babel/plugin-transform-shorthand-properties': 7.24.6(@babel/core@7.24.6) 3047 + '@babel/plugin-transform-spread': 7.24.6(@babel/core@7.24.6) 3048 + '@babel/plugin-transform-sticky-regex': 7.24.6(@babel/core@7.24.6) 3049 + '@babel/plugin-transform-template-literals': 7.24.6(@babel/core@7.24.6) 3050 + '@babel/plugin-transform-typeof-symbol': 7.24.6(@babel/core@7.24.6) 3051 + '@babel/plugin-transform-unicode-escapes': 7.24.6(@babel/core@7.24.6) 3052 + '@babel/plugin-transform-unicode-property-regex': 7.24.6(@babel/core@7.24.6) 3053 + '@babel/plugin-transform-unicode-regex': 7.24.6(@babel/core@7.24.6) 3054 + '@babel/plugin-transform-unicode-sets-regex': 7.24.6(@babel/core@7.24.6) 3055 + '@babel/preset-modules': 0.1.6-no-external-plugins(@babel/core@7.24.6) 3056 + babel-plugin-polyfill-corejs2: 0.4.11(@babel/core@7.24.6) 3057 + babel-plugin-polyfill-corejs3: 0.10.4(@babel/core@7.24.6) 3058 + babel-plugin-polyfill-regenerator: 0.6.2(@babel/core@7.24.6) 3059 + core-js-compat: 3.37.1 3060 + semver: 6.3.1 3061 + transitivePeerDependencies: 3062 + - supports-color 3063 + 3064 + '@babel/preset-modules@0.1.6-no-external-plugins(@babel/core@7.24.6)': 3065 + dependencies: 3066 + '@babel/core': 7.24.6 3067 + '@babel/helper-plugin-utils': 7.24.6 3068 + '@babel/types': 7.24.6 3069 + esutils: 2.0.3 3070 + 3071 + '@babel/regjsgen@0.8.0': {} 3072 + 3073 + '@babel/runtime@7.24.6': 3074 + dependencies: 3075 + regenerator-runtime: 0.14.1 3076 + 3077 + '@babel/template@7.24.6': 3078 + dependencies: 3079 + '@babel/code-frame': 7.24.6 3080 + '@babel/parser': 7.24.6 3081 + '@babel/types': 7.24.6 3082 + 3083 + '@babel/traverse@7.24.6': 3084 + dependencies: 3085 + '@babel/code-frame': 7.24.6 3086 + '@babel/generator': 7.24.6 3087 + '@babel/helper-environment-visitor': 7.24.6 3088 + '@babel/helper-function-name': 7.24.6 3089 + '@babel/helper-hoist-variables': 7.24.6 3090 + '@babel/helper-split-export-declaration': 7.24.6 3091 + '@babel/parser': 7.24.6 3092 + '@babel/types': 7.24.6 3093 + debug: 4.3.4 3094 + globals: 11.12.0 3095 + transitivePeerDependencies: 3096 + - supports-color 3097 + 3098 + '@babel/types@7.24.6': 3099 + dependencies: 3100 + '@babel/helper-string-parser': 7.24.6 3101 + '@babel/helper-validator-identifier': 7.24.6 3102 + to-fast-properties: 2.0.0 3103 + 3104 + '@esbuild/aix-ppc64@0.20.2': 3105 + optional: true 3106 + 3107 + '@esbuild/android-arm64@0.20.2': 3108 + optional: true 3109 + 3110 + '@esbuild/android-arm@0.20.2': 3111 + optional: true 3112 + 3113 + '@esbuild/android-x64@0.20.2': 3114 + optional: true 3115 + 3116 + '@esbuild/darwin-arm64@0.20.2': 3117 + optional: true 3118 + 3119 + '@esbuild/darwin-x64@0.20.2': 3120 + optional: true 3121 + 3122 + '@esbuild/freebsd-arm64@0.20.2': 3123 + optional: true 3124 + 3125 + '@esbuild/freebsd-x64@0.20.2': 3126 + optional: true 3127 + 3128 + '@esbuild/linux-arm64@0.20.2': 3129 + optional: true 3130 + 3131 + '@esbuild/linux-arm@0.20.2': 3132 + optional: true 3133 + 3134 + '@esbuild/linux-ia32@0.20.2': 3135 + optional: true 3136 + 3137 + '@esbuild/linux-loong64@0.20.2': 3138 + optional: true 3139 + 3140 + '@esbuild/linux-mips64el@0.20.2': 3141 + optional: true 3142 + 3143 + '@esbuild/linux-ppc64@0.20.2': 3144 + optional: true 3145 + 3146 + '@esbuild/linux-riscv64@0.20.2': 3147 + optional: true 3148 + 3149 + '@esbuild/linux-s390x@0.20.2': 3150 + optional: true 3151 + 3152 + '@esbuild/linux-x64@0.20.2': 3153 + optional: true 3154 + 3155 + '@esbuild/netbsd-x64@0.20.2': 3156 + optional: true 3157 + 3158 + '@esbuild/openbsd-x64@0.20.2': 3159 + optional: true 3160 + 3161 + '@esbuild/sunos-x64@0.20.2': 3162 + optional: true 3163 + 3164 + '@esbuild/win32-arm64@0.20.2': 3165 + optional: true 3166 + 3167 + '@esbuild/win32-ia32@0.20.2': 3168 + optional: true 3169 + 3170 + '@esbuild/win32-x64@0.20.2': 3171 + optional: true 3172 + 3173 + '@externdefs/solid-freeze@0.1.1(solid-js@1.8.17(patch_hash=wunpcbjxb5h4ujg4psj63uuluq))': 3174 + dependencies: 3175 + solid-js: 1.8.17(patch_hash=wunpcbjxb5h4ujg4psj63uuluq) 3176 + 3177 + '@externdefs/solid-query@0.1.4(solid-js@1.8.17(patch_hash=wunpcbjxb5h4ujg4psj63uuluq))': 3178 + dependencies: 3179 + '@tanstack/query-core': 5.17.19(patch_hash=v3r5daycwz67li5mxwjnajy7bm) 3180 + solid-js: 1.8.17(patch_hash=wunpcbjxb5h4ujg4psj63uuluq) 3181 + 3182 + '@floating-ui/core@1.6.2': 3183 + dependencies: 3184 + '@floating-ui/utils': 0.2.2 3185 + 3186 + '@floating-ui/dom@1.6.5': 3187 + dependencies: 3188 + '@floating-ui/core': 1.6.2 3189 + '@floating-ui/utils': 0.2.2 3190 + 3191 + '@floating-ui/utils@0.2.2': {} 3192 + 3193 + '@isaacs/cliui@8.0.2': 3194 + dependencies: 3195 + string-width: 5.1.2 3196 + string-width-cjs: string-width@4.2.3 3197 + strip-ansi: 7.1.0 3198 + strip-ansi-cjs: strip-ansi@6.0.1 3199 + wrap-ansi: 8.1.0 3200 + wrap-ansi-cjs: wrap-ansi@7.0.0 3201 + 3202 + '@jridgewell/gen-mapping@0.3.5': 3203 + dependencies: 3204 + '@jridgewell/set-array': 1.2.1 3205 + '@jridgewell/sourcemap-codec': 1.4.15 3206 + '@jridgewell/trace-mapping': 0.3.25 3207 + 3208 + '@jridgewell/resolve-uri@3.1.2': {} 3209 + 3210 + '@jridgewell/set-array@1.2.1': {} 3211 + 3212 + '@jridgewell/source-map@0.3.6': 3213 + dependencies: 3214 + '@jridgewell/gen-mapping': 0.3.5 3215 + '@jridgewell/trace-mapping': 0.3.25 3216 + 3217 + '@jridgewell/sourcemap-codec@1.4.15': {} 3218 + 3219 + '@jridgewell/trace-mapping@0.3.25': 3220 + dependencies: 3221 + '@jridgewell/resolve-uri': 3.1.2 3222 + '@jridgewell/sourcemap-codec': 1.4.15 3223 + 3224 + '@jsr/mary__bluesky-client@0.5.23': {} 3225 + 3226 + '@jsr/mary__events@0.1.0': {} 3227 + 3228 + '@nodelib/fs.scandir@2.1.5': 3229 + dependencies: 3230 + '@nodelib/fs.stat': 2.0.5 3231 + run-parallel: 1.2.0 3232 + 3233 + '@nodelib/fs.stat@2.0.5': {} 3234 + 3235 + '@nodelib/fs.walk@1.2.8': 3236 + dependencies: 3237 + '@nodelib/fs.scandir': 2.1.5 3238 + fastq: 1.17.1 3239 + 3240 + '@pkgjs/parseargs@0.11.0': 3241 + optional: true 3242 + 3243 + '@rollup/plugin-babel@5.3.1(@babel/core@7.24.6)(@types/babel__core@7.20.5)(rollup@2.79.1)': 3244 + dependencies: 3245 + '@babel/core': 7.24.6 3246 + '@babel/helper-module-imports': 7.24.6 3247 + '@rollup/pluginutils': 3.1.0(rollup@2.79.1) 3248 + rollup: 2.79.1 3249 + optionalDependencies: 3250 + '@types/babel__core': 7.20.5 3251 + 3252 + '@rollup/plugin-node-resolve@15.2.3(rollup@2.79.1)': 3253 + dependencies: 3254 + '@rollup/pluginutils': 5.1.0(rollup@2.79.1) 3255 + '@types/resolve': 1.20.2 3256 + deepmerge: 4.3.1 3257 + is-builtin-module: 3.2.1 3258 + is-module: 1.0.0 3259 + resolve: 1.22.8 3260 + optionalDependencies: 3261 + rollup: 2.79.1 3262 + 3263 + '@rollup/plugin-replace@2.4.2(rollup@2.79.1)': 3264 + dependencies: 3265 + '@rollup/pluginutils': 3.1.0(rollup@2.79.1) 3266 + magic-string: 0.25.9 3267 + rollup: 2.79.1 3268 + 3269 + '@rollup/plugin-terser@0.4.4(rollup@2.79.1)': 3270 + dependencies: 3271 + serialize-javascript: 6.0.2 3272 + smob: 1.5.0 3273 + terser: 5.31.1 3274 + optionalDependencies: 3275 + rollup: 2.79.1 3276 + 3277 + '@rollup/pluginutils@3.1.0(rollup@2.79.1)': 3278 + dependencies: 3279 + '@types/estree': 0.0.39 3280 + estree-walker: 1.0.1 3281 + picomatch: 2.3.1 3282 + rollup: 2.79.1 3283 + 3284 + '@rollup/pluginutils@5.1.0(rollup@2.79.1)': 3285 + dependencies: 3286 + '@types/estree': 1.0.5 3287 + estree-walker: 2.0.2 3288 + picomatch: 2.3.1 3289 + optionalDependencies: 3290 + rollup: 2.79.1 3291 + 3292 + '@rollup/rollup-android-arm-eabi@4.18.0': 3293 + optional: true 3294 + 3295 + '@rollup/rollup-android-arm64@4.18.0': 3296 + optional: true 3297 + 3298 + '@rollup/rollup-darwin-arm64@4.18.0': 3299 + optional: true 3300 + 3301 + '@rollup/rollup-darwin-x64@4.18.0': 3302 + optional: true 3303 + 3304 + '@rollup/rollup-linux-arm-gnueabihf@4.18.0': 3305 + optional: true 3306 + 3307 + '@rollup/rollup-linux-arm-musleabihf@4.18.0': 3308 + optional: true 3309 + 3310 + '@rollup/rollup-linux-arm64-gnu@4.18.0': 3311 + optional: true 3312 + 3313 + '@rollup/rollup-linux-arm64-musl@4.18.0': 3314 + optional: true 3315 + 3316 + '@rollup/rollup-linux-powerpc64le-gnu@4.18.0': 3317 + optional: true 3318 + 3319 + '@rollup/rollup-linux-riscv64-gnu@4.18.0': 3320 + optional: true 3321 + 3322 + '@rollup/rollup-linux-s390x-gnu@4.18.0': 3323 + optional: true 3324 + 3325 + '@rollup/rollup-linux-x64-gnu@4.18.0': 3326 + optional: true 3327 + 3328 + '@rollup/rollup-linux-x64-musl@4.18.0': 3329 + optional: true 3330 + 3331 + '@rollup/rollup-win32-arm64-msvc@4.18.0': 3332 + optional: true 3333 + 3334 + '@rollup/rollup-win32-ia32-msvc@4.18.0': 3335 + optional: true 3336 + 3337 + '@rollup/rollup-win32-x64-msvc@4.18.0': 3338 + optional: true 3339 + 3340 + '@surma/rollup-plugin-off-main-thread@2.2.3': 3341 + dependencies: 3342 + ejs: 3.1.10 3343 + json5: 2.2.3 3344 + magic-string: 0.25.9 3345 + string.prototype.matchall: 4.0.11 3346 + 3347 + '@tanstack/query-core@5.17.19(patch_hash=v3r5daycwz67li5mxwjnajy7bm)': {} 3348 + 3349 + '@types/babel__core@7.20.5': 3350 + dependencies: 3351 + '@babel/parser': 7.24.6 3352 + '@babel/types': 7.24.6 3353 + '@types/babel__generator': 7.6.8 3354 + '@types/babel__template': 7.4.4 3355 + '@types/babel__traverse': 7.20.6 3356 + 3357 + '@types/babel__generator@7.6.8': 3358 + dependencies: 3359 + '@babel/types': 7.24.6 3360 + 3361 + '@types/babel__template@7.4.4': 3362 + dependencies: 3363 + '@babel/parser': 7.24.6 3364 + '@babel/types': 7.24.6 3365 + 3366 + '@types/babel__traverse@7.20.6': 3367 + dependencies: 3368 + '@babel/types': 7.24.6 3369 + 3370 + '@types/dom-close-watcher@1.0.0': {} 3371 + 3372 + '@types/estree@0.0.39': {} 3373 + 3374 + '@types/estree@1.0.5': {} 3375 + 3376 + '@types/resolve@1.20.2': {} 3377 + 3378 + '@types/trusted-types@2.0.7': {} 3379 + 3380 + acorn@8.11.3: {} 3381 + 3382 + ajv@8.14.0: 3383 + dependencies: 3384 + fast-deep-equal: 3.1.3 3385 + json-schema-traverse: 1.0.0 3386 + require-from-string: 2.0.2 3387 + uri-js: 4.4.1 3388 + 3389 + ansi-regex@5.0.1: {} 3390 + 3391 + ansi-regex@6.0.1: {} 3392 + 3393 + ansi-styles@3.2.1: 3394 + dependencies: 3395 + color-convert: 1.9.3 3396 + 3397 + ansi-styles@4.3.0: 3398 + dependencies: 3399 + color-convert: 2.0.1 3400 + 3401 + ansi-styles@6.2.1: {} 3402 + 3403 + any-promise@1.3.0: {} 3404 + 3405 + anymatch@3.1.3: 3406 + dependencies: 3407 + normalize-path: 3.0.0 3408 + picomatch: 2.3.1 3409 + 3410 + arg@5.0.2: {} 3411 + 3412 + array-buffer-byte-length@1.0.1: 3413 + dependencies: 3414 + call-bind: 1.0.7 3415 + is-array-buffer: 3.0.4 3416 + 3417 + arraybuffer.prototype.slice@1.0.3: 3418 + dependencies: 3419 + array-buffer-byte-length: 1.0.1 3420 + call-bind: 1.0.7 3421 + define-properties: 1.2.1 3422 + es-abstract: 1.23.3 3423 + es-errors: 1.3.0 3424 + get-intrinsic: 1.2.4 3425 + is-array-buffer: 3.0.4 3426 + is-shared-array-buffer: 1.0.3 3427 + 3428 + async@3.2.5: {} 3429 + 3430 + at-least-node@1.0.0: {} 3431 + 3432 + autoprefixer@10.4.19(postcss@8.4.38): 3433 + dependencies: 3434 + browserslist: 4.23.0 3435 + caniuse-lite: 1.0.30001623 3436 + fraction.js: 4.3.7 3437 + normalize-range: 0.1.2 3438 + picocolors: 1.0.1 3439 + postcss: 8.4.38 3440 + postcss-value-parser: 4.2.0 3441 + 3442 + available-typed-arrays@1.0.7: 3443 + dependencies: 3444 + possible-typed-array-names: 1.0.0 3445 + 3446 + babel-plugin-jsx-dom-expressions@0.37.21(@babel/core@7.24.6): 3447 + dependencies: 3448 + '@babel/core': 7.24.6 3449 + '@babel/helper-module-imports': 7.18.6 3450 + '@babel/plugin-syntax-jsx': 7.24.6(@babel/core@7.24.6) 3451 + '@babel/types': 7.24.6 3452 + html-entities: 2.3.3 3453 + validate-html-nesting: 1.2.2 3454 + 3455 + babel-plugin-polyfill-corejs2@0.4.11(@babel/core@7.24.6): 3456 + dependencies: 3457 + '@babel/compat-data': 7.24.6 3458 + '@babel/core': 7.24.6 3459 + '@babel/helper-define-polyfill-provider': 0.6.2(@babel/core@7.24.6) 3460 + semver: 6.3.1 3461 + transitivePeerDependencies: 3462 + - supports-color 3463 + 3464 + babel-plugin-polyfill-corejs3@0.10.4(@babel/core@7.24.6): 3465 + dependencies: 3466 + '@babel/core': 7.24.6 3467 + '@babel/helper-define-polyfill-provider': 0.6.2(@babel/core@7.24.6) 3468 + core-js-compat: 3.37.1 3469 + transitivePeerDependencies: 3470 + - supports-color 3471 + 3472 + babel-plugin-polyfill-regenerator@0.6.2(@babel/core@7.24.6): 3473 + dependencies: 3474 + '@babel/core': 7.24.6 3475 + '@babel/helper-define-polyfill-provider': 0.6.2(@babel/core@7.24.6) 3476 + transitivePeerDependencies: 3477 + - supports-color 3478 + 3479 + babel-preset-solid@1.8.17(@babel/core@7.24.6): 3480 + dependencies: 3481 + '@babel/core': 7.24.6 3482 + babel-plugin-jsx-dom-expressions: 0.37.21(@babel/core@7.24.6) 3483 + 3484 + balanced-match@1.0.2: {} 3485 + 3486 + binary-extensions@2.3.0: {} 3487 + 3488 + brace-expansion@1.1.11: 3489 + dependencies: 3490 + balanced-match: 1.0.2 3491 + concat-map: 0.0.1 3492 + 3493 + brace-expansion@2.0.1: 3494 + dependencies: 3495 + balanced-match: 1.0.2 3496 + 3497 + braces@3.0.3: 3498 + dependencies: 3499 + fill-range: 7.1.1 3500 + 3501 + browserslist@4.23.0: 3502 + dependencies: 3503 + caniuse-lite: 1.0.30001623 3504 + electron-to-chromium: 1.4.783 3505 + node-releases: 2.0.14 3506 + update-browserslist-db: 1.0.16(browserslist@4.23.0) 3507 + 3508 + buffer-from@1.1.2: {} 3509 + 3510 + builtin-modules@3.3.0: {} 3511 + 3512 + call-bind@1.0.7: 3513 + dependencies: 3514 + es-define-property: 1.0.0 3515 + es-errors: 1.3.0 3516 + function-bind: 1.1.2 3517 + get-intrinsic: 1.2.4 3518 + set-function-length: 1.2.2 3519 + 3520 + camelcase-css@2.0.1: {} 3521 + 3522 + caniuse-lite@1.0.30001623: {} 3523 + 3524 + cborg@4.0.7(patch_hash=dz6b6r6dc4jadjfng7vtgi53hy): {} 3525 + 3526 + chalk@2.4.2: 3527 + dependencies: 3528 + ansi-styles: 3.2.1 3529 + escape-string-regexp: 1.0.5 3530 + supports-color: 5.5.0 3531 + 3532 + chalk@4.1.2: 3533 + dependencies: 3534 + ansi-styles: 4.3.0 3535 + supports-color: 7.2.0 3536 + 3537 + chokidar@3.6.0: 3538 + dependencies: 3539 + anymatch: 3.1.3 3540 + braces: 3.0.3 3541 + glob-parent: 5.1.2 3542 + is-binary-path: 2.1.0 3543 + is-glob: 4.0.3 3544 + normalize-path: 3.0.0 3545 + readdirp: 3.6.0 3546 + optionalDependencies: 3547 + fsevents: 2.3.3 3548 + 3549 + color-convert@1.9.3: 3550 + dependencies: 3551 + color-name: 1.1.3 3552 + 3553 + color-convert@2.0.1: 3554 + dependencies: 3555 + color-name: 1.1.4 3556 + 3557 + color-name@1.1.3: {} 3558 + 3559 + color-name@1.1.4: {} 3560 + 3561 + commander@2.20.3: {} 3562 + 3563 + commander@4.1.1: {} 3564 + 3565 + common-tags@1.8.2: {} 3566 + 3567 + concat-map@0.0.1: {} 3568 + 3569 + convert-source-map@2.0.0: {} 3570 + 3571 + core-js-compat@3.37.1: 3572 + dependencies: 3573 + browserslist: 4.23.0 3574 + 3575 + cross-spawn@7.0.3: 3576 + dependencies: 3577 + path-key: 3.1.1 3578 + shebang-command: 2.0.0 3579 + which: 2.0.2 3580 + 3581 + crypto-random-string@2.0.0: {} 3582 + 3583 + cssesc@3.0.0: {} 3584 + 3585 + csstype@3.1.3: {} 3586 + 3587 + data-view-buffer@1.0.1: 3588 + dependencies: 3589 + call-bind: 1.0.7 3590 + es-errors: 1.3.0 3591 + is-data-view: 1.0.1 3592 + 3593 + data-view-byte-length@1.0.1: 3594 + dependencies: 3595 + call-bind: 1.0.7 3596 + es-errors: 1.3.0 3597 + is-data-view: 1.0.1 3598 + 3599 + data-view-byte-offset@1.0.0: 3600 + dependencies: 3601 + call-bind: 1.0.7 3602 + es-errors: 1.3.0 3603 + is-data-view: 1.0.1 3604 + 3605 + debug@4.3.4: 3606 + dependencies: 3607 + ms: 2.1.2 3608 + 3609 + deepmerge@4.3.1: {} 3610 + 3611 + define-data-property@1.1.4: 3612 + dependencies: 3613 + es-define-property: 1.0.0 3614 + es-errors: 1.3.0 3615 + gopd: 1.0.1 3616 + 3617 + define-properties@1.2.1: 3618 + dependencies: 3619 + define-data-property: 1.1.4 3620 + has-property-descriptors: 1.0.2 3621 + object-keys: 1.1.1 3622 + 3623 + didyoumean@1.2.2: {} 3624 + 3625 + dlv@1.1.3: {} 3626 + 3627 + eastasianwidth@0.2.0: {} 3628 + 3629 + ejs@3.1.10: 3630 + dependencies: 3631 + jake: 10.9.1 3632 + 3633 + electron-to-chromium@1.4.783: {} 3634 + 3635 + emoji-regex@8.0.0: {} 3636 + 3637 + emoji-regex@9.2.2: {} 3638 + 3639 + es-abstract@1.23.3: 3640 + dependencies: 3641 + array-buffer-byte-length: 1.0.1 3642 + arraybuffer.prototype.slice: 1.0.3 3643 + available-typed-arrays: 1.0.7 3644 + call-bind: 1.0.7 3645 + data-view-buffer: 1.0.1 3646 + data-view-byte-length: 1.0.1 3647 + data-view-byte-offset: 1.0.0 3648 + es-define-property: 1.0.0 3649 + es-errors: 1.3.0 3650 + es-object-atoms: 1.0.0 3651 + es-set-tostringtag: 2.0.3 3652 + es-to-primitive: 1.2.1 3653 + function.prototype.name: 1.1.6 3654 + get-intrinsic: 1.2.4 3655 + get-symbol-description: 1.0.2 3656 + globalthis: 1.0.4 3657 + gopd: 1.0.1 3658 + has-property-descriptors: 1.0.2 3659 + has-proto: 1.0.3 3660 + has-symbols: 1.0.3 3661 + hasown: 2.0.2 3662 + internal-slot: 1.0.7 3663 + is-array-buffer: 3.0.4 3664 + is-callable: 1.2.7 3665 + is-data-view: 1.0.1 3666 + is-negative-zero: 2.0.3 3667 + is-regex: 1.1.4 3668 + is-shared-array-buffer: 1.0.3 3669 + is-string: 1.0.7 3670 + is-typed-array: 1.1.13 3671 + is-weakref: 1.0.2 3672 + object-inspect: 1.13.1 3673 + object-keys: 1.1.1 3674 + object.assign: 4.1.5 3675 + regexp.prototype.flags: 1.5.2 3676 + safe-array-concat: 1.1.2 3677 + safe-regex-test: 1.0.3 3678 + string.prototype.trim: 1.2.9 3679 + string.prototype.trimend: 1.0.8 3680 + string.prototype.trimstart: 1.0.8 3681 + typed-array-buffer: 1.0.2 3682 + typed-array-byte-length: 1.0.1 3683 + typed-array-byte-offset: 1.0.2 3684 + typed-array-length: 1.0.6 3685 + unbox-primitive: 1.0.2 3686 + which-typed-array: 1.1.15 3687 + 3688 + es-define-property@1.0.0: 3689 + dependencies: 3690 + get-intrinsic: 1.2.4 3691 + 3692 + es-errors@1.3.0: {} 3693 + 3694 + es-object-atoms@1.0.0: 3695 + dependencies: 3696 + es-errors: 1.3.0 3697 + 3698 + es-set-tostringtag@2.0.3: 3699 + dependencies: 3700 + get-intrinsic: 1.2.4 3701 + has-tostringtag: 1.0.2 3702 + hasown: 2.0.2 3703 + 3704 + es-to-primitive@1.2.1: 3705 + dependencies: 3706 + is-callable: 1.2.7 3707 + is-date-object: 1.0.5 3708 + is-symbol: 1.0.4 3709 + 3710 + esbuild@0.20.2: 3711 + optionalDependencies: 3712 + '@esbuild/aix-ppc64': 0.20.2 3713 + '@esbuild/android-arm': 0.20.2 3714 + '@esbuild/android-arm64': 0.20.2 3715 + '@esbuild/android-x64': 0.20.2 3716 + '@esbuild/darwin-arm64': 0.20.2 3717 + '@esbuild/darwin-x64': 0.20.2 3718 + '@esbuild/freebsd-arm64': 0.20.2 3719 + '@esbuild/freebsd-x64': 0.20.2 3720 + '@esbuild/linux-arm': 0.20.2 3721 + '@esbuild/linux-arm64': 0.20.2 3722 + '@esbuild/linux-ia32': 0.20.2 3723 + '@esbuild/linux-loong64': 0.20.2 3724 + '@esbuild/linux-mips64el': 0.20.2 3725 + '@esbuild/linux-ppc64': 0.20.2 3726 + '@esbuild/linux-riscv64': 0.20.2 3727 + '@esbuild/linux-s390x': 0.20.2 3728 + '@esbuild/linux-x64': 0.20.2 3729 + '@esbuild/netbsd-x64': 0.20.2 3730 + '@esbuild/openbsd-x64': 0.20.2 3731 + '@esbuild/sunos-x64': 0.20.2 3732 + '@esbuild/win32-arm64': 0.20.2 3733 + '@esbuild/win32-ia32': 0.20.2 3734 + '@esbuild/win32-x64': 0.20.2 3735 + 3736 + escalade@3.1.2: {} 3737 + 3738 + escape-string-regexp@1.0.5: {} 3739 + 3740 + estree-walker@1.0.1: {} 3741 + 3742 + estree-walker@2.0.2: {} 3743 + 3744 + esutils@2.0.3: {} 3745 + 3746 + fast-deep-equal@3.1.3: {} 3747 + 3748 + fast-glob@3.3.2: 3749 + dependencies: 3750 + '@nodelib/fs.stat': 2.0.5 3751 + '@nodelib/fs.walk': 1.2.8 3752 + glob-parent: 5.1.2 3753 + merge2: 1.4.1 3754 + micromatch: 4.0.7 3755 + 3756 + fast-json-stable-stringify@2.1.0: {} 3757 + 3758 + fastq@1.17.1: 3759 + dependencies: 3760 + reusify: 1.0.4 3761 + 3762 + filelist@1.0.4: 3763 + dependencies: 3764 + minimatch: 5.1.6 3765 + 3766 + fill-range@7.1.1: 3767 + dependencies: 3768 + to-regex-range: 5.0.1 3769 + 3770 + for-each@0.3.3: 3771 + dependencies: 3772 + is-callable: 1.2.7 3773 + 3774 + foreground-child@3.1.1: 3775 + dependencies: 3776 + cross-spawn: 7.0.3 3777 + signal-exit: 4.1.0 3778 + 3779 + fraction.js@4.3.7: {} 3780 + 3781 + fs-extra@9.1.0: 3782 + dependencies: 3783 + at-least-node: 1.0.0 3784 + graceful-fs: 4.2.11 3785 + jsonfile: 6.1.0 3786 + universalify: 2.0.1 3787 + 3788 + fs.realpath@1.0.0: {} 3789 + 3790 + fsevents@2.3.3: 3791 + optional: true 3792 + 3793 + function-bind@1.1.2: {} 3794 + 3795 + function.prototype.name@1.1.6: 3796 + dependencies: 3797 + call-bind: 1.0.7 3798 + define-properties: 1.2.1 3799 + es-abstract: 1.23.3 3800 + functions-have-names: 1.2.3 3801 + 3802 + functions-have-names@1.2.3: {} 3803 + 3804 + gensync@1.0.0-beta.2: {} 3805 + 3806 + get-intrinsic@1.2.4: 3807 + dependencies: 3808 + es-errors: 1.3.0 3809 + function-bind: 1.1.2 3810 + has-proto: 1.0.3 3811 + has-symbols: 1.0.3 3812 + hasown: 2.0.2 3813 + 3814 + get-own-enumerable-property-symbols@3.0.2: {} 3815 + 3816 + get-symbol-description@1.0.2: 3817 + dependencies: 3818 + call-bind: 1.0.7 3819 + es-errors: 1.3.0 3820 + get-intrinsic: 1.2.4 3821 + 3822 + glob-parent@5.1.2: 3823 + dependencies: 3824 + is-glob: 4.0.3 3825 + 3826 + glob-parent@6.0.2: 3827 + dependencies: 3828 + is-glob: 4.0.3 3829 + 3830 + glob@10.4.1: 3831 + dependencies: 3832 + foreground-child: 3.1.1 3833 + jackspeak: 3.4.0 3834 + minimatch: 9.0.4 3835 + minipass: 7.1.2 3836 + path-scurry: 1.11.1 3837 + 3838 + glob@7.2.3: 3839 + dependencies: 3840 + fs.realpath: 1.0.0 3841 + inflight: 1.0.6 3842 + inherits: 2.0.4 3843 + minimatch: 3.1.2 3844 + once: 1.4.0 3845 + path-is-absolute: 1.0.1 3846 + 3847 + globals@11.12.0: {} 3848 + 3849 + globalthis@1.0.4: 3850 + dependencies: 3851 + define-properties: 1.2.1 3852 + gopd: 1.0.1 3853 + 3854 + gopd@1.0.1: 3855 + dependencies: 3856 + get-intrinsic: 1.2.4 3857 + 3858 + graceful-fs@4.2.11: {} 3859 + 3860 + has-bigints@1.0.2: {} 3861 + 3862 + has-flag@3.0.0: {} 3863 + 3864 + has-flag@4.0.0: {} 3865 + 3866 + has-property-descriptors@1.0.2: 3867 + dependencies: 3868 + es-define-property: 1.0.0 3869 + 3870 + has-proto@1.0.3: {} 3871 + 3872 + has-symbols@1.0.3: {} 3873 + 3874 + has-tostringtag@1.0.2: 3875 + dependencies: 3876 + has-symbols: 1.0.3 3877 + 3878 + hasown@2.0.2: 3879 + dependencies: 3880 + function-bind: 1.1.2 3881 + 3882 + html-entities@2.3.3: {} 3883 + 3884 + idb@7.1.1: {} 3885 + 3886 + inflight@1.0.6: 3887 + dependencies: 3888 + once: 1.4.0 3889 + wrappy: 1.0.2 3890 + 3891 + inherits@2.0.4: {} 3892 + 3893 + internal-slot@1.0.7: 3894 + dependencies: 3895 + es-errors: 1.3.0 3896 + hasown: 2.0.2 3897 + side-channel: 1.0.6 3898 + 3899 + is-array-buffer@3.0.4: 3900 + dependencies: 3901 + call-bind: 1.0.7 3902 + get-intrinsic: 1.2.4 3903 + 3904 + is-bigint@1.0.4: 3905 + dependencies: 3906 + has-bigints: 1.0.2 3907 + 3908 + is-binary-path@2.1.0: 3909 + dependencies: 3910 + binary-extensions: 2.3.0 3911 + 3912 + is-boolean-object@1.1.2: 3913 + dependencies: 3914 + call-bind: 1.0.7 3915 + has-tostringtag: 1.0.2 3916 + 3917 + is-builtin-module@3.2.1: 3918 + dependencies: 3919 + builtin-modules: 3.3.0 3920 + 3921 + is-callable@1.2.7: {} 3922 + 3923 + is-core-module@2.13.1: 3924 + dependencies: 3925 + hasown: 2.0.2 3926 + 3927 + is-data-view@1.0.1: 3928 + dependencies: 3929 + is-typed-array: 1.1.13 3930 + 3931 + is-date-object@1.0.5: 3932 + dependencies: 3933 + has-tostringtag: 1.0.2 3934 + 3935 + is-extglob@2.1.1: {} 3936 + 3937 + is-fullwidth-code-point@3.0.0: {} 3938 + 3939 + is-glob@4.0.3: 3940 + dependencies: 3941 + is-extglob: 2.1.1 3942 + 3943 + is-module@1.0.0: {} 3944 + 3945 + is-negative-zero@2.0.3: {} 3946 + 3947 + is-number-object@1.0.7: 3948 + dependencies: 3949 + has-tostringtag: 1.0.2 3950 + 3951 + is-number@7.0.0: {} 3952 + 3953 + is-obj@1.0.1: {} 3954 + 3955 + is-regex@1.1.4: 3956 + dependencies: 3957 + call-bind: 1.0.7 3958 + has-tostringtag: 1.0.2 3959 + 3960 + is-regexp@1.0.0: {} 3961 + 3962 + is-shared-array-buffer@1.0.3: 3963 + dependencies: 3964 + call-bind: 1.0.7 3965 + 3966 + is-stream@2.0.1: {} 3967 + 3968 + is-string@1.0.7: 3969 + dependencies: 3970 + has-tostringtag: 1.0.2 3971 + 3972 + is-symbol@1.0.4: 3973 + dependencies: 3974 + has-symbols: 1.0.3 3975 + 3976 + is-typed-array@1.1.13: 3977 + dependencies: 3978 + which-typed-array: 1.1.15 3979 + 3980 + is-weakref@1.0.2: 3981 + dependencies: 3982 + call-bind: 1.0.7 3983 + 3984 + is-what@4.1.16: {} 3985 + 3986 + isarray@2.0.5: {} 3987 + 3988 + isexe@2.0.0: {} 3989 + 3990 + jackspeak@3.4.0: 3991 + dependencies: 3992 + '@isaacs/cliui': 8.0.2 3993 + optionalDependencies: 3994 + '@pkgjs/parseargs': 0.11.0 3995 + 3996 + jake@10.9.1: 3997 + dependencies: 3998 + async: 3.2.5 3999 + chalk: 4.1.2 4000 + filelist: 1.0.4 4001 + minimatch: 3.1.2 4002 + 4003 + jiti@1.21.3: {} 4004 + 4005 + js-tokens@4.0.0: {} 4006 + 4007 + jsesc@0.5.0: {} 4008 + 4009 + jsesc@2.5.2: {} 4010 + 4011 + json-schema-traverse@1.0.0: {} 4012 + 4013 + json-schema@0.4.0: {} 4014 + 4015 + json5@2.2.3: {} 4016 + 4017 + jsonfile@6.1.0: 4018 + dependencies: 4019 + universalify: 2.0.1 4020 + optionalDependencies: 4021 + graceful-fs: 4.2.11 4022 + 4023 + jsonpointer@5.0.1: {} 4024 + 4025 + leven@3.1.0: {} 4026 + 4027 + lilconfig@2.1.0: {} 4028 + 4029 + lilconfig@3.1.1: {} 4030 + 4031 + lines-and-columns@1.2.4: {} 4032 + 4033 + lodash.debounce@4.0.8: {} 4034 + 4035 + lodash.sortby@4.7.0: {} 4036 + 4037 + lodash@4.17.21: {} 4038 + 4039 + lru-cache@10.2.2: {} 4040 + 4041 + lru-cache@5.1.1: 4042 + dependencies: 4043 + yallist: 3.1.1 4044 + 4045 + magic-string@0.25.9: 4046 + dependencies: 4047 + sourcemap-codec: 1.4.8 4048 + 4049 + merge-anything@5.1.7: 4050 + dependencies: 4051 + is-what: 4.1.16 4052 + 4053 + merge2@1.4.1: {} 4054 + 4055 + micromatch@4.0.7: 4056 + dependencies: 4057 + braces: 3.0.3 4058 + picomatch: 2.3.1 4059 + 4060 + minimatch@3.1.2: 4061 + dependencies: 4062 + brace-expansion: 1.1.11 4063 + 4064 + minimatch@5.1.6: 4065 + dependencies: 4066 + brace-expansion: 2.0.1 4067 + 4068 + minimatch@9.0.4: 4069 + dependencies: 4070 + brace-expansion: 2.0.1 4071 + 4072 + minipass@7.1.2: {} 4073 + 4074 + ms@2.1.2: {} 4075 + 4076 + mz@2.7.0: 4077 + dependencies: 4078 + any-promise: 1.3.0 4079 + object-assign: 4.1.1 4080 + thenify-all: 1.6.0 4081 + 4082 + nanoid@3.3.7: {} 4083 + 4084 + nanoid@5.0.7: {} 4085 + 4086 + node-releases@2.0.14: {} 4087 + 4088 + normalize-path@3.0.0: {} 4089 + 4090 + normalize-range@0.1.2: {} 4091 + 4092 + object-assign@4.1.1: {} 4093 + 4094 + object-hash@3.0.0: {} 4095 + 4096 + object-inspect@1.13.1: {} 4097 + 4098 + object-keys@1.1.1: {} 4099 + 4100 + object.assign@4.1.5: 4101 + dependencies: 4102 + call-bind: 1.0.7 4103 + define-properties: 1.2.1 4104 + has-symbols: 1.0.3 4105 + object-keys: 1.1.1 4106 + 4107 + once@1.4.0: 4108 + dependencies: 4109 + wrappy: 1.0.2 4110 + 4111 + path-is-absolute@1.0.1: {} 4112 + 4113 + path-key@3.1.1: {} 4114 + 4115 + path-parse@1.0.7: {} 4116 + 4117 + path-scurry@1.11.1: 4118 + dependencies: 4119 + lru-cache: 10.2.2 4120 + minipass: 7.1.2 4121 + 4122 + picocolors@1.0.1: {} 4123 + 4124 + picomatch@2.3.1: {} 4125 + 4126 + pify@2.3.0: {} 4127 + 4128 + pirates@4.0.6: {} 4129 + 4130 + possible-typed-array-names@1.0.0: {} 4131 + 4132 + postcss-import@15.1.0(postcss@8.4.38): 4133 + dependencies: 4134 + postcss: 8.4.38 4135 + postcss-value-parser: 4.2.0 4136 + read-cache: 1.0.0 4137 + resolve: 1.22.8 4138 + 4139 + postcss-js@4.0.1(postcss@8.4.38): 4140 + dependencies: 4141 + camelcase-css: 2.0.1 4142 + postcss: 8.4.38 4143 + 4144 + postcss-load-config@4.0.2(postcss@8.4.38): 4145 + dependencies: 4146 + lilconfig: 3.1.1 4147 + yaml: 2.4.3 4148 + optionalDependencies: 4149 + postcss: 8.4.38 4150 + 4151 + postcss-nested@6.0.1(postcss@8.4.38): 4152 + dependencies: 4153 + postcss: 8.4.38 4154 + postcss-selector-parser: 6.1.0 4155 + 4156 + postcss-selector-parser@6.1.0: 4157 + dependencies: 4158 + cssesc: 3.0.0 4159 + util-deprecate: 1.0.2 4160 + 4161 + postcss-value-parser@4.2.0: {} 4162 + 4163 + postcss@8.4.38: 4164 + dependencies: 4165 + nanoid: 3.3.7 4166 + picocolors: 1.0.1 4167 + source-map-js: 1.2.0 4168 + 4169 + prettier-plugin-tailwindcss@0.6.3(prettier@3.3.2): 4170 + dependencies: 4171 + prettier: 3.3.2 4172 + 4173 + prettier@3.3.2: {} 4174 + 4175 + pretty-bytes@5.6.0: {} 4176 + 4177 + pretty-bytes@6.1.1: {} 4178 + 4179 + punycode@2.3.1: {} 4180 + 4181 + queue-microtask@1.2.3: {} 4182 + 4183 + randombytes@2.1.0: 4184 + dependencies: 4185 + safe-buffer: 5.2.1 4186 + 4187 + read-cache@1.0.0: 4188 + dependencies: 4189 + pify: 2.3.0 4190 + 4191 + readdirp@3.6.0: 4192 + dependencies: 4193 + picomatch: 2.3.1 4194 + 4195 + regenerate-unicode-properties@10.1.1: 4196 + dependencies: 4197 + regenerate: 1.4.2 4198 + 4199 + regenerate@1.4.2: {} 4200 + 4201 + regenerator-runtime@0.14.1: {} 4202 + 4203 + regenerator-transform@0.15.2: 4204 + dependencies: 4205 + '@babel/runtime': 7.24.6 4206 + 4207 + regexp.prototype.flags@1.5.2: 4208 + dependencies: 4209 + call-bind: 1.0.7 4210 + define-properties: 1.2.1 4211 + es-errors: 1.3.0 4212 + set-function-name: 2.0.2 4213 + 4214 + regexpu-core@5.3.2: 4215 + dependencies: 4216 + '@babel/regjsgen': 0.8.0 4217 + regenerate: 1.4.2 4218 + regenerate-unicode-properties: 10.1.1 4219 + regjsparser: 0.9.1 4220 + unicode-match-property-ecmascript: 2.0.0 4221 + unicode-match-property-value-ecmascript: 2.1.0 4222 + 4223 + regjsparser@0.9.1: 4224 + dependencies: 4225 + jsesc: 0.5.0 4226 + 4227 + require-from-string@2.0.2: {} 4228 + 4229 + resolve@1.22.8: 4230 + dependencies: 4231 + is-core-module: 2.13.1 4232 + path-parse: 1.0.7 4233 + supports-preserve-symlinks-flag: 1.0.0 4234 + 4235 + reusify@1.0.4: {} 4236 + 4237 + rollup@2.79.1: 4238 + optionalDependencies: 4239 + fsevents: 2.3.3 4240 + 4241 + rollup@4.18.0: 4242 + dependencies: 4243 + '@types/estree': 1.0.5 4244 + optionalDependencies: 4245 + '@rollup/rollup-android-arm-eabi': 4.18.0 4246 + '@rollup/rollup-android-arm64': 4.18.0 4247 + '@rollup/rollup-darwin-arm64': 4.18.0 4248 + '@rollup/rollup-darwin-x64': 4.18.0 4249 + '@rollup/rollup-linux-arm-gnueabihf': 4.18.0 4250 + '@rollup/rollup-linux-arm-musleabihf': 4.18.0 4251 + '@rollup/rollup-linux-arm64-gnu': 4.18.0 4252 + '@rollup/rollup-linux-arm64-musl': 4.18.0 4253 + '@rollup/rollup-linux-powerpc64le-gnu': 4.18.0 4254 + '@rollup/rollup-linux-riscv64-gnu': 4.18.0 4255 + '@rollup/rollup-linux-s390x-gnu': 4.18.0 4256 + '@rollup/rollup-linux-x64-gnu': 4.18.0 4257 + '@rollup/rollup-linux-x64-musl': 4.18.0 4258 + '@rollup/rollup-win32-arm64-msvc': 4.18.0 4259 + '@rollup/rollup-win32-ia32-msvc': 4.18.0 4260 + '@rollup/rollup-win32-x64-msvc': 4.18.0 4261 + fsevents: 2.3.3 4262 + 4263 + run-parallel@1.2.0: 4264 + dependencies: 4265 + queue-microtask: 1.2.3 4266 + 4267 + safe-array-concat@1.1.2: 4268 + dependencies: 4269 + call-bind: 1.0.7 4270 + get-intrinsic: 1.2.4 4271 + has-symbols: 1.0.3 4272 + isarray: 2.0.5 4273 + 4274 + safe-buffer@5.2.1: {} 4275 + 4276 + safe-regex-test@1.0.3: 4277 + dependencies: 4278 + call-bind: 1.0.7 4279 + es-errors: 1.3.0 4280 + is-regex: 1.1.4 4281 + 4282 + semver@6.3.1: {} 4283 + 4284 + serialize-javascript@6.0.2: 4285 + dependencies: 4286 + randombytes: 2.1.0 4287 + 4288 + seroval-plugins@1.0.7(seroval@1.0.7): 4289 + dependencies: 4290 + seroval: 1.0.7 4291 + 4292 + seroval@1.0.7: {} 4293 + 4294 + set-function-length@1.2.2: 4295 + dependencies: 4296 + define-data-property: 1.1.4 4297 + es-errors: 1.3.0 4298 + function-bind: 1.1.2 4299 + get-intrinsic: 1.2.4 4300 + gopd: 1.0.1 4301 + has-property-descriptors: 1.0.2 4302 + 4303 + set-function-name@2.0.2: 4304 + dependencies: 4305 + define-data-property: 1.1.4 4306 + es-errors: 1.3.0 4307 + functions-have-names: 1.2.3 4308 + has-property-descriptors: 1.0.2 4309 + 4310 + shebang-command@2.0.0: 4311 + dependencies: 4312 + shebang-regex: 3.0.0 4313 + 4314 + shebang-regex@3.0.0: {} 4315 + 4316 + side-channel@1.0.6: 4317 + dependencies: 4318 + call-bind: 1.0.7 4319 + es-errors: 1.3.0 4320 + get-intrinsic: 1.2.4 4321 + object-inspect: 1.13.1 4322 + 4323 + signal-exit@4.1.0: {} 4324 + 4325 + smob@1.5.0: {} 4326 + 4327 + solid-floating-ui@0.2.1(@floating-ui/dom@1.6.5)(solid-js@1.8.17(patch_hash=wunpcbjxb5h4ujg4psj63uuluq)): 4328 + dependencies: 4329 + '@floating-ui/dom': 1.6.5 4330 + solid-js: 1.8.17(patch_hash=wunpcbjxb5h4ujg4psj63uuluq) 4331 + 4332 + solid-js@1.8.17(patch_hash=wunpcbjxb5h4ujg4psj63uuluq): 4333 + dependencies: 4334 + csstype: 3.1.3 4335 + seroval: 1.0.7 4336 + seroval-plugins: 1.0.7(seroval@1.0.7) 4337 + 4338 + solid-refresh@0.6.3(solid-js@1.8.17(patch_hash=wunpcbjxb5h4ujg4psj63uuluq)): 4339 + dependencies: 4340 + '@babel/generator': 7.24.6 4341 + '@babel/helper-module-imports': 7.24.6 4342 + '@babel/types': 7.24.6 4343 + solid-js: 1.8.17(patch_hash=wunpcbjxb5h4ujg4psj63uuluq) 4344 + 4345 + solid-textarea-autosize@0.0.5(patch_hash=xoixqosplh7bmfbnvr4hschede)(solid-js@1.8.17(patch_hash=wunpcbjxb5h4ujg4psj63uuluq)): 4346 + dependencies: 4347 + solid-js: 1.8.17(patch_hash=wunpcbjxb5h4ujg4psj63uuluq) 4348 + 4349 + source-map-js@1.2.0: {} 4350 + 4351 + source-map-support@0.5.21: 4352 + dependencies: 4353 + buffer-from: 1.1.2 4354 + source-map: 0.6.1 4355 + 4356 + source-map@0.6.1: {} 4357 + 4358 + source-map@0.8.0-beta.0: 4359 + dependencies: 4360 + whatwg-url: 7.1.0 4361 + 4362 + sourcemap-codec@1.4.8: {} 4363 + 4364 + string-width@4.2.3: 4365 + dependencies: 4366 + emoji-regex: 8.0.0 4367 + is-fullwidth-code-point: 3.0.0 4368 + strip-ansi: 6.0.1 4369 + 4370 + string-width@5.1.2: 4371 + dependencies: 4372 + eastasianwidth: 0.2.0 4373 + emoji-regex: 9.2.2 4374 + strip-ansi: 7.1.0 4375 + 4376 + string.prototype.matchall@4.0.11: 4377 + dependencies: 4378 + call-bind: 1.0.7 4379 + define-properties: 1.2.1 4380 + es-abstract: 1.23.3 4381 + es-errors: 1.3.0 4382 + es-object-atoms: 1.0.0 4383 + get-intrinsic: 1.2.4 4384 + gopd: 1.0.1 4385 + has-symbols: 1.0.3 4386 + internal-slot: 1.0.7 4387 + regexp.prototype.flags: 1.5.2 4388 + set-function-name: 2.0.2 4389 + side-channel: 1.0.6 4390 + 4391 + string.prototype.trim@1.2.9: 4392 + dependencies: 4393 + call-bind: 1.0.7 4394 + define-properties: 1.2.1 4395 + es-abstract: 1.23.3 4396 + es-object-atoms: 1.0.0 4397 + 4398 + string.prototype.trimend@1.0.8: 4399 + dependencies: 4400 + call-bind: 1.0.7 4401 + define-properties: 1.2.1 4402 + es-object-atoms: 1.0.0 4403 + 4404 + string.prototype.trimstart@1.0.8: 4405 + dependencies: 4406 + call-bind: 1.0.7 4407 + define-properties: 1.2.1 4408 + es-object-atoms: 1.0.0 4409 + 4410 + stringify-object@3.3.0: 4411 + dependencies: 4412 + get-own-enumerable-property-symbols: 3.0.2 4413 + is-obj: 1.0.1 4414 + is-regexp: 1.0.0 4415 + 4416 + strip-ansi@6.0.1: 4417 + dependencies: 4418 + ansi-regex: 5.0.1 4419 + 4420 + strip-ansi@7.1.0: 4421 + dependencies: 4422 + ansi-regex: 6.0.1 4423 + 4424 + strip-comments@2.0.1: {} 4425 + 4426 + sucrase@3.35.0: 4427 + dependencies: 4428 + '@jridgewell/gen-mapping': 0.3.5 4429 + commander: 4.1.1 4430 + glob: 10.4.1 4431 + lines-and-columns: 1.2.4 4432 + mz: 2.7.0 4433 + pirates: 4.0.6 4434 + ts-interface-checker: 0.1.13 4435 + 4436 + supports-color@5.5.0: 4437 + dependencies: 4438 + has-flag: 3.0.0 4439 + 4440 + supports-color@7.2.0: 4441 + dependencies: 4442 + has-flag: 4.0.0 4443 + 4444 + supports-preserve-symlinks-flag@1.0.0: {} 4445 + 4446 + tailwindcss@3.4.4: 4447 + dependencies: 4448 + '@alloc/quick-lru': 5.2.0 4449 + arg: 5.0.2 4450 + chokidar: 3.6.0 4451 + didyoumean: 1.2.2 4452 + dlv: 1.1.3 4453 + fast-glob: 3.3.2 4454 + glob-parent: 6.0.2 4455 + is-glob: 4.0.3 4456 + jiti: 1.21.3 4457 + lilconfig: 2.1.0 4458 + micromatch: 4.0.7 4459 + normalize-path: 3.0.0 4460 + object-hash: 3.0.0 4461 + picocolors: 1.0.1 4462 + postcss: 8.4.38 4463 + postcss-import: 15.1.0(postcss@8.4.38) 4464 + postcss-js: 4.0.1(postcss@8.4.38) 4465 + postcss-load-config: 4.0.2(postcss@8.4.38) 4466 + postcss-nested: 6.0.1(postcss@8.4.38) 4467 + postcss-selector-parser: 6.1.0 4468 + resolve: 1.22.8 4469 + sucrase: 3.35.0 4470 + transitivePeerDependencies: 4471 + - ts-node 4472 + 4473 + temp-dir@2.0.0: {} 4474 + 4475 + tempy@0.6.0: 4476 + dependencies: 4477 + is-stream: 2.0.1 4478 + temp-dir: 2.0.0 4479 + type-fest: 0.16.0 4480 + unique-string: 2.0.0 4481 + 4482 + terser@5.31.1: 4483 + dependencies: 4484 + '@jridgewell/source-map': 0.3.6 4485 + acorn: 8.11.3 4486 + commander: 2.20.3 4487 + source-map-support: 0.5.21 4488 + 4489 + thenify-all@1.6.0: 4490 + dependencies: 4491 + thenify: 3.3.1 4492 + 4493 + thenify@3.3.1: 4494 + dependencies: 4495 + any-promise: 1.3.0 4496 + 4497 + to-fast-properties@2.0.0: {} 4498 + 4499 + to-regex-range@5.0.1: 4500 + dependencies: 4501 + is-number: 7.0.0 4502 + 4503 + tr46@1.0.1: 4504 + dependencies: 4505 + punycode: 2.3.1 4506 + 4507 + ts-interface-checker@0.1.13: {} 4508 + 4509 + type-fest@0.16.0: {} 4510 + 4511 + typed-array-buffer@1.0.2: 4512 + dependencies: 4513 + call-bind: 1.0.7 4514 + es-errors: 1.3.0 4515 + is-typed-array: 1.1.13 4516 + 4517 + typed-array-byte-length@1.0.1: 4518 + dependencies: 4519 + call-bind: 1.0.7 4520 + for-each: 0.3.3 4521 + gopd: 1.0.1 4522 + has-proto: 1.0.3 4523 + is-typed-array: 1.1.13 4524 + 4525 + typed-array-byte-offset@1.0.2: 4526 + dependencies: 4527 + available-typed-arrays: 1.0.7 4528 + call-bind: 1.0.7 4529 + for-each: 0.3.3 4530 + gopd: 1.0.1 4531 + has-proto: 1.0.3 4532 + is-typed-array: 1.1.13 4533 + 4534 + typed-array-length@1.0.6: 4535 + dependencies: 4536 + call-bind: 1.0.7 4537 + for-each: 0.3.3 4538 + gopd: 1.0.1 4539 + has-proto: 1.0.3 4540 + is-typed-array: 1.1.13 4541 + possible-typed-array-names: 1.0.0 4542 + 4543 + typescript@5.4.5: {} 4544 + 4545 + unbox-primitive@1.0.2: 4546 + dependencies: 4547 + call-bind: 1.0.7 4548 + has-bigints: 1.0.2 4549 + has-symbols: 1.0.3 4550 + which-boxed-primitive: 1.0.2 4551 + 4552 + unicode-canonical-property-names-ecmascript@2.0.0: {} 4553 + 4554 + unicode-match-property-ecmascript@2.0.0: 4555 + dependencies: 4556 + unicode-canonical-property-names-ecmascript: 2.0.0 4557 + unicode-property-aliases-ecmascript: 2.1.0 4558 + 4559 + unicode-match-property-value-ecmascript@2.1.0: {} 4560 + 4561 + unicode-property-aliases-ecmascript@2.1.0: {} 4562 + 4563 + unique-string@2.0.0: 4564 + dependencies: 4565 + crypto-random-string: 2.0.0 4566 + 4567 + universalify@2.0.1: {} 4568 + 4569 + upath@1.2.0: {} 4570 + 4571 + update-browserslist-db@1.0.16(browserslist@4.23.0): 4572 + dependencies: 4573 + browserslist: 4.23.0 4574 + escalade: 3.1.2 4575 + picocolors: 1.0.1 4576 + 4577 + uri-js@4.4.1: 4578 + dependencies: 4579 + punycode: 2.3.1 4580 + 4581 + util-deprecate@1.0.2: {} 4582 + 4583 + validate-html-nesting@1.2.2: {} 4584 + 4585 + vite-plugin-pwa@0.17.4(patch_hash=ve5hypcrajivuvoyst6zln6qyq)(@types/babel__core@7.20.5)(vite@5.2.11(patch_hash=rtipi3fkkgeet3kqyzne4ksswy)(terser@5.31.1)): 4586 + dependencies: 4587 + debug: 4.3.4 4588 + fast-glob: 3.3.2 4589 + pretty-bytes: 6.1.1 4590 + vite: 5.2.11(patch_hash=rtipi3fkkgeet3kqyzne4ksswy)(terser@5.31.1) 4591 + workbox-build: 7.1.0(@types/babel__core@7.20.5) 4592 + workbox-window: 7.1.0 4593 + transitivePeerDependencies: 4594 + - '@types/babel__core' 4595 + - supports-color 4596 + 4597 + vite-plugin-solid@2.10.2(solid-js@1.8.17(patch_hash=wunpcbjxb5h4ujg4psj63uuluq))(vite@5.2.11(patch_hash=rtipi3fkkgeet3kqyzne4ksswy)(terser@5.31.1)): 4598 + dependencies: 4599 + '@babel/core': 7.24.6 4600 + '@types/babel__core': 7.20.5 4601 + babel-preset-solid: 1.8.17(@babel/core@7.24.6) 4602 + merge-anything: 5.1.7 4603 + solid-js: 1.8.17(patch_hash=wunpcbjxb5h4ujg4psj63uuluq) 4604 + solid-refresh: 0.6.3(solid-js@1.8.17(patch_hash=wunpcbjxb5h4ujg4psj63uuluq)) 4605 + vite: 5.2.11(patch_hash=rtipi3fkkgeet3kqyzne4ksswy)(terser@5.31.1) 4606 + vitefu: 0.2.5(vite@5.2.11(patch_hash=rtipi3fkkgeet3kqyzne4ksswy)(terser@5.31.1)) 4607 + transitivePeerDependencies: 4608 + - supports-color 4609 + 4610 + vite@5.2.11(patch_hash=rtipi3fkkgeet3kqyzne4ksswy)(terser@5.31.1): 4611 + dependencies: 4612 + esbuild: 0.20.2 4613 + postcss: 8.4.38 4614 + rollup: 4.18.0 4615 + optionalDependencies: 4616 + fsevents: 2.3.3 4617 + terser: 5.31.1 4618 + 4619 + vitefu@0.2.5(vite@5.2.11(patch_hash=rtipi3fkkgeet3kqyzne4ksswy)(terser@5.31.1)): 4620 + optionalDependencies: 4621 + vite: 5.2.11(patch_hash=rtipi3fkkgeet3kqyzne4ksswy)(terser@5.31.1) 4622 + 4623 + webidl-conversions@4.0.2: {} 4624 + 4625 + whatwg-url@7.1.0: 4626 + dependencies: 4627 + lodash.sortby: 4.7.0 4628 + tr46: 1.0.1 4629 + webidl-conversions: 4.0.2 4630 + 4631 + which-boxed-primitive@1.0.2: 4632 + dependencies: 4633 + is-bigint: 1.0.4 4634 + is-boolean-object: 1.1.2 4635 + is-number-object: 1.0.7 4636 + is-string: 1.0.7 4637 + is-symbol: 1.0.4 4638 + 4639 + which-typed-array@1.1.15: 4640 + dependencies: 4641 + available-typed-arrays: 1.0.7 4642 + call-bind: 1.0.7 4643 + for-each: 0.3.3 4644 + gopd: 1.0.1 4645 + has-tostringtag: 1.0.2 4646 + 4647 + which@2.0.2: 4648 + dependencies: 4649 + isexe: 2.0.0 4650 + 4651 + workbox-background-sync@7.1.0: 4652 + dependencies: 4653 + idb: 7.1.1 4654 + workbox-core: 7.1.0 4655 + 4656 + workbox-broadcast-update@7.1.0: 4657 + dependencies: 4658 + workbox-core: 7.1.0 4659 + 4660 + workbox-build@7.1.0(@types/babel__core@7.20.5): 4661 + dependencies: 4662 + '@apideck/better-ajv-errors': 0.3.6(ajv@8.14.0) 4663 + '@babel/core': 7.24.6 4664 + '@babel/preset-env': 7.24.6(@babel/core@7.24.6) 4665 + '@babel/runtime': 7.24.6 4666 + '@rollup/plugin-babel': 5.3.1(@babel/core@7.24.6)(@types/babel__core@7.20.5)(rollup@2.79.1) 4667 + '@rollup/plugin-node-resolve': 15.2.3(rollup@2.79.1) 4668 + '@rollup/plugin-replace': 2.4.2(rollup@2.79.1) 4669 + '@rollup/plugin-terser': 0.4.4(rollup@2.79.1) 4670 + '@surma/rollup-plugin-off-main-thread': 2.2.3 4671 + ajv: 8.14.0 4672 + common-tags: 1.8.2 4673 + fast-json-stable-stringify: 2.1.0 4674 + fs-extra: 9.1.0 4675 + glob: 7.2.3 4676 + lodash: 4.17.21 4677 + pretty-bytes: 5.6.0 4678 + rollup: 2.79.1 4679 + source-map: 0.8.0-beta.0 4680 + stringify-object: 3.3.0 4681 + strip-comments: 2.0.1 4682 + tempy: 0.6.0 4683 + upath: 1.2.0 4684 + workbox-background-sync: 7.1.0 4685 + workbox-broadcast-update: 7.1.0 4686 + workbox-cacheable-response: 7.1.0 4687 + workbox-core: 7.1.0 4688 + workbox-expiration: 7.1.0 4689 + workbox-google-analytics: 7.1.0 4690 + workbox-navigation-preload: 7.1.0 4691 + workbox-precaching: 7.1.0(patch_hash=uwqzx25dqx6gokakqgp7nxcupi) 4692 + workbox-range-requests: 7.1.0 4693 + workbox-recipes: 7.1.0 4694 + workbox-routing: 7.1.0 4695 + workbox-strategies: 7.1.0 4696 + workbox-streams: 7.1.0 4697 + workbox-sw: 7.1.0 4698 + workbox-window: 7.1.0 4699 + transitivePeerDependencies: 4700 + - '@types/babel__core' 4701 + - supports-color 4702 + 4703 + workbox-cacheable-response@7.1.0: 4704 + dependencies: 4705 + workbox-core: 7.1.0 4706 + 4707 + workbox-core@7.1.0: {} 4708 + 4709 + workbox-expiration@7.1.0: 4710 + dependencies: 4711 + idb: 7.1.1 4712 + workbox-core: 7.1.0 4713 + 4714 + workbox-google-analytics@7.1.0: 4715 + dependencies: 4716 + workbox-background-sync: 7.1.0 4717 + workbox-core: 7.1.0 4718 + workbox-routing: 7.1.0 4719 + workbox-strategies: 7.1.0 4720 + 4721 + workbox-navigation-preload@7.1.0: 4722 + dependencies: 4723 + workbox-core: 7.1.0 4724 + 4725 + workbox-precaching@7.1.0(patch_hash=uwqzx25dqx6gokakqgp7nxcupi): 4726 + dependencies: 4727 + workbox-core: 7.1.0 4728 + workbox-routing: 7.1.0 4729 + workbox-strategies: 7.1.0 4730 + 4731 + workbox-range-requests@7.1.0: 4732 + dependencies: 4733 + workbox-core: 7.1.0 4734 + 4735 + workbox-recipes@7.1.0: 4736 + dependencies: 4737 + workbox-cacheable-response: 7.1.0 4738 + workbox-core: 7.1.0 4739 + workbox-expiration: 7.1.0 4740 + workbox-precaching: 7.1.0(patch_hash=uwqzx25dqx6gokakqgp7nxcupi) 4741 + workbox-routing: 7.1.0 4742 + workbox-strategies: 7.1.0 4743 + 4744 + workbox-routing@7.1.0: 4745 + dependencies: 4746 + workbox-core: 7.1.0 4747 + 4748 + workbox-strategies@7.1.0: 4749 + dependencies: 4750 + workbox-core: 7.1.0 4751 + 4752 + workbox-streams@7.1.0: 4753 + dependencies: 4754 + workbox-core: 7.1.0 4755 + workbox-routing: 7.1.0 4756 + 4757 + workbox-sw@7.1.0: {} 4758 + 4759 + workbox-window@7.1.0: 4760 + dependencies: 4761 + '@types/trusted-types': 2.0.7 4762 + workbox-core: 7.1.0 4763 + 4764 + wrap-ansi@7.0.0: 4765 + dependencies: 4766 + ansi-styles: 4.3.0 4767 + string-width: 4.2.3 4768 + strip-ansi: 6.0.1 4769 + 4770 + wrap-ansi@8.1.0: 4771 + dependencies: 4772 + ansi-styles: 6.2.1 4773 + string-width: 5.1.2 4774 + strip-ansi: 7.1.0 4775 + 4776 + wrappy@1.0.2: {} 4777 + 4778 + yallist@3.1.1: {} 4779 + 4780 + yaml@2.4.3: {}
+6
postcss.config.js
··· 1 + export default { 2 + plugins: { 3 + tailwindcss: {}, 4 + autoprefixer: {}, 5 + }, 6 + };
+92
src/api/cache/post-shadow.ts
··· 1 + import { batch, createSignal, onCleanup } from 'solid-js'; 2 + 3 + import type { AppBskyFeedDefs } from '@mary/bluesky-client/lexicons'; 4 + import { EventEmitter } from '@mary/events'; 5 + import type { QueryClient } from '@mary/solid-query'; 6 + 7 + import { findAllPostsInQueryData as findAllPostsInTimelineQueryData } from '../queries/timeline'; 8 + import { EQUALS_DEQUAL } from '../utils/dequal'; 9 + 10 + export interface PostShadow { 11 + deleted?: boolean; 12 + likeUri?: string; 13 + repostUri?: string; 14 + threadMuted?: boolean; 15 + } 16 + 17 + export interface PostShadowView { 18 + deleted: boolean; 19 + likeCount: number; 20 + likeUri: string | undefined; 21 + repostCount: number; 22 + repostUri: string | undefined; 23 + threadMuted: boolean; 24 + } 25 + 26 + const emitter = new EventEmitter<{ [uri: string]: () => void }>(); 27 + const shadows = new WeakMap<AppBskyFeedDefs.PostView, PostShadow>(); 28 + 29 + export const usePostShadow = (post: AppBskyFeedDefs.PostView) => { 30 + const [view, setView] = createSignal(getPostShadow(post), EQUALS_DEQUAL); 31 + 32 + onCleanup(emitter.on(post.uri, () => setView(getPostShadow(post)))); 33 + return view; 34 + }; 35 + 36 + const getPostShadow = (post: AppBskyFeedDefs.PostView): PostShadowView => { 37 + const shadow = shadows.get(post) ?? {}; 38 + 39 + let likeCount = post.likeCount ?? 0; 40 + let repostCount = post.repostCount ?? 0; 41 + 42 + if ('likeUri' in shadow) { 43 + const wasLiked = !!post.viewer?.like; 44 + const isLiked = !!shadow.likeUri; 45 + 46 + if (wasLiked && !isLiked) { 47 + likeCount--; 48 + } else if (!wasLiked && isLiked) { 49 + likeCount++; 50 + } 51 + 52 + likeCount = Math.max(0, likeCount); 53 + } 54 + 55 + if ('repostUri' in shadow) { 56 + const wasReposted = !!post.viewer?.repost; 57 + const isReposted = !!shadow.repostUri; 58 + 59 + if (wasReposted && !isReposted) { 60 + repostCount--; 61 + } else if (!wasReposted && isReposted) { 62 + repostCount++; 63 + } 64 + 65 + repostCount = Math.max(0, repostCount); 66 + } 67 + 68 + return { 69 + deleted: shadow.deleted ?? false, 70 + likeCount: likeCount, 71 + likeUri: 'likeUri' in shadow ? shadow.likeUri : post.viewer?.like, 72 + repostCount: repostCount, 73 + repostUri: 'repostUri' in shadow ? shadow.repostUri : post.viewer?.repost, 74 + threadMuted: ('threadMuted' in shadow ? shadow.threadMuted : post.viewer?.threadMuted) ?? false, 75 + }; 76 + }; 77 + 78 + export const updatePostShadow = (queryClient: QueryClient, uri: string, value: Partial<PostShadow>) => { 79 + for (const post of findPostsInCache(queryClient, uri)) { 80 + shadows.set(post, { ...shadows.get(post), ...value }); 81 + } 82 + 83 + batch(() => emitter.emit(uri)); 84 + }; 85 + 86 + export function* findPostsInCache( 87 + queryClient: QueryClient, 88 + uri: string, 89 + includeQuote = false, 90 + ): Generator<AppBskyFeedDefs.PostView> { 91 + yield* findAllPostsInTimelineQueryData(queryClient, uri, includeQuote); 92 + }
+59
src/api/cache/profile-shadow.ts
··· 1 + import { batch, createSignal, onCleanup } from 'solid-js'; 2 + 3 + import type { AppBskyActorDefs, At } from '@mary/bluesky-client/lexicons'; 4 + import { EventEmitter } from '@mary/events'; 5 + import type { QueryClient } from '@mary/solid-query'; 6 + 7 + import { findAllProfilesInQueryData as findAllProfilesInProfileQueryData } from '../queries/profile'; 8 + import { findAllProfilesInQueryData as findAllProfilesInTimelineQueryData } from '../queries/timeline'; 9 + import { EQUALS_DEQUAL } from '../utils/dequal'; 10 + 11 + export interface ProfileShadow { 12 + blockUri?: string; 13 + followUri?: string; 14 + muted?: boolean; 15 + } 16 + 17 + export interface ProfileShadowView { 18 + blockUri: string | undefined; 19 + followUri: string | undefined; 20 + muted: boolean; 21 + } 22 + 23 + type AllProfileView = 24 + | AppBskyActorDefs.ProfileView 25 + | AppBskyActorDefs.ProfileViewBasic 26 + | AppBskyActorDefs.ProfileViewDetailed; 27 + 28 + const emitter = new EventEmitter<{ [uri: At.DID]: () => void }>(); 29 + const shadows = new WeakMap<AllProfileView, ProfileShadow>(); 30 + 31 + export const useProfileShadow = (profile: AllProfileView) => { 32 + const [view, setView] = createSignal(getProfileShadow(profile), EQUALS_DEQUAL); 33 + 34 + onCleanup(emitter.on(profile.did, () => setView(getProfileShadow(profile)))); 35 + return view; 36 + }; 37 + 38 + const getProfileShadow = (profile: AllProfileView): ProfileShadowView => { 39 + const shadow = shadows.get(profile) ?? {}; 40 + 41 + return { 42 + blockUri: 'blockUri' in shadow ? shadow.blockUri : profile.viewer?.blocking, 43 + followUri: 'followUri' in shadow ? shadow.followUri : profile.viewer?.following, 44 + muted: ('muted' in shadow ? shadow.muted : profile.viewer?.muted) ?? false, 45 + }; 46 + }; 47 + 48 + export const updateProfileShadow = (queryClient: QueryClient, did: At.DID, value: Partial<ProfileShadow>) => { 49 + for (const profile of findProfilesInCache(queryClient, did)) { 50 + shadows.set(profile, { ...shadows.get(profile), ...value }); 51 + } 52 + 53 + batch(() => emitter.emit(did)); 54 + }; 55 + 56 + export function* findProfilesInCache(queryClient: QueryClient, did: At.DID): Generator<AllProfileView> { 57 + yield* findAllProfilesInProfileQueryData(queryClient, did); 58 + yield* findAllProfilesInTimelineQueryData(queryClient, did); 59 + }
+14
src/api/cache/types.ts
··· 1 + // This isn't a real property, but it prevents T being compatible with Shadow<T>. 2 + declare const IsShadow: unique symbol; 3 + 4 + export type Shadow<T> = T & { [IsShadow]: true }; 5 + 6 + export const castAsShadow = <T>(value: T): Shadow<T> => { 7 + return value as any as Shadow<T>; 8 + }; 9 + 10 + export interface PostCacheFindOptions { 11 + uri?: string; 12 + rootUri?: string; 13 + includeQuote?: boolean; 14 + }
+10
src/api/defaults.ts
··· 1 + import type { DataServer } from './types'; 2 + 3 + export const DEFAULT_APP_VIEW = 'https://public.api.bsky.app'; 4 + 5 + export const DEFAULT_DATA_SERVER: DataServer = { 6 + name: 'Bluesky Social', 7 + uri: 'https://bsky.social', 8 + }; 9 + 10 + export const BLUESKY_MODERATION_DID = 'did:plc:ar7c4by46qjdydhdevvrndac';
+164
src/api/models/timeline.ts
··· 1 + import type { AppBskyActorDefs, AppBskyFeedDefs } from '@mary/bluesky-client/lexicons'; 2 + 3 + type Post = AppBskyFeedDefs.PostView; 4 + type TimelineItem = AppBskyFeedDefs.FeedViewPost; 5 + type ReplyRef = AppBskyFeedDefs.ReplyRef; 6 + 7 + // EnsuredTimelineItem 8 + export interface EnsuredReplyRef { 9 + root: Post | undefined; 10 + parent: Post | undefined; 11 + grandparentAuthor: AppBskyActorDefs.ProfileViewBasic | undefined; 12 + } 13 + 14 + export const ensureReplyRef = (reply: ReplyRef | undefined): EnsuredReplyRef | undefined => { 15 + if (reply) { 16 + const root = reply.root; 17 + const parent = reply.parent; 18 + const grandparentAuthor = reply.grandparentAuthor; 19 + 20 + // Thread started, or this is replying to a blocked user, skip this. 21 + // Thread started by a blocked user, skip this. 22 + if ( 23 + root.$type === 'app.bsky.feed.defs#blockedPost' || 24 + parent.$type === 'app.bsky.feed.defs#blockedPost' 25 + ) { 26 + return; 27 + } 28 + 29 + return { 30 + root: root.$type === 'app.bsky.feed.defs#postView' ? root : undefined, 31 + parent: parent.$type === 'app.bsky.feed.defs#postView' ? parent : undefined, 32 + grandparentAuthor: grandparentAuthor, 33 + }; 34 + } 35 + }; 36 + 37 + export interface EnsuredTimelineItem { 38 + post: Post; 39 + reply: EnsuredReplyRef | undefined; 40 + reason: TimelineItem['reason']; 41 + } 42 + 43 + export const ensureTimelineItem = (item: TimelineItem): EnsuredTimelineItem => { 44 + return { 45 + post: item.post, 46 + reply: ensureReplyRef(item.reply), 47 + reason: item.reason, 48 + }; 49 + }; 50 + // TimelineSlice 51 + export interface TimelineSlice { 52 + items: EnsuredTimelineItem[]; 53 + } 54 + 55 + // UiTimelineItem 56 + export interface UiTimelineItem extends EnsuredTimelineItem { 57 + prev: boolean; 58 + next: boolean; 59 + } 60 + 61 + export type SliceFilter = (slice: TimelineSlice) => boolean | TimelineSlice[]; 62 + export type PostFilter = (item: EnsuredTimelineItem) => boolean; 63 + 64 + const isNextInThread = (slice: TimelineSlice, item: EnsuredTimelineItem) => { 65 + const items = slice.items; 66 + const last = items[items.length - 1]; 67 + 68 + const parent = item.reply?.parent; 69 + 70 + return !!parent && last.post.cid == parent.cid; 71 + }; 72 + 73 + const isFirstInThread = (slice: TimelineSlice, item: EnsuredTimelineItem) => { 74 + const items = slice.items; 75 + const first = items[0]; 76 + 77 + const parent = first.reply?.parent; 78 + 79 + return !!parent && parent.cid === item.post.cid; 80 + }; 81 + 82 + const isArray = Array.isArray; 83 + 84 + export const createJoinedItems = ( 85 + arr: TimelineItem[], 86 + filterSlice?: SliceFilter, 87 + filterPost?: PostFilter, 88 + ): UiTimelineItem[] => { 89 + let slices: TimelineSlice[] = []; 90 + let jlen = 0; 91 + 92 + // arrange the posts into connected slices 93 + loop: for (let i = arr.length - 1; i >= 0; i--) { 94 + const item = ensureTimelineItem(arr[i]); 95 + 96 + if (filterPost && !filterPost(item)) { 97 + continue; 98 + } 99 + 100 + // if we find a matching slice and it's currently not in front, then bump 101 + // it to the front. this is so that new reply don't get buried away because 102 + // there's multiple posts separating it and the parent post. 103 + for (let j = 0; j < jlen; j++) { 104 + const slice = slices[j]; 105 + 106 + if (isFirstInThread(slice, item)) { 107 + slice.items.unshift(item); 108 + 109 + if (j !== 0) { 110 + slices.splice(j, 1); 111 + slices.unshift(slice); 112 + } 113 + 114 + continue loop; 115 + } else if (isNextInThread(slice, item)) { 116 + slice.items.push(item); 117 + 118 + if (j !== 0) { 119 + slices.splice(j, 1); 120 + slices.unshift(slice); 121 + } 122 + 123 + continue loop; 124 + } 125 + } 126 + 127 + slices.unshift({ items: [item] }); 128 + jlen++; 129 + } 130 + 131 + if (filterSlice && jlen > 0) { 132 + const unfiltered = slices; 133 + slices = []; 134 + 135 + for (let j = 0; j < jlen; j++) { 136 + const slice = unfiltered[j]; 137 + const result = filterSlice(slice); 138 + 139 + if (result) { 140 + if (isArray(result)) { 141 + for (let k = 0, klen = result.length; k < klen; k++) { 142 + const slice = result[k]; 143 + slices.push(slice); 144 + } 145 + } else { 146 + slices.push(slice); 147 + } 148 + } 149 + } 150 + } 151 + 152 + return slices.flatMap((slice) => { 153 + const arr = slice.items; 154 + const len = arr.length; 155 + 156 + return arr.map((item, idx): UiTimelineItem => { 157 + return { 158 + ...item, 159 + prev: idx !== 0, 160 + next: idx !== len - 1, 161 + }; 162 + }); 163 + }); 164 + };
+31
src/api/moderation/entities/post.ts
··· 1 + import type { AppBskyFeedDefs, AppBskyFeedPost } from '@mary/bluesky-client/lexicons'; 2 + 3 + import type { ProfileShadowView } from '~/api/cache/profile-shadow'; 4 + import { unwrapPostEmbedText } from '~/api/utils/post'; 5 + 6 + import { 7 + PreferenceWarn, 8 + TargetContent, 9 + decideLabelModeration, 10 + decideMutedKeywordModeration, 11 + type ModerationCause, 12 + type ModerationOptions, 13 + } from '..'; 14 + import { moderateProfile } from './profile'; 15 + 16 + export const moderatePost = ( 17 + post: AppBskyFeedDefs.PostView, 18 + authorShadow: ProfileShadowView, 19 + opts: ModerationOptions, 20 + ) => { 21 + const author = post.author; 22 + const record = post.record as AppBskyFeedPost.Record; 23 + const text = record.text + unwrapPostEmbedText(record.embed); 24 + 25 + const accu: ModerationCause[] = moderateProfile(author, authorShadow, opts); 26 + 27 + decideLabelModeration(accu, TargetContent, post.labels, author.did, opts); 28 + decideMutedKeywordModeration(accu, text, !!authorShadow.followUri, PreferenceWarn, opts); 29 + 30 + return accu; 31 + };
+38
src/api/moderation/entities/profile.ts
··· 1 + import type { AppBskyActorDefs } from '@mary/bluesky-client/lexicons'; 2 + 3 + import { type ProfileShadowView } from '~/api/cache/profile-shadow'; 4 + 5 + import { 6 + TargetAccount, 7 + TargetProfile, 8 + decideLabelModeration, 9 + decideMutedPermanentModeration, 10 + decideMutedTemporaryModeration, 11 + type ModerationCause, 12 + type ModerationOptions, 13 + } from '..'; 14 + 15 + type AllProfileView = 16 + | AppBskyActorDefs.ProfileView 17 + | AppBskyActorDefs.ProfileViewBasic 18 + | AppBskyActorDefs.ProfileViewDetailed; 19 + 20 + export const moderateProfile = ( 21 + profile: AllProfileView, 22 + shadow: ProfileShadowView, 23 + opts: ModerationOptions, 24 + ) => { 25 + const accu: ModerationCause[] = []; 26 + const did = profile.did; 27 + 28 + const labels = profile.labels; 29 + const profileLabels = labels?.filter((label) => label.uri.endsWith('/app.bsky.actor.profile/self')); 30 + const accountLabels = labels?.filter((label) => !label.uri.endsWith('/app.bsky.actor.profile/self')); 31 + 32 + decideLabelModeration(accu, TargetProfile, profileLabels, did, opts); 33 + decideLabelModeration(accu, TargetAccount, accountLabels, did, opts); 34 + decideMutedPermanentModeration(accu, shadow.muted); 35 + decideMutedTemporaryModeration(accu, did, opts); 36 + 37 + return accu; 38 + };
+31
src/api/moderation/entities/quote.ts
··· 1 + import type { AppBskyEmbedRecord, AppBskyFeedPost } from '@mary/bluesky-client/lexicons'; 2 + 3 + import type { ProfileShadowView } from '~/api/cache/profile-shadow'; 4 + import { unwrapPostEmbedText } from '~/api/utils/post'; 5 + 6 + import { 7 + PreferenceWarn, 8 + TargetContent, 9 + decideLabelModeration, 10 + decideMutedKeywordModeration, 11 + type ModerationCause, 12 + type ModerationOptions, 13 + } from '..'; 14 + import { moderateProfile } from './profile'; 15 + 16 + export const moderateQuote = ( 17 + quote: AppBskyEmbedRecord.ViewRecord, 18 + authorShadow: ProfileShadowView, 19 + opts: ModerationOptions, 20 + ) => { 21 + const author = quote.author; 22 + const record = quote.value as AppBskyFeedPost.Record; 23 + const text = record.text + unwrapPostEmbedText(record.embed); 24 + 25 + const accu: ModerationCause[] = moderateProfile(author, authorShadow, opts); 26 + 27 + decideLabelModeration(accu, TargetContent, quote.labels, author.did, opts); 28 + decideMutedKeywordModeration(accu, text, !!authorShadow.followUri, PreferenceWarn, opts); 29 + 30 + return accu; 31 + };
+619
src/api/moderation/index.ts
··· 1 + import type { At, ComAtprotoLabelDefs } from '@mary/bluesky-client/lexicons'; 2 + 3 + type Label = ComAtprotoLabelDefs.Label; 4 + 5 + /** Ignore this label */ 6 + export const PreferenceIgnore = 1; 7 + /** Warn when viewing content with this label present */ 8 + export const PreferenceWarn = 2; 9 + /** Hide content if this label is present */ 10 + export const PreferenceHide = 3; 11 + 12 + export type LabelPreference = 1 | 2 | 3; 13 + export type KeywordPreference = 1 | 2 | 3; 14 + 15 + /** Don't blur any parts of the content */ 16 + export const BlurNone = 0; 17 + /** Only blur the media present in the content */ 18 + export const BlurMedia = 1; 19 + /** Blur the entire content */ 20 + export const BlurContent = 2; 21 + /** Special blur value, guaranteed blurring of profile and content */ 22 + export const BlurForced = 3; 23 + 24 + export type LabelBlur = 0 | 1 | 2 | 3; 25 + 26 + /** Don't inform the user */ 27 + export const SeverityNone = 0; 28 + /** Lightly inform the user about this label's presence */ 29 + export const SeverityInform = 1; 30 + /** Alert the user about this label's presence */ 31 + export const SeverityAlert = 2; 32 + 33 + export type LabelSeverity = 0 | 1 | 2; 34 + 35 + /** No flags are present */ 36 + export const FlagsNone = 0; 37 + /** Don't allow blurred content to be expanded */ 38 + export const FlagsForced = 1 << 0; 39 + /** Don't apply label to self */ 40 + export const FlagsNoSelf = 1 << 1; 41 + /** Label is adult-only. */ 42 + export const FlagsAdultOnly = 1 << 2; 43 + 44 + /** Label is intended for content */ 45 + export const TargetContent = 0; 46 + /** Label is intended for profile itself */ 47 + export const TargetProfile = 1; 48 + /** Label is intended for the whole account */ 49 + export const TargetAccount = 2; 50 + 51 + export type LabelTarget = 0 | 1 | 2; 52 + 53 + /** Concerns viewing a post in full */ 54 + export const ContextContentView = 0; 55 + /** Concerns the media of a post */ 56 + export const ContextContentMedia = 1; 57 + /** Concerns post feed */ 58 + export const ContextContentList = 2; 59 + /** Concerns viewing a profile in full */ 60 + export const ContextProfileView = 3; 61 + /** Concerns avatar and banner of a profile */ 62 + export const ContextProfileMedia = 4; 63 + /** Concerns profile listing (follows, liked by, reposted by, etc...) */ 64 + export const ContextProfileList = 5; 65 + 66 + export type ModerationContext = 0 | 1 | 2 | 3 | 4 | 5; 67 + 68 + /** Label should cause blurring */ 69 + const BehaviorBlur = 0; 70 + /** Label should cause blurring if it has the adult flag, fallback to alert/inform if it isn't */ 71 + const BehaviorBlurIfAdultOrAlert = 1; 72 + /** Label should be alerted/informed */ 73 + const BehaviorAlertOrInform = 2; 74 + 75 + type ModerationBehavior = 0 | 1 | 2; 76 + 77 + export interface LabelLocale { 78 + /** Locale code */ 79 + i: string; 80 + /** Label name */ 81 + n: string; 82 + /** Label description */ 83 + d: string; 84 + } 85 + 86 + export interface LabelDefinition { 87 + /** Label identifier */ 88 + i: string; 89 + /** Default preference value */ 90 + d: LabelPreference; 91 + /** How the content should be blurred */ 92 + b: LabelBlur; 93 + /** How the content should be informed */ 94 + s: LabelSeverity; 95 + /** Additional flags for the label */ 96 + f: number; 97 + /** Descriptions for the label */ 98 + l: LabelLocale[]; 99 + } 100 + 101 + export type LabelDefinitionMapping = Record<string, LabelDefinition>; 102 + export type LabelPreferenceMapping = Record<string, LabelPreference | undefined>; 103 + 104 + export const GLOBAL_LABELS: LabelDefinitionMapping = { 105 + '!hide': { 106 + i: '!hide', 107 + d: PreferenceHide, 108 + b: BlurForced, 109 + s: SeverityNone, 110 + f: FlagsForced | FlagsNoSelf, 111 + l: [{ i: 'en', n: `Hidden by moderators`, d: `` }], 112 + }, 113 + '!warn': { 114 + i: '!warn', 115 + d: PreferenceWarn, 116 + b: BlurForced, 117 + s: SeverityAlert, 118 + f: FlagsNoSelf, 119 + l: [{ i: 'en', n: `Content warning`, d: `` }], 120 + }, 121 + porn: { 122 + i: 'porn', 123 + d: PreferenceWarn, 124 + b: BlurMedia, 125 + s: SeverityNone, 126 + f: FlagsAdultOnly, 127 + l: [{ i: 'en', n: `Adult content`, d: `Erotic nudity or explicit sexual activity` }], 128 + }, 129 + sexual: { 130 + i: 'sexual', 131 + d: PreferenceWarn, 132 + b: BlurMedia, 133 + s: SeverityNone, 134 + f: FlagsAdultOnly, 135 + l: [{ i: 'en', n: `Sexually suggestive`, d: `Not pornographic but sexual in nature` }], 136 + }, 137 + 'graphic-media': { 138 + i: 'graphic-media', 139 + d: PreferenceWarn, 140 + b: BlurMedia, 141 + s: SeverityNone, 142 + f: FlagsAdultOnly, 143 + l: [{ i: 'en', n: `Graphic media`, d: `Disturbing content` }], 144 + }, 145 + nudity: { 146 + i: 'nudity', 147 + d: PreferenceWarn, 148 + b: BlurMedia, 149 + s: SeverityNone, 150 + f: FlagsNone, 151 + l: [{ i: 'en', n: `Nudity`, d: `Artistic or non-erotic nudity` }], 152 + }, 153 + }; 154 + 155 + type LabelBehavioralMapping = { 156 + [K in LabelBlur]: { [K in LabelTarget]: { [K in ModerationContext]?: ModerationBehavior } }; 157 + }; 158 + 159 + const LABEL_BEHAVIORAL_MAPPING: LabelBehavioralMapping = { 160 + [BlurForced]: { 161 + [TargetAccount]: { 162 + [ContextProfileList]: BehaviorBlur, 163 + [ContextProfileView]: BehaviorBlur, 164 + [ContextContentList]: BehaviorBlur, 165 + [ContextContentView]: BehaviorBlur, 166 + }, 167 + [TargetProfile]: { 168 + [ContextProfileList]: BehaviorBlur, 169 + [ContextProfileView]: BehaviorBlur, 170 + }, 171 + [TargetContent]: { 172 + [ContextContentList]: BehaviorBlur, 173 + [ContextContentView]: BehaviorBlur, 174 + }, 175 + }, 176 + [BlurContent]: { 177 + [TargetAccount]: { 178 + [ContextProfileList]: BehaviorAlertOrInform, 179 + [ContextProfileView]: BehaviorAlertOrInform, 180 + [ContextContentList]: BehaviorBlur, 181 + [ContextContentView]: BehaviorBlurIfAdultOrAlert, 182 + }, 183 + [TargetProfile]: { 184 + [ContextProfileList]: BehaviorAlertOrInform, 185 + [ContextProfileView]: BehaviorAlertOrInform, 186 + }, 187 + [TargetContent]: { 188 + [ContextContentList]: BehaviorBlur, 189 + [ContextContentView]: BehaviorBlurIfAdultOrAlert, 190 + }, 191 + }, 192 + [BlurMedia]: { 193 + [TargetAccount]: { 194 + [ContextProfileList]: BehaviorAlertOrInform, 195 + [ContextProfileView]: BehaviorAlertOrInform, 196 + [ContextProfileMedia]: BehaviorBlur, 197 + }, 198 + [TargetProfile]: { 199 + [ContextProfileList]: BehaviorAlertOrInform, 200 + [ContextProfileView]: BehaviorAlertOrInform, 201 + [ContextProfileMedia]: BehaviorBlur, 202 + }, 203 + [TargetContent]: { 204 + [ContextContentMedia]: BehaviorBlur, 205 + }, 206 + }, 207 + [BlurNone]: { 208 + [TargetAccount]: { 209 + [ContextProfileList]: BehaviorAlertOrInform, 210 + [ContextProfileView]: BehaviorAlertOrInform, 211 + [ContextContentList]: BehaviorAlertOrInform, 212 + [ContextContentView]: BehaviorAlertOrInform, 213 + }, 214 + [TargetProfile]: { 215 + [ContextProfileList]: BehaviorAlertOrInform, 216 + [ContextProfileView]: BehaviorAlertOrInform, 217 + }, 218 + [TargetContent]: { 219 + [ContextContentList]: BehaviorAlertOrInform, 220 + [ContextContentView]: BehaviorAlertOrInform, 221 + }, 222 + }, 223 + }; 224 + 225 + export const getLocalizedLabel = (label: LabelDefinition): LabelLocale => { 226 + // Get English definitions first before giving up 227 + const locales = label.l; 228 + 229 + return locales.length > 0 230 + ? locales.find(({ i }) => i.split('-')[0] === 'en') || locales[0] 231 + : { i: 'en', n: label.i, d: `` }; 232 + }; 233 + 234 + export const CauseLabel = 0; 235 + export const CauseMutedPermanent = 1; 236 + export const CauseMutedTemporary = 2; 237 + export const CauseMutedKeyword = 3; 238 + 239 + export type ModerationCauseType = 0 | 1 | 2 | 3; 240 + 241 + interface BaseModerationCause { 242 + /** Cause type */ 243 + t: ModerationCauseType; 244 + /** Cause priority */ 245 + p: 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8; 246 + } 247 + 248 + export interface LabelModerationCause { 249 + t: typeof CauseLabel; 250 + p: 1 | 2 | 5 | 7 | 8; 251 + 252 + /** Target of the label */ 253 + k: LabelTarget; 254 + 255 + /** Label source, self-label if empty */ 256 + s: ModerationLabeler | undefined; 257 + /** Whether label definition is global or not (also true for nonexistent definitions) */ 258 + g: boolean; 259 + 260 + /** Label object */ 261 + l: Label; 262 + /** Label definition */ 263 + d: LabelDefinition; 264 + /** User-set preference for this label */ 265 + v: LabelPreference; 266 + } 267 + 268 + export interface MutedPermanentModerationCause extends BaseModerationCause { 269 + t: typeof CauseMutedPermanent; 270 + p: 6; 271 + } 272 + 273 + export interface MutedTemporaryModerationCause extends BaseModerationCause { 274 + t: typeof CauseMutedTemporary; 275 + p: 6; 276 + 277 + /** Temporary mute duration */ 278 + d: number; 279 + } 280 + 281 + export interface MutedKeywordModerationCause extends BaseModerationCause { 282 + t: typeof CauseMutedKeyword; 283 + p: 6; 284 + 285 + /** Name of keyword muting in effect */ 286 + n: string; 287 + /** User's set preference for this cause */ 288 + v: KeywordPreference; 289 + } 290 + 291 + export type ModerationCause = 292 + | LabelModerationCause 293 + | MutedPermanentModerationCause 294 + | MutedTemporaryModerationCause 295 + | MutedKeywordModerationCause; 296 + 297 + export type KeywordFilterMatcher = [keyword: string, whole: boolean]; 298 + 299 + export interface KeywordFilter { 300 + // Only these properties are ever used during actual filtering 301 + name: string; 302 + pref: KeywordPreference; 303 + match: string; 304 + noFollows: boolean; 305 + 306 + // These are used in the preferences UI 307 + id: string; 308 + matchers: KeywordFilterMatcher[]; 309 + } 310 + 311 + export interface ModerationLabeler { 312 + /** DID of the labeler */ 313 + did: At.DID; 314 + /** Profile details of the labeler */ 315 + profile: { 316 + avatar?: string; 317 + handle: string; 318 + displayName?: string; 319 + }; 320 + /** Labels that this service claims to provide */ 321 + provides: string[]; 322 + /** Labels defined by this service */ 323 + definitions: LabelDefinitionMapping; 324 + } 325 + 326 + export interface ModerationLabelerPreferences { 327 + /** Whether it should apply takedowns from this labeler */ 328 + redact: boolean; 329 + /** Whether it's privileged to hide content by default */ 330 + privileged: boolean; 331 + /** Preferences for local-defined labels from this labeler */ 332 + labels: LabelPreferenceMapping; 333 + } 334 + 335 + export interface ModerationPreferences { 336 + /** Preferences for global-defined labels */ 337 + labels: LabelPreferenceMapping; 338 + /** Preferences for labels from subscribed labelers */ 339 + labelers: Record<At.DID, ModerationLabelerPreferences>; 340 + /** Keyword filters */ 341 + keywords: KeywordFilter[]; 342 + 343 + /** List of users to hide reposts from */ 344 + hideReposts: At.DID[]; 345 + /** Mapping of users and how long they should be temporarily muted for */ 346 + tempMutes: Record<At.DID, number | undefined>; 347 + } 348 + 349 + export interface ModerationOptions { 350 + _filtersCache?: [raw: string, match: RegExp][]; 351 + 352 + preferences: ModerationPreferences; 353 + labelerDefinitions: Record<At.DID, ModerationLabeler>; 354 + } 355 + 356 + export const decideLabelModeration = ( 357 + accu: ModerationCause[], 358 + target: LabelTarget, 359 + labels: Label[] | undefined, 360 + userDid: At.DID, 361 + opts: ModerationOptions, 362 + ) => { 363 + if (labels /* && labels.length > 0 */) { 364 + const globalPrefs = opts.preferences.labels; 365 + const labelerPrefs = opts.preferences.labelers; 366 + const labelerDefinitions = opts.labelerDefinitions; 367 + 368 + for (let i = 0, il = labels.length; i < il; i++) { 369 + const label = labels[i]; 370 + 371 + const val = label.val; 372 + const src = label.src; 373 + 374 + const isSelfLabeled = src === userDid; 375 + const isSystem = val[0] === '!'; 376 + 377 + let labeler: ModerationLabeler | undefined = labelerDefinitions[src]; 378 + let def: LabelDefinition | undefined = GLOBAL_LABELS[val]; 379 + let isGlobalDef = true; 380 + 381 + if (!isSystem && labeler && val in labeler.definitions) { 382 + isGlobalDef = false; 383 + def = labeler.definitions[val]; 384 + } 385 + 386 + // skip if: 387 + // - we don't have the definitions for this label value 388 + // - this is self-labeled, and the definition says it can't be used for it 389 + // - the service isn't reporting that it's using this global label definition 390 + if ( 391 + !def || 392 + (isSelfLabeled && def.f & FlagsNoSelf) || 393 + (labeler && isGlobalDef && !labeler.provides.includes(val)) 394 + ) { 395 + continue; 396 + } 397 + 398 + const labelerPref = labelerPrefs[src]; 399 + 400 + const defaultLabelPref = def.d; 401 + let labelPref = (labelerPref?.labels ?? globalPrefs)[val]; 402 + 403 + if (labelPref === undefined) { 404 + if (defaultLabelPref === PreferenceHide && labelerPref && !labelerPref.privileged) { 405 + labelPref = PreferenceWarn; 406 + } else { 407 + labelPref = defaultLabelPref; 408 + } 409 + } 410 + 411 + if (labelPref !== PreferenceHide && labelPref !== PreferenceWarn) { 412 + continue; 413 + } 414 + 415 + let prio: LabelModerationCause['p']; 416 + if (def.f & FlagsForced) { 417 + prio = 1; 418 + } else if (labelPref === PreferenceHide) { 419 + prio = 2; 420 + } else if (def.b === BlurContent) { 421 + prio = def.f & FlagsAdultOnly ? 5 : 7; 422 + } else if (def.b === BlurMedia) { 423 + prio = 7; 424 + } else { 425 + if (def.s === SeverityNone) { 426 + continue; 427 + } 428 + 429 + prio = 8; 430 + } 431 + 432 + accu.push({ 433 + t: CauseLabel, 434 + p: prio, 435 + 436 + k: target, 437 + 438 + s: labeler, 439 + g: isGlobalDef, 440 + l: label, 441 + d: def, 442 + v: labelPref, 443 + }); 444 + } 445 + } 446 + }; 447 + 448 + export const decideMutedPermanentModeration = (accu: ModerationCause[], muted: boolean | undefined) => { 449 + if (muted) { 450 + accu.push({ t: CauseMutedPermanent, p: 6 }); 451 + } 452 + }; 453 + 454 + export const decideMutedTemporaryModeration = ( 455 + accu: ModerationCause[], 456 + userDid: At.DID, 457 + opts: ModerationOptions, 458 + ) => { 459 + const duration = isProfileTempMuted(opts, userDid); 460 + 461 + if (duration != null) { 462 + accu.push({ t: CauseMutedTemporary, p: 6, d: duration }); 463 + } 464 + }; 465 + 466 + const shouldAllowKeywordFilter = (filterPref: KeywordPreference, pref: KeywordPreference) => { 467 + if (pref === PreferenceWarn) { 468 + return filterPref === pref || filterPref === PreferenceHide; 469 + } 470 + 471 + if (pref === PreferenceHide) { 472 + return filterPref === pref; 473 + } 474 + 475 + return false; 476 + }; 477 + 478 + export const decideMutedKeywordModeration = ( 479 + accu: ModerationCause[], 480 + text: string, 481 + following: boolean, 482 + pref: KeywordPreference, 483 + opts: ModerationOptions, 484 + ) => { 485 + const filters = opts.preferences.keywords; 486 + 487 + let cache = opts._filtersCache; 488 + let init = true; 489 + 490 + for (let idx = 0, len = filters.length; idx < len; idx++) { 491 + const filter = filters[idx]; 492 + 493 + if (!shouldAllowKeywordFilter(filter.pref, pref) || (following && filter.noFollows)) { 494 + continue; 495 + } 496 + 497 + if (init) { 498 + if (cache) { 499 + cache.length = len; 500 + } else { 501 + cache = opts._filtersCache = new Array(len); 502 + } 503 + 504 + init = !init; 505 + } 506 + 507 + let match = filter.match; 508 + 509 + let matcher: RegExp; 510 + let cachedMatcher = cache![idx]; 511 + 512 + if (!cachedMatcher || cachedMatcher[0] !== match) { 513 + cache![idx] = [match, (matcher = new RegExp(match, 'i'))]; 514 + } else { 515 + matcher = cachedMatcher[1]; 516 + } 517 + 518 + if (matcher.test(text)) { 519 + accu.push({ t: CauseMutedKeyword, p: 6, n: filter.name, v: pref }); 520 + } 521 + } 522 + 523 + return accu; 524 + }; 525 + 526 + export interface ModerationUI { 527 + /** Whether it's overridable */ 528 + o: boolean; 529 + 530 + /** Filters */ 531 + f: ModerationCause[]; 532 + /** Blurs */ 533 + b: ModerationCause[]; 534 + /** Alerts */ 535 + a: ModerationCause[]; 536 + /** Informs */ 537 + i: ModerationCause[]; 538 + } 539 + 540 + export const getModerationUI = (causes: ModerationCause[] = [], context: ModerationContext): ModerationUI => { 541 + const filters: ModerationCause[] = []; 542 + const blurs: ModerationCause[] = []; 543 + const alerts: ModerationCause[] = []; 544 + const informs: ModerationCause[] = []; 545 + 546 + let overridable = true; 547 + 548 + for (let i = 0, il = causes.length; i < il; i++) { 549 + const cause = causes[i]; 550 + const type = cause.t; 551 + 552 + if (type === CauseLabel) { 553 + const target = cause.k; 554 + const def = cause.d; 555 + 556 + const flags = def.f; 557 + const blur = def.b; 558 + const severity = def.s; 559 + 560 + const behavior = LABEL_BEHAVIORAL_MAPPING[blur][target][context]; 561 + 562 + if (cause.v === PreferenceHide) { 563 + if ( 564 + (context === ContextProfileList && target === TargetAccount) || 565 + (context === ContextContentList && (target === TargetContent || target === TargetAccount)) 566 + ) { 567 + filters.push(cause); 568 + } 569 + } 570 + 571 + if (behavior === BehaviorBlur || (behavior === BehaviorBlurIfAdultOrAlert && flags & FlagsAdultOnly)) { 572 + blurs.push(cause); 573 + 574 + if (flags & FlagsForced) { 575 + overridable = false; 576 + } 577 + } else if (behavior === BehaviorAlertOrInform || behavior === BehaviorBlurIfAdultOrAlert) { 578 + if (severity === SeverityAlert) { 579 + alerts.push(cause); 580 + } else if (severity === SeverityInform) { 581 + informs.push(cause); 582 + } 583 + } 584 + } else if (type === CauseMutedKeyword) { 585 + if (context === ContextContentList) { 586 + if (cause.v === PreferenceHide) { 587 + filters.push(cause); 588 + } 589 + 590 + blurs.push(cause); 591 + } 592 + } else if (type === CauseMutedTemporary || type === CauseMutedPermanent) { 593 + if (context === ContextContentList) { 594 + filters.push(cause); 595 + } 596 + 597 + if (context !== ContextContentMedia && context !== ContextProfileMedia) { 598 + blurs.push(cause); 599 + } 600 + } 601 + } 602 + 603 + return { 604 + o: overridable, 605 + f: filters.sort(sortByPriority), 606 + b: blurs.sort(sortByPriority), 607 + a: alerts, 608 + i: informs, 609 + }; 610 + }; 611 + 612 + const sortByPriority = (a: ModerationCause, b: ModerationCause) => { 613 + return a.p - b.p; 614 + }; 615 + 616 + export const isProfileTempMuted = (opts: ModerationOptions, actor: At.DID): number | null => { 617 + const date = opts.preferences.tempMutes[actor]; 618 + return date !== undefined && Date.now() < date ? date : null; 619 + };
+107
src/api/moderation/labeler.ts
··· 1 + import type { AppBskyLabelerDefs } from '@mary/bluesky-client/lexicons'; 2 + 3 + import { mapDefined } from '~/lib/misc'; 4 + 5 + import { 6 + BlurContent, 7 + BlurMedia, 8 + BlurNone, 9 + FlagsAdultOnly, 10 + FlagsNone, 11 + PreferenceIgnore, 12 + PreferenceWarn, 13 + SeverityAlert, 14 + SeverityInform, 15 + SeverityNone, 16 + type LabelBlur, 17 + type LabelDefinitionMapping, 18 + type LabelPreference, 19 + type LabelSeverity, 20 + type ModerationLabeler, 21 + } from '.'; 22 + 23 + export const interpretLabelerDefinition = ( 24 + service: AppBskyLabelerDefs.LabelerViewDetailed, 25 + ): ModerationLabeler => { 26 + const creator = service.creator; 27 + const policies = service.policies; 28 + 29 + const values = policies.labelValues; 30 + 31 + const supported = new Set(values); 32 + const defs: LabelDefinitionMapping = {}; 33 + 34 + // Sort the definitions as per labelValues 35 + for (const def of policies.labelValueDefinitions || []) { 36 + const id = def.identifier; 37 + 38 + // - Skip system label 39 + // - Skip if it's not even on labelValues 40 + if (id[0] === '!' || !supported.has(id)) { 41 + continue; 42 + } 43 + 44 + defs[id] = { 45 + i: id, 46 + d: convertPreferenceValue(def.defaultSetting), 47 + b: convertBlurValue(def.blurs), 48 + s: convertSeverityValue(def.severity), 49 + f: def.adultOnly ? FlagsAdultOnly : FlagsNone, 50 + l: mapDefined(def.locales, (locale) => { 51 + try { 52 + // Normalize locale codes 53 + const parsed = new Intl.Locale(locale.lang); 54 + 55 + return { 56 + i: parsed.baseName, 57 + n: locale.name, 58 + d: locale.description, 59 + }; 60 + } catch {} 61 + }), 62 + }; 63 + } 64 + 65 + return { 66 + did: creator.did, 67 + profile: { 68 + handle: creator.handle, 69 + avatar: creator.avatar, 70 + displayName: creator.displayName, 71 + }, 72 + provides: values, 73 + definitions: defs, 74 + }; 75 + }; 76 + 77 + const convertPreferenceValue = (value: string | undefined): LabelPreference => { 78 + if (value === 'warn' || value === 'hide') { 79 + return PreferenceWarn; 80 + } 81 + 82 + return PreferenceIgnore; 83 + }; 84 + 85 + const convertBlurValue = (value: string | undefined): LabelBlur => { 86 + if (value === 'content') { 87 + return BlurContent; 88 + } 89 + 90 + if (value === 'media') { 91 + return BlurMedia; 92 + } 93 + 94 + return BlurNone; 95 + }; 96 + 97 + const convertSeverityValue = (value: string | undefined): LabelSeverity => { 98 + if (value === 'alert') { 99 + return SeverityAlert; 100 + } 101 + 102 + if (value === 'inform') { 103 + return SeverityInform; 104 + } 105 + 106 + return SeverityNone; 107 + };
+25
src/api/queries/handle.ts
··· 1 + import { createQuery } from '@mary/solid-query'; 2 + 3 + import { useAgent } from '~/lib/states/agent'; 4 + 5 + export const useResolveHandleQuery = (handle: () => string) => { 6 + const { rpc } = useAgent(); 7 + 8 + return createQuery(() => { 9 + const $handle = handle(); 10 + 11 + return { 12 + queryKey: ['resolve-handle', $handle], 13 + async queryFn(ctx) { 14 + const { data } = await rpc.get('com.atproto.identity.resolveHandle', { 15 + signal: ctx.signal, 16 + params: { 17 + handle: $handle, 18 + }, 19 + }); 20 + 21 + return data.did; 22 + }, 23 + }; 24 + }); 25 + };
+46
src/api/queries/profile.ts
··· 1 + import type { AppBskyActorDefs, At } from '@mary/bluesky-client/lexicons'; 2 + import { QueryClient, createQuery, type QueryPersister } from '@mary/solid-query'; 3 + 4 + import { useAgent } from '~/lib/states/agent'; 5 + import { useSession } from '~/lib/states/session'; 6 + 7 + export const useProfileQuery = (did: () => At.DID | undefined, persister?: QueryPersister) => { 8 + const { rpc } = useAgent(); 9 + const { currentAccount } = useSession(); 10 + 11 + return createQuery(() => { 12 + const $did = did(); 13 + 14 + return { 15 + queryKey: ['profile', $did], 16 + enabled: $did !== undefined, 17 + persister: persister as any, 18 + async queryFn(ctx) { 19 + const { data } = await rpc.get('app.bsky.actor.getProfile', { 20 + signal: ctx.signal, 21 + params: { 22 + actor: $did!, 23 + }, 24 + }); 25 + 26 + if (currentAccount !== undefined && currentAccount.did === $did) { 27 + // Unset `knownFollowers` as we don't need that on our own profile. 28 + data.viewer!.knownFollowers = undefined; 29 + } 30 + 31 + return data; 32 + }, 33 + }; 34 + }); 35 + }; 36 + 37 + export function* findAllProfilesInQueryData( 38 + queryClient: QueryClient, 39 + did: At.DID, 40 + ): Generator<AppBskyActorDefs.ProfileViewDetailed> { 41 + const data = queryClient.getQueryData<AppBskyActorDefs.ProfileViewDetailed>(['profile', did]); 42 + 43 + if (data !== undefined && data.did === did) { 44 + yield data; 45 + } 46 + }
+755
src/api/queries/timeline.ts
··· 1 + import { createMemo, createRenderEffect, untrack } from 'solid-js'; 2 + 3 + import type { BskyXRPC } from '@mary/bluesky-client'; 4 + import type { 5 + AppBskyActorDefs, 6 + AppBskyEmbedRecord, 7 + AppBskyFeedDefs, 8 + AppBskyFeedGetTimeline, 9 + AppBskyFeedPost, 10 + At, 11 + } from '@mary/bluesky-client/lexicons'; 12 + import { 13 + QueryClient, 14 + createInfiniteQuery, 15 + createQuery, 16 + useQueryClient, 17 + type InfiniteData, 18 + } from '@mary/solid-query'; 19 + 20 + import { assert } from '~/lib/invariant'; 21 + import { useAgent } from '~/lib/states/agent'; 22 + import { useModerationOptions } from '~/lib/states/moderation'; 23 + import { useSession } from '~/lib/states/session'; 24 + 25 + import { 26 + createJoinedItems, 27 + type EnsuredReplyRef, 28 + type EnsuredTimelineItem, 29 + type PostFilter, 30 + type SliceFilter, 31 + type TimelineSlice, 32 + type UiTimelineItem, 33 + } from '../models/timeline'; 34 + import { 35 + ContextContentList, 36 + PreferenceHide, 37 + TargetContent, 38 + decideLabelModeration, 39 + decideMutedKeywordModeration, 40 + getModerationUI, 41 + type ModerationCause, 42 + type ModerationOptions, 43 + } from '../moderation'; 44 + 45 + import { EQUALS_DEQUAL } from '../utils/dequal'; 46 + import { embedViewRecordToPostView, getEmbeddedPost, unwrapPostEmbedText } from '../utils/post'; 47 + import { resetInfiniteData, wrapQuery } from '../utils/query'; 48 + import { parseAtUri } from '../utils/strings'; 49 + 50 + type PostRecord = AppBskyFeedPost.Record; 51 + 52 + export interface FollowingTimelineParams { 53 + type: 'following'; 54 + showReplies: 'follows' | boolean; 55 + showReposts: boolean; 56 + showQuotes: boolean; 57 + } 58 + 59 + export interface FeedTimelineParams { 60 + type: 'feed'; 61 + uri: string; 62 + showReplies: boolean; 63 + showReposts: boolean; 64 + showQuotes: boolean; 65 + } 66 + 67 + export interface ListTimelineParams { 68 + type: 'list'; 69 + uri: string; 70 + showReplies: boolean; 71 + showQuotes: boolean; 72 + } 73 + 74 + export interface ProfileTimelineParams { 75 + type: 'profile'; 76 + actor: At.DID; 77 + tab: 'posts' | 'replies' | 'likes' | 'media'; 78 + } 79 + 80 + export interface SearchTimelineParams { 81 + type: 'search'; 82 + query: string; 83 + sort: 'top' | 'latest'; 84 + } 85 + 86 + export type TimelineParams = 87 + | FeedTimelineParams 88 + | FollowingTimelineParams 89 + | ListTimelineParams 90 + | ProfileTimelineParams 91 + | SearchTimelineParams; 92 + 93 + export interface TimelinePage { 94 + cursor: string | undefined; 95 + cid: string | undefined; 96 + items: UiTimelineItem[]; 97 + } 98 + 99 + export interface TimelineLatestResult { 100 + cid: string | undefined; 101 + } 102 + 103 + const MAX_TIMELINE_POSTS = 50; 104 + 105 + export function* findAllPostsInQueryData( 106 + queryClient: QueryClient, 107 + uri: string, 108 + includeQuote = false, 109 + ): Generator<AppBskyFeedDefs.PostView> { 110 + const entries = queryClient.getQueriesData<InfiniteData<TimelinePage>>({ 111 + queryKey: ['timeline'], 112 + }); 113 + 114 + for (const [_key, data] of entries) { 115 + if (data === undefined) { 116 + continue; 117 + } 118 + 119 + for (const page of data.pages) { 120 + for (const item of page.items) { 121 + const post = item.post; 122 + const reply = item.reply; 123 + 124 + if (post.uri === uri) { 125 + yield post; 126 + } 127 + 128 + if (reply !== undefined) { 129 + const parent = reply.parent; 130 + const root = reply.root; 131 + 132 + if (parent !== undefined) { 133 + if (parent.uri === uri) { 134 + yield parent; 135 + } 136 + 137 + if (includeQuote) { 138 + const embeddedPost = getEmbeddedPost(parent.embed); 139 + if (embeddedPost && embeddedPost.uri === uri) { 140 + yield embedViewRecordToPostView(embeddedPost); 141 + } 142 + } 143 + } 144 + 145 + if (root !== undefined) { 146 + if (root.uri === uri) { 147 + yield root; 148 + } 149 + 150 + if (includeQuote) { 151 + const embeddedPost = getEmbeddedPost(root.embed); 152 + if (embeddedPost && embeddedPost.uri === uri) { 153 + yield embedViewRecordToPostView(embeddedPost); 154 + } 155 + } 156 + } 157 + } 158 + } 159 + } 160 + } 161 + } 162 + 163 + export function* findAllProfilesInQueryData( 164 + queryClient: QueryClient, 165 + did: At.DID, 166 + ): Generator<AppBskyActorDefs.ProfileViewBasic> { 167 + const entries = queryClient.getQueriesData<InfiniteData<TimelinePage>>({ 168 + queryKey: ['timeline'], 169 + }); 170 + 171 + for (const [_key, data] of entries) { 172 + if (data === undefined) { 173 + continue; 174 + } 175 + 176 + for (const page of data.pages) { 177 + for (const item of page.items) { 178 + const post = item.post; 179 + const reply = item.reply; 180 + 181 + if (post.author.did === did) { 182 + yield post.author; 183 + } 184 + 185 + if (reply !== undefined) { 186 + const parent = reply.parent; 187 + const root = reply.root; 188 + 189 + if (parent !== undefined) { 190 + if (parent.author.did === did) { 191 + yield parent.author; 192 + } 193 + 194 + const embeddedPost = getEmbeddedPost(parent.embed); 195 + if (embeddedPost && embeddedPost.author.did === did) { 196 + yield embeddedPost.author; 197 + } 198 + } 199 + 200 + if (root !== undefined) { 201 + if (root.author.did === did) { 202 + yield root.author; 203 + } 204 + 205 + const embeddedPost = getEmbeddedPost(root.embed); 206 + if (embeddedPost && embeddedPost.author.did === did) { 207 + yield embeddedPost.author; 208 + } 209 + } 210 + } 211 + } 212 + } 213 + } 214 + } 215 + 216 + export const useTimelineQuery = (_params: () => TimelineParams) => { 217 + const getParams = createMemo(() => _params(), EQUALS_DEQUAL); 218 + 219 + const { rpc } = useAgent(); 220 + const { currentAccount } = useSession(); 221 + const queryClient = useQueryClient(); 222 + const moderationOptions = useModerationOptions(); 223 + 224 + const limit = MAX_TIMELINE_POSTS; 225 + 226 + const timeline = createInfiniteQuery(() => { 227 + const params = getParams(); 228 + 229 + return { 230 + queryKey: ['timeline', params], 231 + initialPageParam: undefined, 232 + getNextPageParam: (last) => last.cursor, 233 + staleTime: Infinity, 234 + structuralSharing: false, 235 + queryFn: wrapQuery<TimelinePage, string | undefined>(async (ctx) => { 236 + const uid = currentAccount?.did; 237 + const moderation = moderationOptions(); 238 + 239 + const type = params.type; 240 + 241 + let cursor = ctx.pageParam; 242 + 243 + let sliceFilter: SliceFilter | undefined; 244 + let postFilter: PostFilter | undefined; 245 + 246 + if (type === 'following') { 247 + assert(uid !== undefined); 248 + 249 + sliceFilter = createHomeSliceFilter(uid, params.showReplies === 'follows'); 250 + 251 + postFilter = combine([ 252 + createHiddenRepostFilter(moderation), 253 + createDuplicatePostFilter(), 254 + 255 + !params.showReplies && createHideRepliesFilter(), 256 + !params.showQuotes && createHideQuotesFilter(), 257 + !params.showReposts && createHideRepostsFilter(), 258 + 259 + createInvalidReplyFilter(), 260 + createLabelPostFilter(moderation), 261 + createTempMutePostFilter(uid, moderation), 262 + ]); 263 + } else if (type === 'feed' || type === 'list') { 264 + sliceFilter = createFeedSliceFilter(); 265 + 266 + postFilter = combine([ 267 + type === 'feed' && createHiddenRepostFilter(moderation), 268 + createDuplicatePostFilter(), 269 + 270 + !params.showReplies && createHideRepliesFilter(), 271 + !params.showQuotes && createHideQuotesFilter(), 272 + type === 'feed' && !params.showReposts && createHideRepostsFilter(), 273 + 274 + createLabelPostFilter(moderation), 275 + uid && createTempMutePostFilter(uid, moderation), 276 + ]); 277 + } else if (type === 'profile') { 278 + postFilter = createLabelPostFilter(moderation); 279 + 280 + if (params.tab === 'posts') { 281 + sliceFilter = createProfileSliceFilter(params.actor); 282 + postFilter = combine([createInvalidReplyFilter(), createLabelPostFilter(moderation)]); 283 + } 284 + } else { 285 + postFilter = createLabelPostFilter(moderation); 286 + } 287 + 288 + const timeline = await fetchPage(rpc, params, limit, cursor, ctx.signal); 289 + 290 + const feed = timeline.feed; 291 + const result = createJoinedItems(feed, sliceFilter, postFilter); 292 + 293 + const page: TimelinePage = { 294 + cursor: timeline.cursor, 295 + cid: feed.length > 0 ? feed[0].post.cid : undefined, 296 + items: result, 297 + }; 298 + 299 + return page; 300 + }), 301 + }; 302 + }); 303 + 304 + const latest = createQuery<TimelineLatestResult>(() => { 305 + const params = getParams(); 306 + const timelineData = timeline.data; 307 + 308 + return { 309 + queryKey: ['timeline-latest', params], 310 + enabled: timelineData !== undefined, 311 + staleTime: 30_000, 312 + refetchOnWindowFocus: (query) => { 313 + return !isTimelineStale(timelineData, query.state.data); 314 + }, 315 + refetchInterval: (query) => { 316 + if (!isTimelineStale(timelineData, query.state.data)) { 317 + // 1 minute, or 5 minutes 318 + return !document.hidden ? 60_000 : 5 * 60_000; 319 + } 320 + 321 + return false; 322 + }, 323 + async queryFn(ctx): Promise<TimelineLatestResult> { 324 + const timeline = await fetchPage(rpc, params, 1, undefined, ctx.signal); 325 + const feed = timeline.feed; 326 + 327 + return { cid: feed.length > 0 ? feed[0].post.cid : undefined }; 328 + }, 329 + }; 330 + }); 331 + 332 + const reset = () => { 333 + const params = untrack(getParams); 334 + 335 + resetInfiniteData(queryClient, ['timeline', params]); 336 + timeline.refetch(); 337 + }; 338 + 339 + const isStale = () => { 340 + return isTimelineStale(timeline.data, latest.data); 341 + }; 342 + 343 + // This is a render effect such that changes to the timeline query immediately 344 + // mutates the stale check query before it has the chance to react itself. 345 + createRenderEffect((prev: typeof timeline.data | 0) => { 346 + const next = timeline.data; 347 + 348 + if (prev !== 0 && next) { 349 + const pages = next.pages; 350 + const length = pages.length; 351 + 352 + // Only mutate stale check if the length is exactly 1, as in, we've just 353 + // loaded the timeline, or refreshed it. 354 + if (length === 1) { 355 + // Untrack so we don't become dependent on this. 356 + const params = untrack(getParams); 357 + 358 + queryClient.setQueryData( 359 + ['timeline-latest', params], 360 + { cid: pages[0].cid }, 361 + { updatedAt: timeline.dataUpdatedAt }, 362 + ); 363 + } 364 + } 365 + 366 + return next; 367 + }, 0 as const); 368 + 369 + return { timeline, reset, isStale }; 370 + }; 371 + 372 + const isTimelineStale = ( 373 + timelineData: InfiniteData<TimelinePage> | undefined, 374 + latestData: TimelineLatestResult | undefined, 375 + ) => { 376 + return latestData?.cid && timelineData ? latestData.cid !== timelineData.pages[0].cid : false; 377 + }; 378 + 379 + //// Raw fetch 380 + const fetchPage = async ( 381 + rpc: BskyXRPC, 382 + params: TimelineParams, 383 + limit: number, 384 + cursor: string | undefined, 385 + signal: AbortSignal, 386 + ): Promise<AppBskyFeedGetTimeline.Output> => { 387 + const type = params.type; 388 + 389 + if (type === 'following') { 390 + const response = await rpc.get('app.bsky.feed.getTimeline', { 391 + signal: signal, 392 + params: { 393 + algorithm: 'reverse-chronological', 394 + cursor: cursor, 395 + limit: limit, 396 + }, 397 + }); 398 + 399 + return response.data; 400 + } else if (type === 'feed') { 401 + const response = await rpc.get('app.bsky.feed.getFeed', { 402 + signal: signal, 403 + headers: { 404 + 'accent-language': navigator.languages.join(','), 405 + }, 406 + params: { 407 + feed: params.uri, 408 + cursor: cursor, 409 + limit: limit, 410 + }, 411 + }); 412 + 413 + return response.data; 414 + } else if (type === 'list') { 415 + const response = await rpc.get('app.bsky.feed.getListFeed', { 416 + signal: signal, 417 + params: { 418 + list: params.uri, 419 + cursor: cursor, 420 + limit: limit, 421 + }, 422 + }); 423 + 424 + return response.data; 425 + } else if (type === 'profile') { 426 + if (params.tab === 'likes') { 427 + const response = await rpc.get('app.bsky.feed.getActorLikes', { 428 + signal: signal, 429 + params: { 430 + actor: params.actor, 431 + cursor: cursor, 432 + limit: limit, 433 + }, 434 + }); 435 + 436 + return response.data; 437 + } else { 438 + const response = await rpc.get('app.bsky.feed.getAuthorFeed', { 439 + signal: signal, 440 + params: { 441 + actor: params.actor, 442 + cursor: cursor, 443 + limit: limit, 444 + filter: 445 + params.tab === 'media' 446 + ? 'posts_with_media' 447 + : params.tab === 'replies' 448 + ? 'posts_with_replies' 449 + : 'posts_and_author_threads', 450 + }, 451 + }); 452 + 453 + return response.data; 454 + } 455 + } else if (type === 'search') { 456 + const response = await rpc.get('app.bsky.feed.searchPosts', { 457 + signal: signal, 458 + params: { 459 + sort: 'latest', 460 + q: params.query, 461 + cursor: cursor, 462 + limit: limit, 463 + }, 464 + }); 465 + 466 + const data = response.data; 467 + 468 + return { cursor: data.cursor, feed: data.posts.map((view) => ({ post: view })) }; 469 + } else { 470 + assert(false, `Unknown type: ${type}`); 471 + } 472 + }; 473 + 474 + /// Timeline filters 475 + type FilterFn<T> = (data: T) => boolean; 476 + 477 + const combine = <T>(filters: Array<undefined | false | FilterFn<T>>): FilterFn<T> | undefined => { 478 + const filtered = filters.filter((filter): filter is FilterFn<T> => !!filter); 479 + const len = filtered.length; 480 + 481 + // if (len === 1) { 482 + // return filtered[0]; 483 + // } 484 + 485 + // if (len === 0) { 486 + // return; 487 + // } 488 + 489 + return (data: T) => { 490 + for (let idx = 0; idx < len; idx++) { 491 + const filter = filtered[idx]; 492 + 493 + if (!filter(data)) { 494 + return false; 495 + } 496 + } 497 + 498 + return true; 499 + }; 500 + }; 501 + 502 + //// Post filters 503 + const createDuplicatePostFilter = (): PostFilter => { 504 + const map: Record<string, boolean> = {}; 505 + 506 + return (item) => { 507 + const uri = item.post.uri; 508 + 509 + if (map[uri]) { 510 + return false; 511 + } 512 + 513 + return (map[uri] = true); 514 + }; 515 + }; 516 + 517 + const createInvalidReplyFilter = (): PostFilter => { 518 + return (item) => { 519 + // Don't allow posts that isn't being a hydrated with a reply when it should 520 + return ( 521 + // Allow reposts 522 + item.reason?.$type === 'app.bsky.feed.defs#reasonRepost' || 523 + // Allow posts with a timeline reply attached 524 + item.reply?.parent !== undefined || 525 + // Allow posts whose record doesn't have the reply object 526 + (item.post.record as PostRecord).reply === undefined 527 + ); 528 + }; 529 + }; 530 + 531 + const createLabelPostFilter = (opts: ModerationOptions): PostFilter | undefined => { 532 + return (item) => { 533 + const post = item.post; 534 + 535 + const author = post.author; 536 + const record = post.record as PostRecord; 537 + 538 + const isFollowing = !!author.viewer?.following; 539 + const text = record.text + unwrapPostEmbedText(record.embed); 540 + 541 + record.embed; 542 + 543 + const accu: ModerationCause[] = []; 544 + decideLabelModeration(accu, TargetContent, post.labels, post.author.did, opts); 545 + decideMutedKeywordModeration(accu, text, isFollowing, PreferenceHide, opts); 546 + 547 + const decision = getModerationUI(accu, ContextContentList); 548 + 549 + return decision.f.length === 0; 550 + }; 551 + }; 552 + 553 + const createHiddenRepostFilter = (opts: ModerationOptions): PostFilter | undefined => { 554 + const hidden = opts.preferences.hideReposts; 555 + 556 + if (hidden.length === 0) { 557 + return; 558 + } 559 + 560 + return (item) => { 561 + const reason = item.reason; 562 + 563 + return !reason || reason.$type !== 'app.bsky.feed.defs#reasonRepost' || !hidden.includes(reason.by.did); 564 + }; 565 + }; 566 + 567 + const createTempMutePostFilter = (uid: At.DID, opts: ModerationOptions): PostFilter | undefined => { 568 + // We won't be checking if any of the temporary mutes are stale, those should 569 + // be handled within the UI. 570 + const mutes = opts.preferences.tempMutes; 571 + const hasMutes = Object.keys(mutes).length !== 0; 572 + 573 + if (!hasMutes) { 574 + return; 575 + } 576 + 577 + return (item) => { 578 + const reason = item.reason; 579 + 580 + if (reason) { 581 + const did = reason.by.did; 582 + 583 + if (did !== uid && did in mutes) { 584 + return false; 585 + } 586 + } 587 + 588 + const did = item.post.author.did; 589 + 590 + if (did !== uid && did in mutes) { 591 + return false; 592 + } 593 + 594 + return true; 595 + }; 596 + }; 597 + 598 + const createHideRepliesFilter = (): PostFilter => { 599 + return (item) => { 600 + const reason = item.reason; 601 + 602 + return ( 603 + // Allow reposts 604 + (reason !== undefined && reason.$type === 'app.bsky.feed.defs#reasonRepost') || 605 + // Allow posts that aren't a reply 606 + (item.reply === undefined && (item.post.record as PostRecord).reply === undefined) 607 + ); 608 + }; 609 + }; 610 + 611 + const createHideRepostsFilter = (): PostFilter => { 612 + return (item) => { 613 + const reason = item.reason; 614 + 615 + // Allow posts with no reason, or the reasoning isn't a repost. 616 + return reason === undefined || reason.$type !== 'app.bsky.feed.defs#reasonRepost'; 617 + }; 618 + }; 619 + 620 + const createHideQuotesFilter = (): PostFilter => { 621 + return (item) => { 622 + const post = item.post.record as PostRecord; 623 + const record = getRecordEmbed(post.embed); 624 + 625 + return record === undefined || parseAtUri(record.record.uri).collection === 'app.bsky.feed.post'; 626 + }; 627 + }; 628 + 629 + //// Slice filters 630 + const createFeedSliceFilter = (): SliceFilter | undefined => { 631 + return (slice) => { 632 + const items = slice.items; 633 + const first = items[0]; 634 + 635 + const reply = first.reply; 636 + 637 + // Skip any posts that are in reply to a muted user 638 + if (reply) { 639 + for (const author of getReplyAuthors(reply)) { 640 + if (!author) { 641 + continue; 642 + } 643 + 644 + const viewer = author.viewer; 645 + 646 + if (!viewer) { 647 + // If this one doesn't have viewer state then none of them does. 648 + break; 649 + } 650 + 651 + if (viewer.muted) { 652 + return yankReposts(items); 653 + } 654 + } 655 + } 656 + 657 + return true; 658 + }; 659 + }; 660 + 661 + const createHomeSliceFilter = (uid: At.DID, followsOnly: boolean): SliceFilter | undefined => { 662 + return (slice) => { 663 + const items = slice.items; 664 + const first = items[0]; 665 + 666 + const reply = first.reply; 667 + const reason = first.reason; 668 + 669 + // Skip any posts that are in reply to non-followed user or a muted user 670 + if (reply && (!reason || reason.$type !== 'app.bsky.feed.defs#reasonRepost')) { 671 + for (const author of getReplyAuthors(reply)) { 672 + if (!author) { 673 + continue; 674 + } 675 + 676 + const viewer = author.viewer; 677 + 678 + if (!viewer) { 679 + // If this one doesn't have viewer state then none of them does. 680 + break; 681 + } 682 + 683 + if (author.did !== uid && ((followsOnly && !viewer.following) || !!viewer.muted)) { 684 + return yankReposts(items); 685 + } 686 + } 687 + } 688 + 689 + return true; 690 + }; 691 + }; 692 + 693 + const createProfileSliceFilter = (did: At.DID): SliceFilter | undefined => { 694 + return (slice) => { 695 + const items = slice.items; 696 + const first = items[0]; 697 + 698 + const reply = first.reply; 699 + const reason = first.reason; 700 + 701 + // Skip any posts that doesn't seem to look like a self-thread 702 + if (reply && (!reason || reason.$type !== 'app.bsky.feed.defs#reasonRepost')) { 703 + for (const author of getReplyAuthors(reply)) { 704 + if (!author) { 705 + continue; 706 + } 707 + 708 + if (author.did !== did) { 709 + return yankReposts(items); 710 + } 711 + } 712 + } 713 + 714 + return true; 715 + }; 716 + }; 717 + 718 + // Get the reposts out of the gutter 719 + const yankReposts = (items: EnsuredTimelineItem[]): TimelineSlice[] | false => { 720 + let slices: TimelineSlice[] | false = false; 721 + let last: EnsuredTimelineItem[] | undefined; 722 + 723 + for (let idx = 0, len = items.length; idx < len; idx++) { 724 + const item = items[idx]; 725 + const reason = item.reason; 726 + 727 + if (reason && reason.$type === 'app.bsky.feed.defs#reasonRepost') { 728 + if (last) { 729 + last.push(item); 730 + } else { 731 + (slices ||= []).push({ items: (last = [item]) }); 732 + } 733 + } else { 734 + last = undefined; 735 + } 736 + } 737 + 738 + return slices; 739 + }; 740 + 741 + const getReplyAuthors = (reply: EnsuredReplyRef) => { 742 + return [reply.root?.author, reply.grandparentAuthor, reply.parent?.author]; 743 + }; 744 + 745 + const getRecordEmbed = (embed: PostRecord['embed']): AppBskyEmbedRecord.Main | undefined => { 746 + if (embed) { 747 + if (embed.$type === 'app.bsky.embed.record') { 748 + return embed; 749 + } 750 + 751 + if (embed.$type === 'app.bsky.embed.recordWithMedia') { 752 + return embed.record; 753 + } 754 + } 755 + };
+83
src/api/richtext/segment.ts
··· 1 + import type { AppBskyRichtextFacet } from '@mary/bluesky-client/lexicons'; 2 + 3 + import type { UnwrapArray } from '../utils/types'; 4 + 5 + const encoder = new TextEncoder(); 6 + const decoder = new TextDecoder(); 7 + 8 + interface UtfString { 9 + u16: string; 10 + u8: Uint8Array; 11 + } 12 + 13 + const createUtfString = (utf16: string): UtfString => { 14 + return { u16: utf16, u8: encoder.encode(utf16) }; 15 + }; 16 + 17 + const getUtf8Length = (utf: UtfString) => { 18 + return utf.u8.byteLength; 19 + }; 20 + 21 + const sliceUtf8 = (utf: UtfString, start?: number, end?: number) => { 22 + return decoder.decode(utf.u8.slice(start, end)); 23 + }; 24 + 25 + type Facet = AppBskyRichtextFacet.Main; 26 + type FacetFeature = UnwrapArray<Facet['features']>; 27 + 28 + export interface RichtextSegment { 29 + text: string; 30 + feature: FacetFeature | undefined; 31 + } 32 + 33 + const createRichtextSegment = (text: string, feature: FacetFeature | undefined): RichtextSegment => { 34 + return { text: text, feature: feature }; 35 + }; 36 + 37 + export const segmentRichText = (rtText: string, facets: Facet[] | undefined): RichtextSegment[] => { 38 + if (facets === undefined || facets.length === 0) { 39 + return [createRichtextSegment(rtText, undefined)]; 40 + } 41 + 42 + const text = createUtfString(rtText); 43 + 44 + const segments: RichtextSegment[] = []; 45 + const length = getUtf8Length(text); 46 + 47 + const facetsLength = facets.length; 48 + 49 + let textCursor = 0; 50 + let facetCursor = 0; 51 + 52 + do { 53 + const facet = facets[facetCursor]; 54 + const { byteStart, byteEnd } = facet.index; 55 + 56 + if (textCursor < byteStart) { 57 + segments.push(createRichtextSegment(sliceUtf8(text, textCursor, byteStart), undefined)); 58 + } else if (textCursor > byteStart) { 59 + facetCursor++; 60 + continue; 61 + } 62 + 63 + if (byteStart < byteEnd) { 64 + const subtext = sliceUtf8(text, byteStart, byteEnd); 65 + const features = facet.features; 66 + 67 + if (features.length === 0 || subtext.trim().length === 0) { 68 + segments.push(createRichtextSegment(subtext, undefined)); 69 + } else { 70 + segments.push(createRichtextSegment(subtext, features[0])); 71 + } 72 + } 73 + 74 + textCursor = byteEnd; 75 + facetCursor++; 76 + } while (facetCursor < facetsLength); 77 + 78 + if (textCursor < length) { 79 + segments.push(createRichtextSegment(sliceUtf8(text, textCursor, length), undefined)); 80 + } 81 + 82 + return segments; 83 + };
+4
src/api/types.ts
··· 1 + export interface DataServer { 2 + name: string; 3 + uri: string; 4 + }
+54
src/api/utils/dequal.ts
··· 1 + const keys = Object.keys; 2 + 3 + export const dequal = (a: any, b: any): boolean => { 4 + let ctor: any; 5 + let len: number; 6 + 7 + if (a === b) { 8 + return true; 9 + } 10 + 11 + if (a && b && (ctor = a.constructor) === b.constructor) { 12 + if (ctor === Array) { 13 + if ((len = a.length) === b.length) { 14 + while (len--) { 15 + if (!dequal(a[len], b[len])) { 16 + return false; 17 + } 18 + } 19 + } 20 + 21 + return len === -1; 22 + } else if (!ctor || ctor === Object) { 23 + len = 0; 24 + 25 + for (ctor in a) { 26 + len++; 27 + 28 + if (!(ctor in b) || !dequal(a[ctor], b[ctor])) { 29 + return false; 30 + } 31 + } 32 + 33 + return keys(b).length === len; 34 + } 35 + } 36 + 37 + return a !== a && b !== b; 38 + }; 39 + 40 + export const EQUALS_DEQUAL = { equals: dequal } as const; 41 + 42 + export const sequal = (a: any[], b: any[]): boolean => { 43 + let len = a.length; 44 + 45 + if (len === b.length) { 46 + while (len--) { 47 + if (a[len] !== b[len]) { 48 + return false; 49 + } 50 + } 51 + } 52 + 53 + return len === -1; 54 + };
+102
src/api/utils/did-doc.ts
··· 1 + import { BskyXRPC, getPdsEndpoint, type DidDocument } from '@mary/bluesky-client'; 2 + import type { At } from '@mary/bluesky-client/lexicons'; 3 + 4 + import { DEFAULT_APP_VIEW } from '../defaults'; 5 + import type { DataServer } from '../types'; 6 + import { isDid } from './strings'; 7 + 8 + const HOST_RE = /^([a-zA-Z0-9-]+(?:\.[a-zA-Z0-9-]+)*(?:\.[a-zA-Z]+))$/; 9 + 10 + export type ResolutionErrorKind = 11 + | 'DID_UNSUPPORTED' 12 + | 'PLC_NOT_FOUND' 13 + | 'PLC_UNREACHABLE' 14 + | 'WEB_INVALID' 15 + | 'WEB_NOT_FOUND' 16 + | 'WEB_UNREACHABLE'; 17 + 18 + export class DidResolutionError extends Error { 19 + message!: ResolutionErrorKind; 20 + 21 + constructor(kind: ResolutionErrorKind) { 22 + super(kind); 23 + } 24 + } 25 + 26 + export const findDidDocument = async (identifier: string): Promise<DidDocument> => { 27 + let did: At.DID; 28 + 29 + if (isDid(identifier)) { 30 + did = identifier; 31 + } else { 32 + const rpc = new BskyXRPC({ service: DEFAULT_APP_VIEW }); 33 + const response = await rpc.get('com.atproto.identity.resolveHandle', { 34 + params: { 35 + handle: identifier, 36 + }, 37 + }); 38 + 39 + did = response.data.did; 40 + } 41 + 42 + const colon_index = did.indexOf(':', 4); 43 + 44 + const type = did.slice(4, colon_index); 45 + const ident = did.slice(colon_index + 1); 46 + 47 + // 2. retrieve their DID documents 48 + let doc: DidDocument; 49 + 50 + if (type === 'plc') { 51 + const response = await fetch(`https://plc.directory/${did}`); 52 + 53 + if (response.status === 404) { 54 + throw new DidResolutionError('PLC_NOT_FOUND'); 55 + } else if (!response.ok) { 56 + throw new DidResolutionError('PLC_UNREACHABLE'); 57 + } 58 + 59 + const json = await response.json(); 60 + 61 + doc = json as DidDocument; 62 + } else if (type === 'web') { 63 + if (!HOST_RE.test(ident)) { 64 + throw new DidResolutionError('WEB_INVALID'); 65 + } 66 + 67 + const response = await fetch(`https://${ident}/.well-known/did.json`); 68 + 69 + if (response.status === 404) { 70 + throw new DidResolutionError('WEB_NOT_FOUND'); 71 + } else if (!response.ok) { 72 + throw new DidResolutionError('WEB_UNREACHABLE'); 73 + } 74 + 75 + const json = await response.json(); 76 + 77 + doc = json as DidDocument; 78 + } else { 79 + throw new DidResolutionError('DID_UNSUPPORTED'); 80 + } 81 + 82 + return doc; 83 + }; 84 + 85 + export const getDataServer = (doc: DidDocument): DataServer | null => { 86 + const pds = getPdsEndpoint(doc); 87 + 88 + if (pds) { 89 + // Check if this is bsky.social, and give it a nice name. 90 + const url = new URL(pds); 91 + const host = url.host; 92 + 93 + const isBskySocial = host === 'bsky.social' || host.endsWith('.host.bsky.network'); 94 + 95 + return { 96 + name: isBskySocial ? `Bluesky Social` : host, 97 + uri: pds, 98 + }; 99 + } 100 + 101 + return null; 102 + };
+22
src/api/utils/did.ts
··· 1 + import type { BskyXRPC } from '@mary/bluesky-client'; 2 + import type { At } from '@mary/bluesky-client/lexicons'; 3 + 4 + import { isDid } from './strings'; 5 + 6 + const getDid = async (rpc: BskyXRPC, actor: string, signal?: AbortSignal) => { 7 + let did: At.DID; 8 + if (isDid(actor)) { 9 + did = actor; 10 + } else { 11 + const response = await rpc.get('com.atproto.identity.resolveHandle', { 12 + signal: signal, 13 + params: { handle: actor }, 14 + }); 15 + 16 + did = response.data.did; 17 + } 18 + 19 + return did; 20 + }; 21 + 22 + export default getDid;
+20
src/api/utils/error.ts
··· 1 + import { XRPCError } from '@mary/bluesky-client/xrpc'; 2 + 3 + export const formatXRPCError = (err: XRPCError): string => { 4 + const name = err.kind; 5 + return (name ? name + ': ' : '') + err.message; 6 + }; 7 + 8 + export const formatQueryError = (err: unknown) => { 9 + if (err instanceof XRPCError) { 10 + const error = err.kind; 11 + 12 + if (error === 'InvalidToken' || error === 'ExpiredToken') { 13 + return `Account session invalid, please sign in again`; 14 + } 15 + 16 + return formatXRPCError(err); 17 + } 18 + 19 + return '' + err; 20 + };
+94
src/api/utils/post.ts
··· 1 + import type { 2 + AppBskyEmbedExternal, 3 + AppBskyEmbedImages, 4 + AppBskyEmbedRecord, 5 + AppBskyFeedDefs, 6 + AppBskyFeedPost, 7 + Brand, 8 + } from '@mary/bluesky-client/lexicons'; 9 + 10 + type RecordEmbed = AppBskyFeedPost.Record['embed']; 11 + type ViewEmbed = AppBskyFeedDefs.PostView['embed']; 12 + 13 + export const unwrapPostEmbedText = (embed: RecordEmbed | ViewEmbed): string => { 14 + let str = ''; 15 + 16 + const media = getMediaEmbed(embed); 17 + 18 + if (media) { 19 + const type = media.$type; 20 + 21 + if (type === 'app.bsky.embed.external' || type === 'app.bsky.embed.external#view') { 22 + str += ` ` + media.external.title; 23 + } else if (type === 'app.bsky.embed.images' || type === 'app.bsky.embed.images#view') { 24 + const images = media.images; 25 + 26 + for (let idx = 0, len = images.length; idx < len; idx++) { 27 + str += ` ` + images[idx].alt; 28 + } 29 + } 30 + } 31 + 32 + return str; 33 + }; 34 + 35 + type RecordMedia = AppBskyEmbedExternal.Main | AppBskyEmbedImages.Main; 36 + type ViewMedia = AppBskyEmbedExternal.View | AppBskyEmbedImages.View; 37 + 38 + const getMediaEmbed = (embed: RecordEmbed | ViewEmbed): Brand.Union<RecordMedia | ViewMedia> | undefined => { 39 + if (embed) { 40 + const type = embed.$type; 41 + 42 + if ( 43 + type === 'app.bsky.embed.external' || 44 + type === 'app.bsky.embed.external#view' || 45 + type === 'app.bsky.embed.images' || 46 + type === 'app.bsky.embed.images#view' 47 + ) { 48 + return embed; 49 + } else if (type === 'app.bsky.embed.recordWithMedia' || type === 'app.bsky.embed.recordWithMedia#view') { 50 + const media = embed.media; 51 + const mediatype = media.$type; 52 + 53 + if ( 54 + mediatype === 'app.bsky.embed.external' || 55 + mediatype === 'app.bsky.embed.external#view' || 56 + mediatype === 'app.bsky.embed.images' || 57 + mediatype === 'app.bsky.embed.images#view' 58 + ) { 59 + return media; 60 + } 61 + } 62 + } 63 + }; 64 + 65 + export const getEmbeddedPost = ( 66 + embed: AppBskyFeedDefs.PostView['embed'], 67 + ): AppBskyEmbedRecord.ViewRecord | undefined => { 68 + if (embed) { 69 + if (embed.$type === 'app.bsky.embed.record#view') { 70 + if (embed.record.$type === 'app.bsky.embed.record#viewRecord') { 71 + return embed.record; 72 + } 73 + } else if (embed.$type === 'app.bsky.embed.recordWithMedia#view') { 74 + if (embed.record.record.$type === 'app.bsky.embed.record#viewRecord') { 75 + return embed.record.record; 76 + } 77 + } 78 + } 79 + }; 80 + 81 + export const embedViewRecordToPostView = (v: AppBskyEmbedRecord.ViewRecord): AppBskyFeedDefs.PostView => { 82 + return { 83 + uri: v.uri, 84 + cid: v.cid, 85 + author: v.author, 86 + record: v.value, 87 + indexedAt: v.indexedAt, 88 + labels: v.labels, 89 + embed: v.embeds?.[0], 90 + likeCount: v.likeCount, 91 + replyCount: v.replyCount, 92 + repostCount: v.repostCount, 93 + }; 94 + };
+40
src/api/utils/query.ts
··· 1 + import { 2 + type InfiniteData, 3 + type QueryClient, 4 + type QueryFunctionContext, 5 + type QueryKey, 6 + } from '@mary/solid-query'; 7 + 8 + export const resetInfiniteData = (client: QueryClient, key: QueryKey) => { 9 + client.setQueryData<InfiniteData<unknown>>(key, (data) => { 10 + if (data && data.pages.length > 1) { 11 + return { 12 + pages: data.pages.slice(0, 1), 13 + pageParams: data.pageParams.slice(0, 1), 14 + }; 15 + } 16 + 17 + return data; 18 + }); 19 + }; 20 + 21 + const errorMap = new WeakMap<WeakKey, { pageParam: any; direction: 'forward' | 'backward' }>(); 22 + 23 + export const wrapQuery = <TQueryData, TPageParam, TQueryKey extends QueryKey = QueryKey>( 24 + fn: (ctx: QueryFunctionContext<TQueryKey, TPageParam>) => Promise<TQueryData>, 25 + ) => { 26 + return async (ctx: QueryFunctionContext<TQueryKey, TPageParam>): Promise<TQueryData> => { 27 + try { 28 + return await fn(ctx); 29 + } catch (err) { 30 + // @ts-expect-error 31 + errorMap.set(err as any, { pageParam: ctx.pageParam, direction: ctx.direction }); 32 + throw err; 33 + } 34 + }; 35 + }; 36 + 37 + export const getQueryErrorInfo = (err: unknown) => { 38 + const info = errorMap.get(err as any); 39 + return info; 40 + };
+98
src/api/utils/strings.ts
··· 1 + import type { At } from '@mary/bluesky-client/lexicons'; 2 + 3 + import { assert } from '~/lib/invariant'; 4 + 5 + export const isDid = (value: string): value is At.DID => { 6 + return value.startsWith('did:'); 7 + }; 8 + 9 + export const ATURI_RE = 10 + /^at:\/\/(did:[a-z0-9:%-]+)\/([a-zA-Z0-9-.]+)\/([a-zA-Z0-9._~:@!$&%')(*+,;=-]+)(?:#(\/[a-zA-Z0-9._~:@!$&%')(*+,;=\-[\]/\\]*))?$/; 11 + 12 + export const DID_RE = /^did:([a-z]+):([a-zA-Z0-9._:%-]*[a-zA-Z0-9._-])$/; 13 + 14 + export interface AtUri { 15 + repo: At.DID; 16 + collection: string; 17 + rkey: string; 18 + fragment: string | undefined; 19 + } 20 + 21 + export const parseAtUri = (str: string): AtUri => { 22 + const match = ATURI_RE.exec(str); 23 + assert(match !== null); 24 + 25 + return { 26 + repo: match[1] as At.DID, 27 + collection: match[2], 28 + rkey: match[3], 29 + fragment: match[4], 30 + }; 31 + }; 32 + 33 + // Check for the existence of URL.parse and use that if available, removes the 34 + // performance hit from try..catch blocks. 35 + export const safeUrlParse = 36 + 'parse' in URL 37 + ? (text: string): URL | null => { 38 + // @ts-expect-error 39 + const url = URL.parse(text) as URL | null; 40 + 41 + if (url !== null && (url.protocol === 'https:' || url.protocol === 'http:')) { 42 + return url; 43 + } 44 + 45 + return null; 46 + } 47 + : (text: string): URL | null => { 48 + try { 49 + const url = new URL(text); 50 + 51 + if (url.protocol === 'https:' || url.protocol === 'http:') { 52 + return url; 53 + } 54 + } catch {} 55 + 56 + return null; 57 + }; 58 + 59 + const TRIM_HOST_RE = /^www\./; 60 + const TRIM_URLTEXT_RE = /^\s*(https?:\/\/)?(?:www\.)?/; 61 + // const PATH_MAX_LENGTH = 18; 62 + 63 + export const isLinkValid = (uri: string, text: string) => { 64 + const url = safeUrlParse(uri); 65 + 66 + if (url === null) { 67 + return false; 68 + } 69 + 70 + const expectedHost = buildHostPart(url); 71 + const length = expectedHost.length; 72 + 73 + const normalized = text.replace(TRIM_URLTEXT_RE, '').toLowerCase(); 74 + const normalizedLength = normalized.length; 75 + 76 + const boundary = normalizedLength >= length ? normalized[length] : undefined; 77 + 78 + return ( 79 + (boundary === undefined || boundary === '/' || boundary === '?' || boundary === '#') && 80 + normalized.startsWith(expectedHost) 81 + ); 82 + }; 83 + 84 + const buildHostPart = (url: URL) => { 85 + const username = url.username; 86 + // const password = url.password; 87 + 88 + const hostname = url.hostname.replace(TRIM_HOST_RE, '').toLowerCase(); 89 + const port = url.port; 90 + 91 + // Perhaps might be best if we always warn on authentication being passed. 92 + // const auth = username ? username + (password ? ':' + password : '') + '@' : ''; 93 + const auth = username ? '\0@@\0' : ''; 94 + 95 + const host = hostname + (port ? ':' + port : ''); 96 + 97 + return auth + host; 98 + };
+1
src/api/utils/types.ts
··· 1 + export type UnwrapArray<T> = T extends (infer V)[] ? V : never;
+3
src/api/utils/utils.ts
··· 1 + export const getCurrentDate = () => { 2 + return new Date().toISOString(); 3 + };
+1
src/assets/default-feed-avatar.svg
··· 1 + <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32"><path fill="#0070FF" d="M0 0h32v32H0z"/><path fill="#fff" d="M13.5 7.25c0-.691.559-1.25 1.25-1.25C20.965 6 26 11.035 26 17.25c0 .691-.559 1.25-1.25 1.25s-1.25-.559-1.25-1.25a8.75 8.75 0 0 0-8.75-8.75c-.691 0-1.25-.559-1.25-1.25Zm-5.133 7.367 4.067 4.067 1.109-1.11A1.249 1.249 0 0 1 14.75 16c.691 0 1.25.559 1.25 1.25a1.249 1.249 0 0 1-1.574 1.207l-1.11 1.11 4.067 4.066c.566.566.46 1.515-.285 1.808a8.158 8.158 0 0 1-2.973.559 8.124 8.124 0 0 1-7.563-11.098c.293-.742 1.243-.851 1.81-.285h-.005ZM14.75 9.75c4.14 0 7.5 3.36 7.5 7.5 0 .691-.559 1.25-1.25 1.25s-1.25-.559-1.25-1.25a5 5 0 0 0-5-5c-.691 0-1.25-.559-1.25-1.25s.559-1.25 1.25-1.25Z"/></svg>
+1
src/assets/default-labeler-avatar.svg
··· 1 + <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32"><path fill="#6900FF" d="M0 0h32v32H0z"/><path fill="none" stroke="#fff" stroke-linejoin="round" stroke-width="2" d="M24 9.75 16 7 8 9.75v6.162c0 4.973 4 7.088 8 9.246 4-2.158 8-4.273 8-9.246V9.75Z"/></svg>
+1
src/assets/default-list-avatar.svg
··· 1 + <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32"><path fill="#0070FF" d="M0 0h32v32H0z"/><path fill="#fff" d="M22.153 22.354a9.328 9.328 0 0 0 3.837-.491 3.076 3.076 0 0 0-4.802-2.79m.965 3.281a6.128 6.128 0 0 0-.965-3.28Zm-11.342-3.28a3.077 3.077 0 0 0-4.801 2.79 9.21 9.21 0 0 0 3.835.49m.966-3.28a6.127 6.127 0 0 0-.966 3.28Zm8.265-8.997a3.076 3.076 0 1 1-6.153 0 3.076 3.076 0 0 1 6.153 0Zm6.154 3.077a2.307 2.307 0 1 1-4.615 0 2.307 2.307 0 0 1 4.615 0Zm-13.847 0a2.307 2.307 0 1 1-4.614 0 2.307 2.307 0 0 1 4.614 0Z"/><path fill="#fff" d="M22 22c0 3.314-2.686 3.5-6 3.5s-6-.186-6-3.5a6 6 0 0 1 12 0Z"/></svg>
+1
src/assets/default-user-avatar.svg
··· 1 + <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path fill="#0070ff" d="M0 0h24v24H0z"/><circle cx="12" cy="9.5" r="3.5" fill="#fff"/><path fill="#fff" d="M12.058 22.784c-2.636 0-5.051-.948-6.921-2.522.53-2.274 3.397-4.012 6.853-4.012 3.504 0 6.401 1.786 6.874 4.107a10.71 10.71 0 0 1-6.806 2.427Z"/></svg>
+104
src/components/avatar.tsx
··· 1 + import { Match, Switch } from 'solid-js'; 2 + 3 + import { ContextProfileMedia, getModerationUI, type ModerationCause } from '~/api/moderation'; 4 + 5 + import DefaultFeedAvatar from '~/assets/default-feed-avatar.svg?url'; 6 + import DefaultLabelerAvatar from '~/assets/default-labeler-avatar.svg?url'; 7 + import DefaultListAvatar from '~/assets/default-list-avatar.svg?url'; 8 + import DefaultUserAvatar from '~/assets/default-user-avatar.svg?url'; 9 + 10 + import { handleLinkClick } from './button'; 11 + 12 + const AVATARS = { 13 + feed: DefaultFeedAvatar, 14 + labeler: DefaultLabelerAvatar, 15 + list: DefaultListAvatar, 16 + user: DefaultUserAvatar, 17 + }; 18 + 19 + export interface AvatarProps { 20 + type: keyof typeof AVATARS; 21 + src?: string; 22 + moderation?: ModerationCause[]; 23 + title?: string; 24 + disabled?: boolean; 25 + href?: string; 26 + onClick?: () => void; 27 + 28 + class?: string; 29 + size?: 'xs' | 'sm' | 'md' | 'lg'; 30 + } 31 + 32 + const Avatar = (props: AvatarProps) => { 33 + const hasModeration = 'moderation' in props; 34 + 35 + const shouldBlurAvatar = hasModeration 36 + ? () => getModerationUI(props.moderation!, ContextProfileMedia).b.length > 0 37 + : undefined; 38 + 39 + return ( 40 + <Switch> 41 + <Match when={!props.disabled && props.href}> 42 + {(href) => ( 43 + <a 44 + href={href()} 45 + title={props.title} 46 + onClick={handleLinkClick(() => props.onClick)} 47 + class={avatarClassNames(props, true)} 48 + > 49 + {renderAvatar(props.type, props.src, shouldBlurAvatar)} 50 + </a> 51 + )} 52 + </Match> 53 + 54 + <Match when={!props.disabled && props.onClick} keyed> 55 + {(onClick) => ( 56 + <button title={props.title} onClick={onClick} class={avatarClassNames(props, true)}> 57 + {renderAvatar(props.type, props.src, shouldBlurAvatar)} 58 + </button> 59 + )} 60 + </Match> 61 + 62 + <Match when> 63 + <div title={props.title} class={avatarClassNames(props, false)}> 64 + {renderAvatar(props.type, props.src, shouldBlurAvatar)} 65 + </div> 66 + </Match> 67 + </Switch> 68 + ); 69 + }; 70 + 71 + export default Avatar; 72 + 73 + const renderAvatar = (type: keyof typeof AVATARS, src: string | undefined, shouldBlur?: () => boolean) => { 74 + return ( 75 + <img 76 + src={/* @once */ src ?? AVATARS[type]} 77 + class={`h-full w-full` + (src && shouldBlur?.() ? ` blur` : ``)} 78 + /> 79 + ); 80 + }; 81 + 82 + const avatarClassNames = ({ size = 'md', class: className }: AvatarProps, interactive: boolean): string => { 83 + let cn = `shrink-0 overflow-hidden rounded-full bg-c-contrast-200`; 84 + 85 + if (interactive) { 86 + cn += ` hover:opacity-80`; 87 + } 88 + 89 + if (size === 'md') { 90 + cn += ` h-9 w-9`; 91 + } else if (size === 'sm') { 92 + cn += ` h-6 w-6`; 93 + } else if (size === 'xs') { 94 + cn += ` h-5 w-5`; 95 + } else if (size === 'lg') { 96 + cn += ` h-10 w-10`; 97 + } 98 + 99 + if (className) { 100 + return `${cn} ${className}`; 101 + } else { 102 + return cn; 103 + } 104 + };
+126
src/components/button.tsx
··· 1 + import type { JSX } from 'solid-js'; 2 + 3 + import { history } from '~/globals/navigation'; 4 + 5 + import { useFieldset } from './fieldset'; 6 + 7 + export interface ButtonProps { 8 + title?: string; 9 + href?: string; 10 + disabled?: boolean; 11 + type?: 'button' | 'submit' | 'reset'; 12 + onClick?: (ev: MouseEvent) => void; 13 + 14 + children: JSX.Element; 15 + 16 + size?: 'sm' | 'md'; 17 + variant?: 'outline' | 'primary' | 'ghost'; 18 + class?: string; 19 + } 20 + 21 + const Button = (props: ButtonProps) => { 22 + const fieldset = useFieldset(); 23 + const isDisabled = (): boolean => fieldset.disabled || !!props.disabled; 24 + 25 + if ('href' in props) { 26 + return ( 27 + <a 28 + href={!isDisabled() ? props.href : undefined} 29 + title={props.title} 30 + onClick={handleLinkClick(() => props.onClick)} 31 + class={buttonClassNames(isDisabled, props)} 32 + > 33 + {props.children} 34 + </a> 35 + ); 36 + } 37 + 38 + return ( 39 + <button 40 + type={props.type || 'button'} 41 + disabled={isDisabled()} 42 + title={props.title} 43 + onClick={props.onClick} 44 + class={buttonClassNames(isDisabled, props)} 45 + > 46 + {props.children} 47 + </button> 48 + ); 49 + }; 50 + 51 + export default Button; 52 + 53 + const buttonClassNames = ( 54 + isDisabled: () => boolean, 55 + { size = 'sm', variant = 'outline', class: className }: ButtonProps, 56 + ): string => { 57 + var cn = `flex select-none items-center justify-center rounded text-sm font-semibold leading-none`; 58 + 59 + if (isDisabled()) { 60 + cn += ` pointer-events-none`; 61 + } 62 + 63 + if (size === 'sm') { 64 + cn += ` h-8 px-4`; 65 + } else if (size === 'md') { 66 + cn += ` h-10 px-4`; 67 + } 68 + 69 + if (variant === 'primary') { 70 + if (!isDisabled()) { 71 + cn += ` bg-c-primary-500 text-c-white hover:bg-c-primary-600`; 72 + } else { 73 + cn += ` bg-c-primary-700 text-c-white/50`; 74 + } 75 + } else if (variant === 'outline') { 76 + if (!isDisabled()) { 77 + cn += ` border border-c-contrast-300 text-c-contrast-900 hover:bg-c-contrast-25`; 78 + } else { 79 + cn += ` border border-c-contrast-100 text-c-contrast-400`; 80 + } 81 + } else if (variant === 'ghost') { 82 + if (!isDisabled()) { 83 + cn += ` text-c-contrast-900 hover:bg-c-contrast-50`; 84 + } else { 85 + cn += ` text-c-contrast-400`; 86 + } 87 + } 88 + 89 + if (className) { 90 + cn += ` ${className}`; 91 + } 92 + 93 + return cn; 94 + }; 95 + 96 + export const handleLinkClick = (fn: () => ButtonProps['onClick']) => { 97 + return (event: MouseEvent): void => { 98 + fn()?.(event); 99 + handleLinkNavigation(event); 100 + }; 101 + }; 102 + 103 + export const handleLinkNavigation = (event: MouseEvent) => { 104 + if (!event.defaultPrevented) { 105 + const anchor = event.currentTarget as HTMLAnchorElement; 106 + const href = anchor.href; 107 + const target = anchor.target; 108 + 109 + if (href !== '' && (target === '' || target === '_self') && isLinkEvent(event)) { 110 + const { origin, pathname, search, hash } = new URL(href); 111 + 112 + if (location.origin === origin) { 113 + event.preventDefault(); 114 + history.navigate({ pathname, search, hash }); 115 + } 116 + } 117 + } 118 + }; 119 + 120 + const isLinkEvent = (event: MouseEvent) => { 121 + return event.button === 0 && !isModifiedEvent(event); 122 + }; 123 + 124 + const isModifiedEvent = (event: MouseEvent) => { 125 + return event.metaKey || event.altKey || event.ctrlKey || event.shiftKey; 126 + };
+27
src/components/circular-progress.tsx
··· 1 + export interface CircularProgressProps { 2 + size?: number; 3 + } 4 + 5 + const CircularProgress = (props: CircularProgressProps) => { 6 + return ( 7 + <svg 8 + viewBox="0 0 32 32" 9 + class="animate-spin" 10 + style={`height:${props.size ?? 24}px;width:${props.size ?? 24}px`} 11 + > 12 + <circle cx="16" cy="16" fill="none" r="14" stroke-width="4" class="stroke-c-primary-500 opacity-20" /> 13 + <circle 14 + cx="16" 15 + cy="16" 16 + fill="none" 17 + r="14" 18 + stroke-width="4" 19 + stroke-dasharray="80px" 20 + stroke-dashoffset="60px" 21 + class="stroke-c-primary-500" 22 + /> 23 + </svg> 24 + ); 25 + }; 26 + 27 + export default CircularProgress;
+159
src/components/dialog.tsx
··· 1 + import { type ParentProps } from 'solid-js'; 2 + 3 + import { useModalContext } from '~/globals/modals'; 4 + import { useMediaQuery } from '~/lib/hooks/media-query'; 5 + 6 + import { useModalClose } from '~/lib/hooks/modal-close'; 7 + import { useTheme } from '~/lib/states/theme'; 8 + 9 + import { Fieldset } from './fieldset'; 10 + import IconButton from './icon-button'; 11 + import CrossLargeOutlinedIcon from './icons-central/cross-large-outline'; 12 + 13 + const DialogBackdrop = () => { 14 + const theme = useTheme(); 15 + 16 + return ( 17 + <div 18 + class={`fixed inset-0 z-0` + (theme.currentTheme === 'light' ? ` bg-t-black/40` : ` bg-t-blue-low/40`)} 19 + ></div> 20 + ); 21 + }; 22 + 23 + export { DialogBackdrop as Backdrop }; 24 + 25 + export interface DialogContainerProps extends ParentProps { 26 + fullHeight?: boolean; 27 + centered?: boolean; 28 + maxWidth?: 'sm' | 'md'; 29 + disabled?: boolean; 30 + } 31 + 32 + const DialogContainer = (props: DialogContainerProps) => { 33 + const { close, isActive } = useModalContext(); 34 + 35 + const isDesktop = useMediaQuery('(width >= 688px) and (height >= 500px)'); 36 + const isDisabled = () => !!props.disabled; 37 + 38 + const containerRef = (node: HTMLElement): void => { 39 + useModalClose(node, close, () => isActive() && !isDisabled()); 40 + }; 41 + 42 + return ( 43 + <Fieldset standalone disabled={isDisabled()}> 44 + <div 45 + ref={containerRef} 46 + role="dialog" 47 + class={containerClasses(isDesktop, props)} 48 + style={{ '--dialog-width': getMaxDialogWidth(props) }} 49 + > 50 + <div class="mx-auto flex h-full min-h-0 w-full max-w-[var(--dialog-width)] flex-col overflow-hidden"> 51 + {props.children} 52 + </div> 53 + </div> 54 + </Fieldset> 55 + ); 56 + }; 57 + 58 + const getMaxDialogWidth = ({ maxWidth = 'md' }: DialogContainerProps) => { 59 + if (maxWidth === 'md') { 60 + return `600px`; 61 + } else if (maxWidth === 'sm') { 62 + return `446px`; 63 + } 64 + }; 65 + 66 + const containerClasses = (isDesktop: () => boolean, props: DialogContainerProps): string => { 67 + var cn = `z-1 bg-c-contrast-0`; 68 + 69 + if (isDesktop()) { 70 + cn += ` a-dialog-desktop w-full max-w-[var(--dialog-width)] rounded-xl`; 71 + 72 + if (props.fullHeight) { 73 + cn += ` grow`; 74 + } 75 + 76 + if (props.centered) { 77 + cn += ` my-auto`; 78 + } else { 79 + cn += ` mt-11`; 80 + } 81 + } else { 82 + cn += ` h-full w-full grow`; 83 + } 84 + 85 + return cn; 86 + }; 87 + 88 + export { DialogContainer as Container }; 89 + 90 + export interface DialogHeaderProps extends ParentProps {} 91 + 92 + const DialogHeader = (props: DialogHeaderProps) => { 93 + return <div class="flex h-13 shrink-0 items-center justify-between gap-4 px-2.5">{props.children}</div>; 94 + }; 95 + 96 + export { DialogHeader as Header }; 97 + 98 + export interface DialogHeadingProps { 99 + title?: string; 100 + subtitle?: string; 101 + } 102 + 103 + const DialogHeading = (props: DialogHeadingProps) => { 104 + return ( 105 + <div class="flex min-w-0 grow flex-col gap-0.5"> 106 + <p class="text-base font-bold empty:hidden">{props.title}</p> 107 + </div> 108 + ); 109 + }; 110 + 111 + export { DialogHeading as Heading }; 112 + 113 + export interface DialogHeaderAccessoryProps extends ParentProps {} 114 + 115 + const DialogHeaderAccessory = (props: DialogHeaderAccessoryProps) => { 116 + return <div class="flex shrink-0 gap-2 empty:hidden">{props.children}</div>; 117 + }; 118 + 119 + export { DialogHeaderAccessory as HeaderAccessory }; 120 + 121 + const DialogClose = () => { 122 + const { close } = useModalContext(); 123 + return <IconButton title="Close dialog" icon={CrossLargeOutlinedIcon} onClick={close} />; 124 + }; 125 + 126 + export { DialogClose as Close }; 127 + 128 + export interface DialogBodyProps extends ParentProps { 129 + unpadded?: boolean; 130 + class?: string; 131 + } 132 + 133 + const DialogBody = (props: DialogBodyProps) => { 134 + return ( 135 + <div class={`grow overflow-y-auto` + (!props.unpadded ? ` p-4` : ``) + ` ` + (props.class ?? '')}> 136 + {props.children} 137 + </div> 138 + ); 139 + }; 140 + 141 + export { DialogBody as Body }; 142 + 143 + export interface DialogActionsProps extends ParentProps {} 144 + 145 + const DialogActions = (props: DialogActionsProps) => { 146 + const isDesktop = useMediaQuery('(width >= 688px) and (height >= 500px)'); 147 + 148 + return ( 149 + <div 150 + class={ 151 + `flex shrink-0 gap-4 px-4 py-5` + (isDesktop() ? ` items-center justify-end` : ` flex-col-reverse`) 152 + } 153 + > 154 + {props.children} 155 + </div> 156 + ); 157 + }; 158 + 159 + export { DialogActions as Actions };
+31
src/components/divider.tsx
··· 1 + type Gutter = false | 'sm' | 'md'; 2 + 3 + export interface DividerProps { 4 + gutter?: Gutter; 5 + gutterTop?: Gutter; 6 + gutterBottom?: Gutter; 7 + } 8 + 9 + const Divider = (props: DividerProps) => { 10 + return <hr class={dividerClassNames(props)} />; 11 + }; 12 + 13 + const dividerClassNames = ({ gutter = false, gutterBottom = gutter, gutterTop = gutter }: DividerProps) => { 14 + let cn = `border-c-contrast-200`; 15 + 16 + if (gutterBottom === 'sm') { 17 + cn += ` mb-1`; 18 + } else if (gutterBottom === 'md') { 19 + cn += ` mb-3`; 20 + } 21 + 22 + if (gutterTop === 'sm') { 23 + cn += ` mt-1`; 24 + } else if (gutterTop === 'md') { 25 + cn += ` mt-3`; 26 + } 27 + 28 + return cn; 29 + }; 30 + 31 + export default Divider;
+124
src/components/embeds/embed.tsx
··· 1 + import type { 2 + AppBskyEmbedExternal, 3 + AppBskyEmbedImages, 4 + AppBskyEmbedRecord, 5 + AppBskyFeedDefs, 6 + Brand, 7 + } from '@mary/bluesky-client/lexicons'; 8 + 9 + import { ContextContentMedia, getModerationUI, type ModerationCause } from '~/api/moderation'; 10 + import { parseAtUri } from '~/api/utils/strings'; 11 + 12 + import ContentHider from '../moderation/content-hider'; 13 + 14 + import ImageEmbed from './image-embed'; 15 + import QuoteEmbed from './quote-embed'; 16 + 17 + export interface EmbedProps { 18 + /** Expected to be static */ 19 + embed: NonNullable<AppBskyFeedDefs.PostView['embed']>; 20 + /** Expected to be static */ 21 + gutterTop?: boolean; 22 + /** Expected to be static */ 23 + large?: boolean; 24 + moderation?: ModerationCause[]; 25 + } 26 + 27 + const Embed = (props: EmbedProps) => { 28 + const embed = props.embed; 29 + const gutterTop = props.gutterTop; 30 + const large = props.large; 31 + 32 + const type = embed.$type; 33 + 34 + return ( 35 + <div class={`flex flex-col gap-3` + (gutterTop ? ` mt-3` : ``)}> 36 + {type === 'app.bsky.embed.recordWithMedia#view' ? ( 37 + <> 38 + <MediaEmbed embed={/* @once */ embed.media} moderation={props.moderation} /> 39 + <RecordEmbed embed={/* @once */ embed.record} large={large} /> 40 + </> 41 + ) : type !== 'app.bsky.embed.record#view' ? ( 42 + <MediaEmbed embed={embed} moderation={props.moderation} /> 43 + ) : ( 44 + <RecordEmbed embed={embed} large={large} /> 45 + )} 46 + </div> 47 + ); 48 + }; 49 + 50 + export default Embed; 51 + 52 + interface MediaEmbedProps { 53 + /** Expected to be static */ 54 + embed: Brand.Union<AppBskyEmbedExternal.View | AppBskyEmbedImages.View>; 55 + moderation?: ModerationCause[]; 56 + } 57 + 58 + const MediaEmbed = (props: MediaEmbedProps) => { 59 + return ( 60 + <ContentHider 61 + ui={getModerationUI(props.moderation, ContextContentMedia)} 62 + childContainerClass="flex flex-col mt-1.5" 63 + children={(() => { 64 + const embed = props.embed; 65 + const type = embed.$type; 66 + 67 + if (type === 'app.bsky.embed.images#view') { 68 + return <ImageEmbed embed={embed} interactive />; 69 + } 70 + 71 + return renderEmpty(`Unsupported media`); 72 + })()} 73 + /> 74 + ); 75 + }; 76 + 77 + interface RecordEmbedProps { 78 + /** Expected to be static */ 79 + embed: AppBskyEmbedRecord.View; 80 + /** Expected to be static */ 81 + large?: boolean; 82 + } 83 + 84 + const RecordEmbed = (props: RecordEmbedProps) => { 85 + const embed = props.embed; 86 + const large = props.large; 87 + 88 + const record = embed.record; 89 + const type = record.$type; 90 + 91 + if (type === 'app.bsky.embed.record#viewNotFound' || type === 'app.bsky.embed.record#viewBlocked') { 92 + const { collection } = parseAtUri(record.uri); 93 + 94 + if ( 95 + collection === 'app.bsky.feed.post' && 96 + type === 'app.bsky.embed.record#viewBlocked' && 97 + record.author.viewer?.blocking 98 + ) { 99 + return renderEmpty(`Blocking`); 100 + } 101 + 102 + return renderEmpty(`This post is unavailable`); 103 + } 104 + 105 + if (type === 'app.bsky.embed.record#viewRecord') { 106 + return <QuoteEmbed quote={record} large={large} interactive />; 107 + } 108 + 109 + if (type === 'app.bsky.feed.defs#generatorView') { 110 + } 111 + 112 + if (type === 'app.bsky.graph.defs#listView') { 113 + } 114 + 115 + return renderEmpty(`Unsupported record`); 116 + }; 117 + 118 + const renderEmpty = (msg: string) => { 119 + return ( 120 + <div class="rounded-md border border-c-contrast-200 p-3"> 121 + <p class="text-sm text-c-contrast-600">{msg}</p> 122 + </div> 123 + ); 124 + };
+134
src/components/embeds/image-embed.tsx
··· 1 + import type { AppBskyEmbedImages } from '@mary/bluesky-client/lexicons'; 2 + 3 + export interface ImageEmbedProps { 4 + /** Expected to be static */ 5 + embed: AppBskyEmbedImages.View; 6 + blur?: boolean; 7 + /** Expected to be static */ 8 + borderless?: boolean; 9 + /** Expected to be static */ 10 + interactive?: boolean; 11 + } 12 + 13 + const enum RenderMode { 14 + MULTIPLE, 15 + MULTIPLE_SQUARE, 16 + STANDALONE, 17 + STANDALONE_RATIO, 18 + } 19 + 20 + const ImageEmbed = (props: ImageEmbedProps) => { 21 + const { embed, borderless, interactive } = props; 22 + 23 + const images = embed.images; 24 + const length = images.length; 25 + 26 + const hasStandaloneImage = interactive ? length === 1 && images[0].aspectRatio !== undefined : false; 27 + 28 + const render = (image: AppBskyEmbedImages.ViewImage, mode: RenderMode) => { 29 + const { alt, thumb, aspectRatio } = image; 30 + 31 + // FIXME: with STANDALONE_RATIO, we are resizing the image to make it fit 32 + // the container with our given constraints, but this doesn't work when the 33 + // image hasn't had its metadata loaded yet, the browser will snap to the 34 + // smallest possible size for our layout. 35 + 36 + // clients will typically just shove the actual resolution info to the 37 + // `aspectRatio` field, but we can't rely on that as it could send 38 + // simplified ratios instead. 39 + 40 + // so what we'll do here is to just have an empty <div> sized to the device 41 + // screen width and height. there's no issue with keeping the <div> around, 42 + // so we'll do just that. 43 + 44 + let cn: string | undefined; 45 + let ratio: string | undefined; 46 + 47 + if (mode === RenderMode.MULTIPLE) { 48 + cn = `min-h-0 grow basis-0 overflow-hidden`; 49 + } else if (mode === RenderMode.MULTIPLE_SQUARE) { 50 + cn = `aspect-square overflow-hidden`; 51 + } else if (mode === RenderMode.STANDALONE) { 52 + cn = `aspect-video overflow-hidden`; 53 + } else if (mode === RenderMode.STANDALONE_RATIO) { 54 + cn = `max-h-80 min-h-16 min-w-16 max-w-full overflow-hidden`; 55 + ratio = `${aspectRatio!.width}/${aspectRatio!.height}`; 56 + } 57 + 58 + return ( 59 + <div class={`relative ` + cn} style={{ 'aspect-ratio': ratio }}> 60 + <img 61 + src={thumb} 62 + title={alt} 63 + class={ 64 + `h-full w-full object-cover text-[0px]` + 65 + (interactive ? ` cursor-pointer` : ``) + 66 + // prettier-ignore 67 + (props.blur ? ` scale-110` + (!borderless ? ` blur` : ` blur-lg`) : ``) 68 + } 69 + /> 70 + 71 + {/* @once */ mode === RenderMode.STANDALONE_RATIO && <div class="h-screen w-screen"></div>} 72 + 73 + {interactive && alt && ( 74 + <button 75 + class="text-white absolute bottom-0 left-0 m-2 h-5 rounded bg-t-black/70 px-1 text-xs font-medium" 76 + title="Show image description" 77 + > 78 + ALT 79 + </button> 80 + )} 81 + </div> 82 + ); 83 + }; 84 + 85 + return ( 86 + <div 87 + class={ 88 + `bg-c-contrast-0 ` + 89 + (!borderless ? ` overflow-hidden rounded-md border border-c-contrast-200` : ``) + 90 + (hasStandaloneImage ? ` max-w-full self-start` : ``) 91 + } 92 + > 93 + {length === 4 ? ( 94 + <div class="flex gap-0.5"> 95 + <div class="flex grow basis-0 flex-col gap-0.5"> 96 + {/* @once */ render(images[0], RenderMode.MULTIPLE_SQUARE)} 97 + {/* @once */ render(images[2], RenderMode.MULTIPLE_SQUARE)} 98 + </div> 99 + 100 + <div class="flex grow basis-0 flex-col gap-0.5"> 101 + {/* @once */ render(images[1], RenderMode.MULTIPLE_SQUARE)} 102 + {/* @once */ render(images[3], RenderMode.MULTIPLE_SQUARE)} 103 + </div> 104 + </div> 105 + ) : length === 3 ? ( 106 + <div class="flex gap-0.5"> 107 + <div class="flex aspect-square grow-2 basis-0 flex-col gap-0.5"> 108 + {/* @once */ render(images[0], RenderMode.MULTIPLE)} 109 + </div> 110 + 111 + <div class="flex grow basis-0 flex-col gap-0.5"> 112 + {/* @once */ render(images[1], RenderMode.MULTIPLE_SQUARE)} 113 + {/* @once */ render(images[2], RenderMode.MULTIPLE_SQUARE)} 114 + </div> 115 + </div> 116 + ) : length === 2 ? ( 117 + <div class="flex aspect-video gap-0.5"> 118 + <div class="flex grow basis-0 flex-col gap-0.5"> 119 + {/* @once */ render(images[0], RenderMode.MULTIPLE)} 120 + </div> 121 + <div class="flex grow basis-0 flex-col gap-0.5"> 122 + {/* @once */ render(images[1], RenderMode.MULTIPLE)} 123 + </div> 124 + </div> 125 + ) : hasStandaloneImage ? ( 126 + <>{/* @once */ render(images[0], RenderMode.STANDALONE_RATIO)}</> 127 + ) : length === 1 ? ( 128 + <>{/* @once */ render(images[0], RenderMode.STANDALONE)}</> 129 + ) : null} 130 + </div> 131 + ); 132 + }; 133 + 134 + export default ImageEmbed;
+113
src/components/embeds/quote-embed.tsx
··· 1 + import { createMemo, type JSX } from 'solid-js'; 2 + 3 + import type { 4 + AppBskyEmbedImages, 5 + AppBskyEmbedRecord, 6 + AppBskyFeedDefs, 7 + AppBskyFeedPost, 8 + } from '@mary/bluesky-client/lexicons'; 9 + 10 + import { useProfileShadow } from '~/api/cache/profile-shadow'; 11 + import { moderateQuote } from '~/api/moderation/entities/quote'; 12 + import { parseAtUri } from '~/api/utils/strings'; 13 + 14 + import { useModerationOptions } from '~/lib/states/moderation'; 15 + 16 + import { ContextContentMedia, getModerationUI } from '~/api/moderation'; 17 + import Avatar from '../avatar'; 18 + import { handleLinkNavigation } from '../button'; 19 + import TimeAgo from '../time-ago'; 20 + import ImageEmbed from './image-embed'; 21 + 22 + export interface QuoteEmbedProps { 23 + /** Expected to be static */ 24 + quote: AppBskyEmbedRecord.ViewRecord; 25 + /** Expected to be static */ 26 + interactive?: boolean; 27 + /** Expected to be static. Whether it should show a large UI for image embeds */ 28 + large?: boolean; 29 + } 30 + 31 + const QuoteEmbed = ({ quote, interactive, large }: QuoteEmbedProps) => { 32 + const record = quote.value as AppBskyFeedPost.Record; 33 + const author = quote.author; 34 + const authorShadow = useProfileShadow(author); 35 + 36 + const uri = parseAtUri(quote.uri); 37 + const href = `/${author.did}/${uri.rkey}`; 38 + 39 + const text = record.text.trim(); 40 + const image = getPostImage(quote.embeds?.[0]); 41 + 42 + const moderationOptions = useModerationOptions(); 43 + const moderation = createMemo(() => moderateQuote(quote, authorShadow(), moderationOptions())); 44 + 45 + const showLargeImages = image && (large || !text); 46 + const shouldBlurImage = () => getModerationUI(moderation(), ContextContentMedia).b.length !== 0; 47 + 48 + return ( 49 + <a 50 + href={interactive ? href : undefined} 51 + onClick={interactive ? handleLinkNavigation : undefined} 52 + class={ 53 + `overflow-hidden rounded-md border border-c-contrast-200` + 54 + (interactive ? ` hover:bg-c-contrast-25` : ``) 55 + } 56 + > 57 + <div class="mx-3 mt-3 flex min-w-0 text-sm text-c-contrast-600"> 58 + <Avatar type="user" src={/* @once */ author.avatar} size="xs" class="mr-2" /> 59 + 60 + <span class="flex max-w-full gap-1 overflow-hidden text-ellipsis whitespace-nowrap text-left"> 61 + <bdi class="overflow-hidden text-ellipsis"> 62 + <span class="font-bold text-c-contrast-900"> 63 + {/* @once */ author.displayName || author.handle} 64 + </span> 65 + </bdi> 66 + <span class="block overflow-hidden text-ellipsis whitespace-nowrap"> 67 + @{/* @once */ author.handle} 68 + </span> 69 + </span> 70 + 71 + <span class="px-1">·</span> 72 + 73 + <span class="whitespace-nowrap"> 74 + <TimeAgo value={/* @once */ quote.indexedAt}> 75 + {(relative, _absolute) => relative as unknown as JSX.Element} 76 + </TimeAgo> 77 + </span> 78 + </div> 79 + 80 + {text ? ( 81 + <div class="flex items-start"> 82 + {image && !large && ( 83 + <div class="mb-3 ml-3 mt-2 grow basis-0"> 84 + <ImageEmbed embed={image} blur={shouldBlurImage()} /> 85 + </div> 86 + )} 87 + 88 + <div class="mx-3 mb-3 mt-2 line-clamp-6 min-w-0 grow-4 basis-0 whitespace-pre-wrap break-words text-sm empty:hidden"> 89 + {text} 90 + </div> 91 + </div> 92 + ) : ( 93 + <div class="mt-3"></div> 94 + )} 95 + 96 + {showLargeImages && <ImageEmbed embed={image} borderless blur={shouldBlurImage()} />} 97 + </a> 98 + ); 99 + }; 100 + 101 + export default QuoteEmbed; 102 + 103 + const getPostImage = (embed: AppBskyFeedDefs.PostView['embed']): AppBskyEmbedImages.View | undefined => { 104 + if (embed) { 105 + if (embed.$type === 'app.bsky.embed.images#view') { 106 + return embed; 107 + } 108 + 109 + if (embed.$type === 'app.bsky.embed.recordWithMedia#view') { 110 + return getPostImage(embed.media); 111 + } 112 + } 113 + };
+25
src/components/error-view.tsx
··· 1 + import { formatQueryError } from '~/api/utils/error'; 2 + 3 + import Button from './button'; 4 + 5 + export interface ErrorViewProps { 6 + error: unknown; 7 + onRetry?: () => void; 8 + } 9 + 10 + const ErrorView = (props: ErrorViewProps) => { 11 + return ( 12 + <div class="p-4"> 13 + <div class="mb-4 text-sm"> 14 + <p class="font-bold">Something went wrong</p> 15 + <p class="text-muted-fg">{formatQueryError(props.error)}</p> 16 + </div> 17 + 18 + <Button onClick={props.onRetry} variant="primary"> 19 + Try again 20 + </Button> 21 + </div> 22 + ); 23 + }; 24 + 25 + export default ErrorView;
+28
src/components/fab.tsx
··· 1 + import type { Component } from 'solid-js'; 2 + 3 + export interface FABProps { 4 + label: string; 5 + icon: Component; 6 + onClick?: () => void; 7 + } 8 + 9 + const FAB = (props: FABProps) => { 10 + return ( 11 + <button title={props.label} class={fabClassNames()}> 12 + {(() => { 13 + const Icon = props.icon; 14 + return <Icon />; 15 + })()} 16 + </button> 17 + ); 18 + }; 19 + 20 + export default FAB; 21 + 22 + const fabClassNames = (): string => { 23 + let cn = `flex h-12 w-12 items-center justify-center`; 24 + 25 + cn += ` bg-c-primary-500 text-c-white hover:bg-c-primary-600`; 26 + 27 + return cn; 28 + };
+121
src/components/feeds/post-actions.tsx
··· 1 + import type { AppBskyFeedDefs } from '@mary/bluesky-client/lexicons'; 2 + import { useQueryClient } from '@mary/solid-query'; 3 + 4 + import { updatePostShadow, type PostShadowView } from '~/api/cache/post-shadow'; 5 + 6 + import { openModal } from '~/globals/modals'; 7 + 8 + import * as Menu from '../menu'; 9 + import HeartOutlinedIcon from '../icons-central/heart-outline'; 10 + import HeartSolidIcon from '../icons-central/heart-solid'; 11 + import RepeatOutlinedIcon from '../icons-central/repeat-outline'; 12 + import ReplyOutlinedIcon from '../icons-central/reply-outline'; 13 + import ShareOutlinedIcon from '../icons-central/share-outline'; 14 + import WriteOutlinedIcon from '../icons-central/write-outline'; 15 + 16 + export interface PostActionsProps { 17 + /** Expected to be static */ 18 + post: AppBskyFeedDefs.PostView; 19 + shadow: PostShadowView; 20 + } 21 + 22 + const PostActions = (props: PostActionsProps) => { 23 + const queryClient = useQueryClient(); 24 + 25 + const post = props.post; 26 + 27 + const replyCount = post.replyCount ?? 0; 28 + const isLiked = () => !!props.shadow.likeUri; 29 + const isReposted = () => !!props.shadow.repostUri; 30 + 31 + const toggleLike = () => { 32 + updatePostShadow(queryClient, post.uri, { likeUri: isLiked() ? undefined : `pending` }); 33 + }; 34 + 35 + const toggleRepost = () => { 36 + updatePostShadow(queryClient, post.uri, { repostUri: isReposted() ? undefined : `pending` }); 37 + }; 38 + 39 + return ( 40 + <div class="mt-3 flex items-center text-c-contrast-600"> 41 + <div class="min-w-0 grow basis-0"> 42 + <button class={`group flex max-w-full grow basis-0 items-end gap-0.5`}> 43 + <div class="-my-1.5 -ml-2 flex h-8 w-8 shrink-0 items-center justify-center rounded-full text-base group-hover:bg-c-contrast-50"> 44 + <ReplyOutlinedIcon /> 45 + </div> 46 + 47 + <span class="overflow-hidden text-ellipsis whitespace-nowrap pr-2 text-de">{replyCount}</span> 48 + </button> 49 + </div> 50 + 51 + <div class="min-w-0 grow basis-0"> 52 + <button 53 + onClick={(ev) => { 54 + const anchor = ev.currentTarget; 55 + 56 + openModal(({ close }) => ( 57 + <Menu.Container anchor={anchor} placement="bottom"> 58 + <Menu.Item 59 + icon={RepeatOutlinedIcon} 60 + label={!isReposted() ? `Repost` : `Undo repost`} 61 + onClick={() => { 62 + close(); 63 + toggleRepost(); 64 + }} 65 + /> 66 + 67 + <Menu.Item 68 + icon={WriteOutlinedIcon} 69 + label="Quote" 70 + onClick={() => { 71 + close(); 72 + }} 73 + /> 74 + </Menu.Container> 75 + )); 76 + }} 77 + class={ 78 + `group flex max-w-full grow basis-0 items-end gap-0.5` + 79 + (isReposted() ? ` text-c-positive-600` : ``) 80 + } 81 + > 82 + <div class="-my-1.5 -ml-2 flex h-8 w-8 shrink-0 items-center justify-center rounded-full text-base group-hover:bg-c-contrast-50"> 83 + <RepeatOutlinedIcon /> 84 + </div> 85 + 86 + <span class="overflow-hidden text-ellipsis whitespace-nowrap pr-2 text-de"> 87 + {props.shadow.repostCount} 88 + </span> 89 + </button> 90 + </div> 91 + 92 + <div class="min-w-0 grow basis-0"> 93 + <button 94 + onClick={toggleLike} 95 + class={ 96 + `group flex max-w-full grow basis-0 items-end gap-0.5` + (isLiked() ? ` text-c-negative-400` : ``) 97 + } 98 + > 99 + <div class="-my-1.5 -ml-2 flex h-8 w-8 shrink-0 items-center justify-center rounded-full text-base group-hover:bg-c-contrast-50"> 100 + {(() => { 101 + const Icon = !isLiked() ? HeartOutlinedIcon : HeartSolidIcon; 102 + return <Icon />; 103 + })()} 104 + </div> 105 + 106 + <span class="overflow-hidden text-ellipsis whitespace-nowrap pr-2 text-de"> 107 + {props.shadow.likeCount} 108 + </span> 109 + </button> 110 + </div> 111 + 112 + <div class="shrink-0"> 113 + <button class="-mx-2 -my-1.5 flex h-8 w-8 items-center justify-center rounded-full text-base hover:bg-c-contrast-50"> 114 + <ShareOutlinedIcon /> 115 + </button> 116 + </div> 117 + </div> 118 + ); 119 + }; 120 + 121 + export default PostActions;
+111
src/components/feeds/post-feed-item.tsx
··· 1 + import { createMemo } from 'solid-js'; 2 + 3 + import type { AppBskyFeedPost, At } from '@mary/bluesky-client/lexicons'; 4 + 5 + import { usePostShadow } from '~/api/cache/post-shadow'; 6 + import { useProfileShadow } from '~/api/cache/profile-shadow'; 7 + import type { UiTimelineItem } from '~/api/models/timeline'; 8 + import { moderatePost } from '~/api/moderation/entities/post'; 9 + import { parseAtUri } from '~/api/utils/strings'; 10 + 11 + import { useModerationOptions } from '~/lib/states/moderation'; 12 + 13 + import Avatar from '../avatar'; 14 + import { handleLinkNavigation } from '../button'; 15 + import RepeatOutlinedIcon from '../icons-central/repeat-outline'; 16 + import RichText from '../rich-text'; 17 + 18 + import PostActions from './post-actions'; 19 + import PostMeta from './post-meta'; 20 + import PostReplyContext from './post-reply-context'; 21 + import Embed from '../embeds/embed'; 22 + 23 + export interface PostFeedItemProps { 24 + /** Expected to be static */ 25 + item: UiTimelineItem; 26 + timelineDid?: At.DID; 27 + } 28 + 29 + const PostFeedItem = ({ item, timelineDid }: PostFeedItemProps) => { 30 + const moderationOptions = useModerationOptions(); 31 + 32 + const { post, reason, next, prev } = item; 33 + 34 + const author = post.author; 35 + const record = post.record as AppBskyFeedPost.Record; 36 + const embed = post.embed; 37 + 38 + const shadow = usePostShadow(post); 39 + const authorShadow = useProfileShadow(author); 40 + 41 + const uri = parseAtUri(post.uri); 42 + const authorHref = `/${author.did}`; 43 + const href = `/${author.did}/${uri.rkey}`; 44 + 45 + const moderation = createMemo(() => moderatePost(post, authorShadow(), moderationOptions())); 46 + 47 + return ( 48 + <div hidden={shadow().deleted} class={`relative border-c-contrast-200 px-4` + (!next ? ` border-b` : ``)}> 49 + <div class="relative flex flex-col pb-1 pt-2"> 50 + {prev && ( 51 + <div class="flex w-9 flex-col items-center"> 52 + <div class="absolute bottom-1 top-0 grow border-l-2 border-c-contrast-200" /> 53 + </div> 54 + )} 55 + 56 + {/* @once */ renderReason(reason)} 57 + </div> 58 + 59 + <div class="flex gap-3"> 60 + <div class="flex shrink-0 flex-col items-center"> 61 + <Avatar 62 + type={/* @once */ author.associated?.labeler ? 'labeler' : 'user'} 63 + src={/* @once */ author.avatar} 64 + href={authorHref} 65 + moderation={moderation()} 66 + /> 67 + 68 + {next && <div class="mt-1 grow border-l-2 border-c-contrast-200" />} 69 + </div> 70 + 71 + <div class="min-w-0 grow pb-3"> 72 + <PostMeta post={post} href={href} authorHref={authorHref} gutterBottom /> 73 + <PostReplyContext item={item} /> 74 + 75 + <RichText text={/* @once */ record.text} facets={/* @once */ record.facets} clipped /> 76 + {embed && <Embed embed={embed} moderation={moderation()} gutterTop />} 77 + 78 + <PostActions post={post} shadow={shadow()} /> 79 + </div> 80 + </div> 81 + </div> 82 + ); 83 + }; 84 + 85 + export default PostFeedItem; 86 + 87 + const renderReason = (reason: UiTimelineItem['reason']) => { 88 + if (reason) { 89 + const type = reason.$type; 90 + 91 + if (type === 'app.bsky.feed.defs#reasonRepost') { 92 + const by = reason.by; 93 + const did = by.did; 94 + const name = by.displayName || by.handle; 95 + 96 + return ( 97 + <div class="flex items-center gap-3 text-de text-c-contrast-600"> 98 + <div class="flex w-9 shrink-0 justify-end"> 99 + <RepeatOutlinedIcon class="text-sm" /> 100 + </div> 101 + <a href={`/${did}`} onClick={handleLinkNavigation} class="flex min-w-0 font-medium hover:underline"> 102 + <span dir="auto" class="overflow-hidden text-ellipsis whitespace-nowrap"> 103 + {name} 104 + </span> 105 + <span class="shrink-0 whitespace-pre"> Reposted</span> 106 + </a> 107 + </div> 108 + ); 109 + } 110 + } 111 + };
+66
src/components/feeds/post-meta.tsx
··· 1 + import type { AppBskyFeedDefs } from '@mary/bluesky-client/lexicons'; 2 + 3 + import { handleLinkNavigation } from '../button'; 4 + import MoreHorizOutlinedIcon from '../icons-central/more-horiz-outline'; 5 + import TimeAgo from '../time-ago'; 6 + 7 + export interface PostMetaProps { 8 + /** Expected to be static */ 9 + post: AppBskyFeedDefs.PostView; 10 + authorHref: string; 11 + href: string; 12 + gutterBottom?: boolean; 13 + } 14 + 15 + const PostMeta = ({ post, authorHref, href, gutterBottom }: PostMetaProps) => { 16 + const author = post.author; 17 + 18 + const displayName = author.displayName; 19 + const handle = author.handle; 20 + const indexedAt = post.indexedAt; 21 + 22 + return ( 23 + <div 24 + class={`flex items-center justify-between gap-4 text-c-contrast-600` + (gutterBottom ? ` mb-0.5` : ``)} 25 + > 26 + <div class="flex items-center overflow-hidden text-sm"> 27 + <a 28 + href={authorHref} 29 + onClick={handleLinkNavigation} 30 + class="flex max-w-full gap-1 overflow-hidden text-ellipsis whitespace-nowrap text-left" 31 + > 32 + {displayName && ( 33 + <bdi class="overflow-hidden text-ellipsis font-bold text-c-contrast-900 hover:underline"> 34 + {displayName} 35 + </bdi> 36 + )} 37 + 38 + <span class="block overflow-hidden text-ellipsis whitespace-nowrap">@{handle}</span> 39 + </a> 40 + 41 + <span class="px-1">·</span> 42 + 43 + <TimeAgo value={indexedAt}> 44 + {(relative, absolute) => ( 45 + <a 46 + title={absolute()} 47 + href={href} 48 + onClick={handleLinkNavigation} 49 + class="whitespace-nowrap hover:underline" 50 + > 51 + {relative()} 52 + </a> 53 + )} 54 + </TimeAgo> 55 + </div> 56 + 57 + <div class="shrink-0"> 58 + <button class="-mx-2 -my-1.5 flex h-8 w-8 items-center justify-center rounded-full text-base hover:bg-c-contrast-50"> 59 + <MoreHorizOutlinedIcon /> 60 + </button> 61 + </div> 62 + </div> 63 + ); 64 + }; 65 + 66 + export default PostMeta;
+40
src/components/feeds/post-reply-context.tsx
··· 1 + import type { AppBskyFeedPost } from '@mary/bluesky-client/lexicons'; 2 + import type { UiTimelineItem } from '~/api/models/timeline'; 3 + 4 + export interface PostReplyContextProps { 5 + /** Expected to be static */ 6 + item: UiTimelineItem; 7 + } 8 + 9 + const PostReplyContext = (props: PostReplyContextProps) => { 10 + const { post, reply, prev } = props.item; 11 + 12 + if (!prev) { 13 + const parent = reply?.parent; 14 + if (parent) { 15 + const author = parent.author; 16 + const did = author.did; 17 + const handle = author.handle; 18 + 19 + return ( 20 + <div class="mb-0.5 flex text-sm text-c-contrast-500"> 21 + <span class="shrink-0 whitespace-pre">Replying to </span> 22 + <a 23 + dir="auto" 24 + href={`/${did}`} 25 + class="overflow-hidden text-ellipsis whitespace-nowrap text-c-primary-400 hover:underline" 26 + > 27 + @{handle} 28 + </a> 29 + </div> 30 + ); 31 + } 32 + 33 + const record = post.record as AppBskyFeedPost.Record; 34 + if (record.reply) { 35 + return <div class="mb-0.5 text-sm text-c-contrast-500">Replying to unknown</div>; 36 + } 37 + } 38 + }; 39 + 40 + export default PostReplyContext;
+42
src/components/fieldset.tsx
··· 1 + import { createContext, createMemo, useContext, type ParentProps } from 'solid-js'; 2 + 3 + export interface FieldsetContext { 4 + readonly disabled: boolean; 5 + } 6 + 7 + const DEFAULT_FIELDSET: FieldsetContext = { 8 + disabled: false, 9 + }; 10 + 11 + const Context = createContext<FieldsetContext>(DEFAULT_FIELDSET); 12 + 13 + export const useFieldset = (): FieldsetContext => { 14 + return useContext(Context); 15 + }; 16 + 17 + export interface FieldsetProps extends ParentProps { 18 + standalone?: boolean; 19 + disabled?: boolean; 20 + } 21 + 22 + export const Fieldset = (props: FieldsetProps) => { 23 + let context: FieldsetContext; 24 + 25 + if (!('disabled' in props) && props.standalone) { 26 + context = DEFAULT_FIELDSET; 27 + } else { 28 + const parent = useFieldset(); 29 + 30 + const isDisabled = createMemo((): boolean => { 31 + return (!props.standalone && parent.disabled) || !!props.disabled; 32 + }); 33 + 34 + context = { 35 + get disabled() { 36 + return isDisabled(); 37 + }, 38 + }; 39 + } 40 + 41 + return <Context.Provider value={context}>{props.children}</Context.Provider>; 42 + };
+69
src/components/icon-button.tsx
··· 1 + import type { Component } from 'solid-js'; 2 + import { useFieldset } from './fieldset'; 3 + 4 + export interface IconButtonProps { 5 + icon: Component; 6 + title: string; 7 + // href?: string; 8 + disabled?: boolean; 9 + onClick?: (ev: MouseEvent) => void; 10 + 11 + variant?: 'ghost' | 'outline'; 12 + size?: 'md' | 'sm'; 13 + class?: string; 14 + } 15 + 16 + const IconButton = (props: IconButtonProps) => { 17 + const fieldset = useFieldset(); 18 + const isDisabled = (): boolean => fieldset.disabled || !!props.disabled; 19 + 20 + return ( 21 + <button 22 + type="button" 23 + disabled={isDisabled()} 24 + title={props.title} 25 + onClick={props.onClick} 26 + class={iconButtonClasses(isDisabled, props)} 27 + > 28 + {(() => { 29 + const Icon = props.icon; 30 + return <Icon />; 31 + })()} 32 + </button> 33 + ); 34 + }; 35 + 36 + const iconButtonClasses = ( 37 + isDisabled: () => boolean, 38 + { variant = 'ghost', size = 'md', class: className }: IconButtonProps, 39 + ) => { 40 + var cn = `grid place-items-center rounded-full`; 41 + 42 + if (variant === 'ghost') { 43 + if (!isDisabled()) { 44 + cn += ` text-c-contrast-900 hover:bg-c-contrast-50`; 45 + } else { 46 + cn += ` text-c-contrast-400`; 47 + } 48 + } else if (variant === 'outline') { 49 + if (!isDisabled()) { 50 + cn += ` border border-c-contrast-300 text-c-contrast-900 hover:bg-c-contrast-25`; 51 + } else { 52 + cn += ` border border-c-contrast-100 text-c-contrast-400`; 53 + } 54 + } 55 + 56 + if (size === 'md') { 57 + cn += ` h-9 w-9 text-lg`; 58 + } else if (size === 'sm') { 59 + cn += ` h-8 w-8 text-lg`; 60 + } 61 + 62 + if (className) { 63 + return `${cn} ${className}`; 64 + } else { 65 + return cn; 66 + } 67 + }; 68 + 69 + export default IconButton;
+15
src/components/icons-central/_icon.tsx
··· 1 + import { type ComponentProps, type JSX } from 'solid-js'; 2 + import { spread } from 'solid-js/web'; 3 + 4 + /*#__NO_SIDE_EFFECTS__*/ 5 + export const createIcon = (path: () => JSX.Element) => { 6 + // @ts-expect-error 7 + return Icon.bind(path); 8 + }; 9 + 10 + function Icon(this: () => Element, props: ComponentProps<'svg'>) { 11 + const svg = this(); 12 + spread(svg, props, true, true); 13 + 14 + return svg; 15 + }
+10
src/components/icons-central/add-outline.tsx
··· 1 + import { createIcon } from './_icon'; 2 + 3 + // plus-large 4 + const AddOutlinedIcon = createIcon(() => ( 5 + <svg width="1em" height="1em" fill="none" viewBox="0 0 24 24"> 6 + <path stroke="currentColor" stroke-linecap="square" stroke-width="2" d="M12 4v8m0 0v8m0-8H4m8 0h8" /> 7 + </svg> 8 + )); 9 + 10 + export default AddOutlinedIcon;
+9
src/components/icons-central/arrow-left-outline.tsx
··· 1 + import { createIcon } from './_icon'; 2 + 3 + const ArrowLeftOutlinedIcon = createIcon(() => ( 4 + <svg width="1em" height="1em" fill="none" viewBox="0 0 24 24"> 5 + <path stroke="currentColor" stroke-linecap="square" stroke-width="2" d="m10 6-6 6 6 6m-5-6h15" /> 6 + </svg> 7 + )); 8 + 9 + export default ArrowLeftOutlinedIcon;
+13
src/components/icons-central/bell-outline.tsx
··· 1 + import { createIcon } from './_icon'; 2 + 3 + const BellOutlinedIcon = createIcon(() => ( 4 + <svg width="1em" height="1em" fill="none" viewBox="0 0 24 24"> 5 + <path 6 + stroke="currentColor" 7 + stroke-width="2" 8 + d="M8.22 18.5C8.976 19.994 10.386 21 12 21c1.615 0 3.025-1.006 3.78-2.5M20 18l-1.207-9.053a6.853 6.853 0 0 0-13.586 0L4 18h16Z" 9 + /> 10 + </svg> 11 + )); 12 + 13 + export default BellOutlinedIcon;
+14
src/components/icons-central/bell-solid.tsx
··· 1 + import { createIcon } from './_icon'; 2 + 3 + const BellSolidIcon = createIcon(() => ( 4 + <svg width="1em" height="1em" fill="none" viewBox="0 0 24 24"> 5 + <path 6 + fill="currentColor" 7 + fill-rule="evenodd" 8 + d="M12 2a7.853 7.853 0 0 0-7.784 6.815L2.858 19h4.496c.904 1.748 2.607 3 4.646 3 2.039 0 3.742-1.252 4.646-3h4.496L19.784 8.815A7.853 7.853 0 0 0 12 2Zm2.222 17H9.778c.61.637 1.399 1 2.222 1s1.613-.363 2.222-1Z" 9 + clip-rule="evenodd" 10 + /> 11 + </svg> 12 + )); 13 + 14 + export default BellSolidIcon;
+14
src/components/icons-central/bullet-list-outline.tsx
··· 1 + import { createIcon } from './_icon'; 2 + 3 + const BulletListOutlinedIcon = createIcon(() => ( 4 + <svg width="1em" height="1em" fill="none" viewBox="0 0 24 24"> 5 + <path 6 + stroke="currentColor" 7 + stroke-linecap="square" 8 + stroke-width="2" 9 + d="M13 17h7M13 7h7M8 7a2 2 0 1 1-4 0 2 2 0 0 1 4 0Zm0 10a2 2 0 1 1-4 0 2 2 0 0 1 4 0Z" 10 + /> 11 + </svg> 12 + )); 13 + 14 + export default BulletListOutlinedIcon;
+15
src/components/icons-central/chevron-right-outline.tsx
··· 1 + import { createIcon } from './_icon'; 2 + 3 + // chevron-right-small 4 + const ChevronRightOutlinedIcon = createIcon(() => ( 5 + <svg width="1em" height="1em" fill="none" viewBox="0 0 24 24"> 6 + <path 7 + fill="currentColor" 8 + fill-rule="evenodd" 9 + d="M10 6.586 15.414 12 10 17.414 8.586 16l4-4-4-4L10 6.586Z" 10 + clip-rule="evenodd" 11 + /> 12 + </svg> 13 + )); 14 + 15 + export default ChevronRightOutlinedIcon;
+14
src/components/icons-central/circle-check-solid.tsx
··· 1 + import { createIcon } from './_icon'; 2 + 3 + const CircleCheckSolidIcon = createIcon(() => ( 4 + <svg width="1em" height="1em" fill="none" viewBox="0 0 24 24"> 5 + <path 6 + fill="currentColor" 7 + fill-rule="evenodd" 8 + d="M12 22c5.523 0 10-4.477 10-10S17.523 2 12 2 2 6.477 2 12s4.477 10 10 10Zm-1.426-5.512 5.833-7.129-1.548-1.266-4.433 5.418L8.5 11.586 7.086 13l3.488 3.488Z" 9 + clip-rule="evenodd" 10 + /> 11 + </svg> 12 + )); 13 + 14 + export default CircleCheckSolidIcon;
+9
src/components/icons-central/cross-large-outline.tsx
··· 1 + import { createIcon } from './_icon'; 2 + 3 + const CrossLargeOutlinedIcon = createIcon(() => ( 4 + <svg width="1em" height="1em" fill="none" viewBox="0 0 24 24"> 5 + <path stroke="currentColor" stroke-linecap="square" stroke-width="2" d="m5 5 14 14m0-14L5 19" /> 6 + </svg> 7 + )); 8 + 9 + export default CrossLargeOutlinedIcon;
+10
src/components/icons-central/filter-outline.tsx
··· 1 + import { createIcon } from './_icon'; 2 + 3 + // filter-1 4 + const FilterOutlinedIcon = createIcon(() => ( 5 + <svg width="1em" height="1em" fill="none" viewBox="0 0 24 24"> 6 + <path stroke="currentColor" stroke-width="2" d="M20 4H4v4l6 6v7l4-1v-6l6-6V4Z" /> 7 + </svg> 8 + )); 9 + 10 + export default FilterOutlinedIcon;
+15
src/components/icons-central/gear-outline.tsx
··· 1 + import { createIcon } from './_icon'; 2 + 3 + // settings-gear-2 4 + const GearOutlinedIcon = createIcon(() => ( 5 + <svg width="1em" height="1em" fill="none" viewBox="0 0 24 24"> 6 + <path 7 + stroke="currentColor" 8 + stroke-width="2" 9 + d="m9.3 5.7-2.925-.675-1.35 1.35L5.7 9.3 3 11.1v1.8l2.7 1.8-.675 2.925 1.35 1.35L9.3 18.3l1.8 2.7h1.8l1.8-2.7 2.925.675 1.35-1.35L18.3 14.7l2.7-1.8v-1.8l-2.7-1.8.675-2.925-1.35-1.35L14.7 5.7 12.9 3h-1.8L9.3 5.7Z" 10 + /> 11 + <path stroke="currentColor" stroke-width="2" d="M15 12a3 3 0 1 1-6 0 3 3 0 0 1 6 0Z" /> 12 + </svg> 13 + )); 14 + 15 + export default GearOutlinedIcon;
+14
src/components/icons-central/hashtag-outline.tsx
··· 1 + import { createIcon } from './_icon'; 2 + 3 + const HashtagOutlinedIcon = createIcon(() => ( 4 + <svg width="1em" height="1em" fill="none" viewBox="0 0 24 24"> 5 + <path 6 + stroke="currentColor" 7 + stroke-linecap="square" 8 + stroke-width="2" 9 + d="M9 4 7 20M17 4l-2 16M4 8h16m0 8H4" 10 + /> 11 + </svg> 12 + )); 13 + 14 + export default HashtagOutlinedIcon;
+14
src/components/icons-central/heart-outline.tsx
··· 1 + import { createIcon } from './_icon'; 2 + 3 + // heart-2 4 + const HeartOutlinedIcon = createIcon(() => ( 5 + <svg width="1em" height="1em" fill="none" viewBox="0 0 24 24"> 6 + <path 7 + stroke="currentColor" 8 + stroke-width="2" 9 + d="M12 5.768c6.162-6.25 16.725 5.358 0 14.732C-4.725 11.126 5.838-.482 12 5.768Z" 10 + /> 11 + </svg> 12 + )); 13 + 14 + export default HeartOutlinedIcon;
+13
src/components/icons-central/heart-solid.tsx
··· 1 + import { createIcon } from './_icon'; 2 + 3 + // heart-2 4 + const HeartSolidIcon = createIcon(() => ( 5 + <svg width="1em" height="1em" fill="none" viewBox="0 0 24 24"> 6 + <path 7 + fill="currentColor" 8 + d="M12.489 21.372c8.528-4.78 10.626-10.47 9.022-14.47-.779-1.941-2.414-3.333-4.342-3.763-1.697-.378-3.552.003-5.169 1.287-1.617-1.284-3.472-1.665-5.17-1.287-1.927.43-3.562 1.822-4.34 3.764-1.605 4 .493 9.69 9.021 14.47l.489.274.489-.274Z" 9 + /> 10 + </svg> 11 + )); 12 + 13 + export default HeartSolidIcon;
+10
src/components/icons-central/home-outline.tsx
··· 1 + import { createIcon } from './_icon'; 2 + 3 + // home-open 4 + const HomeOutlinedIcon = createIcon(() => ( 5 + <svg width="1em" height="1em" fill="none" viewBox="0 0 24 24"> 6 + <path stroke="currentColor" stroke-width="2" d="M20 20V9l-8-6.5L4 9v11h6v-6h4v6h6Z" /> 7 + </svg> 8 + )); 9 + 10 + export default HomeOutlinedIcon;
+10
src/components/icons-central/home-solid.tsx
··· 1 + import { createIcon } from './_icon'; 2 + 3 + // home-open 4 + const HomeSolidIcon = createIcon(() => ( 5 + <svg width="1em" height="1em" fill="none" viewBox="0 0 24 24"> 6 + <path fill="currentColor" d="m21 8.524-9-7.312-9 7.312V21h7v-7h4v7h7V8.524Z" /> 7 + </svg> 8 + )); 9 + 10 + export default HomeSolidIcon;
+21
src/components/icons-central/info-outline.tsx
··· 1 + import { createIcon } from './_icon'; 2 + 3 + // info-circle 4 + const InfoOutlinedIcon = createIcon(() => ( 5 + <svg width="1em" height="1em" fill="none" viewBox="0 0 24 24"> 6 + <path 7 + stroke="currentColor" 8 + stroke-linecap="square" 9 + stroke-width="2" 10 + d="M11 11h1v5m9-4a9 9 0 1 1-18 0 9 9 0 0 1 18 0Z" 11 + /> 12 + <path 13 + fill="currentColor" 14 + stroke="currentColor" 15 + stroke-width=".5" 16 + d="M11.5 7.25h-.25v1.5h1.5v-1.5H11.5Z" 17 + /> 18 + </svg> 19 + )); 20 + 21 + export default InfoOutlinedIcon;
+15
src/components/icons-central/leave-outline.tsx
··· 1 + import { createIcon } from './_icon'; 2 + 3 + // arrow-box-left 4 + const LeaveOutlinedIcon = createIcon(() => ( 5 + <svg width="1em" height="1em" fill="none" viewBox="0 0 24 24"> 6 + <path 7 + stroke="currentColor" 8 + stroke-linecap="square" 9 + stroke-width="2" 10 + d="M11 20H4V4h7m-2 8h10m-3.5 4.5L20 12l-4.5-4.5" 11 + /> 12 + </svg> 13 + )); 14 + 15 + export default LeaveOutlinedIcon;
+14
src/components/icons-central/magnifying-glass-outline.tsx
··· 1 + import { createIcon } from './_icon'; 2 + 3 + const MagnifyingGlassOutlinedIcon = createIcon(() => ( 4 + <svg width="1em" height="1em" fill="none" viewBox="0 0 24 24"> 5 + <path 6 + stroke="currentColor" 7 + stroke-linecap="square" 8 + stroke-width="2" 9 + d="m20 20-3.95-3.95M18 11a7 7 0 1 1-14 0 7 7 0 0 1 14 0Z" 10 + /> 11 + </svg> 12 + )); 13 + 14 + export default MagnifyingGlassOutlinedIcon;
+13
src/components/icons-central/mail-outline.tsx
··· 1 + import { createIcon } from './_icon'; 2 + 3 + // mail-3 4 + const MailOutlinedIcon = createIcon(() => ( 5 + <svg width="1em" height="1em" fill="none" viewBox="0 0 24 24"> 6 + <path 7 + fill="currentColor" 8 + d="M21 5h1V4h-1v1Zm0 14v1h1v-1h-1ZM3 19H2v1h1v-1ZM3 5V4H2v1h1Zm9 8-.447.894.447.224.447-.224L12 13Zm8-8v14h2V5h-2Zm1 13H3v2h18v-2ZM4 19V5H2v14h2ZM3 6h18V4H3v2Zm9.447 6.106L3.735 7.75l-.894 1.789 8.712 4.355.894-1.788Zm7.818-4.356-8.712 4.356.894 1.788L21.16 9.54l-.894-1.79Z" 9 + /> 10 + </svg> 11 + )); 12 + 13 + export default MailOutlinedIcon;
+11
src/components/icons-central/mail-solid.tsx
··· 1 + import { createIcon } from './_icon'; 2 + 3 + // mail-3 4 + const MailSolidIcon = createIcon(() => ( 5 + <svg width="1em" height="1em" fill="none" viewBox="0 0 24 24"> 6 + <path fill="currentColor" d="M2 6.882V4h20v2.882l-10 5-10-5Z" /> 7 + <path fill="currentColor" d="M2 9.118V20h20V9.118l-10 5-10-5Z" /> 8 + </svg> 9 + )); 10 + 11 + export default MailSolidIcon;
+15
src/components/icons-central/menu-outline.tsx
··· 1 + import { createIcon } from './_icon'; 2 + 3 + // bars-three 4 + const MenuOutlinedIcon = createIcon(() => ( 5 + <svg width="1em" height="1em" fill="none" viewBox="0 0 24 24"> 6 + <path 7 + fill="currentColor" 8 + fill-rule="evenodd" 9 + d="M2 5h20v2H2V5Zm0 6h20v2H2v-2Zm0 6h20v2H2v-2Z" 10 + clip-rule="evenodd" 11 + /> 12 + </svg> 13 + )); 14 + 15 + export default MenuOutlinedIcon;
+15
src/components/icons-central/more-horiz-outline.tsx
··· 1 + import { createIcon } from './_icon'; 2 + 3 + // dot-grid-1x3-horizontal 4 + const MoreHorizOutlinedIcon = createIcon(() => ( 5 + <svg width="1em" height="1em" fill="none" viewBox="0 0 24 24"> 6 + <path 7 + fill="currentColor" 8 + fill-rule="evenodd" 9 + d="M2 12a2 2 0 1 1 4 0 2 2 0 0 1-4 0Zm8 0a2 2 0 1 1 4 0 2 2 0 0 1-4 0Zm8 0a2 2 0 1 1 4 0 2 2 0 0 1-4 0Z" 10 + clip-rule="evenodd" 11 + /> 12 + </svg> 13 + )); 14 + 15 + export default MoreHorizOutlinedIcon;
+13
src/components/icons-central/person-outline.tsx
··· 1 + import { createIcon } from './_icon'; 2 + 3 + // people 4 + const PersonOutlinedIcon = createIcon(() => ( 5 + <svg width="1em" height="1em" fill="none" viewBox="0 0 24 24"> 6 + <path 7 + fill="currentColor" 8 + d="m4.5 20-.996-.094L3.402 21H4.5v-1Zm15 0v1h1.098l-.102-1.094L19.5 20Zm-5-13.5A2.5 2.5 0 0 1 12 9v2a4.5 4.5 0 0 0 4.5-4.5h-2ZM12 9a2.5 2.5 0 0 1-2.5-2.5h-2A4.5 4.5 0 0 0 12 11V9ZM9.5 6.5A2.5 2.5 0 0 1 12 4V2a4.5 4.5 0 0 0-4.5 4.5h2ZM12 4a2.5 2.5 0 0 1 2.5 2.5h2A4.5 4.5 0 0 0 12 2v2ZM5.496 20.094C5.82 16.63 8.377 14 12 14v-2c-4.758 0-8.083 3.521-8.496 7.906l1.992.188ZM12 14c3.623 0 6.179 2.63 6.504 6.094l1.992-.188C20.083 15.521 16.758 12 12 12v2Zm-7.5 7h15v-2h-15v2Z" 9 + /> 10 + </svg> 11 + )); 12 + 13 + export default PersonOutlinedIcon;
+13
src/components/icons-central/person-remove-outline.tsx
··· 1 + import { createIcon } from './_icon'; 2 + 3 + // people-remove 4 + const PersonRemoveOutlinedIcon = createIcon(() => ( 5 + <svg width="1em" height="1em" fill="none" viewBox="0 0 24 24"> 6 + <path 7 + fill="currentColor" 8 + d="m4.5 20-.996-.094L3.402 21H4.5v-1Zm16.207-3.293.707-.707L20 14.586l-.707.707 1.414 1.414Zm-5.414 2.586-.707.707L16 21.414l.707-.707-1.414-1.414Zm1.414-4L16 14.586 14.586 16l.707.707 1.414-1.414Zm2.586 5.414.707.707L21.414 20l-.707-.707-1.414 1.414ZM14.5 6.5A2.5 2.5 0 0 1 12 9v2a4.5 4.5 0 0 0 4.5-4.5h-2ZM12 9a2.5 2.5 0 0 1-2.5-2.5h-2A4.5 4.5 0 0 0 12 11V9ZM9.5 6.5A2.5 2.5 0 0 1 12 4V2a4.5 4.5 0 0 0-4.5 4.5h2ZM12 4a2.5 2.5 0 0 1 2.5 2.5h2A4.5 4.5 0 0 0 12 2v2ZM5.496 20.094C5.82 16.63 8.377 14 12 14v-2c-4.758 0-8.083 3.521-8.496 7.906l1.992.188ZM4.5 21H13v-2H4.5v2Zm7.5-7c.621 0 1.206.077 1.748.218l.504-1.936A8.931 8.931 0 0 0 12 12v2Zm7.293 1.293-2 2 1.414 1.414 2-2-1.414-1.414Zm-2 2-2 2 1.414 1.414 2-2-1.414-1.414Zm-2-.586 2 2 1.414-1.414-2-2-1.414 1.414Zm2 2 2 2 1.414-1.414-2-2-1.414 1.414Z" 9 + /> 10 + </svg> 11 + )); 12 + 13 + export default PersonRemoveOutlinedIcon;
+15
src/components/icons-central/problem-outline.tsx
··· 1 + import { createIcon } from './_icon'; 2 + 3 + // triangle-exclamation 4 + const ProblemOutlinedIcon = createIcon(() => ( 5 + <svg width="1em" height="1em" fill="none" viewBox="0 0 24 24"> 6 + <path 7 + stroke="currentColor" 8 + stroke-linecap="square" 9 + stroke-width="2" 10 + d="M12 16v-.01M12 10v3m0-10L2.5 19h19L12 3Z" 11 + /> 12 + </svg> 13 + )); 14 + 15 + export default ProblemOutlinedIcon;
+15
src/components/icons-central/repeat-outline.tsx
··· 1 + import { createIcon } from './_icon'; 2 + 3 + // arrows-repeat-right-left 4 + const RepeatOutlinedIcon = createIcon(() => ( 5 + <svg width="1em" height="1em" fill="none" viewBox="0 0 24 24"> 6 + <path 7 + stroke="currentColor" 8 + stroke-linecap="square" 9 + stroke-width="2" 10 + d="m17 3 3 3-3 3M7 21l-3-3 3-3m-2 3h15v-5M4 11V6h15" 11 + /> 12 + </svg> 13 + )); 14 + 15 + export default RepeatOutlinedIcon;
+15
src/components/icons-central/reply-outline.tsx
··· 1 + import { createIcon } from './_icon'; 2 + 3 + // bubble-2 4 + const ReplyOutlinedIcon = createIcon(() => ( 5 + <svg width="1em" height="1em" fill="none" viewBox="0 0 24 24"> 6 + <path 7 + stroke="currentColor" 8 + stroke-linecap="square" 9 + stroke-width="2" 10 + d="M3.002 4h18v14h-9l-5 3v-3h-4V4Z" 11 + /> 12 + </svg> 13 + )); 14 + 15 + export default ReplyOutlinedIcon;
+13
src/components/icons-central/share-outline.tsx
··· 1 + import { createIcon } from './_icon'; 2 + 3 + const ShareOutlinedIcon = createIcon(() => ( 4 + <svg width="1em" height="1em" fill="none" viewBox="0 0 24 24"> 5 + <path 6 + stroke="currentColor" 7 + stroke-width="2" 8 + d="M14.335 7.38a3 3 0 1 0 5.328-2.76 3 3 0 0 0-5.328 2.76Zm0 0-5.67 3.24m0 0a3 3 0 1 0 0 2.76m0-2.76c.214.413.335.883.335 1.38 0 .498-.121.967-.335 1.38m0 0 5.67 3.24m0 0a3 3 0 1 0 5.33 2.76 3 3 0 0 0-5.33-2.76Z" 9 + /> 10 + </svg> 11 + )); 12 + 13 + export default ShareOutlinedIcon;
+14
src/components/icons-central/shield-outline.tsx
··· 1 + import { createIcon } from './_icon'; 2 + 3 + const ShieldOutlinedIcon = createIcon(() => ( 4 + <svg width="1em" height="1em" fill="none" viewBox="0 0 24 24"> 5 + <path 6 + stroke="currentColor" 7 + stroke-linecap="square" 8 + stroke-width="2" 9 + d="M20 5.75 12 3 4 5.75v6.162c0 4.973 4 7.088 8 9.246 4-2.158 8-4.273 8-9.246V5.75Z" 10 + /> 11 + </svg> 12 + )); 13 + 14 + export default ShieldOutlinedIcon;
+13
src/components/icons-central/write-outline.tsx
··· 1 + import { createIcon } from './_icon'; 2 + 3 + // edit-big 4 + const WriteOutlinedIcon = createIcon(() => ( 5 + <svg width="1em" height="1em" fill="none" viewBox="0 0 24 24"> 6 + <path 7 + fill="currentColor" 8 + d="M20 20v1h1v-1h-1ZM4 20H3v1h1v-1ZM4 4V3H3v1h1Zm7 1h1V3h-1v2Zm10 8v-1h-2v1h2ZM9 12l-.707-.707-.293.293V12h1Zm0 3H8v1h1v-1Zm3 0v1h.414l.293-.293L12 15Zm6-12 .707-.707L18 1.586l-.707.707L18 3Zm3 3 .707.707.707-.707-.707-.707L21 6Zm-1 13H4v2h16v-2ZM5 20V4H3v16h2ZM4 5h7V3H4v2Zm15 8v7h2v-7h-2ZM8 12v3h2v-3H8Zm1 4h3v-2H9v2Zm.707-3.293 9-9-1.414-1.414-9 9 1.414 1.414Zm7.586-9 3 3 1.414-1.414-3-3-1.414 1.414Zm3 1.586-9 9 1.414 1.414 9-9-1.414-1.414Z" 9 + /> 10 + </svg> 11 + )); 12 + 13 + export default WriteOutlinedIcon;
+38
src/components/inline-link.tsx
··· 1 + import type { ParentProps } from 'solid-js'; 2 + import { useFieldset } from './fieldset'; 3 + 4 + export interface InlineLinkProps extends ParentProps { 5 + href?: string; 6 + disabled?: boolean; 7 + onClick?: () => void; 8 + } 9 + 10 + const InlineLink = (props: InlineLinkProps) => { 11 + const fieldset = useFieldset(); 12 + const isDisabled = (): boolean => fieldset.disabled || !!props.disabled; 13 + 14 + return ( 15 + <button 16 + type="button" 17 + disabled={isDisabled()} 18 + onClick={props.onClick} 19 + class={inlineLinkClassNames(isDisabled)} 20 + > 21 + {props.children} 22 + </button> 23 + ); 24 + }; 25 + 26 + const inlineLinkClassNames = (isDisabled: () => boolean): string => { 27 + var cn = `text-left text-de hover:underline`; 28 + 29 + if (!isDisabled()) { 30 + cn += ` text-c-primary-400`; 31 + } else { 32 + cn += ` text-c-primary-700`; 33 + } 34 + 35 + return cn; 36 + }; 37 + 38 + export default InlineLink;
+114
src/components/list.tsx
··· 1 + import { For, Match, Switch, untrack, type JSX } from 'solid-js'; 2 + 3 + import { getQueryErrorInfo } from '~/api/utils/query'; 4 + 5 + import { ifIntersect } from '~/lib/element-refs'; 6 + 7 + import CircularProgress from './circular-progress'; 8 + import ErrorView from './error-view'; 9 + 10 + export interface ListProps<T> { 11 + data?: T[]; 12 + error?: unknown; 13 + render: (item: T, index: () => number) => JSX.Element; 14 + fallback?: JSX.Element; 15 + manualScroll?: boolean; 16 + hasNewData?: boolean; 17 + hasNextPage?: boolean; 18 + isRefreshing?: boolean; 19 + isFetchingNextPage?: boolean; 20 + onEndReached?: () => void; 21 + onRefresh?: () => void; 22 + } 23 + 24 + const List = <T,>(props: ListProps<T>) => { 25 + const render = props.render; 26 + 27 + const onEndReached = props.onEndReached; 28 + const onRefresh = props.onRefresh; 29 + 30 + return ( 31 + <div class="flex flex-col"> 32 + <Switch> 33 + <Match when={props.isRefreshing}> 34 + <div class="grid h-13 shrink-0 place-items-center border-b border-c-contrast-200"> 35 + <CircularProgress /> 36 + </div> 37 + </Match> 38 + 39 + <Match when={props.hasNewData}> 40 + <button 41 + onClick={onRefresh} 42 + class="grid h-13 shrink-0 place-items-center border-b border-c-contrast-200 text-sm text-c-primary-400 hover:bg-c-contrast-25" 43 + > 44 + Show new items 45 + </button> 46 + </Match> 47 + </Switch> 48 + 49 + <For 50 + each={props.data} 51 + fallback={ 52 + (() => { 53 + if (props.manualScroll || !props.hasNextPage) { 54 + return untrack(() => props.fallback); 55 + } 56 + }) as unknown as JSX.Element 57 + } 58 + > 59 + {render} 60 + </For> 61 + 62 + <Switch> 63 + <Match when={props.isRefreshing}>{null}</Match> 64 + 65 + <Match when={props.error}> 66 + {(err) => ( 67 + <ErrorView 68 + error={err()} 69 + onRetry={() => { 70 + const info = getQueryErrorInfo(err()); 71 + 72 + if (info && info.pageParam === undefined) { 73 + onRefresh?.(); 74 + } else { 75 + onEndReached?.(); 76 + } 77 + }} 78 + /> 79 + )} 80 + </Match> 81 + 82 + <Match when={props.manualScroll && !props.isFetchingNextPage && props.hasNextPage}> 83 + <button 84 + onClick={onEndReached} 85 + class="grid h-13 shrink-0 place-items-center text-sm text-c-primary-400 hover:bg-c-contrast-25" 86 + > 87 + Show more 88 + </button> 89 + </Match> 90 + 91 + <Match when={props.isFetchingNextPage || props.hasNextPage}> 92 + <div 93 + ref={(node) => { 94 + if (onEndReached) { 95 + ifIntersect(node, () => !props.isFetchingNextPage && props.hasNextPage, onEndReached); 96 + } 97 + }} 98 + class="grid h-13 shrink-0 place-items-center" 99 + > 100 + <CircularProgress /> 101 + </div> 102 + </Match> 103 + 104 + <Match when={props.data}> 105 + <div class="grid h-13 shrink-0 place-items-center"> 106 + <p class="text-sm text-c-contrast-400">End of list</p> 107 + </div> 108 + </Match> 109 + </Switch> 110 + </div> 111 + ); 112 + }; 113 + 114 + export default List;
+165
src/components/main/main-sidebar-authenticated.tsx
··· 1 + import { For, Match, Switch, createMemo } from 'solid-js'; 2 + 3 + import { useProfileQuery } from '~/api/queries/profile'; 4 + 5 + import { openModal, useModalContext } from '~/globals/modals'; 6 + 7 + import { useSession } from '~/lib/states/session'; 8 + 9 + import Avatar from '../avatar'; 10 + import IconButton from '../icon-button'; 11 + import AddOutlinedIcon from '../icons-central/add-outline'; 12 + import BulletListOutlinedIcon from '../icons-central/bullet-list-outline'; 13 + import GearOutlinedIcon from '../icons-central/gear-outline'; 14 + import LeaveOutlinedIcon from '../icons-central/leave-outline'; 15 + import MoreHorizOutlinedIcon from '../icons-central/more-horiz-outline'; 16 + import PersonOutlinedIcon from '../icons-central/person-outline'; 17 + import ShieldOutlinedIcon from '../icons-central/shield-outline'; 18 + import * as Sidebar from '../sidebar'; 19 + 20 + import ManageAccountDialogLazy from './manage-account-dialog-lazy'; 21 + 22 + const MainSidebarAuthenticated = () => { 23 + const { close } = useModalContext(); 24 + 25 + return ( 26 + <> 27 + <Sidebar.Backdrop /> 28 + <Sidebar.Container> 29 + <AuthenticatedHeader /> 30 + <Sidebar.Item 31 + icon={PersonOutlinedIcon} 32 + label="Profile" 33 + onClick={() => { 34 + close(); 35 + }} 36 + /> 37 + <Sidebar.Item 38 + icon={BulletListOutlinedIcon} 39 + label="Lists" 40 + onClick={() => { 41 + close(); 42 + }} 43 + /> 44 + <Sidebar.Item 45 + icon={ShieldOutlinedIcon} 46 + label="Moderation" 47 + onClick={() => { 48 + close(); 49 + }} 50 + /> 51 + <Sidebar.Item 52 + icon={GearOutlinedIcon} 53 + label="Settings" 54 + onClick={() => { 55 + close(); 56 + }} 57 + /> 58 + <Sidebar.Item 59 + icon={LeaveOutlinedIcon} 60 + label="Sign out" 61 + onClick={() => { 62 + close(); 63 + }} 64 + /> 65 + </Sidebar.Container> 66 + </> 67 + ); 68 + }; 69 + 70 + export default MainSidebarAuthenticated; 71 + 72 + const AuthenticatedHeader = () => { 73 + const { close } = useModalContext(); 74 + 75 + const { currentAccount, getAccounts, resumeSession } = useSession(); 76 + const query = useProfileQuery(() => currentAccount!.did); 77 + 78 + const otherAccounts = createMemo(() => { 79 + return getAccounts() 80 + .filter((acc) => acc.did !== currentAccount!.did) 81 + .slice(0, 2); 82 + }); 83 + 84 + return ( 85 + <Switch> 86 + <Match when={query.data}> 87 + {(profile) => { 88 + return ( 89 + <div class="flex flex-col p-4"> 90 + <div class="flex justify-between"> 91 + <Avatar type="user" src={profile().avatar} size="lg" /> 92 + 93 + {otherAccounts().length === 0 ? ( 94 + <IconButton 95 + icon={AddOutlinedIcon} 96 + title="Manage accounts" 97 + size="sm" 98 + onClick={() => { 99 + close(); 100 + openModal(() => <ManageAccountDialogLazy />); 101 + }} 102 + /> 103 + ) : ( 104 + <div class="flex gap-2"> 105 + <For each={otherAccounts()}> 106 + {(account) => { 107 + const profile = useProfileQuery(() => account.did); 108 + const handleClick = () => { 109 + resumeSession(account); 110 + close(); 111 + }; 112 + 113 + return ( 114 + <IconButton 115 + icon={() => { 116 + return <Avatar type="user" src={profile.data?.avatar} size="sm" />; 117 + }} 118 + title={`@${profile.data?.handle ?? account.session.handle}`} 119 + size="sm" 120 + onClick={handleClick} 121 + /> 122 + ); 123 + }} 124 + </For> 125 + 126 + <IconButton 127 + icon={MoreHorizOutlinedIcon} 128 + title="Manage accounts" 129 + size="sm" 130 + onClick={() => { 131 + close(); 132 + openModal(() => <ManageAccountDialogLazy />); 133 + }} 134 + /> 135 + </div> 136 + )} 137 + </div> 138 + 139 + <div class="mt-2 flex flex-col"> 140 + <p class="overflow-hidden break-words text-lg font-bold empty:hidden"> 141 + {profile().displayName} 142 + </p> 143 + <p class="overflow-hidden break-words text-sm text-c-contrast-600"> 144 + {'@' + profile().handle} 145 + </p> 146 + </div> 147 + 148 + <div class="mt-3 flex min-w-0 flex-wrap gap-5 text-sm"> 149 + <a onClick={close}> 150 + <span class="font-bold">{profile().followsCount ?? 0}</span> 151 + <span class="text-c-contrast-600"> Following</span> 152 + </a> 153 + 154 + <a onClick={close}> 155 + <span class="font-bold">{profile().followersCount ?? 0}</span> 156 + <span class="text-c-contrast-600"> Followers</span> 157 + </a> 158 + </div> 159 + </div> 160 + ); 161 + }} 162 + </Match> 163 + </Switch> 164 + ); 165 + };
+12
src/components/main/main-sidebar-public.tsx
··· 1 + import * as Sidebar from '../sidebar'; 2 + 3 + const MainSidebarPublic = () => { 4 + return ( 5 + <> 6 + <Sidebar.Backdrop /> 7 + <Sidebar.Container></Sidebar.Container> 8 + </> 9 + ); 10 + }; 11 + 12 + export default MainSidebarPublic;
+18
src/components/main/main-sidebar.tsx
··· 1 + import { lazy } from 'solid-js'; 2 + 3 + import { useSession } from '~/lib/states/session'; 4 + 5 + const MainSidebarAuthenticatedLazy = lazy(() => import('./main-sidebar-authenticated')); 6 + const MainSidebarPublicLazy = lazy(() => import('./main-sidebar-public')); 7 + 8 + const MainSidebar = () => { 9 + const { currentAccount } = useSession(); 10 + 11 + if (currentAccount) { 12 + return <MainSidebarAuthenticatedLazy />; 13 + } else { 14 + return <MainSidebarPublicLazy />; 15 + } 16 + }; 17 + 18 + export default MainSidebar;
+5
src/components/main/manage-account-dialog-lazy.tsx
··· 1 + import { lazy } from 'solid-js'; 2 + 3 + const ManageAccountDialogLazy = lazy(() => import('./manage-account-dialog')); 4 + 5 + export default ManageAccountDialogLazy;
+109
src/components/main/manage-account-dialog.tsx
··· 1 + import { For } from 'solid-js'; 2 + 3 + import { useProfileQuery } from '~/api/queries/profile'; 4 + 5 + import { closeAllModals, openModal } from '~/globals/modals'; 6 + 7 + import type { AccountData } from '~/lib/preferences/sessions'; 8 + import { useSession } from '~/lib/states/session'; 9 + 10 + import Avatar from '../avatar'; 11 + import * as Dialog from '../dialog'; 12 + import Divider from '../divider'; 13 + import CircleCheckSolidIcon from '../icons-central/circle-check-solid'; 14 + 15 + import SignInDialogLazy from './sign-in-dialog-lazy'; 16 + 17 + const ManageAccountDialog = () => { 18 + const { currentAccount, getAccounts, resumeSession } = useSession(); 19 + 20 + const switchAccount = async (account: AccountData) => { 21 + resumeSession(account); 22 + closeAllModals(); 23 + }; 24 + 25 + return ( 26 + <> 27 + <Dialog.Backdrop /> 28 + <Dialog.Container maxWidth="sm" fullHeight> 29 + <Dialog.Header> 30 + <Dialog.HeaderAccessory> 31 + <Dialog.Close /> 32 + </Dialog.HeaderAccessory> 33 + 34 + <Dialog.Heading title="Manage accounts" /> 35 + </Dialog.Header> 36 + <Dialog.Body unpadded> 37 + <div class="flex flex-col"> 38 + <CurrentAccountItem /> 39 + <For each={getAccounts().filter((account) => account.did !== currentAccount!.did)}> 40 + {(account) => <AccountItem account={account} onClick={() => switchAccount(account)} />} 41 + </For> 42 + </div> 43 + 44 + <Divider gutterTop="md" /> 45 + 46 + <div class="flex flex-col"> 47 + <button 48 + onClick={() => { 49 + openModal(() => <SignInDialogLazy />); 50 + }} 51 + class="px-4 py-3 text-left text-sm text-c-primary-400 hover:bg-c-primary-975" 52 + > 53 + Add new account 54 + </button> 55 + 56 + <button class="px-4 py-3 text-left text-sm text-c-negative-400 hover:bg-c-negative-975"> 57 + Sign out of all accounts 58 + </button> 59 + </div> 60 + </Dialog.Body> 61 + </Dialog.Container> 62 + </> 63 + ); 64 + }; 65 + 66 + export default ManageAccountDialog; 67 + 68 + const CurrentAccountItem = () => { 69 + const { currentAccount } = useSession(); 70 + const profile = useProfileQuery(() => currentAccount!.did); 71 + 72 + return ( 73 + <div class="flex gap-4 px-4 py-3"> 74 + <Avatar type="user" src={profile.data?.avatar} class="mt-0.5" /> 75 + 76 + <div class="min-w-0 grow self-center text-sm"> 77 + <p class="overflow-hidden text-ellipsis whitespace-nowrap font-bold empty:hidden"> 78 + {profile.data?.displayName} 79 + </p> 80 + <p class="overflow-hidden text-ellipsis whitespace-nowrap text-de text-c-contrast-600"> 81 + {'@' + (profile.data?.handle ?? currentAccount!.data.session.handle)} 82 + </p> 83 + </div> 84 + 85 + <div class="mt-2.5 shrink-0 text-lg text-c-positive-600"> 86 + <CircleCheckSolidIcon /> 87 + </div> 88 + </div> 89 + ); 90 + }; 91 + 92 + const AccountItem = ({ account, onClick }: { account: AccountData; onClick?: () => void }) => { 93 + const profile = useProfileQuery(() => account.did); 94 + 95 + return ( 96 + <button onClick={onClick} class="flex gap-4 px-4 py-3 text-left hover:bg-c-contrast-25"> 97 + <Avatar type="user" src={profile.data?.avatar} class="mt-0.5" /> 98 + 99 + <div class="min-w-0 grow self-center text-sm"> 100 + <p class="overflow-hidden text-ellipsis whitespace-nowrap font-bold empty:hidden"> 101 + {profile.data?.displayName} 102 + </p> 103 + <p class="overflow-hidden text-ellipsis whitespace-nowrap text-de text-c-contrast-600"> 104 + {'@' + (profile.data?.handle ?? account.session.handle)} 105 + </p> 106 + </div> 107 + </button> 108 + ); 109 + };
+82
src/components/main/modal-renderer.tsx
··· 1 + import { For, Suspense, onCleanup } from 'solid-js'; 2 + 3 + import { INTERNAL_ModalContext, INTERNAL_modals, closeModal, type ModalContext } from '~/globals/modals'; 4 + 5 + import * as Dialog from '../dialog'; 6 + import CircularProgress from '../circular-progress'; 7 + 8 + let isScrollbarSizeDetermined = false; 9 + 10 + const ModalRenderer = () => { 11 + return ( 12 + <For each={INTERNAL_modals()}> 13 + {({ id, render }) => { 14 + const context: ModalContext = { 15 + id: id, 16 + isActive(): boolean { 17 + const array = INTERNAL_modals(); 18 + const last = array[array.length - 1]; 19 + return last !== undefined && last.id === id; 20 + }, 21 + close(): void { 22 + return closeModal(id); 23 + }, 24 + }; 25 + 26 + // Restore focus 27 + { 28 + const focused = document.activeElement; 29 + if (focused !== null && focused !== document.body) { 30 + onCleanup(() => { 31 + queueMicrotask(() => { 32 + if (document.contains(focused)) { 33 + (focused as any).focus(); 34 + } 35 + }); 36 + }); 37 + } 38 + } 39 + 40 + // Determine scrollbar size 41 + if (!isScrollbarSizeDetermined) { 42 + determineScrollbarSize(); 43 + isScrollbarSizeDetermined = true; 44 + } 45 + 46 + return ( 47 + <INTERNAL_ModalContext.Provider value={context}> 48 + <div 49 + inert={!context.isActive()} 50 + class="fixed inset-0 flex flex-col items-center justify-start overflow-hidden" 51 + data-modal 52 + > 53 + <Suspense fallback={<FallbackLoader />}>{render(context)}</Suspense> 54 + </div> 55 + </INTERNAL_ModalContext.Provider> 56 + ); 57 + }} 58 + </For> 59 + ); 60 + }; 61 + 62 + export default ModalRenderer; 63 + 64 + const FallbackLoader = () => { 65 + return ( 66 + <> 67 + <Dialog.Backdrop /> 68 + <div class="grid grow place-items-center"> 69 + <CircularProgress /> 70 + </div> 71 + </> 72 + ); 73 + }; 74 + 75 + const determineScrollbarSize = () => { 76 + const docEl = document.documentElement; 77 + 78 + const documentWidth = docEl.clientWidth; 79 + const scrollbarSize = Math.abs(window.innerWidth - documentWidth); 80 + 81 + docEl.style.setProperty('--sb-width', `${scrollbarSize}px`); 82 + };
+5
src/components/main/sign-in-dialog-lazy.tsx
··· 1 + import { lazy } from 'solid-js'; 2 + 3 + const SignInDialogLazy = lazy(() => import('./sign-in-dialog')); 4 + 5 + export default SignInDialogLazy;
+444
src/components/main/sign-in-dialog.tsx
··· 1 + import { Match, Switch, batch, createSignal } from 'solid-js'; 2 + 3 + import { XRPCError } from '@mary/bluesky-client/xrpc'; 4 + import { createMutation } from '@mary/solid-query'; 5 + 6 + import { DEFAULT_DATA_SERVER } from '~/api/defaults'; 7 + import type { DataServer } from '~/api/types'; 8 + import { DidResolutionError, findDidDocument, getDataServer } from '~/api/utils/did-doc'; 9 + import { isDid } from '~/api/utils/strings'; 10 + 11 + import { closeAllModals } from '~/globals/modals'; 12 + 13 + import { autofocusIfEnabled, autofocusOnMutation, modelText } from '~/lib/input-refs'; 14 + import { useSession } from '~/lib/states/session'; 15 + 16 + import Button from '../button'; 17 + import * as Dialog from '../dialog'; 18 + import { Fieldset } from '../fieldset'; 19 + import InlineLink from '../inline-link'; 20 + import TextInput from '../text-input'; 21 + 22 + type View = 23 + | { type: 'handle_initial' } 24 + | { type: 'handle_password' } 25 + | { type: 'email_initial' } 26 + | { type: 'otp'; from: 'handle' | 'email' }; 27 + 28 + type TargetedError = { target: 'identifier' | 'password' | 'otp'; msg: string }; 29 + 30 + const SignInDialog = () => { 31 + const session = useSession(); 32 + 33 + const [view, setView] = createSignal<View>({ type: 'handle_initial' }); 34 + 35 + const [service, setService] = createSignal(DEFAULT_DATA_SERVER); 36 + const [identifier, setIdentifier] = createSignal(''); 37 + const [password, setPassword] = createSignal(''); 38 + const [otp, setOtp] = createSignal(''); 39 + 40 + const [error, setError] = createSignal<TargetedError>(); 41 + 42 + const pdsMutation = createMutation(() => { 43 + return { 44 + async mutationFn({ identifier }: { identifier: string }) { 45 + const didDoc = await findDidDocument(identifier); 46 + const service = getDataServer(didDoc); 47 + if (!service) { 48 + throw new Error(`PDS_NOT_FOUND`); 49 + } 50 + 51 + return { didDoc, service }; 52 + }, 53 + onSuccess({ service }) { 54 + setTimeout(() => { 55 + batch(() => { 56 + setView({ type: 'handle_password' }); 57 + setService(service); 58 + }); 59 + }, 0); 60 + }, 61 + onError(error, { identifier }) { 62 + let msg = `Unknown error, try again later`; 63 + 64 + if (error instanceof DidResolutionError) { 65 + const type = error.message; 66 + 67 + if (type === 'DID_UNSUPPORTED') { 68 + if (isDid(identifier)) { 69 + msg = `Unsupported DID method`; 70 + } else { 71 + msg = `Account uses an unsupported DID method`; 72 + } 73 + } else if (type === 'PLC_NOT_FOUND') { 74 + msg = `DID not found in PLC directory`; 75 + } else if (type === 'PLC_UNREACHABLE') { 76 + msg = `Can't reach PLC directory right now, try again later`; 77 + } else if (type === 'WEB_INVALID') { 78 + msg = `Specified did:web is invalid`; 79 + } else if (type === 'WEB_NOT_FOUND') { 80 + msg = `Can't find your account, did you type it correctly?`; 81 + } else if (type === 'WEB_UNREACHABLE') { 82 + msg = `Can't reach your DID document right now, try again later`; 83 + } 84 + } else if (error instanceof XRPCError) { 85 + const err = error.kind; 86 + 87 + if (error.message === 'Unable to resolve handle') { 88 + msg = `Can't find your account, did you type it correctly?`; 89 + } else if (err === 'InvalidRequest') { 90 + msg = `That doesn't seem right, did you type it correctly?`; 91 + } 92 + } else if (error instanceof Error) { 93 + if (error.message === 'PDS_NOT_FOUND') { 94 + msg = `Account is not attached to a hosting provider`; 95 + } 96 + } 97 + 98 + setError({ target: 'identifier', msg }); 99 + }, 100 + }; 101 + }); 102 + 103 + const loginMutation = createMutation(() => ({ 104 + async mutationFn({ 105 + service, 106 + identifier, 107 + password, 108 + authFactorToken, 109 + }: { 110 + from: 'handle' | 'email'; 111 + service: DataServer; 112 + identifier: string; 113 + password: string; 114 + authFactorToken: string | undefined; 115 + }) { 116 + await session.login({ 117 + service: service.uri, 118 + identifier: identifier, 119 + password: password, 120 + authFactorToken: authFactorToken, 121 + }); 122 + }, 123 + onSuccess() { 124 + closeAllModals(); 125 + }, 126 + onError(error: unknown, { from }) { 127 + let msg = `Unknown error, try again later`; 128 + 129 + if (error instanceof XRPCError) { 130 + const err = error.kind; 131 + 132 + if (err === 'AuthFactorTokenRequired') { 133 + setView({ type: 'otp', from: from }); 134 + return; 135 + } else if (err === 'AuthenticationRequired') { 136 + msg = `Invalid password`; 137 + } else if (err === 'AccountTakedown') { 138 + msg = `Your account has been taken down`; 139 + } 140 + } else if (error instanceof DOMException) { 141 + if (error.name === 'AbortError') { 142 + msg = `Login attempt aborted, try again`; 143 + } 144 + } 145 + 146 + setError({ target: 'password', msg }); 147 + }, 148 + })); 149 + 150 + return ( 151 + <> 152 + <Dialog.Backdrop /> 153 + <Dialog.Container maxWidth="sm" centered disabled={loginMutation.isPending}> 154 + <form 155 + class="contents" 156 + onsubmit={(ev) => { 157 + const $view = view(); 158 + 159 + ev.preventDefault(); 160 + 161 + batch(() => { 162 + setError(undefined); 163 + 164 + if ($view.type === 'handle_initial') { 165 + pdsMutation.mutate({ identifier: identifier() }); 166 + } else if ($view.type === 'handle_password') { 167 + loginMutation.mutate({ 168 + from: 'handle', 169 + service: service(), 170 + identifier: identifier(), 171 + password: password(), 172 + authFactorToken: undefined, 173 + }); 174 + } else if ($view.type === 'email_initial') { 175 + loginMutation.mutate({ 176 + from: 'email', 177 + service: service(), 178 + identifier: identifier(), 179 + password: password(), 180 + authFactorToken: undefined, 181 + }); 182 + } else if ($view.type === 'otp') { 183 + loginMutation.mutate({ 184 + from: $view.from, 185 + service: service(), 186 + identifier: identifier(), 187 + password: password(), 188 + authFactorToken: formatEmailOtpCode(otp()), 189 + }); 190 + } 191 + }); 192 + }} 193 + > 194 + <Dialog.Header> 195 + <Dialog.HeaderAccessory> 196 + <Dialog.Close /> 197 + </Dialog.HeaderAccessory> 198 + </Dialog.Header> 199 + 200 + <Fieldset disabled={pdsMutation.isPending}> 201 + <Dialog.Body class="flex flex-col gap-6"> 202 + <Switch> 203 + <Match when={view().type === 'handle_initial'}> 204 + <div class="flex flex-col gap-1"> 205 + <h2 class="text-2xl font-bold">Sign in</h2> 206 + <h3 class="text-base text-c-contrast-800">To begin, enter your Bluesky handle or DID</h3> 207 + </div> 208 + 209 + <div class="flex flex-col gap-4"> 210 + <TextInput 211 + ref={(node) => { 212 + autofocusOnMutation(node, pdsMutation); 213 + modelText(node, identifier, setIdentifier); 214 + }} 215 + autocomplete="username" 216 + pattern="([a-zA-Z0-9\-]+(?:\.[a-zA-Z0-9\-]+)*(?:\.[a-zA-Z]+))|did:[a-z]+:[a-zA-Z0-9._\-]+" 217 + required 218 + label="Bluesky handle or DID" 219 + placeholder="paul.bsky.social" 220 + error={(() => { 221 + const $error = error(); 222 + if ($error && $error.target === 'identifier') { 223 + return $error.msg; 224 + } 225 + })()} 226 + /> 227 + 228 + <input 229 + ref={(node) => { 230 + modelText(node, password, setPassword); 231 + }} 232 + type="password" 233 + autocomplete="current-password" 234 + hidden 235 + /> 236 + 237 + <div class="flex flex-col gap-2"> 238 + <InlineLink 239 + onClick={() => { 240 + batch(() => { 241 + setView({ type: 'email_initial' }); 242 + 243 + setError(); 244 + 245 + setService(DEFAULT_DATA_SERVER); 246 + setIdentifier(''); 247 + setPassword(''); 248 + }); 249 + }} 250 + > 251 + Sign in with email address instead 252 + </InlineLink> 253 + </div> 254 + </div> 255 + </Match> 256 + 257 + <Match when={view().type === 'handle_password'}> 258 + <div class="flex flex-col gap-1"> 259 + <h2 class="text-2xl font-bold">Enter your password</h2> 260 + </div> 261 + 262 + <div class="flex flex-col gap-4"> 263 + <TextInput 264 + disabled 265 + autocomplete="username" 266 + label="Bluesky handle or DID" 267 + value={identifier()} 268 + /> 269 + 270 + <TextInput 271 + ref={(node) => { 272 + autofocusOnMutation(node, loginMutation); 273 + modelText(node, password, setPassword); 274 + }} 275 + type="password" 276 + autocomplete="current-password" 277 + required 278 + label="Password" 279 + error={(() => { 280 + const $error = error(); 281 + if ($error && $error.target === 'password') { 282 + return $error.msg; 283 + } 284 + })()} 285 + /> 286 + 287 + <div class="flex flex-col gap-2"> 288 + <InlineLink 289 + onClick={() => { 290 + batch(() => { 291 + setView({ type: 'handle_initial' }); 292 + 293 + setError(); 294 + setPassword(''); 295 + }); 296 + }} 297 + > 298 + Sign in with another account 299 + </InlineLink> 300 + </div> 301 + </div> 302 + </Match> 303 + 304 + <Match when={view().type === 'email_initial'}> 305 + <div class="flex flex-col gap-1"> 306 + <h2 class="text-2xl font-bold">Sign in</h2> 307 + </div> 308 + 309 + <div class="flex flex-col gap-4"> 310 + <TextInput 311 + ref={(node) => { 312 + autofocusIfEnabled(node, () => true); 313 + modelText(node, identifier, setIdentifier); 314 + }} 315 + type="email" 316 + required 317 + label="Email address" 318 + placeholder="emma@contoso.com" 319 + error={(() => { 320 + const $error = error(); 321 + if ($error && $error.target === 'identifier') { 322 + return $error.msg; 323 + } 324 + })()} 325 + /> 326 + 327 + <TextInput 328 + ref={(node) => { 329 + autofocusOnMutation(node, loginMutation, false); 330 + modelText(node, password, setPassword); 331 + }} 332 + type="password" 333 + autocomplete="current-password" 334 + required 335 + label="Password" 336 + error={(() => { 337 + const $error = error(); 338 + if ($error && $error.target === 'password') { 339 + return $error.msg; 340 + } 341 + })()} 342 + /> 343 + 344 + <div class="flex flex-col gap-2"> 345 + <InlineLink 346 + onClick={() => { 347 + batch(() => { 348 + setView({ type: 'handle_initial' }); 349 + 350 + setError(); 351 + setIdentifier(''); 352 + setPassword(''); 353 + }); 354 + }} 355 + > 356 + Sign in with Bluesky handle instead 357 + </InlineLink> 358 + </div> 359 + </div> 360 + </Match> 361 + 362 + <Match 363 + when={(() => { 364 + const $view = view(); 365 + if ($view.type === 'otp') { 366 + return $view; 367 + } 368 + })()} 369 + keyed 370 + > 371 + {({ from }) => ( 372 + <> 373 + <div class="flex flex-col gap-1"> 374 + <h2 class="text-2xl font-bold">Enter verification code</h2> 375 + <h3 class="max-w-84 text-base text-c-contrast-800"> 376 + Check your inbox for an email containing the code and enter it here 377 + </h3> 378 + </div> 379 + 380 + <div class="flex flex-col gap-4"> 381 + <TextInput 382 + ref={(node) => { 383 + autofocusOnMutation(node, loginMutation); 384 + modelText(node, otp, setOtp); 385 + }} 386 + autocomplete="one-time-code" 387 + required 388 + label="Verification code" 389 + placeholder="AAAAA-BBBBB" 390 + error={(() => { 391 + const $error = error(); 392 + if ($error && $error.target === 'otp') { 393 + return $error.msg; 394 + } 395 + })()} 396 + /> 397 + 398 + <InlineLink 399 + onClick={() => { 400 + batch(() => { 401 + setView({ type: `${from}_initial` }); 402 + 403 + setError(); 404 + setOtp(''); 405 + }); 406 + }} 407 + > 408 + Sign in with another account 409 + </InlineLink> 410 + </div> 411 + </> 412 + )} 413 + </Match> 414 + </Switch> 415 + </Dialog.Body> 416 + 417 + <Dialog.Actions> 418 + <Button type="submit" variant="primary" size="md"> 419 + {(() => { 420 + const $view = view(); 421 + if ($view.type === 'handle_initial' || $view.type === 'otp') { 422 + return `Continue`; 423 + } 424 + 425 + return `Sign in`; 426 + })()} 427 + </Button> 428 + </Dialog.Actions> 429 + </Fieldset> 430 + </form> 431 + </Dialog.Container> 432 + </> 433 + ); 434 + }; 435 + 436 + export default SignInDialog; 437 + 438 + const formatEmailOtpCode = (code: string): string | undefined => { 439 + if (code.length === 0) { 440 + return undefined; 441 + } 442 + 443 + return (code.includes('-') ? code : code.slice(0, 5) + '-' + code.slice(5)).toUpperCase(); 444 + };
+139
src/components/menu.tsx
··· 1 + import { useFloating } from 'solid-floating-ui'; 2 + import { createSignal, type Component, type JSX } from 'solid-js'; 3 + 4 + import { flip, shift, size } from '@floating-ui/dom'; 5 + import { getSide, type Placement } from '@floating-ui/utils'; 6 + 7 + import { useModalContext } from '~/globals/modals'; 8 + 9 + import { useMediaQuery } from '~/lib/hooks/media-query'; 10 + import { useModalClose } from '~/lib/hooks/modal-close'; 11 + import { on } from '~/lib/misc'; 12 + import { useTheme } from '~/lib/states/theme'; 13 + 14 + import Button from './button'; 15 + 16 + export interface MenuContainerProps { 17 + anchor: HTMLElement; 18 + placement?: Placement; 19 + children: JSX.Element; 20 + } 21 + 22 + const MenuContainer = (props: MenuContainerProps) => { 23 + const { close, isActive } = useModalContext(); 24 + const isDesktop = useMediaQuery('(width >= 688px) and (height >= 500px)'); 25 + 26 + const containerRef = (node: HTMLElement): void => { 27 + useModalClose(node, close, isActive); 28 + }; 29 + 30 + return on(isDesktop, ($isDesktop) => { 31 + if ($isDesktop) { 32 + const [floating, setFloating] = createSignal<HTMLElement>(); 33 + const position = useFloating(() => props.anchor, floating, { 34 + placement: props.placement ?? 'bottom-end', 35 + strategy: 'absolute', 36 + middleware: [ 37 + { 38 + name: 'offset', 39 + fn(state) { 40 + const reference = state.rects.reference; 41 + const x = state.x; 42 + const y = state.y; 43 + 44 + const multi = getSide(state.placement) === 'bottom' ? 1 : -1; 45 + 46 + return { 47 + x: x, 48 + y: y - reference.height * multi, 49 + }; 50 + }, 51 + }, 52 + flip({ 53 + padding: 16, 54 + crossAxis: false, 55 + }), 56 + shift({ 57 + padding: 16, 58 + }), 59 + size({ 60 + padding: 16, 61 + apply({ availableWidth, availableHeight, elements }) { 62 + Object.assign(elements.floating.style, { 63 + maxWidth: `${availableWidth}px`, 64 + maxHeight: `${availableHeight}px`, 65 + }); 66 + }, 67 + }), 68 + ], 69 + }); 70 + 71 + const ref = (node: HTMLElement) => { 72 + setFloating(node); 73 + containerRef(node); 74 + }; 75 + 76 + return ( 77 + <div 78 + ref={ref} 79 + role="menu" 80 + style={{ top: `${position.y ?? 0}px`, left: `${position.x ?? 0}px` }} 81 + class="absolute flex max-w-sm flex-col overflow-hidden overflow-y-auto rounded-md border border-c-contrast-200 bg-c-contrast-0" 82 + > 83 + {props.children} 84 + </div> 85 + ); 86 + } else { 87 + const theme = useTheme(); 88 + 89 + return ( 90 + <div 91 + class={ 92 + `flex grow flex-col self-stretch overflow-y-auto` + 93 + (theme.currentTheme === 'light' ? ` bg-t-black/40` : ` bg-t-blue-low/40`) 94 + } 95 + > 96 + <div class="h-[50vh] shrink-0"></div> 97 + <div ref={containerRef} role="menu" class="mt-auto flex flex-col bg-c-black"> 98 + <div class="flex flex-col pt-1">{props.children}</div> 99 + 100 + <div class="flex flex-col px-4 pb-4 pt-3"> 101 + <Button onClick={close} size="md"> 102 + Cancel 103 + </Button> 104 + </div> 105 + </div> 106 + </div> 107 + ); 108 + } 109 + }) as unknown as JSX.Element; 110 + }; 111 + 112 + export { MenuContainer as Container }; 113 + 114 + export interface MenuItemProps { 115 + icon: Component; 116 + label: string; 117 + onClick?: () => void; 118 + } 119 + 120 + const MenuItem = (props: MenuItemProps) => { 121 + return ( 122 + <button 123 + role="menuitem" 124 + onClick={props.onClick} 125 + class="flex gap-3 px-4 py-3 text-c-contrast-900 hover:bg-c-contrast-25" 126 + > 127 + <div class="mt-0.5 text-lg"> 128 + {(() => { 129 + const Icon = props.icon; 130 + return <Icon />; 131 + })()} 132 + </div> 133 + 134 + <span class="text-sm font-bold">{props.label}</span> 135 + </button> 136 + ); 137 + }; 138 + 139 + export { MenuItem as Item };
+113
src/components/moderation/content-hider.tsx
··· 1 + import { createSignal, type Component, type JSX, type ParentProps } from 'solid-js'; 2 + 3 + import { 4 + CauseLabel, 5 + CauseMutedKeyword, 6 + CauseMutedPermanent, 7 + CauseMutedTemporary, 8 + SeverityAlert, 9 + getLocalizedLabel, 10 + type ModerationCause, 11 + type ModerationCauseType, 12 + type ModerationLabeler, 13 + type ModerationUI, 14 + } from '~/api/moderation'; 15 + 16 + import FilterOutlinedIcon from '../icons-central/filter-outline'; 17 + import InfoOutlinedIcon from '../icons-central/info-outline'; 18 + import PersonRemoveOutlinedIcon from '../icons-central/person-remove-outline'; 19 + import ProblemOutlinedIcon from '../icons-central/problem-outline'; 20 + 21 + export interface ContentHiderProps extends ParentProps { 22 + ui: ModerationUI | undefined; 23 + ignoreMute?: boolean; 24 + class?: string; 25 + childContainerClass?: string; 26 + } 27 + 28 + const ContentHider = (props: ContentHiderProps) => { 29 + return (() => { 30 + const ui = props.ui; 31 + const blur = ui?.b[0]; 32 + 33 + if (!blur || (props.ignoreMute && isOnlyMuted(ui.b))) { 34 + return <div class={`flex flex-col ` + props.class}>{props.children}</div>; 35 + } 36 + 37 + const [override, setOverride] = createSignal(false); 38 + 39 + const type = blur.t; 40 + 41 + let Icon: Component; 42 + let title: string; 43 + let forced: boolean | undefined; 44 + 45 + if (type === CauseLabel) { 46 + const def = blur.d; 47 + const severity = def.s; 48 + 49 + Icon = severity === SeverityAlert ? ProblemOutlinedIcon : InfoOutlinedIcon; 50 + title = getLocalizedLabel(def).n; 51 + forced = !ui.o; 52 + } else if (type === CauseMutedKeyword) { 53 + Icon = FilterOutlinedIcon; 54 + title = blur.n; 55 + } else if (type === CauseMutedTemporary) { 56 + Icon = PersonRemoveOutlinedIcon; 57 + title = `Silenced user`; 58 + } else { 59 + Icon = PersonRemoveOutlinedIcon; 60 + title = `Muted user`; 61 + } 62 + 63 + return ( 64 + <div class={`flex flex-col ` + props.class}> 65 + <button 66 + disabled={forced} 67 + onClick={() => setOverride((next) => !next)} 68 + class="flex h-11 w-full items-center gap-3 self-stretch rounded-md bg-c-contrast-25 px-3 text-c-contrast-900 hover:bg-c-contrast-25" 69 + > 70 + <div class="shrink-0 text-lg text-c-contrast-600"> 71 + <Icon /> 72 + </div> 73 + <span class="grow overflow-hidden text-ellipsis whitespace-nowrap text-left text-sm font-medium"> 74 + {title} 75 + </span> 76 + 77 + <span hidden={forced} class="text-de font-medium text-c-contrast-600"> 78 + {!override() ? `Show` : `Hide`} 79 + </span> 80 + </button> 81 + 82 + {(() => { 83 + if (type === CauseLabel && !override()) { 84 + return null; 85 + } 86 + })()} 87 + 88 + {(() => { 89 + if (override()) { 90 + return <div class={props.childContainerClass}>{props.children}</div>; 91 + } 92 + })()} 93 + </div> 94 + ); 95 + }) as unknown as JSX.Element; 96 + }; 97 + 98 + export default ContentHider; 99 + 100 + const renderLabelSource = (source: ModerationLabeler) => { 101 + const profile = source.profile; 102 + 103 + if (profile) { 104 + return profile.displayName || `@${profile.handle}`; 105 + } 106 + 107 + return source.did; 108 + }; 109 + 110 + const isOnlyMuted = (causes: ModerationCause[]) => { 111 + let t: ModerationCauseType; 112 + return causes.every((c) => (t = c.t) === CauseMutedTemporary || t === CauseMutedPermanent); 113 + };
+78
src/components/page.tsx
··· 1 + import type { ParentProps } from 'solid-js'; 2 + 3 + import { useProfileQuery } from '~/api/queries/profile'; 4 + 5 + import { openModal } from '~/globals/modals'; 6 + 7 + import { useAgent } from '~/lib/states/agent'; 8 + import { useSession } from '~/lib/states/session'; 9 + 10 + import Avatar from './avatar'; 11 + import IconButton from './icon-button'; 12 + import MenuOutlinedIcon from './icons-central/menu-outline'; 13 + import MainSidebarLazy from './main/main-sidebar'; 14 + 15 + export interface PageHeaderProps extends ParentProps {} 16 + 17 + const PageHeader = (props: PageHeaderProps) => { 18 + return ( 19 + <> 20 + <div class="sticky top-0 z-1 flex h-13 w-full max-w-md shrink-0 items-center justify-between gap-4 bg-c-contrast-0 px-2.5"> 21 + {props.children} 22 + </div> 23 + </> 24 + ); 25 + }; 26 + 27 + export { PageHeader as Header }; 28 + 29 + export interface PageHeadingProps { 30 + title?: string; 31 + subtitle?: string; 32 + } 33 + 34 + const PageHeading = (props: PageHeadingProps) => { 35 + return ( 36 + <div class="flex min-w-0 grow flex-col gap-0.5"> 37 + <p class="overflow-hidden text-ellipsis whitespace-nowrap text-base font-bold leading-5"> 38 + {props.title} 39 + </p> 40 + </div> 41 + ); 42 + }; 43 + 44 + export { PageHeading as Heading }; 45 + 46 + export interface PageHeaderAccessoryProps extends ParentProps {} 47 + 48 + const PageHeaderAccessory = (props: PageHeaderAccessoryProps) => { 49 + return <div class="flex shrink-0 gap-2 empty:hidden">{props.children}</div>; 50 + }; 51 + 52 + export { PageHeaderAccessory as HeaderAccessory }; 53 + 54 + export interface PageMainMenuProps {} 55 + 56 + const PageMainMenu = ({}: PageMainMenuProps) => { 57 + const { currentAccount } = useSession(); 58 + const { persister } = useAgent(); 59 + 60 + return ( 61 + <IconButton 62 + title="Open main menu" 63 + icon={() => { 64 + if (currentAccount) { 65 + const profile = useProfileQuery(() => currentAccount.did, persister); 66 + return <Avatar type="user" src={profile.data?.avatar} size="sm" />; 67 + } 68 + 69 + return <MenuOutlinedIcon />; 70 + }} 71 + onClick={() => { 72 + openModal(() => <MainSidebarLazy />); 73 + }} 74 + /> 75 + ); 76 + }; 77 + 78 + export { PageMainMenu as MainMenu };
+146
src/components/rich-text.tsx
··· 1 + import type { JSX } from 'solid-js'; 2 + 3 + import type { AppBskyRichtextFacet } from '@mary/bluesky-client/lexicons'; 4 + 5 + import { segmentRichText } from '~/api/richtext/segment'; 6 + 7 + import { handleLinkNavigation } from './button'; 8 + import { isLinkValid, safeUrlParse } from '~/api/utils/strings'; 9 + 10 + export interface RichTextProps { 11 + text: string; 12 + facets?: AppBskyRichtextFacet.Main[]; 13 + clipped?: boolean; 14 + } 15 + 16 + const EMOJI_RE = /^(\p{Emoji}\ufe0f|\p{Emoji_Presentation}){1,8}$/u; 17 + 18 + const RichText = (props: RichTextProps) => { 19 + return (() => { 20 + const text = props.text; 21 + const facets = props.facets; 22 + 23 + let nodes: JSX.Element; 24 + let large = false; 25 + 26 + if (facets !== undefined && facets.length !== 0) { 27 + const segments = segmentRichText(text, facets); 28 + 29 + nodes = []; 30 + 31 + for (let idx = 0, len = segments.length; idx < len; idx++) { 32 + const segment = segments[idx]; 33 + const subtext = segment.text; 34 + const feature = segment.feature; 35 + 36 + let to: string | undefined; 37 + let external = false; 38 + 39 + if (feature) { 40 + const type = feature.$type; 41 + 42 + if (type === 'app.bsky.richtext.facet#link') { 43 + const uri = feature.uri; 44 + const redirect = findLinkRedirect(uri); 45 + 46 + if (redirect === null) { 47 + to = uri; 48 + external = true; 49 + } else { 50 + to = redirect; 51 + } 52 + } else if (type === 'app.bsky.richtext.facet#mention') { 53 + to = `/${feature.did}`; 54 + } else if (type === 'app.bsky.richtext.facet#tag') { 55 + to = `/topics/${feature.tag}`; 56 + } 57 + } 58 + 59 + if (to !== undefined) { 60 + if (!external) { 61 + nodes.push( 62 + <a href={to} onClick={handleLinkNavigation} class="text-c-primary-400 hover:underline"> 63 + {subtext} 64 + </a>, 65 + ); 66 + } else { 67 + nodes.push( 68 + <a 69 + target="_blank" 70 + href={to} 71 + onClick={handleUnsafeLinkNavigation} 72 + onAuxClick={handleUnsafeLinkNavigation} 73 + class="text-c-primary-400 hover:underline" 74 + > 75 + {subtext} 76 + </a>, 77 + ); 78 + } 79 + } else { 80 + nodes.push(subtext); 81 + } 82 + } 83 + } else { 84 + nodes = text; 85 + large = EMOJI_RE.test(text); 86 + } 87 + 88 + return ( 89 + <p 90 + class={ 91 + `whitespace-pre-wrap break-words` + 92 + (!large ? ` text-sm` : ` text-lg`) + 93 + (props.clipped ? ` line-clamp-[12]` : ``) 94 + } 95 + > 96 + {nodes} 97 + </p> 98 + ); 99 + }) as unknown as JSX.Element; 100 + }; 101 + 102 + export default RichText; 103 + 104 + const handleUnsafeLinkNavigation = (ev: MouseEvent) => { 105 + if (ev.defaultPrevented || (ev.type === 'auxclick' && (ev as MouseEvent).button !== 1)) { 106 + return; 107 + } 108 + 109 + const anchor = ev.currentTarget as HTMLAnchorElement; 110 + const href = anchor.href; 111 + 112 + if (isLinkValid(href, anchor.textContent ?? '')) { 113 + } 114 + }; 115 + 116 + const findLinkRedirect = (uri: string): string | null => { 117 + const url = safeUrlParse(uri); 118 + 119 + if (url === null) { 120 + return null; 121 + } 122 + 123 + const host = url.host; 124 + const pathname = url.pathname; 125 + let match: RegExpExecArray | null | undefined; 126 + 127 + if (host === 'bsky.app') { 128 + if ((match = /^\/profile\/(?=.+[:.])([^/]+)\/?$/.exec(pathname))) { 129 + return `/${match[1]}`; 130 + } 131 + 132 + if ((match = /^\/profile\/(?=.+[:.])([^/]+)\/post\/([^/]{13})\/?$/.exec(pathname))) { 133 + return `/${match[1]}/${match[2]}`; 134 + } 135 + 136 + if ((match = /^\/profile\/(?=.+[:.])([^/]+)\/lists\/([^/]+)\/?$/.exec(pathname))) { 137 + return `/${match[1]}/lists/${match[2]}`; 138 + } 139 + 140 + if ((match = /^\/profile\/(?=.+[:.])([^/]+)\/feed\/([^/]+)\/?$/.exec(pathname))) { 141 + return `/${match[1]}/feeds/${match[2]}`; 142 + } 143 + } 144 + 145 + return null; 146 + };
+56
src/components/sidebar.tsx
··· 1 + import type { Component, ParentProps } from 'solid-js'; 2 + 3 + import { useModalContext } from '~/globals/modals'; 4 + 5 + import { useModalClose } from '~/lib/hooks/modal-close'; 6 + 7 + export { Backdrop } from './dialog'; 8 + 9 + export interface SidebarContainerProps extends ParentProps {} 10 + 11 + const SidebarContainer = (props: SidebarContainerProps) => { 12 + const { close, isActive } = useModalContext(); 13 + 14 + const containerRef = (node: HTMLElement) => { 15 + useModalClose(node, close, isActive); 16 + }; 17 + 18 + return ( 19 + <div 20 + ref={containerRef} 21 + role="menu" 22 + class="z-1 flex w-72 grow flex-col self-start overflow-auto bg-c-contrast-0" 23 + > 24 + {props.children} 25 + </div> 26 + ); 27 + }; 28 + 29 + export { SidebarContainer as Container }; 30 + 31 + export interface SidebarItemProps { 32 + icon: Component; 33 + label: string; 34 + onClick?: () => void; 35 + } 36 + 37 + const SidebarItem = (props: SidebarItemProps) => { 38 + return ( 39 + <button 40 + role="menuitem" 41 + onClick={props.onClick} 42 + class="flex gap-4 px-4 py-3 text-c-contrast-900 hover:bg-c-contrast-25" 43 + > 44 + <div class="mt-0.5 text-xl"> 45 + {(() => { 46 + const Icon = props.icon; 47 + return <Icon />; 48 + })()} 49 + </div> 50 + 51 + <span class="text-base font-bold">{props.label}</span> 52 + </button> 53 + ); 54 + }; 55 + 56 + export { SidebarItem as Item };
+76
src/components/text-input.tsx
··· 1 + import { createId } from '~/lib/hooks/id'; 2 + 3 + import { useFieldset } from './fieldset'; 4 + 5 + export interface TextInputProps { 6 + ref?: (node: HTMLInputElement) => void; 7 + label?: string; 8 + type?: 'text' | 'email' | 'password' | 'search' | 'tel' | 'url'; 9 + autocomplete?: 10 + | 'off' 11 + | 'on' 12 + | 'name' 13 + | 'email' 14 + | 'username' 15 + | 'current-password' 16 + | 'new-password' 17 + | 'one-time-code'; 18 + pattern?: string; 19 + required?: boolean; 20 + disabled?: boolean; 21 + placeholder?: string; 22 + error?: string | null | undefined | false; 23 + value?: string; 24 + onInput?: (ev: InputEvent) => void; 25 + } 26 + 27 + const TextInput = (props: TextInputProps) => { 28 + const fieldset = useFieldset(); 29 + const id = createId(); 30 + 31 + const hasValue = 'value' in props; 32 + const isDisabled = () => fieldset.disabled || !!props.disabled; 33 + 34 + return ( 35 + <div class="flex flex-col gap-2"> 36 + <label 37 + for={id} 38 + class={ 39 + `text-sm font-medium empty:hidden` + 40 + (!isDisabled() ? ` text-c-contrast-900` : ` text-c-contrast-700`) 41 + } 42 + > 43 + {props.label} 44 + </label> 45 + <input 46 + ref={props.ref} 47 + id={id} 48 + type={props.type || 'text'} 49 + autocomplete={props.autocomplete} 50 + pattern={props.pattern} 51 + required={props.required} 52 + disabled={isDisabled()} 53 + value={hasValue ? props.value : ''} 54 + onInput={props.onInput} 55 + placeholder={props.placeholder} 56 + class={buttonClassNames(isDisabled)} 57 + /> 58 + 59 + {props.error && <p class="text-de text-c-negative-300">{props.error}</p>} 60 + </div> 61 + ); 62 + }; 63 + 64 + const buttonClassNames = (isDisabled: () => boolean): string => { 65 + let cn = `rounded bg-c-black px-3 py-2 text-sm leading-6 outline-2 -outline-offset-2 outline-c-primary-400 focus:outline`; 66 + 67 + if (!isDisabled()) { 68 + cn += ` border border-c-contrast-300 text-c-contrast-900 placeholder:text-c-contrast-400`; 69 + } else { 70 + cn += ` border border-c-contrast-100 text-c-contrast-600 placeholder:text-c-contrast-400`; 71 + } 72 + 73 + return cn; 74 + }; 75 + 76 + export default TextInput;
+47
src/components/time-ago.tsx
··· 1 + import { createRenderEffect, createSignal, type Accessor, type JSX } from 'solid-js'; 2 + 3 + import { formatAbsDateTime, formatReltime } from '~/lib/intl/time'; 4 + 5 + export interface TimeAgoProps { 6 + value: string | number; 7 + /** Expected to be static */ 8 + absolute?: (time: number) => string; 9 + /** Expected to be static */ 10 + relative?: (time: number) => string; 11 + children: (relative: Accessor<string>, absolute: Accessor<string>) => JSX.Element; 12 + } 13 + 14 + const [watch, tick] = createSignal<void>(undefined, { equals: false }); 15 + 16 + const tickForward = () => { 17 + tick(); 18 + setTimeout(() => requestIdleCallback(tickForward), 60_000); 19 + }; 20 + 21 + const TimeAgo = (props: TimeAgoProps) => { 22 + const formatAbsolute = props.absolute ?? formatAbsDateTime; 23 + const formatRelative = props.relative ?? formatReltime; 24 + 25 + const [absolute, setAbsolute] = createSignal(''); 26 + const [relative, setRelative] = createSignal(''); 27 + 28 + createRenderEffect(() => { 29 + const time = toInt(props.value); 30 + 31 + setAbsolute(formatAbsolute(time)); 32 + 33 + createRenderEffect(() => { 34 + watch(); 35 + return setRelative(formatRelative(time)); 36 + }); 37 + }); 38 + 39 + return props.children(relative, absolute); 40 + }; 41 + 42 + const toInt = (date: string | number): number => { 43 + return typeof date !== 'number' ? new Date(date).getTime() : date; 44 + }; 45 + 46 + export default TimeAgo; 47 + tickForward();
+139
src/components/virtual-item.tsx
··· 1 + import { createEffect, createRenderEffect, createSignal, onCleanup, runWithOwner, type JSX } from 'solid-js'; 2 + 3 + import { UNSAFE_useViewContext } from '~/lib/navigation/router'; 4 + import { intersectionObserver, resizeObserver } from '~/lib/observer'; 5 + 6 + const createVirtualStore = (ctx: ReturnType<typeof UNSAFE_useViewContext>) => { 7 + return runWithOwner(ctx.owner, () => { 8 + const active = ctx.active; 9 + let disabled = false; 10 + 11 + createRenderEffect(() => { 12 + if (!active()) { 13 + disabled = true; 14 + } 15 + }); 16 + 17 + createEffect(() => { 18 + if (active()) { 19 + disabled = false; 20 + } 21 + }); 22 + 23 + return { 24 + get disabled() { 25 + return disabled; 26 + }, 27 + }; 28 + })!; 29 + }; 30 + 31 + const virtualStoreMap = new WeakMap< 32 + ReturnType<typeof UNSAFE_useViewContext>, 33 + ReturnType<typeof createVirtualStore> 34 + >(); 35 + 36 + const getVirtualStore = (ctx: ReturnType<typeof UNSAFE_useViewContext>) => { 37 + let store = virtualStoreMap.get(ctx); 38 + if (store === undefined) { 39 + virtualStoreMap.set(ctx, (store = createVirtualStore(ctx))); 40 + } 41 + 42 + return store; 43 + }; 44 + 45 + export interface VirtualItemProps { 46 + estimateHeight?: number; 47 + children?: JSX.Element; 48 + } 49 + 50 + const VirtualItem = (props: VirtualItemProps) => { 51 + let _entry: IntersectionObserverEntry | undefined; 52 + let _height: number | undefined; 53 + let _intersecting = false; 54 + 55 + const store = getVirtualStore(UNSAFE_useViewContext()); 56 + const estimateHeight = props.estimateHeight; 57 + 58 + const [intersecting, setIntersecting] = createSignal(_intersecting); 59 + const [storedHeight, setStoredHeight] = createSignal(estimateHeight); 60 + 61 + const shouldHide = () => !intersecting() && (estimateHeight ?? storedHeight()) !== undefined; 62 + 63 + const handleIntersect = (nextEntry: IntersectionObserverEntry) => { 64 + _entry = undefined; 65 + 66 + if (store.disabled) { 67 + return; 68 + } 69 + 70 + const prev = _intersecting; 71 + const next = nextEntry.isIntersecting; 72 + 73 + if (!prev && next) { 74 + // hidden -> visible 75 + setIntersecting((_intersecting = next)); 76 + } else if (prev && !next) { 77 + // visible -> hidden 78 + // unmounting is cheap, but we don't need to immediately unmount it, say 79 + // for scenarios where layout is still being figured out and we don't 80 + // actually know where the virtual container is gonna end up. 81 + 82 + _entry = nextEntry; 83 + 84 + requestIdleCallback(() => { 85 + // bail out if it's no longer us. 86 + if (_entry !== nextEntry) { 87 + return; 88 + } 89 + 90 + _entry = undefined; 91 + setIntersecting((_intersecting = next)); 92 + }); 93 + } 94 + }; 95 + 96 + const handleResize = (nextEntry: ResizeObserverEntry) => { 97 + if (!_intersecting || store.disabled) { 98 + return; 99 + } 100 + 101 + const contentRect = nextEntry.contentRect; 102 + const nextHeight = ((contentRect.height * 1000) | 0) / 1000; 103 + 104 + if (nextHeight !== _height) { 105 + setStoredHeight((_height = nextHeight)); 106 + } 107 + }; 108 + 109 + return ( 110 + <article 111 + ref={startMeasure} 112 + class="shrink-0" 113 + style={{ 114 + contain: 'content', 115 + height: shouldHide() ? `${_height ?? storedHeight()}px` : undefined, 116 + }} 117 + prop:$onintersect={handleIntersect} 118 + prop:$onresize={handleResize} 119 + > 120 + {(() => { 121 + if (!shouldHide()) { 122 + return props.children; 123 + } 124 + })()} 125 + </article> 126 + ); 127 + }; 128 + 129 + export default VirtualItem; 130 + 131 + const startMeasure = (node: HTMLElement) => { 132 + intersectionObserver.observe(node); 133 + resizeObserver.observe(node); 134 + 135 + onCleanup(() => { 136 + intersectionObserver.unobserve(node); 137 + resizeObserver.unobserve(node); 138 + }); 139 + };
+1
src/globals/broadcast.ts
··· 1 + export const focusBroadcast = new BroadcastChannel('focus');
+10
src/globals/events.ts
··· 1 + import { EventEmitter } from '@mary/events'; 2 + 3 + export const globalEvents = new EventEmitter<{ 4 + // Current session has expired 5 + sessionexpired(): void; 6 + // User has published a post 7 + postpublished(): void; 8 + // User tried navigating to the same main page they're already in 9 + softreset(): void; 10 + }>();
+7
src/globals/locales.ts
··· 1 + const uniq = <T>(items: T[]): T[] => { 2 + return Array.from(new Set(items)); 3 + }; 4 + 5 + export const systemLanguages = uniq(navigator.languages.map((lang) => lang.split('-')[0])); 6 + 7 + export const primarySystemLanguage = systemLanguages[0];
+58
src/globals/modals.tsx
··· 1 + import { createContext, createSignal, useContext, type JSX } from 'solid-js'; 2 + import { assert } from '~/lib/invariant'; 3 + 4 + type ModalRenderer = (context: ModalContext) => JSX.Element; 5 + 6 + export interface ModalState { 7 + id: number; 8 + render: ModalRenderer; 9 + } 10 + 11 + const [modals, _setModals] = createSignal<ModalState[]>([]); 12 + let _id = 0; 13 + 14 + export const hasModals = (): boolean => { 15 + return modals().length !== 0; 16 + }; 17 + 18 + export const openModal = (fn: ModalRenderer): number => { 19 + const id = _id++; 20 + 21 + _setModals(($modals) => $modals.concat({ id, render: fn })); 22 + return id; 23 + }; 24 + 25 + export const closeModal = (id: number): void => { 26 + _setModals(($modals) => { 27 + const index = $modals.findIndex((v) => v.id === id); 28 + 29 + if (index === -1) { 30 + return $modals; 31 + } 32 + 33 + return $modals.toSpliced(index, 1); 34 + }); 35 + }; 36 + 37 + export const closeAllModals = (): void => { 38 + _setModals([]); 39 + }; 40 + 41 + export interface ModalContext { 42 + id: number; 43 + /** Whether this dialog is currently the top-most dialog presented */ 44 + isActive(): boolean; 45 + /** Close this dialog */ 46 + close(): void; 47 + } 48 + 49 + const Context = createContext<ModalContext>(); 50 + 51 + export const useModalContext = (): ModalContext => { 52 + const context = useContext(Context); 53 + assert(context !== undefined, `Expected useModalContext to be used under a modal`); 54 + 55 + return context; 56 + }; 57 + 58 + export { Context as INTERNAL_ModalContext, modals as INTERNAL_modals };
+9
src/globals/navigation.ts
··· 1 + import { createBrowserHistory } from '~/lib/navigation/history'; 2 + import { createHistoryLogger } from '~/lib/navigation/logger'; 3 + 4 + export const history = createBrowserHistory(); 5 + export const logger = createHistoryLogger(history); 6 + 7 + export const getEntryAt = (delta: number) => { 8 + return logger.entries[logger.active + delta]; 9 + };
+37
src/globals/preferences.ts
··· 1 + import { createRoot } from 'solid-js'; 2 + 3 + import type { GlobalPreferenceSchema } from '~/lib/preferences/global'; 4 + import type { SessionPreferenceSchema } from '~/lib/preferences/sessions'; 5 + 6 + import { createReactiveLocalStorage } from '~/lib/signals/storage'; 7 + 8 + export const sessions = createRoot(() => { 9 + return createReactiveLocalStorage<SessionPreferenceSchema>('sessions', (version, prev) => { 10 + if (version === 0) { 11 + return { 12 + $version: 1, 13 + active: undefined, 14 + accounts: [], 15 + }; 16 + } 17 + 18 + return prev; 19 + }); 20 + }); 21 + 22 + export const global = createRoot(() => { 23 + return createReactiveLocalStorage<GlobalPreferenceSchema>('global', (version, prev) => { 24 + if (version === 0) { 25 + const prefs: GlobalPreferenceSchema = { 26 + $version: 1, 27 + ui: { 28 + theme: 'system', 29 + }, 30 + }; 31 + 32 + return prefs; 33 + } 34 + 35 + return prev; 36 + }); 37 + });
+35
src/lib/element-refs.ts
··· 1 + import { createEffect } from 'solid-js'; 2 + 3 + import { intersectionObserver } from './observer'; 4 + 5 + export const ifIntersect = ( 6 + node: HTMLElement, 7 + enabled: () => boolean | undefined, 8 + onIntersect: () => void, 9 + ) => { 10 + createEffect((setup: boolean) => { 11 + if (enabled()) { 12 + if (!setup) { 13 + // @ts-expect-error 14 + node.$onintersect = (entry: IntersectionObserverEntry) => { 15 + if (entry.isIntersecting) { 16 + onIntersect(); 17 + } 18 + }; 19 + 20 + intersectionObserver.observe(node); 21 + return true; 22 + } 23 + } else { 24 + if (setup) { 25 + // @ts-expect-error 26 + node.$onintersect = undefined; 27 + 28 + intersectionObserver.unobserve(node); 29 + return false; 30 + } 31 + } 32 + 33 + return setup; 34 + }, false); 35 + };
+22
src/lib/hooks/abortable.ts
··· 1 + import { onCleanup } from 'solid-js'; 2 + 3 + type Abortable = [signal: () => AbortSignal, cleanup: () => void]; 4 + 5 + export const makeAbortable = (): Abortable => { 6 + let controller: AbortController | undefined; 7 + 8 + const cleanup = () => { 9 + return controller?.abort(); 10 + }; 11 + 12 + const signal = () => { 13 + cleanup(); 14 + 15 + controller = new AbortController(); 16 + return controller.signal; 17 + }; 18 + 19 + onCleanup(cleanup); 20 + 21 + return [signal, cleanup]; 22 + };
+23
src/lib/hooks/debounced-value.ts
··· 1 + import { createEffect, createSignal, onCleanup, type Accessor } from 'solid-js'; 2 + 3 + export const createDebouncedValue = <T>( 4 + accessor: Accessor<T>, 5 + delay: number, 6 + equals?: false | ((prev: T, next: T) => boolean), 7 + ): Accessor<T> => { 8 + const initial = accessor(); 9 + const [state, setState] = createSignal(initial, { equals }); 10 + 11 + createEffect((prev: T) => { 12 + const next = accessor(); 13 + 14 + if (prev !== next) { 15 + const timeout = setTimeout(() => setState(() => next), delay); 16 + onCleanup(() => clearTimeout(timeout)); 17 + } 18 + 19 + return next; 20 + }, initial); 21 + 22 + return state; 23 + };
+11
src/lib/hooks/derived-signal.ts
··· 1 + import { createRenderEffect, createSignal, type Accessor, type Signal } from 'solid-js'; 2 + 3 + export const createDerivedSignal = <T>(accessor: Accessor<T>): Signal<T> => { 4 + const [state, setState] = createSignal<T>(); 5 + 6 + createRenderEffect(() => { 7 + setState(accessor); 8 + }); 9 + 10 + return [state, setState] as Signal<T>; 11 + };
+23
src/lib/hooks/escape.ts
··· 1 + import { createEffect } from 'solid-js'; 2 + import { createEventListener } from './event-listener'; 3 + 4 + export const useEscape = (callback: () => void, enabled: () => boolean) => { 5 + createEffect(() => { 6 + if (!enabled()) { 7 + return; 8 + } 9 + 10 + createEventListener(window, 'keydown', (ev) => { 11 + if (ev.key === 'Escape' && !ev.defaultPrevented) { 12 + ev.preventDefault(); 13 + 14 + const focused = document.activeElement; 15 + if (focused !== null && focused !== document.body) { 16 + (focused as any).blur(); 17 + } 18 + 19 + callback(); 20 + } 21 + }); 22 + }); 23 + };
+42
src/lib/hooks/event-listener.ts
··· 1 + import { onCleanup } from 'solid-js'; 2 + 3 + type UnknownFunction = (...args: any[]) => any; 4 + 5 + type InferEventType<TTarget> = TTarget extends { 6 + // we infer from 2 overloads which are super common for event targets in the DOM lib 7 + // we "prioritize" the first one as the first one is always more specific 8 + addEventListener(type: infer P, ...args: any): void; 9 + // we can ignore the second one as it's usually just a fallback that allows bare `string` here 10 + // we use `infer P2` over `any` as we really don't care about this type value 11 + // and we don't want to accidentally fail a type assignability check, remember that `any` isn't assignable to `never` 12 + addEventListener(type: infer P2, ...args: any): void; 13 + } 14 + ? P & string 15 + : never; 16 + 17 + type InferEvent<TTarget, TType extends string> = `on${TType}` extends keyof TTarget 18 + ? Parameters<Extract<TTarget[`on${TType}`], UnknownFunction>>[0] 19 + : Event; 20 + 21 + // For listener objects, the handleEvent function has the object as the `this` binding 22 + type ListenerObject<TEvent extends Event> = { 23 + handleEvent(this: ListenerObject<TEvent>, event: TEvent): void; 24 + }; 25 + 26 + // event listeners can be an object or a function 27 + export type Listener<TTarget extends EventTarget, TType extends string> = 28 + | ListenerObject<InferEvent<TTarget, TType>> 29 + | { (this: TTarget, ev: InferEvent<TTarget, TType>): void }; 30 + 31 + export const createEventListener = < 32 + TTarget extends EventTarget, 33 + TType extends InferEventType<TTarget> | (string & {}), 34 + >( 35 + target: TTarget, 36 + type: TType, 37 + listener: Listener<TTarget, TType>, 38 + options?: boolean | AddEventListenerOptions, 39 + ) => { 40 + onCleanup(target.removeEventListener.bind(target, type, listener, options)); 41 + target.addEventListener(type, listener, options); 42 + };
+5
src/lib/hooks/id.ts
··· 1 + let uid = 0; 2 + 3 + export const createId = () => { 4 + return `_${uid++}_`; 5 + };
+41
src/lib/hooks/media-query.ts
··· 1 + import { createSignal, onCleanup, type Accessor } from 'solid-js'; 2 + 3 + interface MediaStore { 4 + /** State backing */ 5 + a: Accessor<boolean>; 6 + /** Amount of subscriptions */ 7 + n: number; 8 + /** Cleanup function */ 9 + c: () => void; 10 + } 11 + 12 + const map: Record<string, MediaStore> = {}; 13 + 14 + /*#__NO_SIDE_EFFECTS__*/ 15 + export const useMediaQuery = (query: string): Accessor<boolean> => { 16 + let media = map[query]; 17 + 18 + if (!media) { 19 + const matcher = window.matchMedia(query); 20 + const [state, setState] = createSignal(matcher.matches); 21 + 22 + const callback = () => setState(matcher.matches); 23 + matcher.onchange = callback; 24 + 25 + media = map[query] = { 26 + n: 0, 27 + a: state, 28 + c: () => { 29 + if (--media.n < 1) { 30 + matcher.onchange = null; 31 + delete map[query]; 32 + } 33 + }, 34 + }; 35 + } 36 + 37 + media.n++; 38 + onCleanup(media.c); 39 + 40 + return media.a; 41 + };
+72
src/lib/hooks/modal-close.ts
··· 1 + import { createEffect, onCleanup } from 'solid-js'; 2 + 3 + import { createEventListener } from './event-listener'; 4 + 5 + const isCloseWatcherAvailable = typeof CloseWatcher !== 'undefined'; 6 + 7 + export const useModalClose = (container: HTMLElement, callback: () => void, enabled: () => boolean) => { 8 + createEffect(() => { 9 + if (!enabled()) { 10 + return; 11 + } 12 + 13 + // Close modal if clicks happen outside of container 14 + let initialTarget: HTMLElement | null = null; 15 + 16 + createEventListener(document, 'pointerdown', (ev) => { 17 + // We'd like to know where the click initially started from, not where the 18 + // click ended up, this prevents closing the modal prematurely from the 19 + // user (accidentally) overshooting their mouse cursor. 20 + 21 + initialTarget = ev.target as HTMLElement | null; 22 + }); 23 + 24 + createEventListener( 25 + document, 26 + 'click', 27 + () => { 28 + // Don't do anything if `initialTarget` is somehow missing 29 + if (!initialTarget) { 30 + return; 31 + } 32 + 33 + // Unset `initialTarget` now that we're here 34 + const target = initialTarget; 35 + initialTarget = null; 36 + 37 + // Don't do anything if `target` is inside `container` 38 + if (container.contains(target)) { 39 + return; 40 + } 41 + 42 + // Call back since this click happened outside `container`. 43 + callback(); 44 + }, 45 + true, 46 + ); 47 + 48 + // Start a close watcher if available, otherwise, listen to Escape key only 49 + if (isCloseWatcherAvailable) { 50 + const watcher = new CloseWatcher(); 51 + watcher.oncancel = (ev) => { 52 + ev.preventDefault(); 53 + close(); 54 + }; 55 + 56 + onCleanup(() => watcher.close()); 57 + } else { 58 + createEventListener(window, 'keydown', (ev) => { 59 + if (ev.key === 'Escape' && !ev.defaultPrevented) { 60 + ev.preventDefault(); 61 + 62 + const focused = document.activeElement; 63 + if (focused !== null && focused !== document.body) { 64 + (focused as any).blur(); 65 + } 66 + 67 + callback(); 68 + } 69 + }); 70 + } 71 + }); 72 + };
+45
src/lib/hooks/outside-click.ts
··· 1 + import { createEffect } from 'solid-js'; 2 + 3 + import { createEventListener } from './event-listener'; 4 + 5 + export const useOutsideClick = (container: HTMLElement, callback: () => void, enabled: () => boolean) => { 6 + createEffect(() => { 7 + if (!enabled()) { 8 + return; 9 + } 10 + 11 + let initialTarget: HTMLElement | null = null; 12 + 13 + createEventListener(document, 'pointerdown', (ev) => { 14 + // We'd like to know where the click initially started from, not where the 15 + // click ended up, this prevents closing the modal prematurely from the 16 + // user (accidentally) overshooting their mouse cursor. 17 + 18 + initialTarget = ev.target as HTMLElement | null; 19 + }); 20 + 21 + createEventListener( 22 + document, 23 + 'click', 24 + () => { 25 + // Don't do anything if `initialTarget` is somehow missing 26 + if (!initialTarget) { 27 + return; 28 + } 29 + 30 + // Unset `initialTarget` now that we're here 31 + const target = initialTarget; 32 + initialTarget = null; 33 + 34 + // Don't do anything if `target` is inside `container` 35 + if (container.contains(target)) { 36 + return; 37 + } 38 + 39 + // Call back since this click happened outside `container`. 40 + callback(); 41 + }, 42 + true, 43 + ); 44 + }); 45 + };
+44
src/lib/input-refs.ts
··· 1 + import { createEffect, createRenderEffect } from 'solid-js'; 2 + 3 + import type { CreateMutationResult } from '@mary/solid-query'; 4 + 5 + type FocusableElement = HTMLButtonElement | HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement; 6 + type TextInput = HTMLInputElement | HTMLTextAreaElement; 7 + 8 + export const autofocusOnMutation = ( 9 + node: FocusableElement, 10 + mutation: CreateMutationResult<any, any, any, any>, 11 + first = true, 12 + ) => { 13 + // Render effects are not affected by <Suspense> 14 + createRenderEffect((first: boolean) => { 15 + if (mutation.isError || first) { 16 + setTimeout(() => node.focus(), 0); 17 + } 18 + 19 + return false; 20 + }, first); 21 + }; 22 + 23 + export const autofocusIfEnabled = (node: FocusableElement, enabled: () => boolean) => { 24 + // Render effects are not affected by <Suspense> 25 + createRenderEffect(() => { 26 + if (enabled()) { 27 + setTimeout(() => node.focus(), 0); 28 + } 29 + }); 30 + }; 31 + 32 + export const modelText = (node: TextInput, getter: () => string, setter: (next: string) => void) => { 33 + let current: string | undefined; 34 + 35 + createEffect(() => { 36 + if (current !== (current = getter())) { 37 + node.value = current; 38 + } 39 + }); 40 + 41 + node.addEventListener('input', (ev) => { 42 + setter((current = node.value)); 43 + }); 44 + };
+44
src/lib/interaction.ts
··· 1 + export const isMac = /^Mac/i.test(navigator.platform); 2 + 3 + const DEFAULT_EXCLUSION = 'a, button, img, video, dialog, [role=button]'; 4 + export const INTERACTION_TAGS = 'a, button, [role=button]'; 5 + 6 + export const hasSelectionRange = () => { 7 + const selection = window.getSelection(); 8 + return selection !== null && selection.type === 'Range'; 9 + }; 10 + 11 + export const isElementClicked = (ev: Event, exclusion = DEFAULT_EXCLUSION) => { 12 + const target = ev.currentTarget as HTMLElement; 13 + const path = ev.composedPath() as HTMLElement[]; 14 + 15 + if ( 16 + !path.includes(target) || 17 + (ev.type === 'keydown' && (ev as KeyboardEvent).key !== 'Enter') || 18 + (ev.type === 'auxclick' && (ev as MouseEvent).button !== 1) 19 + ) { 20 + return false; 21 + } 22 + 23 + for (let idx = 0, len = path.length; idx < len; idx++) { 24 + const node = path[idx]; 25 + 26 + if (node == target) { 27 + break; 28 + } 29 + 30 + if (node.matches(exclusion)) { 31 + return false; 32 + } 33 + } 34 + 35 + return !hasSelectionRange(); 36 + }; 37 + 38 + export const isElementAltClicked = (ev: MouseEvent | KeyboardEvent) => { 39 + return ev.type === 'auxclick' || isCtrlKeyPressed(ev); 40 + }; 41 + 42 + export const isCtrlKeyPressed = (ev: MouseEvent | KeyboardEvent) => { 43 + return isMac ? ev.metaKey : ev.ctrlKey; 44 + };
+87
src/lib/intl/time.ts
··· 1 + let startOfYear = 0; 2 + let endOfYear = 0; 3 + 4 + const SECOND = 1e3; 5 + const NOW = SECOND * 10; 6 + const MINUTE = SECOND * 60; 7 + const HOUR = MINUTE * 60; 8 + const DAY = HOUR * 24; 9 + const WEEK = DAY * 7; 10 + // const MONTH = WEEK * 4; 11 + // const YEAR = MONTH * 12; 12 + 13 + const absWithYearFormat = new Intl.DateTimeFormat('en-US', { dateStyle: 'medium' }); 14 + const absFormat = new Intl.DateTimeFormat('en-US', { month: 'short', day: 'numeric' }); 15 + const absTimeFormat = new Intl.DateTimeFormat('en-US', { dateStyle: 'long', timeStyle: 'short' }); 16 + 17 + const formatters: Record<string, Intl.NumberFormat> = {}; 18 + const getNow = Date.now; 19 + 20 + export const formatReltime = (time: number): string => { 21 + const now = getNow(); 22 + const delta = now - time; 23 + 24 + if (delta < 0 || delta > WEEK) { 25 + if (now > endOfYear) { 26 + const date = new Date(); 27 + 28 + date.setMonth(0, 1); 29 + date.setHours(0, 0, 0); 30 + startOfYear = date.getTime(); 31 + 32 + date.setFullYear(date.getFullYear() + 1, 0, 0); 33 + date.setHours(23, 59, 59, 999); 34 + endOfYear = date.getTime(); 35 + } 36 + 37 + // if it happened this year, don't show the year. 38 + if (time >= startOfYear && time <= endOfYear) { 39 + return absFormat.format(time); 40 + } 41 + 42 + return absWithYearFormat.format(time); 43 + } 44 + 45 + if (delta < NOW) { 46 + return `now`; 47 + } 48 + 49 + { 50 + let value: number; 51 + let unit: Intl.RelativeTimeFormatUnit; 52 + 53 + if (delta < MINUTE) { 54 + value = Math.floor(delta / SECOND); 55 + unit = 'second'; 56 + } else if (delta < HOUR) { 57 + value = Math.floor(delta / MINUTE); 58 + unit = 'minute'; 59 + } else if (delta < DAY) { 60 + value = Math.floor(delta / HOUR); 61 + unit = 'hour'; 62 + } else { 63 + // use rounding, this handles the following scenario: 64 + // - 2024-02-13T09:00Z <- 2024-02-15T07:00Z = 2d 65 + value = Math.round(delta / DAY); 66 + unit = 'day'; 67 + } 68 + 69 + const formatter = (formatters[unit] ||= new Intl.NumberFormat('en-US', { 70 + style: 'unit', 71 + unit: unit, 72 + unitDisplay: 'narrow', 73 + })); 74 + 75 + return formatter.format(Math.abs(value)); 76 + } 77 + }; 78 + 79 + export const formatAbsDate = (time: string | number) => { 80 + const date = new Date(time); 81 + return absWithYearFormat.format(date); 82 + }; 83 + 84 + export const formatAbsDateTime = (time: string | number) => { 85 + const date = new Date(time); 86 + return absTimeFormat.format(date); 87 + };
+15
src/lib/invariant.ts
··· 1 + export function assert(condition: any, message?: string): asserts condition { 2 + if (import.meta.env.DEV && !condition) { 3 + throw new Error(`Assertion failed` + message ? `: ${message}` : ``); 4 + } 5 + } 6 + 7 + export function assertStrong(condition: any, message?: string): asserts condition { 8 + if (!condition) { 9 + if (import.meta.env.DEV) { 10 + throw new Error(`Assertion failed` + message ? `: ${message}` : ``); 11 + } 12 + 13 + throw new Error(`Assertion failed`); 14 + } 15 + }
+28
src/lib/misc.ts
··· 1 + import { createMemo, untrack } from 'solid-js'; 2 + 3 + export const mapDefined = <T, R>(array: T[], mapper: (value: T) => R | undefined): R[] => { 4 + var mapped: R[] = []; 5 + 6 + var idx = 0; 7 + var len = array.length; 8 + var temp: R | undefined; 9 + 10 + for (; idx < len; idx++) { 11 + if ((temp = mapper(array[idx])) !== undefined) { 12 + mapped.push(temp); 13 + } 14 + } 15 + 16 + return mapped; 17 + }; 18 + 19 + export const on = <T, R>(accessor: () => T, callback: (value: T) => R): (() => R) => { 20 + return () => { 21 + const value = accessor(); 22 + return untrack(() => callback(value)); 23 + }; 24 + }; 25 + 26 + export const memoizedOn = <T, R>(accessor: () => T, callback: (value: T) => R): (() => R) => { 27 + return createMemo(on(accessor, callback)); 28 + };
+372
src/lib/navigation/history.ts
··· 1 + // Fork of `history` npm package 2 + // Repository: github.com/remix-run/history 3 + // Commit: 3e9dab413f4eda8d6bce565388c5ddb7aeff9f7e 4 + 5 + // Most of the changes are just trimming it down to only include the browser 6 + // history implementation. 7 + 8 + import { nanoid } from 'nanoid/non-secure'; 9 + 10 + export type Action = 'traverse' | 'push' | 'replace' | 'update'; 11 + 12 + export interface Path { 13 + /** A URL pathname, beginning with a /. */ 14 + pathname: string; 15 + /** A URL search string, beginning with a ?. */ 16 + search: string; 17 + /** A URL fragment identifier, beginning with a #. */ 18 + hash: string; 19 + } 20 + 21 + export interface Location extends Path { 22 + /** Position of this history */ 23 + index: number; 24 + /** A value of arbitrary data associated with this location. */ 25 + state: unknown; 26 + /** A unique string associated with this location */ 27 + key: string; 28 + } 29 + 30 + export interface Update { 31 + action: Action; 32 + location: Location; 33 + } 34 + 35 + export type Listener = (update: Update) => void; 36 + 37 + export interface Transition extends Update { 38 + retry(): void; 39 + } 40 + 41 + export type Blocker = (tx: Transition) => void; 42 + 43 + export type To = string | Partial<Path>; 44 + 45 + export interface History { 46 + readonly location: Location; 47 + 48 + createHref(to: To): string; 49 + 50 + navigate(to: To, options?: NavigateOptions): void; 51 + update(state: any): void; 52 + 53 + go(delta: number): void; 54 + back(): void; 55 + forward(): void; 56 + 57 + listen(listener: Listener): () => void; 58 + block(blocker: Blocker): () => void; 59 + } 60 + 61 + export interface NavigateOptions { 62 + replace?: boolean; 63 + state?: unknown; 64 + } 65 + 66 + /** 67 + * A browser history stores the current location in regular URLs in a web 68 + * browser environment. This is the standard for most web apps and provides the 69 + * cleanest URLs the browser's address bar. 70 + */ 71 + export interface BrowserHistory extends History {} 72 + 73 + const warning = (cond: any, message: string) => { 74 + if (!import.meta.env.PROD && !cond) { 75 + console.warn(message); 76 + } 77 + }; 78 + 79 + interface HistoryState { 80 + usr: any; 81 + key?: string; 82 + idx: number; 83 + } 84 + 85 + const BeforeUnloadEventType = 'beforeunload'; 86 + const PopStateEventType = 'popstate'; 87 + 88 + export interface BrowserHistoryOptions { 89 + window?: Window; 90 + } 91 + 92 + /** 93 + * Browser history stores the location in regular URLs. This is the standard for 94 + * most web apps, but it requires some configuration on the server to ensure you 95 + * serve the same app at multiple URLs. 96 + */ 97 + export const createBrowserHistory = (options: BrowserHistoryOptions = {}): BrowserHistory => { 98 + const { window = document.defaultView! } = options; 99 + const globalHistory = window.history; 100 + 101 + const getCurrentLocation = (): Location => { 102 + const { pathname, search, hash } = window.location; 103 + const state = globalHistory.state || {}; 104 + return { 105 + pathname, 106 + search, 107 + hash, 108 + index: state.idx, 109 + state: state.usr || null, 110 + key: state.key || 'default', 111 + }; 112 + }; 113 + 114 + let blockedPopTx: Transition | null = null; 115 + const handlePop = () => { 116 + if (blockedPopTx) { 117 + blockers.call(blockedPopTx); 118 + blockedPopTx = null; 119 + } else { 120 + const nextAction: Action = 'traverse'; 121 + const nextLocation = getCurrentLocation(); 122 + const nextIndex = nextLocation.index; 123 + 124 + if (blockers.length) { 125 + if (nextIndex != null) { 126 + const delta = location.index - nextIndex; 127 + if (delta) { 128 + // Revert the POP 129 + blockedPopTx = { 130 + action: nextAction, 131 + location: nextLocation, 132 + retry() { 133 + go(delta * -1); 134 + }, 135 + }; 136 + 137 + go(delta); 138 + } 139 + } else { 140 + // Trying to POP to a location with no index. We did not create 141 + // this location, so we can't effectively block the navigation. 142 + warning( 143 + false, 144 + // TODO: Write up a doc that explains our blocking strategy in 145 + // detail and link to it here so people can understand better what 146 + // is going on and how to avoid it. 147 + `You are trying to block a POP navigation to a location that was not ` + 148 + `created by the history library. The block will fail silently in ` + 149 + `production, but in general you should do all navigation with the ` + 150 + `history library (instead of using window.history.pushState directly) ` + 151 + `to avoid this situation.`, 152 + ); 153 + } 154 + } else { 155 + applyTx(nextAction); 156 + } 157 + } 158 + }; 159 + 160 + const listeners = createEvents<Listener>(); 161 + const blockers = createEvents<Blocker>(); 162 + 163 + let location = getCurrentLocation(); 164 + 165 + window.addEventListener(PopStateEventType, handlePop); 166 + 167 + if (location.index == null) { 168 + globalHistory.replaceState({ ...globalHistory.state, idx: (location.index = 0) }, ''); 169 + } 170 + 171 + const createHref = (to: To): string => { 172 + return typeof to === 'string' ? to : createPath(to); 173 + }; 174 + 175 + // state defaults to `null` because `window.history.state` does 176 + const getNextLocation = (to: To, index: number, state: any = null): Location => { 177 + return { 178 + pathname: location.pathname, 179 + hash: '', 180 + search: '', 181 + ...(typeof to === 'string' ? parsePath(to) : to), 182 + index, 183 + state, 184 + key: createKey(), 185 + }; 186 + }; 187 + 188 + const getHistoryStateAndUrl = (nextLocation: Location): [HistoryState, string] => { 189 + return [ 190 + { 191 + usr: nextLocation.state, 192 + key: nextLocation.key, 193 + idx: nextLocation.index, 194 + }, 195 + createHref(nextLocation), 196 + ]; 197 + }; 198 + 199 + const allowTx = (action: Action, location: Location, retry: () => void): boolean => { 200 + return !blockers.length || (blockers.call({ action, location, retry }), false); 201 + }; 202 + 203 + const applyTx = (nextAction: Action): void => { 204 + location = getCurrentLocation(); 205 + listeners.call({ action: nextAction, location }); 206 + }; 207 + 208 + const navigate = (to: To, { replace, state }: NavigateOptions = {}): void => { 209 + const nextAction: Action = !replace ? 'push' : 'replace'; 210 + const nextIndex = location.index + (!replace ? 1 : 0); 211 + const nextLocation = getNextLocation(to, nextIndex, state); 212 + 213 + const retry = () => { 214 + navigate(to, { replace, state }); 215 + }; 216 + 217 + if (allowTx(nextAction, nextLocation, retry)) { 218 + const [historyState, url] = getHistoryStateAndUrl(nextLocation); 219 + 220 + // TODO: Support forced reloading 221 + if (!replace) { 222 + // try...catch because iOS limits us to 100 pushState calls :/ 223 + try { 224 + globalHistory.pushState(historyState, '', url); 225 + } catch { 226 + // They are going to lose state here, but there is no real 227 + // way to warn them about it since the page will refresh... 228 + window.location.assign(url); 229 + } 230 + } else { 231 + globalHistory.replaceState(historyState, '', url); 232 + } 233 + 234 + applyTx(nextAction); 235 + } 236 + }; 237 + 238 + const update = (state: any): void => { 239 + const nextAction: Action = 'update'; 240 + const nextLocation = { ...location, state }; 241 + 242 + const [historyState, url] = getHistoryStateAndUrl(nextLocation); 243 + 244 + // TODO: Support forced reloading 245 + globalHistory.replaceState(historyState, '', url); 246 + 247 + applyTx(nextAction); 248 + }; 249 + 250 + const go = (delta: number): void => { 251 + globalHistory.go(delta); 252 + }; 253 + 254 + const history: BrowserHistory = { 255 + get location() { 256 + return location; 257 + }, 258 + createHref, 259 + navigate, 260 + update, 261 + go, 262 + back: () => { 263 + return go(-1); 264 + }, 265 + forward: () => { 266 + return go(1); 267 + }, 268 + listen: (listener) => { 269 + return listeners.push(listener); 270 + }, 271 + block: (blocker) => { 272 + const unblock = blockers.push(blocker); 273 + 274 + if (blockers.length === 1) { 275 + window.addEventListener(BeforeUnloadEventType, promptBeforeUnload); 276 + } 277 + 278 + return () => { 279 + unblock(); 280 + 281 + // Remove the beforeunload listener so the document may 282 + // still be salvageable in the pagehide event. 283 + // See https://html.spec.whatwg.org/#unloading-documents 284 + if (!blockers.length) { 285 + window.removeEventListener(BeforeUnloadEventType, promptBeforeUnload); 286 + } 287 + }; 288 + }, 289 + }; 290 + 291 + return history; 292 + }; 293 + 294 + const promptBeforeUnload = (event: BeforeUnloadEvent): void => { 295 + // Cancel the event. 296 + event.preventDefault(); 297 + // Chrome (and legacy IE) requires returnValue to be set. 298 + event.returnValue = ''; 299 + }; 300 + 301 + interface Events<F extends (arg: any) => void> { 302 + length: number; 303 + push: (fn: F) => () => void; 304 + call: (arg: Parameters<F>[0]) => void; 305 + } 306 + 307 + const createEvents = <F extends (arg: any) => void>(): Events<F> => { 308 + const handlers: F[] = []; 309 + 310 + return { 311 + get length() { 312 + return handlers.length; 313 + }, 314 + push(fn: F) { 315 + handlers.push(fn); 316 + 317 + return () => { 318 + const index = handlers.indexOf(fn); 319 + handlers.splice(index, 1); 320 + }; 321 + }, 322 + call(arg) { 323 + for (let idx = 0, len = handlers.length; idx < len; idx++) { 324 + (0, handlers[idx])(arg); 325 + } 326 + }, 327 + }; 328 + }; 329 + 330 + const createKey = () => { 331 + return nanoid(8); 332 + }; 333 + 334 + /** 335 + * Creates a string URL path from the given pathname, search, and hash components. 336 + */ 337 + export const createPath = ({ pathname = '/', search = '', hash = '' }: Partial<Path>) => { 338 + if (search && search !== '?') { 339 + pathname += search.charAt(0) === '?' ? search : '?' + search; 340 + } 341 + if (hash && hash !== '#') { 342 + pathname += hash.charAt(0) === '#' ? hash : '#' + hash; 343 + } 344 + return pathname; 345 + }; 346 + 347 + /** 348 + * Parses a string URL path into its separate pathname, search, and hash components. 349 + */ 350 + export const parsePath = (path: string): Partial<Path> => { 351 + const parsedPath: Partial<Path> = {}; 352 + 353 + if (path) { 354 + const hashIndex = path.indexOf('#'); 355 + if (hashIndex >= 0) { 356 + parsedPath.hash = path.substr(hashIndex); 357 + path = path.substr(0, hashIndex); 358 + } 359 + 360 + const searchIndex = path.indexOf('?'); 361 + if (searchIndex >= 0) { 362 + parsedPath.search = path.substr(searchIndex); 363 + path = path.substr(0, searchIndex); 364 + } 365 + 366 + if (path) { 367 + parsedPath.pathname = path; 368 + } 369 + } 370 + 371 + return parsedPath; 372 + };
+91
src/lib/navigation/logger.ts
··· 1 + // Keeps a best-effort log of history entries. 2 + 3 + // To simplify our stack router implementation on dealing with PWA-specific 4 + // aspects, `createHistoryLogger` is set to drop forward entries by default. 5 + 6 + import type { History, Location } from './history'; 7 + 8 + export interface HistoryLogger { 9 + readonly current: Location; 10 + readonly active: number; 11 + readonly entries: (Location | null)[]; 12 + readonly canGoBack: boolean; 13 + readonly canGoForward: boolean; 14 + } 15 + 16 + export const createHistoryLogger = (history: History, keepForwardEntries = false): HistoryLogger => { 17 + const loc = history.location; 18 + 19 + let active = loc.index; 20 + let entries = arr(active + 1, (i) => (i === active ? loc : null)); 21 + 22 + history.listen(({ action, location }) => { 23 + const index = location.index; 24 + 25 + if (action === 'push') { 26 + // New page pushed 27 + 28 + entries = entries.toSpliced(active + 1, entries.length, location); 29 + } else if (action === 'replace' || action === 'update') { 30 + // Current page replaced, or updated with new state 31 + 32 + entries = entries.with(active, location); 33 + } else if (action === 'traverse') { 34 + // Traversal happened 35 + 36 + if (keepForwardEntries) { 37 + if (index >= entries.length) { 38 + const length = entries.length; 39 + const delta = index - length; 40 + 41 + const extras = arr(delta + 1, (i) => (i === delta ? location : null)); 42 + 43 + entries = entries.concat(extras); 44 + } else if (entries[index] === null) { 45 + entries = entries.with(index, location); 46 + } 47 + } else { 48 + if (index < active) { 49 + if (entries[index] !== null) { 50 + entries = entries.slice(0, index + 1); 51 + } else { 52 + entries = entries.toSpliced(index, entries.length, location); 53 + } 54 + } else if (index >= entries.length) { 55 + const length = entries.length; 56 + const delta = index - length; 57 + 58 + const extras = arr(delta + 1, (i) => (i === delta ? location : null)); 59 + 60 + entries = entries.concat(extras); 61 + } 62 + } 63 + } 64 + 65 + active = index; 66 + }); 67 + 68 + return { 69 + get current() { 70 + // Current entry is guaranteed to exist 71 + return entries[active]!; 72 + }, 73 + get active() { 74 + return active; 75 + }, 76 + get entries() { 77 + return entries; 78 + }, 79 + 80 + get canGoBack() { 81 + return active !== 0; 82 + }, 83 + get canGoForward() { 84 + return active !== entries.length - 1; 85 + }, 86 + }; 87 + }; 88 + 89 + const arr = <T>(length: number, map: (index: number) => T): T[] => { 90 + return Array.from({ length }, (_, idx) => map(idx)); 91 + };
+324
src/lib/navigation/router.tsx
··· 1 + /* @refresh reload */ 2 + 3 + import { 4 + For, 5 + createContext, 6 + createMemo, 7 + createRenderEffect, 8 + createRoot, 9 + createSignal, 10 + getOwner, 11 + onCleanup, 12 + useContext, 13 + type Component, 14 + type JSX, 15 + type Owner, 16 + } from 'solid-js'; 17 + 18 + import { Freeze } from '@mary/solid-freeze'; 19 + 20 + import type { History, Location } from './history'; 21 + import type { HistoryLogger } from './logger'; 22 + 23 + // This is the only application-specific code we have here, might need to 24 + // move it elsewhere, maybe as a separate package? 25 + export interface RouteMeta { 26 + name?: string; 27 + main?: boolean; 28 + public?: boolean; 29 + } 30 + 31 + export interface RouteDefinition { 32 + path: string; 33 + component: Component; 34 + single?: boolean; 35 + meta?: RouteMeta; 36 + validate?: (params: Record<string, string>) => boolean; 37 + } 38 + 39 + interface InternalRouteDefinition extends RouteDefinition { 40 + _regex?: RegExp; 41 + } 42 + 43 + export interface RouterOptions { 44 + history: History; 45 + logger: HistoryLogger; 46 + routes: RouteDefinition[]; 47 + } 48 + 49 + interface MatchedRoute { 50 + readonly id: string | undefined; 51 + readonly def: RouteDefinition; 52 + readonly params: Record<string, string>; 53 + } 54 + 55 + export interface MatchedRouteState extends MatchedRoute { 56 + readonly id: string; 57 + } 58 + 59 + interface RouterState { 60 + active: string; 61 + views: Record<string, MatchedRouteState>; 62 + singles: Record<string, MatchedRouteState>; 63 + } 64 + 65 + interface ViewContextObject { 66 + active(): boolean; 67 + owner: Owner | null; 68 + route: MatchedRouteState; 69 + } 70 + 71 + let _entry: Location; 72 + 73 + let _routes: InternalRouteDefinition[] | undefined; 74 + let _cleanup: (() => void) | undefined; 75 + 76 + const [state, setState] = createSignal<RouterState>({ 77 + active: '', 78 + views: {}, 79 + singles: {}, 80 + }); 81 + 82 + export const configureRouter = ({ history, logger: log, routes }: RouterOptions) => { 83 + _cleanup?.(); 84 + 85 + _routes = routes; 86 + 87 + { 88 + _entry = log.current; 89 + 90 + const pathname = _entry.pathname; 91 + const matched = matchRoute(pathname); 92 + 93 + if (matched) { 94 + const nextKey = matched.id || _entry.key; 95 + 96 + const isSingle = !!matched.id; 97 + const matchedState: MatchedRouteState = { ...matched, id: nextKey }; 98 + 99 + const next: Record<string, MatchedRouteState> = { [nextKey]: matchedState }; 100 + 101 + setState({ 102 + active: nextKey, 103 + views: isSingle ? {} : next, 104 + singles: isSingle ? next : {}, 105 + }); 106 + } 107 + } 108 + 109 + _cleanup = history.listen(({ action, location: nextEntry }) => { 110 + const currentEntry = _entry; 111 + _entry = nextEntry; 112 + 113 + if (action !== 'update') { 114 + const pathname = nextEntry.pathname; 115 + const matched = matchRoute(pathname); 116 + 117 + if (!matched) { 118 + return; 119 + } 120 + 121 + const current = state(); 122 + 123 + let views = current.views; 124 + let singles = current.singles; 125 + 126 + const nextId = matched.id || nextEntry.key; 127 + const matchedState: MatchedRouteState = { ...matched, id: nextId }; 128 + 129 + if (!matched.id) { 130 + let nextViews: typeof views | undefined; 131 + 132 + // Recreate the views object to remove no longer reachable views if: 133 + // - We're pushing a new page, or replacing the current page 134 + // - We're traversing and the intended index is lower than current 135 + if (action !== 'traverse' || nextEntry.index < currentEntry.index) { 136 + const entries = log.entries; 137 + 138 + nextViews = {}; 139 + 140 + for (let idx = 0, len = entries.length; idx < len; idx++) { 141 + const entry = entries[idx]; 142 + const key = entry?.key; 143 + 144 + if (key !== undefined && key in views) { 145 + nextViews[key] = views[key]; 146 + } 147 + } 148 + } 149 + 150 + // Add this view, if it's already present, set `shouldCall` to true 151 + if (!(nextId in views)) { 152 + if (nextViews) { 153 + nextViews[nextId] = matchedState; 154 + } else { 155 + nextViews = { ...views, [nextId]: matchedState }; 156 + } 157 + } 158 + 159 + if (nextViews) { 160 + views = nextViews; 161 + } 162 + } else { 163 + // Add this view, if it's already present, set `shouldCall` to true 164 + if (!(nextId in singles)) { 165 + singles = { ...singles, [nextId]: matchedState }; 166 + } 167 + } 168 + 169 + setState({ active: nextId, views: views, singles: singles }); 170 + 171 + // Scroll to top if we're pushing or replacing, it's a new page. 172 + if (!matched.id && (action === 'push' || action === 'replace')) { 173 + window.scrollTo({ top: 0, behavior: 'instant' }); 174 + } 175 + } 176 + }); 177 + }; 178 + 179 + const ViewContext = createContext<ViewContextObject>(); 180 + 181 + const getMatchedRoute = () => { 182 + const current = state(); 183 + const active = current.active; 184 + 185 + const match = current.singles[active] || current.views[active]; 186 + 187 + if (match) { 188 + return match; 189 + } 190 + }; 191 + 192 + export const useMatchedRoute = () => { 193 + return createMemo(getMatchedRoute); 194 + }; 195 + 196 + export const UNSAFE_useViewContext = () => { 197 + return useContext(ViewContext)!; 198 + }; 199 + 200 + export const useParams = <T extends Record<string, string>>() => { 201 + return UNSAFE_useViewContext().route.params as T; 202 + }; 203 + 204 + export const createFocusRoot = (fn: () => void) => { 205 + const { active } = UNSAFE_useViewContext(); 206 + 207 + let destroy: (() => void) | undefined; 208 + 209 + const cleanup = () => { 210 + if (destroy) { 211 + destroy(); 212 + destroy = undefined; 213 + } 214 + }; 215 + 216 + createRenderEffect(() => { 217 + if (active()) { 218 + onCleanup(cleanup); 219 + createRoot((_destroy) => { 220 + destroy = _destroy; 221 + fn(); 222 + }); 223 + } 224 + }); 225 + }; 226 + 227 + export interface RouterViewProps { 228 + render: (matched: MatchedRouteState) => JSX.Element; 229 + } 230 + 231 + export const RouterView = (props: RouterViewProps) => { 232 + const render = props.render; 233 + 234 + const renderView = (matched: MatchedRouteState) => { 235 + const def = matched.def; 236 + const id = matched.id; 237 + 238 + const active = createMemo((): boolean => state().active === id); 239 + 240 + const context: ViewContextObject = { 241 + owner: getOwner(), 242 + active: active, 243 + route: matched, 244 + }; 245 + 246 + if (def.single) { 247 + let storedHeight: number | undefined; 248 + 249 + createRenderEffect((inited: boolean) => { 250 + const next = active(); 251 + 252 + if (inited) { 253 + if (!next) { 254 + storedHeight = document.documentElement.scrollTop; 255 + } else { 256 + setTimeout(() => window.scrollTo({ top: storedHeight, behavior: 'instant' }), 0); 257 + } 258 + } 259 + 260 + return true; 261 + }, false); 262 + } 263 + 264 + return ( 265 + <Freeze freeze={!active()}> 266 + <ViewContext.Provider value={context}>{render(matched)}</ViewContext.Provider> 267 + </Freeze> 268 + ); 269 + }; 270 + 271 + return ( 272 + <> 273 + <For each={Object.values(state().views)}>{renderView}</For> 274 + <For each={Object.values(state().singles)}>{renderView}</For> 275 + </> 276 + ); 277 + }; 278 + 279 + const matchRoute = (path: string): MatchedRoute | null => { 280 + for (let idx = 0, len = _routes!.length; idx < len; idx++) { 281 + const route = _routes![idx]; 282 + 283 + const validate = route.validate; 284 + const pattern = (route._regex ||= buildPathRegex(route.path)); 285 + 286 + const match = pattern.exec(path); 287 + 288 + if (!match || (validate && !validate(match.groups!))) { 289 + continue; 290 + } 291 + 292 + const params = match.groups!; 293 + 294 + let id: string | undefined; 295 + if (route.single) { 296 + id = '@' + idx; 297 + for (const param in params) { 298 + id += '/' + params[param]; 299 + } 300 + } 301 + 302 + return { id: id, def: route, params: params }; 303 + } 304 + 305 + return null; 306 + }; 307 + 308 + const buildPathRegex = (path: string) => { 309 + let source = 310 + '^' + 311 + path 312 + .replace(/\/*\*?$/, '') 313 + .replace(/^\/*/, '/') 314 + .replace(/[\\.*+^${}|()[\]]/g, '\\$&') 315 + .replace(/\/:([\w-]+)(\?)?/g, '/$2(?<$1>[^\\/]+)$2'); 316 + 317 + source += path.endsWith('*') 318 + ? path === '*' || path === '/*' 319 + ? '(?<$>.*)$' 320 + : '(?:\\/(?<$>.+)|\\/*)$' 321 + : '\\/*$'; 322 + 323 + return new RegExp(source, 'i'); 324 + };
+41
src/lib/observer.ts
··· 1 + const intersectionCallback: IntersectionObserverCallback = (entries, observer) => { 2 + for (let idx = 0, len = entries.length; idx < len; idx++) { 3 + const entry = entries[idx]; 4 + 5 + const target = entry.target as any; 6 + const listener = target.$onintersect; 7 + 8 + if (listener) { 9 + listener(entry); 10 + } else { 11 + observer.unobserve(target); 12 + } 13 + } 14 + }; 15 + 16 + const resizeCallback: ResizeObserverCallback = (entries, observer) => { 17 + for (let idx = 0, len = entries.length; idx < len; idx++) { 18 + const entry = entries[idx]; 19 + 20 + const target = entry.target as any; 21 + const listener = target.$onresize; 22 + 23 + if (listener) { 24 + listener(entry); 25 + } else { 26 + observer.unobserve(target); 27 + } 28 + } 29 + }; 30 + 31 + export const intersectionObserver = new IntersectionObserver(intersectionCallback); 32 + export const resizeObserver = new ResizeObserver(resizeCallback); 33 + 34 + declare module 'solid-js' { 35 + namespace JSX { 36 + interface ExplicitProperties { 37 + $onintersect: (entry: IntersectionObserverEntry) => void; 38 + $onresize: (entry: ResizeObserverEntry) => void; 39 + } 40 + } 41 + }
+35
src/lib/preferences/account.ts
··· 1 + import type { At } from '@mary/bluesky-client/lexicons'; 2 + 3 + import type { ModerationLabeler, ModerationPreferences } from '~/api/moderation'; 4 + 5 + export interface PerAccountPreferenceSchema { 6 + $version: 1; 7 + feeds: SavedFeed[]; 8 + language: LanguagePreferences; 9 + moderation: ModerationPreferences; 10 + } 11 + 12 + export interface ModerationLabelerPreferences { 13 + updated: number; 14 + definitions: Record<At.DID, ModerationLabeler>; 15 + } 16 + 17 + export interface SavedFeed { 18 + readonly uri: string; 19 + pinned: boolean; 20 + info: SavedFeedInfo; 21 + avatar?: string; 22 + indexedAt?: string; 23 + } 24 + 25 + export interface SavedFeedInfo { 26 + name: string; 27 + avatar?: string; 28 + acceptsInteraction?: boolean; 29 + indexedAt?: string; 30 + } 31 + 32 + export interface LanguagePreferences { 33 + /** Default language to use when composing a new post */ 34 + defaultPostLanguage: 'none' | 'system' | (string & {}); 35 + }
+8
src/lib/preferences/global.ts
··· 1 + export interface GlobalPreferenceSchema { 2 + $version: 1; 3 + ui: UiPreferences; 4 + } 5 + 6 + export interface UiPreferences { 7 + theme: 'system' | 'light' | 'dark'; 8 + }
+19
src/lib/preferences/sessions.ts
··· 1 + import type { AtpSessionData } from '@mary/bluesky-client'; 2 + import type { At } from '@mary/bluesky-client/lexicons'; 3 + 4 + export interface SessionPreferenceSchema { 5 + $version: 1; 6 + active: At.DID | undefined; 7 + accounts: AccountData[]; 8 + } 9 + 10 + export interface AccountData { 11 + /** Account DID */ 12 + readonly did: At.DID; 13 + /** Account's PDS or entryway (bsky.social instances) */ 14 + service: string; 15 + /** Account's session data, from `BskyAuth` */ 16 + session: AtpSessionData; 17 + /** Whether an account has a defined scope, from app passwords. */ 18 + scope: 'limited' | 'privileged' | undefined; 19 + }
+36
src/lib/signals/index.ts
··· 1 + import { type Accessor, type Setter, type SignalOptions, createSignal, untrack } from 'solid-js'; 2 + 3 + // Solid's createSignal is pretty clunky to carry around as it returns an array 4 + // that is expected to be destructured, this Signal class serves as a wrapper. 5 + 6 + export class Signal<T> { 7 + #get: Accessor<T>; 8 + #set: Setter<T>; 9 + 10 + constructor(value: T, options?: SignalOptions<T>) { 11 + const impl = createSignal(value, options); 12 + this.#get = impl[0]; 13 + this.#set = impl[1]; 14 + } 15 + 16 + get value() { 17 + return this.#get(); 18 + } 19 + 20 + set value(next: T) { 21 + // @ts-expect-error 22 + this.#set(typeof next === 'function' ? () => next : next); 23 + } 24 + 25 + peek() { 26 + return untrack(this.#get); 27 + } 28 + } 29 + 30 + export interface ReadonlySignal<T> extends Signal<T> { 31 + readonly value: T; 32 + } 33 + 34 + export const signal = <T>(value: T, options?: SignalOptions<T>) => { 35 + return new Signal(value, options); 36 + };
+51
src/lib/signals/storage.ts
··· 1 + import { createEffect } from 'solid-js'; 2 + import { createMutable, modifyMutable, reconcile, type StoreNode } from 'solid-js/store'; 3 + import { createEventListener } from '../hooks/event-listener'; 4 + 5 + type MigrateFn<T> = (version: number, prev: any) => T; 6 + 7 + /** Useful for knowing whether an effect occured by external writes */ 8 + export let isExternalWriting = false; 9 + 10 + const parse = <T>(raw: string | null, migrate: MigrateFn<T>): T => { 11 + if (raw !== null) { 12 + try { 13 + const persisted = JSON.parse(raw); 14 + 15 + if (persisted != null) { 16 + return migrate(persisted.$version || 0, persisted); 17 + } 18 + } catch {} 19 + } 20 + 21 + return migrate(0, null); 22 + }; 23 + 24 + export const createReactiveLocalStorage = <T extends StoreNode>(name: string, migrate: MigrateFn<T>) => { 25 + const mutable = createMutable<T>(parse(localStorage.getItem(name), migrate)); 26 + 27 + createEffect((inited) => { 28 + const json = JSON.stringify(mutable); 29 + 30 + if (inited && !isExternalWriting) { 31 + localStorage.setItem(name, json); 32 + } 33 + 34 + return true; 35 + }, false); 36 + 37 + createEventListener(window, 'storage', (ev) => { 38 + if (ev.key === name) { 39 + // Prevent our own effects from running, since this is already persisted. 40 + 41 + try { 42 + isExternalWriting = true; 43 + modifyMutable(mutable, reconcile(parse(ev.newValue, migrate), { merge: true })); 44 + } finally { 45 + isExternalWriting = false; 46 + } 47 + } 48 + }); 49 + 50 + return mutable; 51 + };
+70
src/lib/states/agent.tsx
··· 1 + import { createContext, createMemo, useContext, type JSX, type ParentProps } from 'solid-js'; 2 + 3 + import { BskyXRPC, type BskyAuth } from '@mary/bluesky-client'; 4 + import { QueryClient, QueryClientProvider } from '@mary/solid-query'; 5 + 6 + import { assert } from '../invariant'; 7 + import { memoizedOn } from '../misc'; 8 + import { createQueryPersister } from '../utils/query-storage'; 9 + 10 + import { useSession } from './session'; 11 + 12 + export interface AgentContext { 13 + rpc: BskyXRPC; 14 + auth: BskyAuth | null; 15 + persister: ReturnType<typeof createQueryPersister>; 16 + } 17 + 18 + const Context = createContext<AgentContext>(); 19 + 20 + export const AgentProvider = (props: ParentProps) => { 21 + const session = useSession(); 22 + 23 + const agent = createMemo((): AgentContext => { 24 + const currentAccount = session.currentAccount; 25 + 26 + if (currentAccount) { 27 + return { 28 + rpc: currentAccount.rpc, 29 + auth: currentAccount.auth, 30 + persister: createQueryPersister({ name: `queryCache-${currentAccount.did}` }), 31 + }; 32 + } 33 + 34 + return { 35 + rpc: new BskyXRPC({ service: 'https://public.api.bsky.app' }), 36 + auth: null, 37 + persister: createQueryPersister({ name: `queryCache-public` }), 38 + }; 39 + }); 40 + 41 + return memoizedOn(agent, ($agent) => { 42 + // Always use a new QueryClient when the agent changes, 43 + // this way we don't need to manually reset on switching accounts. 44 + const queryClient = new QueryClient({ 45 + defaultOptions: { 46 + queries: { 47 + gcTime: 2_000, 48 + staleTime: 30_000, 49 + refetchOnReconnect: false, 50 + refetchOnWindowFocus: false, 51 + retry: false, 52 + }, 53 + }, 54 + }); 55 + 56 + return ( 57 + <QueryClientProvider client={queryClient}> 58 + <Context.Provider value={$agent}>{props.children}</Context.Provider> 59 + </QueryClientProvider> 60 + ); 61 + }) as unknown as JSX.Element; 62 + }; 63 + 64 + // Safe to destructure when under <AgentProvider> 65 + export const useAgent = (): AgentContext => { 66 + const agent = useContext(Context); 67 + assert(agent !== undefined, `Expected useAgent to be called under <AgentProvider>`); 68 + 69 + return agent; 70 + };
+106
src/lib/states/moderation.tsx
··· 1 + import { createContext, createMemo, useContext, type ParentProps } from 'solid-js'; 2 + import { unwrap } from 'solid-js/store'; 3 + 4 + import type { AppBskyLabelerDefs, At } from '@mary/bluesky-client/lexicons'; 5 + import { createQueries } from '@mary/solid-query'; 6 + 7 + import { BLUESKY_MODERATION_DID } from '~/api/defaults'; 8 + import type { ModerationLabeler, ModerationOptions, ModerationPreferences } from '~/api/moderation'; 9 + import { interpretLabelerDefinition } from '~/api/moderation/labeler'; 10 + 11 + import { assert } from '~/lib/invariant'; 12 + import { mapDefined } from '~/lib/misc'; 13 + import { createBatchedFetch } from '~/lib/utils/batch-fetch'; 14 + 15 + import { useAgent } from './agent'; 16 + import { useSession } from './session'; 17 + 18 + type Labeler = AppBskyLabelerDefs.LabelerViewDetailed; 19 + 20 + const DEFAULT_MODERATION_PREFERENCES: ModerationPreferences = { 21 + hideReposts: [], 22 + keywords: [], 23 + labelers: { 24 + [BLUESKY_MODERATION_DID]: { 25 + redact: true, 26 + privileged: true, 27 + labels: {}, 28 + }, 29 + }, 30 + labels: {}, 31 + tempMutes: {}, 32 + }; 33 + 34 + const Context = createContext<() => ModerationOptions>(); 35 + 36 + export const ModerationProvider = (props: ParentProps) => { 37 + const { rpc, persister } = useAgent(); 38 + const { currentAccount } = useSession(); 39 + 40 + const modPreferences = createMemo(() => { 41 + if (!currentAccount) { 42 + return DEFAULT_MODERATION_PREFERENCES; 43 + } 44 + 45 + return currentAccount.preferences.moderation; 46 + }); 47 + 48 + const fetchLabeler = createBatchedFetch<At.DID, At.DID, ModerationLabeler>({ 49 + limit: 20, 50 + timeout: 1, 51 + idFromQuery: (query) => query, 52 + idFromData: (data) => data.did, 53 + async fetch(dids) { 54 + const { data } = await rpc.get('app.bsky.labeler.getServices', { 55 + params: { 56 + dids: dids, 57 + detailed: true, 58 + }, 59 + }); 60 + 61 + const views = data.views as Labeler[]; 62 + 63 + return views.map((view) => interpretLabelerDefinition(view)); 64 + }, 65 + }); 66 + 67 + const labelerDefs = createQueries(() => { 68 + return { 69 + queries: Object.keys(modPreferences().labelers).map((_did) => { 70 + const did = _did as At.DID; 71 + 72 + return { 73 + queryKey: ['labeler-definition', did], 74 + queryFn: () => fetchLabeler(did), 75 + staleTime: 21600000, // 6 hours 76 + gcTime: 86400000, // 24 hours 77 + refetchOnWindowFocus: true, 78 + persister: persister, 79 + }; 80 + }), 81 + combine(results) { 82 + const defs = mapDefined(results, (result) => result.data); 83 + const fields = Object.fromEntries(defs.map((def) => [def.did, def])); 84 + 85 + return fields as Record<At.DID, ModerationLabeler>; 86 + }, 87 + }; 88 + }); 89 + 90 + const modOptions = createMemo((prev?: ModerationOptions): ModerationOptions => { 91 + return { 92 + _filtersCache: prev?._filtersCache, 93 + preferences: unwrap(modPreferences()), 94 + labelerDefinitions: labelerDefs(), 95 + }; 96 + }); 97 + 98 + return <Context.Provider value={modOptions}>{props.children}</Context.Provider>; 99 + }; 100 + 101 + export const useModerationOptions = (): (() => ModerationOptions) => { 102 + const options = useContext(Context); 103 + assert(options !== undefined, `Expected useModerationOptions to be used under <ModerationProvider>`); 104 + 105 + return options; 106 + };
+258
src/lib/states/session.tsx
··· 1 + import { 2 + batch, 3 + createContext, 4 + createEffect, 5 + createRoot, 6 + createSignal, 7 + untrack, 8 + useContext, 9 + type ParentProps, 10 + } from 'solid-js'; 11 + import { unwrap } from 'solid-js/store'; 12 + 13 + import { BskyAuth, BskyMod, BskyXRPC, type AtpAccessJwt, type BskyAuthOptions } from '@mary/bluesky-client'; 14 + import type { At } from '@mary/bluesky-client/lexicons'; 15 + import { decodeJwt } from '@mary/bluesky-client/utils/jwt'; 16 + 17 + import { BLUESKY_MODERATION_DID } from '~/api/defaults'; 18 + 19 + import { globalEvents } from '~/globals/events'; 20 + import { sessions } from '~/globals/preferences'; 21 + 22 + import { makeAbortable } from '../hooks/abortable'; 23 + import type { PerAccountPreferenceSchema } from '../preferences/account'; 24 + import type { AccountData } from '../preferences/sessions'; 25 + import { createReactiveLocalStorage, isExternalWriting } from '../signals/storage'; 26 + 27 + import { assert } from '../invariant'; 28 + 29 + interface LoginOptions { 30 + service: string; 31 + identifier: string; 32 + password: string; 33 + authFactorToken?: string; 34 + } 35 + 36 + export interface CurrentAccountState { 37 + readonly did: At.DID; 38 + readonly data: AccountData; 39 + readonly preferences: PerAccountPreferenceSchema; 40 + 41 + readonly rpc: BskyXRPC; 42 + readonly auth: BskyAuth; 43 + readonly _cleanup: () => void; 44 + } 45 + 46 + export interface SessionContext { 47 + readonly currentAccount: CurrentAccountState | undefined; 48 + 49 + getAccounts(): AccountData[]; 50 + resumeSession(account: AccountData): Promise<void>; 51 + removeAccount(account: AccountData): void; 52 + 53 + login(opts: LoginOptions): Promise<void>; 54 + logout(): void; 55 + } 56 + 57 + const Context = createContext<SessionContext>(); 58 + 59 + export const SessionProvider = (props: ParentProps) => { 60 + const [getSignal] = makeAbortable(); 61 + const [state, _setState] = createSignal<CurrentAccountState>(); 62 + 63 + const replaceState = (next: CurrentAccountState | undefined) => { 64 + _setState((prev) => { 65 + prev?._cleanup(); 66 + return next; 67 + }); 68 + }; 69 + 70 + const createAccountState = (account: AccountData, rpc: BskyXRPC, auth: BskyAuth): CurrentAccountState => { 71 + return createRoot((cleanup): CurrentAccountState => { 72 + const preferences = createAccountPreferences(account.did); 73 + const mod = new BskyMod(rpc); 74 + 75 + createEffect(() => { 76 + const entries = Object.entries(preferences.moderation.labelers); 77 + mod.labelers = entries.map(([did, info]) => ({ did: did as At.DID, redact: info.redact })); 78 + }); 79 + 80 + return { 81 + did: account.did, 82 + data: account, 83 + preferences: preferences, 84 + 85 + auth: auth, 86 + rpc: rpc, 87 + _cleanup: cleanup, 88 + }; 89 + }, null); 90 + }; 91 + 92 + const getAuthOptions = (): BskyAuthOptions => { 93 + return { 94 + onExpired() { 95 + globalEvents.emit('sessionexpired'); 96 + }, 97 + onRefresh(session) { 98 + const did = session.did; 99 + const existing = sessions.accounts.find((acc) => acc.did === did); 100 + 101 + if (existing) { 102 + batch(() => Object.assign(existing.session, session)); 103 + } 104 + }, 105 + }; 106 + }; 107 + 108 + const context: SessionContext = { 109 + get currentAccount() { 110 + return state(); 111 + }, 112 + 113 + getAccounts(): AccountData[] { 114 + return sessions.accounts; 115 + }, 116 + async resumeSession(account: AccountData): Promise<void> { 117 + const signal = getSignal(); 118 + const session = unwrap(account.session); 119 + 120 + const rpc = new BskyXRPC({ service: account.service }); 121 + const auth = new BskyAuth(rpc, getAuthOptions()); 122 + 123 + await auth.resume(session); 124 + signal.throwIfAborted(); 125 + 126 + batch(() => { 127 + sessions.active = account.did; 128 + sessions.accounts = [account, ...sessions.accounts.filter((acc) => acc.did !== session.did)]; 129 + replaceState(createAccountState(account, rpc, auth)); 130 + }); 131 + }, 132 + removeAccount(account: AccountData): void { 133 + const $state = untrack(state); 134 + const isLoggedIn = $state !== undefined && $state.did === account.did; 135 + 136 + batch(() => { 137 + if (isLoggedIn) { 138 + replaceState(undefined); 139 + } 140 + }); 141 + }, 142 + 143 + async login(opts: LoginOptions): Promise<void> { 144 + const signal = getSignal(); 145 + 146 + const rpc = new BskyXRPC({ service: opts.service }); 147 + const auth = new BskyAuth(rpc, getAuthOptions()); 148 + 149 + await auth.login({ identifier: opts.identifier, password: opts.password, code: opts.authFactorToken }); 150 + signal.throwIfAborted(); 151 + 152 + const session = auth.session!; 153 + const sessionJwt = decodeJwt(session.accessJwt) as AtpAccessJwt; 154 + 155 + const scope = sessionJwt.scope; 156 + let accountScope: AccountData['scope']; 157 + if (scope === 'com.atproto.appPass') { 158 + accountScope = 'limited'; 159 + } else if (scope === 'com.atproto.appPassPrivileged') { 160 + accountScope = 'privileged'; 161 + } 162 + 163 + const account: AccountData = { 164 + did: session.did, 165 + service: opts.service, 166 + session: session, 167 + scope: accountScope, 168 + }; 169 + 170 + batch(() => { 171 + sessions.active = account.did; 172 + sessions.accounts = [account, ...sessions.accounts.filter((acc) => acc.did !== session.did)]; 173 + replaceState(createAccountState(account, rpc, auth)); 174 + }); 175 + }, 176 + logout(): void { 177 + const $state = untrack(state); 178 + if ($state !== undefined) { 179 + this.removeAccount($state.data); 180 + } 181 + }, 182 + }; 183 + 184 + createEffect(() => { 185 + const active = sessions.active; 186 + const activeAccount = active && sessions.accounts.find((acc) => acc.did === active); 187 + 188 + // Only run this on external changes 189 + if (isExternalWriting) { 190 + const untrackedState = untrack(state); 191 + 192 + if (active) { 193 + if (active !== untrackedState?.did) { 194 + // Current active account doesn't match what we have 195 + 196 + // Still logged in, so log out of this one 197 + if (untrackedState) { 198 + replaceState(undefined); 199 + } 200 + 201 + // Account data exists, try to login as that account 202 + if (activeAccount) { 203 + context.resumeSession(activeAccount); 204 + } 205 + } else if (activeAccount) { 206 + // It's likely that this external write occured due to session changes 207 + // so update it to whatever it is now 208 + untrackedState.auth.session = unwrap(activeAccount.session); 209 + } 210 + } else if (untrackedState) { 211 + // No active account yet we have a session, log out 212 + replaceState(undefined); 213 + } 214 + } 215 + }); 216 + 217 + return <Context.Provider value={context}>{props.children}</Context.Provider>; 218 + }; 219 + 220 + // Safe to destructure when under <AgentProvider> 221 + export const useSession = (): SessionContext => { 222 + const session = useContext(Context); 223 + assert(session !== undefined, `Expected useSession to be called under <SessionProvider>`); 224 + 225 + return session; 226 + }; 227 + 228 + const createAccountPreferences = (did: At.DID) => { 229 + const key = `account-${did}`; 230 + return createReactiveLocalStorage<PerAccountPreferenceSchema>(key, (version, prev) => { 231 + if (version === 0) { 232 + const obj: PerAccountPreferenceSchema = { 233 + $version: 1, 234 + feeds: [], 235 + language: { 236 + defaultPostLanguage: 'system', 237 + }, 238 + moderation: { 239 + hideReposts: [], 240 + keywords: [], 241 + labelers: { 242 + [BLUESKY_MODERATION_DID]: { 243 + redact: true, 244 + privileged: false, 245 + labels: {}, 246 + }, 247 + }, 248 + labels: {}, 249 + tempMutes: {}, 250 + }, 251 + }; 252 + 253 + return obj; 254 + } 255 + 256 + return prev; 257 + }); 258 + };
+52
src/lib/states/theme.tsx
··· 1 + import { createContext, createRenderEffect, createSignal, useContext, type ParentProps } from 'solid-js'; 2 + 3 + import * as preferences from '~/globals/preferences'; 4 + 5 + import { useMediaQuery } from '../hooks/media-query'; 6 + import { assert } from '../invariant'; 7 + 8 + type Theme = 'light' | 'dark'; 9 + 10 + export interface ThemeContext { 11 + readonly currentTheme: Theme; 12 + } 13 + 14 + const Context = createContext<ThemeContext>(); 15 + 16 + export const useTheme = (): ThemeContext => { 17 + const context = useContext(Context); 18 + assert(context !== undefined, `Expected useTheme to be used under ThemeProvider`); 19 + 20 + return context; 21 + }; 22 + 23 + export const ThemeProvider = (props: ParentProps) => { 24 + const [theme, setTheme] = createSignal<Theme>('light'); 25 + 26 + const context: ThemeContext = { 27 + get currentTheme() { 28 + return theme(); 29 + }, 30 + }; 31 + 32 + createRenderEffect(() => { 33 + const theme = preferences.global.ui.theme; 34 + 35 + if (theme === 'system') { 36 + const isDark = useMediaQuery('(prefers-color-scheme: dark)'); 37 + createRenderEffect(() => setTheme(!isDark() ? 'light' : 'dark')); 38 + } else { 39 + setTheme(theme); 40 + } 41 + }); 42 + 43 + createRenderEffect(() => { 44 + const cl = document.documentElement.classList; 45 + const $theme = theme(); 46 + 47 + cl.toggle('theme-light', $theme === 'light'); 48 + cl.toggle('theme-dark', $theme === 'dark'); 49 + }); 50 + 51 + return <Context.Provider value={context}>{props.children}</Context.Provider>; 52 + };
+16
src/lib/styles.ts
··· 1 + export const clsx = (classes: (string | false | null | undefined | 0)[]): string => { 2 + var result = ''; 3 + var subsequent = false; 4 + var temp: string | false | null | undefined | 0; 5 + 6 + for (var idx = 0, len = classes.length; idx < len; idx++) { 7 + if ((temp = classes[idx])) { 8 + subsequent && (result += ' '); 9 + result += temp; 10 + 11 + subsequent = true; 12 + } 13 + } 14 + 15 + return result; 16 + };
+106
src/lib/utils/batch-fetch.ts
··· 1 + // we would sometimes rely on fetching multiple individual posts, and it would 2 + // be preferrable if it can be batched. 3 + 4 + type Promisable<T> = T | Promise<T>; 5 + 6 + export type QueryId = string | number; 7 + 8 + export interface BatchedFetchOptions<Query, Id extends QueryId, Data> { 9 + limit: number; 10 + timeout: number; 11 + fetch: (queries: Query[]) => Promisable<Data[]>; 12 + key?: (query: Query) => string | number; 13 + idFromQuery: (query: Query) => Id; 14 + idFromData: (data: Data) => Id; 15 + } 16 + 17 + interface BatchedFetchMap<Query, Id, Data> { 18 + key: string | number | undefined; 19 + timeout: any; 20 + queries: Query[]; 21 + pending: Map<Id, PromiseWithResolvers<Data>>; 22 + } 23 + 24 + export class ResourceMissingError extends Error { 25 + name = 'ResourceMissingError'; 26 + } 27 + 28 + /*#__NO_SIDE_EFFECTS__*/ 29 + export const createBatchedFetch = <Query, Id extends QueryId, Data>( 30 + options: BatchedFetchOptions<Query, Id, Data>, 31 + ) => { 32 + const { limit, timeout, fetch, key: _key, idFromData, idFromQuery } = options; 33 + 34 + let curr: BatchedFetchMap<Query, Id, Data> | undefined; 35 + 36 + return (query: Query): Promise<Data> => { 37 + const id = idFromQuery(query); 38 + const key = _key?.(query); 39 + 40 + let map = curr; 41 + 42 + if (!map || map.queries.length >= limit || map.key !== key) { 43 + map = curr = { 44 + key, 45 + timeout: undefined, 46 + queries: [], 47 + pending: new Map(), 48 + }; 49 + } 50 + 51 + let deferred = map.pending.get(id); 52 + 53 + if (!deferred) { 54 + deferred = Promise.withResolvers<Data>(); 55 + 56 + map.queries.push(query); 57 + map.pending.set(id, deferred); 58 + } 59 + 60 + clearTimeout(map.timeout); 61 + 62 + map.timeout = setTimeout(() => { 63 + if (curr === map) { 64 + curr = undefined; 65 + } 66 + 67 + perform(map!, fetch, idFromData); 68 + }, timeout); 69 + 70 + return deferred.promise; 71 + }; 72 + }; 73 + 74 + const perform = async <Query, Id extends QueryId, Data>( 75 + map: BatchedFetchMap<Query, Id, Data>, 76 + fetch: (queries: Query[]) => Promisable<Data[]>, 77 + idFromData: (data: Data) => Id, 78 + ) => { 79 + const queries = map.queries; 80 + const pending = map.pending; 81 + 82 + let errored = false; 83 + 84 + try { 85 + const dataset = await fetch(queries); 86 + 87 + for (const data of dataset) { 88 + const id = idFromData(data); 89 + const deferred = pending.get(id); 90 + 91 + deferred?.resolve(data); 92 + } 93 + } catch (error) { 94 + errored = true; 95 + 96 + for (const deferred of pending.values()) { 97 + deferred.reject(error); 98 + } 99 + } finally { 100 + if (!errored) { 101 + for (const deferred of pending.values()) { 102 + deferred.reject(new ResourceMissingError()); 103 + } 104 + } 105 + } 106 + };
+122
src/lib/utils/query-storage.ts
··· 1 + import type { Query, QueryFunctionContext, QueryKey } from '@mary/solid-query'; 2 + 3 + import { createEventListener } from '../hooks/event-listener'; 4 + 5 + export interface QueryPersistOptions { 6 + name: string; 7 + key?: string; 8 + maxAge?: number; 9 + } 10 + 11 + export const createQueryPersister = ({ 12 + name, 13 + key = '', 14 + maxAge = 1000 * 60 * 60 * 24, // 24 hours 15 + }: QueryPersistOptions) => { 16 + let storage: QueryPersistenceSchema = {}; 17 + 18 + const write = () => { 19 + localStorage.setItem(name, JSON.stringify(storage)); 20 + }; 21 + 22 + const read = (raw: string | null) => { 23 + storage = JSON.parse(raw ?? '{}') as any; 24 + 25 + let now = Date.now(); 26 + let written = false; 27 + 28 + for (const queryHash in storage) { 29 + const persisted = storage[queryHash]; 30 + const persistedKey = persisted.key; 31 + 32 + const age = now - persisted.dataUpdatedAt; 33 + 34 + const expired = age > maxAge; 35 + const mismatch = persistedKey !== key; 36 + 37 + if (expired || mismatch) { 38 + delete storage[queryHash]; 39 + written = true; 40 + } 41 + } 42 + 43 + if (written) { 44 + write(); 45 + } 46 + }; 47 + 48 + const persist = async <T, TQueryKey extends QueryKey>( 49 + queryFn: (context: QueryFunctionContext<TQueryKey>) => T | Promise<T>, 50 + context: QueryFunctionContext<TQueryKey>, 51 + query: Query, 52 + ) => { 53 + const hash = query.queryHash; 54 + 55 + { 56 + const persisted = storage[hash]; 57 + 58 + if (persisted) { 59 + const persistedKey = persisted.key; 60 + const dataUpdatedAt = persisted.dataUpdatedAt; 61 + 62 + const age = Date.now() - dataUpdatedAt; 63 + 64 + const expired = age > maxAge; 65 + const mismatch = persistedKey !== key; 66 + 67 + if (expired || mismatch) { 68 + delete storage[hash]; 69 + write(); 70 + } else if (query.state.data === undefined || dataUpdatedAt > query.state.dataUpdatedAt) { 71 + // Just after restoring we want to get fresh data from the server if it's stale 72 + setTimeout(() => { 73 + // Set proper updatedAt, since resolving in the first pass overrides those values 74 + query.setState({ dataUpdatedAt: dataUpdatedAt }); 75 + 76 + if (query.isStale()) { 77 + query.fetch(); 78 + } 79 + }, 0); 80 + 81 + return persisted.data as T; 82 + } 83 + } 84 + } 85 + 86 + const result = await queryFn(context); 87 + 88 + { 89 + setTimeout(() => { 90 + const state = query.state; 91 + 92 + storage[hash] = { 93 + key: key, 94 + data: state.data, 95 + dataUpdatedAt: state.dataUpdatedAt, 96 + }; 97 + 98 + write(); 99 + }, 0); 100 + } 101 + 102 + return result; 103 + }; 104 + 105 + read(localStorage.getItem(name)); 106 + 107 + createEventListener(window, 'storage', (ev) => { 108 + if (ev.key === name) { 109 + read(ev.newValue); 110 + } 111 + }); 112 + 113 + return persist; 114 + }; 115 + 116 + interface QueryPersistenceSchema { 117 + [queryHash: string]: { 118 + key: string; 119 + data: unknown; 120 + dataUpdatedAt: number; 121 + }; 122 + }
+83
src/main.tsx
··· 1 + /* @refresh reload */ 2 + 3 + import './styles/app.css'; 4 + 5 + import { createSignal, onMount, type JSX } from 'solid-js'; 6 + import { render } from 'solid-js/web'; 7 + 8 + import * as navigation from './globals/navigation'; 9 + import * as preferences from './globals/preferences'; 10 + 11 + import { memoizedOn } from './lib/misc'; 12 + import { configureRouter } from './lib/navigation/router'; 13 + 14 + import type { AccountData } from './lib/preferences/sessions'; 15 + 16 + import { AgentProvider } from './lib/states/agent'; 17 + import { ModerationProvider } from './lib/states/moderation'; 18 + import { SessionProvider, useSession } from './lib/states/session'; 19 + import { ThemeProvider } from './lib/states/theme'; 20 + 21 + import ModalRenderer from './components/main/modal-renderer'; 22 + import routes from './routes'; 23 + import Shell from './shell'; 24 + 25 + // Configure routing 26 + configureRouter({ 27 + history: navigation.history, 28 + logger: navigation.logger, 29 + routes: routes, 30 + }); 31 + 32 + const InnerApp = () => { 33 + const [ready, setReady] = createSignal(false); 34 + const session = useSession(); 35 + 36 + onMount(() => { 37 + const resumeAccount = async (account: AccountData | undefined) => { 38 + try { 39 + if (account) { 40 + await session.resumeSession(account); 41 + } 42 + } finally { 43 + setReady(true); 44 + } 45 + }; 46 + 47 + { 48 + const { active, accounts } = preferences.sessions; 49 + const account = active && accounts.find((acc) => acc.did === active); 50 + 51 + resumeAccount(account); 52 + } 53 + }); 54 + 55 + return memoizedOn(ready, ($ready) => { 56 + if (!$ready) { 57 + return; 58 + } 59 + 60 + return ( 61 + <AgentProvider> 62 + {/* Anything under <AgentProvider> gets remounted on account changes */} 63 + <ModerationProvider> 64 + <Shell /> 65 + <ModalRenderer /> 66 + </ModerationProvider> 67 + </AgentProvider> 68 + ); 69 + }) as unknown as JSX.Element; 70 + }; 71 + 72 + const App = () => { 73 + return ( 74 + <ThemeProvider> 75 + <SessionProvider> 76 + <InnerApp /> 77 + </SessionProvider> 78 + </ThemeProvider> 79 + ); 80 + }; 81 + 82 + // Render the app 83 + render(App, document.body);
+57
src/routes.ts
··· 1 + import { lazy } from 'solid-js'; 2 + import type { RouteDefinition } from './lib/navigation/router'; 3 + 4 + const routes: RouteDefinition[] = [ 5 + { 6 + path: '/', 7 + component: lazy(() => import('./views/main/home')), 8 + meta: { 9 + name: 'Home', 10 + main: true, 11 + // public: true, 12 + }, 13 + }, 14 + { 15 + path: '/search', 16 + component: lazy(() => import('./views/main/search')), 17 + meta: { 18 + name: 'Search', 19 + main: true, 20 + // public: true, 21 + }, 22 + }, 23 + { 24 + path: '/notifications', 25 + component: lazy(() => import('./views/main/notifications')), 26 + meta: { 27 + name: 'Notifications', 28 + main: true, 29 + }, 30 + }, 31 + { 32 + path: '/messages', 33 + component: lazy(() => import('./views/main/messages')), 34 + meta: { 35 + name: 'Messages', 36 + main: true, 37 + }, 38 + }, 39 + { 40 + path: '/feeds', 41 + component: lazy(() => import('./views/main/feeds')), 42 + meta: { 43 + name: 'Feeds', 44 + main: true, 45 + }, 46 + }, 47 + 48 + { 49 + path: '*', 50 + component: lazy(() => import('./views/not-found')), 51 + meta: { 52 + public: true, 53 + }, 54 + }, 55 + ]; 56 + 57 + export default routes;
+169
src/shell.tsx
··· 1 + import { Suspense, lazy, type Accessor, type Component, type ComponentProps } from 'solid-js'; 2 + 3 + import { globalEvents } from './globals/events'; 4 + import { hasModals } from './globals/modals'; 5 + import { history } from './globals/navigation'; 6 + 7 + import { RouterView, useMatchedRoute, type MatchedRouteState } from './lib/navigation/router'; 8 + import { useSession } from './lib/states/session'; 9 + 10 + import BellOutlinedIcon from './components/icons-central/bell-outline'; 11 + import BellSolidIcon from './components/icons-central/bell-solid'; 12 + import HashtagOutlinedIcon from './components/icons-central/hashtag-outline'; 13 + import HomeOutlinedIcon from './components/icons-central/home-outline'; 14 + import HomeSolidIcon from './components/icons-central/home-solid'; 15 + import MagnifyingGlassOutlinedIcon from './components/icons-central/magnifying-glass-outline'; 16 + import MailOutlinedIcon from './components/icons-central/mail-outline'; 17 + import MailSolidIcon from './components/icons-central/mail-solid'; 18 + 19 + const SignedOutView = lazy(() => import('./views/_signed-out')); 20 + 21 + const Shell = () => { 22 + const { currentAccount } = useSession(); 23 + 24 + // Will always match because we've set a 404 handler. 25 + const route = useMatchedRoute() as Accessor<MatchedRouteState>; 26 + 27 + return ( 28 + <div 29 + inert={hasModals()} 30 + class="relative z-0 mx-auto flex min-h-screen max-w-md flex-col-reverse border-c-contrast-200 sm:border-x" 31 + > 32 + {!!(currentAccount && route().def.meta?.main) && <NavBar route={route} />} 33 + 34 + <div class="z-0 flex min-h-0 grow flex-col overflow-clip"> 35 + <RouterView 36 + render={({ def }) => { 37 + return ( 38 + <Suspense 39 + children={(() => { 40 + if (!currentAccount && !def.meta?.public) { 41 + return <SignedOutView />; 42 + } 43 + 44 + return <def.component />; 45 + })()} 46 + /> 47 + ); 48 + }} 49 + /> 50 + </div> 51 + </div> 52 + ); 53 + }; 54 + 55 + export default Shell; 56 + 57 + const enum MainTabs { 58 + HOME = 'Home', 59 + SEARCH = 'Search', 60 + NOTIFICATIONS = 'Notifications', 61 + MESSAGES = 'Messages', 62 + FEEDS = 'Feeds', 63 + } 64 + 65 + const MainTabsRoutes = { 66 + [MainTabs.HOME]: '/', 67 + [MainTabs.SEARCH]: '/search', 68 + [MainTabs.NOTIFICATIONS]: '/notifications', 69 + [MainTabs.MESSAGES]: '/messages', 70 + [MainTabs.FEEDS]: '/feeds', 71 + }; 72 + 73 + const NavBar = ({ route }: { route: Accessor<MatchedRouteState> }) => { 74 + const active = () => route().def.meta?.name; 75 + 76 + const bindClick = (to: MainTabs) => { 77 + return () => { 78 + const from = active(); 79 + 80 + if (from === to) { 81 + window.scrollTo({ top: 0, behavior: 'smooth' }); 82 + globalEvents.emit('softreset'); 83 + return; 84 + } 85 + 86 + const fromHome = !!(history.location.state as any)?.fromHome; 87 + const href = MainTabsRoutes[to]; 88 + 89 + if (to === MainTabs.HOME && fromHome) { 90 + history.back(); 91 + return; 92 + } 93 + 94 + history.navigate(href, { 95 + replace: from !== MainTabs.HOME, 96 + state: { 97 + // inherit `fromHome` state 98 + fromHome: fromHome || from === MainTabs.HOME, 99 + }, 100 + }); 101 + }; 102 + }; 103 + 104 + return ( 105 + <> 106 + <div class="sticky bottom-0 z-1 flex h-13 w-full max-w-md shrink-0 items-stretch border-t border-c-contrast-200 bg-c-contrast-0"> 107 + <NavItem 108 + label="Home" 109 + active={active() === MainTabs.HOME} 110 + onClick={bindClick(MainTabs.HOME)} 111 + icon={HomeOutlinedIcon} 112 + iconActive={HomeSolidIcon} 113 + /> 114 + <NavItem 115 + label="Search" 116 + active={active() === MainTabs.SEARCH} 117 + onClick={bindClick(MainTabs.SEARCH)} 118 + icon={MagnifyingGlassOutlinedIcon} 119 + /> 120 + <NavItem 121 + label="Notifications" 122 + active={active() === MainTabs.NOTIFICATIONS} 123 + onClick={bindClick(MainTabs.NOTIFICATIONS)} 124 + icon={BellOutlinedIcon} 125 + iconActive={BellSolidIcon} 126 + /> 127 + <NavItem 128 + label="Direct Messages" 129 + active={active() === MainTabs.MESSAGES} 130 + onClick={bindClick(MainTabs.MESSAGES)} 131 + icon={MailOutlinedIcon} 132 + iconActive={MailSolidIcon} 133 + /> 134 + <NavItem 135 + label="Feeds" 136 + active={active() === MainTabs.FEEDS} 137 + onClick={bindClick(MainTabs.FEEDS)} 138 + icon={HashtagOutlinedIcon} 139 + /> 140 + </div> 141 + </> 142 + ); 143 + }; 144 + 145 + type IconComponent = Component<ComponentProps<'svg'>>; 146 + 147 + interface NavItemProps { 148 + active?: boolean; 149 + label: string; 150 + icon: IconComponent; 151 + iconActive?: IconComponent; 152 + onClick?: () => void; 153 + } 154 + 155 + const NavItem = (props: NavItemProps) => { 156 + const InactiveIcon = props.icon; 157 + const ActiveIcon = props.iconActive; 158 + 159 + return ( 160 + <button title={props.label} onClick={props.onClick} class="grid grow basis-0 place-items-center"> 161 + {(() => { 162 + const active = props.active; 163 + 164 + const Icon = active && ActiveIcon ? ActiveIcon : InactiveIcon; 165 + return <Icon class={`text-2xl` + (active && !ActiveIcon ? ` stroke-3 stroke-c-contrast-900` : ``)} />; 166 + })()} 167 + </button> 168 + ); 169 + };
+214
src/styles/app.css
··· 1 + @tailwind base; 2 + @tailwind components; 3 + @tailwind utilities; 4 + 5 + @layer base { 6 + body { 7 + @apply overflow-y-scroll bg-c-contrast-0 text-c-contrast-900; 8 + } 9 + 10 + body:has(div[data-modal]) { 11 + @apply overflow-hidden pr-[--sb-width]; 12 + } 13 + 14 + ::selection { 15 + @apply bg-c-primary-600 text-c-white; 16 + } 17 + 18 + .a-dialog-desktop { 19 + max-height: min(100vh - 88px, 652px); 20 + } 21 + } 22 + 23 + @layer base { 24 + :root { 25 + --t-black: 0 0% 0%; 26 + 27 + --t-gray-0: 0 0% 100%; 28 + --t-gray-25: 0 0% 95.3%; 29 + --t-gray-50: 0 0% 90.6%; 30 + --t-gray-100: 0 0% 85.9%; 31 + --t-gray-200: 0 0% 81.2%; 32 + --t-gray-300: 0 0% 71.8%; 33 + --t-gray-400: 0 0% 62.4%; 34 + --t-gray-500: 0 0% 53%; 35 + --t-gray-600: 0 0% 43.6%; 36 + --t-gray-700: 0 0% 34.2%; 37 + --t-gray-800: 0 0% 24.8%; 38 + --t-gray-900: 0 0% 20.1%; 39 + --t-gray-950: 0 0% 15.4%; 40 + --t-gray-975: 0 0% 10.7%; 41 + --t-gray-1000: 0 0% 6%; 42 + 43 + --t-blue-25: 211 99% 97%; 44 + --t-blue-50: 211 99% 95%; 45 + --t-blue-100: 211 99% 90%; 46 + --t-blue-200: 211 99% 80%; 47 + --t-blue-300: 211 99% 70%; 48 + --t-blue-400: 211 99% 60%; 49 + --t-blue-500: 211 99% 53%; 50 + --t-blue-600: 211 99% 42%; 51 + --t-blue-700: 211 99% 34%; 52 + --t-blue-800: 211 99% 26%; 53 + --t-blue-900: 211 99% 18%; 54 + --t-blue-950: 211 99% 10%; 55 + --t-blue-975: 211 99% 7%; 56 + --t-blue-low: 214.99 13% 44%; 57 + 58 + --t-green-25: 152 82% 97%; 59 + --t-green-50: 152 82% 95%; 60 + --t-green-100: 152 82% 90%; 61 + --t-green-200: 152 82% 80%; 62 + --t-green-300: 152 82% 70%; 63 + --t-green-400: 152 82% 60%; 64 + --t-green-500: 152 82% 50%; 65 + --t-green-600: 152 82% 42%; 66 + --t-green-700: 152 82% 34%; 67 + --t-green-800: 152 82% 26%; 68 + --t-green-900: 152 82% 18%; 69 + --t-green-950: 152 82% 10%; 70 + --t-green-975: 152 82% 7%; 71 + 72 + --t-red-25: 346 91% 97%; 73 + --t-red-50: 346 91% 95%; 74 + --t-red-100: 346 91% 90%; 75 + --t-red-200: 346 91% 80%; 76 + --t-red-300: 346 91% 70%; 77 + --t-red-400: 346 91% 60%; 78 + --t-red-500: 346 91% 50%; 79 + --t-red-600: 346 91% 42%; 80 + --t-red-700: 346 91% 34%; 81 + --t-red-800: 346 91% 26%; 82 + --t-red-900: 346 91% 18%; 83 + --t-red-950: 346 91% 10%; 84 + --t-red-975: 346 91% 7%; 85 + } 86 + 87 + .theme-light { 88 + color-scheme: light; 89 + 90 + --c-white: var(--t-gray-0); 91 + --c-black: var(--t-gray-1000); 92 + 93 + --c-contrast-0: var(--t-gray-0); 94 + --c-contrast-25: var(--t-gray-25); 95 + --c-contrast-50: var(--t-gray-50); 96 + --c-contrast-100: var(--t-gray-100); 97 + --c-contrast-200: var(--t-gray-200); 98 + --c-contrast-300: var(--t-gray-300); 99 + --c-contrast-400: var(--t-gray-400); 100 + --c-contrast-500: var(--t-gray-500); 101 + --c-contrast-600: var(--t-gray-600); 102 + --c-contrast-700: var(--t-gray-700); 103 + --c-contrast-800: var(--t-gray-800); 104 + --c-contrast-900: var(--t-gray-900); 105 + --c-contrast-950: var(--t-gray-950); 106 + --c-contrast-975: var(--t-gray-975); 107 + 108 + --c-primary-25: var(--t-blue-25); 109 + --c-primary-50: var(--t-blue-50); 110 + --c-primary-100: var(--t-blue-100); 111 + --c-primary-200: var(--t-blue-200); 112 + --c-primary-300: var(--t-blue-300); 113 + --c-primary-400: var(--t-blue-400); 114 + --c-primary-500: var(--t-blue-500); 115 + --c-primary-600: var(--t-blue-600); 116 + --c-primary-700: var(--t-blue-700); 117 + --c-primary-800: var(--t-blue-800); 118 + --c-primary-900: var(--t-blue-900); 119 + --c-primary-950: var(--t-blue-950); 120 + --c-primary-975: var(--t-blue-975); 121 + 122 + --c-positive-25: var(--t-green-25); 123 + --c-positive-50: var(--t-green-50); 124 + --c-positive-100: var(--t-green-100); 125 + --c-positive-200: var(--t-green-200); 126 + --c-positive-300: var(--t-green-300); 127 + --c-positive-400: var(--t-green-400); 128 + --c-positive-500: var(--t-green-500); 129 + --c-positive-600: var(--t-green-600); 130 + --c-positive-700: var(--t-green-700); 131 + --c-positive-800: var(--t-green-800); 132 + --c-positive-900: var(--t-green-900); 133 + --c-positive-950: var(--t-green-950); 134 + --c-positive-975: var(--t-green-975); 135 + 136 + --c-negative-25: var(--t-red-25); 137 + --c-negative-50: var(--t-red-50); 138 + --c-negative-100: var(--t-red-100); 139 + --c-negative-200: var(--t-red-200); 140 + --c-negative-300: var(--t-red-300); 141 + --c-negative-400: var(--t-red-400); 142 + --c-negative-500: var(--t-red-500); 143 + --c-negative-600: var(--t-red-600); 144 + --c-negative-700: var(--t-red-700); 145 + --c-negative-800: var(--t-red-800); 146 + --c-negative-900: var(--t-red-900); 147 + --c-negative-950: var(--t-red-950); 148 + --c-negative-975: var(--t-red-975); 149 + } 150 + 151 + .theme-dark { 152 + color-scheme: dark; 153 + 154 + --c-white: var(--t-gray-0); 155 + --c-black: var(--t-black); 156 + 157 + --c-contrast-0: var(--t-black); 158 + --c-contrast-25: var(--t-gray-1000); 159 + --c-contrast-50: var(--t-gray-975); 160 + --c-contrast-100: var(--t-gray-950); 161 + --c-contrast-200: var(--t-gray-900); 162 + --c-contrast-300: var(--t-gray-800); 163 + --c-contrast-400: var(--t-gray-700); 164 + --c-contrast-500: var(--t-gray-600); 165 + --c-contrast-600: var(--t-gray-500); 166 + --c-contrast-700: var(--t-gray-400); 167 + --c-contrast-800: var(--t-gray-300); 168 + --c-contrast-900: var(--t-gray-200); 169 + --c-contrast-950: var(--t-gray-100); 170 + --c-contrast-975: var(--t-gray-50); 171 + 172 + --c-primary-25: var(--t-blue-25); 173 + --c-primary-50: var(--t-blue-50); 174 + --c-primary-100: var(--t-blue-100); 175 + --c-primary-200: var(--t-blue-200); 176 + --c-primary-300: var(--t-blue-300); 177 + --c-primary-400: var(--t-blue-400); 178 + --c-primary-500: var(--t-blue-500); 179 + --c-primary-600: var(--t-blue-600); 180 + --c-primary-700: var(--t-blue-700); 181 + --c-primary-800: var(--t-blue-800); 182 + --c-primary-900: var(--t-blue-900); 183 + --c-primary-950: var(--t-blue-950); 184 + --c-primary-975: var(--t-blue-975); 185 + 186 + --c-positive-25: var(--t-green-25); 187 + --c-positive-50: var(--t-green-50); 188 + --c-positive-100: var(--t-green-100); 189 + --c-positive-200: var(--t-green-200); 190 + --c-positive-300: var(--t-green-300); 191 + --c-positive-400: var(--t-green-400); 192 + --c-positive-500: var(--t-green-500); 193 + --c-positive-600: var(--t-green-600); 194 + --c-positive-700: var(--t-green-700); 195 + --c-positive-800: var(--t-green-800); 196 + --c-positive-900: var(--t-green-900); 197 + --c-positive-950: var(--t-green-950); 198 + --c-positive-975: var(--t-green-975); 199 + 200 + --c-negative-25: var(--t-red-25); 201 + --c-negative-50: var(--t-red-50); 202 + --c-negative-100: var(--t-red-100); 203 + --c-negative-200: var(--t-red-200); 204 + --c-negative-300: var(--t-red-300); 205 + --c-negative-400: var(--t-red-400); 206 + --c-negative-500: var(--t-red-500); 207 + --c-negative-600: var(--t-red-600); 208 + --c-negative-700: var(--t-red-700); 209 + --c-negative-800: var(--t-red-800); 210 + --c-negative-900: var(--t-red-900); 211 + --c-negative-950: var(--t-red-950); 212 + --c-negative-975: var(--t-red-975); 213 + } 214 + }
+31
src/views/_signed-out/index.tsx
··· 1 + import Button from '~/components/button'; 2 + import { openModal } from '~/globals/modals'; 3 + 4 + import SignInDialogLazy from '~/components/main/sign-in-dialog-lazy'; 5 + 6 + const SignedOutPage = () => { 7 + return ( 8 + <> 9 + <div class="flex grow flex-col items-center justify-center gap-1"> 10 + <p class="text-lg">here's where i would've put a logo</p> 11 + <p class="text-2xl font-medium">IF I HAD ONE</p> 12 + </div> 13 + <div class="flex shrink-0 flex-col gap-4 p-6"> 14 + <Button 15 + onClick={() => { 16 + openModal(() => <SignInDialogLazy />); 17 + }} 18 + variant="primary" 19 + size="md" 20 + > 21 + Sign in 22 + </Button> 23 + <Button size="md" disabled> 24 + Create account 25 + </Button> 26 + </div> 27 + </> 28 + ); 29 + }; 30 + 31 + export default SignedOutPage;
+5
src/views/main/feeds.tsx
··· 1 + const FeedsPage = () => { 2 + return <div>feeds</div>; 3 + }; 4 + 5 + export default FeedsPage;
+63
src/views/main/home.tsx
··· 1 + import { useTimelineQuery } from '~/api/queries/timeline'; 2 + import PostFeedItem from '~/components/feeds/post-feed-item'; 3 + 4 + import IconButton from '~/components/icon-button'; 5 + import ChevronRightOutlinedIcon from '~/components/icons-central/chevron-right-outline'; 6 + import GearOutlinedIcon from '~/components/icons-central/gear-outline'; 7 + import List from '~/components/list'; 8 + import * as Page from '~/components/page'; 9 + import VirtualItem from '~/components/virtual-item'; 10 + 11 + const HomePage = () => { 12 + const { timeline, isStale, reset } = useTimelineQuery(() => { 13 + return { 14 + type: 'following', 15 + showQuotes: true, 16 + showReplies: 'follows', 17 + showReposts: true, 18 + }; 19 + }); 20 + 21 + return ( 22 + <> 23 + <Page.Header> 24 + <Page.HeaderAccessory> 25 + <Page.MainMenu /> 26 + </Page.HeaderAccessory> 27 + 28 + <div class="flex min-w-0 grow"> 29 + <button class="-mx-2 flex items-center gap-1 overflow-hidden rounded px-2 py-1 hover:bg-c-contrast-50"> 30 + <span class="overflow-hidden text-ellipsis whitespace-nowrap text-base font-bold"> 31 + {'Following'} 32 + </span> 33 + <ChevronRightOutlinedIcon class="-mr-1 shrink-0 rotate-90 text-lg text-c-contrast-600" /> 34 + </button> 35 + </div> 36 + 37 + <Page.HeaderAccessory> 38 + <IconButton title="Home settings" icon={GearOutlinedIcon} /> 39 + </Page.HeaderAccessory> 40 + </Page.Header> 41 + 42 + <List 43 + data={timeline.data?.pages.flatMap((page) => page.items)} 44 + error={timeline.error} 45 + render={(item) => { 46 + return ( 47 + <VirtualItem estimateHeight={99}> 48 + <PostFeedItem item={item} /> 49 + </VirtualItem> 50 + ); 51 + }} 52 + hasNewData={isStale()} 53 + hasNextPage={timeline.hasNextPage} 54 + isFetchingNextPage={timeline.isFetchingNextPage || timeline.isLoading} 55 + isRefreshing={timeline.isRefetching} 56 + onEndReached={() => timeline.fetchNextPage()} 57 + onRefresh={reset} 58 + /> 59 + </> 60 + ); 61 + }; 62 + 63 + export default HomePage;
+5
src/views/main/messages.tsx
··· 1 + const MessagesPage = () => { 2 + return <div>messages</div>; 3 + }; 4 + 5 + export default MessagesPage;
+17
src/views/main/notifications.tsx
··· 1 + import * as Page from '~/components/page'; 2 + 3 + const NotificationsPage = () => { 4 + return ( 5 + <> 6 + <Page.Header> 7 + <Page.HeaderAccessory> 8 + <Page.MainMenu /> 9 + </Page.HeaderAccessory> 10 + 11 + <Page.Heading title="Notifications" /> 12 + </Page.Header> 13 + </> 14 + ); 15 + }; 16 + 17 + export default NotificationsPage;
+5
src/views/main/search.tsx
··· 1 + const SearchPage = () => { 2 + return <div>search</div>; 3 + }; 4 + 5 + export default SearchPage;
+33
src/views/not-found.tsx
··· 1 + import Button from '~/components/button'; 2 + import { history, logger } from '~/globals/navigation'; 3 + 4 + const NotFoundPage = () => { 5 + return ( 6 + <> 7 + <div class="p-4"> 8 + <h2 class="text-lg font-bold">Page not found</h2> 9 + <p class="mb-4 text-sm"> 10 + We're sorry, but the link you followed might be broken, or the page may have been removed. 11 + </p> 12 + 13 + <div class="mb-4"> 14 + <Button 15 + onClick={() => { 16 + if (logger.canGoBack) { 17 + history.back(); 18 + } else { 19 + history.navigate('/', { replace: true }); 20 + } 21 + }} 22 + size="sm" 23 + variant="primary" 24 + > 25 + Go back 26 + </Button> 27 + </div> 28 + </div> 29 + </> 30 + ); 31 + }; 32 + 33 + export default NotFoundPage;
+2
src/vite-env.d.ts
··· 1 + /// <reference types="svelte" /> 2 + /// <reference types="vite/client" />
+232
tailwind.config.js
··· 1 + import plugin from 'tailwindcss/plugin'; 2 + 3 + /** @type {import('tailwindcss').Config} */ 4 + export default { 5 + content: ['./src/**/*.tsx'], 6 + theme: { 7 + extend: { 8 + fontSize: { 9 + de: ['0.8125rem', '1.25rem'], 10 + }, 11 + zIndex: { 12 + 1: '1', 13 + 2: '2', 14 + }, 15 + spacing: { 16 + 0.75: '0.1875rem', 17 + 7.5: '1.875rem', 18 + 9.5: '2.375rem', 19 + 13: '3.25rem', 20 + 17: '4.24rem', 21 + 22: '5.5rem', 22 + 30: '7.5rem', 23 + 84: '21rem', 24 + 120: '30rem', 25 + }, 26 + borderWidth: { 27 + 3: '3px', 28 + }, 29 + minWidth: { 30 + 14: '3.5rem', 31 + 16: '4rem', 32 + }, 33 + minHeight: { 34 + 16: '4rem', 35 + }, 36 + maxHeight: { 37 + 141: '35.25rem', 38 + '50vh': '50vh', 39 + }, 40 + flexGrow: { 41 + 2: '2', 42 + 4: '4', 43 + }, 44 + aspectRatio: { 45 + banner: '3 / 1', 46 + }, 47 + keyframes: { 48 + indeterminate: { 49 + '0%': { 50 + translate: '-100%', 51 + }, 52 + '100%': { 53 + translate: '400%', 54 + }, 55 + }, 56 + }, 57 + animation: { 58 + indeterminate: 'indeterminate 1s linear infinite', 59 + }, 60 + boxShadow: { 61 + menu: 'rgba(var(--primary) / 0.2) 0px 0px 15px, rgba(var(--primary) / 0.15) 0px 0px 3px 1px', 62 + }, 63 + dropShadow: { 64 + DEFAULT: ['0 1px 2px rgb(0 0 0 / .3)', '0 1px 1px rgb(0 0 0 / .1)'], 65 + }, 66 + }, 67 + fontFamily: { 68 + sans: `"Roboto", ui-sans-serif, sans-serif, "Noto Color Emoji", "Twemoji Mozilla"`, 69 + mono: `"JetBrains Mono NL", ui-monospace, monospace`, 70 + }, 71 + colors: { 72 + transparent: 'transparent', 73 + c: { 74 + white: 'hsl(var(--c-white))', 75 + black: 'hsl(var(--c-black))', 76 + contrast: { 77 + 0: 'hsl(var(--c-contrast-0))', 78 + 25: 'hsl(var(--c-contrast-25))', 79 + 50: 'hsl(var(--c-contrast-50))', 80 + 100: 'hsl(var(--c-contrast-100))', 81 + 200: 'hsl(var(--c-contrast-200))', 82 + 300: 'hsl(var(--c-contrast-300))', 83 + 400: 'hsl(var(--c-contrast-400))', 84 + 500: 'hsl(var(--c-contrast-500))', 85 + 600: 'hsl(var(--c-contrast-600))', 86 + 700: 'hsl(var(--c-contrast-700))', 87 + 800: 'hsl(var(--c-contrast-800))', 88 + 900: 'hsl(var(--c-contrast-900))', 89 + 950: 'hsl(var(--c-contrast-950))', 90 + 975: 'hsl(var(--c-contrast-975))', 91 + }, 92 + primary: { 93 + 25: 'hsl(var(--c-primary-25))', 94 + 50: 'hsl(var(--c-primary-50))', 95 + 100: 'hsl(var(--c-primary-100))', 96 + 200: 'hsl(var(--c-primary-200))', 97 + 300: 'hsl(var(--c-primary-300))', 98 + 400: 'hsl(var(--c-primary-400))', 99 + 500: 'hsl(var(--c-primary-500))', 100 + 600: 'hsl(var(--c-primary-600))', 101 + 700: 'hsl(var(--c-primary-700))', 102 + 800: 'hsl(var(--c-primary-800))', 103 + 900: 'hsl(var(--c-primary-900))', 104 + 950: 'hsl(var(--c-primary-950))', 105 + 975: 'hsl(var(--c-primary-975))', 106 + }, 107 + positive: { 108 + 25: 'hsl(var(--c-positive-25))', 109 + 50: 'hsl(var(--c-positive-50))', 110 + 100: 'hsl(var(--c-positive-100))', 111 + 200: 'hsl(var(--c-positive-200))', 112 + 300: 'hsl(var(--c-positive-300))', 113 + 400: 'hsl(var(--c-positive-400))', 114 + 500: 'hsl(var(--c-positive-500))', 115 + 600: 'hsl(var(--c-positive-600))', 116 + 700: 'hsl(var(--c-positive-700))', 117 + 800: 'hsl(var(--c-positive-800))', 118 + 900: 'hsl(var(--c-positive-900))', 119 + 950: 'hsl(var(--c-positive-950))', 120 + 975: 'hsl(var(--c-positive-975))', 121 + }, 122 + negative: { 123 + 25: 'hsl(var(--c-negative-25))', 124 + 50: 'hsl(var(--c-negative-50))', 125 + 100: 'hsl(var(--c-negative-100))', 126 + 200: 'hsl(var(--c-negative-200))', 127 + 300: 'hsl(var(--c-negative-300))', 128 + 400: 'hsl(var(--c-negative-400))', 129 + 500: 'hsl(var(--c-negative-500))', 130 + 600: 'hsl(var(--c-negative-600))', 131 + 700: 'hsl(var(--c-negative-700))', 132 + 800: 'hsl(var(--c-negative-800))', 133 + 900: 'hsl(var(--c-negative-900))', 134 + 950: 'hsl(var(--c-negative-950))', 135 + 975: 'hsl(var(--c-negative-975))', 136 + }, 137 + }, 138 + t: { 139 + black: 'hsl(var(--t-black))', 140 + gray: { 141 + 0: 'hsl(var(--t-gray-0))', 142 + 25: 'hsl(var(--t-gray-25))', 143 + 50: 'hsl(var(--t-gray-50))', 144 + 100: 'hsl(var(--t-gray-100))', 145 + 200: 'hsl(var(--t-gray-200))', 146 + 300: 'hsl(var(--t-gray-300))', 147 + 400: 'hsl(var(--t-gray-400))', 148 + 500: 'hsl(var(--t-gray-500))', 149 + 600: 'hsl(var(--t-gray-600))', 150 + 700: 'hsl(var(--t-gray-700))', 151 + 800: 'hsl(var(--t-gray-800))', 152 + 900: 'hsl(var(--t-gray-900))', 153 + 950: 'hsl(var(--t-gray-950))', 154 + 975: 'hsl(var(--t-gray-975))', 155 + 1000: 'hsl(var(--t-gray-1000))', 156 + }, 157 + blue: { 158 + 25: 'hsl(var(--t-blue-25))', 159 + 50: 'hsl(var(--t-blue-50))', 160 + 100: 'hsl(var(--t-blue-100))', 161 + 200: 'hsl(var(--t-blue-200))', 162 + 300: 'hsl(var(--t-blue-300))', 163 + 400: 'hsl(var(--t-blue-400))', 164 + 500: 'hsl(var(--t-blue-500))', 165 + 600: 'hsl(var(--t-blue-600))', 166 + 700: 'hsl(var(--t-blue-700))', 167 + 800: 'hsl(var(--t-blue-800))', 168 + 900: 'hsl(var(--t-blue-900))', 169 + 950: 'hsl(var(--t-blue-950))', 170 + 975: 'hsl(var(--t-blue-975))', 171 + low: 'hsl(var(--t-blue-low))', 172 + }, 173 + green: { 174 + 25: 'hsl(var(--t-green-25))', 175 + 50: 'hsl(var(--t-green-50))', 176 + 100: 'hsl(var(--t-green-100))', 177 + 200: 'hsl(var(--t-green-200))', 178 + 300: 'hsl(var(--t-green-300))', 179 + 400: 'hsl(var(--t-green-400))', 180 + 500: 'hsl(var(--t-green-500))', 181 + 600: 'hsl(var(--t-green-600))', 182 + 700: 'hsl(var(--t-green-700))', 183 + 800: 'hsl(var(--t-green-800))', 184 + 900: 'hsl(var(--t-green-900))', 185 + 950: 'hsl(var(--t-green-950))', 186 + 975: 'hsl(var(--t-green-975))', 187 + }, 188 + red: { 189 + 25: 'hsl(var(--t-red-25))', 190 + 50: 'hsl(var(--t-red-50))', 191 + 100: 'hsl(var(--t-red-100))', 192 + 200: 'hsl(var(--t-red-200))', 193 + 300: 'hsl(var(--t-red-300))', 194 + 400: 'hsl(var(--t-red-400))', 195 + 500: 'hsl(var(--t-red-500))', 196 + 600: 'hsl(var(--t-red-600))', 197 + 700: 'hsl(var(--t-red-700))', 198 + 800: 'hsl(var(--t-red-800))', 199 + 900: 'hsl(var(--t-red-900))', 200 + 950: 'hsl(var(--t-red-950))', 201 + 975: 'hsl(var(--t-red-975))', 202 + }, 203 + }, 204 + }, 205 + }, 206 + corePlugins: { 207 + outlineStyle: false, 208 + }, 209 + plugins: [ 210 + plugin(({ addVariant, addUtilities }) => { 211 + addVariant('modal', '&:modal'); 212 + addVariant('focus-within', '&:has(:focus-visible)'); 213 + 214 + addUtilities({ 215 + '.scrollbar-hide': { 216 + '-ms-overflow-style': 'none', 217 + 'scrollbar-width': 'none', 218 + 219 + '&::-webkit-scrollbar': { 220 + display: 'none', 221 + }, 222 + }, 223 + 224 + '.outline-none': { 'outline-style': 'none' }, 225 + '.outline': { 'outline-style': 'solid' }, 226 + '.outline-dashed': { 'outline-style': 'dashed' }, 227 + '.outline-dotted': { 'outline-style': 'dotted' }, 228 + '.outline-double': { 'outline-style': 'double' }, 229 + }); 230 + }), 231 + ], 232 + };
+30
tsconfig.json
··· 1 + { 2 + "compilerOptions": { 3 + "target": "ESNext", 4 + "lib": ["DOM", "DOM.Iterable", "ESNext"], 5 + "types": ["dom-close-watcher"], 6 + "skipLibCheck": true, 7 + 8 + "module": "ESNext", 9 + "moduleResolution": "Bundler", 10 + "allowImportingTsExtensions": true, 11 + "resolveJsonModule": true, 12 + "noEmit": true, 13 + "jsx": "preserve", 14 + "jsxImportSource": "solid-js", 15 + 16 + "incremental": true, 17 + "strict": true, 18 + "verbatimModuleSyntax": true, 19 + "noUnusedLocals": true, 20 + "noUnusedParameters": true, 21 + "noFallthroughCasesInSwitch": true, 22 + 23 + "useDefineForClassFields": false, 24 + 25 + "paths": { 26 + "~/*": ["./src/*"], 27 + }, 28 + }, 29 + "include": ["src"], 30 + }
+16
vite.config.js
··· 1 + import * as path from 'node:path'; 2 + 3 + import { defineConfig } from 'vite'; 4 + import solid from 'vite-plugin-solid'; 5 + 6 + export default defineConfig({ 7 + plugins: [solid()], 8 + build: { 9 + minify: 'terser', 10 + }, 11 + resolve: { 12 + alias: { 13 + '~': path.join(__dirname, './src'), 14 + }, 15 + }, 16 + });