diff --git a/apps/cyberstorm-remix/app/commonComponents/Footer/Footer.css b/apps/cyberstorm-remix/app/commonComponents/Footer/Footer.css
index 086c2db7f..b17a32dd2 100644
--- a/apps/cyberstorm-remix/app/commonComponents/Footer/Footer.css
+++ b/apps/cyberstorm-remix/app/commonComponents/Footer/Footer.css
@@ -119,16 +119,10 @@
margin-right: 3.5rem;
padding: 5rem 0 0 5rem;
- > h2 {
- z-index: 1;
- }
-
&::before {
position: absolute;
right: 0;
bottom: 0;
-
- /* z-index: -1; */
height: 100%;
background-image: linear-gradient(
165deg,
@@ -143,7 +137,6 @@
}
.manager-ad__description {
- z-index: 1;
flex-shrink: 0;
max-width: 475px;
padding-bottom: var(--space-16);
@@ -171,10 +164,6 @@
min-height: 0;
}
- .manager-ad__get-manager-button {
- z-index: 1;
- }
-
.footnote {
display: flex;
align-items: center;
diff --git a/apps/cyberstorm-remix/app/commonComponents/Navigation/Navigation.tsx b/apps/cyberstorm-remix/app/commonComponents/Navigation/Navigation.tsx
index b474d64d0..a1fe7817b 100644
--- a/apps/cyberstorm-remix/app/commonComponents/Navigation/Navigation.tsx
+++ b/apps/cyberstorm-remix/app/commonComponents/Navigation/Navigation.tsx
@@ -231,7 +231,7 @@ export function DesktopLoginPopover() {
return (
- Review Package
-
+
+
+
+
+ Review Package
+
+ }
+ titleContent="Review Package"
+ >
+
Changes might take several minutes to show publicly! Info shown below
is always up to date.
@@ -730,8 +744,8 @@ function ReviewPackageForm(props: {
rootClasses="review-package__textarea"
/>
-
-
+
+
@@ -771,8 +785,8 @@ function ReviewPackageForm(props: {
>
Approve
-
-
+
+
);
}
@@ -1003,32 +1017,15 @@ function managementTools(
{packagePermissions.permissions.can_moderate ? (
@@ -359,9 +345,12 @@ function DeletePackageWikiPageForm(props: {
});
return (
-
-
Delete wiki page
-
+
Delete page}
+ titleContent="Delete wiki page"
+ >
+
You are about to delete wiki page{" "}
-
-
+
+
@@ -391,8 +380,8 @@ function DeletePackageWikiPageForm(props: {
>
Delete
-
-
+
+
);
}
diff --git a/apps/cyberstorm-remix/app/settings/teams/Teams.css b/apps/cyberstorm-remix/app/settings/teams/Teams.css
index 4df457ed8..bd5d333de 100644
--- a/apps/cyberstorm-remix/app/settings/teams/Teams.css
+++ b/apps/cyberstorm-remix/app/settings/teams/Teams.css
@@ -21,7 +21,6 @@
.create-team-form__body {
display: flex;
flex-direction: column;
- gap: var(--gap-2xl);
align-items: flex-start;
align-self: stretch;
}
diff --git a/apps/cyberstorm-remix/app/settings/teams/Teams.tsx b/apps/cyberstorm-remix/app/settings/teams/Teams.tsx
index c36139300..2089fe39a 100644
--- a/apps/cyberstorm-remix/app/settings/teams/Teams.tsx
+++ b/apps/cyberstorm-remix/app/settings/teams/Teams.tsx
@@ -12,7 +12,7 @@ import {
} from "@thunderstore/cyberstorm";
import { faPlus } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
-import { useReducer } from "react";
+import { useReducer, useState } from "react";
import { PageHeader } from "~/commonComponents/PageHeader/PageHeader";
import { useToast } from "@thunderstore/cyberstorm/src/newComponents/Toast/Provider";
import {
@@ -72,23 +72,7 @@ export default function Teams() {
Teams
Manage your teams
-
- Create Team
-
-
-
-
- }
- >
-
-
+
{currentUser?.teams_full && currentUser.teams_full.length !== 0 ? (
@@ -198,11 +182,26 @@ function CreateTeamForm(props: { config: () => RequestConfig }) {
},
});
+ const [open, setOpen] = useState(false);
+
return (
-
-
Create team
-
-
+
+ Create Team
+
+
+
+
+ }
+ >
+ Create Team
+
+
Enter the name of the team you wish to create. Team names can contain
the characters a-z A-Z 0-9 _ and must not start or end with an _.
@@ -221,12 +220,17 @@ function CreateTeamForm(props: { config: () => RequestConfig }) {
id="teamName"
/>
-
-
-
+
+
+ {
+ strongForm.submit().then(() => setOpen(false));
+ }}
+ csVariant="accent"
+ >
Create
-
-
+
+
);
}
diff --git a/apps/cyberstorm-remix/app/settings/teams/team/tabs/Members/Members.tsx b/apps/cyberstorm-remix/app/settings/teams/team/tabs/Members/Members.tsx
index b996c55e9..4a63fe661 100644
--- a/apps/cyberstorm-remix/app/settings/teams/team/tabs/Members/Members.tsx
+++ b/apps/cyberstorm-remix/app/settings/teams/team/tabs/Members/Members.tsx
@@ -29,7 +29,7 @@ import { getSessionTools } from "cyberstorm/security/publicEnvVariables";
import { useToast } from "@thunderstore/cyberstorm/src/newComponents/Toast/Provider";
import { type SelectOption } from "@thunderstore/cyberstorm/src/newComponents/Select/Select";
import { useStrongForm } from "cyberstorm/utils/StrongForm/useStrongForm";
-import { useReducer } from "react";
+import { useReducer, useState } from "react";
// REMIX TODO: Add check for "user has permission to see this page"
export async function clientLoader({ params }: LoaderFunctionArgs) {
@@ -151,34 +151,13 @@ export default function Members() {
},
{
value: (
-
-
-
-
- Kick
-
- }
- popoverId={`memberKickModal-${member.username}-${index}`}
- >
-
-
+
),
sortValue: 0,
},
@@ -191,27 +170,11 @@ export default function Members() {
Teams
Manage your teams
-
- Add Member
-
-
-
-
- }
- >
-
-
+
RequestConfig;
}) {
const toast = useToast();
+ const [open, setOpen] = useState(false);
function formFieldUpdateAction(
state: TeamAddMemberRequestData,
@@ -283,6 +247,7 @@ function AddTeamMemberForm(props: {
children: `Team member added`,
duration: 4000,
});
+ setOpen(false);
},
onSubmitError: (error) => {
toast.addToast({
@@ -294,9 +259,21 @@ function AddTeamMemberForm(props: {
});
return (
-
-
Add Member
-
+
+ Add Member
+
+
+
+
+ }
+ >
+
Enter the username of the user you wish to add to the team{" "}
{props.teamName}.
@@ -335,26 +312,27 @@ function AddTeamMemberForm(props: {
/>
-
-
+
+
Add member
-
-
+
+
);
}
AddTeamMemberForm.displayName = "AddTeamMemberForm";
function RemoveTeamMemberForm(props: {
+ indexKey?: string;
userName: string;
teamName: string;
updateTrigger: () => Promise
;
config: () => RequestConfig;
- popoverTarget: string;
}) {
const toast = useToast();
+ const [open, setOpen] = useState(false);
const kickMemberAction = ApiAction({
endpoint: teamRemoveMember,
@@ -376,9 +354,21 @@ function RemoveTeamMemberForm(props: {
});
return (
-
-
Confirm member removal
-
+
+
+
+
+ Kick
+
+ }
+ >
+
You are about to kick member{" "}
@@ -386,14 +376,11 @@ function RemoveTeamMemberForm(props: {
.
-
-
-
- Cancel
-
+
+
+
+ Cancel
+
@@ -402,13 +389,15 @@ function RemoveTeamMemberForm(props: {
params: { team_name: props.teamName, username: props.userName },
queryParams: {},
data: {},
+ }).then(() => {
+ setOpen(false);
})
}
>
Kick member
-
-
+
+
);
}
diff --git a/apps/cyberstorm-remix/app/settings/teams/team/tabs/ServiceAccounts/ServiceAccounts.tsx b/apps/cyberstorm-remix/app/settings/teams/team/tabs/ServiceAccounts/ServiceAccounts.tsx
index 549b321e4..62fd6402a 100644
--- a/apps/cyberstorm-remix/app/settings/teams/team/tabs/ServiceAccounts/ServiceAccounts.tsx
+++ b/apps/cyberstorm-remix/app/settings/teams/team/tabs/ServiceAccounts/ServiceAccounts.tsx
@@ -7,6 +7,7 @@ import {
NewIcon,
NewTextInput,
Heading,
+ CodeBox,
} from "@thunderstore/cyberstorm";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { faPlus, faTrash } from "@fortawesome/free-solid-svg-icons";
@@ -121,15 +122,9 @@ export default function ServiceAccounts() {
value: (
+
@@ -138,45 +133,40 @@ export default function ServiceAccounts() {
}
csSize="small"
>
-
-
- Remove service account
-
-
-
- This cannot be undone! Related API token will stop working
- immediately if the service account is removed.
-
-
- You are about to remove service account{" "}
-
- {serviceAccount.name}
-
- .
-
+
+
+ This cannot be undone! Related API token will stop working
+ immediately if the service account is removed.
+
+
+ You are about to remove service account{" "}
+
+ {serviceAccount.name}
+
+ .
-
- {
- removeServiceAccountAction({
- config: outletContext.requestConfig,
- params: {
- team_name: teamName,
- uuid: serviceAccount.identifier,
- },
- queryParams: {},
- data: {},
- });
- }}
- csVariant="danger"
- >
-
-
-
- Remove service account
-
-
-
+
+
+ {
+ removeServiceAccountAction({
+ config: outletContext.requestConfig,
+ params: {
+ team_name: teamName,
+ uuid: serviceAccount.identifier,
+ },
+ queryParams: {},
+ data: {},
+ });
+ }}
+ csVariant="danger"
+ >
+
+
+
+ Remove service account
+
+
),
sortValue: 0,
@@ -184,82 +174,17 @@ export default function ServiceAccounts() {
];
});
- // Add service account stuff
- const [serviceAccountAdded, setServiceAccountAdded] = useState(false);
- const [addedServiceAccountToken, setAddedServiceAccountToken] = useState("");
- const [addedServiceAccountNickname, setAddedServiceAccountNickname] =
- useState("");
-
- function onSuccess(
- result: Awaited
>
- ) {
- setServiceAccountAdded(true);
- setAddedServiceAccountToken(result.api_token);
- setAddedServiceAccountNickname(result.nickname);
- serviceAccountRevalidate();
- }
-
return (
Service accounts
Your loyal servants
-
- Add Service Account
-
-
-
-
- }
- csSize="small"
- >
- {serviceAccountAdded ? (
-
-
-
- New service account{" "}
- {addedServiceAccountNickname} was created
- successfully. It can be used with this API token:
-
-
-
{addedServiceAccountToken}
- {/*
*/}
-
-
- Store this token securely, as it can't be retrieved
- later, and treat it as you would treat an important
- password.
-
-
-
- {
- setAddedServiceAccountToken("");
- setAddedServiceAccountNickname("");
- setServiceAccountAdded(false);
- }}
- popoverTarget="serviceAccountAdd"
- popoverTargetAction="hide"
- >
- Close
-
-
-
- ) : (
-
- )}
-
+
RequestConfig;
- onSuccess: (
- result: Awaited>
- ) => void;
+ serviceAccountRevalidate?: () => void;
}) {
+ const [open, setOpen] = useState(false);
+ const [serviceAccountAdded, setServiceAccountAdded] = useState(false);
+ const [addedServiceAccountToken, setAddedServiceAccountToken] = useState("");
+ const [addedServiceAccountNickname, setAddedServiceAccountNickname] =
+ useState("");
+
+ function onSuccess(
+ result: Awaited>
+ ) {
+ setServiceAccountAdded(true);
+ setAddedServiceAccountToken(result.api_token);
+ setAddedServiceAccountNickname(result.nickname);
+ }
+
const toast = useToast();
function formFieldUpdateAction(
@@ -326,7 +263,8 @@ function AddServiceAccountForm(props: {
>({
inputs: formInputs,
submitor,
- onSubmitSuccess: () => {
+ onSubmitSuccess: (result) => {
+ onSuccess(result);
toast.addToast({
csVariant: "success",
children: `Service account added`,
@@ -342,29 +280,69 @@ function AddServiceAccountForm(props: {
},
});
+ const handleOpenChange = (nextOpen: boolean) => {
+ setOpen(nextOpen);
+ if (!nextOpen) {
+ setServiceAccountAdded(false);
+ setAddedServiceAccountToken("");
+ setAddedServiceAccountNickname("");
+ updateFormFieldState({ field: "nickname", value: "" });
+ }
+ };
+
return (
-
-
Add service account
-
-
- Enter the nickname of the service account you wish to add to the team{" "}
- {props.teamName}
-
-
- {
- updateFormFieldState({
- field: "nickname",
- value: e.target.value,
- });
- }}
- placeholder={"ExampleName"}
- />
-
-
-
- Add Service Account
-
-
+
+ Add Service Account
+
+
+
+
+ }
+ csSize="small"
+ titleContent="Add service account"
+ >
+ {serviceAccountAdded ? (
+
+
+ New service account {addedServiceAccountNickname} was
+ created successfully. It can be used with this API token:
+
+
+
+
+
+ Store this token securely, as it can't be retrieved later, and
+ treat it as you would treat an important password.
+
+
+ ) : (
+
+
+ Enter the nickname of the service account you wish to add to the
+ team {props.teamName}
+
+
+ {
+ updateFormFieldState({
+ field: "nickname",
+ value: e.target.value,
+ });
+ }}
+ placeholder={"ExampleName"}
+ />
+
+
+ )}
+ {serviceAccountAdded ? null : (
+
+ Add Service Account
+
+ )}
+
);
}
diff --git a/apps/cyberstorm-remix/app/settings/teams/team/tabs/Settings/Settings.tsx b/apps/cyberstorm-remix/app/settings/teams/team/tabs/Settings/Settings.tsx
index 8a63937e1..a028d2ddd 100644
--- a/apps/cyberstorm-remix/app/settings/teams/team/tabs/Settings/Settings.tsx
+++ b/apps/cyberstorm-remix/app/settings/teams/team/tabs/Settings/Settings.tsx
@@ -22,7 +22,7 @@ import {
import { ApiAction } from "@thunderstore/ts-api-react-actions";
import { NotLoggedIn } from "~/commonComponents/NotLoggedIn/NotLoggedIn";
import { useStrongForm } from "cyberstorm/utils/StrongForm/useStrongForm";
-import { useReducer } from "react";
+import { useReducer, useState } from "react";
// REMIX TODO: Make sure user is redirected of this page, if the user is not logged in
export default function Settings() {
@@ -67,31 +67,13 @@ export default function Settings() {
If you are the owner of the team, you can only leave if the team has
another owner assigned.
-
-
-
-
- Leave team
-
- }
- >
-
-
+
@@ -112,30 +94,12 @@ export default function Settings() {
you need to archive a team with existing pages, contact Mythic#0001
on the Thunderstore Discord.
-
-
-
-
- Disband team
-
- }
- >
-
-
+
@@ -150,6 +114,7 @@ function LeaveTeamForm(props: {
toast: ReturnType
;
}) {
const { userName, teamName, toast, updateTrigger, config } = props;
+ const [open, setOpen] = useState(false);
const kickMemberAction = ApiAction({
endpoint: teamRemoveMember,
onSubmitSuccess: () => {
@@ -170,10 +135,25 @@ function LeaveTeamForm(props: {
});
return (
-
-
Leave team
-
-
+
+
+
+
+ Leave team
+
+ }
+ >
+
+
You are about to leave the team{" "}
{teamName}
-
-
-
+
+
+
@@ -199,8 +179,8 @@ function LeaveTeamForm(props: {
>
Leave team
-
-
+
+
);
}
@@ -214,6 +194,8 @@ function DisbandTeamForm(props: {
}) {
const { teamName, toast, updateTrigger, config } = props;
+ const [open, setOpen] = useState(false);
+
function formFieldUpdateAction(
state: TeamDisbandRequestData,
action: {
@@ -263,6 +245,7 @@ function DisbandTeamForm(props: {
duration: 4000,
});
updateTrigger();
+ setOpen(false);
},
onSubmitError: (error) => {
toast.addToast({
@@ -274,9 +257,24 @@ function DisbandTeamForm(props: {
});
return (
-
-
Disband team
-
+
+
+
+
+ Disband team
+
+ }
+ >
+
You are about to disband the team{" "}
-
-
+
+
{
@@ -311,8 +309,8 @@ function DisbandTeamForm(props: {
Disband team
-
-
+
+
);
}
diff --git a/apps/cyberstorm-remix/app/settings/user/Account/Account.tsx b/apps/cyberstorm-remix/app/settings/user/Account/Account.tsx
index 45bce990a..2e0fd86cb 100644
--- a/apps/cyberstorm-remix/app/settings/user/Account/Account.tsx
+++ b/apps/cyberstorm-remix/app/settings/user/Account/Account.tsx
@@ -7,16 +7,13 @@ import {
NewTextInput,
} from "@thunderstore/cyberstorm";
import { faTrashCan } from "@fortawesome/pro-solid-svg-icons";
-import {
- UserAccountDeleteRequestData,
- userDelete,
-} from "../../../../../../packages/thunderstore-api/src";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { NotLoggedIn } from "~/commonComponents/NotLoggedIn/NotLoggedIn";
import { type OutletContextShape } from "~/root";
import { useToast } from "@thunderstore/cyberstorm/src/newComponents/Toast/Provider";
import { useStrongForm } from "cyberstorm/utils/StrongForm/useStrongForm";
import { useReducer } from "react";
+import { userDelete } from "@thunderstore/thunderstore-api";
export default function Account() {
const outletContext = useOutletContext() as OutletContextShape;
@@ -66,6 +63,10 @@ export default function Account() {
);
}
+type UserAccountDeleteRequestData = {
+ verification: string;
+};
+
function DeleteAccountForm(props: {
currentUser: OutletContextShape["currentUser"];
requestConfig: OutletContextShape["requestConfig"];
@@ -94,11 +95,13 @@ function DeleteAccountForm(props: {
async function submitor(data: typeof formInputs): Promise
{
if (!props.currentUser || !props.currentUser.username)
throw new Error("User not logged in");
+ if (data.verification !== props.currentUser.username)
+ throw new Error("Verification input does not match username");
return await userDelete({
config: props.requestConfig,
params: { username: props.currentUser.username },
queryParams: {},
- data: { verification: data.verification },
+ data: {},
});
}
diff --git a/apps/storybook/src/stories/cyberstormComponents/Modal.css b/apps/storybook/src/stories/cyberstormComponents/Modal.css
new file mode 100644
index 000000000..1c9f25bbb
--- /dev/null
+++ b/apps/storybook/src/stories/cyberstormComponents/Modal.css
@@ -0,0 +1,13 @@
+@layer storybook-stories {
+ .custom-modal-title {
+ background-color: red;
+ }
+
+ .custom-modal-body {
+ background-color: red;
+ }
+
+ .custom-modal-footer {
+ background-color: red;
+ }
+}
diff --git a/apps/storybook/src/stories/cyberstormComponents/Modal.stories.tsx b/apps/storybook/src/stories/cyberstormComponents/Modal.stories.tsx
index a18d5cd40..388ec8010 100644
--- a/apps/storybook/src/stories/cyberstormComponents/Modal.stories.tsx
+++ b/apps/storybook/src/stories/cyberstormComponents/Modal.stories.tsx
@@ -1,11 +1,10 @@
import type { Meta, StoryObj } from "@storybook/react-vite";
import "@thunderstore/cyberstorm-theme";
-import { Modal, NewButton, type ModalProps } from "@thunderstore/cyberstorm";
+import { Modal, NewButton } from "@thunderstore/cyberstorm";
import {
ModalSizesList,
ModalVariantsList,
} from "@thunderstore/cyberstorm-theme/src/components";
-import { useEffect } from "react";
const meta = {
title: "Cyberstorm/Modal",
@@ -14,65 +13,158 @@ const meta = {
argTypes: {
csVariant: { control: "select", options: ModalVariantsList },
csSize: { control: "select", options: ModalSizesList },
+ onOpenChange: { action: "onOpenChange" },
+ open: { control: "boolean" },
+ trigger: { control: false },
+ disableTitle: { control: "boolean" },
+ disableBody: { control: "boolean" },
+ disableFooter: { control: "boolean" },
+ disableExit: { control: "boolean" },
+ disableDefaultSubComponents: { control: "boolean" },
+ titleContent: { control: "text" },
+ footerContent: { control: "text" },
+ ariaDescribedby: { control: "text" },
},
args: {
csVariant: ModalVariantsList[0],
csSize: ModalSizesList[0],
+ trigger: Open modal,
+ disableTitle: false,
+ disableBody: false,
+ disableFooter: false,
+ disableExit: false,
+ disableDefaultSubComponents: false,
+ titleContent: "Modal Title",
+ footerContent: "Modal Footer",
+ ariaDescribedby: "modal-description",
},
+ render: (args) => ,
} satisfies Meta;
export default meta;
type Story = StoryObj;
export const Default: Story = {
+ args: {},
+};
+
+export const DisabledTitleDefaultSubComponents: Story = {
args: {
- popoverId: "modal-1",
- trigger: (
-
- Open modal
-
- ),
+ disableTitle: true,
+ open: true,
+ },
+};
+export const DisabledBodyDefaultSubComponents: Story = {
+ args: {
+ disableBody: true,
+ open: true,
+ },
+};
+export const DisabledFooterDefaultSubComponents: Story = {
+ args: {
+ disableFooter: true,
+ open: true,
+ },
+};
+export const DisabledExitDefaultSubComponents: Story = {
+ args: {
+ disableExit: true,
+ open: true,
+ },
+};
+export const DisabledAllDefaultSubComponents: Story = {
+ args: {
+ disableDefaultSubComponents: true,
+ open: true,
},
- render: (args) => ,
};
-function DefaultComponent(props: { args: ModalProps }) {
- const { args } = props;
- useEffect(() => {
- const modalElement = document.getElementById(args.popoverId);
- if (!modalElement) return;
- modalElement.showPopover();
- }, [args.popoverId]);
- return (
-
- Modal content
-
- );
-}
+export const Variants: Story = {
+ render: () => {
+ const size = "small";
+ return (
+ <>
+ {ModalVariantsList.map((variant) => (
+
+
+ {size}-{variant}
+
+
+ {size}-{variant}
+
+
+ {size}-{variant}
+
+
+ ))}
+ >
+ );
+ },
+};
-export const SmallSize: Story = {
- args: {
- popoverId: "modal-2",
- trigger: (
-
- Open modal
-
- ),
- csSize: ModalSizesList[1],
+export const Sizes: Story = {
+ render: () => {
+ const variant = "default";
+ return (
+ <>
+ {ModalSizesList.map((size) => (
+
+
+ {size}-{variant}
+
+
+ {size}-{variant}
+
+
+ {size}-{variant}
+
+
+ ))}
+ >
+ );
+ },
+};
+
+export const ModalTitle: Story = {
+ render: (args) => {
+ return (
+
+
+ Custom Modal Title
+
+
+ );
+ },
+};
+
+export const ModalBody: Story = {
+ render: (args) => {
+ return (
+
+ Custom Modal Body
+
+ );
},
- render: (args) => ,
};
-function SmallSizeComponent(props: { args: ModalProps }) {
- const { args } = props;
- useEffect(() => {
- const modalElement = document.getElementById(args.popoverId);
- if (!modalElement) return;
- modalElement.showPopover();
- }, [args.popoverId]);
- return (
-
- Modal content
-
- );
-}
+export const ModalFooter: Story = {
+ render: (args) => {
+ return (
+
+
+ Custom Modal Footer
+
+
+ );
+ },
+};
diff --git a/packages/cyberstorm/package.json b/packages/cyberstorm/package.json
index fbe25793a..c108d8621 100644
--- a/packages/cyberstorm/package.json
+++ b/packages/cyberstorm/package.json
@@ -23,7 +23,7 @@
"@fortawesome/pro-solid-svg-icons": "6.6.0",
"@fortawesome/react-fontawesome": "^0.2.2",
"@radix-ui/react-checkbox": "^1.1.1",
- "@radix-ui/react-dialog": "^1.1.1",
+ "@radix-ui/react-dialog": "1.1.15",
"@radix-ui/react-dropdown-menu": "^2.1.1",
"@radix-ui/react-radio-group": "^1.2.0",
"@radix-ui/react-select": "^2.1.1",
diff --git a/packages/cyberstorm/src/newComponents/BreadCrumbs/BreadCrumbs.css b/packages/cyberstorm/src/newComponents/BreadCrumbs/BreadCrumbs.css
index 7f2f87e90..ee223fba2 100644
--- a/packages/cyberstorm/src/newComponents/BreadCrumbs/BreadCrumbs.css
+++ b/packages/cyberstorm/src/newComponents/BreadCrumbs/BreadCrumbs.css
@@ -70,7 +70,6 @@
}
> *:first-child {
- z-index: 1;
padding-left: 0;
> svg {
diff --git a/packages/cyberstorm/src/newComponents/Modal/Modal.css b/packages/cyberstorm/src/newComponents/Modal/Modal.css
index fc7ba2050..af341b768 100644
--- a/packages/cyberstorm/src/newComponents/Modal/Modal.css
+++ b/packages/cyberstorm/src/newComponents/Modal/Modal.css
@@ -1,8 +1,24 @@
@layer components {
- .modal {
+ .modal__overlay {
+ position: fixed;
+ background: rgb(0 0 0 / 0.6);
+ backdrop-filter: blur(10px);
+ animation: overlay-show 150ms cubic-bezier(0.16, 1, 0.3, 1);
+ inset: 0;
+ }
+
+ .modal__content {
position: fixed;
top: 50%;
left: 50%;
+ display: flex;
+ flex-direction: column;
+
+ /* gap: var(--modal-content-gap); */
+ align-items: flex-start;
+ align-self: stretch;
+ justify-content: center;
+ padding: var(--modal-content-padding);
border: var(--modal-border);
border-radius: var(--modal-border-radius);
color: var(--modal-color);
@@ -10,26 +26,75 @@
box-shadow: var(--modal-box-shadow);
transform: translate(-50%, -50%);
+ animation: content-show 150ms cubic-bezier(0.16, 1, 0.3, 1);
+ }
- &::backdrop {
- background: rgb(0 0 0 / 0.6);
- backdrop-filter: blur(10px);
- }
+ .modal__content:focus {
+ outline: none;
}
- .modal__content {
+ .modal__title {
display: flex;
- flex-direction: column;
- gap: var(--modal-content-gap);
- align-items: flex-start;
+ align-items: center;
align-self: stretch;
- justify-content: center;
- padding: var(--modal-content-padding);
+ height: 60px;
+ padding: var(--space-8) var(--space-8) var(--space-8) var(--space-32);
+
+ padding-right: 92px;
+ border-bottom: 1px solid var(--modal-border-color--default);
+ color: var(--color-text-primary);
+ font-weight: var(--font-weight-bold);
+ font-size: 21px;
+ font-style: normal;
+ line-height: 120%; /* 25.2px */
}
.modal__button {
- position: absolute;
+ position: fixed;
top: 0.5rem;
right: 0.5rem;
}
+
+ .modal__body {
+ display: flex;
+ flex-direction: column;
+ gap: var(--gap-md);
+ align-items: flex-start;
+ align-self: stretch;
+ max-height: 800px;
+ padding: var(--space-32);
+ overflow-y: auto;
+ }
+
+ .modal__footer {
+ display: flex;
+ gap: var(--gap-md);
+ align-items: center;
+ align-self: stretch;
+ justify-content: flex-end;
+ padding: var(--space-16) var(--space-24);
+ border-top: 1px solid var(--modal-border-color--default);
+ }
+
+ @keyframes overlay-show {
+ from {
+ opacity: 0;
+ }
+
+ to {
+ opacity: 1;
+ }
+ }
+
+ @keyframes content-show {
+ from {
+ transform: translate(-50%, -48%) scale(0.96);
+ opacity: 0;
+ }
+
+ to {
+ transform: translate(-50%, -50%) scale(1);
+ opacity: 1;
+ }
+ }
}
diff --git a/packages/cyberstorm/src/newComponents/Modal/Modal.tsx b/packages/cyberstorm/src/newComponents/Modal/Modal.tsx
index e21f95fdc..c5dc080fc 100644
--- a/packages/cyberstorm/src/newComponents/Modal/Modal.tsx
+++ b/packages/cyberstorm/src/newComponents/Modal/Modal.tsx
@@ -1,8 +1,9 @@
-import { type ReactNode } from "react";
import {
- Frame,
- type FrameModalProps,
-} from "../../primitiveComponents/Frame/Frame";
+ cloneElement,
+ isValidElement,
+ type PropsWithChildren,
+ type ReactNode,
+} from "react";
import "./Modal.css";
import { NewButton, NewIcon } from "../..";
import { type ModalVariants } from "@thunderstore/cyberstorm-theme/src/components";
@@ -10,46 +11,384 @@ import { classnames, componentClasses } from "../../utils/utils";
import { faXmarkLarge } from "@fortawesome/pro-solid-svg-icons";
import { type ModalSizes } from "@thunderstore/cyberstorm-theme/src/components/Modal/Modal";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
+import * as Dialog from "@radix-ui/react-dialog";
+import { type ButtonComponentProps } from "../Button/Button";
-export interface ModalProps extends Omit {
+/**
+ * Props for the `Modal` component.
+ *
+ * The modal is built on top of Radix UI Dialog and supports both
+ * controlled and uncontrolled usage. Provide `open` and
+ * `onOpenChange` to control it programmatically, or omit them and
+ * pass a `trigger` element to let Radix manage state internally.
+ */
+export interface ModalProps
+ extends PropsWithChildren,
+ Pick {
+ /**
+ * Optional element that acts as the opener for the modal.
+ * Rendered through Radix `Dialog.Trigger` with `asChild` so your
+ * element receives the trigger behavior without extra wrappers.
+ *
+ * Omit this when you control the modal via `open`/`onOpenChange` only.
+ */
trigger?: ReactNode;
+ /**
+ * Visual variant (theme-driven).
+ * @default "default"
+ */
csVariant?: ModalVariants;
+ /**
+ * Visual size (theme-driven).
+ * @default "medium"
+ */
csSize?: ModalSizes;
+ /**
+ * Additional class names applied to the modal content container.
+ */
+ contentClasses?: string;
+ /**
+ * When true, prevents rendering the default Title region.
+ * Useful when you supply your own `Modal.Title` as a child.
+ */
+ disableTitle?: boolean;
+ /**
+ * When true, prevents rendering the default Body region.
+ * Use this if you want to fully control child layout yourself.
+ */
+ disableBody?: boolean;
+ /**
+ * When true, prevents rendering the default Footer region.
+ */
+ disableFooter?: boolean;
+ /**
+ * When true, prevents rendering the default Exit (close) button.
+ * You can provide your own `Modal.Exit` as a child to replace it.
+ */
+ disableExit?: boolean;
+ /**
+ * When true, disables the default subcomponent parsing entirely and
+ * renders children exactly as provided.
+ */
+ disableDefaultSubComponents?: boolean;
+ /**
+ * Shorthand content for the title region; rendered as ``.
+ */
+ titleContent?: ReactNode;
+ /**
+ * Shorthand content for the footer region; rendered as ``.
+ */
+ footerContent?: ReactNode;
+ /**
+ * Value forwarded to `aria-describedby` on the dialog content.
+ * Provide the id of an element that describes the dialog.
+ */
+ ariaDescribedby?: string;
}
+// TODO: Style system compatibility is currently degraded, improve to a agreed upon level according to specifications
-// TODO: Add storybook story
-// TODO: Currently the same modal can't be used in 2 different places in the same page. Fix that somehow
+/**
+ * Accessible modal dialog built on Radix UI Dialog.
+ *
+ * This component provides an opinionated layout with optional subcomponents:
+ * `Modal.Exit`, `Modal.Title`, `Modal.Body`, and `Modal.Footer`. You can either
+ * let the component render sensible defaults (via `titleContent`/`footerContent`),
+ * or provide the subcomponents directly as children for full control.
+ *
+ * Control model:
+ * - Uncontrolled: omit `open`/`onOpenChange` and provide a `trigger` element.
+ * - Controlled: pass `open` and `onOpenChange` to manage visibility from state.
+ *
+ * Styling is driven by `csVariant` and `csSize` which map to the theme system.
+ *
+ * @param props - See `ModalProps` for full list of options.
+ *
+ * @example Uncontrolled with trigger and shorthand title/footer
+ * ```tsx
+ * function Example() {
+ * return (
+ * Open modal}
+ * titleContent="Example modal"
+ * footerContent={
+ * <>
+ * Cancel
+ * Confirm
+ * >
+ * }
+ * >
+ * Body content goes here.
+ *
+ * );
+ * }
+ * ```
+ *
+ * @example Controlled (programmatic open/close via state)
+ * ```tsx
+ * import { useState } from "react";
+ *
+ * function ControlledExample() {
+ * const [open, setOpen] = useState(false);
+ *
+ * return (
+ * <>
+ * // Programmatically open the modal without a trigger
+ * setOpen(true)}>
+ * Open programmatically
+ *
+ *
+ *
+ * Controlled body content.
+ *
+ * setOpen(false)}>
+ * Close
+ *
+ *
+ *
+ * >
+ * );
+ * }
+ * ```
+ *
+ * @example Using explicit subcomponents and custom footer structure
+ * ```tsx
+ * function CustomStructure() {
+ * return (
+ * Open}>
+ *
+ * Custom title
+ *
+ * Rich body content…
+ * Rich body content…
+ * Rich body content…
+ *
+ *
+ * Dismiss
+ *
+ *
+ * );
+ * }
+ * ```
+ */
export function Modal(props: ModalProps) {
- const { children, csVariant = "default", csSize = "medium", trigger } = props;
+ const {
+ children,
+ csVariant = "default",
+ csSize = "medium",
+ trigger,
+ disableTitle,
+ disableBody,
+ disableFooter,
+ disableExit,
+ disableDefaultSubComponents,
+ titleContent,
+ footerContent,
+ ariaDescribedby,
+ } = props;
+
+ const filteredChildren: ReactNode[] = [];
+
+ let exit = ;
+
+ let title = titleContent ? {titleContent} : null;
+
+ let body = undefined;
+
+ let footer = footerContent ? (
+ {footerContent}
+ ) : null;
+
+ if (!disableDefaultSubComponents && children) {
+ for (const child of children instanceof Array ? children : [children]) {
+ if (child == null) continue;
+ if (isValidElement(child)) {
+ const childDisplayName =
+ typeof child.type === "function" &&
+ Object.prototype.hasOwnProperty.call(child.type, "displayName")
+ ? (child.type as { displayName?: string }).displayName
+ : "";
+ if (!disableExit && childDisplayName === ModalExit.displayName) {
+ exit = child;
+ continue;
+ }
+ if (!disableTitle && childDisplayName === ModalTitle.displayName) {
+ title = child;
+ continue;
+ }
+ if (!disableFooter && childDisplayName === ModalFooter.displayName) {
+ footer = child;
+ continue;
+ }
+ if (!disableBody && childDisplayName === ModalBody.displayName) {
+ body = child;
+ continue;
+ }
+ filteredChildren.push(child);
+ } else {
+ filteredChildren.push(child);
+ }
+ }
+ }
return (
- <>
- {trigger}
-
-
-
-
-
-
- {children}
-
- >
+
+ {trigger ? {trigger} : null}
+
+
+
+ {disableDefaultSubComponents ? (
+ children
+ ) : (
+ <>
+ {disableExit ? null : exit}
+ {disableTitle ? null : title}
+ {disableBody ? (
+ filteredChildren
+ ) : body ? (
+ body
+ ) : (
+ {filteredChildren}
+ )}
+ {disableFooter ? null : footer}
+ >
+ )}
+
+
+
+
);
}
Modal.displayName = "Modal";
+
+function ModalExit(props: {
+ modalCloseProps?: Dialog.DialogCloseProps;
+ asChild?: boolean;
+ buttonProps?: ButtonComponentProps;
+}) {
+ return (
+
+
+
+
+
+
+
+ );
+}
+
+ModalExit.displayName = "ModalExit";
+
+function ModalTitle(props: Dialog.DialogTitleProps) {
+ return (
+
+ );
+}
+
+ModalTitle.displayName = "ModalTitle";
+
+function ModalBody(
+ props: { asChild?: boolean; className?: string } & PropsWithChildren
+) {
+ if (props.asChild) {
+ if (isValidElement(props.children)) {
+ return cloneElement(
+ props.children as React.ReactElement<{ className?: string }>,
+ {
+ className: classnames(
+ "modal__body",
+ (props.children.props as { className?: string }).className,
+ props.className
+ ),
+ }
+ );
+ } else {
+ console.warn("Modal.Body child is not valid for usage with asChild");
+ return (
+
+ {props.children}
+
+ );
+ }
+ }
+
+ return (
+
+ {props.children}
+
+ );
+}
+
+ModalBody.displayName = "ModalBody";
+
+function ModalFooter(
+ props: { asChild?: boolean; className?: string } & PropsWithChildren
+) {
+ if (props.asChild) {
+ if (isValidElement(props.children)) {
+ return cloneElement(
+ props.children as React.ReactElement<{ className?: string }>,
+ {
+ className: classnames(
+ "modal__footer",
+ (props.children.props as { className?: string }).className,
+ props.className
+ ),
+ }
+ );
+ } else {
+ console.warn("Modal.Footer child is not valid for usage with asChild");
+ return (
+
+ {props.children}
+
+ );
+ }
+ }
+
+ return (
+
+ {props.children}
+
+ );
+}
+
+ModalFooter.displayName = "ModalFooter";
+
+// Expose subcomponents for easier access
+Modal.Root = Dialog.Root;
+Modal.Trigger = Dialog.Trigger;
+Modal.Portal = Dialog.Portal;
+Modal.Overlay = Dialog.Overlay;
+Modal.Content = Dialog.Content;
+Modal.Close = Dialog.Close;
+Modal.Exit = ModalExit;
+Modal.Title = ModalTitle;
+Modal.Body = ModalBody;
+Modal.Footer = ModalFooter;
diff --git a/packages/cyberstorm/src/primitiveComponents/Frame/Frame.tsx b/packages/cyberstorm/src/primitiveComponents/Frame/Frame.tsx
index fe569f169..6b47ea812 100644
--- a/packages/cyberstorm/src/primitiveComponents/Frame/Frame.tsx
+++ b/packages/cyberstorm/src/primitiveComponents/Frame/Frame.tsx
@@ -230,18 +230,19 @@ export const Frame = memo(function Frame(
<>
{Children.map(children, (child) => {
if (React.isValidElement(child)) {
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
- return cloneElement(child as React.ReactElement, {
- className: classnames(
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
- (child.props as any).className,
- "icon",
- csMode === "inline" ? "icon--inline" : null,
- rootClasses
- ),
- ref: svgIconRef,
- ...svgFProps,
- });
+ return cloneElement(
+ child as React.ReactElement<{ className?: string }>,
+ {
+ className: classnames(
+ (child.props as { className?: string }).className,
+ "icon",
+ csMode === "inline" ? "icon--inline" : null,
+ rootClasses
+ ),
+ ref: svgIconRef,
+ ...svgFProps,
+ }
+ );
} else {
return null;
}
@@ -258,17 +259,18 @@ export const Frame = memo(function Frame(
>
{Children.map(children, (child) => {
if (React.isValidElement(child)) {
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
- return cloneElement(child as React.ReactElement, {
- className: classnames(
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
- (child.props as any).className,
- "icon",
- "icon--inline",
- rootClasses
- ),
- ...svgFProps,
- });
+ return cloneElement(
+ child as React.ReactElement<{ className?: string }>,
+ {
+ className: classnames(
+ (child.props as { className?: string }).className,
+ "icon",
+ "icon--inline",
+ rootClasses
+ ),
+ ...svgFProps,
+ }
+ );
} else {
return null;
}
@@ -286,15 +288,18 @@ export const Frame = memo(function Frame(
{Children.map(children, (child) => {
if (React.isValidElement(child)) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
- return cloneElement(child as React.ReactElement, {
- className: classnames(
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
- (child.props as any).className,
- "icon",
- rootClasses
- ),
- ...svgFProps,
- });
+ return cloneElement(
+ child as React.ReactElement<{ className?: string }>,
+ {
+ className: classnames(
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ (child.props as { className?: string }).className,
+ "icon",
+ rootClasses
+ ),
+ ...svgFProps,
+ }
+ );
} else {
return null;
}
diff --git a/yarn.lock b/yarn.lock
index e5e97dd00..c93180b60 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -2061,6 +2061,11 @@
resolved "https://registry.yarnpkg.com/@radix-ui/primitive/-/primitive-1.1.2.tgz#83f415c4425f21e3d27914c12b3272a32e3dae65"
integrity sha512-XnbHrrprsNqZKQhStrSwgRUQzoCI1glLzdw79xiZPoofhGICeZRSQ3dIxAKH1gb3OHfNf4d6f+vAv3kil2eggA==
+"@radix-ui/primitive@1.1.3":
+ version "1.1.3"
+ resolved "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.3.tgz#e2dbc13bdc5e4168f4334f75832d7bdd3e2de5ba"
+ integrity sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg==
+
"@radix-ui/react-arrow@1.1.7":
version "1.1.7"
resolved "https://registry.yarnpkg.com/@radix-ui/react-arrow/-/react-arrow-1.1.7.tgz#e14a2657c81d961598c5e72b73dd6098acc04f09"
@@ -2102,20 +2107,20 @@
resolved "https://registry.yarnpkg.com/@radix-ui/react-context/-/react-context-1.1.2.tgz#61628ef269a433382c364f6f1e3788a6dc213a36"
integrity sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA==
-"@radix-ui/react-dialog@^1.1.1":
- version "1.1.14"
- resolved "https://registry.yarnpkg.com/@radix-ui/react-dialog/-/react-dialog-1.1.14.tgz#4c69c80c258bc6561398cfce055202ea11075107"
- integrity sha512-+CpweKjqpzTmwRwcYECQcNYbI8V9VSQt0SNFKeEBLgfucbsLssU6Ppq7wUdNXEGb573bMjFhVjKVll8rmV6zMw==
+"@radix-ui/react-dialog@1.1.15":
+ version "1.1.15"
+ resolved "https://registry.npmjs.org/@radix-ui/react-dialog/-/react-dialog-1.1.15.tgz#1de3d7a7e9a17a9874d29c07f5940a18a119b632"
+ integrity sha512-TCglVRtzlffRNxRMEyR36DGBLJpeusFcgMVD9PZEzAKnUs1lKCgX5u9BmC2Yg+LL9MgZDugFFs1Vl+Jp4t/PGw==
dependencies:
- "@radix-ui/primitive" "1.1.2"
+ "@radix-ui/primitive" "1.1.3"
"@radix-ui/react-compose-refs" "1.1.2"
"@radix-ui/react-context" "1.1.2"
- "@radix-ui/react-dismissable-layer" "1.1.10"
- "@radix-ui/react-focus-guards" "1.1.2"
+ "@radix-ui/react-dismissable-layer" "1.1.11"
+ "@radix-ui/react-focus-guards" "1.1.3"
"@radix-ui/react-focus-scope" "1.1.7"
"@radix-ui/react-id" "1.1.1"
"@radix-ui/react-portal" "1.1.9"
- "@radix-ui/react-presence" "1.1.4"
+ "@radix-ui/react-presence" "1.1.5"
"@radix-ui/react-primitive" "2.1.3"
"@radix-ui/react-slot" "1.2.3"
"@radix-ui/react-use-controllable-state" "1.2.2"
@@ -2138,6 +2143,17 @@
"@radix-ui/react-use-callback-ref" "1.1.1"
"@radix-ui/react-use-escape-keydown" "1.1.1"
+"@radix-ui/react-dismissable-layer@1.1.11":
+ version "1.1.11"
+ resolved "https://registry.npmjs.org/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.1.11.tgz#e33ab6f6bdaa00f8f7327c408d9f631376b88b37"
+ integrity sha512-Nqcp+t5cTB8BinFkZgXiMJniQH0PsUt2k51FUhbdfeKvc4ACcG2uQniY/8+h1Yv6Kza4Q7lD7PQV0z0oicE0Mg==
+ dependencies:
+ "@radix-ui/primitive" "1.1.3"
+ "@radix-ui/react-compose-refs" "1.1.2"
+ "@radix-ui/react-primitive" "2.1.3"
+ "@radix-ui/react-use-callback-ref" "1.1.1"
+ "@radix-ui/react-use-escape-keydown" "1.1.1"
+
"@radix-ui/react-dropdown-menu@^2.1.1":
version "2.1.15"
resolved "https://registry.yarnpkg.com/@radix-ui/react-dropdown-menu/-/react-dropdown-menu-2.1.15.tgz#f507320de8e11bc1e671a6ec0c27a7a89e725131"
@@ -2156,6 +2172,11 @@
resolved "https://registry.yarnpkg.com/@radix-ui/react-focus-guards/-/react-focus-guards-1.1.2.tgz#4ec9a7e50925f7fb661394460045b46212a33bed"
integrity sha512-fyjAACV62oPV925xFCrH8DR5xWhg9KYtJT4s3u54jxp+L/hbpTY2kIeEFFbFe+a/HCE94zGQMZLIpVTPVZDhaA==
+"@radix-ui/react-focus-guards@1.1.3":
+ version "1.1.3"
+ resolved "https://registry.npmjs.org/@radix-ui/react-focus-guards/-/react-focus-guards-1.1.3.tgz#2a5669e464ad5fde9f86d22f7fdc17781a4dfa7f"
+ integrity sha512-0rFg/Rj2Q62NCm62jZw0QX7a3sz6QCQU0LpZdNrJX8byRGaGVTqbrW9jAoIAHyMQqsNpeZ81YgSizOt5WXq0Pw==
+
"@radix-ui/react-focus-scope@1.1.7":
version "1.1.7"
resolved "https://registry.yarnpkg.com/@radix-ui/react-focus-scope/-/react-focus-scope-1.1.7.tgz#dfe76fc103537d80bf42723a183773fd07bfb58d"
@@ -2228,6 +2249,14 @@
"@radix-ui/react-compose-refs" "1.1.2"
"@radix-ui/react-use-layout-effect" "1.1.1"
+"@radix-ui/react-presence@1.1.5":
+ version "1.1.5"
+ resolved "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.1.5.tgz#5d8f28ac316c32f078afce2996839250c10693db"
+ integrity sha512-/jfEwNDdQVBCNvjkGit4h6pMOzq8bHkopq458dPt2lMjx+eBQUohZNG9A7DtO/O5ukSbxuaNGXMjHicgwy6rQQ==
+ dependencies:
+ "@radix-ui/react-compose-refs" "1.1.2"
+ "@radix-ui/react-use-layout-effect" "1.1.1"
+
"@radix-ui/react-primitive@2.1.3":
version "2.1.3"
resolved "https://registry.yarnpkg.com/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz#db9b8bcff49e01be510ad79893fb0e4cda50f1bc"