0
0
Fork 0
mirror of https://github.com/discourse/discourse.git synced 2026-08-07 13:19:19 +08:00
discourse/app/assets/stylesheets/common/base/modal.scss
Keegan George abe5700838
FEATURE: Allow inviting new users directly as admins (#41748)
**Previously**, getting a second admin onto a site took many steps:
create an invite, wait for the person to sign up, find their profile,
grant admin, and confirm via email.

**In this update**, a redesigned invite modal (gated behind the
`enable_admin_invites` upcoming change) lets admins invite someone as an
admin by email in one step. The invitee becomes a moderator on signup
and an admin once the inviter confirms via the existing admin
confirmation email. The admin onboarding panel's "Invite collaborators"
step now opens this flow with the admin option preselected.

| Invite members | Advanced options | Email delivery |
| --- | --- | --- |
|
![invite-members.png](https://github.com/discourse/discourse/releases/download/_gh-attach-assets/invite-members.png)
|
![invite-members-advanced.png](https://github.com/discourse/discourse/releases/download/_gh-attach-assets/invite-members-advanced.png)
|
![invite-members-email.png](https://github.com/discourse/discourse/releases/download/_gh-attach-assets/invite-members-email.png)
|

| Invite admins | Advanced options | Invite sent |
| --- | --- | --- |
|
![invite-admins.png](https://github.com/discourse/discourse/releases/download/_gh-attach-assets/invite-admins.png)
|
![invite-admins-advanced.png](https://github.com/discourse/discourse/releases/download/_gh-attach-assets/invite-admins-advanced.png)
|
![admin-invite-sent.png](https://github.com/discourse/discourse/releases/download/_gh-attach-assets/admin-invite-sent.png)
|

| Link created | Email invitation sent | Onboarding panel |
| --- | --- | --- |
|
![invite-created.png](https://github.com/discourse/discourse/releases/download/_gh-attach-assets/invite-created.png)
|
![invitation-sent.png](https://github.com/discourse/discourse/releases/download/_gh-attach-assets/invitation-sent.png)
|
![onboarding-banner.png](https://github.com/discourse/discourse/releases/download/_gh-attach-assets/onboarding-banner.png)
|
2026-07-16 14:59:24 -07:00

363 lines
6.4 KiB
SCSS
Vendored

@use "lib/viewport";
html.modal-open {
// prevents bg scrolling when modal is open
overflow: hidden;
scrollbar-gutter: stable;
}
html.keyboard-visible.mobile-view {
.d-modal {
max-height: calc(var(--composer-vh, 1dvh) * 100);
height: calc(var(--composer-vh, 1dvh) * 100);
bottom: 0;
}
.d-modal__container {
max-height: calc(var(--composer-vh, 1dvh) * 100);
height: calc(var(--composer-vh, 1dvh) * 100);
}
}
html:not(.keyboard-visible).mobile-device {
.d-modal__container {
padding-bottom: env(safe-area-inset-bottom, 1rem);
}
}
.d-modal {
pointer-events: none; // Allow clicks through wrappers so they hit the adjacent backdrop element
display: flex;
align-items: center;
width: 100%;
height: 100%;
position: fixed;
top: 0;
z-index: z("modal", "content");
overflow: auto;
@include viewport.until(sm) {
align-items: flex-end;
}
@include viewport.from(sm) {
--modal-max-width: 600px;
--modal-width: 30em; // set in ems to scale with user font-size
--modal-min-width: 400px;
}
&__container {
display: flex;
flex-direction: column;
pointer-events: auto;
box-sizing: border-box;
border-radius: var(--d-border-radius);
margin: 0 auto;
background-color: var(--secondary);
box-shadow: var(--shadow-modal);
max-height: var(--modal-max-height, 80vh); // unset, optional theme utility
@include viewport.until(sm) {
// this is a hack to prevent issues on safari with transforms
position: fixed;
width: 100%;
max-width: 100%;
max-height: calc(var(--composer-vh, 1dvh) * 85);
&.is-entering {
@include d-animation(modal-enter-mobile, 250ms, ease);
}
&.is-exiting {
@include d-animation(modal-exit-mobile, 250ms, ease);
}
}
@include viewport.from(sm) {
max-width: var(--modal-max-width);
min-width: var(--modal-min-width);
min-height: var(--modal-min-height); // unset, optional theme utility
.d-modal.--large & {
max-width: 800px;
}
.d-modal.--max & {
max-width: 90vw;
}
.d-modal.has-search & {
height: 80vh; // fixed height so the modal doesnt jump when the search results appear/filter
}
&.is-entering {
@include d-animation(
modal-enter-desktop,
250ms,
cubic-bezier(0.4, 0, 0.2, 1)
);
}
&.is-exiting {
@include d-animation(
modal-exit-desktop,
250ms,
cubic-bezier(0.4, 0, 0.2, 1)
);
}
}
}
&__header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 0.5rem;
border-bottom: 1px solid var(--content-border-color);
&:not(.--has-primary-action) {
padding: 0.5rem 0.5rem 0.5rem 1.5rem; // offset for close button padding
}
.modal-close {
margin-left: auto;
align-self: start;
.d-icon {
font-size: var(--font-up-2);
color: var(--primary-high);
}
&:focus-visible {
.d-icon {
color: var(--primary);
}
}
}
}
.d-modal__primary-action,
.d-modal__dismiss-action {
.btn {
text-transform: capitalize;
}
}
&__title-text {
font-size: var(--font-up-1-rem);
line-height: var(--line-height-medium);
@include viewport.from(sm) {
font-size: var(--font-up-3);
}
}
&__title-text,
&__subtitle-text {
margin: 0;
}
&__body {
overflow-y: auto;
padding: 1rem 1.5rem;
box-sizing: border-box;
&.empty {
display: none;
}
input {
width: auto;
&[type="text"],
&[type="password"],
&[type="number"],
&[type="email"] {
width: 100%;
}
}
textarea {
width: 100%;
&:not(.--expandable) {
height: 80px;
}
}
.password-confirmation {
display: none;
}
section.field {
padding: 0.25em 0;
margin-bottom: 5px;
&.with-items {
display: flex;
align-items: flex-start;
}
.field-item {
display: inline-block;
margin-right: 10px;
}
}
pre code {
white-space: pre-wrap;
max-width: 100%;
}
}
&__footer {
display: flex;
flex-wrap: wrap;
align-items: center;
padding: 1rem 1.5rem;
border-top: 1px solid var(--content-border-color);
gap: 0.5rem;
@include viewport.until(sm) {
&__footer {
margin-top: auto;
.--stacked & {
flex-direction: column;
align-items: stretch;
}
}
}
}
&__backdrop {
user-select: none;
position: fixed;
top: 0;
right: 0;
bottom: 0;
left: 0;
z-index: z("modal", "overlay");
background-color: #000;
animation: fade 0.3s forwards;
&.fade {
opacity: 0;
}
@media (prefers-reduced-motion) {
animation-duration: 0s;
}
&.is-exiting {
@include d-animation(
backdrop-fade-out,
250ms,
cubic-bezier(0.4, 0, 0.2, 1)
);
}
}
#modal-alert {
padding-left: 1.5rem;
}
.ios-device & {
&__footer {
margin-top: auto;
}
}
// fixes modal placement on Android when keyboard is visible
html.keyboard-visible:not(.ios-device) & {
height: calc(100% - env(keyboard-inset-height));
.d-modal__container {
max-height: 100%;
min-height: 100%;
height: 100%;
}
}
pre code {
white-space: pre-wrap;
max-width: var(--modal-max-width);
}
@include viewport.from(sm) {
.category-chooser {
width: 50%;
}
}
.input-hint-text {
margin-left: 0.5em;
color: var(--secondary-high);
}
}
// fade in
@keyframes fade {
from {
opacity: 0;
}
to {
opacity: 0.6;
}
}
// Modal animations
@keyframes modal-enter-desktop {
from {
transform: scale(0.7);
opacity: 0;
}
to {
transform: scale(1);
opacity: 1;
}
}
@keyframes modal-exit-desktop {
from {
transform: scale(1);
opacity: 1;
}
to {
transform: scale(0.7);
opacity: 0;
}
}
@keyframes modal-enter-mobile {
from {
transform: translateY(100%);
}
to {
transform: translateY(0);
}
}
@keyframes modal-exit-mobile {
from {
transform: translateY(0);
}
to {
transform: translateY(100%);
}
}
@keyframes backdrop-fade-out {
from {
opacity: 0.6;
}
to {
opacity: 0;
}
}