Application & pat ui improvements (#5271)

* Add categories, make localizable

* Run fix

* Run prepr

* Improve pat modal ui

* Fix pat token actions

* Make scope category localization shared

* Fix casing

* Fix casing

---------

Co-authored-by: Creeperkatze <178587183+Creeperkatze@users.noreply.github.com>
Co-authored-by: Prospector <6166773+Prospector@users.noreply.github.com>
This commit is contained in:
Creeperkatze
2026-02-02 19:20:28 +01:00
committed by GitHub
parent 56c8bb1950
commit 3f6c79b00d
4 changed files with 529 additions and 79 deletions

View File

@@ -323,6 +323,57 @@ export const scopeMessages = defineMessages({
},
})
export const scopeCategoryMessages = defineMessages({
categoryUserAccount: {
id: 'scopes.category.user-account',
defaultMessage: 'User account',
},
categoryProjects: {
id: 'scopes.category.projects',
defaultMessage: 'Projects',
},
categoryVersions: {
id: 'scopes.category.versions',
defaultMessage: 'Versions',
},
categoryCollections: {
id: 'scopes.category.collections',
defaultMessage: 'Collections',
},
categoryOrganizations: {
id: 'scopes.category.organizations',
defaultMessage: 'Organizations',
},
categoryReports: {
id: 'scopes.category.reports',
defaultMessage: 'Reports',
},
categoryThreads: {
id: 'scopes.category.threads',
defaultMessage: 'Threads',
},
categoryPats: {
id: 'scopes.category.pats',
defaultMessage: 'PATs',
},
categorySessions: {
id: 'scopes.category.sessions',
defaultMessage: 'Sessions',
},
categoryNotifications: {
id: 'scopes.category.notifications',
defaultMessage: 'Notifications',
},
categoryPayouts: {
id: 'scopes.category.payouts',
defaultMessage: 'Payouts',
},
categoryAnalytics: {
id: 'scopes.category.analytics',
defaultMessage: 'Analytics',
},
})
const scopeDefinitions = [
{
id: 'USER_READ_EMAIL',

View File

@@ -2444,6 +2444,42 @@
"scopes.analytics.label": {
"message": "Read analytics"
},
"scopes.category.analytics": {
"message": "Analytics"
},
"scopes.category.collections": {
"message": "Collections"
},
"scopes.category.notifications": {
"message": "Notifications"
},
"scopes.category.organizations": {
"message": "Organizations"
},
"scopes.category.pats": {
"message": "PATs"
},
"scopes.category.payouts": {
"message": "Payouts"
},
"scopes.category.projects": {
"message": "Projects"
},
"scopes.category.reports": {
"message": "Reports"
},
"scopes.category.sessions": {
"message": "Sessions"
},
"scopes.category.threads": {
"message": "Threads"
},
"scopes.category.user-account": {
"message": "User account"
},
"scopes.category.versions": {
"message": "Versions"
},
"scopes.collectionCreate.description": {
"message": "Create collections"
},
@@ -2741,6 +2777,102 @@
"servers.plan.small.name": {
"message": "Small"
},
"settings.applications.about": {
"message": "About"
},
"settings.applications.button.add-more": {
"message": "Add more"
},
"settings.applications.button.add-redirect-uri": {
"message": "Add a redirect uri"
},
"settings.applications.button.cancel": {
"message": "Cancel"
},
"settings.applications.button.create": {
"message": "Create app"
},
"settings.applications.button.delete": {
"message": "Delete"
},
"settings.applications.button.edit": {
"message": "Edit"
},
"settings.applications.button.new": {
"message": "New application"
},
"settings.applications.button.save-changes": {
"message": "Save changes"
},
"settings.applications.button.upload-icon": {
"message": "Upload icon"
},
"settings.applications.client-id": {
"message": "Client ID"
},
"settings.applications.client-secret": {
"message": "Client secret"
},
"settings.applications.created-on": {
"message": "Created on {date}"
},
"settings.applications.delete.confirm.button": {
"message": "Delete this application"
},
"settings.applications.delete.confirm.description": {
"message": "This will permanently delete this application and revoke all access tokens. (forever!)"
},
"settings.applications.delete.confirm.title": {
"message": "Are you sure you want to delete this application?"
},
"settings.applications.description.intro": {
"message": "Applications can be used to authenticate Modrinth's users with your products. For more information, see <docs-link>Modrinth's API documentation</docs-link>."
},
"settings.applications.field.description": {
"message": "Description"
},
"settings.applications.field.description.placeholder": {
"message": "Enter the application's description..."
},
"settings.applications.field.icon": {
"message": "Icon"
},
"settings.applications.field.name": {
"message": "Name"
},
"settings.applications.field.name.placeholder": {
"message": "Enter the application's name..."
},
"settings.applications.field.redirect-uri.placeholder": {
"message": "https://example.com/auth/callback"
},
"settings.applications.field.redirect-uris": {
"message": "Redirect uris"
},
"settings.applications.field.scopes": {
"message": "Scopes"
},
"settings.applications.field.url": {
"message": "URL"
},
"settings.applications.field.url.placeholder": {
"message": "https://example.com"
},
"settings.applications.modal.header": {
"message": "Application information"
},
"settings.applications.notification.error.title": {
"message": "An error occurred"
},
"settings.applications.notification.icon-updated.description": {
"message": "Your application icon has been updated."
},
"settings.applications.notification.icon-updated.title": {
"message": "Icon updated"
},
"settings.applications.secret.disclaimer": {
"message": "Save your secret now, it will be hidden after you leave this page!"
},
"settings.billing.modal.cancel.action": {
"message": "Cancel subscription"
},

View File

@@ -2,29 +2,33 @@
<div class="universal-card">
<ConfirmModal
ref="modal_confirm"
title="Are you sure you want to delete this application?"
description="This will permanently delete this application and revoke all access tokens. (forever!)"
proceed-label="Delete this application"
:title="formatMessage(messages.deleteConfirmTitle)"
:description="formatMessage(messages.deleteConfirmDescription)"
:proceed-label="formatMessage(messages.deleteConfirmButton)"
@proceed="removeApp(editingId)"
/>
<Modal ref="appModal" header="Application information">
<Modal ref="appModal" :header="formatMessage(messages.modalHeader)">
<div class="universal-modal">
<label for="app-name"><span class="label__title">Name</span> </label>
<label for="app-name"
><span class="label__title">{{ formatMessage(messages.nameLabel) }}</span>
</label>
<input
id="app-name"
v-model="name"
maxlength="2048"
type="text"
autocomplete="off"
placeholder="Enter the application's name..."
:placeholder="formatMessage(messages.namePlaceholder)"
/>
<label v-if="editingId" for="app-icon"><span class="label__title">Icon</span> </label>
<label v-if="editingId" for="app-icon"
><span class="label__title">{{ formatMessage(messages.iconLabel) }}</span>
</label>
<div v-if="editingId" class="icon-submission">
<Avatar size="md" :src="icon" />
<FileInput
:max-size="262144"
class="btn"
prompt="Upload icon"
:prompt="formatMessage(messages.uploadIcon)"
accept="image/png,image/jpeg,image/gif,image/webp"
@change="onImageSelection"
>
@@ -32,7 +36,7 @@
</FileInput>
</div>
<label v-if="editingId" for="app-url">
<span class="label__title">URL</span>
<span class="label__title">{{ formatMessage(messages.urlLabel) }}</span>
</label>
<input
v-if="editingId"
@@ -41,10 +45,10 @@
maxlength="255"
type="url"
autocomplete="off"
placeholder="https://example.com"
:placeholder="formatMessage(messages.urlPlaceholder)"
/>
<label v-if="editingId" for="app-description">
<span class="label__title">Description</span>
<span class="label__title">{{ formatMessage(messages.descriptionLabel) }}</span>
</label>
<textarea
v-if="editingId"
@@ -54,19 +58,33 @@
maxlength="255"
type="text"
autocomplete="off"
placeholder="Enter the application's description..."
:placeholder="formatMessage(messages.descriptionPlaceholder)"
/>
<label for="app-scopes"><span class="label__title">Scopes</span> </label>
<div id="app-scopes" class="checkboxes">
<Checkbox
v-for="scope in scopeList"
:key="scope"
:label="scopesToLabels(getScopeValue(scope)).join(', ')"
:model-value="hasScope(scopesVal, scope)"
@update:model-value="() => (scopesVal = toggleScope(scopesVal, scope))"
/>
<label for="app-scopes"
><span class="label__title">{{ formatMessage(messages.scopesLabel) }}</span>
</label>
<div
id="app-scopes"
class="scope-items mt-2 grid grid-cols-1 gap-x-6 gap-y-4 min-[600px]:grid-cols-2"
>
<div v-for="category in scopeCategories" :key="category.name" class="flex flex-col gap-2">
<h4 class="m-0 border-b border-divider pb-1 text-base font-bold text-contrast">
{{ category.name }}
</h4>
<div class="flex flex-col gap-2">
<Checkbox
v-for="scope in category.scopes"
:key="scope"
:label="scopesToLabels(getScopeValue(scope)).join(', ')"
:model-value="hasScope(scopesVal, scope)"
@update:model-value="() => (scopesVal = toggleScope(scopesVal, scope))"
/>
</div>
</div>
</div>
<label for="app-redirect-uris"><span class="label__title">Redirect uris</span> </label>
<label for="app-redirect-uris" class="mt-4"
><span class="label__title">{{ formatMessage(messages.redirectUrisLabel) }}</span>
</label>
<div class="uri-input-list">
<div v-for="(_, index) in redirectUris" :key="index">
<div class="input-group url-input-group-fixes">
@@ -75,7 +93,7 @@
maxlength="2048"
type="url"
autocomplete="off"
placeholder="https://example.com/auth/callback"
:placeholder="formatMessage(messages.redirectUriPlaceholder)"
/>
<Button v-if="index !== 0" icon-only @click="() => redirectUris.splice(index, 1)">
<TrashIcon />
@@ -86,13 +104,13 @@
icon-only
@click="() => redirectUris.push('')"
>
<PlusIcon /> Add more
<PlusIcon /> {{ formatMessage(messages.addMore) }}
</Button>
</div>
</div>
<div v-if="redirectUris.length <= 0">
<Button color="primary" icon-only @click="() => redirectUris.push('')">
<PlusIcon /> Add a redirect uri
<PlusIcon /> {{ formatMessage(messages.addRedirectUri) }}
</Button>
</div>
</div>
@@ -100,7 +118,7 @@
<div class="submit-row input-group push-right">
<button class="iconified-button" @click="$refs.appModal.hide()">
<XIcon />
Cancel
{{ formatMessage(messages.cancel) }}
</button>
<button
v-if="editingId"
@@ -110,7 +128,7 @@
@click="editApp"
>
<SaveIcon />
Save changes
{{ formatMessage(messages.saveChanges) }}
</button>
<button
v-else
@@ -120,7 +138,7 @@
@click="createApp"
>
<PlusIcon />
Create App
{{ formatMessage(messages.createApp) }}
</button>
</div>
</div>
@@ -144,13 +162,17 @@
}
"
>
<PlusIcon /> New Application
<PlusIcon /> {{ formatMessage(messages.newApplication) }}
</button>
</div>
<p>
Applications can be used to authenticate Modrinth's users with your products. For more
information, see
<a class="text-link" href="https://docs.modrinth.com">Modrinth's API documentation</a>.
<IntlFormatted :message-id="messages.descriptionIntro">
<template #docs-link="{ children }">
<a class="text-link" href="https://docs.modrinth.com">
<component :is="() => normalizeChildren(children)" />
</a>
</template>
</IntlFormatted>
</p>
<div v-for="app in usersApps" :key="app.id" class="universal-card recessed token mt-4">
<div class="token-info">
@@ -158,25 +180,31 @@
<Avatar size="sm" :src="app.icon_url" />
<div>
<h2 class="token-title">{{ app.name }}</h2>
<div>Created on {{ new Date(app.created).toLocaleDateString() }}</div>
<div>
{{
formatMessage(messages.createdOn, {
date: new Date(app.created).toLocaleDateString(),
})
}}
</div>
</div>
</div>
<div>
<label for="token-information">
<span class="label__title">About</span>
<span class="label__title">{{ formatMessage(messages.aboutLabel) }}</span>
</label>
<div class="token-content">
<div>
Client ID
{{ formatMessage(messages.clientId) }}
<CopyCode :text="app.id" />
</div>
<div v-if="!!clientCreatedInState(app.id)">
<div>
Client Secret
{{ formatMessage(messages.clientSecret) }}
<CopyCode :text="clientCreatedInState(app.id)?.client_secret" />
</div>
<div class="secret_disclaimer">
<i> Save your secret now, it will be hidden after you leave this page! </i>
<i>{{ formatMessage(messages.secretDisclaimer) }}</i>
</div>
</div>
</div>
@@ -196,7 +224,7 @@
"
>
<EditIcon />
Edit
{{ formatMessage(messages.edit) }}
</Button>
<Button
color="danger"
@@ -209,7 +237,7 @@
"
>
<TrashIcon />
Delete
{{ formatMessage(messages.delete) }}
</Button>
</div>
</div>
@@ -224,8 +252,11 @@ import {
commonSettingsMessages,
ConfirmModal,
CopyCode,
defineMessages,
FileInput,
injectNotificationManager,
IntlFormatted,
normalizeChildren,
useVIntl,
} from '@modrinth/ui'
@@ -233,6 +264,7 @@ import Modal from '~/components/ui/Modal.vue'
import {
getScopeValue,
hasScope,
scopeCategoryMessages,
scopeList,
toggleScope,
useScopes,
@@ -249,8 +281,196 @@ useHead({
title: 'Applications - Modrinth',
})
const messages = defineMessages({
modalHeader: {
id: 'settings.applications.modal.header',
defaultMessage: 'Application information',
},
deleteConfirmTitle: {
id: 'settings.applications.delete.confirm.title',
defaultMessage: 'Are you sure you want to delete this application?',
},
deleteConfirmDescription: {
id: 'settings.applications.delete.confirm.description',
defaultMessage:
'This will permanently delete this application and revoke all access tokens. (forever!)',
},
deleteConfirmButton: {
id: 'settings.applications.delete.confirm.button',
defaultMessage: 'Delete this application',
},
nameLabel: {
id: 'settings.applications.field.name',
defaultMessage: 'Name',
},
namePlaceholder: {
id: 'settings.applications.field.name.placeholder',
defaultMessage: "Enter the application's name...",
},
iconLabel: {
id: 'settings.applications.field.icon',
defaultMessage: 'Icon',
},
uploadIcon: {
id: 'settings.applications.button.upload-icon',
defaultMessage: 'Upload icon',
},
urlLabel: {
id: 'settings.applications.field.url',
defaultMessage: 'URL',
},
urlPlaceholder: {
id: 'settings.applications.field.url.placeholder',
defaultMessage: 'https://example.com',
},
descriptionLabel: {
id: 'settings.applications.field.description',
defaultMessage: 'Description',
},
descriptionPlaceholder: {
id: 'settings.applications.field.description.placeholder',
defaultMessage: "Enter the application's description...",
},
scopesLabel: {
id: 'settings.applications.field.scopes',
defaultMessage: 'Scopes',
},
redirectUrisLabel: {
id: 'settings.applications.field.redirect-uris',
defaultMessage: 'Redirect uris',
},
redirectUriPlaceholder: {
id: 'settings.applications.field.redirect-uri.placeholder',
defaultMessage: 'https://example.com/auth/callback',
},
addMore: {
id: 'settings.applications.button.add-more',
defaultMessage: 'Add more',
},
addRedirectUri: {
id: 'settings.applications.button.add-redirect-uri',
defaultMessage: 'Add a redirect uri',
},
cancel: {
id: 'settings.applications.button.cancel',
defaultMessage: 'Cancel',
},
saveChanges: {
id: 'settings.applications.button.save-changes',
defaultMessage: 'Save changes',
},
createApp: {
id: 'settings.applications.button.create',
defaultMessage: 'Create app',
},
newApplication: {
id: 'settings.applications.button.new',
defaultMessage: 'New application',
},
descriptionIntro: {
id: 'settings.applications.description.intro',
defaultMessage:
"Applications can be used to authenticate Modrinth's users with your products. For more information, see <docs-link>Modrinth's API documentation</docs-link>.",
},
aboutLabel: {
id: 'settings.applications.about',
defaultMessage: 'About',
},
clientId: {
id: 'settings.applications.client-id',
defaultMessage: 'Client ID',
},
clientSecret: {
id: 'settings.applications.client-secret',
defaultMessage: 'Client secret',
},
secretDisclaimer: {
id: 'settings.applications.secret.disclaimer',
defaultMessage: 'Save your secret now, it will be hidden after you leave this page!',
},
createdOn: {
id: 'settings.applications.created-on',
defaultMessage: 'Created on {date}',
},
edit: {
id: 'settings.applications.button.edit',
defaultMessage: 'Edit',
},
delete: {
id: 'settings.applications.button.delete',
defaultMessage: 'Delete',
},
iconUpdatedTitle: {
id: 'settings.applications.notification.icon-updated.title',
defaultMessage: 'Icon updated',
},
iconUpdatedDescription: {
id: 'settings.applications.notification.icon-updated.description',
defaultMessage: 'Your application icon has been updated.',
},
errorTitle: {
id: 'settings.applications.notification.error.title',
defaultMessage: 'An error occurred',
},
})
const { scopesToLabels } = useScopes()
const scopeCategories = computed(() => {
return [
{
name: formatMessage(scopeCategoryMessages.categoryUserAccount),
scopes: scopeList.filter((s) => s.startsWith('USER_')),
},
{
name: formatMessage(scopeCategoryMessages.categoryProjects),
scopes: scopeList.filter((s) => s.startsWith('PROJECT_')),
},
{
name: formatMessage(scopeCategoryMessages.categoryVersions),
scopes: scopeList.filter((s) => s.startsWith('VERSION_')),
},
{
name: formatMessage(scopeCategoryMessages.categoryCollections),
scopes: scopeList.filter((s) => s.startsWith('COLLECTION_')),
},
{
name: formatMessage(scopeCategoryMessages.categoryOrganizations),
scopes: scopeList.filter((s) => s.startsWith('ORGANIZATION_')),
},
{
name: formatMessage(scopeCategoryMessages.categoryReports),
scopes: scopeList.filter((s) => s.startsWith('REPORT_')),
},
{
name: formatMessage(scopeCategoryMessages.categoryThreads),
scopes: scopeList.filter((s) => s.startsWith('THREAD_')),
},
{
name: formatMessage(scopeCategoryMessages.categoryPats),
scopes: scopeList.filter((s) => s.startsWith('PAT_')),
},
{
name: formatMessage(scopeCategoryMessages.categorySessions),
scopes: scopeList.filter((s) => s.startsWith('SESSION_')),
},
{
name: formatMessage(scopeCategoryMessages.categoryNotifications),
scopes: scopeList.filter((s) => s.startsWith('NOTIFICATION_')),
},
{
name: formatMessage(scopeCategoryMessages.categoryPayouts),
scopes: scopeList.filter((s) => s.startsWith('PAYOUTS_')),
},
{
name: formatMessage(scopeCategoryMessages.categoryAnalytics),
scopes: scopeList.filter(
(s) => s.startsWith('ANALYTICS') || s.startsWith('PERFORM_ANALYTICS'),
),
},
].filter((c) => c.scopes.length > 0)
})
const appModal = ref()
// Any apps created in the current state will be stored here
@@ -347,8 +567,8 @@ async function onImageSelection(files) {
}
addNotification({
title: 'Icon updated',
text: 'Your application icon has been updated.',
title: formatMessage(messages.iconUpdatedTitle),
text: formatMessage(messages.iconUpdatedDescription),
type: 'success',
})
}
@@ -377,7 +597,7 @@ async function createApp() {
await refresh()
} catch (err) {
addNotification({
title: 'An error occurred',
title: formatMessage(messages.errorTitle),
text: err.data ? err.data.description : err,
type: 'error',
})
@@ -445,7 +665,7 @@ async function editApp() {
appModal.value.hide()
} catch (err) {
addNotification({
title: 'An error occurred',
title: formatMessage(messages.errorTitle),
text: err.data ? err.data.description : err,
type: 'error',
})
@@ -468,7 +688,7 @@ async function removeApp() {
editingId.value = null
} catch (err) {
addNotification({
title: 'An error occurred',
title: formatMessage(messages.errorTitle),
text: err.data ? err.data.description : err,
type: 'error',
})
@@ -500,17 +720,10 @@ async function removeApp() {
flex-basis: 24rem !important;
}
}
.checkboxes {
display: grid;
column-gap: 0.5rem;
@media screen and (min-width: 432px) {
grid-template-columns: repeat(2, 1fr);
}
@media screen and (min-width: 800px) {
grid-template-columns: repeat(3, 1fr);
}
.scope-items :deep(.checkbox-outer) {
white-space: nowrap !important;
justify-content: flex-start !important;
}
.icon-submission {

View File

@@ -29,16 +29,26 @@
<label for="pat-scopes">
<span class="label__title">{{ formatMessage(commonMessages.scopesLabel) }}</span>
</label>
<div id="pat-scopes" class="checkboxes">
<Checkbox
v-for="scope in scopeList"
:key="scope"
:label="scopesToLabels(getScopeValue(scope)).join(', ')"
:model-value="hasScope(scopesVal, scope)"
@update:model-value="scopesVal = toggleScope(scopesVal, scope)"
/>
<div
id="pat-scopes"
class="scope-items mt-2 grid grid-cols-1 gap-x-6 gap-y-4 min-[600px]:grid-cols-2"
>
<div v-for="category in scopeCategories" :key="category.name" class="flex flex-col gap-2">
<h4 class="m-0 border-b border-divider pb-1 text-base font-bold text-contrast">
{{ category.name }}
</h4>
<div class="flex flex-col gap-2">
<Checkbox
v-for="scope in category.scopes"
:key="scope"
:label="scopesToLabels(getScopeValue(scope)).join(', ')"
:model-value="hasScope(scopesVal, scope)"
@update:model-value="scopesVal = toggleScope(scopesVal, scope)"
/>
</div>
</div>
</div>
<label for="pat-name">
<label for="pat-name" class="mt-4">
<span class="label__title">{{ formatMessage(createModalMessages.expiresLabel) }}</span>
</label>
<input id="pat-name" v-model="expires" type="date" />
@@ -171,7 +181,7 @@
</template>
</div>
</div>
<div class="input-group">
<div class="token-actions ml-auto flex flex-col gap-2">
<button
class="iconified-button raised-button"
@click="
@@ -220,6 +230,7 @@ import Modal from '~/components/ui/Modal.vue'
import {
getScopeValue,
hasScope,
scopeCategoryMessages,
scopeList,
toggleScope,
useScopes,
@@ -338,6 +349,61 @@ const displayPats = computed(() => {
return pats.value.toSorted((a, b) => new Date(b.created) - new Date(a.created))
})
const scopeCategories = computed(() => {
return [
{
name: formatMessage(scopeCategoryMessages.categoryUserAccount),
scopes: scopeList.filter((s) => s.startsWith('USER_')),
},
{
name: formatMessage(scopeCategoryMessages.categoryProjects),
scopes: scopeList.filter((s) => s.startsWith('PROJECT_')),
},
{
name: formatMessage(scopeCategoryMessages.categoryVersions),
scopes: scopeList.filter((s) => s.startsWith('VERSION_')),
},
{
name: formatMessage(scopeCategoryMessages.categoryCollections),
scopes: scopeList.filter((s) => s.startsWith('COLLECTION_')),
},
{
name: formatMessage(scopeCategoryMessages.categoryOrganizations),
scopes: scopeList.filter((s) => s.startsWith('ORGANIZATION_')),
},
{
name: formatMessage(scopeCategoryMessages.categoryReports),
scopes: scopeList.filter((s) => s.startsWith('REPORT_')),
},
{
name: formatMessage(scopeCategoryMessages.categoryThreads),
scopes: scopeList.filter((s) => s.startsWith('THREAD_')),
},
{
name: formatMessage(scopeCategoryMessages.categoryPats),
scopes: scopeList.filter((s) => s.startsWith('PAT_')),
},
{
name: formatMessage(scopeCategoryMessages.categorySessions),
scopes: scopeList.filter((s) => s.startsWith('SESSION_')),
},
{
name: formatMessage(scopeCategoryMessages.categoryNotifications),
scopes: scopeList.filter((s) => s.startsWith('NOTIFICATION_')),
},
{
name: formatMessage(scopeCategoryMessages.categoryPayouts),
scopes: scopeList.filter((s) => s.startsWith('PAYOUTS_')),
},
{
name: formatMessage(scopeCategoryMessages.categoryAnalytics),
scopes: scopeList.filter(
(s) => s.startsWith('ANALYTICS') || s.startsWith('PERFORM_ANALYTICS'),
),
},
].filter((c) => c.scopes.length > 0)
})
async function createPat() {
startLoading()
loading.value = true
@@ -407,17 +473,9 @@ async function removePat(id) {
}
</script>
<style lang="scss" scoped>
.checkboxes {
display: grid;
column-gap: 0.5rem;
@media screen and (min-width: 432px) {
grid-template-columns: repeat(2, 1fr);
}
@media screen and (min-width: 800px) {
grid-template-columns: repeat(3, 1fr);
}
.scope-items :deep(.checkbox-outer) {
white-space: nowrap !important;
justify-content: flex-start !important;
}
.token {
@@ -428,10 +486,6 @@ async function removePat(id) {
@media screen and (min-width: 800px) {
flex-direction: row;
align-items: center;
.input-group {
margin-left: auto;
}
}
}
</style>