Merge remote-tracking branch 'weblate/develop' into develop

This commit is contained in:
ThatOneCalculator 2023-04-29 19:52:43 -07:00
commit 7373fc625a
No known key found for this signature in database
GPG Key ID: 8703CACD01000000
56 changed files with 872 additions and 681 deletions

View File

@ -675,3 +675,48 @@ useGlobalSetting: Fes servir els ajustos globals
useGlobalSettingDesc: Si s'activa, es faran servir els ajustos de notificacions del useGlobalSettingDesc: Si s'activa, es faran servir els ajustos de notificacions del
teu compte. Si es desactiva , es poden fer configuracions individuals. teu compte. Si es desactiva , es poden fer configuracions individuals.
other: Altres other: Altres
menu: Menú
addItem: Afegeix un element
divider: Divisor
relays: Relés
addRelay: Afegeix un Relé
inboxUrl: Adreça de la safata d'entrada
addedRelays: Relés afegits
serviceworkerInfo: Ha de estar activat per les notificacions push.
poll: Enquesta
deletedNote: Article eliminat
disablePlayer: Tancar el reproductor de vídeo
fileIdOrUrl: ID o adreça URL del fitxer
behavior: Comportament
regenerateLoginTokenDescription: Regenera el token que es fa servir de manera interna
durant l'inici de sessió. Normalment això no és necessari. Si es torna a genera
el token, es tancarà la sessió a tots els dispositius.
setMultipleBySeparatingWithSpace: Separa diferents entrades amb espais.
reportAbuseOf: Informa sobre {name}
sample: Exemple
abuseReports: Informes
reportAbuse: Informe
reporter: Informador
reporterOrigin: Origen d'el informador
forwardReport: Envia l'informe a una instancia remota
abuseReported: El teu informe ha sigut enviat. Moltes gràcies.
reporteeOrigin: Origen de l'informe
send: Enviar
abuseMarkAsResolved: Marcar l'informe com a resolt
visibility: Visibilitat
useCw: Amaga el contingut
enablePlayer: Obre el reproductor de vídeo
yourAccountSuspendedDescription: Aquest compte ha sigut suspesa per no seguir els
termes de servei del servidor o quelcom similar. Contacte amb l'administrador si
vols conèixer la raó amb més detall. Si us plau no facis un compte nou.
invisibleNote: Article ocult
enableInfiniteScroll: Carregar més de forma automàtica
fillAbuseReportDescription: Si us plau omple els detalls sobre aquest informe. Si
es sobre un article en concret, si us plau inclou l'adreça URL.
forwardReportIsAnonymous: Com a informador a l'instància remota no es mostrarà el
teu compte, si no un compte anònim.
openInNewTab: Obrir en una pestanya nova
openInSideView: Obrir a la vista lateral
defaultNavigationBehaviour: Navegació per defecte
editTheseSettingsMayBreakAccount: Si edites aquestes configuracions pots fer mal bé
el teu compte.

View File

@ -1042,7 +1042,7 @@ moveFromLabel: "Account you're moving from:"
moveFromDescription: "This will set an alias of your old account so that you can move\ moveFromDescription: "This will set an alias of your old account so that you can move\
\ from that account to this current one. Do this BEFORE moving from your older account.\ \ from that account to this current one. Do this BEFORE moving from your older account.\
\ Please enter the tag of the account formatted like @person@instance.com" \ Please enter the tag of the account formatted like @person@instance.com"
migrationConfirm: "Are you absolutely sure you want to migrate your acccount to {account}?\ migrationConfirm: "Are you absolutely sure you want to migrate your account to {account}?\
\ Once you do this, you won't be able to reverse it, and you won't be able to use\ \ Once you do this, you won't be able to reverse it, and you won't be able to use\
\ your account normally again.\nAlso, please ensure that you've set this current\ \ your account normally again.\nAlso, please ensure that you've set this current\
\ account as the account you're moving from." \ account as the account you're moving from."

View File

@ -3,7 +3,7 @@ fetchingAsApObject: Hae Fedeversestä
gotIt: Selvä! gotIt: Selvä!
cancel: Peruuta cancel: Peruuta
enterUsername: Anna käyttäjänimi enterUsername: Anna käyttäjänimi
renotedBy: Buustannut {käyttäjä} renotedBy: Buustannut {user}
noNotes: Ei lähetyksiä noNotes: Ei lähetyksiä
noNotifications: Ei ilmoituksia noNotifications: Ei ilmoituksia
instance: Instanssi instance: Instanssi
@ -41,3 +41,183 @@ favorite: Lisää kirjanmerkkeihin
copyContent: Kopioi sisältö copyContent: Kopioi sisältö
deleteAndEdit: Poista ja muokkaa deleteAndEdit: Poista ja muokkaa
copyLink: Kopioi linkki copyLink: Kopioi linkki
makeFollowManuallyApprove: Seuraajapyyntö vaatii hyväksymistä
follow: Seuraa
pinned: Kiinnitä profiiliin
followRequestPending: Seuraajapyyntö odottaa
you: Sinä
unrenote: Peruuta buustaus
reaction: Reaktiot
reactionSettingDescription2: Vedä uudelleenjärjestelläksesi, napsauta poistaaksesi,
paina "+" lisätäksesi.
attachCancel: Poista liite
enterFileName: Anna tiedostonimi
mute: Hiljennä
unmute: Poista hiljennys
headlineMisskey: Avoimen lähdekoodin, hajautettu sosiaalisen median alusta, joka on
ikuisesti ilmainen! 🚀
monthAndDay: '{day}/{month}'
deleteAndEditConfirm: Oletko varma, että haluat poistaa tämän lähetyksen ja muokata
sitä? Menetät kaikki reaktiot, buustaukset ja vastaukset lähetyksestäsi.
addToList: Lisää listaan
sendMessage: Lähetä viesti
reply: Vastaa
loadMore: Lataa enemmän
showMore: Näytä enemmän
receiveFollowRequest: Seuraajapyyntö vastaanotettu
followRequestAccepted: Seuraajapyyntö hyväksytty
mentions: Maininnat
importAndExport: Tuo/Vie Tietosisältö
import: Tuo
export: Vie
files: Tiedostot
download: Lataa
unfollowConfirm: Oletko varma, ettet halua seurata enää käyttäjää {name}?
noLists: Sinulla ei ole listoja
note: Lähetys
notes: Lähetykset
following: Seuraa
createList: Luo lista
manageLists: Hallitse listoja
error: Virhe
somethingHappened: On tapahtunut virhe
retry: Yritä uudelleen
pageLoadError: Virhe ladattaessa sivua.
serverIsDead: Tämä palvelin ei vastaa. Yritä hetken kuluttua uudelleen.
youShouldUpgradeClient: Nähdäksesi tämän sivun, virkistä päivittääksesi asiakasohjelmasi.
privacy: Tietosuoja
defaultNoteVisibility: Oletusnäkyvyys
followRequest: Seuraajapyyntö
followRequests: Seuraajapyynnöt
unfollow: Poista seuraaminen
enterEmoji: Syötä emoji
renote: Buustaa
renoted: Buustattu.
cantRenote: Tätä lähetystä ei voi buustata.
cantReRenote: Buustausta ei voi buustata.
quote: Lainaus
pinnedNote: Lukittu lähetys
clickToShow: Napsauta nähdäksesi
sensitive: Herkkää sisältöä (NSFW)
add: Lisää
enableEmojiReactions: Ota käyttöön emoji-reaktiot
showEmojisInReactionNotifications: Näytä emojit reaktioilmoituksissa
reactionSetting: Reaktiot näytettäväksi reaktiovalitsimessa
rememberNoteVisibility: Muista lähetyksen näkyvyysasetukset
markAsSensitive: Merkitse herkäksi sisällöksi (NSFW)
unmarkAsSensitive: Poista merkintä herkkää sisältöä (NSFW)
renoteMute: Hiljennä buustit
renoteUnmute: Poista buustien hiljennys
block: Estä
unblock: Poista esto
unsuspend: Poista keskeytys
suspend: Keskeytys
blockConfirm: Oletko varma, että haluat estää tämän tilin?
unblockConfirm: Oletko varma, että haluat poistaa tämän tilin eston?
selectAntenna: Valitse antenni
selectWidget: Valitse vimpain
editWidgets: Muokkaa vimpaimia
editWidgetsExit: Valmis
emoji: Emoji
emojis: Emojit
emojiName: Emojin nimi
emojiUrl: Emojin URL-linkki
cacheRemoteFiles: Taltioi etätiedostot välimuistiin
flagAsBot: Merkitse tili botiksi
flagAsBotDescription: Ota tämä vaihtoehto käyttöön, jos tätä tiliä ohjaa ohjelma.
Jos se on käytössä, se toimii lippuna muille kehittäjille, jotta estetään loputtomat
vuorovaikutusketjut muiden bottien kanssa ja säädetään Calckeyn sisäiset järjestelmät
käsittelemään tätä tiliä botina.
flagAsCat: Oletko kissa? 🐱
flagAsCatDescription: Saat kissan korvat ja puhut kuin kissa!
flagSpeakAsCat: Puhu kuin kissa
flagShowTimelineReplies: Näytä vastaukset aikajanalla
addAccount: Lisää tili
loginFailed: Kirjautuminen epäonnistui
showOnRemote: Katsele etäinstanssilla
general: Yleistä
accountMoved: 'Käyttäjä on muuttanut uuteen tiliin:'
wallpaper: Taustakuva
setWallpaper: Aseta taustakuva
searchWith: 'Etsi: {q}'
youHaveNoLists: Sinulla ei ole listoja
followConfirm: Oletko varma, että haluat seurata käyttäjää {name}?
host: Isäntä
selectUser: Valitse käyttäjä
annotation: Kommentit
registeredAt: Rekisteröity
latestRequestReceivedAt: Viimeisin pyyntö vastaanotettu
latestRequestSentAt: Viimeisin pyyntö lähetetty
storageUsage: Tallennustilan käyttö
charts: Kaaviot
stopActivityDelivery: Lopeta toimintojen lähettäminen
blockThisInstance: Estä tämä instanssi
operations: Toiminnot
metadata: Metatieto
monitor: Seuranta
jobQueue: Työjono
cpuAndMemory: Prosessori ja muisti
network: Verkko
disk: Levy
clearCachedFiles: Tyhjennä välimuisti
clearCachedFilesConfirm: Oletko varma, että haluat tyhjentää kaikki välimuistiin tallennetut
etätiedostot?
blockedInstances: Estetyt instanssit
hiddenTags: Piilotetut asiatunnisteet
mention: Maininta
copyUsername: Kopioi käyttäjänimi
searchUser: Etsi käyttäjää
showLess: Sulje
youGotNewFollower: seurasi sinua
directNotes: Yksityisviestit
driveFileDeleteConfirm: Oletko varma, että haluat poistaa tiedoston " {name}"? Lähetykset,
jotka sisältyvät tiedostoon, poistuvat myös.
importRequested: Olet pyytänyt viemistä. Tämä voi viedä hetken.
exportRequested: Olet pyytänyt tuomista. Tämä voi viedä hetken. Se lisätään asemaan
kun tuonti valmistuu.
lists: Listat
followers: Seuraajat
followsYou: Seuraa sinua
pageLoadErrorDescription: Tämä yleensä johtuu verkkovirheistä tai selaimen välimuistista.
Kokeile tyhjentämällä välimuisti ja yritä sitten hetken kuluttua uudelleen.
enterListName: Anna listalle nimi
withNFiles: '{n} tiedosto(t)'
instanceInfo: Instanssin tiedot
clearQueue: Tyhjennä jono
suspendConfirm: Oletko varma, että haluat keskeyttää tämän tilin?
unsuspendConfirm: Oletko varma, että haluat poistaa tämän tilin keskeytyksen?
selectList: Valitse lista
customEmojis: Kustomoitu Emoji
addEmoji: Lisää
settingGuide: Suositellut asetukset
cacheRemoteFilesDescription: Kun tämä asetus ei ole käytössä, etätiedostot on ladattu
suoraan etäinstanssilta. Asetuksen poistaminen käytöstä vähentää tallennustilan
käyttöä, mutta lisää verkkoliikennettä kun pienoiskuvat eivät muodostu.
flagSpeakAsCatDescription: Lähetyksesi nyanifioidaan, kun olet kissatilassa
flagShowTimelineRepliesDescription: Näyttää käyttäjien vastaukset muiden käyttäjien
lähetyksiin aikajanalla, jos se on päällä.
autoAcceptFollowed: Automaattisesti hyväksy seuraamispyynnöt käyttäjiltä, joita seuraat
perHour: Tunnissa
removeWallpaper: Poista taustakuva
recipient: Vastaanottaja(t)
federation: Federaatio
software: Ohjelmisto
proxyAccount: Proxy-tili
proxyAccountDescription: Välitystili (Proxy-tili) on tili, joka toimii käyttäjien
etäseuraajana tietyin edellytyksin. Kun käyttäjä esimerkiksi lisää etäkäyttäjän
luetteloon, etäkäyttäjän toimintaa ei toimiteta instanssiin, jos yksikään paikallinen
käyttäjä ei seuraa kyseistä käyttäjää, joten välitystili seuraa sen sijaan.
latestStatus: Viimeisin tila
selectInstance: Valitse instanssi
instances: Instanssit
perDay: Päivässä
version: Versio
statistics: Tilastot
clearQueueConfirmTitle: Oletko varma, että haluat tyhjentää jonon?
introMisskey: Tervetuloa! Calckey on avoimen lähdekoodin, hajautettu sosiaalisen median
alusta, joka on ikuisesti ilmainen! 🚀
clearQueueConfirmText: Mitkään välittämättömät lähetykset, jotka ovat jonossa, eivät
federoidu. Yleensä tätä toimintoa ei tarvita.
blockedInstancesDescription: Lista instanssien isäntänimistä, jotka haluat estää.
Listatut instanssit eivät kykene kommunikoimaan enää tämän instanssin kanssa.
_lang_: Suomi

View File

@ -1,6 +1,6 @@
{ {
"name": "calckey", "name": "calckey",
"version": "13.2.0-dev40", "version": "13.2.0-dev41",
"codename": "aqua", "codename": "aqua",
"repository": { "repository": {
"type": "git", "type": "git",
@ -40,8 +40,6 @@
"@bull-board/ui": "^4.10.2", "@bull-board/ui": "^4.10.2",
"@napi-rs/cli": "^2.15.0", "@napi-rs/cli": "^2.15.0",
"@tensorflow/tfjs": "^3.21.0", "@tensorflow/tfjs": "^3.21.0",
"focus-trap": "^7.2.0",
"focus-trap-vue": "^4.0.1",
"js-yaml": "4.1.0", "js-yaml": "4.1.0",
"seedrandom": "^3.0.5" "seedrandom": "^3.0.5"
}, },

View File

@ -0,0 +1,23 @@
export class LibreTranslate1682777547198 {
name = "LibreTranslate1682777547198";
async up(queryRunner) {
await queryRunner.query(`
ALTER TABLE "meta"
ADD "libreTranslateApiUrl" character varying(512)
`);
await queryRunner.query(`
ALTER TABLE "meta"
ADD "libreTranslateApiKey" character varying(128)
`);
}
async down(queryRunner) {
await queryRunner.query(`
ALTER TABLE "meta" DROP COLUMN "libreTranslateApiKey"
`);
await queryRunner.query(`
ALTER TABLE "meta" DROP COLUMN "libreTranslateApiUrl"
`);
}
}

View File

@ -89,6 +89,11 @@ export type Source = {
authKey?: string; authKey?: string;
isPro?: boolean; isPro?: boolean;
}; };
libreTranslate: {
managed?: boolean;
apiUrl?: string;
apiKey?: string;
};
email: { email: {
managed?: boolean; managed?: boolean;
address?: string; address?: string;

View File

@ -386,6 +386,18 @@ export class Meta {
}) })
public deeplIsPro: boolean; public deeplIsPro: boolean;
@Column('varchar', {
length: 512,
nullable: true,
})
public libreTranslateApiUrl: string | null;
@Column('varchar', {
length: 128,
nullable: true,
})
public libreTranslateApiKey: string | null;
@Column('varchar', { @Column('varchar', {
length: 512, length: 512,
nullable: true, nullable: true,

View File

@ -30,6 +30,17 @@ export default define(meta, paramDef, async (ps, me) => {
set.deeplIsPro = config.deepl.isPro; set.deeplIsPro = config.deepl.isPro;
} }
} }
if (
config.libreTranslate.managed != null &&
config.libreTranslate.managed === true
) {
if (typeof config.libreTranslate.apiUrl === "string") {
set.libreTranslateApiUrl = config.libreTranslate.apiUrl;
}
if (typeof config.libreTranslate.apiKey === "string") {
set.libreTranslateApiKey = config.libreTranslate.apiKey;
}
}
if (config.email.managed != null && config.email.managed === true) { if (config.email.managed != null && config.email.managed === true) {
set.enableEmail = true; set.enableEmail = true;
if (typeof config.email.address === "string") { if (typeof config.email.address === "string") {

View File

@ -512,7 +512,8 @@ export default define(meta, paramDef, async (ps, me) => {
enableGithubIntegration: instance.enableGithubIntegration, enableGithubIntegration: instance.enableGithubIntegration,
enableDiscordIntegration: instance.enableDiscordIntegration, enableDiscordIntegration: instance.enableDiscordIntegration,
enableServiceWorker: instance.enableServiceWorker, enableServiceWorker: instance.enableServiceWorker,
translatorAvailable: instance.deeplAuthKey != null, translatorAvailable:
instance.deeplAuthKey != null || instance.libreTranslateApiUrl != null,
pinnedPages: instance.pinnedPages, pinnedPages: instance.pinnedPages,
pinnedClipId: instance.pinnedClipId, pinnedClipId: instance.pinnedClipId,
cacheRemoteFiles: instance.cacheRemoteFiles, cacheRemoteFiles: instance.cacheRemoteFiles,
@ -564,6 +565,8 @@ export default define(meta, paramDef, async (ps, me) => {
objectStorageS3ForcePathStyle: instance.objectStorageS3ForcePathStyle, objectStorageS3ForcePathStyle: instance.objectStorageS3ForcePathStyle,
deeplAuthKey: instance.deeplAuthKey, deeplAuthKey: instance.deeplAuthKey,
deeplIsPro: instance.deeplIsPro, deeplIsPro: instance.deeplIsPro,
libreTranslateApiUrl: instance.libreTranslateApiUrl,
libreTranslateApiKey: instance.libreTranslateApiKey,
enableIpLogging: instance.enableIpLogging, enableIpLogging: instance.enableIpLogging,
enableActiveEmailValidation: instance.enableActiveEmailValidation, enableActiveEmailValidation: instance.enableActiveEmailValidation,
}; };

View File

@ -124,6 +124,8 @@ export const paramDef = {
summalyProxy: { type: "string", nullable: true }, summalyProxy: { type: "string", nullable: true },
deeplAuthKey: { type: "string", nullable: true }, deeplAuthKey: { type: "string", nullable: true },
deeplIsPro: { type: "boolean" }, deeplIsPro: { type: "boolean" },
libreTranslateApiUrl: { type: "string", nullable: true },
libreTranslateApiKey: { type: "string", nullable: true },
enableTwitterIntegration: { type: "boolean" }, enableTwitterIntegration: { type: "boolean" },
twitterConsumerKey: { type: "string", nullable: true }, twitterConsumerKey: { type: "string", nullable: true },
twitterConsumerSecret: { type: "string", nullable: true }, twitterConsumerSecret: { type: "string", nullable: true },
@ -515,6 +517,22 @@ export default define(meta, paramDef, async (ps, me) => {
set.deeplIsPro = ps.deeplIsPro; set.deeplIsPro = ps.deeplIsPro;
} }
if (ps.libreTranslateApiUrl !== undefined) {
if (ps.libreTranslateApiUrl === "") {
set.libreTranslateApiUrl = null;
} else {
set.libreTranslateApiUrl = ps.libreTranslateApiUrl;
}
}
if (ps.libreTranslateApiKey !== undefined) {
if (ps.libreTranslateApiKey === "") {
set.libreTranslateApiKey = null;
} else {
set.libreTranslateApiKey = ps.libreTranslateApiKey;
}
}
if (ps.enableIpLogging !== undefined) { if (ps.enableIpLogging !== undefined) {
set.enableIpLogging = ps.enableIpLogging; set.enableIpLogging = ps.enableIpLogging;
} }

View File

@ -482,7 +482,8 @@ export default define(meta, paramDef, async (ps, me) => {
enableServiceWorker: instance.enableServiceWorker, enableServiceWorker: instance.enableServiceWorker,
translatorAvailable: instance.deeplAuthKey != null, translatorAvailable:
instance.deeplAuthKey != null || instance.libreTranslateApiUrl != null,
defaultReaction: instance.defaultReaction, defaultReaction: instance.defaultReaction,
...(ps.detail ...(ps.detail

View File

@ -51,15 +51,54 @@ export default define(meta, paramDef, async (ps, user) => {
const instance = await fetchMeta(); const instance = await fetchMeta();
if (instance.deeplAuthKey == null) { if (instance.deeplAuthKey == null && instance.libreTranslateApiUrl == null) {
return 204; // TODO: 良い感じのエラー返す return 204; // TODO: 良い感じのエラー返す
} }
let targetLang = ps.targetLang; let targetLang = ps.targetLang;
if (targetLang.includes("-")) targetLang = targetLang.split("-")[0]; if (targetLang.includes("-")) targetLang = targetLang.split("-")[0];
if (instance.libreTranslateApiUrl != null) {
const jsonBody = {
q: note.text,
source: "auto",
target: targetLang,
format: "text",
api_key: instance.libreTranslateApiKey ?? "",
};
const url = new URL(instance.libreTranslateApiUrl);
if (url.pathname.endsWith("/")) {
url.pathname = url.pathname.slice(0, -1);
}
if (!url.pathname.endsWith("/translate")) {
url.pathname += "/translate";
}
const res = await fetch(url.toString(), {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(jsonBody),
agent: getAgentByUrl,
});
const json = (await res.json()) as {
detectedLanguage?: {
confidence: number;
language: string;
};
translatedText: string;
};
return {
sourceLang: json.detectedLanguage?.language,
text: json.translatedText,
};
}
const params = new URLSearchParams(); const params = new URLSearchParams();
params.append("auth_key", instance.deeplAuthKey); params.append("auth_key", instance.deeplAuthKey ?? "");
params.append("text", note.text); params.append("text", note.text);
params.append("target_lang", targetLang); params.append("target_lang", targetLang);

View File

@ -1,7 +0,0 @@
node_modules
/built
/coverage
/.eslintrc.js
/jest.config.ts
/test
/test-d

View File

@ -1,65 +0,0 @@
module.exports = {
root: true,
parser: "@typescript-eslint/parser",
parserOptions: {
tsconfigRootDir: __dirname,
project: ["./tsconfig.json"],
},
plugins: ["@typescript-eslint"],
extends: ["eslint:recommended", "plugin:@typescript-eslint/recommended"],
rules: {
indent: [
"error",
"tab",
{
SwitchCase: 1,
MemberExpression: "off",
flatTernaryExpressions: true,
ArrayExpression: "first",
ObjectExpression: "first",
},
],
"eol-last": ["error", "always"],
semi: ["error", "always"],
quotes: ["error", "single"],
"comma-dangle": ["error", "always-multiline"],
"keyword-spacing": [
"error",
{
before: true,
after: true,
},
],
"key-spacing": [
"error",
{
beforeColon: false,
afterColon: true,
},
],
"space-infix-ops": ["error"],
"space-before-blocks": ["error", "always"],
"object-curly-spacing": ["error", "always"],
"nonblock-statement-body-position": ["error", "beside"],
eqeqeq: ["error", "always", { null: "ignore" }],
"no-multiple-empty-lines": ["error", { max: 1 }],
"no-multi-spaces": ["error"],
"no-var": ["error"],
"prefer-arrow-callback": ["error"],
"no-throw-literal": ["error"],
"no-param-reassign": ["warn"],
"no-constant-condition": ["warn"],
"no-empty-pattern": ["warn"],
"@typescript-eslint/no-unnecessary-condition": ["error"],
"@typescript-eslint/no-inferrable-types": ["warn"],
"@typescript-eslint/no-non-null-assertion": ["warn"],
"@typescript-eslint/explicit-function-return-type": ["warn"],
"@typescript-eslint/no-misused-promises": [
"error",
{
checksVoidReturn: false,
},
],
"@typescript-eslint/consistent-type-imports": "error",
},
};

View File

@ -9,9 +9,8 @@
"tsd": "tsd", "tsd": "tsd",
"api": "pnpm api-extractor run --local --verbose", "api": "pnpm api-extractor run --local --verbose",
"api-prod": "pnpm api-extractor run --verbose", "api-prod": "pnpm api-extractor run --verbose",
"eslint": "eslint . --ext .js,.jsx,.ts,.tsx",
"typecheck": "tsc --noEmit", "typecheck": "tsc --noEmit",
"lint": "pnpm typecheck && pnpm eslint", "lint": "pnpm typecheck && pnpm rome check \"src/*.ts\"",
"jest": "jest --coverage --detectOpenHandles", "jest": "jest --coverage --detectOpenHandles",
"test": "pnpm jest && pnpm tsd" "test": "pnpm jest && pnpm tsd"
}, },

View File

@ -195,7 +195,8 @@ function onMousedown(evt: MouseEvent): void {
} }
&:focus-visible { &:focus-visible {
outline: auto; outline: solid 2px var(--focus);
outline-offset: 2px;
} }
&.inline { &.inline {

View File

@ -1,6 +1,5 @@
<template> <template>
<button <button
ref="el"
class="_button" class="_button"
:class="{ showLess: modelValue, fade: !modelValue }" :class="{ showLess: modelValue, fade: !modelValue }"
@click.stop="toggle" @click.stop="toggle"
@ -13,7 +12,7 @@
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { computed, ref } from "vue"; import { computed } from "vue";
import { length } from "stringz"; import { length } from "stringz";
import * as misskey from "calckey-js"; import * as misskey from "calckey-js";
import { concat } from "@/scripts/array"; import { concat } from "@/scripts/array";
@ -28,8 +27,6 @@ const emit = defineEmits<{
(ev: "update:modelValue", v: boolean): void; (ev: "update:modelValue", v: boolean): void;
}>(); }>();
const el = ref<HTMLElement>();
const label = computed(() => { const label = computed(() => {
return concat([ return concat([
props.note.text props.note.text
@ -46,14 +43,6 @@ const label = computed(() => {
const toggle = () => { const toggle = () => {
emit("update:modelValue", !props.modelValue); emit("update:modelValue", !props.modelValue);
}; };
function focus() {
el.value.focus();
}
defineExpose({
focus
});
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
@ -73,7 +62,7 @@ defineExpose({
} }
} }
} }
&:hover > span, &:focus > span { &:hover > span {
background: var(--cwFg) !important; background: var(--cwFg) !important;
color: var(--cwBg) !important; color: var(--cwBg) !important;
} }

View File

@ -1,5 +1,5 @@
<template> <template>
<button ref="thumbnail" class="zdjebgpv"> <div ref="thumbnail" class="zdjebgpv">
<ImgWithBlurhash <ImgWithBlurhash
v-if="isThumbnailAvailable" v-if="isThumbnailAvailable"
:hash="file.blurhash" :hash="file.blurhash"
@ -36,7 +36,7 @@
v-if="isThumbnailAvailable && is === 'video'" v-if="isThumbnailAvailable && is === 'video'"
class="ph-file-video ph-bold ph-lg icon-sub" class="ph-file-video ph-bold ph-lg icon-sub"
></i> ></i>
</button> </div>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
@ -88,9 +88,6 @@ const isThumbnailAvailable = computed(() => {
background: var(--panel); background: var(--panel);
border-radius: 8px; border-radius: 8px;
overflow: clip; overflow: clip;
border: 0;
padding: 0;
cursor: pointer;
> .icon-sub { > .icon-sub {
position: absolute; position: absolute;

View File

@ -1,160 +1,157 @@
<template> <template>
<FocusTrap v-bind:active="isActive"> <div
<div class="omfetrab"
class="omfetrab" :class="['s' + size, 'w' + width, 'h' + height, { asDrawer }]"
:class="['s' + size, 'w' + width, 'h' + height, { asDrawer }]" :style="{ maxHeight: maxHeight ? maxHeight + 'px' : undefined }"
:style="{ maxHeight: maxHeight ? maxHeight + 'px' : undefined }" >
tabindex="-1" <input
> ref="search"
<input v-model.trim="q"
ref="search" class="search"
v-model.trim="q" data-prevent-emoji-insert
class="search" :class="{ filled: q != null && q != '' }"
data-prevent-emoji-insert :placeholder="i18n.ts.search"
:class="{ filled: q != null && q != '' }" type="search"
:placeholder="i18n.ts.search" @paste.stop="paste"
type="search" @keyup.enter="done()"
@paste.stop="paste" />
@keyup.enter="done()" <div ref="emojis" class="emojis">
/> <section class="result">
<div ref="emojis" class="emojis"> <div v-if="searchResultCustom.length > 0" class="body">
<section class="result"> <button
<div v-if="searchResultCustom.length > 0" class="body"> v-for="emoji in searchResultCustom"
:key="emoji.id"
class="_button item"
:title="emoji.name"
tabindex="0"
@click="chosen(emoji, $event)"
>
<!--<MkEmoji v-if="emoji.char != null" :emoji="emoji.char"/>-->
<img
class="emoji"
:src="
disableShowingAnimatedImages
? getStaticImageUrl(emoji.url)
: emoji.url
"
/>
</button>
</div>
<div v-if="searchResultUnicode.length > 0" class="body">
<button
v-for="emoji in searchResultUnicode"
:key="emoji.name"
class="_button item"
:title="emoji.name"
tabindex="0"
@click="chosen(emoji, $event)"
>
<MkEmoji class="emoji" :emoji="emoji.char" />
</button>
</div>
</section>
<div v-if="tab === 'index'" class="group index">
<section v-if="showPinned">
<div class="body">
<button <button
v-for="emoji in searchResultCustom" v-for="emoji in pinned"
:key="emoji.id" :key="emoji"
class="_button item" class="_button item"
:title="emoji.name"
tabindex="0" tabindex="0"
@click="chosen(emoji, $event)" @click="chosen(emoji, $event)"
> >
<!--<MkEmoji v-if="emoji.char != null" :emoji="emoji.char"/>--> <MkEmoji
<img
class="emoji" class="emoji"
:src=" :emoji="emoji"
disableShowingAnimatedImages :normal="true"
? getStaticImageUrl(emoji.url)
: emoji.url
"
/> />
</button> </button>
</div> </div>
<div v-if="searchResultUnicode.length > 0" class="body">
<button
v-for="emoji in searchResultUnicode"
:key="emoji.name"
class="_button item"
:title="emoji.name"
tabindex="0"
@click="chosen(emoji, $event)"
>
<MkEmoji class="emoji" :emoji="emoji.char" />
</button>
</div>
</section> </section>
<div v-if="tab === 'index'" class="group index"> <section>
<section v-if="showPinned"> <header class="_acrylic">
<div class="body"> <i class="ph-alarm ph-bold ph-fw ph-lg"></i>
<button {{ i18n.ts.recentUsed }}
v-for="emoji in pinned" </header>
:key="emoji" <div class="body">
class="_button item" <button
tabindex="0" v-for="emoji in recentlyUsedEmojis"
@click="chosen(emoji, $event)" :key="emoji"
> class="_button item"
<MkEmoji @click="chosen(emoji, $event)"
class="emoji" >
:emoji="emoji" <MkEmoji
:normal="true" class="emoji"
/> :emoji="emoji"
</button> :normal="true"
</div> />
</section> </button>
</div>
<section> </section>
<header class="_acrylic">
<i class="ph-alarm ph-bold ph-fw ph-lg"></i>
{{ i18n.ts.recentUsed }}
</header>
<div class="body">
<button
v-for="emoji in recentlyUsedEmojis"
:key="emoji"
class="_button item"
@click="chosen(emoji, $event)"
>
<MkEmoji
class="emoji"
:emoji="emoji"
:normal="true"
/>
</button>
</div>
</section>
</div>
<div v-once class="group">
<header>{{ i18n.ts.customEmojis }}</header>
<XSection
v-for="category in customEmojiCategories"
:key="'custom:' + category"
:initial-shown="false"
:emojis="
customEmojis
.filter((e) => e.category === category)
.map((e) => ':' + e.name + ':')
"
@chosen="chosen"
>{{ category || i18n.ts.other }}</XSection
>
</div>
<div v-once class="group">
<header>{{ i18n.ts.emoji }}</header>
<XSection
v-for="category in categories"
:key="category"
:emojis="
emojilist
.filter((e) => e.category === category)
.map((e) => e.char)
"
@chosen="chosen"
>{{ category }}</XSection
>
</div>
</div> </div>
<div class="tabs"> <div v-once class="group">
<button <header>{{ i18n.ts.customEmojis }}</header>
class="_button tab" <XSection
:class="{ active: tab === 'index' }" v-for="category in customEmojiCategories"
@click="tab = 'index'" :key="'custom:' + category"
:initial-shown="false"
:emojis="
customEmojis
.filter((e) => e.category === category)
.map((e) => ':' + e.name + ':')
"
@chosen="chosen"
>{{ category || i18n.ts.other }}</XSection
> >
<i class="ph-asterisk ph-bold ph-lg ph-fw ph-lg"></i> </div>
</button> <div v-once class="group">
<button <header>{{ i18n.ts.emoji }}</header>
class="_button tab" <XSection
:class="{ active: tab === 'custom' }" v-for="category in categories"
@click="tab = 'custom'" :key="category"
:emojis="
emojilist
.filter((e) => e.category === category)
.map((e) => e.char)
"
@chosen="chosen"
>{{ category }}</XSection
> >
<i class="ph-smiley ph-bold ph-lg ph-fw ph-lg"></i>
</button>
<button
class="_button tab"
:class="{ active: tab === 'unicode' }"
@click="tab = 'unicode'"
>
<i class="ph-leaf ph-bold ph-lg ph-fw ph-lg"></i>
</button>
<button
class="_button tab"
:class="{ active: tab === 'tags' }"
@click="tab = 'tags'"
>
<i class="ph-hash ph-bold ph-lg ph-fw ph-lg"></i>
</button>
</div> </div>
</div> </div>
</FocusTrap> <div class="tabs">
<button
class="_button tab"
:class="{ active: tab === 'index' }"
@click="tab = 'index'"
>
<i class="ph-asterisk ph-bold ph-lg ph-fw ph-lg"></i>
</button>
<button
class="_button tab"
:class="{ active: tab === 'custom' }"
@click="tab = 'custom'"
>
<i class="ph-smiley ph-bold ph-lg ph-fw ph-lg"></i>
</button>
<button
class="_button tab"
:class="{ active: tab === 'unicode' }"
@click="tab = 'unicode'"
>
<i class="ph-leaf ph-bold ph-lg ph-fw ph-lg"></i>
</button>
<button
class="_button tab"
:class="{ active: tab === 'tags' }"
@click="tab = 'tags'"
>
<i class="ph-hash ph-bold ph-lg ph-fw ph-lg"></i>
</button>
</div>
</div>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
@ -174,7 +171,6 @@ import { deviceKind } from "@/scripts/device-kind";
import { emojiCategories, instance } from "@/instance"; import { emojiCategories, instance } from "@/instance";
import { i18n } from "@/i18n"; import { i18n } from "@/i18n";
import { defaultStore } from "@/store"; import { defaultStore } from "@/store";
import { FocusTrap } from 'focus-trap-vue';
const props = withDefaults( const props = withDefaults(
defineProps<{ defineProps<{

View File

@ -139,7 +139,7 @@ function close() {
height: 100px; height: 100px;
border-radius: 10px; border-radius: 10px;
&:hover, &:focus-visible { &:hover {
color: var(--accent); color: var(--accent);
background: var(--accentedBg); background: var(--accentedBg);
text-decoration: none; text-decoration: none;

View File

@ -138,10 +138,6 @@ watch(
background-position: center; background-position: center;
background-size: contain; background-size: contain;
background-repeat: no-repeat; background-repeat: no-repeat;
box-sizing: border-box;
&:focus-visible {
border: 2px solid var(--accent);
}
> .gif { > .gif {
background-color: var(--fg); background-color: var(--fg);

View File

@ -1,14 +1,14 @@
<template> <template>
<div ref="el" class="sfhdhdhr" tabindex="-1"> <div ref="el" class="sfhdhdhr">
<MkMenu <MkMenu
ref="menu" ref="menu"
:items="items" :items="items"
:align="align" :align="align"
:width="width" :width="width"
:as-drawer="false" :as-drawer="false"
@close="onChildClosed" @close="onChildClosed"
/> />
</div> </div>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
@ -23,6 +23,7 @@ import {
} from "vue"; } from "vue";
import MkMenu from "./MkMenu.vue"; import MkMenu from "./MkMenu.vue";
import { MenuItem } from "@/types/menu"; import { MenuItem } from "@/types/menu";
import * as os from "@/os";
const props = defineProps<{ const props = defineProps<{
items: MenuItem[]; items: MenuItem[];

View File

@ -1,188 +1,191 @@
<template> <template>
<FocusTrap v-bind:active="isActive"> <div>
<div tabindex="-1" v-focus> <div
<div ref="itemsEl"
ref="itemsEl" v-hotkey="keymap"
class="rrevdjwt _popup _shadow" class="rrevdjwt _popup _shadow"
:class="{ center: align === 'center', asDrawer }" :class="{ center: align === 'center', asDrawer }"
:style="{ :style="{
width: width && !asDrawer ? width + 'px' : '', width: width && !asDrawer ? width + 'px' : '',
maxHeight: maxHeight ? maxHeight + 'px' : '', maxHeight: maxHeight ? maxHeight + 'px' : '',
}" }"
@contextmenu.self="(e) => e.preventDefault()" @contextmenu.self="(e) => e.preventDefault()"
> >
<template v-for="(item, i) in items2"> <template v-for="(item, i) in items2">
<div v-if="item === null" class="divider"></div> <div v-if="item === null" class="divider"></div>
<span v-else-if="item.type === 'label'" class="label item"> <span v-else-if="item.type === 'label'" class="label item">
<span :style="item.textStyle || ''">{{ item.text }}</span> <span :style="item.textStyle || ''">{{ item.text }}</span>
</span>
<span
v-else-if="item.type === 'pending'"
class="pending item"
>
<span><MkEllipsis /></span>
</span>
<MkA
v-else-if="item.type === 'link'"
:to="item.to"
class="_button item"
@click.passive="close(true)"
@mouseenter.passive="onItemMouseEnter(item)"
@mouseleave.passive="onItemMouseLeave(item)"
>
<i
v-if="item.icon"
class="ph-fw ph-lg"
:class="item.icon"
></i>
<span v-else-if="item.icons">
<i
v-for="icon in item.icons"
class="ph-fw ph-lg"
:class="icon"
></i>
</span>
<MkAvatar
v-if="item.avatar"
:user="item.avatar"
class="avatar"
disableLink
/>
<span :style="item.textStyle || ''">{{ item.text }}</span>
<span v-if="item.indicate" class="indicator"
><i class="ph-circle ph-fill"></i
></span>
</MkA>
<a
v-else-if="item.type === 'a'"
:href="item.href"
:target="item.target"
:download="item.download"
class="_button item"
@click="close(true)"
@mouseenter.passive="onItemMouseEnter(item)"
@mouseleave.passive="onItemMouseLeave(item)"
>
<i
v-if="item.icon"
class="ph-fw ph-lg"
:class="item.icon"
></i>
<span v-else-if="item.icons">
<i
v-for="icon in item.icons"
class="ph-fw ph-lg"
:class="icon"
></i>
</span>
<span :style="item.textStyle || ''">{{ item.text }}</span>
<span v-if="item.indicate" class="indicator"
><i class="ph-circle ph-fill"></i
></span>
</a>
<button
v-else-if="item.type === 'user' && !items.hidden"
class="_button item"
:class="{ active: item.active }"
:disabled="item.active"
@click="clicked(item.action, $event)"
@mouseenter.passive="onItemMouseEnter(item)"
@mouseleave.passive="onItemMouseLeave(item)"
>
<MkAvatar :user="item.user" class="avatar" disableLink /><MkUserName
:user="item.user"
/>
<span v-if="item.indicate" class="indicator"
><i class="ph-circle ph-fill"></i
></span>
</button>
<span
v-else-if="item.type === 'switch'"
class="item"
@mouseenter.passive="onItemMouseEnter(item)"
@mouseleave.passive="onItemMouseLeave(item)"
>
<FormSwitch
v-model="item.ref"
:disabled="item.disabled"
class="form-switch"
:style="item.textStyle || ''"
>{{ item.text }}</FormSwitch
>
</span>
<button
v-else-if="item.type === 'parent'"
class="_button item parent"
:class="{ childShowing: childShowingItem === item }"
@mouseenter="showChildren(item, $event)"
@click="showChildren(item, $event)"
>
<i
v-if="item.icon"
class="ph-fw ph-lg"
:class="item.icon"
></i>
<span v-else-if="item.icons">
<i
v-for="icon in item.icons"
class="ph-fw ph-lg"
:class="icon"
></i>
</span>
<span :style="item.textStyle || ''">{{ item.text }}</span>
<span class="caret"
><i class="ph-caret-right ph-bold ph-lg ph-fw ph-lg"></i
></span>
</button>
<button
v-else-if="!item.hidden"
class="_button item"
:class="{ danger: item.danger, active: item.active }"
:disabled="item.active"
@click="clicked(item.action, $event)"
@mouseenter.passive="onItemMouseEnter(item)"
@mouseleave.passive="onItemMouseLeave(item)"
>
<i
v-if="item.icon"
class="ph-fw ph-lg"
:class="item.icon"
></i>
<span v-else-if="item.icons">
<i
v-for="icon in item.icons"
class="ph-fw ph-lg"
:class="icon"
></i>
</span>
<MkAvatar
v-if="item.avatar"
:user="item.avatar"
class="avatar"
disableLink
/>
<span :style="item.textStyle || ''">{{ item.text }}</span>
<span v-if="item.indicate" class="indicator"
><i class="ph-circle ph-fill"></i
></span>
</button>
</template>
<span v-if="items2.length === 0" class="none item">
<span>{{ i18n.ts.none }}</span>
</span> </span>
</div> <span
<div v-if="childMenu" class="child"> v-else-if="item.type === 'pending'"
<XChild :tabindex="i"
ref="child" class="pending item"
:items="childMenu" >
:target-element="childTarget" <span><MkEllipsis /></span>
:root-element="itemsEl" </span>
showing <MkA
@actioned="childActioned" v-else-if="item.type === 'link'"
/> :to="item.to"
</div> :tabindex="i"
class="_button item"
@click.passive="close(true)"
@mouseenter.passive="onItemMouseEnter(item)"
@mouseleave.passive="onItemMouseLeave(item)"
>
<i
v-if="item.icon"
class="ph-fw ph-lg"
:class="item.icon"
></i>
<span v-else-if="item.icons">
<i
v-for="icon in item.icons"
class="ph-fw ph-lg"
:class="icon"
></i>
</span>
<MkAvatar
v-if="item.avatar"
:user="item.avatar"
class="avatar"
/>
<span :style="item.textStyle || ''">{{ item.text }}</span>
<span v-if="item.indicate" class="indicator"
><i class="ph-circle ph-fill"></i
></span>
</MkA>
<a
v-else-if="item.type === 'a'"
:href="item.href"
:target="item.target"
:download="item.download"
:tabindex="i"
class="_button item"
@click="close(true)"
@mouseenter.passive="onItemMouseEnter(item)"
@mouseleave.passive="onItemMouseLeave(item)"
>
<i
v-if="item.icon"
class="ph-fw ph-lg"
:class="item.icon"
></i>
<span v-else-if="item.icons">
<i
v-for="icon in item.icons"
class="ph-fw ph-lg"
:class="icon"
></i>
</span>
<span :style="item.textStyle || ''">{{ item.text }}</span>
<span v-if="item.indicate" class="indicator"
><i class="ph-circle ph-fill"></i
></span>
</a>
<button
v-else-if="item.type === 'user' && !items.hidden"
:tabindex="i"
class="_button item"
:class="{ active: item.active }"
:disabled="item.active"
@click="clicked(item.action, $event)"
@mouseenter.passive="onItemMouseEnter(item)"
@mouseleave.passive="onItemMouseLeave(item)"
>
<MkAvatar :user="item.user" class="avatar" /><MkUserName
:user="item.user"
/>
<span v-if="item.indicate" class="indicator"
><i class="ph-circle ph-fill"></i
></span>
</button>
<span
v-else-if="item.type === 'switch'"
:tabindex="i"
class="item"
@mouseenter.passive="onItemMouseEnter(item)"
@mouseleave.passive="onItemMouseLeave(item)"
>
<FormSwitch
v-model="item.ref"
:disabled="item.disabled"
class="form-switch"
:style="item.textStyle || ''"
>{{ item.text }}</FormSwitch
>
</span>
<button
v-else-if="item.type === 'parent'"
:tabindex="i"
class="_button item parent"
:class="{ childShowing: childShowingItem === item }"
@mouseenter="showChildren(item, $event)"
>
<i
v-if="item.icon"
class="ph-fw ph-lg"
:class="item.icon"
></i>
<span v-else-if="item.icons">
<i
v-for="icon in item.icons"
class="ph-fw ph-lg"
:class="icon"
></i>
</span>
<span :style="item.textStyle || ''">{{ item.text }}</span>
<span class="caret"
><i class="ph-caret-right ph-bold ph-lg ph-fw ph-lg"></i
></span>
</button>
<button
v-else-if="!item.hidden"
:tabindex="i"
class="_button item"
:class="{ danger: item.danger, active: item.active }"
:disabled="item.active"
@click="clicked(item.action, $event)"
@mouseenter.passive="onItemMouseEnter(item)"
@mouseleave.passive="onItemMouseLeave(item)"
>
<i
v-if="item.icon"
class="ph-fw ph-lg"
:class="item.icon"
></i>
<span v-else-if="item.icons">
<i
v-for="icon in item.icons"
class="ph-fw ph-lg"
:class="icon"
></i>
</span>
<MkAvatar
v-if="item.avatar"
:user="item.avatar"
class="avatar"
/>
<span :style="item.textStyle || ''">{{ item.text }}</span>
<span v-if="item.indicate" class="indicator"
><i class="ph-circle ph-fill"></i
></span>
</button>
</template>
<span v-if="items2.length === 0" class="none item">
<span>{{ i18n.ts.none }}</span>
</span>
</div> </div>
</FocusTrap> <div v-if="childMenu" class="child">
<XChild
ref="child"
:items="childMenu"
:target-element="childTarget"
:root-element="itemsEl"
showing
@actioned="childActioned"
/>
</div>
</div>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
@ -203,7 +206,6 @@ import FormSwitch from "@/components/form/switch.vue";
import { MenuItem, InnerMenuItem, MenuPending, MenuAction } from "@/types/menu"; import { MenuItem, InnerMenuItem, MenuPending, MenuAction } from "@/types/menu";
import * as os from "@/os"; import * as os from "@/os";
import { i18n } from "@/i18n"; import { i18n } from "@/i18n";
import { FocusTrap } from 'focus-trap-vue';
const XChild = defineAsyncComponent(() => import("./MkMenu.child.vue")); const XChild = defineAsyncComponent(() => import("./MkMenu.child.vue"));
@ -226,6 +228,12 @@ let items2: InnerMenuItem[] = $ref([]);
let child = $ref<InstanceType<typeof XChild>>(); let child = $ref<InstanceType<typeof XChild>>();
let keymap = computed(() => ({
"up|k|shift+tab": focusUp,
"down|j|tab": focusDown,
esc: close,
}));
let childShowingItem = $ref<MenuItem | null>(); let childShowingItem = $ref<MenuItem | null>();
watch( watch(
@ -356,7 +364,8 @@ onBeforeUnmount(() => {
font-size: 0.9em; font-size: 0.9em;
line-height: 20px; line-height: 20px;
text-align: left; text-align: left;
outline: none; overflow: hidden;
text-overflow: ellipsis;
&:before { &:before {
content: ""; content: "";
@ -380,7 +389,7 @@ onBeforeUnmount(() => {
transform: translateY(0em); transform: translateY(0em);
} }
&:not(:disabled):hover, &:focus-visible { &:not(:disabled):hover {
color: var(--accent); color: var(--accent);
text-decoration: none; text-decoration: none;
@ -388,9 +397,6 @@ onBeforeUnmount(() => {
background: var(--accentedBg); background: var(--accentedBg);
} }
} }
&:focus-visible:before {
outline: auto;
}
&.danger { &.danger {
color: #eb6f92; color: #eb6f92;

View File

@ -14,59 +14,54 @@
:duration="transitionDuration" :duration="transitionDuration"
appear appear
@after-leave="emit('closed')" @after-leave="emit('closed')"
@keyup.esc="emit('click')"
@enter="emit('opening')" @enter="emit('opening')"
@after-enter="onOpened" @after-enter="onOpened"
> >
<FocusTrap v-model:active="isActive"> <div
v-show="manualShowing != null ? manualShowing : showing"
v-hotkey.global="keymap"
:class="[
$style.root,
{
[$style.drawer]: type === 'drawer',
[$style.dialog]: type === 'dialog' || type === 'dialog:top',
[$style.popup]: type === 'popup',
},
]"
:style="{
zIndex,
pointerEvents: (manualShowing != null ? manualShowing : showing)
? 'auto'
: 'none',
'--transformOrigin': transformOrigin,
}"
>
<div <div
v-show="manualShowing != null ? manualShowing : showing" class="_modalBg data-cy-bg"
v-hotkey.global="keymap"
:class="[ :class="[
$style.root, $style.bg,
{ {
[$style.drawer]: type === 'drawer', [$style.bgTransparent]: isEnableBgTransparent,
[$style.dialog]: type === 'dialog' || type === 'dialog:top', 'data-cy-transparent': isEnableBgTransparent,
[$style.popup]: type === 'popup',
}, },
]" ]"
:style="{ :style="{ zIndex }"
zIndex, @click="onBgClick"
pointerEvents: (manualShowing != null ? manualShowing : showing) @mousedown="onBgClick"
? 'auto' @contextmenu.prevent.stop="() => {}"
: 'none', ></div>
'--transformOrigin': transformOrigin, <div
}" ref="content"
tabindex="-1" :class="[
v-focus $style.content,
{ [$style.fixed]: fixed, top: type === 'dialog:top' },
]"
:style="{ zIndex }"
@click.self="onBgClick"
> >
<div <slot :max-height="maxHeight" :type="type"></slot>
class="_modalBg data-cy-bg"
:class="[
$style.bg,
{
[$style.bgTransparent]: isEnableBgTransparent,
'data-cy-transparent': isEnableBgTransparent,
},
]"
:style="{ zIndex }"
@click="onBgClick"
@mousedown="onBgClick"
@contextmenu.prevent.stop="() => {}"
></div>
<div
ref="content"
:class="[
$style.content,
{ [$style.fixed]: fixed, top: type === 'dialog:top' },
]"
:style="{ zIndex }"
@click.self="onBgClick"
>
<slot :max-height="maxHeight" :type="type"></slot>
</div>
</div> </div>
</FocusTrap> </div>
</Transition> </Transition>
</template> </template>
@ -76,7 +71,6 @@ import * as os from "@/os";
import { isTouchUsing } from "@/scripts/touch"; import { isTouchUsing } from "@/scripts/touch";
import { defaultStore } from "@/store"; import { defaultStore } from "@/store";
import { deviceKind } from "@/scripts/device-kind"; import { deviceKind } from "@/scripts/device-kind";
import { FocusTrap } from 'focus-trap-vue';
function getFixedContainer(el: Element | null): Element | null { function getFixedContainer(el: Element | null): Element | null {
if (el == null || el.tagName === "BODY") return null; if (el == null || el.tagName === "BODY") return null;
@ -172,7 +166,6 @@ let transitionDuration = $computed(() =>
let contentClicking = false; let contentClicking = false;
const focusedElement = document.activeElement;
function close(opts: { useSendAnimation?: boolean } = {}) { function close(opts: { useSendAnimation?: boolean } = {}) {
if (opts.useSendAnimation) { if (opts.useSendAnimation) {
useSendAnime = true; useSendAnime = true;
@ -182,12 +175,10 @@ function close(opts: { useSendAnimation?: boolean } = {}) {
if (props.src) props.src.style.pointerEvents = "auto"; if (props.src) props.src.style.pointerEvents = "auto";
showing = false; showing = false;
emit("close"); emit("close");
focusedElement.focus();
} }
function onBgClick() { function onBgClick() {
if (contentClicking) return; if (contentClicking) return;
focusedElement.focus();
emit("click"); emit("click");
} }
@ -490,7 +481,6 @@ defineExpose({
} }
.root { .root {
outline: none;
&.dialog { &.dialog {
> .content { > .content {
position: fixed; position: fixed;

View File

@ -158,7 +158,6 @@ function onContextmenu(ev: MouseEvent) {
flex-direction: column; flex-direction: column;
contain: content; contain: content;
border-radius: var(--radius); border-radius: var(--radius);
margin: auto;
--root-margin: 24px; --root-margin: 24px;

View File

@ -3,64 +3,59 @@
ref="modal" ref="modal"
:prefer-type="'dialog'" :prefer-type="'dialog'"
@click="onBgClick" @click="onBgClick"
@keyup.esc="$emit('close')"
@closed="$emit('closed')" @closed="$emit('closed')"
> >
<FocusTrap v-model:active="isActive"> <div
<div ref="rootEl"
ref="rootEl" class="ebkgoccj"
class="ebkgoccj" :style="{
:style="{ width: `${width}px`,
width: `${width}px`, height: scroll
height: scroll ? height
? height ? `${height}px`
? `${height}px` : null
: null : height
: height ? `min(${height}px, 100%)`
? `min(${height}px, 100%)` : '100%',
: '100%', }"
}" @keydown="onKeydown"
@keydown="onKeydown" >
tabindex="-1" <div ref="headerEl" class="header">
> <button
<div ref="headerEl" class="header"> v-if="withOkButton"
<button class="_button"
v-if="withOkButton" @click="$emit('close')"
class="_button" >
@click="$emit('close')" <i class="ph-x ph-bold ph-lg"></i>
> </button>
<i class="ph-x ph-bold ph-lg"></i> <span class="title">
</button> <slot name="header"></slot>
<span class="title"> </span>
<slot name="header"></slot> <button
</span> v-if="!withOkButton"
<button class="_button"
v-if="!withOkButton" @click="$emit('close')"
class="_button" >
@click="$emit('close')" <i class="ph-x ph-bold ph-lg"></i>
> </button>
<i class="ph-x ph-bold ph-lg"></i> <button
</button> v-if="withOkButton"
<button class="_button"
v-if="withOkButton" :disabled="okButtonDisabled"
class="_button" @click="$emit('ok')"
:disabled="okButtonDisabled" >
@click="$emit('ok')" <i class="ph-check ph-bold ph-lg"></i>
> </button>
<i class="ph-check ph-bold ph-lg"></i>
</button>
</div>
<div class="body">
<slot :width="bodyWidth" :height="bodyHeight"></slot>
</div>
</div> </div>
</FocusTrap> <div class="body">
<slot :width="bodyWidth" :height="bodyHeight"></slot>
</div>
</div>
</MkModal> </MkModal>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { onMounted, onUnmounted } from "vue"; import { onMounted, onUnmounted } from "vue";
import { FocusTrap } from 'focus-trap-vue';
import MkModal from "./MkModal.vue"; import MkModal from "./MkModal.vue";
const props = withDefaults( const props = withDefaults(

View File

@ -84,7 +84,6 @@
:detailedView="detailedView" :detailedView="detailedView"
:parentId="appearNote.parentId" :parentId="appearNote.parentId"
@push="(e) => router.push(notePage(e))" @push="(e) => router.push(notePage(e))"
@focusfooter="footerEl.focus()"
></MkSubNoteContent> ></MkSubNoteContent>
<div v-if="translating || translation" class="translation"> <div v-if="translating || translation" class="translation">
<MkLoading v-if="translating" mini /> <MkLoading v-if="translating" mini />
@ -118,7 +117,7 @@
<MkTime :time="appearNote.createdAt" mode="absolute" /> <MkTime :time="appearNote.createdAt" mode="absolute" />
</MkA> </MkA>
</div> </div>
<footer ref="footerEl" class="footer" @click.stop tabindex="-1"> <footer ref="el" class="footer" @click.stop>
<XReactionsViewer <XReactionsViewer
v-if="enableEmojiReactions" v-if="enableEmojiReactions"
ref="reactionsViewer" ref="reactionsViewer"
@ -279,7 +278,6 @@ const isRenote =
note.poll == null; note.poll == null;
const el = ref<HTMLElement>(); const el = ref<HTMLElement>();
const footerEl = ref<HTMLElement>();
const menuButton = ref<HTMLElement>(); const menuButton = ref<HTMLElement>();
const starButton = ref<InstanceType<typeof XStarButton>>(); const starButton = ref<InstanceType<typeof XStarButton>>();
const renoteButton = ref<InstanceType<typeof XRenoteButton>>(); const renoteButton = ref<InstanceType<typeof XRenoteButton>>();
@ -300,8 +298,8 @@ const keymap = {
r: () => reply(true), r: () => reply(true),
"e|a|plus": () => react(true), "e|a|plus": () => react(true),
q: () => renoteButton.value.renote(true), q: () => renoteButton.value.renote(true),
"up|k": focusBefore, "up|k|shift+tab": focusBefore,
"down|j": focusAfter, "down|j|tab": focusAfter,
esc: blur, esc: blur,
"m|o": () => menu(true), "m|o": () => menu(true),
s: () => showContent.value !== showContent.value, s: () => showContent.value !== showContent.value,

View File

@ -1,6 +1,6 @@
<template> <template>
<div v-size="{ min: [350, 500] }" class="fefdfafb"> <div v-size="{ min: [350, 500] }" class="fefdfafb">
<MkAvatar class="avatar" :user="$i" disableLink /> <MkAvatar class="avatar" :user="$i" />
<div class="main"> <div class="main">
<div class="header"> <div class="header">
<MkUserName :user="$i" /> <MkUserName :user="$i" />

View File

@ -26,7 +26,6 @@
:note="note" :note="note"
:parentId="appearNote.parentId" :parentId="appearNote.parentId"
:conversation="conversation" :conversation="conversation"
@focusfooter="footerEl.focus()"
/> />
<div v-if="translating || translation" class="translation"> <div v-if="translating || translation" class="translation">
<MkLoading v-if="translating" mini /> <MkLoading v-if="translating" mini />
@ -47,7 +46,7 @@
</div> </div>
</div> </div>
</div> </div>
<footer ref="footerEl" class="footer" @click.stop tabindex="-1"> <footer class="footer" @click.stop>
<XReactionsViewer <XReactionsViewer
v-if="enableEmojiReactions" v-if="enableEmojiReactions"
ref="reactionsViewer" ref="reactionsViewer"
@ -213,7 +212,6 @@ const isRenote =
note.poll == null; note.poll == null;
const el = ref<HTMLElement>(); const el = ref<HTMLElement>();
const footerEl = ref<HTMLElement>();
const menuButton = ref<HTMLElement>(); const menuButton = ref<HTMLElement>();
const starButton = ref<InstanceType<typeof XStarButton>>(); const starButton = ref<InstanceType<typeof XStarButton>>();
const renoteButton = ref<InstanceType<typeof XRenoteButton>>(); const renoteButton = ref<InstanceType<typeof XRenoteButton>>();

View File

@ -7,8 +7,6 @@
:transparent-bg="true" :transparent-bg="true"
@click="modal.close()" @click="modal.close()"
@closed="emit('closed')" @closed="emit('closed')"
tabindex="-1"
v-focus
> >
<MkMenu <MkMenu
:items="items" :items="items"

View File

@ -198,6 +198,7 @@ export default defineComponent({
height: 64px; height: 64px;
margin-right: 4px; margin-right: 4px;
border-radius: 4px; border-radius: 4px;
overflow: hidden;
cursor: move; cursor: move;
&:hover > .remove { &:hover > .remove {

View File

@ -35,11 +35,7 @@
class="content" class="content"
:class="{ collapsed, isLong, showContent: note.cw && !showContent }" :class="{ collapsed, isLong, showContent: note.cw && !showContent }"
> >
<XCwButton ref="cwButton" v-if="note.cw && !showContent" v-model="showContent" :note="note" v-on:keydown="focusFooter" /> <div class="body">
<div
class="body"
v-bind="{ 'aria-label': !showContent ? '' : null, 'tabindex': !showContent ? '-1' : null }"
>
<span v-if="note.deletedAt" style="opacity: 0.5" <span v-if="note.deletedAt" style="opacity: 0.5"
>({{ i18n.ts.deleted }})</span >({{ i18n.ts.deleted }})</span
> >
@ -100,11 +96,6 @@
<XNoteSimple :note="note.renote" /> <XNoteSimple :note="note.renote" />
</div> </div>
</template> </template>
<div
v-if="note.cw && !showContent"
tabindex="0"
v-on:focus="cwButton?.focus()"
></div>
</div> </div>
<button <button
v-if="isLong && collapsed" v-if="isLong && collapsed"
@ -126,7 +117,7 @@
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { ref } from "vue"; import {} from "vue";
import * as misskey from "calckey-js"; import * as misskey from "calckey-js";
import * as mfm from "mfm-js"; import * as mfm from "mfm-js";
import XNoteSimple from "@/components/MkNoteSimple.vue"; import XNoteSimple from "@/components/MkNoteSimple.vue";
@ -147,10 +138,8 @@ const props = defineProps<{
const emit = defineEmits<{ const emit = defineEmits<{
(ev: "push", v): void; (ev: "push", v): void;
(ev: "focusfooter"): void;
}>(); }>();
const cwButton = ref<HTMLElement>();
const isLong = const isLong =
!props.detailedView && !props.detailedView &&
props.note.cw == null && props.note.cw == null &&
@ -162,13 +151,6 @@ const urls = props.note.text
: null; : null;
let showContent = $ref(false); let showContent = $ref(false);
function focusFooter(ev) {
if (ev.key == "Tab" && !ev.getModifierState("Shift")) {
emit("focusfooter");
}
}
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
@ -260,9 +242,6 @@ function focusFooter(ev) {
margin-top: -50px; margin-top: -50px;
padding-top: 50px; padding-top: 50px;
overflow: hidden; overflow: hidden;
user-select: none;
-webkit-user-select: none;
-moz-user-select: none;
} }
&.collapsed > .body { &.collapsed > .body {
box-sizing: border-box; box-sizing: border-box;

View File

@ -9,6 +9,7 @@
v-if="item.type === 'a'" v-if="item.type === 'a'"
:href="item.href" :href="item.href"
:target="item.target" :target="item.target"
:tabindex="i"
class="_button item" class="_button item"
:class="{ danger: item.danger, active: item.active }" :class="{ danger: item.danger, active: item.active }"
> >
@ -21,6 +22,7 @@
</a> </a>
<button <button
v-else-if="item.type === 'button'" v-else-if="item.type === 'button'"
:tabindex="i"
class="_button item" class="_button item"
:class="{ danger: item.danger, active: item.active }" :class="{ danger: item.danger, active: item.active }"
:disabled="item.active" :disabled="item.active"
@ -36,6 +38,7 @@
<MkA <MkA
v-else v-else
:to="item.to" :to="item.to"
:tabindex="i"
class="_button item" class="_button item"
:class="{ danger: item.danger, active: item.active }" :class="{ danger: item.danger, active: item.active }"
> >
@ -96,7 +99,7 @@ export default defineComponent({
font-size: 0.9em; font-size: 0.9em;
margin-bottom: 0.3rem; margin-bottom: 0.3rem;
&:hover, &:focus-visible { &:hover {
text-decoration: none; text-decoration: none;
background: var(--panelHighlight); background: var(--panelHighlight);
} }

View File

@ -46,7 +46,6 @@
:user="user" :user="user"
class="avatar" class="avatar"
:show-indicator="true" :show-indicator="true"
disableLink
/> />
<div class="body"> <div class="body">
<MkUserName :user="user" class="name" /> <MkUserName :user="user" class="name" />
@ -74,7 +73,6 @@
:user="user" :user="user"
class="avatar" class="avatar"
:show-indicator="true" :show-indicator="true"
disableLink
/> />
<div class="body"> <div class="body">
<MkUserName :user="user" class="name" /> <MkUserName :user="user" class="name" />

View File

@ -7,7 +7,7 @@
> >
<div class="beaffaef"> <div class="beaffaef">
<div v-for="u in users" :key="u.id" class="user"> <div v-for="u in users" :key="u.id" class="user">
<MkAvatar class="avatar" :user="u" disableLink /> <MkAvatar class="avatar" :user="u" />
<MkUserName class="name" :user="u" :nowrap="true" /> <MkUserName class="name" :user="u" :nowrap="true" />
</div> </div>
<div v-if="users.length < count" class="omitted"> <div v-if="users.length < count" class="omitted">

View File

@ -1,7 +1,7 @@
<template> <template>
<div class="vjoppmmu"> <div class="vjoppmmu">
<template v-if="edit"> <template v-if="edit">
<header tabindex="-1" v-focus> <header>
<MkSelect <MkSelect
v-model="widgetAdderSelected" v-model="widgetAdderSelected"
style="margin-bottom: var(--margin)" style="margin-bottom: var(--margin)"

View File

@ -1,6 +1,6 @@
<template> <template>
<div class="dwzlatin" :class="{ opened }"> <div class="dwzlatin" :class="{ opened }">
<button class="header _button" @click="toggle"> <div class="header _button" @click="toggle">
<span class="icon"><slot name="icon"></slot></span> <span class="icon"><slot name="icon"></slot></span>
<span class="text"><slot name="label"></slot></span> <span class="text"><slot name="label"></slot></span>
<span class="right"> <span class="right">
@ -8,7 +8,7 @@
<i v-if="opened" class="ph-caret-up ph-bold ph-lg icon"></i> <i v-if="opened" class="ph-caret-up ph-bold ph-lg icon"></i>
<i v-else class="ph-caret-down ph-bold ph-lg icon"></i> <i v-else class="ph-caret-down ph-bold ph-lg icon"></i>
</span> </span>
</button> </div>
<KeepAlive> <KeepAlive>
<div v-if="openedAtLeastOnce" v-show="opened" class="body"> <div v-if="openedAtLeastOnce" v-show="opened" class="body">
<MkSpacer :margin-min="14" :margin-max="22"> <MkSpacer :margin-min="14" :margin-max="22">

View File

@ -66,9 +66,6 @@ function toggle(): void {
&:hover { &:hover {
border-color: var(--inputBorderHover) !important; border-color: var(--inputBorderHover) !important;
} }
&:focus-within {
outline: auto;
}
&.checked { &.checked {
background-color: var(--accentedBg) !important; background-color: var(--accentedBg) !important;

View File

@ -99,9 +99,6 @@ const toggle = () => {
border-color: var(--inputBorderHover) !important; border-color: var(--inputBorderHover) !important;
} }
} }
&:focus-within > .button {
outline: auto;
}
> .label { > .label {
margin-left: 12px; margin-left: 12px;

View File

@ -19,7 +19,6 @@
class="avatar" class="avatar"
:user="$i" :user="$i"
:disable-preview="true" :disable-preview="true"
disableLink
/> />
</div> </div>
<template v-if="metadata"> <template v-if="metadata">
@ -34,7 +33,6 @@
:user="metadata.avatar" :user="metadata.avatar"
:disable-preview="true" :disable-preview="true"
:show-indicator="true" :show-indicator="true"
disableLink
/> />
<i <i
v-else-if="metadata.icon && !narrow" v-else-if="metadata.icon && !narrow"

View File

@ -5,9 +5,6 @@
:is="currentPageComponent" :is="currentPageComponent"
:key="key" :key="key"
v-bind="Object.fromEntries(currentPageProps)" v-bind="Object.fromEntries(currentPageProps)"
tabindex="-1"
v-focus
style="outline: none;"
/> />
<template #fallback> <template #fallback>

View File

@ -1,3 +0,0 @@
export default {
mounted: (el) => el.focus()
}

View File

@ -11,7 +11,6 @@ import anim from "./anim";
import clickAnime from "./click-anime"; import clickAnime from "./click-anime";
import panel from "./panel"; import panel from "./panel";
import adaptiveBorder from "./adaptive-border"; import adaptiveBorder from "./adaptive-border";
import focus from "./focus";
export default function (app: App) { export default function (app: App) {
app.directive("userPreview", userPreview); app.directive("userPreview", userPreview);
@ -26,5 +25,4 @@ export default function (app: App) {
app.directive("click-anime", clickAnime); app.directive("click-anime", clickAnime);
app.directive("panel", panel); app.directive("panel", panel);
app.directive("adaptive-border", adaptiveBorder); app.directive("adaptive-border", adaptiveBorder);
app.directive("focus", focus);
} }

View File

@ -76,32 +76,23 @@ export default {
ev.preventDefault(); ev.preventDefault();
}); });
function showTooltip() {
window.clearTimeout(self.showTimer);
window.clearTimeout(self.hideTimer);
self.showTimer = window.setTimeout(self.show, delay);
}
function hideTooltip() {
window.clearTimeout(self.showTimer);
window.clearTimeout(self.hideTimer);
self.hideTimer = window.setTimeout(self.close, delay);
}
el.addEventListener( el.addEventListener(
start, showTooltip, start,
{ passive: true }, () => {
); window.clearTimeout(self.showTimer);
el.addEventListener( window.clearTimeout(self.hideTimer);
"focusin", showTooltip, self.showTimer = window.setTimeout(self.show, delay);
},
{ passive: true }, { passive: true },
); );
el.addEventListener( el.addEventListener(
end, hideTooltip, end,
{ passive: true }, () => {
); window.clearTimeout(self.showTimer);
el.addEventListener( window.clearTimeout(self.hideTimer);
"focusout", hideTooltip, self.hideTimer = window.setTimeout(self.close, delay);
},
{ passive: true }, { passive: true },
); );

View File

@ -313,7 +313,11 @@ onUnmounted(() => {
font-weight: normal; font-weight: normal;
opacity: 0.7; opacity: 0.7;
&:hover, &:focus-visible, &.active { &:hover {
opacity: 1;
}
&.active {
opacity: 1; opacity: 1;
} }

View File

@ -12,7 +12,7 @@
class="user" class="user"
:to="`/user-info/${user.id}`" :to="`/user-info/${user.id}`"
> >
<MkAvatar :user="user" class="avatar" indicator disableLink /> <MkAvatar :user="user" class="avatar" indicator />
</MkA> </MkA>
</div> </div>
</Transition> </Transition>

View File

@ -371,6 +371,34 @@
<template #label>Pro account</template> <template #label>Pro account</template>
</FormSwitch> </FormSwitch>
</FormSection> </FormSection>
<FormSection>
<template #label>Libre Translate</template>
<FormInput
v-model="libreTranslateApiUrl"
class="_formBlock"
>
<template #prefix
><i class="ph-link ph-bold ph-lg"></i
></template>
<template #label
>Libre Translate API URL</template
>
</FormInput>
<FormInput
v-model="libreTranslateApiKey"
class="_formBlock"
>
<template #prefix
><i class="ph-key ph-bold ph-lg"></i
></template>
<template #label
>Libre Translate API Key</template
>
</FormInput>
</FormSection>
</div> </div>
</FormSuspense> </FormSuspense>
</MkSpacer> </MkSpacer>
@ -422,6 +450,8 @@ let swPublicKey: any = $ref(null);
let swPrivateKey: any = $ref(null); let swPrivateKey: any = $ref(null);
let deeplAuthKey: string = $ref(""); let deeplAuthKey: string = $ref("");
let deeplIsPro: boolean = $ref(false); let deeplIsPro: boolean = $ref(false);
let libreTranslateApiUrl: string = $ref("");
let libreTranslateApiKey: string = $ref("");
let defaultReaction: string = $ref(""); let defaultReaction: string = $ref("");
let defaultReactionCustom: string = $ref(""); let defaultReactionCustom: string = $ref("");
@ -456,6 +486,8 @@ async function init() {
swPrivateKey = meta.swPrivateKey; swPrivateKey = meta.swPrivateKey;
deeplAuthKey = meta.deeplAuthKey; deeplAuthKey = meta.deeplAuthKey;
deeplIsPro = meta.deeplIsPro; deeplIsPro = meta.deeplIsPro;
libreTranslateApiUrl = meta.libreTranslateApiUrl;
libreTranslateApiKey = meta.libreTranslateApiKey;
defaultReaction = ["⭐", "👍", "❤️"].includes(meta.defaultReaction) defaultReaction = ["⭐", "👍", "❤️"].includes(meta.defaultReaction)
? meta.defaultReaction ? meta.defaultReaction
: "custom"; : "custom";
@ -498,6 +530,8 @@ function save() {
swPrivateKey, swPrivateKey,
deeplAuthKey, deeplAuthKey,
deeplIsPro, deeplIsPro,
libreTranslateApiUrl,
libreTranslateApiKey,
defaultReaction, defaultReaction,
}).then(() => { }).then(() => {
fetchInstance(); fetchInstance();

View File

@ -23,7 +23,6 @@
class="avatar" class="avatar"
:user="req.follower" :user="req.follower"
:show-indicator="true" :show-indicator="true"
disableLink
/> />
<div class="body"> <div class="body">
<div class="name"> <div class="name">

View File

@ -6,14 +6,14 @@
{{ i18n.ts.addAccount }}</FormButton {{ i18n.ts.addAccount }}</FormButton
> >
<button <div
v-for="account in accounts" v-for="account in accounts"
:key="account.id" :key="account.id"
class="_panel _button lcjjdxlm" class="_panel _button lcjjdxlm"
@click="menu(account, $event)" @click="menu(account, $event)"
> >
<div class="avatar"> <div class="avatar">
<MkAvatar :user="account" class="avatar" disableLink /> <MkAvatar :user="account" class="avatar" />
</div> </div>
<div class="body"> <div class="body">
<div class="name"> <div class="name">
@ -23,7 +23,7 @@
<MkAcct :user="account" /> <MkAcct :user="account" />
</div> </div>
</div> </div>
</button> </div>
</FormSuspense> </FormSuspense>
</div> </div>
</template> </template>
@ -158,8 +158,6 @@ definePageMetadata({
.lcjjdxlm { .lcjjdxlm {
display: flex; display: flex;
padding: 16px; padding: 16px;
width: 100%;
text-align: unset;
> .avatar { > .avatar {
display: block; display: block;

View File

@ -204,6 +204,10 @@ hr {
pointer-events: none; pointer-events: none;
} }
&:focus-visible {
outline: none;
}
&:disabled { &:disabled {
opacity: 0.5; opacity: 0.5;
cursor: default; cursor: default;

View File

@ -18,7 +18,6 @@
<MkAvatar <MkAvatar
:user="$i" :user="$i"
class="icon" class="icon"
disableLink
/><!-- <MkAcct class="text" :user="$i"/> --> /><!-- <MkAcct class="text" :user="$i"/> -->
</button> </button>
</div> </div>

View File

@ -18,7 +18,6 @@
<MkAvatar <MkAvatar
:user="$i" :user="$i"
class="icon" class="icon"
disableLink
/><!-- <MkAcct class="text" :user="$i"/> --> /><!-- <MkAcct class="text" :user="$i"/> -->
</button> </button>
</div> </div>
@ -335,7 +334,6 @@ function more(ev: MouseEvent) {
} }
&:hover, &:hover,
&:focus-within,
&.active { &.active {
&:before { &:before {
background: var(--accentLighten); background: var(--accentLighten);
@ -400,6 +398,8 @@ function more(ev: MouseEvent) {
padding-left: 30px; padding-left: 30px;
line-height: 2.85rem; line-height: 2.85rem;
margin-bottom: 0.5rem; margin-bottom: 0.5rem;
text-overflow: ellipsis;
overflow: hidden;
white-space: nowrap; white-space: nowrap;
width: 100%; width: 100%;
text-align: left; text-align: left;
@ -425,12 +425,9 @@ function more(ev: MouseEvent) {
> .text { > .text {
position: relative; position: relative;
font-size: 0.9em; font-size: 0.9em;
overflow: hidden;
text-overflow: ellipsis;
} }
&:hover, &:hover {
&:focus-within {
text-decoration: none; text-decoration: none;
color: var(--navHoverFg); color: var(--navHoverFg);
transition: all 0.4s ease; transition: all 0.4s ease;
@ -440,8 +437,7 @@ function more(ev: MouseEvent) {
color: var(--navActive); color: var(--navActive);
} }
&:hover, &:hover,
&:focus-within,
&.active { &.active {
color: var(--accent); color: var(--accent);
transition: all 0.4s ease; transition: all 0.4s ease;
@ -532,7 +528,6 @@ function more(ev: MouseEvent) {
} }
&:hover, &:hover,
&:focus-within,
&.active { &.active {
&:before { &:before {
background: var(--accentLighten); background: var(--accentLighten);
@ -618,7 +613,6 @@ function more(ev: MouseEvent) {
} }
&:hover, &:hover,
&:focus-within,
&.active { &.active {
text-decoration: none; text-decoration: none;
color: var(--accent); color: var(--accent);
@ -648,12 +642,5 @@ function more(ev: MouseEvent) {
} }
} }
} }
.item {
outline: none;
&:focus-visible:before {
outline: auto;
}
}
} }
</style> </style>

View File

@ -83,7 +83,6 @@
<MkAvatar :user="$i" class="avatar" /><MkAcct <MkAvatar :user="$i" class="avatar" /><MkAcct
class="acct" class="acct"
:user="$i" :user="$i"
disableLink
/> />
</button> </button>
<div class="post" @click="post"> <div class="post" @click="post">

View File

@ -5,7 +5,7 @@
class="item _button account" class="item _button account"
@click="openAccountMenu" @click="openAccountMenu"
> >
<MkAvatar :user="$i" class="avatar" disableLink /><MkAcct <MkAvatar :user="$i" class="avatar" /><MkAcct
class="text" class="text"
:user="$i" :user="$i"
/> />
@ -299,7 +299,6 @@ function openInstanceMenu(ev: MouseEvent) {
width: 46px; width: 46px;
height: 46px; height: 46px;
padding: 0; padding: 0;
margin-inline: 0 !important;
} }
} }
@ -373,7 +372,6 @@ function openInstanceMenu(ev: MouseEvent) {
> i { > i {
width: 32px; width: 32px;
justify-content: center;
} }
> i, > i,

View File

@ -227,8 +227,6 @@ onMounted(() => {
} }
.gbhvwtnk { .gbhvwtnk {
display: flex;
justify-content: center;
$ui-font-size: 1em; $ui-font-size: 1em;
$widgets-hide-threshold: 1200px; $widgets-hide-threshold: 1200px;

View File

@ -19,12 +19,6 @@ importers:
'@tensorflow/tfjs': '@tensorflow/tfjs':
specifier: ^3.21.0 specifier: ^3.21.0
version: 3.21.0(seedrandom@3.0.5) version: 3.21.0(seedrandom@3.0.5)
focus-trap:
specifier: ^7.2.0
version: 7.2.0
focus-trap-vue:
specifier: ^4.0.1
version: 4.0.1(focus-trap@7.2.0)(vue@3.2.45)
js-yaml: js-yaml:
specifier: 4.1.0 specifier: 4.1.0
version: 4.1.0 version: 4.1.0
@ -3809,12 +3803,14 @@ packages:
'@vue/shared': 3.2.45 '@vue/shared': 3.2.45
estree-walker: 2.0.2 estree-walker: 2.0.2
source-map: 0.6.1 source-map: 0.6.1
dev: true
/@vue/compiler-dom@3.2.45: /@vue/compiler-dom@3.2.45:
resolution: {integrity: sha512-tyYeUEuKqqZO137WrZkpwfPCdiiIeXYCcJ8L4gWz9vqaxzIQRccTSwSWZ/Axx5YR2z+LvpUbmPNXxuBU45lyRw==} resolution: {integrity: sha512-tyYeUEuKqqZO137WrZkpwfPCdiiIeXYCcJ8L4gWz9vqaxzIQRccTSwSWZ/Axx5YR2z+LvpUbmPNXxuBU45lyRw==}
dependencies: dependencies:
'@vue/compiler-core': 3.2.45 '@vue/compiler-core': 3.2.45
'@vue/shared': 3.2.45 '@vue/shared': 3.2.45
dev: true
/@vue/compiler-sfc@2.7.14: /@vue/compiler-sfc@2.7.14:
resolution: {integrity: sha512-aNmNHyLPsw+sVvlQFQ2/8sjNuLtK54TC6cuKnVzAY93ks4ZBrvwQSnkkIh7bsbNhum5hJBS00wSDipQ937f5DA==} resolution: {integrity: sha512-aNmNHyLPsw+sVvlQFQ2/8sjNuLtK54TC6cuKnVzAY93ks4ZBrvwQSnkkIh7bsbNhum5hJBS00wSDipQ937f5DA==}
@ -3837,12 +3833,14 @@ packages:
magic-string: 0.25.9 magic-string: 0.25.9
postcss: 8.4.21 postcss: 8.4.21
source-map: 0.6.1 source-map: 0.6.1
dev: true
/@vue/compiler-ssr@3.2.45: /@vue/compiler-ssr@3.2.45:
resolution: {integrity: sha512-6BRaggEGqhWht3lt24CrIbQSRD5O07MTmd+LjAn5fJj568+R9eUD2F7wMQJjX859seSlrYog7sUtrZSd7feqrQ==} resolution: {integrity: sha512-6BRaggEGqhWht3lt24CrIbQSRD5O07MTmd+LjAn5fJj568+R9eUD2F7wMQJjX859seSlrYog7sUtrZSd7feqrQ==}
dependencies: dependencies:
'@vue/compiler-dom': 3.2.45 '@vue/compiler-dom': 3.2.45
'@vue/shared': 3.2.45 '@vue/shared': 3.2.45
dev: true
/@vue/reactivity-transform@3.2.45: /@vue/reactivity-transform@3.2.45:
resolution: {integrity: sha512-BHVmzYAvM7vcU5WmuYqXpwaBHjsS8T63jlKGWVtHxAHIoMIlmaMyurUSEs1Zcg46M4AYT5MtB1U274/2aNzjJQ==} resolution: {integrity: sha512-BHVmzYAvM7vcU5WmuYqXpwaBHjsS8T63jlKGWVtHxAHIoMIlmaMyurUSEs1Zcg46M4AYT5MtB1U274/2aNzjJQ==}
@ -3852,17 +3850,20 @@ packages:
'@vue/shared': 3.2.45 '@vue/shared': 3.2.45
estree-walker: 2.0.2 estree-walker: 2.0.2
magic-string: 0.25.9 magic-string: 0.25.9
dev: true
/@vue/reactivity@3.2.45: /@vue/reactivity@3.2.45:
resolution: {integrity: sha512-PRvhCcQcyEVohW0P8iQ7HDcIOXRjZfAsOds3N99X/Dzewy8TVhTCT4uXpAHfoKjVTJRA0O0K+6QNkDIZAxNi3A==} resolution: {integrity: sha512-PRvhCcQcyEVohW0P8iQ7HDcIOXRjZfAsOds3N99X/Dzewy8TVhTCT4uXpAHfoKjVTJRA0O0K+6QNkDIZAxNi3A==}
dependencies: dependencies:
'@vue/shared': 3.2.45 '@vue/shared': 3.2.45
dev: true
/@vue/runtime-core@3.2.45: /@vue/runtime-core@3.2.45:
resolution: {integrity: sha512-gzJiTA3f74cgARptqzYswmoQx0fIA+gGYBfokYVhF8YSXjWTUA2SngRzZRku2HbGbjzB6LBYSbKGIaK8IW+s0A==} resolution: {integrity: sha512-gzJiTA3f74cgARptqzYswmoQx0fIA+gGYBfokYVhF8YSXjWTUA2SngRzZRku2HbGbjzB6LBYSbKGIaK8IW+s0A==}
dependencies: dependencies:
'@vue/reactivity': 3.2.45 '@vue/reactivity': 3.2.45
'@vue/shared': 3.2.45 '@vue/shared': 3.2.45
dev: true
/@vue/runtime-dom@3.2.45: /@vue/runtime-dom@3.2.45:
resolution: {integrity: sha512-cy88YpfP5Ue2bDBbj75Cb4bIEZUMM/mAkDMfqDTpUYVgTf/kuQ2VQ8LebuZ8k6EudgH8pYhsGWHlY0lcxlvTwA==} resolution: {integrity: sha512-cy88YpfP5Ue2bDBbj75Cb4bIEZUMM/mAkDMfqDTpUYVgTf/kuQ2VQ8LebuZ8k6EudgH8pYhsGWHlY0lcxlvTwA==}
@ -3870,6 +3871,7 @@ packages:
'@vue/runtime-core': 3.2.45 '@vue/runtime-core': 3.2.45
'@vue/shared': 3.2.45 '@vue/shared': 3.2.45
csstype: 2.6.21 csstype: 2.6.21
dev: true
/@vue/server-renderer@3.2.45(vue@3.2.45): /@vue/server-renderer@3.2.45(vue@3.2.45):
resolution: {integrity: sha512-ebiMq7q24WBU1D6uhPK//2OTR1iRIyxjF5iVq/1a5I1SDMDyDu4Ts6fJaMnjrvD3MqnaiFkKQj+LKAgz5WIK3g==} resolution: {integrity: sha512-ebiMq7q24WBU1D6uhPK//2OTR1iRIyxjF5iVq/1a5I1SDMDyDu4Ts6fJaMnjrvD3MqnaiFkKQj+LKAgz5WIK3g==}
@ -3879,9 +3881,11 @@ packages:
'@vue/compiler-ssr': 3.2.45 '@vue/compiler-ssr': 3.2.45
'@vue/shared': 3.2.45 '@vue/shared': 3.2.45
vue: 3.2.45 vue: 3.2.45
dev: true
/@vue/shared@3.2.45: /@vue/shared@3.2.45:
resolution: {integrity: sha512-Ewzq5Yhimg7pSztDV+RH1UDKBzmtqieXQlpTVm2AwraoRL/Rks96mvd8Vgi7Lj+h+TH8dv7mXD3FRZR3TUvbSg==} resolution: {integrity: sha512-Ewzq5Yhimg7pSztDV+RH1UDKBzmtqieXQlpTVm2AwraoRL/Rks96mvd8Vgi7Lj+h+TH8dv7mXD3FRZR3TUvbSg==}
dev: true
/@webassemblyjs/ast@1.11.1: /@webassemblyjs/ast@1.11.1:
resolution: {integrity: sha512-ukBh14qFLjxTQNTXocdyksN5QdM28S1CxHt2rdskFyL+xFV7VremuBLVbmCePj+URalXBENx/9Lm7lnhihtCSw==} resolution: {integrity: sha512-ukBh14qFLjxTQNTXocdyksN5QdM28S1CxHt2rdskFyL+xFV7VremuBLVbmCePj+URalXBENx/9Lm7lnhihtCSw==}
@ -6070,6 +6074,7 @@ packages:
/csstype@2.6.21: /csstype@2.6.21:
resolution: {integrity: sha512-Z1PhmomIfypOpoMjRQB70jfvy/wxT50qW08YXO5lMIJkrdq4yOTR+AW7FqutScmB9NkLwxo+jU+kZLbofZZq/w==} resolution: {integrity: sha512-Z1PhmomIfypOpoMjRQB70jfvy/wxT50qW08YXO5lMIJkrdq4yOTR+AW7FqutScmB9NkLwxo+jU+kZLbofZZq/w==}
dev: true
/csstype@3.1.1: /csstype@3.1.1:
resolution: {integrity: sha512-DJR/VvkAvSZW9bTouZue2sSxDwdTN92uHjqeKVm+0dAqdfNykRzQ95tay8aXMBAAPpUiq4Qcug2L7neoRh2Egw==} resolution: {integrity: sha512-DJR/VvkAvSZW9bTouZue2sSxDwdTN92uHjqeKVm+0dAqdfNykRzQ95tay8aXMBAAPpUiq4Qcug2L7neoRh2Egw==}
@ -6974,6 +6979,7 @@ packages:
/estree-walker@2.0.2: /estree-walker@2.0.2:
resolution: {integrity: sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==} resolution: {integrity: sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==}
dev: true
/esutils@2.0.3: /esutils@2.0.3:
resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==}
@ -7439,22 +7445,6 @@ packages:
readable-stream: 2.3.7 readable-stream: 2.3.7
dev: true dev: true
/focus-trap-vue@4.0.1(focus-trap@7.2.0)(vue@3.2.45):
resolution: {integrity: sha512-2iqOeoSvgq7Um6aL+255a/wXPskj6waLq2oKCa4gOnMORPo15JX7wN6J5bl1SMhMlTlkHXGSrQ9uJPJLPZDl5w==}
peerDependencies:
focus-trap: ^7.0.0
vue: ^3.0.0
dependencies:
focus-trap: 7.2.0
vue: 3.2.45
dev: false
/focus-trap@7.2.0:
resolution: {integrity: sha512-v4wY6HDDYvzkBy4735kW5BUEuw6Yz9ABqMYLuTNbzAFPcBOGiGHwwcNVMvUz4G0kgSYh13wa/7TG3XwTeT4O/A==}
dependencies:
tabbable: 6.1.1
dev: false
/follow-redirects@1.15.2(debug@4.3.4): /follow-redirects@1.15.2(debug@4.3.4):
resolution: {integrity: sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA==} resolution: {integrity: sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA==}
engines: {node: '>=4.0'} engines: {node: '>=4.0'}
@ -10360,6 +10350,7 @@ packages:
resolution: {integrity: sha512-RmF0AsMzgt25qzqqLc1+MbHmhdx0ojF2Fvs4XnOqz2ZOBXzzkEwc/dJQZCYHAn7v1jbVOjAZfK8msRn4BxO4VQ==} resolution: {integrity: sha512-RmF0AsMzgt25qzqqLc1+MbHmhdx0ojF2Fvs4XnOqz2ZOBXzzkEwc/dJQZCYHAn7v1jbVOjAZfK8msRn4BxO4VQ==}
dependencies: dependencies:
sourcemap-codec: 1.4.8 sourcemap-codec: 1.4.8
dev: true
/mailcheck@1.1.1: /mailcheck@1.1.1:
resolution: {integrity: sha512-3WjL8+ZDouZwKlyJBMp/4LeziLFXgleOdsYu87piGcMLqhBzCsy2QFdbtAwv757TFC/rtqd738fgJw1tFQCSgA==} resolution: {integrity: sha512-3WjL8+ZDouZwKlyJBMp/4LeziLFXgleOdsYu87piGcMLqhBzCsy2QFdbtAwv757TFC/rtqd738fgJw1tFQCSgA==}
@ -13276,6 +13267,7 @@ packages:
/sourcemap-codec@1.4.8: /sourcemap-codec@1.4.8:
resolution: {integrity: sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA==} resolution: {integrity: sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA==}
deprecated: Please use @jridgewell/sourcemap-codec instead deprecated: Please use @jridgewell/sourcemap-codec instead
dev: true
/sparkles@1.0.1: /sparkles@1.0.1:
resolution: {integrity: sha512-dSO0DDYUahUt/0/pD/Is3VIm5TGJjludZ0HVymmhYF6eNA53PVLhnUk0znSYbH8IYBuJdCE+1luR22jNLMaQdw==} resolution: {integrity: sha512-dSO0DDYUahUt/0/pD/Is3VIm5TGJjludZ0HVymmhYF6eNA53PVLhnUk0znSYbH8IYBuJdCE+1luR22jNLMaQdw==}
@ -13694,10 +13686,6 @@ packages:
resolution: {integrity: sha512-g9rPT3V1Q4WjWFZ/t5BdGC1mT/FpYnsLdBl+M5e6MlRkuE1RSR+R43wcY/3mKI59B9KEr+vxdWCuWNMD3oNHKA==} resolution: {integrity: sha512-g9rPT3V1Q4WjWFZ/t5BdGC1mT/FpYnsLdBl+M5e6MlRkuE1RSR+R43wcY/3mKI59B9KEr+vxdWCuWNMD3oNHKA==}
dev: true dev: true
/tabbable@6.1.1:
resolution: {integrity: sha512-4kl5w+nCB44EVRdO0g/UGoOp3vlwgycUVtkk/7DPyeLZUCuNFFKCFG6/t/DgHLrUPHjrZg6s5tNm+56Q2B0xyg==}
dev: false
/tapable@2.2.1: /tapable@2.2.1:
resolution: {integrity: sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==} resolution: {integrity: sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==}
engines: {node: '>=6'} engines: {node: '>=6'}
@ -14788,6 +14776,7 @@ packages:
'@vue/runtime-dom': 3.2.45 '@vue/runtime-dom': 3.2.45
'@vue/server-renderer': 3.2.45(vue@3.2.45) '@vue/server-renderer': 3.2.45(vue@3.2.45)
'@vue/shared': 3.2.45 '@vue/shared': 3.2.45
dev: true
/vuedraggable@4.1.0(vue@3.2.45): /vuedraggable@4.1.0(vue@3.2.45):
resolution: {integrity: sha512-FU5HCWBmsf20GpP3eudURW3WdWTKIbEIQxh9/8GE806hydR9qZqRRxRE3RjqX7PkuLuMQG/A7n3cfj9rCEchww==} resolution: {integrity: sha512-FU5HCWBmsf20GpP3eudURW3WdWTKIbEIQxh9/8GE806hydR9qZqRRxRE3RjqX7PkuLuMQG/A7n3cfj9rCEchww==}