Merge branch 'develop'

This commit is contained in:
syuilo 2021-10-16 19:55:44 +09:00
commit 8a1f3a4c0b
362 changed files with 7023 additions and 4889 deletions

33
.github/workflows/docker-develop.yml vendored Normal file
View file

@ -0,0 +1,33 @@
name: Publish Docker image (develop)
on:
push:
branches:
- develop
workflow_dispatch:
jobs:
push_to_registry:
name: Push Docker image to Docker Hub
runs-on: ubuntu-latest
steps:
- name: Check out the repo
uses: actions/checkout@v2
- name: Docker meta
id: meta
uses: docker/metadata-action@v3
with:
images: misskey/misskey
- name: Log in to Docker Hub
uses: docker/login-action@v1
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
- name: Build and Push to Docker Hub
uses: docker/build-push-action@v2
with:
context: .
push: true
tags: misskey/misskey:develop
labels: develop

View file

@ -16,16 +16,16 @@ jobs:
services: services:
postgres: postgres:
image: postgres:10-alpine image: postgres:12.2-alpine
ports: ports:
- 5432:5432 - 54312:5432
env: env:
POSTGRES_DB: test-misskey POSTGRES_DB: test-misskey
POSTGRES_HOST_AUTH_METHOD: trust POSTGRES_HOST_AUTH_METHOD: trust
redis: redis:
image: redis:alpine image: redis:4.0-alpine
ports: ports:
- 6379:6379 - 56312:6379
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v2
@ -40,7 +40,7 @@ jobs:
- name: Check yarn.lock - name: Check yarn.lock
run: git diff --exit-code yarn.lock run: git diff --exit-code yarn.lock
- name: Copy Configure - name: Copy Configure
run: cp .circleci/misskey/*.yml .config run: cp test/test.yml .config
- name: Build - name: Build
run: yarn build run: yarn build
- name: Test - name: Test

View file

@ -1,12 +1,10 @@
{ {
"recommendations": [ "recommendations": [
"ducksoupdev.vue2",
"editorconfig.editorconfig", "editorconfig.editorconfig",
"eg2.vscode-npm-script", "eg2.vscode-npm-script",
"hollowtree.vue-snippets",
"ms-vscode.typescript-javascript-grammar", "ms-vscode.typescript-javascript-grammar",
"ms-vscode.vscode-typescript-tslint-plugin", "ms-vscode.vscode-typescript-tslint-plugin",
"octref.vetur", "johnsoncodehk.volar",
"sysoev.language-stylus" "sysoev.language-stylus"
] ]
} }

View file

@ -2,11 +2,49 @@
## 12.x.x (unreleased) ## 12.x.x (unreleased)
### Improvements ### Improvements
- ページロードエラーページにリロードボタンを追加
### Bugfixes ### Bugfixes
--> -->
## 12.92.0 (2021/10/16)
### Improvements
- アカウント登録にメールアドレスの設定を必須にするオプション
- クライアント: 全体的なUIのブラッシュアップ
- クライアント: MFM関数構文のサジェストを実装
- クライアント: ノート本文を投稿フォーム内でプレビューできるように
- クライアント: 未読の通知のみ表示する機能
- クライアント: 通知ページで通知の種類によるフィルタ
- クライアント: アニメーションを減らす設定の適用範囲を拡充
- クライアント: 新しいダークテーマを追加
- クライアント: テーマコンパイラに hue と saturate 関数を追加
- ActivityPub: HTML -> MFMの変換を強化
- API: グループから抜ける users/groups/leave エンドポイントを実装
- API: i/notifications に unreadOnly オプションを追加
- API: ap系のエンドポイントをログイン必須化+レートリミット追加
- MFM: Add tag syntaxes of bold <b></b> and strikethrough <s></s>
### Bugfixes
- Fix createDeleteAccountJob
- admin inbox queue does not show individual jobs
- クライアント: ヘッダーのタブが折り返される問題を修正
- クライアント: ヘッダーにタブが表示されている状態でタイトルをクリックしたときにタブ選択が表示されるのを修正
- クライアント: ユーザーページのタブが機能していない問題を修正
- クライアント: ピン留めユーザーの設定項目がない問題を修正
- クライアント: Deck UIにおいて、重ねたカラムの片方を畳んだ状態で右に出すと表示が壊れる問題を修正
- API: 管理者およびモデレーターをブロックできてしまう問題を修正
- MFM: Mentions in the link label are parsed as text
- MFM: Add a property to the URL node indicating whether it was enclosed in <>
- MFM: Disallows < and > in hashtags
### Changes
- 保守性やユーザビリティの観点から、Misskeyのコマンドラインオプションが削除されました。
- 必要であれば、代わりに環境変数で設定することができます
- MFM: パフォーマンス、保守性、構文誤認識抑制の観点から、旧関数構文のサポートが削除されました。
- 旧構文(`[foo bar]`)を使用せず、現行の構文(`$[foo bar]`)を使用してください。
## 12.91.0 (2021/09/22) ## 12.91.0 (2021/09/22)
### Improvements ### Improvements

View file

@ -57,6 +57,17 @@ If your language is not listed in Crowdin, please open an issue.
- Test codes are located in [`/test`](/test). - Test codes are located in [`/test`](/test).
### Run test ### Run test
Create a config file.
```
cp test/test.yml .config/
```
Prepare DB/Redis for testing.
```
docker-compose -f test/docker-compose.yml up
```
Alternatively, prepare an empty (data can be erased) DB and edit `.config/test.yml`.
Run all test.
``` ```
npm run test npm run test
``` ```
@ -177,6 +188,10 @@ npx ts-node ./node_modules/typeorm/cli.js migration:generate -n 変更の名前
### JSONのimportに気を付けよう ### JSONのimportに気を付けよう
TypeScriptでjsonをimportすると、tscでコンパイルするときにそのjsonファイルも一緒にdistディレクトリに吐き出されてしまう。この挙動により、意図せずファイルの書き換えが発生することがあるので、jsonをimportするときは書き換えられても良いものかどうか確認すること。書き換えされて欲しくない場合は、importで読み込むのではなく、`fs.readFileSync`などの関数を使って読み込むようにすればよい。 TypeScriptでjsonをimportすると、tscでコンパイルするときにそのjsonファイルも一緒にdistディレクトリに吐き出されてしまう。この挙動により、意図せずファイルの書き換えが発生することがあるので、jsonをimportするときは書き換えられても良いものかどうか確認すること。書き換えされて欲しくない場合は、importで読み込むのではなく、`fs.readFileSync`などの関数を使って読み込むようにすればよい。
### コンポーネントのスタイル定義でmarginを持たせない
コンポーネント自身がmarginを設定するのは問題の元となることはよく知られている
marginはそのコンポーネントを使う側が設定する
## その他 ## その他
### HTMLのクラス名で follow という単語は使わない ### HTMLのクラス名で follow という単語は使わない
広告ブロッカーで誤ってブロックされる 広告ブロッカーで誤ってブロックされる

View file

@ -6,6 +6,7 @@ search: "البحث"
notifications: "الإشعارات" notifications: "الإشعارات"
username: "اسم المستخدم" username: "اسم المستخدم"
password: "الكلمة السرية" password: "الكلمة السرية"
forgotPassword: "نسيتَ كلمة السر"
fetchingAsApObject: "جارٍ جلبه مِن الفديفرس…" fetchingAsApObject: "جارٍ جلبه مِن الفديفرس…"
ok: " حسناً" ok: " حسناً"
gotIt: "فهِمت" gotIt: "فهِمت"

View file

@ -81,6 +81,8 @@ somethingHappened: "Ein Fehler ist aufgetreten"
retry: "Wiederholen" retry: "Wiederholen"
pageLoadError: "Laden der Seite fehlgeschlagen." pageLoadError: "Laden der Seite fehlgeschlagen."
pageLoadErrorDescription: "Dieser Fehler wird meist durch Netzwerkfehler oder den Browser-Cache verursacht. Bitte leere den Cache oder versuche es nach einiger Zeit erneut." pageLoadErrorDescription: "Dieser Fehler wird meist durch Netzwerkfehler oder den Browser-Cache verursacht. Bitte leere den Cache oder versuche es nach einiger Zeit erneut."
serverIsDead: "Dieser Server antwortet nicht. Bitte warte einen Moment und versuche es dann erneut."
youShouldUpgradeClient: "Bitte aktualisiere diese Seite, um eine neuere Version deines Clients zu verwenden."
enterListName: "Name der Liste eingeben" enterListName: "Name der Liste eingeben"
privacy: "Privatsphäre" privacy: "Privatsphäre"
makeFollowManuallyApprove: "Follow-Anfragen benötigen Bestätigung" makeFollowManuallyApprove: "Follow-Anfragen benötigen Bestätigung"
@ -545,7 +547,7 @@ invisibleNote: "Private Notiz"
enableInfiniteScroll: "Automatisch mehr Notizen laden" enableInfiniteScroll: "Automatisch mehr Notizen laden"
visibility: "Sichtbarkeit" visibility: "Sichtbarkeit"
poll: "Umfrage" poll: "Umfrage"
useCw: "Inhalt verbergen" useCw: "Inhaltswarnung verwenden"
enablePlayer: "Video-Player öffnen" enablePlayer: "Video-Player öffnen"
disablePlayer: "Video-Player schließen" disablePlayer: "Video-Player schließen"
expandTweet: "Tweet ausklappen" expandTweet: "Tweet ausklappen"
@ -764,6 +766,7 @@ middle: "Mittel"
low: "Niedrig" low: "Niedrig"
emailNotConfiguredWarning: "Keine Email-Adresse hinterlegt" emailNotConfiguredWarning: "Keine Email-Adresse hinterlegt"
ratio: "Verhältnis" ratio: "Verhältnis"
previewNoteText: "Vorschau anzeigen"
customCss: "Benutzerdefiniertes CSS" customCss: "Benutzerdefiniertes CSS"
customCssWarn: "Verwende diese Einstellung nur, wenn du weißt, was sie tut. Ungültige Eingaben können dazu führen, dass der Client nicht mehr normal funktioniert." customCssWarn: "Verwende diese Einstellung nur, wenn du weißt, was sie tut. Ungültige Eingaben können dazu führen, dass der Client nicht mehr normal funktioniert."
global: "Global" global: "Global"
@ -782,11 +785,22 @@ translatedFrom: "Aus {x} übersetzt"
accountDeletionInProgress: "Löschung des Benutzerkontos momentan in Bearbeitung" accountDeletionInProgress: "Löschung des Benutzerkontos momentan in Bearbeitung"
usernameInfo: "Ein Name, durch den dein Benutzerkonto auf diesem Server identifiziert werden kann. Du kannst das Alphabet (a~z, A~Z), Ziffern (0~9) oder Unterstriche (_) verwenden. Benutzernamen können später nicht geändert werden." usernameInfo: "Ein Name, durch den dein Benutzerkonto auf diesem Server identifiziert werden kann. Du kannst das Alphabet (a~z, A~Z), Ziffern (0~9) oder Unterstriche (_) verwenden. Benutzernamen können später nicht geändert werden."
aiChanMode: "Ai Modus" aiChanMode: "Ai Modus"
keepCw: "Inhaltswarnung beibehalten" keepCw: "Inhaltswarnungen beibehalten"
pubSub: "Pub/Sub Benutzerkonten" pubSub: "Pub/Sub Benutzerkonten"
lastCommunication: "Letzte Kommunikation" lastCommunication: "Letzte Kommunikation"
resolved: "Gelöst" resolved: "Gelöst"
unresolved: "Ungelöst" unresolved: "Ungelöst"
itsOn: "Eingeschaltet"
itsOff: "Ausgeschaltet"
emailRequiredForSignup: "Angaben einer Email-Adresse als benötigt markieren"
unread: "Ungelesen"
filter: "Filter"
controllPanel: "Systemsteuerung"
manageAccounts: "Benutzerkonten verwalten"
_signup:
almostThere: "Fast geschafft"
emailAddressInfo: "Bitte gib deine Email-Adresse ein."
emailSent: "An deine Email-Adresse ({email}) wurde soeben eine Bestätigungsmail geschickt. Bitte klicke auf den enthaltenen Link, um die Erstellung deines Benutzerkontos abzuschließen."
_accountDelete: _accountDelete:
accountDelete: "Benutzerkonto löschen" accountDelete: "Benutzerkonto löschen"
mayTakeTime: "Da die Löschung eines Benutzerkontos ein aufwendiger Prozess ist, kann dessen Dauer davon abhängen, wie viel Inhalt in diesem erstellt wurde oder wie viele Dateien hochgeladen wurden." mayTakeTime: "Da die Löschung eines Benutzerkontos ein aufwendiger Prozess ist, kann dessen Dauer davon abhängen, wie viel Inhalt in diesem erstellt wurde oder wie viele Dateien hochgeladen wurden."
@ -901,6 +915,8 @@ _mfm:
fontDescription: "Setzt die Schriftart des Inhaltes fest." fontDescription: "Setzt die Schriftart des Inhaltes fest."
rainbow: "Regenbogen" rainbow: "Regenbogen"
rainbowDescription: "Lässt den Inhalt in Regenbogenfarben erscheinen." rainbowDescription: "Lässt den Inhalt in Regenbogenfarben erscheinen."
sparkle: "Glitzer"
sparkleDescription: "Verleiht Inhalt einen glitzernden Partikeleffekt."
_reversi: _reversi:
reversi: "Reversi" reversi: "Reversi"
gameSettings: "Spieleinstellungen" gameSettings: "Spieleinstellungen"
@ -1024,9 +1040,9 @@ _theme:
infoFg: "Text von Informationen" infoFg: "Text von Informationen"
infoWarnBg: "Hintergrund von Warnungen" infoWarnBg: "Hintergrund von Warnungen"
infoWarnFg: "Text von Warnungen" infoWarnFg: "Text von Warnungen"
cwBg: "Hintergrund von verborgenen Inhalten" cwBg: "Hintergrund des Inhaltswarnungsknopfs"
cwFg: "Text von verborgenen Inhalten" cwFg: "Text des Inhaltswarnungsknopfs"
cwHoverBg: "Hintergrund von verborgenen Inhalten (Mouseover)" cwHoverBg: "Hintergrund des Inhaltswarnungsknopfs (Mouseover)"
toastBg: "Hintergrund von Benachrichtigungen" toastBg: "Hintergrund von Benachrichtigungen"
toastFg: "Text von Benachrichtigungen" toastFg: "Text von Benachrichtigungen"
buttonBg: "Hintergrund von Schaltflächen" buttonBg: "Hintergrund von Schaltflächen"
@ -1173,7 +1189,7 @@ _widgets:
aiscript: "AiScript-Konsole" aiscript: "AiScript-Konsole"
aichan: "Ai" aichan: "Ai"
_cw: _cw:
hide: "Verbergen" hide: "Inhalt verbergen"
show: "Inhalt anzeigen" show: "Inhalt anzeigen"
chars: "{count} Zeichen" chars: "{count} Zeichen"
files: "{count} Datei(en)" files: "{count} Datei(en)"

View file

@ -1,7 +1,7 @@
--- ---
_lang_: "English" _lang_: "English"
headlineMisskey: "A network connected by notes" headlineMisskey: "A network connected by notes"
introMisskey: "Welcome! Misskey is an open source, decentralized microblogging service.\nCreate \"notes\" to share what your thoughts with everyone around you. 📡\nWith \"reactions\", you can also quickly express your feelings about everyone's notes. 👍\nLet's explore a new world! 🚀" introMisskey: "Welcome! Misskey is an open source, decentralized microblogging service.\nCreate \"notes\" to share your thoughts with everyone around you. 📡\nWith \"reactions\", you can also quickly express your feelings about everyone's notes. 👍\nLet's explore a new world! 🚀"
monthAndDay: "{month}/{day}" monthAndDay: "{month}/{day}"
search: "Search" search: "Search"
notifications: "Notifications" notifications: "Notifications"
@ -81,6 +81,8 @@ somethingHappened: "An error occurred"
retry: "Retry" retry: "Retry"
pageLoadError: "Failed to load page." pageLoadError: "Failed to load page."
pageLoadErrorDescription: "This is normally caused by network errors or the browser's cache. Try clearing the cache and then try again after waiting a little while." pageLoadErrorDescription: "This is normally caused by network errors or the browser's cache. Try clearing the cache and then try again after waiting a little while."
serverIsDead: "This server is not responding. Please wait for a while and try again."
youShouldUpgradeClient: "To view this page, please refresh to update your client."
enterListName: "Enter a list name" enterListName: "Enter a list name"
privacy: "Privacy" privacy: "Privacy"
makeFollowManuallyApprove: "Follow requests require approval" makeFollowManuallyApprove: "Follow requests require approval"
@ -764,6 +766,7 @@ middle: "Medium"
low: "Low" low: "Low"
emailNotConfiguredWarning: "Email address not set." emailNotConfiguredWarning: "Email address not set."
ratio: "Ratio" ratio: "Ratio"
previewNoteText: "Show preview"
customCss: "Custom CSS" customCss: "Custom CSS"
customCssWarn: "This setting should only be used if you know what it does. Entering improper values may cause the client to stop functioning normally." customCssWarn: "This setting should only be used if you know what it does. Entering improper values may cause the client to stop functioning normally."
global: "Global" global: "Global"
@ -782,11 +785,22 @@ translatedFrom: "Translated from {x}"
accountDeletionInProgress: "Account deletion is currently in progress" accountDeletionInProgress: "Account deletion is currently in progress"
usernameInfo: "A name that identifies your account from others on this server. You can use the alphabet (a~z, A~Z), digits (0~9) or underscores (_). Usernames cannot be changed later." usernameInfo: "A name that identifies your account from others on this server. You can use the alphabet (a~z, A~Z), digits (0~9) or underscores (_). Usernames cannot be changed later."
aiChanMode: "Ai Mode" aiChanMode: "Ai Mode"
keepCw: "Keep Content Warning" keepCw: "Keep Content Warnings"
pubSub: "Pub/Sub Accounts" pubSub: "Pub/Sub Accounts"
lastCommunication: "Last communication" lastCommunication: "Last communication"
resolved: "Resolved" resolved: "Resolved"
unresolved: "Unresolved" unresolved: "Unresolved"
itsOn: "Enabled"
itsOff: "Disabled"
emailRequiredForSignup: "Require email address for sign-up"
unread: "Unread"
filter: "Filter"
controllPanel: "Control Panel"
manageAccounts: "Manage Accounts"
_signup:
almostThere: "Almost there"
emailAddressInfo: "Please enter your email address."
emailSent: "A confirmation email has been sent to your email address ({email}). Please click the included link to complete account creation."
_accountDelete: _accountDelete:
accountDelete: "Delete Account" accountDelete: "Delete Account"
mayTakeTime: "As account deletion is a resource-heavy process, it may take some time to complete depending on how much content you have created and how many files you have uploaded." mayTakeTime: "As account deletion is a resource-heavy process, it may take some time to complete depending on how much content you have created and how many files you have uploaded."

View file

@ -1,8 +1,8 @@
--- ---
_lang_: "Esperanto" _lang_: "Esperanto"
headlineMisskey: "Jen la reto konektita de notoj" headlineMisskey: "Jen la reto konektata de notoj"
introMisskey: "Bonvenon! Misskey estas malfermitkoda malcentraliza mikrobloga servo.\nKreu \"noto\"n por diskonigu tion kio nun okazas, aŭ por parolu pri vi. 📡\nUzu la funkcion \"reago\" por esprimu rapide vian senton pri ies noto. 👍\nBonvole esploru novan mondon. 🚀" introMisskey: "Bonvenon! Misskey estas malfermitkoda malcentraliza etbloga servo.\nKreu \"noto\"n por paroli vian penson al iuj ĉirkaŭ vi. 📡\nLa funkcion \"reago\" ebligas esprimi rapide vian senton pri ies noto en Fediverso. 👍\nBonvole esploru novan mondon. 🚀"
monthAndDay: "{day}a/{month}" monthAndDay: "La {day}-a de la {month}-a monato"
search: "Serĉi" search: "Serĉi"
notifications: "Sciigoj" notifications: "Sciigoj"
username: "Uzantnomo" username: "Uzantnomo"
@ -33,18 +33,18 @@ save: "Konservi"
users: "Uzantoj" users: "Uzantoj"
addUser: "Aldoni uzanton" addUser: "Aldoni uzanton"
favorite: "Preferi" favorite: "Preferi"
favorites: "Preferataĵoj" favorites: "Preferaĵoj"
unfavorite: "Malpreferi" unfavorite: "Malpreferi"
favorited: "Aldonita al preferataĵoj" favorited: "Aldonita al via listo de preferaĵoj."
alreadyFavorited: "Ankoraŭ aldonita al via listo de preferaĵoj." alreadyFavorited: "Ĝi jam estis aldonita al via listo de preferaĵoj."
cantFavorite: "Ne aldonita al preferataĵoj" cantFavorite: "Ne aldonita al via listo de preferaĵoj."
pin: "Alpingli al la profilo" pin: "Alpingli"
unpin: "Depingli" unpin: "Depingli"
copyContent: "Kopii enhavon" copyContent: "Kopii enhavon"
copyLink: "Kopii ligilon" copyLink: "Kopii ligilon"
delete: "Forviŝi" delete: "Forviŝi"
deleteAndEdit: "Forviŝi kaj redakti" deleteAndEdit: "Redakti foriginte"
deleteAndEditConfirm: "Ĉu vi certas, ke vi volas forigi kaj redakti la noton? Kun tio foriĝos reagoj, plusendaĵoj, kaj respondoj ĉiuj de ĝi." deleteAndEditConfirm: "Ĉu vi certas, ke vi volas forigi kaj redakti la noton? Tio forviŝos reagojn, notojn plusendintajn, kaj respondojn ĉiujn de ĝi."
addToList: "Aldoni al listo" addToList: "Aldoni al listo"
sendMessage: "Sendi mesaĝon" sendMessage: "Sendi mesaĝon"
copyUsername: "Kopii uzantnomon" copyUsername: "Kopii uzantnomon"
@ -57,17 +57,17 @@ receiveFollowRequest: "Peto de sekvado estas ricevita"
followRequestAccepted: "La peto de sekvado akceptita" followRequestAccepted: "La peto de sekvado akceptita"
mention: "Mencioj" mention: "Mencioj"
mentions: "Al vi" mentions: "Al vi"
directNotes: "Notoj rektaj" directNotes: "Rekte senditaj"
importAndExport: "Importi/eksporti" importAndExport: "Importi/eksporti"
import: "Importi" import: "Importi"
export: "Eksporti" export: "Eksporti"
files: "Dosieroj" files: "Dosieroj"
download: "Elŝuti" download: "Elŝuti"
driveFileDeleteConfirm: "Ĉu vi certas, ke vi volas forviŝi la dosieron \"{name}\"? Pro tio forviŝiĝos ankaŭ la notoj kiuj enhavas ĝin." driveFileDeleteConfirm: "Ĉu vi certas, ke vi volas forviŝi la dosieron \"{name}\"? Pro tio forviŝiĝos ankaŭ la notoj kiuj enhavas ĝin."
unfollowConfirm: "Ĉu vi certas, ke vi volas ne plu sekvi {name}'(o)n?" unfollowConfirm: "Ĉu vi certas, ke vi volas ĉesi sekvi {name}'(o)n?"
lists: "Listoj" lists: "Listoj"
noLists: "Neniu listo" noLists: "Neniu listo"
note: "Notoj" note: "Sendi"
notes: "Notoj" notes: "Notoj"
following: "Sekvatoj" following: "Sekvatoj"
followers: "Sekvantoj" followers: "Sekvantoj"
@ -92,13 +92,13 @@ cantRenote: "Oni ne povas plusendi la noton."
cantReRenote: "Plusendado ne estas plusendebla." cantReRenote: "Plusendado ne estas plusendebla."
quote: "Citi" quote: "Citi"
pinnedNote: "Alpinglita noto" pinnedNote: "Alpinglita noto"
pinned: "Alpingli al la profilo" pinned: "Alpingli"
you: "Vi" you: "Vi"
clickToShow: "Klaku por malkaŝu" clickToShow: "Klaku por malkaŝu"
sensitive: "Enhavo ne estas deca por laborejo (NSFW)" sensitive: "Enhavo ne estas deca por laborejo (NSFW)"
add: "Aldoni" add: "Aldoni"
reaction: "Reagoj" reaction: "Reagoj"
rememberNoteVisibility: "Rememoru la videblecon de la noto laste sendita" rememberNoteVisibility: "Rememoru videblecon de la noto laste sendita "
attachCancel: "Deigi aldonaĵon" attachCancel: "Deigi aldonaĵon"
markAsSensitive: "Troviĝi NSFW" markAsSensitive: "Troviĝi NSFW"
unmarkAsSensitive: "Ne troviĝi NSFW" unmarkAsSensitive: "Ne troviĝi NSFW"
@ -137,7 +137,7 @@ removeWallpaper: "Forviŝi ekranfonon. "
searchWith: "Serĉi: {q}" searchWith: "Serĉi: {q}"
youHaveNoLists: "Vi ne havas listojn." youHaveNoLists: "Vi ne havas listojn."
followConfirm: "Ĉu vi certas ke vi volas sekvi {name}'(o)n?" followConfirm: "Ĉu vi certas ke vi volas sekvi {name}'(o)n?"
host: "Gastiganto" host: "Gastigo"
selectUser: "Elekti uzanton" selectUser: "Elekti uzanton"
recipient: "Ricevonto" recipient: "Ricevonto"
annotation: "Komentarioj" annotation: "Komentarioj"
@ -254,7 +254,7 @@ unwatch: "Malobservi"
accept: "Permesi" accept: "Permesi"
normal: "Normala" normal: "Normala"
instanceName: "Nomo de la nodo" instanceName: "Nomo de la nodo"
instanceDescription: "Mempriskribo de la nodo " instanceDescription: "Priskribo de la nodo "
maintainerName: "Nomo de la administranto" maintainerName: "Nomo de la administranto"
maintainerEmail: "Retpoŝto de la administranto" maintainerEmail: "Retpoŝto de la administranto"
tosUrl: "URL de kondiĉoj de uzado" tosUrl: "URL de kondiĉoj de uzado"
@ -323,6 +323,7 @@ newPasswordIs: "La nova pasvorto estas {password}."
share: "Diskonigi" share: "Diskonigi"
notFound: "Ne trovita" notFound: "Ne trovita"
cacheClear: "Malplenigi staplon" cacheClear: "Malplenigi staplon"
markAsReadAllNotifications: "Marki ĉiujn sciigojn kiel legito"
help: "Manlibro de uzado" help: "Manlibro de uzado"
inputMessageHere: "Entajpu masaĝo tie ĉi" inputMessageHere: "Entajpu masaĝo tie ĉi"
close: "Fermi" close: "Fermi"
@ -344,10 +345,10 @@ quoteAttached: "Kun citaĵo"
quoteQuestion: "Ĉu vi aldonas citaĵon?" quoteQuestion: "Ĉu vi aldonas citaĵon?"
noMessagesYet: "Ankoraŭ neniu mesaĝo" noMessagesYet: "Ankoraŭ neniu mesaĝo"
newMessageExists: "Vi ricevis novan mesaĝon." newMessageExists: "Vi ricevis novan mesaĝon."
onlyOneFileCanBeAttached: "Vi povas aldoni nur unu dosieron po unu mesaĝo." onlyOneFileCanBeAttached: "Oni povas aldoni nur unu dosieron po mesaĝo."
signinRequired: "Bonvolu ensaluti" signinRequired: "Bonvolu ensaluti"
invitations: "Inviti" invitations: "Inviti"
invitationCode: "Kodo de invito" invitationCode: "Invita kodo"
unavailable: "Ne disponebla" unavailable: "Ne disponebla"
passwordMatched: "Konforma" passwordMatched: "Konforma"
passwordNotMatched: "Nekonforma" passwordNotMatched: "Nekonforma"
@ -417,7 +418,7 @@ enablePlayer: "Vidi videon"
disablePlayer: "Fermi videon" disablePlayer: "Fermi videon"
expandTweet: "Disvolvi pepon" expandTweet: "Disvolvi pepon"
themeEditor: "Redaktilo de koloraroj" themeEditor: "Redaktilo de koloraroj"
description: "Priskribe" description: "Priskribo"
describeFile: "Priskribi la bildon" describeFile: "Priskribi la bildon"
enterFileDescription: "Priskribu" enterFileDescription: "Priskribu"
author: "Aŭtoro" author: "Aŭtoro"
@ -433,7 +434,7 @@ emailServer: "Retpoŝta servilo"
email: "Retpoŝto" email: "Retpoŝto"
emailAddress: "Retpoŝta adreso" emailAddress: "Retpoŝta adreso"
smtpConfig: "Agordoj de SMTP servilo" smtpConfig: "Agordoj de SMTP servilo"
smtpHost: "Gastiganto" smtpHost: "Gastigo"
smtpPort: "Pordo" smtpPort: "Pordo"
smtpUser: "Uzantnomo" smtpUser: "Uzantnomo"
smtpPass: "Pasvorto" smtpPass: "Pasvorto"
@ -481,7 +482,7 @@ notSet: "Ne elektita"
emailVerified: "Via retpoŝto estis kontrolita." emailVerified: "Via retpoŝto estis kontrolita."
noteFavoritesCount: "La nombro de notoj preferataj" noteFavoritesCount: "La nombro de notoj preferataj"
pageLikesCount: "La nombro de paĝoj kiun la uzanto preferas" pageLikesCount: "La nombro de paĝoj kiun la uzanto preferas"
pageLikedCount: "La nombro de uzantoj kiuj preferas la paĝon" pageLikedCount: "La nombro de uzantoj, kiuj preferas paĝon de ĉi tiu uzanto"
contact: "Kontakto" contact: "Kontakto"
makeExplorable: "Videbligi konton sur la paĝo \"Esplori\"" makeExplorable: "Videbligi konton sur la paĝo \"Esplori\""
duplicate: "Duobligi" duplicate: "Duobligi"
@ -509,7 +510,7 @@ inUse: "Uzata"
editCode: "Redakti kodon" editCode: "Redakti kodon"
emailNotification: "Sciigoj per retpoŝto" emailNotification: "Sciigoj per retpoŝto"
inChannelSearch: "Serĉi en kanalo" inChannelSearch: "Serĉi en kanalo"
useReactionPickerForContextMenu: "Oni malfermas reago-elektilon per dekstro-kliki" useReactionPickerForContextMenu: "Malfermi reago-elektilon per dekstro-klaki"
typingUsers: "{users} nun entajpas…" typingUsers: "{users} nun entajpas…"
clear: "Vakigi" clear: "Vakigi"
goBack: "Reiri antaŭ" goBack: "Reiri antaŭ"
@ -540,6 +541,7 @@ troubleshooting: "Problemsolvi"
learnMore: "Lernu pli" learnMore: "Lernu pli"
translate: "Traduki" translate: "Traduki"
translatedFrom: "Tradukita el {x}" translatedFrom: "Tradukita el {x}"
controllPanel: "Ŝaltpodio"
_docs: _docs:
continueReading: "Legi plu" continueReading: "Legi plu"
features: "Funkcioj" features: "Funkcioj"
@ -614,8 +616,8 @@ _wordMute:
mutedNotes: "Silentigitaj notoj" mutedNotes: "Silentigitaj notoj"
_theme: _theme:
manage: "Administri kolorarojn" manage: "Administri kolorarojn"
code: "Kodo de koloraro" code: "Kolorara kodo"
description: "Priskribe" description: "Priskribo"
color: "Koloro" color: "Koloro"
darken: "Malbrileco" darken: "Malbrileco"
lighten: "Brileco" lighten: "Brileco"
@ -667,8 +669,8 @@ _permissions:
"write:blocks": "Redakti vian liston de blokitoj" "write:blocks": "Redakti vian liston de blokitoj"
"read:drive": "Legi vian diskon" "read:drive": "Legi vian diskon"
"write:drive": "Ĉia operacio por skribi, forviŝi, aŭ alimaniere ŝanĝi la informon de dosiero en via disko de Misskey" "write:drive": "Ĉia operacio por skribi, forviŝi, aŭ alimaniere ŝanĝi la informon de dosiero en via disko de Misskey"
"read:favorites": "Vidi vian liston de preferataĵoj" "read:favorites": "Vidi vian liston de preferaĵoj"
"write:favorites": "Redakti vian liston de preferataĵoj." "write:favorites": "Redakti vian liston de preferaĵoj"
"read:following": "Vidi la infomaciojn pri tio, kion vi sekvas" "read:following": "Vidi la infomaciojn pri tio, kion vi sekvas"
"write:following": "Sekvi aŭ malsekvi alian uzanton" "write:following": "Sekvi aŭ malsekvi alian uzanton"
"read:messaging": "Vidi vian retbabiladon" "read:messaging": "Vidi vian retbabiladon"
@ -715,23 +717,24 @@ _poll:
vote: "Baloti" vote: "Baloti"
closed: "Oni jam balotis ĝin" closed: "Oni jam balotis ĝin"
_visibility: _visibility:
public: "Publika"
publicDescription: "Via noto estos videbla de ĉiuj uzantoj" publicDescription: "Via noto estos videbla de ĉiuj uzantoj"
home: "Hejma" home: "Hejma"
homeDescription: "Dissendi nur sur hejma templinio" homeDescription: "Dissendi nur sur hejma templinio"
followers: "Sekvantoj" followers: "Nur al sekvantoj"
followersDescription: "Nur al sekvantoj al mi" followersDescription: "Publiki nur al viaj sekvantoj"
specified: "Rekta" specified: "Rekte"
specifiedDescription: "Publikigi nur al specifaj uzantoj" specifiedDescription: "Montri nur al specifaj uzantoj"
localOnly: "Nur loka" localOnly: "Nur loka"
localOnlyDescription: "Ne montri al transaj uzantoj" localOnlyDescription: "Ne montri al transaj uzantoj"
_postForm: _postForm:
replyPlaceholder: "Respondi tiun noton…" replyPlaceholder: "Respondi la noton…"
quotePlaceholder: "Citi tiun noton…" quotePlaceholder: "Citi la noton…"
channelPlaceholder: "Mencii en kanalo…" channelPlaceholder: "Mencii en kanalo…"
_profile: _profile:
name: "Nomo" name: "Nomo"
username: "Uzantnomo" username: "Uzantnomo"
description: "Pri mi" description: "Sinprezento"
metadata: "Kromaj informoj" metadata: "Kromaj informoj"
metadataEdit: "Redakti kromaj informoj" metadataEdit: "Redakti kromaj informoj"
changeAvatar: "Ŝanĝi profilbildon" changeAvatar: "Ŝanĝi profilbildon"
@ -746,7 +749,7 @@ _charts:
federationInstancesTotal: "La totala nombro de nodoj kunfederantaj" federationInstancesTotal: "La totala nombro de nodoj kunfederantaj"
usersTotal: "La totala nombro de la uzantoj" usersTotal: "La totala nombro de la uzantoj"
activeUsers: "La nombro de la uzantoj aktivaj" activeUsers: "La nombro de la uzantoj aktivaj"
notesTotal: "La totala nombro de la notoj" notesTotal: "La totala nombro de notoj"
filesTotal: "La totala nombro de la dosieroj" filesTotal: "La totala nombro de la dosieroj"
_timelines: _timelines:
home: "Hejma" home: "Hejma"
@ -859,16 +862,21 @@ _pages:
argVariables: "Eniga junto" argVariables: "Eniga junto"
_notification: _notification:
fileUploaded: "La dosiero sukcese alŝutiĝis." fileUploaded: "La dosiero sukcese alŝutiĝis."
youGotMention: "{name} mencis"
youGotReply: "{name} respondis"
youGotQuote: "{name} citis"
youRenoted: "{name} plusendis" youRenoted: "{name} plusendis"
youGotPoll: "{name} balotis" youGotPoll: "{name} balotis"
youGotMessagingMessageFromUser: "{name} sentis mesaĝon al vi." youGotMessagingMessageFromUser: "{name} sentis mesaĝon al vi."
youGotMessagingMessageFromGroup: "Retbabilan mesaĝon oni sendis al la grupo {name}" youGotMessagingMessageFromGroup: "Retbabilan mesaĝon oni sendis al la grupo {name}"
youWereFollowed: "eksekvis vin" youWereFollowed: "eksekvis vin"
youReceivedFollowRequest: "Vi ricevis peton de sekvado" youReceivedFollowRequest: "Vi ricevis peton de sekvado"
yourFollowRequestAccepted: "Via peto por sekvado estis akceptita." yourFollowRequestAccepted: "Via peto de sekvado estis akceptita."
_types: _types:
follow: "Sekvatoj" all: "Ĉio"
follow: "Nova sekvatoj"
mention: "Mencioj" mention: "Mencioj"
reply: "Respondoj"
renote: "Notoj plusenditaj" renote: "Notoj plusenditaj"
quote: "Citi" quote: "Citi"
reaction: "Reagoj" reaction: "Reagoj"
@ -882,4 +890,4 @@ _deck:
antenna: "Antenoj" antenna: "Antenoj"
list: "Listoj" list: "Listoj"
mentions: "Al vi" mentions: "Al vi"
direct: "Notoj rektaj" direct: "Rekte"

View file

@ -7,6 +7,7 @@ search: "Buscar"
notifications: "Notificaciones" notifications: "Notificaciones"
username: "Nombre de usuario" username: "Nombre de usuario"
password: "Contraseña" password: "Contraseña"
forgotPassword: "Olvidé mi Contraseña"
fetchingAsApObject: "Buscando en el fediverso" fetchingAsApObject: "Buscando en el fediverso"
ok: "OK" ok: "OK"
gotIt: "Entendido" gotIt: "Entendido"
@ -279,6 +280,7 @@ emptyDrive: "El drive está vacío"
emptyFolder: "La carpeta está vacía" emptyFolder: "La carpeta está vacía"
unableToDelete: "No se puede borrar" unableToDelete: "No se puede borrar"
inputNewFileName: "Ingrese un nuevo nombre de archivo" inputNewFileName: "Ingrese un nuevo nombre de archivo"
inputNewDescription: "Ingrese nueva descripción"
inputNewFolderName: "Ingrese un nuevo nombre de la carpeta" inputNewFolderName: "Ingrese un nuevo nombre de la carpeta"
circularReferenceFolder: "La carpeta de destino es una sub-carpeta de la carpeta que quieres mover." circularReferenceFolder: "La carpeta de destino es una sub-carpeta de la carpeta que quieres mover."
hasChildFilesOrFolders: "No se puede borrar esta carpeta. No está vacía." hasChildFilesOrFolders: "No se puede borrar esta carpeta. No está vacía."
@ -310,6 +312,8 @@ monthX: "Mes {month}"
yearX: "Año {year}" yearX: "Año {year}"
pages: "Páginas" pages: "Páginas"
integration: "Integración" integration: "Integración"
connectService: "Conectar"
disconnectService: "Desconectar"
enableLocalTimeline: "Habilitar linea de tiempo local" enableLocalTimeline: "Habilitar linea de tiempo local"
enableGlobalTimeline: "Habilitar linea de tiempo global" enableGlobalTimeline: "Habilitar linea de tiempo global"
disablingTimelinesInfo: "Aunque se desactiven estas lineas de tiempo, por conveniencia el administrador y los moderadores pueden seguir usándolos" disablingTimelinesInfo: "Aunque se desactiven estas lineas de tiempo, por conveniencia el administrador y los moderadores pueden seguir usándolos"
@ -323,6 +327,7 @@ driveCapacityPerRemoteAccount: "Capacidad del drive por usuario remoto"
inMb: "En megabytes" inMb: "En megabytes"
iconUrl: "URL de la imagen del avatar" iconUrl: "URL de la imagen del avatar"
bannerUrl: "URL de la imagen del banner" bannerUrl: "URL de la imagen del banner"
backgroundImageUrl: "URL de la imagen de fondo"
basicInfo: "Información básica" basicInfo: "Información básica"
pinnedUsers: "Usuarios fijados" pinnedUsers: "Usuarios fijados"
pinnedUsersDescription: "Describir los usuarios que quiere fijar en la página \"Descubrir\" separados por una linea nueva" pinnedUsersDescription: "Describir los usuarios que quiere fijar en la página \"Descubrir\" separados por una linea nueva"
@ -524,6 +529,9 @@ removeAllFollowing: "Retener todos los siguientes"
removeAllFollowingDescription: "Cancelar todos los siguientes del servidor {host}. Ejecutar en caso de que esta instancia haya dejado de existir" removeAllFollowingDescription: "Cancelar todos los siguientes del servidor {host}. Ejecutar en caso de que esta instancia haya dejado de existir"
userSuspended: "Este usuario ha sido suspendido." userSuspended: "Este usuario ha sido suspendido."
userSilenced: "Este usuario ha sido silenciado." userSilenced: "Este usuario ha sido silenciado."
yourAccountSuspendedTitle: "Esta cuenta ha sido suspendida"
yourAccountSuspendedDescription: "Esta cuenta ha sido suspendida debido a violaciones de los términos de servicio del servidor y otras razones. Para más información, póngase en contacto con el administrador. Por favor, no cree una nueva cuenta."
menu: "Menú"
divider: "Divisor" divider: "Divisor"
addItem: "Agregar elemento" addItem: "Agregar elemento"
rooms: "Cuartos" rooms: "Cuartos"
@ -543,6 +551,8 @@ disablePlayer: "Cerrar reproductor"
expandTweet: "Expandir tweet" expandTweet: "Expandir tweet"
themeEditor: "Editor de temas" themeEditor: "Editor de temas"
description: "Descripción" description: "Descripción"
describeFile: "Añade una descripción"
enterFileDescription: "Introducir un título"
author: "Autor" author: "Autor"
leaveConfirm: "Hay modificaciones sin guardar. ¿Desea descartarlas?" leaveConfirm: "Hay modificaciones sin guardar. ¿Desea descartarlas?"
manage: "Administrar" manage: "Administrar"
@ -645,29 +655,90 @@ driveFilesCount: "Cantidad de archivos en el drive"
driveUsage: "Uso del drive" driveUsage: "Uso del drive"
noCrawle: "Rechazar indexación del crawler" noCrawle: "Rechazar indexación del crawler"
noCrawleDescription: "Pedir a los motores de búsqueda que no indexen tu perfil, notas, páginas, etc." noCrawleDescription: "Pedir a los motores de búsqueda que no indexen tu perfil, notas, páginas, etc."
lockedAccountInfo: "A menos que configures la visibilidad de tus notas como \"Sólo seguidores\", tus notas serán visibles para cualquiera, incluso si requieres que los seguidores sean aprobados manualmente."
alwaysMarkSensitive: "Marcar los medios de comunicación como contenido sensible por defecto" alwaysMarkSensitive: "Marcar los medios de comunicación como contenido sensible por defecto"
loadRawImages: "Cargar las imágenes originales en lugar de mostrar las miniaturas"
disableShowingAnimatedImages: "No reproducir imágenes animadas"
verificationEmailSent: "Se le ha enviado un correo electrónico de confirmación. Por favor, acceda al enlace proporcionado en el correo electrónico para completar la configuración." verificationEmailSent: "Se le ha enviado un correo electrónico de confirmación. Por favor, acceda al enlace proporcionado en el correo electrónico para completar la configuración."
notSet: "Sin especificar" notSet: "Sin especificar"
emailVerified: "Su dirección de correo electrónico ha sido verificada." emailVerified: "Su dirección de correo electrónico ha sido verificada."
noteFavoritesCount: "Número de notas favoritas" noteFavoritesCount: "Número de notas favoritas"
pageLikesCount: "Número de favoritos en la página" pageLikesCount: "Número de favoritos en la página"
pageLikedCount: "Número de favoritos de su página" pageLikedCount: "Número de favoritos de su página"
reversiCount: "Numero de partidas Reversi"
contact: "Contacto" contact: "Contacto"
useSystemFont: "Utilizar la tipografía por defecto del sistema"
clips: "Clip" clips: "Clip"
experimentalFeatures: "Características experimentales"
developer: "Desarrolladores"
makeExplorable: "Hacer visible la cuenta en \"Explorar\""
makeExplorableDescription: "Si desactiva esta opción, su cuenta no aparecerá en la sección \"Explorar\"."
showGapBetweenNotesInTimeline: "Mostrar un intervalo entre notas en la línea de tiempo"
duplicate: "Duplicar"
left: "Izquierda"
center: "Centrar"
wide: "Ancho"
narrow: "Estrecho"
reloadToApplySetting: "Esta configuración sólo se aplicará después de recargar la página. ¿Recargar ahora?"
showTitlebar: "Mostrar la barra de título"
clearCache: "Limpiar caché" clearCache: "Limpiar caché"
onlineUsersCount: "{n} usuarios en línea"
nUsers: "{n} Usuarios"
nNotes: "{n} Notas"
sendErrorReports: "Envíar informe de errores"
sendErrorReportsDescription: "Si habilita esta opción, ayudará a mejorar la calidad de Misskey compartiendo información detallada sobre los errores cuando se produzca un problema.\nEsto incluye información como la versión de su sistema operativo, el tipo de navegador que utiliza, su historial de actividad, etc."
myTheme: "Mi Tema"
backgroundColor: "Fondo" backgroundColor: "Fondo"
accentColor: "Acento" accentColor: "Acento"
textColor: "Texto" textColor: "Texto"
saveAs: "Guardar como…"
advanced: "Avanzado"
value: "Valores" value: "Valores"
createdAt: "Fecha de creación"
updatedAt: "Actualizado"
saveConfirm: "¿Guardar cambios?"
deleteConfirm: "¿Desea eliminarlo?"
invalidValue: "Este no es un valor válido."
registry: "Registro"
closeAccount: "Cerrar cuenta"
currentVersion: "Versión actual"
latestVersion: "Última versión"
youAreRunningUpToDateClient: "Está utilizando la versión más reciente de su cliente."
newVersionOfClientAvailable: "Hay una versión más nueva de su cliente disponible."
usageAmount: "Uso"
capacity: "Capacidad"
inUse: "Usado"
editCode: "Editar código"
goBack: "Deseleccionar" goBack: "Deseleccionar"
info: "Información" info: "Información"
user: "Usuarios" user: "Usuarios"
administration: "Administrar" administration: "Administrar"
expiration: "Termina el" expiration: "Termina el"
middle: "Mediano" middle: "Mediano"
customCssWarn: "Este ajuste sólo debe utilizarse si se sabe lo que hace. Introducir valores inadecuados puede hacer que el cliente deje de funcionar con normalidad."
global: "Global" global: "Global"
squareAvatars: "Mostrar iconos cuadrados"
sent: "Enviar" sent: "Enviar"
received: "Recibido"
searchResult: "Resultados de búsqueda"
hashtags: "Hashtag" hashtags: "Hashtag"
troubleshooting: "Solución de problemas"
useBlurEffect: "Utilizar efecto de desenfoque en la interfaz de usuario"
learnMore: "Ver más"
misskeyUpdated: "¡Misskey ha sido actualizado!"
whatIsNew: "Mostrar cambios"
translate: "Traducir"
translatedFrom: "Traducido de {x}"
accountDeletionInProgress: "La eliminación de la cuenta está en curso"
usernameInfo: "Un nombre que identifique su cuenta de otras en este servidor. Puede utilizar el alfabeto (a~z, A~Z), dígitos (0~9) o guiones bajos (_). Los nombres de usuario no se pueden cambiar posteriormente."
aiChanMode: "Modo Ai"
keepCw: "Mantener la advertencia de contenido"
pubSub: "Cuentas Pub/Sub"
lastCommunication: "Última comunicación"
resolved: "Resuelto"
unresolved: "Sin resolver"
_accountDelete:
accountDelete: "Eliminar Cuenta"
_docs: _docs:
admin: "Administrar" admin: "Administrar"
_ad: _ad:

View file

@ -81,6 +81,8 @@ somethingHappened: "Une erreur est survenue"
retry: "Réessayer" retry: "Réessayer"
pageLoadError: "Le chargement de la page a échoué" pageLoadError: "Le chargement de la page a échoué"
pageLoadErrorDescription: "Cela est généralement causé par le cache du navigateur ou par un problème réseau. Veuillez vider votre cache ou attendre un peu et réessayer." pageLoadErrorDescription: "Cela est généralement causé par le cache du navigateur ou par un problème réseau. Veuillez vider votre cache ou attendre un peu et réessayer."
serverIsDead: "Le serveur ne répond pas. Patientez quelques instants puis essayez à nouveau."
youShouldUpgradeClient: "Si la page ne s'affiche pas correctement, rechargez-la pour mettre votre client à jour."
enterListName: "Nom de la liste" enterListName: "Nom de la liste"
privacy: "Confidentialité" privacy: "Confidentialité"
makeFollowManuallyApprove: "Accepter manuellement les demandes dabonnement" makeFollowManuallyApprove: "Accepter manuellement les demandes dabonnement"
@ -136,7 +138,7 @@ settingGuide: "Configuration proposée"
cacheRemoteFiles: "Mise en cache des fichiers distants" cacheRemoteFiles: "Mise en cache des fichiers distants"
cacheRemoteFilesDescription: "Lorsque cette option est désactivée, les fichiers distants sont chargés directement depuis linstance distante. La désactiver diminuera certes lutilisation de lespace de stockage local mais augmentera le trafic réseau puisque les miniatures ne seront plus générées." cacheRemoteFilesDescription: "Lorsque cette option est désactivée, les fichiers distants sont chargés directement depuis linstance distante. La désactiver diminuera certes lutilisation de lespace de stockage local mais augmentera le trafic réseau puisque les miniatures ne seront plus générées."
flagAsBot: "Ce compte est un robot" flagAsBot: "Ce compte est un robot"
flagAsBotDescription: "Si ce compte est géré de manière automatisée , définissez cette option. Si elle est activée, elle agira comme un marqueur pour les autres développeurs afin d'éviter des chaînes d'interaction sans fin avec d'autres robots et d'ajuster les systèmes internes de Misskey pour traiter ce compte comme un robot." flagAsBotDescription: "Si ce compte est géré de manière automatisée, choisissez cette option. Si elle est activée, elle agira comme un marqueur pour les autres développeurs afin d'éviter des chaînes d'interaction sans fin avec d'autres robots et d'ajuster les systèmes internes de Misskey pour traiter ce compte comme un robot."
flagAsCat: "Ce compte est un chat" flagAsCat: "Ce compte est un chat"
flagAsCatDescription: "Activer l'option \" Je suis un chat \" pour ce compte." flagAsCatDescription: "Activer l'option \" Je suis un chat \" pour ce compte."
autoAcceptFollowed: "Accepter automatiquement les demandes dabonnement venant dutilisateur·rice·s que vous suivez" autoAcceptFollowed: "Accepter automatiquement les demandes dabonnement venant dutilisateur·rice·s que vous suivez"
@ -377,7 +379,7 @@ aboutMisskey: "À propos de Misskey"
administrator: "Administrateur" administrator: "Administrateur"
token: "Jeton" token: "Jeton"
twoStepAuthentication: "Authentification à deux facteurs" twoStepAuthentication: "Authentification à deux facteurs"
moderator: "Modérateurs" moderator: "Modérateur·rice·s"
nUsersMentioned: "{n} utilisateur·rice·s mentionné·e·s" nUsersMentioned: "{n} utilisateur·rice·s mentionné·e·s"
securityKey: "Clé de sécurité" securityKey: "Clé de sécurité"
securityKeyName: "Nom de la clé" securityKeyName: "Nom de la clé"
@ -495,7 +497,7 @@ objectStorageSetPublicRead: "Régler sur « public » lors de l'envoi"
serverLogs: "Journal du serveur" serverLogs: "Journal du serveur"
deleteAll: "Supprimer tout" deleteAll: "Supprimer tout"
showFixedPostForm: "Afficher le formulaire de publication en haut du fil d'actualité" showFixedPostForm: "Afficher le formulaire de publication en haut du fil d'actualité"
newNoteRecived: "Vous avez reçu une nouvelle note" newNoteRecived: "Voir les nouvelles notes"
sounds: "Sons" sounds: "Sons"
listen: "Écouter" listen: "Écouter"
none: "Rien" none: "Rien"
@ -530,6 +532,7 @@ removeAllFollowingDescription: "Se désabonner de tous les comptes de {host}. Ve
userSuspended: "Cet·te utilisateur·rice a été suspendu·e." userSuspended: "Cet·te utilisateur·rice a été suspendu·e."
userSilenced: "Cette utilisateur·trice a été mis·e en sourdine." userSilenced: "Cette utilisateur·trice a été mis·e en sourdine."
yourAccountSuspendedTitle: "Ce compte est suspendu" yourAccountSuspendedTitle: "Ce compte est suspendu"
yourAccountSuspendedDescription: "Ce compte est suspendu car vous avez enfreint les conditions d'utilisation de l'instance, ou pour un motif similaire. Si vous souhaitez connaître en détail les raisons de cette suspension, renseignez-vous auprès de l'administrateur·rice de votre instance. Merci de ne pas créer de nouveau compte."
menu: "Menu" menu: "Menu"
divider: "Séparateur" divider: "Séparateur"
addItem: "Ajouter un élément" addItem: "Ajouter un élément"
@ -786,6 +789,15 @@ pubSub: "Comptes Pub/Sub"
lastCommunication: "Dernière communication" lastCommunication: "Dernière communication"
resolved: "Résolu" resolved: "Résolu"
unresolved: "En attente" unresolved: "En attente"
emailRequiredForSignup: "Une adresse e-mail est nécessaire pour créer un compte"
unread: "Non lu"
filter: "Filtre"
controllPanel: "Panneau de contrôle"
manageAccounts: "Gérer les comptes"
_signup:
almostThere: "Bientôt fini"
emailAddressInfo: "Insérez votre adresse e-mail."
emailSent: "Un courriel de confirmation vient d'être envoyé à l'adresse que vous avez renseignée ({email}). Cliquez sur le lien contenu dans le message pour terminer la création de votre compte."
_accountDelete: _accountDelete:
accountDelete: "Supprimer le compte" accountDelete: "Supprimer le compte"
mayTakeTime: "La suppression de compte nécessitant beaucoup de ressources, l'exécution du processus peut prendre du temps, en fonction de la quantité de contenus que vous avez créés et du nombre de fichiers que vous avez téléversés." mayTakeTime: "La suppression de compte nécessitant beaucoup de ressources, l'exécution du processus peut prendre du temps, en fonction de la quantité de contenus que vous avez créés et du nombre de fichiers que vous avez téléversés."
@ -900,6 +912,8 @@ _mfm:
fontDescription: "Il est possible de choisir la police." fontDescription: "Il est possible de choisir la police."
rainbow: "Arc-en-ciel" rainbow: "Arc-en-ciel"
rainbowDescription: "Permet d'afficher le contenu en couleurs arc-en-ciel." rainbowDescription: "Permet d'afficher le contenu en couleurs arc-en-ciel."
sparkle: "Paillettes"
sparkleDescription: "Ajoute un effet scintillant au contenu."
_reversi: _reversi:
reversi: "Reversi" reversi: "Reversi"
gameSettings: "Réglages de la partie" gameSettings: "Réglages de la partie"
@ -1243,7 +1257,7 @@ _charts:
federationInstancesTotal: "Nombre total d'instances fédérées" federationInstancesTotal: "Nombre total d'instances fédérées"
usersIncDec: "Variation du nombre d'utilisateur·rice·s" usersIncDec: "Variation du nombre d'utilisateur·rice·s"
usersTotal: "Nombre des utilisateur·rice·s au total" usersTotal: "Nombre des utilisateur·rice·s au total"
activeUsers: "Utilisateur·rice·s actif·ve·s" activeUsers: "Nombre d'utilisateurices actif·ve·s"
notesIncDec: "Variation du nombre des notes" notesIncDec: "Variation du nombre des notes"
localNotesIncDec: "Variation du nombre de notes locales" localNotesIncDec: "Variation du nombre de notes locales"
remoteNotesIncDec: "Variation du nombre de notes distantes" remoteNotesIncDec: "Variation du nombre de notes distantes"

View file

@ -780,6 +780,7 @@ translatedFrom: "Terjemahkan dari {x}"
accountDeletionInProgress: "Penghapusan akun sedang dalam proses" accountDeletionInProgress: "Penghapusan akun sedang dalam proses"
usernameInfo: "Nama yang mengidentifikasikan akun kamu dari yang lain pada server ini. Kamu dapat menggunakan alfabet (a~z, A~Z), digit (0~9) atau garis bawah (_). Username tidak dapat diubah setelahnya." usernameInfo: "Nama yang mengidentifikasikan akun kamu dari yang lain pada server ini. Kamu dapat menggunakan alfabet (a~z, A~Z), digit (0~9) atau garis bawah (_). Username tidak dapat diubah setelahnya."
keepCw: "Biarkan Peringatan Konten" keepCw: "Biarkan Peringatan Konten"
controllPanel: "Panel kontrol"
_accountDelete: _accountDelete:
accountDelete: "Hapus akun" accountDelete: "Hapus akun"
mayTakeTime: "Karena penghapusan akun merupakan proses yang berat dan intensif, kemungkinan dapat membutuhkan waktu untuk menyelesaikan tergantung daripada berapa banyak konten yang kamu buat dan berapa banyak berkas yang telah kamu unggah." mayTakeTime: "Karena penghapusan akun merupakan proses yang berat dan intensif, kemungkinan dapat membutuhkan waktu untuk menyelesaikan tergantung daripada berapa banyak konten yang kamu buat dan berapa banyak berkas yang telah kamu unggah."

View file

@ -482,7 +482,7 @@ objectStorageSetPublicRead: "Imposta \"visibilità pubblica\" al momento di cari
serverLogs: "Log del server" serverLogs: "Log del server"
deleteAll: "Cancella cronologia" deleteAll: "Cancella cronologia"
showFixedPostForm: "Visualizzare la finestra di pubblicazione in cima alla timeline" showFixedPostForm: "Visualizzare la finestra di pubblicazione in cima alla timeline"
newNoteRecived: "Nuova nota ricevuta" newNoteRecived: "Vedi le nuove note"
sounds: "Impostazioni suoni" sounds: "Impostazioni suoni"
listen: "Ascolta" listen: "Ascolta"
none: "Niente" none: "Niente"

View file

@ -81,6 +81,8 @@ somethingHappened: "問題が発生しました"
retry: "再試行" retry: "再試行"
pageLoadError: "ページの読み込みに失敗しました。" pageLoadError: "ページの読み込みに失敗しました。"
pageLoadErrorDescription: "これは通常、ネットワークまたはブラウザキャッシュが原因です。キャッシュをクリアするか、しばらく待ってから再度試してください。" pageLoadErrorDescription: "これは通常、ネットワークまたはブラウザキャッシュが原因です。キャッシュをクリアするか、しばらく待ってから再度試してください。"
serverIsDead: "サーバーの応答がありません。しばらく待ってから再度試してください。"
youShouldUpgradeClient: "このページを表示するためには、リロードして新しいバージョンのクライアントをご利用ください。"
enterListName: "リスト名を入力" enterListName: "リスト名を入力"
privacy: "プライバシー" privacy: "プライバシー"
makeFollowManuallyApprove: "フォローを承認制にする" makeFollowManuallyApprove: "フォローを承認制にする"
@ -764,6 +766,7 @@ middle: "中"
low: "低" low: "低"
emailNotConfiguredWarning: "メールアドレスの設定がされていません。" emailNotConfiguredWarning: "メールアドレスの設定がされていません。"
ratio: "比率" ratio: "比率"
previewNoteText: "本文をプレビュー"
customCss: "カスタムCSS" customCss: "カスタムCSS"
customCssWarn: "この設定は必ず知識のある方が行ってください。不適切な設定を行うとクライアントが正常に使用できなくなる恐れがあります。" customCssWarn: "この設定は必ず知識のある方が行ってください。不適切な設定を行うとクライアントが正常に使用できなくなる恐れがあります。"
global: "グローバル" global: "グローバル"
@ -787,6 +790,18 @@ pubSub: "Pub/Subのアカウント"
lastCommunication: "直近の通信" lastCommunication: "直近の通信"
resolved: "解決済み" resolved: "解決済み"
unresolved: "未解決" unresolved: "未解決"
itsOn: "オンになっています"
itsOff: "オフになっています"
emailRequiredForSignup: "アカウント登録にメールアドレスを必須にする"
unread: "未読"
filter: "フィルタ"
controllPanel: "コントロールパネル"
manageAccounts: "アカウントを管理"
_signup:
almostThere: "ほとんど完了です"
emailAddressInfo: "あなたが使っているメールアドレスを入力してください。"
emailSent: "入力されたメールアドレス({email})宛に確認のメールが送信されました。メールに記載されたリンクにアクセスすると、アカウントの作成が完了します。"
_accountDelete: _accountDelete:
accountDelete: "アカウントの削除" accountDelete: "アカウントの削除"

View file

@ -787,6 +787,7 @@ pubSub: "Pub/Sub 계정"
lastCommunication: "마지막 통신" lastCommunication: "마지막 통신"
resolved: "해결됨" resolved: "해결됨"
unresolved: "해결되지 않음" unresolved: "해결되지 않음"
controllPanel: "제어판"
_accountDelete: _accountDelete:
accountDelete: "계정 삭제" accountDelete: "계정 삭제"
mayTakeTime: "계정 삭제는 서버에 부하를 가하기 때문에, 작성한 콘텐츠나 업로드한 파일의 수가 많으면 완료까지 시간이 걸릴 수 있습니다." mayTakeTime: "계정 삭제는 서버에 부하를 가하기 때문에, 작성한 콘텐츠나 업로드한 파일의 수가 많으면 완료까지 시간이 걸릴 수 있습니다."
@ -901,6 +902,8 @@ _mfm:
fontDescription: "내용의 글꼴을 지정할 수 있습니다." fontDescription: "내용의 글꼴을 지정할 수 있습니다."
rainbow: "무지개" rainbow: "무지개"
rainbowDescription: "내용을 무지개로 표시합니다." rainbowDescription: "내용을 무지개로 표시합니다."
sparkle: "반짝반짝"
sparkleDescription: "반짝이는 파티클 효과를 추가합니다."
_reversi: _reversi:
reversi: "리버시" reversi: "리버시"
gameSettings: "대국 설정" gameSettings: "대국 설정"

View file

@ -81,6 +81,8 @@ somethingHappened: "Что-то пошло не так"
retry: "Повторить попытку" retry: "Повторить попытку"
pageLoadError: "Не удалось загрузить страницу" pageLoadError: "Не удалось загрузить страницу"
pageLoadErrorDescription: "Обычно это случается из-за сбоев в сети или кэша браузера. Попробуйте очистить кэш, или подождать пару минут, а потом попытаться загрузить страницу снова." pageLoadErrorDescription: "Обычно это случается из-за сбоев в сети или кэша браузера. Попробуйте очистить кэш, или подождать пару минут, а потом попытаться загрузить страницу снова."
serverIsDead: "Ответа от сервера нет. Пожалуйста, подождите немного и повторите попытку."
youShouldUpgradeClient: "Чтобы просмотреть эту страницу, пожалуйста, обновите ее."
enterListName: "Название списка" enterListName: "Название списка"
privacy: "Конфиденциальность" privacy: "Конфиденциальность"
makeFollowManuallyApprove: "Принимать подписчиков вручную" makeFollowManuallyApprove: "Принимать подписчиков вручную"
@ -529,6 +531,8 @@ removeAllFollowing: "Удалить всех подписчиков"
removeAllFollowingDescription: "Отменить все подписки с домена {host}? Пожалуйста, применяйте это действие, если инстанс больше не существует." removeAllFollowingDescription: "Отменить все подписки с домена {host}? Пожалуйста, применяйте это действие, если инстанс больше не существует."
userSuspended: "Эта учётная запись заморожена" userSuspended: "Эта учётная запись заморожена"
userSilenced: "Этот пользователь был заглушен" userSilenced: "Этот пользователь был заглушен"
yourAccountSuspendedTitle: "Эта учетная запись заблокирована"
yourAccountSuspendedDescription: "Эта учетная запись была заблокирована из-за нарушения условий предоставления услуг сервера. Свяжитесь с администратором, если вы хотите узнать более подробную причину. Пожалуйста, не создавайте новую учетную запись."
menu: "Меню" menu: "Меню"
divider: "Линия-разделитель" divider: "Линия-разделитель"
addItem: "Добавить элемент" addItem: "Добавить элемент"
@ -775,6 +779,13 @@ useBlurEffect: "Размытие в интерфейсе"
learnMore: "Подробнее" learnMore: "Подробнее"
misskeyUpdated: "Misskey обновился!" misskeyUpdated: "Misskey обновился!"
whatIsNew: "Что новенького?" whatIsNew: "Что новенького?"
translate: "Перевод"
accountDeletionInProgress: "В настоящее время выполняется удаление учетной записи"
usernameInfo: "Имя, которое отличает вашу учетную запись от других на этом сервере. Вы можете использовать алфавит (a~z, A~Z), цифры (0~9) или символы подчеркивания (_). Имена пользователей не могут быть изменены позже."
aiChanMode: "ИИ режим"
keepCw: "Сохраняйте Предупреждения о содержимом"
controllPanel: "Панель управления"
manageAccounts: "Управление аккаунтом"
_docs: _docs:
continueReading: "Читать подробнее" continueReading: "Читать подробнее"
features: "Возможности" features: "Возможности"

View file

@ -81,6 +81,8 @@ somethingHappened: "出现了一些问题!"
retry: "重试" retry: "重试"
pageLoadError: "页面加载失败。" pageLoadError: "页面加载失败。"
pageLoadErrorDescription: "这通常是由于网络或浏览器缓存的原因。请清除缓存或等待片刻后重试。" pageLoadErrorDescription: "这通常是由于网络或浏览器缓存的原因。请清除缓存或等待片刻后重试。"
serverIsDead: "服务器没有响应。 请稍等片刻,然后重试。"
youShouldUpgradeClient: "请重新加载并使用新版本的客户端查看此页面。"
enterListName: "输入列表名称" enterListName: "输入列表名称"
privacy: "隐私" privacy: "隐私"
makeFollowManuallyApprove: "关注者的关注请求需要批准" makeFollowManuallyApprove: "关注者的关注请求需要批准"
@ -764,6 +766,7 @@ middle: "中"
low: "低" low: "低"
emailNotConfiguredWarning: "电子邮件地址未设置。" emailNotConfiguredWarning: "电子邮件地址未设置。"
ratio: "比率" ratio: "比率"
previewNoteText: "预览文本"
customCss: "自定义 CSS" customCss: "自定义 CSS"
customCssWarn: "这些设置必须有相关的基础知识,不当的配置可能导致客户端无法正常使用!" customCssWarn: "这些设置必须有相关的基础知识,不当的配置可能导致客户端无法正常使用!"
global: "全局" global: "全局"
@ -787,6 +790,17 @@ pubSub: "Pub/Sub账户"
lastCommunication: "最近通信" lastCommunication: "最近通信"
resolved: "已解决" resolved: "已解决"
unresolved: "未解决" unresolved: "未解决"
itsOn: "已开启"
itsOff: "已关闭"
emailRequiredForSignup: "注册账户需要电子邮件地址"
unread: "未读"
filter: "筛选"
controllPanel: "控制面板"
manageAccounts: "管理账户"
_signup:
almostThere: "即将完成"
emailAddressInfo: "请输入您所使用的电子邮件地址"
emailSent: "已将确认邮件发送至您输入的电子邮件地址 ({email})。请访问电子邮件中的链接以完成帐户创建。"
_accountDelete: _accountDelete:
accountDelete: "删除帐户" accountDelete: "删除帐户"
mayTakeTime: "删除账号是一个性能损耗较大的处理,如果账号持有的内容数量和上传的文件数量较多的话,完成需要花费一段时间。" mayTakeTime: "删除账号是一个性能损耗较大的处理,如果账号持有的内容数量和上传的文件数量较多的话,完成需要花费一段时间。"
@ -901,6 +915,8 @@ _mfm:
fontDescription: "可以设置内容所使用的字体。" fontDescription: "可以设置内容所使用的字体。"
rainbow: "彩虹" rainbow: "彩虹"
rainbowDescription: "用彩虹色来显示内容。" rainbowDescription: "用彩虹色来显示内容。"
sparkle: "闪光"
sparkleDescription: "添加发光粒子效果。"
_reversi: _reversi:
reversi: "黑白棋" reversi: "黑白棋"
gameSettings: "对局设置" gameSettings: "对局设置"

View file

@ -0,0 +1,14 @@
import {MigrationInterface, QueryRunner} from "typeorm";
export class emailRequiredForSignup1633068642000 implements MigrationInterface {
name = 'emailRequiredForSignup1633068642000'
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`ALTER TABLE "meta" ADD "emailRequiredForSignup" boolean NOT NULL DEFAULT false`);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "emailRequiredForSignup"`);
}
}

View file

@ -0,0 +1,16 @@
import {MigrationInterface, QueryRunner} from "typeorm";
export class userPending1633071909016 implements MigrationInterface {
name = 'userPending1633071909016'
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`CREATE TABLE "user_pending" ("id" character varying(32) NOT NULL, "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL, "code" character varying(128) NOT NULL, "username" character varying(128) NOT NULL, "email" character varying(128) NOT NULL, "password" character varying(128) NOT NULL, CONSTRAINT "PK_d4c84e013c98ec02d19b8fbbafa" PRIMARY KEY ("id"))`);
await queryRunner.query(`CREATE UNIQUE INDEX "IDX_4e5c4c99175638ec0761714ab0" ON "user_pending" ("code") `);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`DROP INDEX "IDX_4e5c4c99175638ec0761714ab0"`);
await queryRunner.query(`DROP TABLE "user_pending"`);
}
}

View file

@ -1,7 +1,7 @@
{ {
"name": "misskey", "name": "misskey",
"author": "syuilo <syuilotan@yahoo.co.jp>", "author": "syuilo <syuilotan@yahoo.co.jp>",
"version": "12.91.0", "version": "12.92.0",
"codename": "indigo", "codename": "indigo",
"repository": { "repository": {
"type": "git", "type": "git",
@ -47,7 +47,7 @@
"@sinonjs/fake-timers": "7.1.2", "@sinonjs/fake-timers": "7.1.2",
"@syuilo/aiscript": "0.11.1", "@syuilo/aiscript": "0.11.1",
"@types/bcryptjs": "2.4.2", "@types/bcryptjs": "2.4.2",
"@types/bull": "3.15.4", "@types/bull": "3.15.5",
"@types/cbor": "6.0.0", "@types/cbor": "6.0.0",
"@types/dateformat": "3.0.1", "@types/dateformat": "3.0.1",
"@types/escape-regexp": "0.0.0", "@types/escape-regexp": "0.0.0",
@ -63,17 +63,17 @@
"@types/koa-bodyparser": "4.3.3", "@types/koa-bodyparser": "4.3.3",
"@types/koa-cors": "0.0.2", "@types/koa-cors": "0.0.2",
"@types/koa-favicon": "2.0.21", "@types/koa-favicon": "2.0.21",
"@types/koa-logger": "3.1.1", "@types/koa-logger": "3.1.2",
"@types/koa-mount": "4.0.1", "@types/koa-mount": "4.0.1",
"@types/koa-send": "4.1.3", "@types/koa-send": "4.1.3",
"@types/koa-views": "7.0.0", "@types/koa-views": "7.0.0",
"@types/koa__cors": "3.0.3", "@types/koa__cors": "3.0.3",
"@types/koa__multer": "2.0.3", "@types/koa__multer": "2.0.3",
"@types/koa__router": "8.0.8", "@types/koa__router": "8.0.8",
"@types/markdown-it": "12.2.1", "@types/markdown-it": "12.2.3",
"@types/matter-js": "0.17.5", "@types/matter-js": "0.17.5",
"@types/mocha": "8.2.3", "@types/mocha": "8.2.3",
"@types/node": "16.9.6", "@types/node": "16.10.3",
"@types/node-fetch": "2.5.12", "@types/node-fetch": "2.5.12",
"@types/nodemailer": "6.4.4", "@types/nodemailer": "6.4.4",
"@types/nprogress": "0.2.0", "@types/nprogress": "0.2.0",
@ -102,47 +102,46 @@
"@types/webpack": "5.28.0", "@types/webpack": "5.28.0",
"@types/webpack-stream": "3.2.12", "@types/webpack-stream": "3.2.12",
"@types/websocket": "1.0.4", "@types/websocket": "1.0.4",
"@types/ws": "7.4.7", "@types/ws": "8.2.0",
"@typescript-eslint/parser": "4.31.2", "@typescript-eslint/parser": "5.0.0",
"@vue/compiler-sfc": "3.2.13", "@vue/compiler-sfc": "3.2.20",
"abort-controller": "3.0.0", "abort-controller": "3.0.0",
"apexcharts": "3.28.3", "apexcharts": "3.28.3",
"autobind-decorator": "2.4.0", "autobind-decorator": "2.4.0",
"autosize": "4.0.4", "autosize": "4.0.4",
"autwh": "0.1.0", "autwh": "0.1.0",
"aws-sdk": "2.992.0", "aws-sdk": "2.1003.0",
"bcryptjs": "2.4.3", "bcryptjs": "2.4.3",
"blurhash": "1.1.4", "blurhash": "1.1.4",
"broadcast-channel": "4.2.0", "broadcast-channel": "4.2.0",
"bull": "3.29.2", "bull": "3.29.3",
"cacheable-lookup": "6.0.1", "cacheable-lookup": "6.0.3",
"cafy": "15.2.1", "cafy": "15.2.1",
"cbor": "8.0.0", "cbor": "8.0.2",
"chalk": "4.1.2", "chalk": "4.1.2",
"chart.js": "2.9.4", "chart.js": "2.9.4",
"cli-highlight": "2.1.11", "cli-highlight": "2.1.11",
"commander": "8.1.0",
"compare-versions": "3.6.0", "compare-versions": "3.6.0",
"concurrently": "6.2.1", "concurrently": "6.3.0",
"content-disposition": "0.5.3", "content-disposition": "0.5.3",
"crc-32": "1.2.0", "crc-32": "1.2.0",
"css-loader": "6.3.0", "css-loader": "6.4.0",
"cssnano": "5.0.8", "cssnano": "5.0.8",
"dateformat": "4.5.1", "dateformat": "4.5.1",
"escape-regexp": "0.0.1", "escape-regexp": "0.0.1",
"eslint": "7.32.0", "eslint": "8.0.1",
"eslint-plugin-vue": "7.18.0", "eslint-plugin-vue": "7.19.1",
"eventemitter3": "4.0.7", "eventemitter3": "4.0.7",
"feed": "4.2.2", "feed": "4.2.2",
"file-type": "16.5.3", "file-type": "16.5.3",
"fluent-ffmpeg": "2.1.2", "fluent-ffmpeg": "2.1.2",
"glob": "7.1.7", "glob": "7.2.0",
"got": "11.8.2", "got": "11.8.2",
"gulp": "4.0.2", "gulp": "4.0.2",
"gulp-cssnano": "2.1.3", "gulp-cssnano": "2.1.3",
"gulp-rename": "2.0.0", "gulp-rename": "2.0.0",
"gulp-replace": "1.1.3", "gulp-replace": "1.1.3",
"gulp-terser": "2.0.1", "gulp-terser": "2.1.0",
"gulp-tslint": "8.1.4", "gulp-tslint": "8.1.4",
"hpagent": "0.1.2", "hpagent": "0.1.2",
"http-signature": "1.3.5", "http-signature": "1.3.5",
@ -157,7 +156,7 @@
"jsonld": "5.2.0", "jsonld": "5.2.0",
"jsrsasign": "8.0.20", "jsrsasign": "8.0.20",
"katex": "0.13.18", "katex": "0.13.18",
"koa": "2.13.1", "koa": "2.13.3",
"koa-bodyparser": "4.3.0", "koa-bodyparser": "4.3.0",
"koa-favicon": "2.1.0", "koa-favicon": "2.1.0",
"koa-json-body": "5.3.0", "koa-json-body": "5.3.0",
@ -170,22 +169,22 @@
"markdown-it": "12.2.0", "markdown-it": "12.2.0",
"markdown-it-anchor": "7.1.0", "markdown-it-anchor": "7.1.0",
"matter-js": "0.17.1", "matter-js": "0.17.1",
"mfm-js": "0.19.0", "mfm-js": "0.20.0",
"misskey-js": "0.0.6", "misskey-js": "0.0.6",
"mocha": "8.4.0", "mocha": "8.4.0",
"ms": "2.1.3", "ms": "2.1.3",
"multer": "1.4.3", "multer": "1.4.3",
"nested-property": "4.0.0", "nested-property": "4.0.0",
"node-fetch": "2.6.1", "node-fetch": "2.6.1",
"nodemailer": "6.6.3", "nodemailer": "6.7.0",
"os-utils": "0.0.14", "os-utils": "0.0.14",
"parse5": "6.0.1", "parse5": "6.0.1",
"pg": "8.7.1", "pg": "8.7.1",
"portscanner": "2.2.0", "portscanner": "2.2.0",
"postcss": "8.3.7", "postcss": "8.3.9",
"postcss-loader": "6.1.1", "postcss-loader": "6.2.0",
"prismjs": "1.25.0", "prismjs": "1.25.0",
"private-ip": "2.2.1", "private-ip": "2.3.0",
"probe-image-size": "7.2.1", "probe-image-size": "7.2.1",
"promise-limit": "2.7.0", "promise-limit": "2.7.0",
"pug": "3.0.2", "pug": "3.0.2",
@ -204,8 +203,8 @@
"rimraf": "3.0.2", "rimraf": "3.0.2",
"rndstr": "1.0.0", "rndstr": "1.0.0",
"s-age": "1.1.2", "s-age": "1.1.2",
"sass": "1.42.1", "sass": "1.43.2",
"sass-loader": "12.1.0", "sass-loader": "12.2.0",
"seedrandom": "3.0.5", "seedrandom": "3.0.5",
"sharp": "0.29.1", "sharp": "0.29.1",
"speakeasy": "2.0.0", "speakeasy": "2.0.0",
@ -213,7 +212,7 @@
"style-loader": "3.3.0", "style-loader": "3.3.0",
"summaly": "2.4.1", "summaly": "2.4.1",
"syslog-pro": "1.0.0", "syslog-pro": "1.0.0",
"systeminformation": "5.9.3", "systeminformation": "5.9.7",
"syuilo-password-strength": "0.0.1", "syuilo-password-strength": "0.0.1",
"textarea-caret": "3.1.0", "textarea-caret": "3.1.0",
"three": "0.117.1", "three": "0.117.1",
@ -221,19 +220,19 @@
"tinycolor2": "1.4.2", "tinycolor2": "1.4.2",
"tmp": "0.2.1", "tmp": "0.2.1",
"ts-loader": "9.2.6", "ts-loader": "9.2.6",
"ts-node": "10.2.1", "ts-node": "10.3.0",
"tsc-alias": "1.3.9", "tsc-alias": "1.3.10",
"tsconfig-paths": "3.11.0", "tsconfig-paths": "3.11.0",
"tslint": "6.1.3", "tslint": "6.1.3",
"tslint-sonarts": "1.9.0", "tslint-sonarts": "1.9.0",
"twemoji-parser": "13.1.0", "twemoji-parser": "13.1.0",
"typeorm": "0.2.37", "typeorm": "0.2.38",
"typescript": "4.4.3", "typescript": "4.4.4",
"ulid": "2.3.0", "ulid": "2.3.0",
"uuid": "8.3.2", "uuid": "8.3.2",
"v-debounce": "0.1.2", "v-debounce": "0.1.2",
"vanilla-tilt": "1.7.2", "vanilla-tilt": "1.7.2",
"vue": "3.2.13", "vue": "3.2.20",
"vue-loader": "16.7.0", "vue-loader": "16.7.0",
"vue-prism-editor": "2.0.0-alpha.2", "vue-prism-editor": "2.0.0-alpha.2",
"vue-router": "4.0.5", "vue-router": "4.0.5",
@ -241,17 +240,17 @@
"vue-svg-loader": "0.17.0-beta.2", "vue-svg-loader": "0.17.0-beta.2",
"vuedraggable": "4.0.1", "vuedraggable": "4.0.1",
"web-push": "3.4.5", "web-push": "3.4.5",
"webpack": "5.53.0", "webpack": "5.58.2",
"webpack-cli": "4.8.0", "webpack-cli": "4.9.0",
"websocket": "1.0.34", "websocket": "1.0.34",
"ws": "8.2.2", "ws": "8.2.3",
"xev": "2.0.1" "xev": "2.0.1"
}, },
"devDependencies": { "devDependencies": {
"@redocly/openapi-core": "1.0.0-beta.54", "@redocly/openapi-core": "1.0.0-beta.54",
"@types/fluent-ffmpeg": "2.1.17", "@types/fluent-ffmpeg": "2.1.17",
"cross-env": "7.0.3", "cross-env": "7.0.3",
"cypress": "8.4.1", "cypress": "8.5.0",
"start-server-and-test": "1.14.0" "start-server-and-test": "1.14.0"
} }
} }

View file

@ -1,23 +0,0 @@
import { Command } from 'commander';
import config from '@/config/index';
const program = new Command();
program.version(config.version);
program.option('--no-daemons', 'Disable daemon processes (for debbuging)');
program.option('--disable-clustering', 'Disable clustering');
program.option('--only-server', 'Run server only (without job queue processing)');
program.option('--only-queue', 'Pocessing job queue only (without server)');
program.option('--quiet', 'Suppress all logs');
program.option('--verbose', 'Enable all logs');
program.option('--with-log-time', 'Include timestamp for each logs');
program.option('--slow', 'Delay all requests (for debbuging)');
program.option('--color', 'This option is a dummy for some external program\'s (e.g. forever) issue.');
program.parse(process.argv);
if (process.env.MK_ONLY_QUEUE) program.onlyQueue = true;
if (process.env.NODE_ENV === 'test') program.disableClustering = true;
//if (process.env.NODE_ENV === 'test') program.quiet = true;
if (process.env.NODE_ENV === 'test') program.noDaemons = true;
export { program };

View file

@ -3,7 +3,7 @@ import * as chalk from 'chalk';
import Xev from 'xev'; import Xev from 'xev';
import Logger from '@/services/logger'; import Logger from '@/services/logger';
import { program } from '../argv'; import { envOption } from '../env';
// for typeorm // for typeorm
import 'reflect-metadata'; import 'reflect-metadata';
@ -20,7 +20,7 @@ const ev = new Xev();
export default async function() { export default async function() {
process.title = `Misskey (${cluster.isMaster ? 'master' : 'worker'})`; process.title = `Misskey (${cluster.isMaster ? 'master' : 'worker'})`;
if (cluster.isMaster || program.disableClustering) { if (cluster.isMaster || envOption.disableClustering) {
await masterMain(); await masterMain();
if (cluster.isMaster) { if (cluster.isMaster) {
@ -28,7 +28,7 @@ export default async function() {
} }
} }
if (cluster.isWorker || program.disableClustering) { if (cluster.isWorker || envOption.disableClustering) {
await workerMain(); await workerMain();
} }
@ -60,7 +60,7 @@ cluster.on('exit', worker => {
}); });
// Display detail of unhandled promise rejection // Display detail of unhandled promise rejection
if (!program.quiet) { if (!envOption.quiet) {
process.on('unhandledRejection', console.dir); process.on('unhandledRejection', console.dir);
} }

View file

@ -11,7 +11,7 @@ import Logger from '@/services/logger';
import loadConfig from '@/config/load'; import loadConfig from '@/config/load';
import { Config } from '@/config/types'; import { Config } from '@/config/types';
import { lessThan } from '@/prelude/array'; import { lessThan } from '@/prelude/array';
import { program } from '../argv'; import { envOption } from '../env';
import { showMachineInfo } from '@/misc/show-machine-info'; import { showMachineInfo } from '@/misc/show-machine-info';
import { initDb } from '../db/postgre'; import { initDb } from '../db/postgre';
@ -25,7 +25,7 @@ const logger = new Logger('core', 'cyan');
const bootLogger = logger.createSubLogger('boot', 'magenta', false); const bootLogger = logger.createSubLogger('boot', 'magenta', false);
function greet() { function greet() {
if (!program.quiet) { if (!envOption.quiet) {
//#region Misskey logo //#region Misskey logo
const v = `v${meta.version}`; const v = `v${meta.version}`;
console.log(' _____ _ _ '); console.log(' _____ _ _ ');
@ -73,13 +73,13 @@ export async function masterMain() {
bootLogger.succ('Misskey initialized'); bootLogger.succ('Misskey initialized');
if (!program.disableClustering) { if (!envOption.disableClustering) {
await spawnWorkers(config.clusterLimit); await spawnWorkers(config.clusterLimit);
} }
bootLogger.succ(`Now listening on port ${config.port} on ${config.url}`, null, true); bootLogger.succ(`Now listening on port ${config.port} on ${config.url}`, null, true);
if (!program.noDaemons) { if (!envOption.noDaemons) {
require('../daemons/server-stats').default(); require('../daemons/server-stats').default();
require('../daemons/queue-stats').default(); require('../daemons/queue-stats').default();
require('../daemons/janitor').default(); require('../daemons/janitor').default();

View file

@ -1,9 +1,10 @@
import { del, get, set } from '@client/scripts/idb-proxy'; import { del, get, set } from '@client/scripts/idb-proxy';
import { reactive } from 'vue'; import { reactive } from 'vue';
import { apiUrl } from '@client/config'; import { apiUrl } from '@client/config';
import { waiting } from '@client/os'; import { waiting, api, popup, popupMenu, success } from '@client/os';
import { unisonReload, reloadChannel } from '@client/scripts/unison-reload'; import { unisonReload, reloadChannel } from '@client/scripts/unison-reload';
import { showSuspendedDialog } from './scripts/show-suspended-dialog'; import { showSuspendedDialog } from './scripts/show-suspended-dialog';
import { i18n } from './i18n';
// TODO: 他のタブと永続化されたstateを同期 // TODO: 他のタブと永続化されたstateを同期
@ -129,6 +130,77 @@ export async function login(token: Account['token'], redirect?: string) {
unisonReload(); unisonReload();
} }
export async function openAccountMenu(ev: MouseEvent) {
function showSigninDialog() {
popup(import('@client/components/signin-dialog.vue'), {}, {
done: res => {
addAccount(res.id, res.i);
success();
},
}, 'closed');
}
function createAccount() {
popup(import('@client/components/signup-dialog.vue'), {}, {
done: res => {
addAccount(res.id, res.i);
switchAccountWithToken(res.i);
},
}, 'closed');
}
async function switchAccount(account: any) {
const storedAccounts = await getAccounts();
const token = storedAccounts.find(x => x.id === account.id).token;
switchAccountWithToken(token);
}
function switchAccountWithToken(token: string) {
login(token);
}
const storedAccounts = await getAccounts().then(accounts => accounts.filter(x => x.id !== $i.id));
const accountsPromise = api('users/show', { userIds: storedAccounts.map(x => x.id) });
const accountItemPromises = storedAccounts.map(a => new Promise(res => {
accountsPromise.then(accounts => {
const account = accounts.find(x => x.id === a.id);
if (account == null) return res(null);
res({
type: 'user',
user: account,
action: () => { switchAccount(account); }
});
});
}));
popupMenu([...[{
type: 'link',
text: i18n.locale.profile,
to: `/@${ $i.username }`,
avatar: $i,
}, null, ...accountItemPromises, {
icon: 'fas fa-plus',
text: i18n.locale.addAccount,
action: () => {
popupMenu([{
text: i18n.locale.existingAccount,
action: () => { showSigninDialog(); },
}, {
text: i18n.locale.createAccount,
action: () => { createAccount(); },
}], ev.currentTarget || ev.target);
},
}, {
type: 'link',
icon: 'fas fa-users',
text: i18n.locale.manageAccounts,
to: `/settings/accounts`,
}]], ev.currentTarget || ev.target, {
align: 'left'
});
}
// このファイルに書きたくないけどここに書かないと何故かVeturが認識しない // このファイルに書きたくないけどここに書かないと何故かVeturが認識しない
declare module '@vue/runtime-core' { declare module '@vue/runtime-core' {
interface ComponentCustomProperties { interface ComponentCustomProperties {

View file

@ -25,7 +25,7 @@
<script lang="ts"> <script lang="ts">
import { defineComponent, markRaw } from 'vue'; import { defineComponent, markRaw } from 'vue';
import XWindow from '@client/components/ui/window.vue'; import XWindow from '@client/components/ui/window.vue';
import MkTextarea from '@client/components/ui/textarea.vue'; import MkTextarea from '@client/components/form/textarea.vue';
import MkButton from '@client/components/ui/button.vue'; import MkButton from '@client/components/ui/button.vue';
import * as os from '@client/os'; import * as os from '@client/os';

View file

@ -10,12 +10,12 @@
</li> </li>
<li @click="chooseUser()" @keydown="onKeydown" tabindex="-1" class="choose">{{ $ts.selectUser }}</li> <li @click="chooseUser()" @keydown="onKeydown" tabindex="-1" class="choose">{{ $ts.selectUser }}</li>
</ol> </ol>
<ol class="hashtags" ref="suggests" v-if="hashtags.length > 0"> <ol class="hashtags" ref="suggests" v-else-if="hashtags.length > 0">
<li v-for="hashtag in hashtags" @click="complete(type, hashtag)" @keydown="onKeydown" tabindex="-1"> <li v-for="hashtag in hashtags" @click="complete(type, hashtag)" @keydown="onKeydown" tabindex="-1">
<span class="name">{{ hashtag }}</span> <span class="name">{{ hashtag }}</span>
</li> </li>
</ol> </ol>
<ol class="emojis" ref="suggests" v-if="emojis.length > 0"> <ol class="emojis" ref="suggests" v-else-if="emojis.length > 0">
<li v-for="emoji in emojis" @click="complete(type, emoji.emoji)" @keydown="onKeydown" tabindex="-1"> <li v-for="emoji in emojis" @click="complete(type, emoji.emoji)" @keydown="onKeydown" tabindex="-1">
<span class="emoji" v-if="emoji.isCustomEmoji"><img :src="$store.state.disableShowingAnimatedImages ? getStaticImageUrl(emoji.url) : emoji.url" :alt="emoji.emoji"/></span> <span class="emoji" v-if="emoji.isCustomEmoji"><img :src="$store.state.disableShowingAnimatedImages ? getStaticImageUrl(emoji.url) : emoji.url" :alt="emoji.emoji"/></span>
<span class="emoji" v-else-if="!$store.state.useOsNativeEmojis"><img :src="emoji.url" :alt="emoji.emoji"/></span> <span class="emoji" v-else-if="!$store.state.useOsNativeEmojis"><img :src="emoji.url" :alt="emoji.emoji"/></span>
@ -24,6 +24,11 @@
<span class="alias" v-if="emoji.aliasOf">({{ emoji.aliasOf }})</span> <span class="alias" v-if="emoji.aliasOf">({{ emoji.aliasOf }})</span>
</li> </li>
</ol> </ol>
<ol class="mfmTags" ref="suggests" v-else-if="mfmTags.length > 0">
<li v-for="tag in mfmTags" @click="complete(type, tag)" @keydown="onKeydown" tabindex="-1">
<span class="tag">{{ tag }}</span>
</li>
</ol>
</div> </div>
</template> </template>
@ -106,6 +111,8 @@ emojiDefinitions.sort((a, b) => a.name.length - b.name.length);
const emojiDb = markRaw(emojiDefinitions.concat(emjdb)); const emojiDb = markRaw(emojiDefinitions.concat(emjdb));
//#endregion //#endregion
const MFM_TAGS = ['tada', 'jelly', 'twitch', 'shake', 'spin', 'jump', 'bounce', 'flip', 'x2', 'x3', 'x4', 'font', 'blur', 'rainbow', 'sparkle'];
export default defineComponent({ export default defineComponent({
props: { props: {
type: { type: {
@ -137,11 +144,6 @@ export default defineComponent({
type: Number, type: Number,
required: true, required: true,
}, },
showing: {
type: Boolean,
required: true
},
}, },
emits: ['done', 'closed'], emits: ['done', 'closed'],
@ -154,18 +156,11 @@ export default defineComponent({
hashtags: [], hashtags: [],
emojis: [], emojis: [],
items: [], items: [],
mfmTags: [],
select: -1, select: -1,
} }
}, },
watch: {
showing() {
if (!this.showing) {
this.$emit('closed');
}
}
},
updated() { updated() {
this.setPosition(); this.setPosition();
this.items = (this.$refs.suggests as Element | undefined)?.children || []; this.items = (this.$refs.suggests as Element | undefined)?.children || [];
@ -236,7 +231,7 @@ export default defineComponent({
} }
} }
if (this.type == 'user') { if (this.type === 'user') {
if (this.q == null) { if (this.q == null) {
this.users = []; this.users = [];
this.fetching = false; this.fetching = false;
@ -262,7 +257,7 @@ export default defineComponent({
sessionStorage.setItem(cacheKey, JSON.stringify(users)); sessionStorage.setItem(cacheKey, JSON.stringify(users));
}); });
} }
} else if (this.type == 'hashtag') { } else if (this.type === 'hashtag') {
if (this.q == null || this.q == '') { if (this.q == null || this.q == '') {
this.hashtags = JSON.parse(localStorage.getItem('hashtags') || '[]'); this.hashtags = JSON.parse(localStorage.getItem('hashtags') || '[]');
this.fetching = false; this.fetching = false;
@ -286,7 +281,7 @@ export default defineComponent({
}); });
} }
} }
} else if (this.type == 'emoji') { } else if (this.type === 'emoji') {
if (this.q == null || this.q == '') { if (this.q == null || this.q == '') {
// 使 // 使
this.emojis = this.$store.state.recentlyUsedEmojis.map(emoji => emojiDb.find(e => e.emoji == emoji)).filter(x => x != null); this.emojis = this.$store.state.recentlyUsedEmojis.map(emoji => emojiDb.find(e => e.emoji == emoji)).filter(x => x != null);
@ -314,6 +309,13 @@ export default defineComponent({
} }
this.emojis = matched; this.emojis = matched;
} else if (this.type === 'mfmTag') {
if (this.q == null || this.q == '') {
this.mfmTags = MFM_TAGS;
return;
}
this.mfmTags = MFM_TAGS.filter(tag => tag.startsWith(this.q));
} }
}, },
@ -490,5 +492,11 @@ export default defineComponent({
margin: 0 0 0 8px; margin: 0 0 0 8px;
} }
} }
> .mfmTags > li {
.name {
}
}
} }
</style> </style>

View file

@ -39,7 +39,7 @@ export default defineComponent({
type: String, type: String,
required: true, required: true,
}, },
value: { modelValue: {
type: String, type: String,
}, },
}, },
@ -116,7 +116,7 @@ export default defineComponent({
} }
}, },
callback(response?: string) { callback(response?: string) {
this.$emit('update:value', typeof response == 'string' ? response : null); this.$emit('update:modelValue', typeof response == 'string' ? response : null);
}, },
}, },
}); });

View file

@ -91,7 +91,7 @@ export default defineComponent({
width: 31px; width: 31px;
} }
&:focus { &:focus-visible {
&:after { &:after {
content: ""; content: "";
pointer-events: none; pointer-events: none;

View file

@ -1,7 +1,7 @@
<template> <template>
<button class="nrvgflfu _button" @click="toggle"> <button class="nrvgflfu _button" @click="toggle">
<b>{{ value ? $ts._cw.hide : $ts._cw.show }}</b> <b>{{ modelValue ? $ts._cw.hide : $ts._cw.show }}</b>
<span v-if="!value">{{ label }}</span> <span v-if="!modelValue">{{ label }}</span>
</button> </button>
</template> </template>
@ -12,7 +12,7 @@ import { concat } from '../../prelude/array';
export default defineComponent({ export default defineComponent({
props: { props: {
value: { modelValue: {
type: Boolean, type: Boolean,
required: true required: true
}, },
@ -36,7 +36,7 @@ export default defineComponent({
length, length,
toggle() { toggle() {
this.$emit('update:value', !this.value); this.$emit('update:modelValue', !this.modelValue);
} }
} }
}); });

View file

@ -21,39 +21,39 @@ export default defineComponent({
<style lang="scss" scoped> <style lang="scss" scoped>
.rbusrurv { .rbusrurv {
// CSS // CSS
--formXPadding: 32px; --debobigegoXPadding: 32px;
--formYPadding: 32px; --debobigegoYPadding: 32px;
--formContentHMargin: 16px; --debobigegoContentHMargin: 16px;
font-size: 95%; font-size: 95%;
line-height: 1.3em; line-height: 1.3em;
background: var(--bg); background: var(--bg);
padding: var(--formYPadding) var(--formXPadding); padding: var(--debobigegoYPadding) var(--debobigegoXPadding);
max-width: 750px; max-width: 750px;
margin: 0 auto; margin: 0 auto;
&:not(.wide).max-width_400px { &:not(.wide).max-width_400px {
--formXPadding: 0px; --debobigegoXPadding: 0px;
> ::v-deep(*) { > ::v-deep(*) {
._formPanel { ._debobigegoPanel {
border: solid 0.5px var(--divider); border: solid 0.5px var(--divider);
border-radius: 0; border-radius: 0;
border-left: none; border-left: none;
border-right: none; border-right: none;
} }
._form_group { ._debobigego_group {
> *:not(._formNoConcat) { > *:not(._debobigegoNoConcat) {
&:not(:last-child):not(._formNoConcatPrev) { &:not(:last-child):not(._debobigegoNoConcatPrev) {
&._formPanel, ._formPanel { &._debobigegoPanel, ._debobigegoPanel {
border-bottom: solid 0.5px var(--divider); border-bottom: solid 0.5px var(--divider);
} }
} }
&:not(:first-child):not(._formNoConcatNext) { &:not(:first-child):not(._debobigegoNoConcatNext) {
&._formPanel, ._formPanel { &._debobigegoPanel, ._debobigegoPanel {
border-top: none; border-top: none;
} }
} }

View file

@ -1,7 +1,7 @@
<template> <template>
<div class="yzpgjkxe _formItem"> <div class="yzpgjkxe _debobigegoItem">
<div class="_formLabel"><slot name="label"></slot></div> <div class="_debobigegoLabel"><slot name="label"></slot></div>
<button class="main _button _formPanel _formClickable" :class="{ center, primary, danger }"> <button class="main _button _debobigegoPanel _debobigegoClickable" :class="{ center, primary, danger }">
<slot></slot> <slot></slot>
<div class="suffix"> <div class="suffix">
<slot name="suffix"></slot> <slot name="suffix"></slot>
@ -10,13 +10,13 @@
</div> </div>
</div> </div>
</button> </button>
<div class="_formCaption"><slot name="desc"></slot></div> <div class="_debobigegoCaption"><slot name="desc"></slot></div>
</div> </div>
</template> </template>
<script lang="ts"> <script lang="ts">
import { defineComponent } from 'vue'; import { defineComponent } from 'vue';
import './form.scss'; import './debobigego.scss';
export default defineComponent({ export default defineComponent({
props: { props: {

View file

@ -1,9 +1,9 @@
._formPanel { ._debobigegoPanel {
background: var(--panel); background: var(--panel);
border-radius: var(--radius); border-radius: var(--radius);
transition: background 0.2s ease; transition: background 0.2s ease;
&._formClickable { &._debobigegoClickable {
&:hover { &:hover {
//background: var(--panelHighlight); //background: var(--panelHighlight);
} }
@ -15,8 +15,8 @@
} }
} }
._formLabel, ._debobigegoLabel,
._formCaption { ._debobigegoCaption {
font-size: 80%; font-size: 80%;
color: var(--fgTransparentWeak); color: var(--fgTransparentWeak);
@ -25,28 +25,28 @@
} }
} }
._formLabel { ._debobigegoLabel {
position: sticky; position: sticky;
top: var(--stickyTop, 0px); top: var(--stickyTop, 0px);
z-index: 2; z-index: 2;
margin: -8px calc(var(--formXPadding) * -1) 0 calc(var(--formXPadding) * -1); margin: -8px calc(var(--debobigegoXPadding) * -1) 0 calc(var(--debobigegoXPadding) * -1);
padding: 8px calc(var(--formContentHMargin) + var(--formXPadding)) 8px calc(var(--formContentHMargin) + var(--formXPadding)); padding: 8px calc(var(--debobigegoContentHMargin) + var(--debobigegoXPadding)) 8px calc(var(--debobigegoContentHMargin) + var(--debobigegoXPadding));
background: var(--X17); background: var(--X17);
-webkit-backdrop-filter: var(--blur, blur(10px)); -webkit-backdrop-filter: var(--blur, blur(10px));
backdrop-filter: var(--blur, blur(10px)); backdrop-filter: var(--blur, blur(10px));
} }
._themeChanging_ ._formLabel { ._themeChanging_ ._debobigegoLabel {
transition: none !important; transition: none !important;
background: transparent; background: transparent;
} }
._formCaption { ._debobigegoCaption {
padding: 8px var(--formContentHMargin) 0 var(--formContentHMargin); padding: 8px var(--debobigegoContentHMargin) 0 var(--debobigegoContentHMargin);
} }
._formItem { ._debobigegoItem {
& + ._formItem { & + ._debobigegoItem {
margin-top: 24px; margin-top: 24px;
} }
} }

View file

@ -1,10 +1,10 @@
<template> <template>
<div class="vrtktovg _formItem _formNoConcat" v-size="{ max: [500] }" v-sticky-container> <div class="vrtktovg _debobigegoItem _debobigegoNoConcat" v-size="{ max: [500] }" v-sticky-container>
<div class="_formLabel"><slot name="label"></slot></div> <div class="_debobigegoLabel"><slot name="label"></slot></div>
<div class="main _form_group" ref="child"> <div class="main _debobigego_group" ref="child">
<slot></slot> <slot></slot>
</div> </div>
<div class="_formCaption"><slot name="caption"></slot></div> <div class="_debobigegoCaption"><slot name="caption"></slot></div>
</div> </div>
</template> </template>
@ -20,9 +20,9 @@ export default defineComponent({
const els = Array.from(child.value.children); const els = Array.from(child.value.children);
for (let i = 0; i < els.length; i++) { for (let i = 0; i < els.length; i++) {
const el = els[i]; const el = els[i];
if (el.classList.contains('_formNoConcat')) { if (el.classList.contains('_debobigegoNoConcat')) {
if (els[i - 1]) els[i - 1].classList.add('_formNoConcatPrev'); if (els[i - 1]) els[i - 1].classList.add('_debobigegoNoConcatPrev');
if (els[i + 1]) els[i + 1].classList.add('_formNoConcatNext'); if (els[i + 1]) els[i + 1].classList.add('_debobigegoNoConcatNext');
} }
} }
}; };
@ -52,21 +52,21 @@ export default defineComponent({
<style lang="scss" scoped> <style lang="scss" scoped>
.vrtktovg { .vrtktovg {
> .main { > .main {
> ::v-deep(*):not(._formNoConcat) { > ::v-deep(*):not(._debobigegoNoConcat) {
&:not(._formNoConcatNext) { &:not(._debobigegoNoConcatNext) {
margin: 0; margin: 0;
} }
&:not(:last-child):not(._formNoConcatPrev) { &:not(:last-child):not(._debobigegoNoConcatPrev) {
&._formPanel, ._formPanel { &._debobigegoPanel, ._debobigegoPanel {
border-bottom: solid 0.5px var(--divider); border-bottom: solid 0.5px var(--divider);
border-bottom-left-radius: 0; border-bottom-left-radius: 0;
border-bottom-right-radius: 0; border-bottom-right-radius: 0;
} }
} }
&:not(:first-child):not(._formNoConcatNext) { &:not(:first-child):not(._debobigegoNoConcatNext) {
&._formPanel, ._formPanel { &._debobigegoPanel, ._debobigegoPanel {
border-top: none; border-top: none;
border-top-left-radius: 0; border-top-left-radius: 0;
border-top-right-radius: 0; border-top-right-radius: 0;

View file

@ -1,6 +1,6 @@
<template> <template>
<div class="fzenkabp _formItem"> <div class="fzenkabp _debobigegoItem">
<div class="_formPanel" :class="{ warn }"> <div class="_debobigegoPanel" :class="{ warn }">
<i v-if="warn" class="fas fa-exclamation-triangle"></i> <i v-if="warn" class="fas fa-exclamation-triangle"></i>
<i v-else class="fas fa-info-circle"></i> <i v-else class="fas fa-info-circle"></i>
<slot></slot> <slot></slot>

View file

@ -1,49 +1,53 @@
<template> <template>
<div class="matxzzsk"> <FormGroup class="_debobigegoItem">
<div class="label" @click="focus"><slot name="label"></slot></div> <template #label><slot></slot></template>
<div class="input" :class="{ inline, disabled, focused }"> <div class="ztzhwixg _debobigegoItem" :class="{ inline, disabled }">
<div class="prefix" ref="prefixEl"><slot name="prefix"></slot></div> <div class="icon" ref="icon"><slot name="icon"></slot></div>
<input ref="inputEl" <div class="input _debobigegoPanel">
:type="type" <div class="prefix" ref="prefixEl"><slot name="prefix"></slot></div>
v-model="v" <input ref="inputEl"
:disabled="disabled" :type="type"
:required="required" v-model="v"
:readonly="readonly" :disabled="disabled"
:placeholder="placeholder" :required="required"
:pattern="pattern" :readonly="readonly"
:autocomplete="autocomplete" :placeholder="placeholder"
:spellcheck="spellcheck" :pattern="pattern"
:step="step" :autocomplete="autocomplete"
@focus="focused = true" :spellcheck="spellcheck"
@blur="focused = false" :step="step"
@keydown="onKeydown($event)" @focus="focused = true"
@input="onInput" @blur="focused = false"
:list="id" @keydown="onKeydown($event)"
> @input="onInput"
<datalist :id="id" v-if="datalist"> :list="id"
<option v-for="data in datalist" :value="data"/> >
</datalist> <datalist :id="id" v-if="datalist">
<div class="suffix" ref="suffixEl"><slot name="suffix"></slot></div> <option v-for="data in datalist" :value="data"/>
</datalist>
<div class="suffix" ref="suffixEl"><slot name="suffix"></slot></div>
</div>
</div> </div>
<div class="caption"><slot name="caption"></slot></div> <template #caption><slot name="desc"></slot></template>
<MkButton v-if="manualSave && changed" @click="updated" primary><i class="fas fa-save"></i> {{ $ts.save }}</MkButton> <FormButton v-if="manualSave && changed" @click="updated" primary><i class="fas fa-save"></i> {{ $ts.save }}</FormButton>
</div> </FormGroup>
</template> </template>
<script lang="ts"> <script lang="ts">
import { defineComponent, onMounted, onUnmounted, nextTick, ref, watch, computed, toRefs } from 'vue'; import { defineComponent, onMounted, onUnmounted, nextTick, ref, watch, computed, toRefs } from 'vue';
import MkButton from './button.vue'; import './debobigego.scss';
import { debounce } from 'throttle-debounce'; import FormButton from './button.vue';
import FormGroup from './group.vue';
export default defineComponent({ export default defineComponent({
components: { components: {
MkButton, FormGroup,
FormButton,
}, },
props: { props: {
modelValue: { modelValue: {
required: true required: false
}, },
type: { type: {
type: String, type: String,
@ -92,20 +96,13 @@ export default defineComponent({
required: false, required: false,
default: false default: false
}, },
debounce: {
type: Boolean,
required: false,
default: false
},
manualSave: { manualSave: {
type: Boolean, type: Boolean,
required: false, required: false,
default: false default: false
}, },
}, },
emits: ['change', 'keydown', 'enter', 'update:modelValue'], emits: ['change', 'keydown', 'enter', 'update:modelValue'],
setup(props, context) { setup(props, context) {
const { modelValue, type, autofocus } = toRefs(props); const { modelValue, type, autofocus } = toRefs(props);
const v = ref(modelValue.value); const v = ref(modelValue.value);
@ -140,19 +137,13 @@ export default defineComponent({
} }
}; };
const debouncedUpdated = debounce(1000, updated); watch(modelValue.value, newValue => {
watch(modelValue, newValue => {
v.value = newValue; v.value = newValue;
}); });
watch(v, newValue => { watch(v, newValue => {
if (!props.manualSave) { if (!props.manualSave) {
if (props.debounce) { updated();
debouncedUpdated();
} else {
updated();
}
} }
invalid.value = inputEl.value.validity.badInput; invalid.value = inputEl.value.validity.badInput;
@ -205,68 +196,59 @@ export default defineComponent({
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
.matxzzsk { .ztzhwixg {
margin: 1.5em 0; position: relative;
> .label { > .icon {
font-size: 0.85em; position: absolute;
padding: 0 0 8px 12px; top: 0;
user-select: none; left: 0;
width: 24px;
text-align: center;
line-height: 32px;
&:empty { &:not(:empty) + .input {
display: none; margin-left: 28px;
}
}
> .caption {
font-size: 0.8em;
padding: 8px 0 0 12px;
color: var(--fgTransparentWeak);
&:empty {
display: none;
} }
} }
> .input { > .input {
$height: 42px; $height: 48px;
position: relative; position: relative;
> input { > input {
appearance: none;
-webkit-appearance: none;
display: block; display: block;
height: $height; height: $height;
width: 100%; width: 100%;
margin: 0; margin: 0;
padding: 0 12px; padding: 0 16px;
font: inherit; font: inherit;
font-weight: normal; font-weight: normal;
font-size: 1em; font-size: 1em;
color: var(--fg); line-height: $height;
background: var(--panel); color: var(--inputText);
border: solid 0.5px var(--inputBorder); background: transparent;
border-radius: 6px; border: none;
border-radius: 0;
outline: none; outline: none;
box-shadow: none; box-shadow: none;
box-sizing: border-box; box-sizing: border-box;
transition: border-color 0.1s ease-out;
&:hover { &[type='file'] {
border-color: var(--inputBorderHover); display: none;
} }
} }
> .prefix, > .prefix,
> .suffix { > .suffix {
display: flex; display: block;
align-items: center;
position: absolute; position: absolute;
z-index: 1; z-index: 1;
top: 0; top: 0;
padding: 0 12px; padding: 0 16px;
font-size: 1em; font-size: 1em;
height: $height; line-height: $height;
color: var(--inputLabel);
pointer-events: none; pointer-events: none;
&:empty { &:empty {
@ -285,32 +267,25 @@ export default defineComponent({
> .prefix { > .prefix {
left: 0; left: 0;
padding-right: 6px; padding-right: 8px;
} }
> .suffix { > .suffix {
right: 0; right: 0;
padding-left: 6px; padding-left: 8px;
} }
}
&.inline { &.inline {
display: inline-block; display: inline-block;
margin: 0; margin: 0;
} }
&.focused { &.disabled {
> input { opacity: 0.7;
border-color: var(--accent);
//box-shadow: 0 0 0 4px var(--focus);
}
}
&.disabled { &, * {
opacity: 0.7; cursor: not-allowed !important;
&, * {
cursor: not-allowed !important;
}
} }
} }
} }

View file

@ -1,6 +1,6 @@
<template> <template>
<div class="_formItem"> <div class="_debobigegoItem">
<div class="_formPanel anocepby"> <div class="_debobigegoPanel anocepby">
<span class="key"><slot name="key"></slot></span> <span class="key"><slot name="key"></slot></span>
<span class="value"><slot name="value"></slot></span> <span class="value"><slot name="value"></slot></span>
</div> </div>
@ -9,7 +9,7 @@
<script lang="ts"> <script lang="ts">
import { defineComponent } from 'vue'; import { defineComponent } from 'vue';
import './form.scss'; import './debobigego.scss';
export default defineComponent({ export default defineComponent({
@ -20,7 +20,7 @@ export default defineComponent({
.anocepby { .anocepby {
display: flex; display: flex;
align-items: center; align-items: center;
padding: 14px var(--formContentHMargin); padding: 14px var(--debobigegoContentHMargin);
> .key { > .key {
margin-right: 12px; margin-right: 12px;

View file

@ -1,6 +1,6 @@
<template> <template>
<div class="qmfkfnzi _formItem"> <div class="qmfkfnzi _debobigegoItem">
<a class="main _button _formPanel _formClickable" :href="to" target="_blank" v-if="external"> <a class="main _button _debobigegoPanel _debobigegoClickable" :href="to" target="_blank" v-if="external">
<span class="icon"><slot name="icon"></slot></span> <span class="icon"><slot name="icon"></slot></span>
<span class="text"><slot></slot></span> <span class="text"><slot></slot></span>
<span class="right"> <span class="right">
@ -8,7 +8,7 @@
<i class="fas fa-external-link-alt icon"></i> <i class="fas fa-external-link-alt icon"></i>
</span> </span>
</a> </a>
<MkA class="main _button _formPanel _formClickable" :class="{ active }" :to="to" :behavior="behavior" v-else> <MkA class="main _button _debobigegoPanel _debobigegoClickable" :class="{ active }" :to="to" :behavior="behavior" v-else>
<span class="icon"><slot name="icon"></slot></span> <span class="icon"><slot name="icon"></slot></span>
<span class="text"><slot></slot></span> <span class="text"><slot></slot></span>
<span class="right"> <span class="right">
@ -21,7 +21,7 @@
<script lang="ts"> <script lang="ts">
import { defineComponent } from 'vue'; import { defineComponent } from 'vue';
import './form.scss'; import './debobigego.scss';
export default defineComponent({ export default defineComponent({
props: { props: {

View file

@ -1,8 +1,8 @@
<template> <template>
<FormGroup class="_formItem"> <FormGroup class="_debobigegoItem">
<template #label><slot></slot></template> <template #label><slot></slot></template>
<div class="drooglns _formItem" :class="{ tall }"> <div class="drooglns _debobigegoItem" :class="{ tall }">
<div class="input _formPanel"> <div class="input _debobigegoPanel">
<textarea class="_monospace" <textarea class="_monospace"
v-model="v" v-model="v"
readonly readonly
@ -17,7 +17,7 @@
<script lang="ts"> <script lang="ts">
import { defineComponent, ref, toRefs, watch } from 'vue'; import { defineComponent, ref, toRefs, watch } from 'vue';
import * as JSON5 from 'json5'; import * as JSON5 from 'json5';
import './form.scss'; import './debobigego.scss';
import FormGroup from './group.vue'; import FormGroup from './group.vue';
export default defineComponent({ export default defineComponent({
@ -75,7 +75,7 @@ export default defineComponent({
max-width: 100%; max-width: 100%;
min-height: 130px; min-height: 130px;
margin: 0; margin: 0;
padding: 16px var(--formContentHMargin); padding: 16px var(--debobigegoContentHMargin);
box-sizing: border-box; box-sizing: border-box;
font: inherit; font: inherit;
font-weight: normal; font-weight: normal;

View file

@ -1,5 +1,5 @@
<template> <template>
<FormGroup class="uljviswt _formItem"> <FormGroup class="uljviswt _debobigegoItem">
<template #label><slot name="label"></slot></template> <template #label><slot name="label"></slot></template>
<slot :items="items"></slot> <slot :items="items"></slot>
<div class="empty" v-if="empty" key="_empty_"> <div class="empty" v-if="empty" key="_empty_">

View file

@ -0,0 +1,112 @@
<script lang="ts">
import { defineComponent, h } from 'vue';
import MkRadio from '@client/components/form/radio.vue';
import './debobigego.scss';
export default defineComponent({
components: {
MkRadio
},
props: {
modelValue: {
required: false
},
},
data() {
return {
value: this.modelValue,
}
},
watch: {
modelValue() {
this.value = this.modelValue;
},
value() {
this.$emit('update:modelValue', this.value);
}
},
render() {
const label = this.$slots.desc();
let options = this.$slots.default();
// Fragment
if (options.length === 1 && options[0].props == null) options = options[0].children;
return h('div', {
class: 'cnklmpwm _debobigegoItem'
}, [
h('div', {
class: '_debobigegoLabel',
}, label),
...options.map(option => h('button', {
class: '_button _debobigegoPanel _debobigegoClickable',
key: option.key,
onClick: () => this.value = option.props.value,
}, [h('span', {
class: ['check', { checked: this.value === option.props.value }],
}), option.children]))
]);
}
});
</script>
<style lang="scss">
.cnklmpwm {
> button {
display: block;
width: 100%;
box-sizing: border-box;
padding: 14px 18px;
text-align: left;
&:not(:first-of-type) {
border-top: none !important;
border-top-left-radius: 0;
border-top-right-radius: 0;
}
&:not(:last-of-type) {
border-bottom: solid 0.5px var(--divider);
border-bottom-left-radius: 0;
border-bottom-right-radius: 0;
}
> .check {
display: inline-block;
vertical-align: bottom;
position: relative;
width: 16px;
height: 16px;
margin-right: 8px;
background: none;
border: 2px solid var(--inputBorder);
border-radius: 100%;
transition: inherit;
&:after {
content: "";
display: block;
position: absolute;
top: 3px;
right: 3px;
bottom: 3px;
left: 3px;
border-radius: 100%;
opacity: 0;
transform: scale(0);
transition: .4s cubic-bezier(.25,.8,.25,1);
}
&.checked {
border-color: var(--accent);
&:after {
background-color: var(--accent);
transform: scale(1);
opacity: 1;
}
}
}
}
}
</style>

View file

@ -0,0 +1,122 @@
<template>
<div class="ifitouly _debobigegoItem" :class="{ focused, disabled }">
<div class="_debobigegoLabel"><slot name="label"></slot></div>
<div class="_debobigegoPanel main">
<input
type="range"
ref="input"
v-model="v"
:disabled="disabled"
:min="min"
:max="max"
:step="step"
@focus="focused = true"
@blur="focused = false"
@input="$emit('update:value', $event.target.value)"
/>
</div>
<div class="_debobigegoCaption"><slot name="caption"></slot></div>
</div>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
export default defineComponent({
props: {
value: {
type: Number,
required: false,
default: 0
},
disabled: {
type: Boolean,
required: false,
default: false
},
min: {
type: Number,
required: false,
default: 0
},
max: {
type: Number,
required: false,
default: 100
},
step: {
type: Number,
required: false,
default: 1
},
},
data() {
return {
v: this.value,
focused: false
};
},
watch: {
value(v) {
this.v = parseFloat(v);
}
},
});
</script>
<style lang="scss" scoped>
.ifitouly {
position: relative;
> .main {
padding: 22px 16px;
> input {
display: block;
-webkit-appearance: none;
-moz-appearance: none;
appearance: none;
background: var(--X10);
height: 4px;
width: 100%;
box-sizing: border-box;
margin: 0;
outline: 0;
border: 0;
border-radius: 7px;
&.disabled {
opacity: 0.6;
cursor: not-allowed;
}
&::-webkit-slider-thumb {
-webkit-appearance: none;
appearance: none;
cursor: pointer;
width: 20px;
height: 20px;
display: block;
border-radius: 50%;
border: none;
background: var(--accent);
box-shadow: 0 0 6px rgba(0, 0, 0, 0.3);
box-sizing: content-box;
}
&::-moz-range-thumb {
-moz-appearance: none;
appearance: none;
cursor: pointer;
width: 20px;
height: 20px;
display: block;
border-radius: 50%;
border: none;
background: var(--accent);
box-shadow: 0 0 6px rgba(0, 0, 0, 0.3);
}
}
}
}
</style>

View file

@ -0,0 +1,145 @@
<template>
<div class="yrtfrpux _debobigegoItem" :class="{ disabled, inline }">
<div class="_debobigegoLabel"><slot name="label"></slot></div>
<div class="icon" ref="icon"><slot name="icon"></slot></div>
<div class="input _debobigegoPanel _debobigegoClickable" @click="focus">
<div class="prefix" ref="prefix"><slot name="prefix"></slot></div>
<select ref="input"
v-model="v"
:required="required"
:disabled="disabled"
@focus="focused = true"
@blur="focused = false"
>
<slot></slot>
</select>
<div class="suffix">
<i class="fas fa-chevron-down"></i>
</div>
</div>
<div class="_debobigegoCaption"><slot name="caption"></slot></div>
</div>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
import './debobigego.scss';
export default defineComponent({
props: {
modelValue: {
required: false
},
required: {
type: Boolean,
required: false
},
disabled: {
type: Boolean,
required: false
},
inline: {
type: Boolean,
required: false,
default: false
},
},
data() {
return {
};
},
computed: {
v: {
get() {
return this.modelValue;
},
set(v) {
this.$emit('update:modelValue', v);
}
},
},
methods: {
focus() {
this.$refs.input.focus();
}
}
});
</script>
<style lang="scss" scoped>
.yrtfrpux {
position: relative;
> .icon {
position: absolute;
top: 0;
left: 0;
width: 24px;
text-align: center;
line-height: 32px;
&:not(:empty) + .input {
margin-left: 28px;
}
}
> .input {
display: flex;
position: relative;
> select {
display: block;
flex: 1;
width: 100%;
padding: 0 16px;
font: inherit;
font-weight: normal;
font-size: 1em;
height: 48px;
background: none;
border: none;
border-radius: 0;
outline: none;
box-shadow: none;
appearance: none;
-webkit-appearance: none;
color: var(--fg);
option,
optgroup {
color: var(--fg);
background: var(--bg);
}
}
> .prefix,
> .suffix {
display: block;
align-self: center;
justify-self: center;
font-size: 1em;
line-height: 32px;
color: var(--inputLabel);
pointer-events: none;
&:empty {
display: none;
}
> * {
display: block;
min-width: 16px;
}
}
> .prefix {
padding-right: 4px;
}
> .suffix {
padding: 0 16px 0 0;
opacity: 0.7;
}
}
}
</style>

View file

@ -1,15 +1,15 @@
<template> <template>
<transition name="fade" mode="out-in"> <transition name="fade" mode="out-in">
<div class="_formItem" v-if="pending"> <div class="_debobigegoItem" v-if="pending">
<div class="_formPanel"> <div class="_debobigegoPanel">
<MkLoading/> <MkLoading/>
</div> </div>
</div> </div>
<div v-else-if="resolved" class="_formItem"> <div v-else-if="resolved" class="_debobigegoItem">
<slot :result="result"></slot> <slot :result="result"></slot>
</div> </div>
<div class="_formItem" v-else> <div class="_debobigegoItem" v-else>
<div class="_formPanel eiurkvay"> <div class="_debobigegoPanel eiurkvay">
<div><i class="fas fa-exclamation-triangle"></i> {{ $ts.somethingHappened }}</div> <div><i class="fas fa-exclamation-triangle"></i> {{ $ts.somethingHappened }}</div>
<MkButton inline @click="retry" class="retry"><i class="fas fa-redo-alt"></i> {{ $ts.retry }}</MkButton> <MkButton inline @click="retry" class="retry"><i class="fas fa-redo-alt"></i> {{ $ts.retry }}</MkButton>
</div> </div>
@ -19,7 +19,7 @@
<script lang="ts"> <script lang="ts">
import { defineComponent, PropType, ref, watch } from 'vue'; import { defineComponent, PropType, ref, watch } from 'vue';
import './form.scss'; import './debobigego.scss';
import MkButton from '@client/components/ui/button.vue'; import MkButton from '@client/components/ui/button.vue';
export default defineComponent({ export default defineComponent({

View file

@ -0,0 +1,132 @@
<template>
<div class="ijnpvmgr _debobigegoItem">
<div class="main _debobigegoPanel _debobigegoClickable"
:class="{ disabled, checked }"
:aria-checked="checked"
:aria-disabled="disabled"
@click.prevent="toggle"
>
<input
type="checkbox"
ref="input"
:disabled="disabled"
@keydown.enter="toggle"
>
<span class="button" v-tooltip="checked ? $ts.itsOn : $ts.itsOff">
<span class="handle"></span>
</span>
<span class="label">
<span><slot></slot></span>
</span>
</div>
<div class="_debobigegoCaption"><slot name="desc"></slot></div>
</div>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
import './debobigego.scss';
export default defineComponent({
props: {
modelValue: {
type: Boolean,
default: false
},
disabled: {
type: Boolean,
default: false
}
},
computed: {
checked(): boolean {
return this.modelValue;
}
},
methods: {
toggle() {
if (this.disabled) return;
this.$emit('update:modelValue', !this.checked);
}
}
});
</script>
<style lang="scss" scoped>
.ijnpvmgr {
> .main {
position: relative;
display: flex;
padding: 14px 16px;
cursor: pointer;
> * {
user-select: none;
}
> input {
position: absolute;
width: 0;
height: 0;
opacity: 0;
margin: 0;
}
> .button {
position: relative;
display: inline-block;
flex-shrink: 0;
margin: 0;
width: 34px;
height: 22px;
background: var(--switchBg);
outline: none;
border-radius: 999px;
transition: all 0.3s;
cursor: pointer;
> .handle {
position: absolute;
top: 0;
left: 3px;
bottom: 0;
margin: auto 0;
border-radius: 100%;
transition: background-color 0.3s, transform 0.3s;
width: 16px;
height: 16px;
background-color: #fff;
pointer-events: none;
}
}
> .label {
margin-left: 12px;
display: block;
transition: inherit;
color: var(--fg);
> span {
display: block;
line-height: 20px;
transition: inherit;
}
}
&.disabled {
opacity: 0.6;
cursor: not-allowed;
}
&.checked {
> .button {
background-color: var(--accent);
> .handle {
transform: translateX(12px);
}
}
}
}
}
</style>

View file

@ -0,0 +1,161 @@
<template>
<FormGroup class="_debobigegoItem">
<template #label><slot></slot></template>
<div class="rivhosbp _debobigegoItem" :class="{ tall, pre }">
<div class="input _debobigegoPanel">
<textarea ref="input" :class="{ code, _monospace: code }"
v-model="v"
:required="required"
:readonly="readonly"
:pattern="pattern"
:autocomplete="autocomplete"
:spellcheck="!code"
@input="onInput"
@focus="focused = true"
@blur="focused = false"
></textarea>
</div>
</div>
<template #caption><slot name="desc"></slot></template>
<FormButton v-if="manualSave && changed" @click="updated" primary><i class="fas fa-save"></i> {{ $ts.save }}</FormButton>
</FormGroup>
</template>
<script lang="ts">
import { defineComponent, ref, toRefs, watch } from 'vue';
import './debobigego.scss';
import FormButton from './button.vue';
import FormGroup from './group.vue';
export default defineComponent({
components: {
FormGroup,
FormButton,
},
props: {
modelValue: {
required: false
},
required: {
type: Boolean,
required: false
},
readonly: {
type: Boolean,
required: false
},
pattern: {
type: String,
required: false
},
autocomplete: {
type: String,
required: false
},
code: {
type: Boolean,
required: false
},
tall: {
type: Boolean,
required: false,
default: false
},
pre: {
type: Boolean,
required: false,
default: false
},
manualSave: {
type: Boolean,
required: false,
default: false
},
},
setup(props, context) {
const { modelValue } = toRefs(props);
const v = ref(modelValue.value);
const changed = ref(false);
const inputEl = ref(null);
const focus = () => inputEl.value.focus();
const onInput = (ev) => {
changed.value = true;
context.emit('change', ev);
};
const updated = () => {
changed.value = false;
context.emit('update:modelValue', v.value);
};
watch(modelValue.value, newValue => {
v.value = newValue;
});
watch(v, newValue => {
if (!props.manualSave) {
updated();
}
});
return {
v,
updated,
changed,
focus,
onInput,
};
}
});
</script>
<style lang="scss" scoped>
.rivhosbp {
position: relative;
> .input {
position: relative;
> textarea {
display: block;
width: 100%;
min-width: 100%;
max-width: 100%;
min-height: 130px;
margin: 0;
padding: 16px;
box-sizing: border-box;
font: inherit;
font-weight: normal;
font-size: 1em;
background: transparent;
border: none;
border-radius: 0;
outline: none;
box-shadow: none;
color: var(--fg);
&.code {
tab-size: 2;
}
}
}
&.tall {
> .input {
> textarea {
min-height: 200px;
}
}
}
&.pre {
> .input {
> textarea {
white-space: pre;
}
}
}
}
</style>

View file

@ -1,5 +1,5 @@
<template> <template>
<div class="wthhikgt _formItem" v-size="{ max: [500] }"> <div class="wthhikgt _debobigegoItem" v-size="{ max: [500] }">
<slot></slot> <slot></slot>
</div> </div>
</template> </template>

View file

@ -40,8 +40,8 @@
import { defineComponent } from 'vue'; import { defineComponent } from 'vue';
import MkModal from '@client/components/ui/modal.vue'; import MkModal from '@client/components/ui/modal.vue';
import MkButton from '@client/components/ui/button.vue'; import MkButton from '@client/components/ui/button.vue';
import MkInput from '@client/components/ui/input.vue'; import MkInput from '@client/components/form/input.vue';
import MkSelect from '@client/components/ui/select.vue'; import MkSelect from '@client/components/form/select.vue';
export default defineComponent({ export default defineComponent({
components: { components: {

View file

@ -153,7 +153,7 @@ export default defineComponent({
height: var(--eachSize); height: var(--eachSize);
border-radius: 4px; border-radius: 4px;
&:focus { &:focus-visible {
outline: solid 2px var(--focus); outline: solid 2px var(--focus);
z-index: 1; z-index: 1;
} }

View file

@ -465,7 +465,7 @@ export default defineComponent({
height: var(--eachSize); height: var(--eachSize);
border-radius: 4px; border-radius: 4px;
&:focus { &:focus-visible {
outline: solid 2px var(--focus); outline: solid 2px var(--focus);
z-index: 1; z-index: 1;
} }

View file

@ -161,7 +161,7 @@ export default defineComponent({
width: 31px; width: 31px;
} }
&:focus { &:focus-visible {
&:after { &:after {
content: ""; content: "";
pointer-events: none; pointer-events: none;

View file

@ -35,7 +35,7 @@
import { defineComponent } from 'vue'; import { defineComponent } from 'vue';
import XModalWindow from '@client/components/ui/modal-window.vue'; import XModalWindow from '@client/components/ui/modal-window.vue';
import MkButton from '@client/components/ui/button.vue'; import MkButton from '@client/components/ui/button.vue';
import MkInput from '@client/components/ui/input.vue'; import MkInput from '@client/components/form/input.vue';
import * as os from '@client/os'; import * as os from '@client/os';
export default defineComponent({ export default defineComponent({

View file

@ -14,23 +14,23 @@
</template> </template>
<FormBase class="xkpnjxcv"> <FormBase class="xkpnjxcv">
<template v-for="item in Object.keys(form).filter(item => !form[item].hidden)"> <template v-for="item in Object.keys(form).filter(item => !form[item].hidden)">
<FormInput v-if="form[item].type === 'number'" v-model:value="values[item]" type="number" :step="form[item].step || 1"> <FormInput v-if="form[item].type === 'number'" v-model="values[item]" type="number" :step="form[item].step || 1">
<span v-text="form[item].label || item"></span><span v-if="form[item].required === false"> ({{ $ts.optional }})</span> <span v-text="form[item].label || item"></span><span v-if="form[item].required === false"> ({{ $ts.optional }})</span>
<template v-if="form[item].description" #desc>{{ form[item].description }}</template> <template v-if="form[item].description" #desc>{{ form[item].description }}</template>
</FormInput> </FormInput>
<FormInput v-else-if="form[item].type === 'string' && !form[item].multiline" v-model:value="values[item]" type="text"> <FormInput v-else-if="form[item].type === 'string' && !form[item].multiline" v-model="values[item]" type="text">
<span v-text="form[item].label || item"></span><span v-if="form[item].required === false"> ({{ $ts.optional }})</span> <span v-text="form[item].label || item"></span><span v-if="form[item].required === false"> ({{ $ts.optional }})</span>
<template v-if="form[item].description" #desc>{{ form[item].description }}</template> <template v-if="form[item].description" #desc>{{ form[item].description }}</template>
</FormInput> </FormInput>
<FormTextarea v-else-if="form[item].type === 'string' && form[item].multiline" v-model:value="values[item]"> <FormTextarea v-else-if="form[item].type === 'string' && form[item].multiline" v-model="values[item]">
<span v-text="form[item].label || item"></span><span v-if="form[item].required === false"> ({{ $ts.optional }})</span> <span v-text="form[item].label || item"></span><span v-if="form[item].required === false"> ({{ $ts.optional }})</span>
<template v-if="form[item].description" #desc>{{ form[item].description }}</template> <template v-if="form[item].description" #desc>{{ form[item].description }}</template>
</FormTextarea> </FormTextarea>
<FormSwitch v-else-if="form[item].type === 'boolean'" v-model:value="values[item]"> <FormSwitch v-else-if="form[item].type === 'boolean'" v-model="values[item]">
<span v-text="form[item].label || item"></span> <span v-text="form[item].label || item"></span>
<template v-if="form[item].description" #desc>{{ form[item].description }}</template> <template v-if="form[item].description" #desc>{{ form[item].description }}</template>
</FormSwitch> </FormSwitch>
<FormSelect v-else-if="form[item].type === 'enum'" v-model:value="values[item]"> <FormSelect v-else-if="form[item].type === 'enum'" v-model="values[item]">
<template #label><span v-text="form[item].label || item"></span><span v-if="form[item].required === false"> ({{ $ts.optional }})</span></template> <template #label><span v-text="form[item].label || item"></span><span v-if="form[item].required === false"> ({{ $ts.optional }})</span></template>
<option v-for="item in form[item].enum" :value="item.value" :key="item.value">{{ item.label }}</option> <option v-for="item in form[item].enum" :value="item.value" :key="item.value">{{ item.label }}</option>
</FormSelect> </FormSelect>
@ -38,7 +38,7 @@
<template #desc><span v-text="form[item].label || item"></span><span v-if="form[item].required === false"> ({{ $ts.optional }})</span></template> <template #desc><span v-text="form[item].label || item"></span><span v-if="form[item].required === false"> ({{ $ts.optional }})</span></template>
<option v-for="item in form[item].options" :value="item.value" :key="item.value">{{ item.label }}</option> <option v-for="item in form[item].options" :value="item.value" :key="item.value">{{ item.label }}</option>
</FormRadios> </FormRadios>
<FormRange v-else-if="form[item].type === 'range'" v-model:value="values[item]" :min="form[item].mim" :max="form[item].max" :step="form[item].step"> <FormRange v-else-if="form[item].type === 'range'" v-model="values[item]" :min="form[item].mim" :max="form[item].max" :step="form[item].step">
<template #label><span v-text="form[item].label || item"></span><span v-if="form[item].required === false"> ({{ $ts.optional }})</span></template> <template #label><span v-text="form[item].label || item"></span><span v-if="form[item].required === false"> ({{ $ts.optional }})</span></template>
<template v-if="form[item].description" #desc>{{ form[item].description }}</template> <template v-if="form[item].description" #desc>{{ form[item].description }}</template>
</FormRange> </FormRange>
@ -53,14 +53,14 @@
<script lang="ts"> <script lang="ts">
import { defineComponent } from 'vue'; import { defineComponent } from 'vue';
import XModalWindow from '@client/components/ui/modal-window.vue'; import XModalWindow from '@client/components/ui/modal-window.vue';
import FormBase from './form/base.vue'; import FormBase from './debobigego/base.vue';
import FormInput from './form/input.vue'; import FormInput from './debobigego/input.vue';
import FormTextarea from './form/textarea.vue'; import FormTextarea from './debobigego/textarea.vue';
import FormSwitch from './form/switch.vue'; import FormSwitch from './debobigego/switch.vue';
import FormSelect from './form/select.vue'; import FormSelect from './debobigego/select.vue';
import FormRange from './form/range.vue'; import FormRange from './debobigego/range.vue';
import FormButton from './form/button.vue'; import FormButton from './debobigego/button.vue';
import FormRadios from './form/radios.vue'; import FormRadios from './debobigego/radios.vue';
export default defineComponent({ export default defineComponent({
components: { components: {

View file

@ -1,53 +1,49 @@
<template> <template>
<FormGroup class="_formItem"> <div class="matxzzsk">
<template #label><slot></slot></template> <div class="label" @click="focus"><slot name="label"></slot></div>
<div class="ztzhwixg _formItem" :class="{ inline, disabled }"> <div class="input" :class="{ inline, disabled, focused }">
<div class="icon" ref="icon"><slot name="icon"></slot></div> <div class="prefix" ref="prefixEl"><slot name="prefix"></slot></div>
<div class="input _formPanel"> <input ref="inputEl"
<div class="prefix" ref="prefixEl"><slot name="prefix"></slot></div> :type="type"
<input ref="inputEl" v-model="v"
:type="type" :disabled="disabled"
v-model="v" :required="required"
:disabled="disabled" :readonly="readonly"
:required="required" :placeholder="placeholder"
:readonly="readonly" :pattern="pattern"
:placeholder="placeholder" :autocomplete="autocomplete"
:pattern="pattern" :spellcheck="spellcheck"
:autocomplete="autocomplete" :step="step"
:spellcheck="spellcheck" @focus="focused = true"
:step="step" @blur="focused = false"
@focus="focused = true" @keydown="onKeydown($event)"
@blur="focused = false" @input="onInput"
@keydown="onKeydown($event)" :list="id"
@input="onInput" >
:list="id" <datalist :id="id" v-if="datalist">
> <option v-for="data in datalist" :value="data"/>
<datalist :id="id" v-if="datalist"> </datalist>
<option v-for="data in datalist" :value="data"/> <div class="suffix" ref="suffixEl"><slot name="suffix"></slot></div>
</datalist>
<div class="suffix" ref="suffixEl"><slot name="suffix"></slot></div>
</div>
</div> </div>
<template #caption><slot name="desc"></slot></template> <div class="caption"><slot name="caption"></slot></div>
<FormButton v-if="manualSave && changed" @click="updated" primary><i class="fas fa-save"></i> {{ $ts.save }}</FormButton> <MkButton v-if="manualSave && changed" @click="updated" primary><i class="fas fa-save"></i> {{ $ts.save }}</MkButton>
</FormGroup> </div>
</template> </template>
<script lang="ts"> <script lang="ts">
import { defineComponent, onMounted, onUnmounted, nextTick, ref, watch, computed, toRefs } from 'vue'; import { defineComponent, onMounted, onUnmounted, nextTick, ref, watch, computed, toRefs } from 'vue';
import './form.scss'; import MkButton from '../ui/button.vue';
import FormButton from './button.vue'; import { debounce } from 'throttle-debounce';
import FormGroup from './group.vue';
export default defineComponent({ export default defineComponent({
components: { components: {
FormGroup, MkButton,
FormButton,
}, },
props: { props: {
value: { modelValue: {
required: false required: true
}, },
type: { type: {
type: String, type: String,
@ -96,16 +92,23 @@ export default defineComponent({
required: false, required: false,
default: false default: false
}, },
debounce: {
type: Boolean,
required: false,
default: false
},
manualSave: { manualSave: {
type: Boolean, type: Boolean,
required: false, required: false,
default: false default: false
}, },
}, },
emits: ['change', 'keydown', 'enter'],
emits: ['change', 'keydown', 'enter', 'update:modelValue'],
setup(props, context) { setup(props, context) {
const { value, type, autofocus } = toRefs(props); const { modelValue, type, autofocus } = toRefs(props);
const v = ref(value.value); const v = ref(modelValue.value);
const id = Math.random().toString(); // TODO: uuid? const id = Math.random().toString(); // TODO: uuid?
const focused = ref(false); const focused = ref(false);
const changed = ref(false); const changed = ref(false);
@ -131,19 +134,25 @@ export default defineComponent({
const updated = () => { const updated = () => {
changed.value = false; changed.value = false;
if (type?.value === 'number') { if (type?.value === 'number') {
context.emit('update:value', parseFloat(v.value)); context.emit('update:modelValue', parseFloat(v.value));
} else { } else {
context.emit('update:value', v.value); context.emit('update:modelValue', v.value);
} }
}; };
watch(value, newValue => { const debouncedUpdated = debounce(1000, updated);
watch(modelValue, newValue => {
v.value = newValue; v.value = newValue;
}); });
watch(v, newValue => { watch(v, newValue => {
if (!props.manualSave) { if (!props.manualSave) {
updated(); if (props.debounce) {
debouncedUpdated();
} else {
updated();
}
} }
invalid.value = inputEl.value.validity.badInput; invalid.value = inputEl.value.validity.badInput;
@ -196,59 +205,66 @@ export default defineComponent({
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
.ztzhwixg { .matxzzsk {
position: relative; > .label {
font-size: 0.85em;
padding: 0 0 8px 12px;
user-select: none;
> .icon { &:empty {
position: absolute; display: none;
top: 0; }
left: 0; }
width: 24px;
text-align: center;
line-height: 32px;
&:not(:empty) + .input { > .caption {
margin-left: 28px; font-size: 0.8em;
padding: 8px 0 0 12px;
color: var(--fgTransparentWeak);
&:empty {
display: none;
} }
} }
> .input { > .input {
$height: 48px; $height: 42px;
position: relative; position: relative;
> input { > input {
appearance: none;
-webkit-appearance: none;
display: block; display: block;
height: $height; height: $height;
width: 100%; width: 100%;
margin: 0; margin: 0;
padding: 0 16px; padding: 0 12px;
font: inherit; font: inherit;
font-weight: normal; font-weight: normal;
font-size: 1em; font-size: 1em;
line-height: $height; color: var(--fg);
color: var(--inputText); background: var(--panel);
background: transparent; border: solid 0.5px var(--inputBorder);
border: none; border-radius: 6px;
border-radius: 0;
outline: none; outline: none;
box-shadow: none; box-shadow: none;
box-sizing: border-box; box-sizing: border-box;
transition: border-color 0.1s ease-out;
&[type='file'] { &:hover {
display: none; border-color: var(--inputBorderHover);
} }
} }
> .prefix, > .prefix,
> .suffix { > .suffix {
display: block; display: flex;
align-items: center;
position: absolute; position: absolute;
z-index: 1; z-index: 1;
top: 0; top: 0;
padding: 0 16px; padding: 0 12px;
font-size: 1em; font-size: 1em;
line-height: $height; height: $height;
color: var(--inputLabel);
pointer-events: none; pointer-events: none;
&:empty { &:empty {
@ -267,25 +283,32 @@ export default defineComponent({
> .prefix { > .prefix {
left: 0; left: 0;
padding-right: 8px; padding-right: 6px;
} }
> .suffix { > .suffix {
right: 0; right: 0;
padding-left: 8px; padding-left: 6px;
} }
}
&.inline { &.inline {
display: inline-block; display: inline-block;
margin: 0; margin: 0;
} }
&.disabled { &.focused {
opacity: 0.7; > input {
border-color: var(--accent);
//box-shadow: 0 0 0 4px var(--focus);
}
}
&, * { &.disabled {
cursor: not-allowed !important; opacity: 0.7;
&, * {
cursor: not-allowed !important;
}
} }
} }
} }

View file

@ -1,7 +1,6 @@
<script lang="ts"> <script lang="ts">
import { defineComponent, h } from 'vue'; import { defineComponent, h } from 'vue';
import MkRadio from '@client/components/ui/radio.vue'; import MkRadio from './radio.vue';
import './form.scss';
export default defineComponent({ export default defineComponent({
components: { components: {
@ -18,9 +17,6 @@ export default defineComponent({
} }
}, },
watch: { watch: {
modelValue() {
this.value = this.modelValue;
},
value() { value() {
this.$emit('update:modelValue', this.value); this.$emit('update:modelValue', this.value);
} }
@ -33,80 +29,38 @@ export default defineComponent({
if (options.length === 1 && options[0].props == null) options = options[0].children; if (options.length === 1 && options[0].props == null) options = options[0].children;
return h('div', { return h('div', {
class: 'cnklmpwm _formItem' class: 'novjtcto'
}, [ }, [
h('div', { h('div', { class: 'label' }, label),
class: '_formLabel', ...options.map(option => h(MkRadio, {
}, label),
...options.map(option => h('button', {
class: '_button _formPanel _formClickable',
key: option.key, key: option.key,
onClick: () => this.value = option.props.value, value: option.props.value,
}, [h('span', { modelValue: this.value,
class: ['check', { checked: this.value === option.props.value }], 'onUpdate:modelValue': value => this.value = value,
}), option.children])) }, option.children))
]); ]);
} }
}); });
</script> </script>
<style lang="scss"> <style lang="scss">
.cnklmpwm { .novjtcto {
> button { > .label {
display: block; font-size: 0.85em;
width: 100%; padding: 0 0 8px 12px;
box-sizing: border-box; user-select: none;
padding: 14px 18px;
text-align: left;
&:not(:first-of-type) { &:empty {
border-top: none !important; display: none;
border-top-left-radius: 0;
border-top-right-radius: 0;
} }
}
&:not(:last-of-type) { &:first-child {
border-bottom: solid 0.5px var(--divider); margin-top: 0;
border-bottom-left-radius: 0; }
border-bottom-right-radius: 0;
}
> .check { &:last-child {
display: inline-block; margin-bottom: 0;
vertical-align: bottom;
position: relative;
width: 16px;
height: 16px;
margin-right: 8px;
background: none;
border: 2px solid var(--inputBorder);
border-radius: 100%;
transition: inherit;
&:after {
content: "";
display: block;
position: absolute;
top: 3px;
right: 3px;
bottom: 3px;
left: 3px;
border-radius: 100%;
opacity: 0;
transform: scale(0);
transition: .4s cubic-bezier(.25,.8,.25,1);
}
&.checked {
border-color: var(--accent);
&:after {
background-color: var(--accent);
transform: scale(1);
opacity: 1;
}
}
}
} }
} }
</style> </style>

View file

@ -1,21 +1,20 @@
<template> <template>
<div class="ifitouly _formItem" :class="{ focused, disabled }"> <div class="timctyfi" :class="{ focused, disabled }">
<div class="_formLabel"><slot name="label"></slot></div> <div class="icon"><slot name="icon"></slot></div>
<div class="_formPanel main"> <span class="label"><slot name="label"></slot></span>
<input <input
type="range" type="range"
ref="input" ref="input"
v-model="v" v-model="v"
:disabled="disabled" :disabled="disabled"
:min="min" :min="min"
:max="max" :max="max"
:step="step" :step="step"
@focus="focused = true" :autofocus="autofocus"
@blur="focused = false" @focus="focused = true"
@input="$emit('update:value', $event.target.value)" @blur="focused = false"
/> @input="$emit('update:value', $event.target.value)"
</div> />
<div class="_formCaption"><slot name="caption"></slot></div>
</div> </div>
</template> </template>
@ -49,6 +48,10 @@ export default defineComponent({
required: false, required: false,
default: 1 default: 1
}, },
autofocus: {
type: Boolean,
required: false
}
}, },
data() { data() {
return { return {
@ -61,61 +64,75 @@ export default defineComponent({
this.v = parseFloat(v); this.v = parseFloat(v);
} }
}, },
mounted() {
if (this.autofocus) {
this.$nextTick(() => {
this.$refs.input.focus();
});
}
}
}); });
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
.ifitouly { .timctyfi {
position: relative; position: relative;
margin: 8px;
> .main { > .icon {
padding: 22px 16px; display: inline-block;
width: 24px;
text-align: center;
}
> input { > .title {
display: block; pointer-events: none;
font-size: 16px;
color: var(--inputLabel);
overflow: hidden;
}
> input {
-webkit-appearance: none;
-moz-appearance: none;
appearance: none;
background: var(--X10);
height: 7px;
margin: 0 8px;
outline: 0;
border: 0;
border-radius: 7px;
&.disabled {
opacity: 0.6;
cursor: not-allowed;
}
&::-webkit-slider-thumb {
-webkit-appearance: none; -webkit-appearance: none;
appearance: none;
cursor: pointer;
width: 20px;
height: 20px;
display: block;
border-radius: 50%;
border: none;
background: var(--accent);
box-shadow: 0 0 6px rgba(0, 0, 0, 0.3);
box-sizing: content-box;
}
&::-moz-range-thumb {
-moz-appearance: none; -moz-appearance: none;
appearance: none; appearance: none;
background: var(--X10); cursor: pointer;
height: 4px; width: 20px;
width: 100%; height: 20px;
box-sizing: border-box; display: block;
margin: 0; border-radius: 50%;
outline: 0; border: none;
border: 0; background: var(--accent);
border-radius: 7px; box-shadow: 0 0 6px rgba(0, 0, 0, 0.3);
&.disabled {
opacity: 0.6;
cursor: not-allowed;
}
&::-webkit-slider-thumb {
-webkit-appearance: none;
appearance: none;
cursor: pointer;
width: 20px;
height: 20px;
display: block;
border-radius: 50%;
border: none;
background: var(--accent);
box-shadow: 0 0 6px rgba(0, 0, 0, 0.3);
box-sizing: content-box;
}
&::-moz-range-thumb {
-moz-appearance: none;
appearance: none;
cursor: pointer;
width: 20px;
height: 20px;
display: block;
border-radius: 50%;
border: none;
background: var(--accent);
box-shadow: 0 0 6px rgba(0, 0, 0, 0.3);
}
} }
} }
} }

View file

@ -0,0 +1,31 @@
<template>
<div class="vrtktovh" v-size="{ max: [500] }" v-sticky-container>
<div class="label"><slot name="label"></slot></div>
<div class="main">
<slot></slot>
</div>
</div>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
export default defineComponent({
});
</script>
<style lang="scss" scoped>
.vrtktovh {
border-top: solid 0.5px var(--divider);
> .label {
font-weight: bold;
padding: 24px 0 16px 0;
}
> .main {
margin-bottom: 32px;
}
}
</style>

View file

@ -1,125 +1,216 @@
<template> <template>
<div class="yrtfrpux _formItem" :class="{ disabled, inline }"> <div class="vblkjoeq">
<div class="_formLabel"><slot name="label"></slot></div> <div class="label" @click="focus"><slot name="label"></slot></div>
<div class="icon" ref="icon"><slot name="icon"></slot></div> <div class="input" :class="{ inline, disabled, focused }">
<div class="input _formPanel _formClickable" @click="focus"> <div class="prefix" ref="prefixEl"><slot name="prefix"></slot></div>
<div class="prefix" ref="prefix"><slot name="prefix"></slot></div> <select ref="inputEl"
<select ref="input"
v-model="v" v-model="v"
:required="required"
:disabled="disabled" :disabled="disabled"
:required="required"
:readonly="readonly"
:placeholder="placeholder"
@focus="focused = true" @focus="focused = true"
@blur="focused = false" @blur="focused = false"
@input="onInput"
> >
<slot></slot> <slot></slot>
</select> </select>
<div class="suffix"> <div class="suffix" ref="suffixEl"><i class="fas fa-chevron-down"></i></div>
<i class="fas fa-chevron-down"></i>
</div>
</div> </div>
<div class="_formCaption"><slot name="caption"></slot></div> <div class="caption"><slot name="caption"></slot></div>
<MkButton v-if="manualSave && changed" @click="updated" primary><i class="fas fa-save"></i> {{ $ts.save }}</MkButton>
</div> </div>
</template> </template>
<script lang="ts"> <script lang="ts">
import { defineComponent } from 'vue'; import { defineComponent, onMounted, onUnmounted, nextTick, ref, watch, computed, toRefs } from 'vue';
import './form.scss'; import MkButton from '../ui/button.vue';
export default defineComponent({ export default defineComponent({
components: {
MkButton,
},
props: { props: {
value: { modelValue: {
required: false required: true
}, },
required: { required: {
type: Boolean, type: Boolean,
required: false required: false
}, },
readonly: {
type: Boolean,
required: false
},
disabled: { disabled: {
type: Boolean, type: Boolean,
required: false required: false
}, },
placeholder: {
type: String,
required: false
},
autofocus: {
type: Boolean,
required: false,
default: false
},
inline: { inline: {
type: Boolean, type: Boolean,
required: false, required: false,
default: false default: false
}, },
}, manualSave: {
data() { type: Boolean,
return { required: false,
}; default: false
},
computed: {
v: {
get() {
return this.value;
},
set(v) {
this.$emit('update:value', v);
}
}, },
}, },
methods: {
focus() { emits: ['change', 'update:modelValue'],
this.$refs.input.focus();
} setup(props, context) {
} const { modelValue, autofocus } = toRefs(props);
const v = ref(modelValue.value);
const focused = ref(false);
const changed = ref(false);
const invalid = ref(false);
const filled = computed(() => v.value !== '' && v.value != null);
const inputEl = ref(null);
const prefixEl = ref(null);
const suffixEl = ref(null);
const focus = () => inputEl.value.focus();
const onInput = (ev) => {
changed.value = true;
context.emit('change', ev);
};
const updated = () => {
changed.value = false;
context.emit('update:modelValue', v.value);
};
watch(modelValue, newValue => {
v.value = newValue;
});
watch(v, newValue => {
if (!props.manualSave) {
updated();
}
invalid.value = inputEl.value.validity.badInput;
});
onMounted(() => {
nextTick(() => {
if (autofocus.value) {
focus();
}
//
// 0
const clock = setInterval(() => {
if (prefixEl.value) {
if (prefixEl.value.offsetWidth) {
inputEl.value.style.paddingLeft = prefixEl.value.offsetWidth + 'px';
}
}
if (suffixEl.value) {
if (suffixEl.value.offsetWidth) {
inputEl.value.style.paddingRight = suffixEl.value.offsetWidth + 'px';
}
}
}, 100);
onUnmounted(() => {
clearInterval(clock);
});
});
});
return {
v,
focused,
invalid,
changed,
filled,
inputEl,
prefixEl,
suffixEl,
focus,
onInput,
updated,
};
},
}); });
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
.yrtfrpux { .vblkjoeq {
position: relative; > .label {
font-size: 0.85em;
padding: 0 0 8px 12px;
user-select: none;
> .icon { &:empty {
position: absolute; display: none;
top: 0; }
left: 0; }
width: 24px;
text-align: center;
line-height: 32px;
&:not(:empty) + .input { > .caption {
margin-left: 28px; font-size: 0.8em;
padding: 8px 0 0 12px;
color: var(--fgTransparentWeak);
&:empty {
display: none;
} }
} }
> .input { > .input {
display: flex; $height: 42px;
position: relative; position: relative;
> select { > select {
appearance: none;
-webkit-appearance: none;
display: block; display: block;
flex: 1; height: $height;
width: 100%; width: 100%;
padding: 0 16px; margin: 0;
padding: 0 12px;
font: inherit; font: inherit;
font-weight: normal; font-weight: normal;
font-size: 1em; font-size: 1em;
height: 48px; color: var(--fg);
background: none; background: var(--panel);
border: none; border: solid 1px var(--inputBorder);
border-radius: 0; border-radius: 6px;
outline: none; outline: none;
box-shadow: none; box-shadow: none;
appearance: none; box-sizing: border-box;
-webkit-appearance: none; cursor: pointer;
color: var(--fg); transition: border-color 0.1s ease-out;
option, &:hover {
optgroup { border-color: var(--inputBorderHover);
color: var(--fg);
background: var(--bg);
} }
} }
> .prefix, > .prefix,
> .suffix { > .suffix {
display: block; display: flex;
align-self: center; align-items: center;
justify-self: center; position: absolute;
z-index: 1;
top: 0;
padding: 0 12px;
font-size: 1em; font-size: 1em;
line-height: 32px; height: $height;
color: var(--inputLabel);
pointer-events: none; pointer-events: none;
&:empty { &:empty {
@ -127,18 +218,42 @@ export default defineComponent({
} }
> * { > * {
display: block; display: inline-block;
min-width: 16px; min-width: 16px;
max-width: 150px;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
} }
} }
> .prefix { > .prefix {
padding-right: 4px; left: 0;
padding-right: 6px;
} }
> .suffix { > .suffix {
padding: 0 16px 0 0; right: 0;
padding-left: 6px;
}
&.inline {
display: inline-block;
margin: 0;
}
&.focused {
> select {
border-color: var(--accent);
}
}
&.disabled {
opacity: 0.7; opacity: 0.7;
&, * {
cursor: not-allowed !important;
}
} }
} }
} }

View file

@ -0,0 +1,50 @@
<template>
<div class="adhpbeou">
<div class="label" @click="focus"><slot name="label"></slot></div>
<div class="content">
<slot></slot>
</div>
<div class="caption"><slot name="caption"></slot></div>
</div>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
export default defineComponent({
});
</script>
<style lang="scss" scoped>
.adhpbeou {
margin: 1.5em 0;
> .label {
font-size: 0.85em;
padding: 0 0 8px 12px;
user-select: none;
&:empty {
display: none;
}
}
> .caption {
font-size: 0.8em;
padding: 8px 0 0 12px;
color: var(--fgTransparentWeak);
&:empty {
display: none;
}
}
> .content {
position: relative;
background: var(--panel);
border: solid 0.5px var(--inputBorder);
border-radius: 6px;
}
}
</style>

View file

@ -1,35 +1,34 @@
<template> <template>
<div class="ijnpvmgr _formItem"> <div
<div class="main _formPanel _formClickable" class="ziffeoms"
:class="{ disabled, checked }" :class="{ disabled, checked }"
:aria-checked="checked" role="switch"
:aria-disabled="disabled" :aria-checked="checked"
@click.prevent="toggle" :aria-disabled="disabled"
@click.prevent="toggle"
>
<input
type="checkbox"
ref="input"
:disabled="disabled"
@keydown.enter="toggle"
> >
<input <span class="button" v-tooltip="checked ? $ts.itsOn : $ts.itsOff">
type="checkbox" <span class="handle"></span>
ref="input" </span>
:disabled="disabled" <span class="label">
@keydown.enter="toggle" <span><slot></slot></span>
> <p><slot name="caption"></slot></p>
<span class="button"> </span>
<span></span>
</span>
<span class="label">
<span><slot></slot></span>
</span>
</div>
<div class="_formCaption"><slot name="desc"></slot></div>
</div> </div>
</template> </template>
<script lang="ts"> <script lang="ts">
import { defineComponent } from 'vue'; import { defineComponent } from 'vue';
import './form.scss';
export default defineComponent({ export default defineComponent({
props: { props: {
value: { modelValue: {
type: Boolean, type: Boolean,
default: false default: false
}, },
@ -40,91 +39,110 @@ export default defineComponent({
}, },
computed: { computed: {
checked(): boolean { checked(): boolean {
return this.value; return this.modelValue;
} }
}, },
methods: { methods: {
toggle() { toggle() {
if (this.disabled) return; if (this.disabled) return;
this.$emit('update:value', !this.checked); this.$emit('update:modelValue', !this.checked);
} }
} }
}); });
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
.ijnpvmgr { .ziffeoms {
> .main { position: relative;
display: flex;
cursor: pointer;
transition: all 0.3s;
&:first-child {
margin-top: 0;
}
&:last-child {
margin-bottom: 0;
}
> * {
user-select: none;
}
> input {
position: absolute;
width: 0;
height: 0;
opacity: 0;
margin: 0;
}
> .button {
position: relative; position: relative;
display: flex; display: inline-block;
padding: 14px 16px; flex-shrink: 0;
cursor: pointer; margin: 0;
width: 36px;
height: 26px;
background: var(--switchBg);
outline: none;
border-radius: 999px;
transition: inherit;
> * { > .handle {
user-select: none;
}
&.disabled {
opacity: 0.6;
cursor: not-allowed;
}
&.checked {
> .button {
background-color: var(--X10);
border-color: var(--X10);
> * {
background-color: var(--accent);
transform: translateX(14px);
}
}
}
> input {
position: absolute; position: absolute;
width: 0; top: 0;
height: 0; bottom: 0;
opacity: 0; left: 5px;
margin: 0; margin: auto 0;
border-radius: 100%;
transition: background-color 0.3s, transform 0.3s;
width: 16px;
height: 16px;
background-color: #fff;
} }
}
> .button { > .label {
position: relative; margin-left: 16px;
display: inline-block; margin-top: 2px;
flex-shrink: 0; display: block;
margin: 3px 0 0 0; cursor: pointer;
width: 34px; transition: inherit;
height: 14px; color: var(--fg);
background: var(--X6);
outline: none;
border-radius: 14px;
transition: all 0.3s;
cursor: pointer;
> * { > span {
position: absolute;
top: -3px;
left: 0;
border-radius: 100%;
transition: background-color 0.3s, transform 0.3s;
width: 20px;
height: 20px;
background-color: #fff;
box-shadow: 0 2px 1px -1px rgba(#000, 0.2), 0 1px 1px 0 rgba(#000, 0.14), 0 1px 3px 0 rgba(#000, 0.12);
}
}
> .label {
margin-left: 12px;
display: block; display: block;
line-height: 20px;
transition: inherit; transition: inherit;
color: var(--fg); }
> span { > p {
display: block; margin: 0;
line-height: 20px; color: var(--fgTransparentWeak);
transition: inherit; font-size: 90%;
}
}
&:hover {
> .button {
background-color: var(--accentedBg);
}
}
&.disabled {
opacity: 0.6;
cursor: not-allowed;
}
&.checked {
> .button {
background-color: var(--accent);
border-color: var(--accent);
> .handle {
transform: translateX(10px);
} }
} }
} }

View file

@ -1,40 +1,45 @@
<template> <template>
<FormGroup class="_formItem"> <div class="adhpbeos">
<template #label><slot></slot></template> <div class="label" @click="focus"><slot name="label"></slot></div>
<div class="rivhosbp _formItem" :class="{ tall, pre }"> <div class="input" :class="{ disabled, focused, tall, pre }">
<div class="input _formPanel"> <textarea ref="inputEl"
<textarea ref="input" :class="{ code, _monospace: code }" :class="{ code, _monospace: code }"
v-model="v" v-model="v"
:required="required" :disabled="disabled"
:readonly="readonly" :required="required"
:pattern="pattern" :readonly="readonly"
:autocomplete="autocomplete" :placeholder="placeholder"
:spellcheck="!code" :pattern="pattern"
@input="onInput" :autocomplete="autocomplete"
@focus="focused = true" :spellcheck="spellcheck"
@blur="focused = false" @focus="focused = true"
></textarea> @blur="focused = false"
</div> @keydown="onKeydown($event)"
@input="onInput"
></textarea>
</div> </div>
<template #caption><slot name="desc"></slot></template> <div class="caption"><slot name="caption"></slot></div>
<FormButton v-if="manualSave && changed" @click="updated" primary><i class="fas fa-save"></i> {{ $ts.save }}</FormButton> <MkButton v-if="manualSave && changed" @click="updated" primary><i class="fas fa-save"></i> {{ $ts.save }}</MkButton>
</FormGroup> </div>
</template> </template>
<script lang="ts"> <script lang="ts">
import { defineComponent, ref, toRefs, watch } from 'vue'; import { defineComponent, onMounted, onUnmounted, nextTick, ref, watch, computed, toRefs } from 'vue';
import './form.scss'; import MkButton from '../ui/button.vue';
import FormButton from './button.vue'; import { debounce } from 'throttle-debounce';
import FormGroup from './group.vue';
export default defineComponent({ export default defineComponent({
components: { components: {
FormGroup, MkButton,
FormButton,
}, },
props: { props: {
value: { modelValue: {
required: true
},
type: {
type: String,
required: false required: false
}, },
required: { required: {
@ -45,14 +50,29 @@ export default defineComponent({
type: Boolean, type: Boolean,
required: false required: false
}, },
disabled: {
type: Boolean,
required: false
},
pattern: { pattern: {
type: String, type: String,
required: false required: false
}, },
autocomplete: { placeholder: {
type: String, type: String,
required: false required: false
}, },
autofocus: {
type: Boolean,
required: false,
default: false
},
autocomplete: {
required: false
},
spellcheck: {
required: false
},
code: { code: {
type: Boolean, type: Boolean,
required: false required: false
@ -67,91 +87,162 @@ export default defineComponent({
required: false, required: false,
default: false default: false
}, },
debounce: {
type: Boolean,
required: false,
default: false
},
manualSave: { manualSave: {
type: Boolean, type: Boolean,
required: false, required: false,
default: false default: false
}, },
}, },
emits: ['change', 'keydown', 'enter', 'update:modelValue'],
setup(props, context) { setup(props, context) {
const { value } = toRefs(props); const { modelValue, autofocus } = toRefs(props);
const v = ref(value.value); const v = ref(modelValue.value);
const focused = ref(false);
const changed = ref(false); const changed = ref(false);
const invalid = ref(false);
const filled = computed(() => v.value !== '' && v.value != null);
const inputEl = ref(null); const inputEl = ref(null);
const focus = () => inputEl.value.focus(); const focus = () => inputEl.value.focus();
const onInput = (ev) => { const onInput = (ev) => {
changed.value = true; changed.value = true;
context.emit('change', ev); context.emit('change', ev);
}; };
const onKeydown = (ev: KeyboardEvent) => {
context.emit('keydown', ev);
if (ev.code === 'Enter') {
context.emit('enter');
}
};
const updated = () => { const updated = () => {
changed.value = false; changed.value = false;
context.emit('update:value', v.value); context.emit('update:modelValue', v.value);
}; };
watch(value, newValue => { const debouncedUpdated = debounce(1000, updated);
watch(modelValue, newValue => {
v.value = newValue; v.value = newValue;
}); });
watch(v, newValue => { watch(v, newValue => {
if (!props.manualSave) { if (!props.manualSave) {
updated(); if (props.debounce) {
debouncedUpdated();
} else {
updated();
}
} }
invalid.value = inputEl.value.validity.badInput;
}); });
onMounted(() => {
nextTick(() => {
if (autofocus.value) {
focus();
}
});
});
return { return {
v, v,
updated, focused,
invalid,
changed, changed,
filled,
inputEl,
focus, focus,
onInput, onInput,
onKeydown,
updated,
}; };
} },
}); });
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
.rivhosbp { .adhpbeos {
position: relative; > .label {
font-size: 0.85em;
padding: 0 0 8px 12px;
user-select: none;
&:empty {
display: none;
}
}
> .caption {
font-size: 0.8em;
padding: 8px 0 0 12px;
color: var(--fgTransparentWeak);
&:empty {
display: none;
}
}
> .input { > .input {
position: relative; position: relative;
> textarea { > textarea {
appearance: none;
-webkit-appearance: none;
display: block; display: block;
width: 100%; width: 100%;
min-width: 100%; min-width: 100%;
max-width: 100%; max-width: 100%;
min-height: 130px; min-height: 130px;
margin: 0; margin: 0;
padding: 16px; padding: 12px;
box-sizing: border-box;
font: inherit; font: inherit;
font-weight: normal; font-weight: normal;
font-size: 1em; font-size: 1em;
background: transparent; color: var(--fg);
border: none; background: var(--panel);
border-radius: 0; border: solid 0.5px var(--inputBorder);
border-radius: 6px;
outline: none; outline: none;
box-shadow: none; box-shadow: none;
color: var(--fg); box-sizing: border-box;
transition: border-color 0.1s ease-out;
&.code { &:hover {
tab-size: 2; border-color: var(--inputBorderHover);
} }
} }
}
&.tall { &.focused {
> .input { > textarea {
border-color: var(--accent);
}
}
&.disabled {
opacity: 0.7;
&, * {
cursor: not-allowed !important;
}
}
&.tall {
> textarea { > textarea {
min-height: 200px; min-height: 200px;
} }
} }
}
&.pre { &.pre {
> .input {
> textarea { > textarea {
white-space: pre; white-space: pre;
} }

View file

@ -0,0 +1,359 @@
<template>
<div class="fdidabkb" :class="{ slim: narrow, thin: thin_ }" :style="{ background: bg }" @click="onClick" ref="el">
<template v-if="info">
<div class="titleContainer" @click="showTabsPopup" v-if="!hideTitle">
<i v-if="info.icon" class="icon" :class="info.icon"></i>
<MkAvatar v-else-if="info.avatar" class="avatar" :user="info.avatar" :disable-preview="true" :show-indicator="true"/>
<div class="title">
<MkUserName v-if="info.userName" :user="info.userName" :nowrap="false" class="title"/>
<div v-else-if="info.title" class="title">{{ info.title }}</div>
<div class="subtitle" v-if="!narrow && info.subtitle">
{{ info.subtitle }}
</div>
<div class="subtitle activeTab" v-if="narrow && hasTabs">
{{ info.tabs.find(tab => tab.active)?.title }}
<i class="chevron fas fa-chevron-down"></i>
</div>
</div>
</div>
<div class="tabs" v-if="!narrow || hideTitle">
<button class="tab _button" v-for="tab in info.tabs" :class="{ active: tab.active }" @click="tab.onClick" v-tooltip="tab.title">
<i v-if="tab.icon" class="icon" :class="tab.icon"></i>
<span v-if="!tab.iconOnly" class="title">{{ tab.title }}</span>
</button>
</div>
</template>
<div class="buttons right">
<template v-if="info && info.actions && !narrow">
<template v-for="action in info.actions">
<MkButton class="fullButton" v-if="action.asFullButton" @click.stop="action.handler" primary><i :class="action.icon" style="margin-right: 6px;"></i>{{ action.text }}</MkButton>
<button v-else class="_button button" :class="{ highlighted: action.highlighted }" @click.stop="action.handler" @touchstart="preventDrag" v-tooltip="action.text"><i :class="action.icon"></i></button>
</template>
</template>
<button v-if="shouldShowMenu" class="_button button" @click.stop="showMenu" @touchstart="preventDrag" v-tooltip="$ts.menu"><i class="fas fa-ellipsis-h"></i></button>
</div>
</div>
</template>
<script lang="ts">
import { computed, defineComponent, onMounted, onUnmounted, PropType, ref, inject } from 'vue';
import * as tinycolor from 'tinycolor2';
import { popupMenu } from '@client/os';
import { url } from '@client/config';
import { scrollToTop } from '@client/scripts/scroll';
import MkButton from '@client/components/ui/button.vue';
import { i18n } from '@client/i18n';
import { globalEvents } from '@client/events';
export default defineComponent({
components: {
MkButton
},
props: {
info: {
type: Object as PropType<{
actions?: {}[];
tabs?: {}[];
}>,
required: true
},
menu: {
required: false
},
thin: {
required: false,
default: false
},
},
setup(props) {
const el = ref<HTMLElement>(null);
const bg = ref(null);
const narrow = ref(false);
const height = ref(0);
const hasTabs = computed(() => {
return props.info.tabs && props.info.tabs.length > 0;
});
const shouldShowMenu = computed(() => {
if (props.info == null) return false;
if (props.info.actions != null && narrow.value) return true;
if (props.info.menu != null) return true;
if (props.info.share != null) return true;
if (props.menu != null) return true;
return false;
});
const share = () => {
navigator.share({
url: url + props.info.path,
...props.info.share,
});
};
const showMenu = (ev: MouseEvent) => {
let menu = props.info.menu ? props.info.menu() : [];
if (narrow.value && props.info.actions) {
menu = [...props.info.actions.map(x => ({
text: x.text,
icon: x.icon,
action: x.handler
})), menu.length > 0 ? null : undefined, ...menu];
}
if (props.info.share) {
if (menu.length > 0) menu.push(null);
menu.push({
text: i18n.locale.share,
icon: 'fas fa-share-alt',
action: share
});
}
if (props.menu) {
if (menu.length > 0) menu.push(null);
menu = menu.concat(props.menu);
}
popupMenu(menu, ev.currentTarget || ev.target);
};
const showTabsPopup = (ev: MouseEvent) => {
if (!hasTabs.value) return;
if (!narrow.value) return;
ev.preventDefault();
ev.stopPropagation();
const menu = props.info.tabs.map(tab => ({
text: tab.title,
icon: tab.icon,
action: tab.onClick,
}));
popupMenu(menu, ev.currentTarget || ev.target);
};
const preventDrag = (ev: TouchEvent) => {
ev.stopPropagation();
};
const onClick = () => {
scrollToTop(el.value, { behavior: 'smooth' });
};
const calcBg = () => {
const rawBg = props.info?.bg || 'var(--bg)';
const tinyBg = tinycolor(rawBg.startsWith('var(') ? getComputedStyle(document.documentElement).getPropertyValue(rawBg.slice(4, -1)) : rawBg);
tinyBg.setAlpha(0.85);
bg.value = tinyBg.toRgbString();
};
onMounted(() => {
calcBg();
globalEvents.on('themeChanged', calcBg);
onUnmounted(() => {
globalEvents.off('themeChanged', calcBg);
});
if (el.value.parentElement) {
narrow.value = el.value.parentElement.offsetWidth < 500;
const ro = new ResizeObserver((entries, observer) => {
if (el.value) {
narrow.value = el.value.parentElement.offsetWidth < 500;
}
});
ro.observe(el.value.parentElement);
onUnmounted(() => {
ro.disconnect();
});
setTimeout(() => {
const currentStickyTop = getComputedStyle(el.value.parentElement).getPropertyValue('--stickyTop') || '0px';
el.value.style.setProperty('--stickyTop', currentStickyTop);
el.value.parentElement.style.setProperty('--stickyTop', `calc(${currentStickyTop} + ${el.value.offsetHeight}px)`);
}, 100); // stickyTop
}
});
return {
el,
bg,
narrow,
height,
hasTabs,
shouldShowMenu,
share,
showMenu,
showTabsPopup,
preventDrag,
onClick,
hideTitle: inject('shouldOmitHeaderTitle', false),
thin_: props.thin || inject('shouldHeaderThin', false)
};
},
});
</script>
<style lang="scss" scoped>
.fdidabkb {
--height: 60px;
display: flex;
position: sticky;
top: var(--stickyTop, 0);
z-index: 1000;
width: 100%;
-webkit-backdrop-filter: var(--blur, blur(15px));
backdrop-filter: var(--blur, blur(15px));
border-bottom: solid 0.5px var(--divider);
&.thin {
--height: 50px;
}
&.slim {
text-align: center;
> .titleContainer {
flex: 1;
margin: 0 auto;
margin-left: var(--height);
> *:first-child {
margin-left: auto;
}
> *:last-child {
margin-right: auto;
}
}
}
> .buttons {
--margin: 8px;
display: flex;
align-items: center;
height: var(--height);
margin: 0 var(--margin);
&.right {
margin-left: auto;
}
&:empty {
width: var(--height);
}
> .button {
display: flex;
align-items: center;
justify-content: center;
height: calc(var(--height) - (var(--margin) * 2));
width: calc(var(--height) - (var(--margin) * 2));
box-sizing: border-box;
position: relative;
border-radius: 5px;
&:hover {
background: rgba(0, 0, 0, 0.05);
}
&.highlighted {
color: var(--accent);
}
}
> .fullButton {
& + .fullButton {
margin-left: 12px;
}
}
}
> .titleContainer {
display: flex;
align-items: center;
overflow: auto;
white-space: nowrap;
text-align: left;
font-weight: bold;
flex-shrink: 0;
margin-left: 24px;
> .avatar {
$size: 32px;
display: inline-block;
width: $size;
height: $size;
vertical-align: bottom;
margin: 0 8px;
pointer-events: none;
}
> .icon {
margin-right: 8px;
}
> .title {
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
line-height: 1.1;
> .subtitle {
opacity: 0.6;
font-size: 0.8em;
font-weight: normal;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
&.activeTab {
text-align: center;
> .chevron {
display: inline-block;
margin-left: 6px;
}
}
}
}
}
> .tabs {
margin-left: 16px;
font-size: 0.8em;
overflow: auto;
white-space: nowrap;
> .tab {
display: inline-block;
position: relative;
padding: 0 10px;
height: 100%;
font-weight: normal;
opacity: 0.7;
&:hover {
opacity: 1;
}
&.active {
opacity: 1;
&:after {
content: "";
display: block;
position: absolute;
bottom: 0;
left: 0;
right: 0;
margin: 0 auto;
width: 100%;
height: 3px;
background: var(--accent);
}
}
> .icon + .title {
margin-left: 8px;
}
}
}
}
</style>

View file

@ -0,0 +1,76 @@
<template>
<div ref="root" :class="$style.root" :style="{ padding: margin + 'px' }">
<div ref="content" :class="$style.content">
<slot></slot>
</div>
</div>
</template>
<script lang="ts">
import { defineComponent, onMounted, onUnmounted, ref } from 'vue';
export default defineComponent({
props: {
contentMax: {
type: Number,
required: false,
default: null,
}
},
setup(props, context) {
let ro: ResizeObserver;
const root = ref<HTMLElement>(null);
const content = ref<HTMLElement>(null);
const margin = ref(0);
const adjust = (rect: { width: number; height: number; }) => {
if (rect.width > (props.contentMax || 500)) {
margin.value = 32;
} else {
margin.value = 12;
}
};
onMounted(() => {
ro = new ResizeObserver((entries) => {
/* iOS
adjust({
width: entries[0].borderBoxSize[0].inlineSize,
height: entries[0].borderBoxSize[0].blockSize,
});
*/
adjust({
width: root.value.offsetWidth,
height: root.value.offsetHeight,
});
});
ro.observe(root.value);
if (props.contentMax) {
content.value.style.maxWidth = `${props.contentMax}px`;
}
});
onUnmounted(() => {
ro.disconnect();
});
return {
root,
content,
margin,
};
},
});
</script>
<style lang="scss" module>
.root {
box-sizing: border-box;
width: 100%;
}
.content {
margin: 0 auto;
}
</style>

View file

@ -13,6 +13,8 @@ import i18n from './global/i18n';
import loading from './global/loading.vue'; import loading from './global/loading.vue';
import error from './global/error.vue'; import error from './global/error.vue';
import ad from './global/ad.vue'; import ad from './global/ad.vue';
import header from './global/header.vue';
import spacer from './global/spacer.vue';
export default function(app: App) { export default function(app: App) {
app.component('I18n', i18n); app.component('I18n', i18n);
@ -28,4 +30,6 @@ export default function(app: App) {
app.component('MkLoading', loading); app.component('MkLoading', loading);
app.component('MkError', error); app.component('MkError', error);
app.component('MkAd', ad); app.component('MkAd', ad);
app.component('MkHeader', header);
app.component('MkSpacer', spacer);
} }

View file

@ -36,7 +36,7 @@
<script lang="ts"> <script lang="ts">
import { defineComponent, markRaw } from 'vue'; import { defineComponent, markRaw } from 'vue';
import Chart from 'chart.js'; import Chart from 'chart.js';
import MkSelect from './ui/select.vue'; import MkSelect from './form/select.vue';
import number from '@client/filters/number'; import number from '@client/filters/number';
const sum = (...arr) => arr.reduce((r, a) => r.map((b, i) => a[i] + b)); const sum = (...arr) => arr.reduce((r, a) => r.map((b, i) => a[i] + b));

View file

@ -3,10 +3,13 @@
<div class="container"> <div class="container">
<div class="fullwidth top-caption"> <div class="fullwidth top-caption">
<div class="mk-dialog"> <div class="mk-dialog">
<header v-if="title"><Mfm :text="title"/></header> <header>
<Mfm v-if="title" class="title" :text="title"/>
<span class="text-count" :class="{ over: remainingLength < 0 }">{{ remainingLength }}</span>
</header>
<textarea autofocus v-model="inputValue" :placeholder="input.placeholder" @keydown="onInputKeydown"></textarea> <textarea autofocus v-model="inputValue" :placeholder="input.placeholder" @keydown="onInputKeydown"></textarea>
<div class="buttons" v-if="(showOkButton || showCancelButton)"> <div class="buttons" v-if="(showOkButton || showCancelButton)">
<MkButton inline @click="ok" primary>{{ $ts.ok }}</MkButton> <MkButton inline @click="ok" primary :disabled="remainingLength < 0">{{ $ts.ok }}</MkButton>
<MkButton inline @click="cancel" >{{ $ts.cancel }}</MkButton> <MkButton inline @click="cancel" >{{ $ts.cancel }}</MkButton>
</div> </div>
</div> </div>
@ -26,10 +29,12 @@
<script lang="ts"> <script lang="ts">
import { defineComponent } from 'vue'; import { defineComponent } from 'vue';
import { length } from 'stringz';
import MkModal from '@client/components/ui/modal.vue'; import MkModal from '@client/components/ui/modal.vue';
import MkButton from '@client/components/ui/button.vue'; import MkButton from '@client/components/ui/button.vue';
import bytes from '@client/filters/bytes'; import bytes from '@client/filters/bytes';
import number from '@client/filters/number'; import number from '@client/filters/number';
import { DB_MAX_IMAGE_COMMENT_LENGTH } from '@/misc/hard-limits';
export default defineComponent({ export default defineComponent({
components: { components: {
@ -79,6 +84,13 @@ export default defineComponent({
document.removeEventListener('keydown', this.onKeydown); document.removeEventListener('keydown', this.onKeydown);
}, },
computed: {
remainingLength(): number {
if (typeof this.inputValue != "string") return DB_MAX_IMAGE_COMMENT_LENGTH;
return DB_MAX_IMAGE_COMMENT_LENGTH - length(this.inputValue);
}
},
methods: { methods: {
bytes, bytes,
number, number,
@ -156,8 +168,18 @@ export default defineComponent({
> header { > header {
margin: 0 0 8px 0; margin: 0 0 8px 0;
font-weight: bold; position: relative;
font-size: 20px;
> .title {
font-weight: bold;
font-size: 20px;
}
> .text-count {
opacity: 0.7;
position: absolute;
right: 0;
}
} }
> .buttons { > .buttons {
@ -184,7 +206,7 @@ export default defineComponent({
min-width: 100%; min-width: 100%;
min-height: 90px; min-height: 90px;
&:focus { &:focus-visible {
outline: none; outline: none;
} }

View file

@ -185,7 +185,7 @@ export default defineComponent({
} }
} }
if (style == null) { if (style == null) {
return h('span', {}, ['[', token.props.name, ...genEl(token.children), ']']); return h('span', {}, ['[', token.props.name, ' ', ...genEl(token.children), ']']);
} else { } else {
return h('span', { return h('span', {
style: 'display: inline-block;' + style, style: 'display: inline-block;' + style,

View file

@ -2,11 +2,15 @@
<MkModal ref="modal" @click="$emit('click')" @closed="$emit('closed')"> <MkModal ref="modal" @click="$emit('click')" @closed="$emit('closed')">
<div class="hrmcaedk _window _narrow_" :style="{ width: `${width}px`, height: (height ? `min(${height}px, 100%)` : '100%') }"> <div class="hrmcaedk _window _narrow_" :style="{ width: `${width}px`, height: (height ? `min(${height}px, 100%)` : '100%') }">
<div class="header" @contextmenu="onContextmenu"> <div class="header" @contextmenu="onContextmenu">
<span class="title"> <button v-if="history.length > 0" class="_button" @click="back()" v-tooltip="$ts.goBack"><i class="fas fa-arrow-left"></i></button>
<XHeader :info="pageInfo" :back-button="history.length > 0" @back="back()" :close-button="true" @close="$refs.modal.close()"/> <span v-else style="display: inline-block; width: 20px"></span>
<span v-if="pageInfo" class="title">
<i v-if="pageInfo.icon" class="icon" :class="pageInfo.icon"></i>
<span>{{ pageInfo.title }}</span>
</span> </span>
<button class="_button" @click="$refs.modal.close()"><i class="fas fa-times"></i></button>
</div> </div>
<div class="body _flat_"> <div class="body _fitSide_">
<keep-alive> <keep-alive>
<component :is="component" v-bind="props" :ref="changePage"/> <component :is="component" v-bind="props" :ref="changePage"/>
</keep-alive> </keep-alive>
@ -18,7 +22,6 @@
<script lang="ts"> <script lang="ts">
import { defineComponent } from 'vue'; import { defineComponent } from 'vue';
import MkModal from '@client/components/ui/modal.vue'; import MkModal from '@client/components/ui/modal.vue';
import XHeader from '@client/ui/_common_/header.vue';
import { popout } from '@client/scripts/popout'; import { popout } from '@client/scripts/popout';
import copyToClipboard from '@client/scripts/copy-to-clipboard'; import copyToClipboard from '@client/scripts/copy-to-clipboard';
import { resolve } from '@client/router'; import { resolve } from '@client/router';
@ -29,7 +32,6 @@ import * as os from '@client/os';
export default defineComponent({ export default defineComponent({
components: { components: {
MkModal, MkModal,
XHeader,
}, },
inject: { inject: {
@ -42,7 +44,8 @@ export default defineComponent({
return { return {
navHook: (path) => { navHook: (path) => {
this.navigate(path); this.navigate(path);
} },
shouldHeaderThin: true,
}; };
}, },
@ -172,19 +175,39 @@ export default defineComponent({
$height-narrow: 42px; $height-narrow: 42px;
display: flex; display: flex;
flex-shrink: 0; flex-shrink: 0;
height: $height;
line-height: $height;
font-weight: bold;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
box-shadow: 0px 1px var(--divider); box-shadow: 0px 1px var(--divider);
> button {
height: $height;
width: $height;
&:hover {
color: var(--fgHighlighted);
}
}
@media (max-width: 500px) {
height: $height-narrow;
line-height: $height-narrow;
padding-left: 16px;
> button {
height: $height-narrow;
width: $height-narrow;
}
}
> .title { > .title {
flex: 1; flex: 1;
height: $height;
font-weight: bold;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
@media (max-width: 500px) { > .icon {
height: $height-narrow; margin-right: 0.5em;
padding-left: 16px;
} }
} }
} }

View file

@ -59,7 +59,7 @@
<div class="body"> <div class="body">
<p v-if="appearNote.cw != null" class="cw"> <p v-if="appearNote.cw != null" class="cw">
<Mfm v-if="appearNote.cw != ''" class="text" :text="appearNote.cw" :author="appearNote.user" :i="$i" :custom-emojis="appearNote.emojis"/> <Mfm v-if="appearNote.cw != ''" class="text" :text="appearNote.cw" :author="appearNote.user" :i="$i" :custom-emojis="appearNote.emojis"/>
<XCwButton v-model:value="showContent" :note="appearNote"/> <XCwButton v-model="showContent" :note="appearNote"/>
</p> </p>
<div class="content" v-show="appearNote.cw == null || showContent"> <div class="content" v-show="appearNote.cw == null || showContent">
<div class="text"> <div class="text">
@ -80,7 +80,7 @@
</div> </div>
<XPoll v-if="appearNote.poll" :note="appearNote" ref="pollViewer" class="poll"/> <XPoll v-if="appearNote.poll" :note="appearNote" ref="pollViewer" class="poll"/>
<MkUrlPreview v-for="url in urls" :url="url" :key="url" :compact="true" :detail="true" class="url-preview"/> <MkUrlPreview v-for="url in urls" :url="url" :key="url" :compact="true" :detail="true" class="url-preview"/>
<div class="renote" v-if="appearNote.renote"><XNotePreview :note="appearNote.renote"/></div> <div class="renote" v-if="appearNote.renote"><XNoteSimple :note="appearNote.renote"/></div>
</div> </div>
<MkA v-if="appearNote.channel && !inChannel" class="channel" :to="`/channels/${appearNote.channel.id}`"><i class="fas fa-satellite-dish"></i> {{ appearNote.channel.name }}</MkA> <MkA v-if="appearNote.channel && !inChannel" class="channel" :to="`/channels/${appearNote.channel.id}`"><i class="fas fa-satellite-dish"></i> {{ appearNote.channel.name }}</MkA>
</div> </div>
@ -132,7 +132,7 @@ import * as mfm from 'mfm-js';
import { sum } from '../../prelude/array'; import { sum } from '../../prelude/array';
import XSub from './note.sub.vue'; import XSub from './note.sub.vue';
import XNoteHeader from './note-header.vue'; import XNoteHeader from './note-header.vue';
import XNotePreview from './note-preview.vue'; import XNoteSimple from './note-simple.vue';
import XReactionsViewer from './reactions-viewer.vue'; import XReactionsViewer from './reactions-viewer.vue';
import XMediaList from './media-list.vue'; import XMediaList from './media-list.vue';
import XCwButton from './cw-button.vue'; import XCwButton from './cw-button.vue';
@ -153,7 +153,7 @@ export default defineComponent({
components: { components: {
XSub, XSub,
XNoteHeader, XNoteHeader,
XNotePreview, XNoteSimple,
XReactionsViewer, XReactionsViewer,
XMediaList, XMediaList,
XCwButton, XCwButton,

View file

@ -1,15 +1,13 @@
<template> <template>
<div class="yohlumlk" v-size="{ min: [350, 500] }"> <div class="fefdfafb" v-size="{ min: [350, 500] }">
<MkAvatar class="avatar" :user="note.user"/> <MkAvatar class="avatar" :user="$i"/>
<div class="main"> <div class="main">
<XNoteHeader class="header" :note="note" :mini="true"/> <div class="header">
<MkUserName :user="$i"/>
</div>
<div class="body"> <div class="body">
<p v-if="note.cw != null" class="cw"> <div class="content">
<span class="text" v-if="note.cw != ''">{{ note.cw }}</span> <Mfm :text="text" :author="$i" :i="$i"/>
<XCwButton v-model:value="showContent" :note="note"/>
</p>
<div class="content" v-show="note.cw == null || showContent">
<XSubNote-content class="text" :note="note"/>
</div> </div>
</div> </div>
</div> </div>
@ -18,35 +16,22 @@
<script lang="ts"> <script lang="ts">
import { defineComponent } from 'vue'; import { defineComponent } from 'vue';
import XNoteHeader from './note-header.vue';
import XSubNoteContent from './sub-note-content.vue';
import XCwButton from './cw-button.vue';
import * as os from '@client/os';
export default defineComponent({ export default defineComponent({
components: { components: {
XNoteHeader,
XSubNoteContent,
XCwButton,
}, },
props: { props: {
note: { text: {
type: Object, type: String,
required: true required: true
} }
}, },
data() {
return {
showContent: false
};
}
}); });
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
.yohlumlk { .fefdfafb {
display: flex; display: flex;
margin: 0; margin: 0;
padding: 0; padding: 0;

View file

@ -0,0 +1,113 @@
<template>
<div class="yohlumlk" v-size="{ min: [350, 500] }">
<MkAvatar class="avatar" :user="note.user"/>
<div class="main">
<XNoteHeader class="header" :note="note" :mini="true"/>
<div class="body">
<p v-if="note.cw != null" class="cw">
<span class="text" v-if="note.cw != ''">{{ note.cw }}</span>
<XCwButton v-model="showContent" :note="note"/>
</p>
<div class="content" v-show="note.cw == null || showContent">
<XSubNote-content class="text" :note="note"/>
</div>
</div>
</div>
</div>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
import XNoteHeader from './note-header.vue';
import XSubNoteContent from './sub-note-content.vue';
import XCwButton from './cw-button.vue';
import * as os from '@client/os';
export default defineComponent({
components: {
XNoteHeader,
XSubNoteContent,
XCwButton,
},
props: {
note: {
type: Object,
required: true
}
},
data() {
return {
showContent: false
};
}
});
</script>
<style lang="scss" scoped>
.yohlumlk {
display: flex;
margin: 0;
padding: 0;
overflow: clip;
font-size: 0.95em;
&.min-width_350px {
> .avatar {
margin: 0 10px 0 0;
width: 44px;
height: 44px;
}
}
&.min-width_500px {
> .avatar {
margin: 0 12px 0 0;
width: 48px;
height: 48px;
}
}
> .avatar {
flex-shrink: 0;
display: block;
margin: 0 10px 0 0;
width: 40px;
height: 40px;
border-radius: 8px;
}
> .main {
flex: 1;
min-width: 0;
> .header {
margin-bottom: 2px;
}
> .body {
> .cw {
cursor: default;
display: block;
margin: 0;
padding: 0;
overflow-wrap: break-word;
> .text {
margin-right: 8px;
}
}
> .content {
> .text {
cursor: default;
margin: 0;
padding: 0;
}
}
}
}
}
</style>

View file

@ -7,7 +7,7 @@
<div class="body"> <div class="body">
<p v-if="note.cw != null" class="cw"> <p v-if="note.cw != null" class="cw">
<Mfm v-if="note.cw != ''" class="text" :text="note.cw" :author="note.user" :i="$i" :custom-emojis="note.emojis" /> <Mfm v-if="note.cw != ''" class="text" :text="note.cw" :author="note.user" :i="$i" :custom-emojis="note.emojis" />
<XCwButton v-model:value="showContent" :note="note"/> <XCwButton v-model="showContent" :note="note"/>
</p> </p>
<div class="content" v-show="note.cw == null || showContent"> <div class="content" v-show="note.cw == null || showContent">
<XSubNote-content class="text" :note="note"/> <XSubNote-content class="text" :note="note"/>

View file

@ -43,7 +43,7 @@
<div class="body"> <div class="body">
<p v-if="appearNote.cw != null" class="cw"> <p v-if="appearNote.cw != null" class="cw">
<Mfm v-if="appearNote.cw != ''" class="text" :text="appearNote.cw" :author="appearNote.user" :i="$i" :custom-emojis="appearNote.emojis"/> <Mfm v-if="appearNote.cw != ''" class="text" :text="appearNote.cw" :author="appearNote.user" :i="$i" :custom-emojis="appearNote.emojis"/>
<XCwButton v-model:value="showContent" :note="appearNote"/> <XCwButton v-model="showContent" :note="appearNote"/>
</p> </p>
<div class="content" :class="{ collapsed }" v-show="appearNote.cw == null || showContent"> <div class="content" :class="{ collapsed }" v-show="appearNote.cw == null || showContent">
<div class="text"> <div class="text">
@ -64,7 +64,7 @@
</div> </div>
<XPoll v-if="appearNote.poll" :note="appearNote" ref="pollViewer" class="poll"/> <XPoll v-if="appearNote.poll" :note="appearNote" ref="pollViewer" class="poll"/>
<MkUrlPreview v-for="url in urls" :url="url" :key="url" :compact="true" :detail="false" class="url-preview"/> <MkUrlPreview v-for="url in urls" :url="url" :key="url" :compact="true" :detail="false" class="url-preview"/>
<div class="renote" v-if="appearNote.renote"><XNotePreview :note="appearNote.renote"/></div> <div class="renote" v-if="appearNote.renote"><XNoteSimple :note="appearNote.renote"/></div>
<button v-if="collapsed" class="fade _button" @click="collapsed = false"> <button v-if="collapsed" class="fade _button" @click="collapsed = false">
<span>{{ $ts.showMore }}</span> <span>{{ $ts.showMore }}</span>
</button> </button>
@ -114,7 +114,7 @@ import * as mfm from 'mfm-js';
import { sum } from '../../prelude/array'; import { sum } from '../../prelude/array';
import XSub from './note.sub.vue'; import XSub from './note.sub.vue';
import XNoteHeader from './note-header.vue'; import XNoteHeader from './note-header.vue';
import XNotePreview from './note-preview.vue'; import XNoteSimple from './note-simple.vue';
import XReactionsViewer from './reactions-viewer.vue'; import XReactionsViewer from './reactions-viewer.vue';
import XMediaList from './media-list.vue'; import XMediaList from './media-list.vue';
import XCwButton from './cw-button.vue'; import XCwButton from './cw-button.vue';
@ -134,7 +134,7 @@ export default defineComponent({
components: { components: {
XSub, XSub,
XNoteHeader, XNoteHeader,
XNotePreview, XNoteSimple,
XReactionsViewer, XReactionsViewer,
XMediaList, XMediaList,
XCwButton, XCwButton,
@ -888,7 +888,7 @@ export default defineComponent({
//content-visibility: auto; //content-visibility: auto;
//contain-intrinsic-size: 0 128px; //contain-intrinsic-size: 0 128px;
&:focus { &:focus-visible {
outline: none; outline: none;
&:after { &:after {

View file

@ -29,10 +29,10 @@
<script lang="ts"> <script lang="ts">
import { defineComponent, PropType } from 'vue'; import { defineComponent, PropType } from 'vue';
import XModalWindow from '@client/components/ui/modal-window.vue'; import XModalWindow from '@client/components/ui/modal-window.vue';
import MkSwitch from './ui/switch.vue'; import MkSwitch from './form/switch.vue';
import MkInfo from './ui/info.vue'; import MkInfo from './ui/info.vue';
import MkButton from './ui/button.vue'; import MkButton from './ui/button.vue';
import { notificationTypes } from '../../types'; import { notificationTypes } from '@/types';
export default defineComponent({ export default defineComponent({
components: { components: {

View file

@ -26,7 +26,7 @@ import paging from '@client/scripts/paging';
import XNotification from './notification.vue'; import XNotification from './notification.vue';
import XList from './date-separated-list.vue'; import XList from './date-separated-list.vue';
import XNote from './note.vue'; import XNote from './note.vue';
import { notificationTypes } from '../../types'; import { notificationTypes } from '@/types';
import * as os from '@client/os'; import * as os from '@client/os';
import MkButton from '@client/components/ui/button.vue'; import MkButton from '@client/components/ui/button.vue';
@ -48,6 +48,11 @@ export default defineComponent({
required: false, required: false,
default: null, default: null,
}, },
unreadOnly: {
type: Boolean,
required: false,
default: false,
},
}, },
data() { data() {
@ -58,6 +63,7 @@ export default defineComponent({
limit: 10, limit: 10,
params: () => ({ params: () => ({
includeTypes: this.allIncludeTypes || undefined, includeTypes: this.allIncludeTypes || undefined,
unreadOnly: this.unreadOnly,
}) })
}, },
}; };
@ -76,6 +82,11 @@ export default defineComponent({
}, },
deep: true deep: true
}, },
unreadOnly: {
handler() {
this.reload();
},
},
// TODO: vue/vuex $i // TODO: vue/vuex $i
// mutingNotificationTypes // mutingNotificationTypes
'$i.mutingNotificationTypes': { '$i.mutingNotificationTypes': {

View file

@ -3,14 +3,20 @@
:initial-width="500" :initial-width="500"
:initial-height="500" :initial-height="500"
:can-resize="true" :can-resize="true"
:close-button="false" :close-button="true"
:contextmenu="contextmenu" :contextmenu="contextmenu"
@closed="$emit('closed')" @closed="$emit('closed')"
> >
<template #header> <template #header>
<XHeader :info="pageInfo" :back-button="history.length > 0" @back="back()" :close-button="true" @close="close()" :title-only="true"/> <template v-if="pageInfo">
<i v-if="pageInfo.icon" class="icon" :class="pageInfo.icon" style="margin-right: 0.5em;"></i>
<span>{{ pageInfo.title }}</span>
</template>
</template> </template>
<div class="yrolvcoq _flat_"> <template #headerLeft>
<button v-if="history.length > 0" class="_button" @click="back()" v-tooltip="$ts.goBack"><i class="fas fa-arrow-left"></i></button>
</template>
<div class="yrolvcoq _fitSide_">
<component :is="component" v-bind="props" :ref="changePage"/> <component :is="component" v-bind="props" :ref="changePage"/>
</div> </div>
</XWindow> </XWindow>
@ -19,7 +25,6 @@
<script lang="ts"> <script lang="ts">
import { defineComponent } from 'vue'; import { defineComponent } from 'vue';
import XWindow from '@client/components/ui/window.vue'; import XWindow from '@client/components/ui/window.vue';
import XHeader from '@client/ui/_common_/header.vue';
import { popout } from '@client/scripts/popout'; import { popout } from '@client/scripts/popout';
import copyToClipboard from '@client/scripts/copy-to-clipboard'; import copyToClipboard from '@client/scripts/copy-to-clipboard';
import { resolve } from '@client/router'; import { resolve } from '@client/router';
@ -29,7 +34,6 @@ import * as symbols from '@client/symbols';
export default defineComponent({ export default defineComponent({
components: { components: {
XWindow, XWindow,
XHeader,
}, },
inject: { inject: {
@ -42,7 +46,8 @@ export default defineComponent({
return { return {
navHook: (path) => { navHook: (path) => {
this.navigate(path); this.navigate(path);
} },
shouldHeaderThin: true,
}; };
}, },

View file

@ -8,7 +8,7 @@
<script lang="ts"> <script lang="ts">
import { computed, defineComponent, PropType } from 'vue'; import { computed, defineComponent, PropType } from 'vue';
import MkInput from '../ui/input.vue'; import MkInput from '../form/input.vue';
import * as os from '@client/os'; import * as os from '@client/os';
import { Hpml } from '@client/scripts/hpml/evaluator'; import { Hpml } from '@client/scripts/hpml/evaluator';
import { NumberInputVarBlock } from '@client/scripts/hpml/block'; import { NumberInputVarBlock } from '@client/scripts/hpml/block';

View file

@ -10,7 +10,7 @@
<script lang="ts"> <script lang="ts">
import { defineComponent, PropType } from 'vue'; import { defineComponent, PropType } from 'vue';
import MkTextarea from '../ui/textarea.vue'; import MkTextarea from '../form/textarea.vue';
import MkButton from '../ui/button.vue'; import MkButton from '../ui/button.vue';
import { apiUrl } from '@client/config'; import { apiUrl } from '@client/config';
import * as os from '@client/os'; import * as os from '@client/os';

View file

@ -7,7 +7,7 @@
<script lang="ts"> <script lang="ts">
import { computed, defineComponent, PropType } from 'vue'; import { computed, defineComponent, PropType } from 'vue';
import MkRadio from '../ui/radio.vue'; import MkRadio from '../form/radio.vue';
import * as os from '@client/os'; import * as os from '@client/os';
import { Hpml } from '@client/scripts/hpml/evaluator'; import { Hpml } from '@client/scripts/hpml/evaluator';
import { RadioButtonVarBlock } from '@client/scripts/hpml/block'; import { RadioButtonVarBlock } from '@client/scripts/hpml/block';

View file

@ -6,7 +6,7 @@
<script lang="ts"> <script lang="ts">
import { computed, defineComponent, PropType } from 'vue'; import { computed, defineComponent, PropType } from 'vue';
import MkSwitch from '../ui/switch.vue'; import MkSwitch from '../form/switch.vue';
import * as os from '@client/os'; import * as os from '@client/os';
import { Hpml } from '@client/scripts/hpml/evaluator'; import { Hpml } from '@client/scripts/hpml/evaluator';
import { SwitchVarBlock } from '@client/scripts/hpml/block'; import { SwitchVarBlock } from '@client/scripts/hpml/block';

View file

@ -8,7 +8,7 @@
<script lang="ts"> <script lang="ts">
import { computed, defineComponent, PropType } from 'vue'; import { computed, defineComponent, PropType } from 'vue';
import MkInput from '../ui/input.vue'; import MkInput from '../form/input.vue';
import * as os from '@client/os'; import * as os from '@client/os';
import { Hpml } from '@client/scripts/hpml/evaluator'; import { Hpml } from '@client/scripts/hpml/evaluator';
import { TextInputVarBlock } from '@client/scripts/hpml/block'; import { TextInputVarBlock } from '@client/scripts/hpml/block';

View file

@ -8,7 +8,7 @@
<script lang="ts"> <script lang="ts">
import { computed, defineComponent, PropType } from 'vue'; import { computed, defineComponent, PropType } from 'vue';
import MkTextarea from '../ui/textarea.vue'; import MkTextarea from '../form/textarea.vue';
import * as os from '@client/os'; import * as os from '@client/os';
import { Hpml } from '@client/scripts/hpml/evaluator'; import { Hpml } from '@client/scripts/hpml/evaluator';
import { HpmlTextInput } from '@client/scripts/hpml'; import { HpmlTextInput } from '@client/scripts/hpml';

View file

@ -6,7 +6,7 @@
import { TextBlock } from '@client/scripts/hpml/block'; import { TextBlock } from '@client/scripts/hpml/block';
import { Hpml } from '@client/scripts/hpml/evaluator'; import { Hpml } from '@client/scripts/hpml/evaluator';
import { defineComponent, PropType } from 'vue'; import { defineComponent, PropType } from 'vue';
import MkTextarea from '../ui/textarea.vue'; import MkTextarea from '../form/textarea.vue';
export default defineComponent({ export default defineComponent({
components: { components: {

View file

@ -51,9 +51,9 @@
import { defineComponent } from 'vue'; import { defineComponent } from 'vue';
import { addTime } from '../../prelude/time'; import { addTime } from '../../prelude/time';
import { formatDateTimeString } from '@/misc/format-time-string'; import { formatDateTimeString } from '@/misc/format-time-string';
import MkInput from './ui/input.vue'; import MkInput from './form/input.vue';
import MkSelect from './ui/select.vue'; import MkSelect from './form/select.vue';
import MkSwitch from './ui/switch.vue'; import MkSwitch from './form/switch.vue';
import MkButton from './ui/button.vue'; import MkButton from './ui/button.vue';
export default defineComponent({ export default defineComponent({

View file

@ -1,6 +1,6 @@
<template> <template>
<div class="gafaadew" :class="{ modal, _popup: modal }" <div class="gafaadew" :class="{ modal, _popup: modal }"
v-size="{ max: [500] }" v-size="{ max: [310, 500] }"
@dragover.stop="onDragover" @dragover.stop="onDragover"
@dragenter="onDragenter" @dragenter="onDragenter"
@dragleave="onDragleave" @dragleave="onDragleave"
@ -17,12 +17,13 @@
<span v-if="visibility === 'followers'"><i class="fas fa-unlock"></i></span> <span v-if="visibility === 'followers'"><i class="fas fa-unlock"></i></span>
<span v-if="visibility === 'specified'"><i class="fas fa-envelope"></i></span> <span v-if="visibility === 'specified'"><i class="fas fa-envelope"></i></span>
</button> </button>
<button class="submit _buttonPrimary" :disabled="!canPost" @click="post" data-cy-open-post-form-submit>{{ submitText }}<i :class="reply ? 'fas fa-reply' : renote ? 'fas fa-quote-right' : 'fas fa-paper-plane'"></i></button> <button class="_button preview" @click="showPreview = !showPreview" :class="{ active: showPreview }" v-tooltip="$ts.previewNoteText"><i class="fas fa-file-code"></i></button>
<button class="submit _buttonGradate" :disabled="!canPost" @click="post" data-cy-open-post-form-submit>{{ submitText }}<i :class="reply ? 'fas fa-reply' : renote ? 'fas fa-quote-right' : 'fas fa-paper-plane'"></i></button>
</div> </div>
</header> </header>
<div class="form" :class="{ fixed }"> <div class="form" :class="{ fixed }">
<XNotePreview class="preview" v-if="reply" :note="reply"/> <XNoteSimple class="preview" v-if="reply" :note="reply"/>
<XNotePreview class="preview" v-if="renote" :note="renote"/> <XNoteSimple class="preview" v-if="renote" :note="renote"/>
<div class="with-quote" v-if="quoteId"><i class="fas fa-quote-left"></i> {{ $ts.quoteAttached }}<button @click="quoteId = null"><i class="fas fa-times"></i></button></div> <div class="with-quote" v-if="quoteId"><i class="fas fa-quote-left"></i> {{ $ts.quoteAttached }}<button @click="quoteId = null"><i class="fas fa-times"></i></button></div>
<div v-if="visibility === 'specified'" class="to-specified"> <div v-if="visibility === 'specified'" class="to-specified">
<span style="margin-right: 8px;">{{ $ts.recipient }}</span> <span style="margin-right: 8px;">{{ $ts.recipient }}</span>
@ -40,6 +41,7 @@
<input v-show="withHashtags" ref="hashtags" class="hashtags" v-model="hashtags" :placeholder="$ts.hashtags" list="hashtags"> <input v-show="withHashtags" ref="hashtags" class="hashtags" v-model="hashtags" :placeholder="$ts.hashtags" list="hashtags">
<XPostFormAttaches class="attaches" :files="files" @updated="updateFiles" @detach="detachFile" @changeSensitive="updateFileSensitive" @changeName="updateFileName"/> <XPostFormAttaches class="attaches" :files="files" @updated="updateFiles" @detach="detachFile" @changeSensitive="updateFileSensitive" @changeName="updateFileName"/>
<XPollEditor v-if="poll" :poll="poll" @destroyed="poll = null" @updated="onPollUpdate"/> <XPollEditor v-if="poll" :poll="poll" @destroyed="poll = null" @updated="onPollUpdate"/>
<XNotePreview class="preview" v-if="showPreview" :text="text"/>
<footer> <footer>
<button class="_button" @click="chooseFileFrom" v-tooltip="$ts.attachFile"><i class="fas fa-photo-video"></i></button> <button class="_button" @click="chooseFileFrom" v-tooltip="$ts.attachFile"><i class="fas fa-photo-video"></i></button>
<button class="_button" @click="togglePoll" :class="{ active: poll }" v-tooltip="$ts.poll"><i class="fas fa-poll-h"></i></button> <button class="_button" @click="togglePoll" :class="{ active: poll }" v-tooltip="$ts.poll"><i class="fas fa-poll-h"></i></button>
@ -61,6 +63,7 @@ import { defineComponent, defineAsyncComponent } from 'vue';
import insertTextAtCursor from 'insert-text-at-cursor'; import insertTextAtCursor from 'insert-text-at-cursor';
import { length } from 'stringz'; import { length } from 'stringz';
import { toASCII } from 'punycode/'; import { toASCII } from 'punycode/';
import XNoteSimple from './note-simple.vue';
import XNotePreview from './note-preview.vue'; import XNotePreview from './note-preview.vue';
import * as mfm from 'mfm-js'; import * as mfm from 'mfm-js';
import { host, url } from '@client/config'; import { host, url } from '@client/config';
@ -80,6 +83,7 @@ import { defaultStore } from '@client/store';
export default defineComponent({ export default defineComponent({
components: { components: {
XNoteSimple,
XNotePreview, XNotePreview,
XPostFormAttaches: defineAsyncComponent(() => import('./post-form-attaches.vue')), XPostFormAttaches: defineAsyncComponent(() => import('./post-form-attaches.vue')),
XPollEditor: defineAsyncComponent(() => import('./poll-editor.vue')), XPollEditor: defineAsyncComponent(() => import('./poll-editor.vue')),
@ -143,6 +147,7 @@ export default defineComponent({
files: [], files: [],
poll: null, poll: null,
useCw: false, useCw: false,
showPreview: false,
cw: null, cw: null,
localOnly: this.$store.state.rememberNoteVisibility ? this.$store.state.localOnly : this.$store.state.defaultNoteLocalOnly, localOnly: this.$store.state.rememberNoteVisibility ? this.$store.state.localOnly : this.$store.state.defaultNoteLocalOnly,
visibility: this.$store.state.rememberNoteVisibility ? this.$store.state.visibility : this.$store.state.defaultNoteVisibility, visibility: this.$store.state.rememberNoteVisibility ? this.$store.state.visibility : this.$store.state.defaultNoteVisibility,
@ -717,7 +722,7 @@ export default defineComponent({
> .visibility { > .visibility {
height: 34px; height: 34px;
width: 34px; width: 34px;
margin: 0 8px; margin: 0 0 0 8px;
& + .localOnly { & + .localOnly {
margin-left: 0 !important; margin-left: 0 !important;
@ -729,6 +734,24 @@ export default defineComponent({
opacity: 0.7; opacity: 0.7;
} }
> .preview {
display: inline-block;
padding: 0;
margin: 0 8px 0 0;
font-size: 16px;
width: 34px;
height: 34px;
border-radius: 6px;
&:hover {
background: var(--X5);
}
&.active {
color: var(--accent);
}
}
> .submit { > .submit {
margin: 16px 16px 16px 0; margin: 16px 16px 16px 0;
padding: 0 12px; padding: 0 12px;
@ -736,6 +759,7 @@ export default defineComponent({
font-weight: bold; font-weight: bold;
vertical-align: bottom; vertical-align: bottom;
border-radius: 4px; border-radius: 4px;
font-size: 0.9em;
&:disabled { &:disabled {
opacity: 0.7; opacity: 0.7;
@ -819,7 +843,7 @@ export default defineComponent({
color: var(--fg); color: var(--fg);
font-family: inherit; font-family: inherit;
&:focus { &:focus-visible {
outline: none; outline: none;
} }
@ -914,5 +938,17 @@ export default defineComponent({
} }
} }
} }
&.max-width_310px {
> .form {
> footer {
> button {
font-size: 14px;
width: 44px;
height: 44px;
}
}
}
}
} }
</style> </style>

View file

@ -30,10 +30,10 @@
<script lang="ts"> <script lang="ts">
import { defineComponent } from 'vue'; import { defineComponent } from 'vue';
import MkButton from '@client/components/ui/button.vue'; import MkButton from '@client/components/ui/button.vue';
import MkInput from '@client/components/ui/input.vue'; import MkInput from '@client/components/form/input.vue';
import MkSwitch from '@client/components/ui/switch.vue'; import MkSwitch from '@client/components/form/switch.vue';
import MkTextarea from '@client/components/ui/textarea.vue'; import MkTextarea from '@client/components/form/textarea.vue';
import MkRadio from '@client/components/ui/radio.vue'; import MkRadio from '@client/components/form/radio.vue';
import * as os from '@client/os'; import * as os from '@client/os';
import * as config from '@client/config'; import * as config from '@client/config';

View file

@ -1,17 +1,17 @@
<template> <template>
<form class="eppvobhk _monolithic_" :class="{ signing, totpLogin }" @submit.prevent="onSubmit"> <form class="eppvobhk _monolithic_" :class="{ signing, totpLogin }" @submit.prevent="onSubmit">
<div class="auth _section"> <div class="auth _section _formRoot">
<div class="avatar" :style="{ backgroundImage: user ? `url('${ user.avatarUrl }')` : null }" v-show="withAvatar"></div> <div class="avatar" :style="{ backgroundImage: user ? `url('${ user.avatarUrl }')` : null }" v-show="withAvatar"></div>
<div class="normal-signin" v-if="!totpLogin"> <div class="normal-signin" v-if="!totpLogin">
<MkInput v-model="username" :placeholder="$ts.username" type="text" pattern="^[a-zA-Z0-9_]+$" spellcheck="false" autofocus required @update:modelValue="onUsernameChange" data-cy-signin-username> <MkInput class="_formBlock" v-model="username" :placeholder="$ts.username" type="text" pattern="^[a-zA-Z0-9_]+$" spellcheck="false" autofocus required @update:modelValue="onUsernameChange" data-cy-signin-username>
<template #prefix>@</template> <template #prefix>@</template>
<template #suffix>@{{ host }}</template> <template #suffix>@{{ host }}</template>
</MkInput> </MkInput>
<MkInput v-model="password" :placeholder="$ts.password" type="password" :with-password-toggle="true" v-if="!user || user && !user.usePasswordLessLogin" required data-cy-signin-password> <MkInput class="_formBlock" v-model="password" :placeholder="$ts.password" type="password" :with-password-toggle="true" v-if="!user || user && !user.usePasswordLessLogin" required data-cy-signin-password>
<template #prefix><i class="fas fa-lock"></i></template> <template #prefix><i class="fas fa-lock"></i></template>
<template #caption><button class="_textButton" @click="resetPassword" type="button">{{ $ts.forgotPassword }}</button></template> <template #caption><button class="_textButton" @click="resetPassword" type="button">{{ $ts.forgotPassword }}</button></template>
</MkInput> </MkInput>
<MkButton type="submit" primary :disabled="signing" style="margin: 0 auto;">{{ signing ? $ts.loggingIn : $ts.login }}</MkButton> <MkButton class="_formBlock" type="submit" primary :disabled="signing" style="margin: 0 auto;">{{ signing ? $ts.loggingIn : $ts.login }}</MkButton>
</div> </div>
<div class="2fa-signin" v-if="totpLogin" :class="{ securityKeys: user && user.securityKeys }"> <div class="2fa-signin" v-if="totpLogin" :class="{ securityKeys: user && user.securityKeys }">
<div v-if="user && user.securityKeys" class="twofa-group tap-group"> <div v-if="user && user.securityKeys" class="twofa-group tap-group">
@ -49,7 +49,7 @@
import { defineComponent } from 'vue'; import { defineComponent } from 'vue';
import { toUnicode } from 'punycode/'; import { toUnicode } from 'punycode/';
import MkButton from '@client/components/ui/button.vue'; import MkButton from '@client/components/ui/button.vue';
import MkInput from '@client/components/ui/input.vue'; import MkInput from '@client/components/form/input.vue';
import { apiUrl, host } from '@client/config'; import { apiUrl, host } from '@client/config';
import { byteify, hexify } from '@client/scripts/2fa'; import { byteify, hexify } from '@client/scripts/2fa';
import * as os from '@client/os'; import * as os from '@client/os';

View file

@ -9,7 +9,7 @@
<div class="_monolithic_"> <div class="_monolithic_">
<div class="_section"> <div class="_section">
<XSignup :auto-set="autoSet" @signup="onSignup"/> <XSignup :auto-set="autoSet" @signup="onSignup" @signupEmailPending="onSignupEmailPending"/>
</div> </div>
</div> </div>
</XModalWindow> </XModalWindow>
@ -40,6 +40,10 @@ export default defineComponent({
onSignup(res) { onSignup(res) {
this.$emit('done', res); this.$emit('done', res);
this.$refs.dialog.close(); this.$refs.dialog.close();
},
onSignupEmailPending() {
this.$refs.dialog.close();
} }
} }
}); });

View file

@ -1,25 +1,35 @@
<template> <template>
<form class="qlvuhzng" @submit.prevent="onSubmit" :autocomplete="Math.random()"> <form class="qlvuhzng _formRoot" @submit.prevent="onSubmit" :autocomplete="Math.random()">
<template v-if="meta"> <template v-if="meta">
<MkInput class="_inputNoTopMargin" v-if="meta.disableRegistration" v-model="invitationCode" type="text" :autocomplete="Math.random()" spellcheck="false" required> <MkInput class="_formBlock" v-if="meta.disableRegistration" v-model="invitationCode" type="text" :autocomplete="Math.random()" spellcheck="false" required>
<template #label>{{ $ts.invitationCode }}</template> <template #label>{{ $ts.invitationCode }}</template>
<template #prefix><i class="fas fa-key"></i></template> <template #prefix><i class="fas fa-key"></i></template>
</MkInput> </MkInput>
<MkInput class="_inputNoTopMargin" v-model="username" type="text" pattern="^[a-zA-Z0-9_]{1,20}$" :autocomplete="Math.random()" spellcheck="false" required @update:modelValue="onChangeUsername" data-cy-signup-username> <MkInput class="_formBlock" v-model="username" type="text" pattern="^[a-zA-Z0-9_]{1,20}$" :autocomplete="Math.random()" spellcheck="false" required @update:modelValue="onChangeUsername" data-cy-signup-username>
<template #label>{{ $ts.username }} <div class="_button _help" v-tooltip:dialog="$ts.usernameInfo"><i class="far fa-question-circle"></i></div></template> <template #label>{{ $ts.username }} <div class="_button _help" v-tooltip:dialog="$ts.usernameInfo"><i class="far fa-question-circle"></i></div></template>
<template #prefix>@</template> <template #prefix>@</template>
<template #suffix>@{{ host }}</template> <template #suffix>@{{ host }}</template>
<template #caption> <template #caption>
<span v-if="usernameState == 'wait'" style="color:#999"><i class="fas fa-spinner fa-pulse fa-fw"></i> {{ $ts.checking }}</span> <span v-if="usernameState === 'wait'" style="color:#999"><i class="fas fa-spinner fa-pulse fa-fw"></i> {{ $ts.checking }}</span>
<span v-if="usernameState == 'ok'" style="color: var(--success)"><i class="fas fa-check fa-fw"></i> {{ $ts.available }}</span> <span v-else-if="usernameState === 'ok'" style="color: var(--success)"><i class="fas fa-check fa-fw"></i> {{ $ts.available }}</span>
<span v-if="usernameState == 'unavailable'" style="color: var(--error)"><i class="fas fa-exclamation-triangle fa-fw"></i> {{ $ts.unavailable }}</span> <span v-else-if="usernameState === 'unavailable'" style="color: var(--error)"><i class="fas fa-exclamation-triangle fa-fw"></i> {{ $ts.unavailable }}</span>
<span v-if="usernameState == 'error'" style="color: var(--error)"><i class="fas fa-exclamation-triangle fa-fw"></i> {{ $ts.error }}</span> <span v-else-if="usernameState === 'error'" style="color: var(--error)"><i class="fas fa-exclamation-triangle fa-fw"></i> {{ $ts.error }}</span>
<span v-if="usernameState == 'invalid-format'" style="color: var(--error)"><i class="fas fa-exclamation-triangle fa-fw"></i> {{ $ts.usernameInvalidFormat }}</span> <span v-else-if="usernameState === 'invalid-format'" style="color: var(--error)"><i class="fas fa-exclamation-triangle fa-fw"></i> {{ $ts.usernameInvalidFormat }}</span>
<span v-if="usernameState == 'min-range'" style="color: var(--error)"><i class="fas fa-exclamation-triangle fa-fw"></i> {{ $ts.tooShort }}</span> <span v-else-if="usernameState === 'min-range'" style="color: var(--error)"><i class="fas fa-exclamation-triangle fa-fw"></i> {{ $ts.tooShort }}</span>
<span v-if="usernameState == 'max-range'" style="color: var(--error)"><i class="fas fa-exclamation-triangle fa-fw"></i> {{ $ts.tooLong }}</span> <span v-else-if="usernameState === 'max-range'" style="color: var(--error)"><i class="fas fa-exclamation-triangle fa-fw"></i> {{ $ts.tooLong }}</span>
</template> </template>
</MkInput> </MkInput>
<MkInput v-model="password" type="password" :autocomplete="Math.random()" required @update:modelValue="onChangePassword" data-cy-signup-password> <MkInput v-if="meta.emailRequiredForSignup" class="_formBlock" v-model="email" type="email" :autocomplete="Math.random()" spellcheck="false" required @update:modelValue="onChangeEmail" data-cy-signup-email>
<template #label>{{ $ts.emailAddress }} <div class="_button _help" v-tooltip:dialog="$ts._signup.emailAddressInfo"><i class="far fa-question-circle"></i></div></template>
<template #prefix><i class="fas fa-envelope"></i></template>
<template #caption>
<span v-if="emailState === 'wait'" style="color:#999"><i class="fas fa-spinner fa-pulse fa-fw"></i> {{ $ts.checking }}</span>
<span v-else-if="emailState === 'ok'" style="color: var(--success)"><i class="fas fa-check fa-fw"></i> {{ $ts.available }}</span>
<span v-else-if="emailState === 'unavailable'" style="color: var(--error)"><i class="fas fa-exclamation-triangle fa-fw"></i> {{ $ts.unavailable }}</span>
<span v-else-if="emailState === 'error'" style="color: var(--error)"><i class="fas fa-exclamation-triangle fa-fw"></i> {{ $ts.error }}</span>
</template>
</MkInput>
<MkInput class="_formBlock" v-model="password" type="password" :autocomplete="Math.random()" required @update:modelValue="onChangePassword" data-cy-signup-password>
<template #label>{{ $ts.password }}</template> <template #label>{{ $ts.password }}</template>
<template #prefix><i class="fas fa-lock"></i></template> <template #prefix><i class="fas fa-lock"></i></template>
<template #caption> <template #caption>
@ -28,7 +38,7 @@
<span v-if="passwordStrength == 'high'" style="color: var(--success)"><i class="fas fa-check fa-fw"></i> {{ $ts.strongPassword }}</span> <span v-if="passwordStrength == 'high'" style="color: var(--success)"><i class="fas fa-check fa-fw"></i> {{ $ts.strongPassword }}</span>
</template> </template>
</MkInput> </MkInput>
<MkInput v-model="retypedPassword" type="password" :autocomplete="Math.random()" required @update:modelValue="onChangePasswordRetype" data-cy-signup-password-retype> <MkInput class="_formBlock" v-model="retypedPassword" type="password" :autocomplete="Math.random()" required @update:modelValue="onChangePasswordRetype" data-cy-signup-password-retype>
<template #label>{{ $ts.password }} ({{ $ts.retype }})</template> <template #label>{{ $ts.password }} ({{ $ts.retype }})</template>
<template #prefix><i class="fas fa-lock"></i></template> <template #prefix><i class="fas fa-lock"></i></template>
<template #caption> <template #caption>
@ -36,7 +46,7 @@
<span v-if="passwordRetypeState == 'not-match'" style="color: var(--error)"><i class="fas fa-exclamation-triangle fa-fw"></i> {{ $ts.passwordNotMatched }}</span> <span v-if="passwordRetypeState == 'not-match'" style="color: var(--error)"><i class="fas fa-exclamation-triangle fa-fw"></i> {{ $ts.passwordNotMatched }}</span>
</template> </template>
</MkInput> </MkInput>
<label v-if="meta.tosUrl" class="tou"> <label v-if="meta.tosUrl" class="_formBlock tou">
<input type="checkbox" v-model="ToSAgreement"> <input type="checkbox" v-model="ToSAgreement">
<I18n :src="$ts.agreeTo"> <I18n :src="$ts.agreeTo">
<template #0> <template #0>
@ -44,9 +54,9 @@
</template> </template>
</I18n> </I18n>
</label> </label>
<captcha v-if="meta.enableHcaptcha" class="captcha" provider="hcaptcha" ref="hcaptcha" v-model:value="hCaptchaResponse" :sitekey="meta.hcaptchaSiteKey"/> <captcha v-if="meta.enableHcaptcha" class="_formBlock captcha" provider="hcaptcha" ref="hcaptcha" v-model="hCaptchaResponse" :sitekey="meta.hcaptchaSiteKey"/>
<captcha v-if="meta.enableRecaptcha" class="captcha" provider="recaptcha" ref="recaptcha" v-model:value="reCaptchaResponse" :sitekey="meta.recaptchaSiteKey"/> <captcha v-if="meta.enableRecaptcha" class="_formBlock captcha" provider="recaptcha" ref="recaptcha" v-model="reCaptchaResponse" :sitekey="meta.recaptchaSiteKey"/>
<MkButton type="submit" :disabled="shouldDisableSubmitting" primary data-cy-signup-submit>{{ $ts.start }}</MkButton> <MkButton class="_formBlock" type="submit" :disabled="shouldDisableSubmitting" gradate data-cy-signup-submit>{{ $ts.start }}</MkButton>
</template> </template>
</form> </form>
</template> </template>
@ -57,8 +67,8 @@ const getPasswordStrength = require('syuilo-password-strength');
import { toUnicode } from 'punycode/'; import { toUnicode } from 'punycode/';
import { host, url } from '@client/config'; import { host, url } from '@client/config';
import MkButton from './ui/button.vue'; import MkButton from './ui/button.vue';
import MkInput from './ui/input.vue'; import MkInput from './form/input.vue';
import MkSwitch from './ui/switch.vue'; import MkSwitch from './form/switch.vue';
import * as os from '@client/os'; import * as os from '@client/os';
import { login } from '@client/account'; import { login } from '@client/account';
@ -87,8 +97,10 @@ export default defineComponent({
password: '', password: '',
retypedPassword: '', retypedPassword: '',
invitationCode: '', invitationCode: '',
email: '',
url, url,
usernameState: null, usernameState: null,
emailState: null,
passwordStrength: '', passwordStrength: '',
passwordRetypeState: null, passwordRetypeState: null,
submitting: false, submitting: false,
@ -148,6 +160,23 @@ export default defineComponent({
}); });
}, },
onChangeEmail() {
if (this.email == '') {
this.emailState = null;
return;
}
this.emailState = 'wait';
os.api('email-address/available', {
emailAddress: this.email
}).then(result => {
this.emailState = result.available ? 'ok' : 'unavailable';
}).catch(err => {
this.emailState = 'error';
});
},
onChangePassword() { onChangePassword() {
if (this.password == '') { if (this.password == '') {
this.passwordStrength = ''; this.passwordStrength = '';
@ -174,20 +203,30 @@ export default defineComponent({
os.api('signup', { os.api('signup', {
username: this.username, username: this.username,
password: this.password, password: this.password,
emailAddress: this.email,
invitationCode: this.invitationCode, invitationCode: this.invitationCode,
'hcaptcha-response': this.hCaptchaResponse, 'hcaptcha-response': this.hCaptchaResponse,
'g-recaptcha-response': this.reCaptchaResponse, 'g-recaptcha-response': this.reCaptchaResponse,
}).then(() => { }).then(() => {
return os.api('signin', { if (this.meta.emailRequiredForSignup) {
username: this.username, os.dialog({
password: this.password type: 'success',
}).then(res => { title: this.$ts._signup.almostThere,
this.$emit('signup', res); text: this.$t('_signup.emailSent', { email: this.email }),
});
this.$emit('signupEmailPending');
} else {
os.api('signin', {
username: this.username,
password: this.password
}).then(res => {
this.$emit('signup', res);
if (this.autoSet) { if (this.autoSet) {
return login(res.i); login(res.i);
} }
}); });
}
}).catch(() => { }).catch(() => {
this.submitting = false; this.submitting = false;
this.$refs.hcaptcha?.reset?.(); this.$refs.hcaptcha?.reset?.();

View file

@ -3,7 +3,7 @@ import { defineComponent, h, resolveDirective, withDirectives } from 'vue';
export default defineComponent({ export default defineComponent({
props: { props: {
value: { modelValue: {
required: true, required: true,
}, },
}, },
@ -13,11 +13,11 @@ export default defineComponent({
return withDirectives(h('div', { return withDirectives(h('div', {
class: 'pxhvhrfw', class: 'pxhvhrfw',
}, options.map(option => withDirectives(h('button', { }, options.map(option => withDirectives(h('button', {
class: ['_button', { active: this.value === option.props.value }], class: ['_button', { active: this.modelValue === option.props.value }],
key: option.key, key: option.key,
disabled: this.value === option.props.value, disabled: this.modelValue === option.props.value,
onClick: () => { onClick: () => {
this.$emit('update:value', option.props.value); this.$emit('update:modelValue', option.props.value);
} }
}, option.children), [ }, option.children), [
[resolveDirective('click-anime')] [resolveDirective('click-anime')]
@ -35,8 +35,8 @@ export default defineComponent({
> button { > button {
flex: 1; flex: 1;
padding: 15px 12px 12px 12px; padding: 10px 8px;
border-bottom: solid 3px transparent; border-radius: 6px;
&:disabled { &:disabled {
opacity: 1 !important; opacity: 1 !important;
@ -45,11 +45,16 @@ export default defineComponent({
&.active { &.active {
color: var(--accent); color: var(--accent);
border-bottom-color: var(--accent); background: var(--accentedBg);
} }
&:not(.active):hover { &:not(.active):hover {
color: var(--fgHighlighted); color: var(--fgHighlighted);
background: var(--panelHighlight);
}
&:not(:first-child) {
margin-left: 8px;
} }
> .icon { > .icon {
@ -61,7 +66,7 @@ export default defineComponent({
font-size: 80%; font-size: 80%;
> button { > button {
padding: 11px 8px 8px 8px; padding: 11px 8px;
} }
} }
} }

View file

@ -9,7 +9,7 @@
<template #header>Req Viewer</template> <template #header>Req Viewer</template>
<div class="rlkneywz"> <div class="rlkneywz">
<MkTab v-model:value="tab" style="border-bottom: solid 0.5px var(--divider);"> <MkTab v-model="tab" style="border-bottom: solid 0.5px var(--divider);">
<option value="req">Request</option> <option value="req">Request</option>
<option value="res">Response</option> <option value="res">Response</option>
</MkTab> </MkTab>

View file

@ -4,7 +4,7 @@
<i class="fas fa-terminal" style="margin-right: 0.5em;"></i>Task Manager <i class="fas fa-terminal" style="margin-right: 0.5em;"></i>Task Manager
</template> </template>
<div class="qljqmnzj _monospace"> <div class="qljqmnzj _monospace">
<MkTab v-model:value="tab" style="border-bottom: solid 0.5px var(--divider);"> <MkTab v-model="tab" style="border-bottom: solid 0.5px var(--divider);">
<option value="windows">Windows</option> <option value="windows">Windows</option>
<option value="stream">Stream</option> <option value="stream">Stream</option>
<option value="streamPool">Stream (Pool)</option> <option value="streamPool">Stream (Pool)</option>

View file

@ -31,9 +31,9 @@
import { defineComponent } from 'vue'; import { defineComponent } from 'vue';
import { kinds } from '@/misc/api-permissions'; import { kinds } from '@/misc/api-permissions';
import XModalWindow from '@client/components/ui/modal-window.vue'; import XModalWindow from '@client/components/ui/modal-window.vue';
import MkInput from './ui/input.vue'; import MkInput from './form/input.vue';
import MkTextarea from './ui/textarea.vue'; import MkTextarea from './form/textarea.vue';
import MkSwitch from './ui/switch.vue'; import MkSwitch from './form/switch.vue';
import MkButton from './ui/button.vue'; import MkButton from './ui/button.vue';
import MkInfo from './ui/info.vue'; import MkInfo from './ui/info.vue';

View file

@ -1,7 +1,6 @@
<template> <template>
<component class="bghgjjyj _button" <button v-if="!link" class="bghgjjyj _button"
:is="link ? 'MkA' : 'button'" :class="{ inline, primary, gradate, danger, rounded, full }"
:class="{ inline, primary, danger, full }"
:type="type" :type="type"
@click="$emit('click', $event)" @click="$emit('click', $event)"
@mousedown="onMousedown" @mousedown="onMousedown"
@ -10,7 +9,17 @@
<div class="content"> <div class="content">
<slot></slot> <slot></slot>
</div> </div>
</component> </button>
<MkA v-else class="bghgjjyj _button"
:class="{ inline, primary, gradate, danger, rounded, full }"
:to="to"
@mousedown="onMousedown"
>
<div ref="ripples" class="ripples"></div>
<div class="content">
<slot></slot>
</div>
</MkA>
</template> </template>
<script lang="ts"> <script lang="ts">
@ -27,6 +36,16 @@ export default defineComponent({
required: false, required: false,
default: false default: false
}, },
gradate: {
type: Boolean,
required: false,
default: false
},
rounded: {
type: Boolean,
required: false,
default: false
},
inline: { inline: {
type: Boolean, type: Boolean,
required: false, required: false,
@ -37,6 +56,10 @@ export default defineComponent({
required: false, required: false,
default: false default: false
}, },
to: {
type: String,
required: false
},
autofocus: { autofocus: {
type: Boolean, type: Boolean,
required: false, required: false,
@ -119,13 +142,13 @@ export default defineComponent({
padding: 8px 14px; padding: 8px 14px;
text-align: center; text-align: center;
font-weight: normal; font-weight: normal;
font-size: 0.9em; font-size: 0.8em;
line-height: 24px; line-height: 22px;
box-shadow: none; box-shadow: none;
text-decoration: none; text-decoration: none;
background: var(--buttonBg); background: var(--buttonBg);
border-radius: 999px; border-radius: 4px;
overflow: hidden; overflow: clip;
box-sizing: border-box; box-sizing: border-box;
transition: background 0.1s ease; transition: background 0.1s ease;
@ -141,6 +164,10 @@ export default defineComponent({
width: 100%; width: 100%;
} }
&.rounded {
border-radius: 999px;
}
&.primary { &.primary {
font-weight: bold; font-weight: bold;
color: var(--fgOnAccent) !important; color: var(--fgOnAccent) !important;
@ -155,6 +182,20 @@ export default defineComponent({
} }
} }
&.gradate {
font-weight: bold;
color: var(--fgOnAccent) !important;
background: linear-gradient(90deg, var(--buttonGradateA), var(--buttonGradateB));
&:not(:disabled):hover {
background: linear-gradient(90deg, var(--X8), var(--X8));
}
&:not(:disabled):active {
background: linear-gradient(90deg, var(--X8), var(--X8));
}
}
&.danger { &.danger {
color: #ff2a2a; color: #ff2a2a;
@ -176,19 +217,11 @@ export default defineComponent({
opacity: 0.7; opacity: 0.7;
} }
&:focus { &:focus-visible {
outline: solid 2px var(--focus); outline: solid 2px var(--focus);
outline-offset: 2px; outline-offset: 2px;
} }
&.inline + .bghgjjyj {
margin-left: 12px;
}
&:not(.inline) + .bghgjjyj {
margin-top: 16px;
}
&.inline { &.inline {
display: inline-block; display: inline-block;
width: auto; width: auto;

View file

@ -1,6 +1,6 @@
<template> <template>
<div class="ssazuxis" v-size="{ max: [500] }"> <div class="ssazuxis" v-size="{ max: [500] }">
<header @click="showBody = !showBody" class="_button"> <header @click="showBody = !showBody" class="_button" :style="{ background: bg }">
<div class="title"><slot name="header"></slot></div> <div class="title"><slot name="header"></slot></div>
<div class="divider"></div> <div class="divider"></div>
<button class="_button"> <button class="_button">
@ -23,6 +23,7 @@
<script lang="ts"> <script lang="ts">
import { defineComponent } from 'vue'; import { defineComponent } from 'vue';
import * as tinycolor from 'tinycolor2';
const localStoragePrefix = 'ui:folder:'; const localStoragePrefix = 'ui:folder:';
@ -41,6 +42,7 @@ export default defineComponent({
}, },
data() { data() {
return { return {
bg: null,
showBody: (this.persistKey && localStorage.getItem(localStoragePrefix + this.persistKey)) ? localStorage.getItem(localStoragePrefix + this.persistKey) === 't' : this.expanded, showBody: (this.persistKey && localStorage.getItem(localStoragePrefix + this.persistKey)) ? localStorage.getItem(localStoragePrefix + this.persistKey) === 't' : this.expanded,
}; };
}, },
@ -51,6 +53,21 @@ export default defineComponent({
} }
} }
}, },
mounted() {
function getParentBg(el: Element | null): string {
if (el == null || el.tagName === 'BODY') return 'var(--bg)';
const bg = el.style.background || el.style.backgroundColor;
if (bg) {
return bg;
} else {
return getParentBg(el.parentElement);
}
}
const rawBg = getParentBg(this.$el);
const bg = tinycolor(rawBg.startsWith('var(') ? getComputedStyle(document.documentElement).getPropertyValue(rawBg.slice(4, -1)) : rawBg);
bg.setAlpha(0.85);
this.bg = bg.toRgbString();
},
methods: { methods: {
toggleContent(show: boolean) { toggleContent(show: boolean) {
this.showBody = show; this.showBody = show;
@ -100,12 +117,8 @@ export default defineComponent({
position: sticky; position: sticky;
top: var(--stickyTop, 0px); top: var(--stickyTop, 0px);
padding: var(--x-padding); padding: var(--x-padding);
background: var(--x-header, var(--panel));
/* TODO panel
background: var(--X17);
-webkit-backdrop-filter: var(--blur, blur(8px)); -webkit-backdrop-filter: var(--blur, blur(8px));
backdrop-filter: var(--blur, blur(20px)); backdrop-filter: var(--blur, blur(20px));
*/
> .title { > .title {
margin: 0; margin: 0;
@ -141,7 +154,7 @@ export default defineComponent({
} }
} }
._flat_ .ssazuxis { ._fitSide_ .ssazuxis {
> header { > header {
padding: 0 16px; padding: 0 16px;
} }

View file

@ -27,7 +27,6 @@ export default defineComponent({
<style lang="scss" scoped> <style lang="scss" scoped>
.fpezltsf { .fpezltsf {
margin: 16px 0;
padding: 16px; padding: 16px;
font-size: 90%; font-size: 90%;
background: var(--infoBg); background: var(--infoBg);
@ -39,20 +38,12 @@ export default defineComponent({
color: var(--infoWarnFg); color: var(--infoWarnFg);
} }
&:first-child {
margin-top: 0;
}
&:last-child {
margin-bottom: 0;
}
> i { > i {
margin-right: 4px; margin-right: 4px;
} }
} }
._flat_ .fpezltsf { ._fitSide_ .fpezltsf {
border-radius: 0; border-radius: 0;
} }
</style> </style>

View file

@ -1,5 +1,5 @@
<template> <template>
<div class="rrevdjwt" :class="{ left: align === 'left', pointer: point === 'top' }" <div class="rrevdjwt" :class="{ center: align === 'center' }"
ref="items" ref="items"
@contextmenu.self="e => e.preventDefault()" @contextmenu.self="e => e.preventDefault()"
v-hotkey="keymap" v-hotkey="keymap"
@ -27,7 +27,7 @@
<MkAvatar :user="item.user" class="avatar"/><MkUserName :user="item.user"/> <MkAvatar :user="item.user" class="avatar"/><MkUserName :user="item.user"/>
<span v-if="item.indicate" class="indicator"><i class="fas fa-circle"></i></span> <span v-if="item.indicate" class="indicator"><i class="fas fa-circle"></i></span>
</button> </button>
<button v-else @click="clicked(item.action, $event)" :tabindex="i" class="_button item" :class="{ danger: item.danger }"> <button v-else @click="clicked(item.action, $event)" :tabindex="i" class="_button item" :class="{ danger: item.danger, active: item.active }" :disabled="item.active">
<i v-if="item.icon" class="fa-fw" :class="item.icon"></i> <i v-if="item.icon" class="fa-fw" :class="item.icon"></i>
<MkAvatar v-if="item.avatar" :user="item.avatar" class="avatar"/> <MkAvatar v-if="item.avatar" :user="item.avatar" class="avatar"/>
<span>{{ item.text }}</span> <span>{{ item.text }}</span>
@ -59,10 +59,6 @@ export default defineComponent({
type: String, type: String,
requried: false requried: false
}, },
point: {
type: String,
requried: false
},
}, },
emits: ['close'], emits: ['close'],
data() { data() {
@ -145,68 +141,83 @@ export default defineComponent({
<style lang="scss" scoped> <style lang="scss" scoped>
.rrevdjwt { .rrevdjwt {
padding: 8px 0; padding: 8px 0;
min-width: 200px;
&.pointer { &.center {
&:before {
--size: 8px;
content: '';
display: block;
position: absolute;
top: calc(0px - (var(--size) * 2));
left: 0;
right: 0;
width: 0;
margin: auto;
border: solid var(--size) transparent;
border-bottom-color: var(--popup);
}
}
&.left {
> .item { > .item {
text-align: left; text-align: center;
} }
} }
> .item { > .item {
display: block; display: block;
position: relative; position: relative;
padding: 8px 16px; padding: 8px 18px;
width: 100%; width: 100%;
box-sizing: border-box; box-sizing: border-box;
white-space: nowrap; white-space: nowrap;
font-size: 0.9em; font-size: 0.9em;
line-height: 20px; line-height: 20px;
text-align: center; text-align: left;
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;
&:before {
content: "";
display: block;
position: absolute;
top: 0;
left: 0;
right: 0;
margin: auto;
width: calc(100% - 16px);
height: 100%;
border-radius: 6px;
}
> * {
position: relative;
}
&.danger { &.danger {
color: #ff2a2a; color: #ff2a2a;
&:hover { &:hover {
color: #fff; color: #fff;
background: #ff4242;
&:before {
background: #ff4242;
}
} }
&:active { &:active {
color: #fff; color: #fff;
background: #d42e2e;
&:before {
background: #d42e2e;
}
} }
} }
&:hover { &.active {
color: var(--fgOnAccent); color: var(--fgOnAccent);
background: var(--accent); opacity: 1;
&:before {
background: var(--accent);
}
}
&:not(:disabled):hover {
color: var(--accent);
text-decoration: none; text-decoration: none;
&:before {
background: var(--accentedBg);
}
} }
&:active { &:not(:active):focus-visible {
color: var(--fgOnAccent);
background: var(--accentDarken);
}
&:not(:active):focus {
box-shadow: 0 0 0 2px var(--focus) inset; box-shadow: 0 0 0 2px var(--focus) inset;
} }
@ -231,12 +242,12 @@ export default defineComponent({
} }
> i { > i {
margin-right: 4px; margin-right: 5px;
width: 20px; width: 20px;
} }
> .avatar { > .avatar {
margin-right: 4px; margin-right: 5px;
width: 20px; width: 20px;
height: 20px; height: 20px;
} }

View file

@ -1,6 +1,6 @@
<template> <template>
<MkPopup ref="popup" :src="src" @closed="$emit('closed')" #default="{point}"> <MkPopup ref="popup" :src="src" @closed="$emit('closed')">
<MkMenu :items="items" :align="align" :point="point" @close="$refs.popup.close()" class="_popup _shadow"/> <MkMenu :items="items" :align="align" @close="$refs.popup.close()" class="_popup _shadow"/>
</MkPopup> </MkPopup>
</template> </template>

View file

@ -1,7 +1,7 @@
<template> <template>
<transition :name="$store.state.animation ? 'popup-menu' : ''" appear @after-leave="onClosed" @enter="$emit('opening')" @after-enter="childRendered"> <transition :name="$store.state.animation ? 'popup-menu' : ''" appear @after-leave="onClosed" @enter="$emit('opening')" @after-enter="childRendered">
<div v-show="manualShowing != null ? manualShowing : showing" class="ccczpooj" :class="{ front, fixed, top: position === 'top' }" ref="content" :style="{ pointerEvents: (manualShowing != null ? manualShowing : showing) ? 'auto' : 'none', '--transformOrigin': transformOrigin }"> <div v-show="manualShowing != null ? manualShowing : showing" class="ccczpooj" :class="{ front, fixed, top: position === 'top' }" ref="content" :style="{ pointerEvents: (manualShowing != null ? manualShowing : showing) ? 'auto' : 'none', '--transformOrigin': transformOrigin }">
<slot :point="point"></slot> <slot></slot>
</div> </div>
</transition> </transition>
</template> </template>
@ -52,7 +52,6 @@ export default defineComponent({
fixed: false, fixed: false,
transformOrigin: 'center', transformOrigin: 'center',
contentClicking: false, contentClicking: false,
point: null,
}; };
}, },
@ -136,10 +135,8 @@ export default defineComponent({
} }
if (top > rect.top + (this.fixed ? 0 : window.pageYOffset)) { if (top > rect.top + (this.fixed ? 0 : window.pageYOffset)) {
this.point = 'top';
this.transformOrigin = 'center top'; this.transformOrigin = 'center top';
} else { } else {
this.point = null;
this.transformOrigin = 'center'; this.transformOrigin = 'center';
} }

View file

@ -1,58 +0,0 @@
<script lang="ts">
import { defineComponent, h } from 'vue';
import MkRadio from '@client/components/ui/radio.vue';
export default defineComponent({
components: {
MkRadio
},
props: {
modelValue: {
required: false
},
},
data() {
return {
value: this.modelValue,
}
},
watch: {
value() {
this.$emit('update:modelValue', this.value);
}
},
render() {
const label = this.$slots.desc();
let options = this.$slots.default();
// Fragment
if (options.length === 1 && options[0].props == null) options = options[0].children;
return h('div', {
class: 'novjtcto'
}, [
h('div', label),
...options.map(option => h(MkRadio, {
key: option.key,
value: option.props.value,
modelValue: this.value,
'onUpdate:modelValue': value => this.value = value,
}, option.children))
]);
}
});
</script>
<style lang="scss">
.novjtcto {
margin: 32px 0;
&:first-child {
margin-top: 0;
}
&:last-child {
margin-bottom: 0;
}
}
</style>

Some files were not shown because too many files have changed in this diff Show more