From ca1fcb538519eec267d448642a011344cc4233b5 Mon Sep 17 00:00:00 2001 From: Oksamies Date: Mon, 6 Oct 2025 21:01:03 +0300 Subject: [PATCH 1/6] Refactor Modal to use Radix Dialog This includes refactoring usages in Nimbus and other places. Also includes some minor fixes Nimbus styles, like z-index removals Worthy to be noted that; the compatibility with style system is degraded, as no clear distinction between component styles and "usage" styles can be deduced from designs. So style system compatibility improvement is a TODO for this component Added AI generated descriptions for the component, params and examples --- .../app/commonComponents/Footer/Footer.css | 11 - .../Navigation/Navigation.tsx | 2 +- .../cyberstorm-remix/app/p/packageListing.tsx | 63 ++- .../app/p/tabs/Wiki/WikiPageEdit.tsx | 49 +-- .../app/settings/teams/Teams.css | 1 - .../app/settings/teams/Teams.tsx | 58 +-- .../teams/team/tabs/Members/Members.tsx | 131 +++--- .../tabs/ServiceAccounts/ServiceAccounts.tsx | 254 +++++------ .../teams/team/tabs/Settings/Settings.tsx | 130 +++--- .../app/settings/user/Account/Account.tsx | 13 +- .../stories/cyberstormComponents/Modal.css | 13 + .../cyberstormComponents/Modal.stories.tsx | 182 ++++++-- packages/cyberstorm/package.json | 2 +- .../newComponents/BreadCrumbs/BreadCrumbs.css | 1 - .../src/newComponents/Modal/Modal.css | 89 +++- .../src/newComponents/Modal/Modal.tsx | 393 ++++++++++++++++-- yarn.lock | 45 +- 17 files changed, 950 insertions(+), 487 deletions(-) create mode 100644 apps/storybook/src/stories/cyberstormComponents/Modal.css 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 ? (
{packagePermissions.permissions.can_moderate ? ( - - - - - Review Package - - } - > - - + ) : null} {/* {packagePermissions.permissions.can_view_listing_admin_page ? ( Editing page
- - Delete page - - } - > - - +
@@ -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..1b0a5bb53 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`, @@ -343,28 +281,58 @@ function AddServiceAccountForm(props: { }); 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..e1b9abaf4 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, + PropsWithChildren, + type ReactNode, +} from "react"; import "./Modal.css"; import { NewButton, NewIcon } from "../.."; import { type ModalVariants } from "@thunderstore/cyberstorm-theme/src/components"; @@ -10,46 +11,374 @@ 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 { 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) 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); + } + } + } return ( <> - {trigger} - - - - - - -
{children}
- + + {trigger} + + + + {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)) { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + return cloneElement(props.children as React.ReactElement, { + className: classnames( + "modal__body", + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (props.children.props as any).className, + props.className + ), + }); + } else { + console.warn("Modal.Body child is not valid for usage with asChild"); + return null; + } + } + + return ( +
+ {props.children} +
+ ); +} + +ModalBody.displayName = "ModalBody"; + +function ModalFooter( + props: { asChild?: boolean; className?: string } & PropsWithChildren +) { + if (props.asChild) { + if (isValidElement(props.children)) { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + return cloneElement(props.children as React.ReactElement, { + className: classnames( + "modal__footer", + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (props.children.props as any).className, + props.className + ), + }); + } else { + console.warn("Modal.Footer child is not valid for usage with asChild"); + return null; + } + } + + 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/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" From 721e39e5e678ee88c6f58e245ec1504983017706 Mon Sep 17 00:00:00 2001 From: Oksamies Date: Mon, 6 Oct 2025 21:14:25 +0300 Subject: [PATCH 2/6] Fix any type casts used with react cloneElement in Frame and Modal components --- .../src/newComponents/Modal/Modal.tsx | 42 +++++------ .../src/primitiveComponents/Frame/Frame.tsx | 69 ++++++++++--------- 2 files changed, 59 insertions(+), 52 deletions(-) diff --git a/packages/cyberstorm/src/newComponents/Modal/Modal.tsx b/packages/cyberstorm/src/newComponents/Modal/Modal.tsx index e1b9abaf4..a9d4d62c3 100644 --- a/packages/cyberstorm/src/newComponents/Modal/Modal.tsx +++ b/packages/cyberstorm/src/newComponents/Modal/Modal.tsx @@ -1,7 +1,7 @@ import { cloneElement, isValidElement, - PropsWithChildren, + type PropsWithChildren, type ReactNode, } from "react"; import "./Modal.css"; @@ -12,7 +12,7 @@ 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 { ButtonComponentProps } from "../Button/Button"; +import { type ButtonComponentProps } from "../Button/Button"; /** * Props for the `Modal` component. @@ -318,15 +318,16 @@ function ModalBody( ) { if (props.asChild) { if (isValidElement(props.children)) { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - return cloneElement(props.children as React.ReactElement, { - className: classnames( - "modal__body", - // eslint-disable-next-line @typescript-eslint/no-explicit-any - (props.children.props as any).className, - props.className - ), - }); + 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 null; @@ -347,15 +348,16 @@ function ModalFooter( ) { if (props.asChild) { if (isValidElement(props.children)) { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - return cloneElement(props.children as React.ReactElement, { - className: classnames( - "modal__footer", - // eslint-disable-next-line @typescript-eslint/no-explicit-any - (props.children.props as any).className, - props.className - ), - }); + 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 null; 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; } From 8b2b070cd9d6b9dbd5cc18463ade6f8851f4553d Mon Sep 17 00:00:00 2001 From: Oksamies Date: Mon, 6 Oct 2025 21:17:01 +0300 Subject: [PATCH 3/6] Reset modal state when closing so the form can be reused After a successful creation the modal stays stuck in the success view; reopening it never shows the input fields again. Reset the view and clear the nickname/token when the dialog closes. --- .../team/tabs/ServiceAccounts/ServiceAccounts.tsx | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) 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 1b0a5bb53..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 @@ -280,10 +280,20 @@ function AddServiceAccountForm(props: { }, }); + const handleOpenChange = (nextOpen: boolean) => { + setOpen(nextOpen); + if (!nextOpen) { + setServiceAccountAdded(false); + setAddedServiceAccountToken(""); + setAddedServiceAccountNickname(""); + updateFormFieldState({ field: "nickname", value: "" }); + } + }; + return ( Add Service Account From 9c3f7420bbaf2fb4540f03d849f7280cddec2a95 Mon Sep 17 00:00:00 2001 From: Oksamies Date: Mon, 6 Oct 2025 21:21:22 +0300 Subject: [PATCH 4/6] Modal: Guard the Trigger render when trigger is undefined. When consumers drive the dialog with open/onOpenChange only, trigger stays undefined. Rendering {trigger} with no child makes Radix throw at runtime. Wrap the trigger block in a conditional so controlled usage keeps working. --- .../src/newComponents/Modal/Modal.tsx | 68 +++++++++---------- 1 file changed, 33 insertions(+), 35 deletions(-) diff --git a/packages/cyberstorm/src/newComponents/Modal/Modal.tsx b/packages/cyberstorm/src/newComponents/Modal/Modal.tsx index a9d4d62c3..9afa44e11 100644 --- a/packages/cyberstorm/src/newComponents/Modal/Modal.tsx +++ b/packages/cyberstorm/src/newComponents/Modal/Modal.tsx @@ -233,41 +233,39 @@ export function Modal(props: ModalProps) { } return ( - <> - - {trigger} - - - - {disableDefaultSubComponents ? ( - children - ) : ( - <> - {disableExit ? null : exit} - {disableTitle ? null : title} - {disableBody ? ( - filteredChildren - ) : body ? ( - body - ) : ( - {filteredChildren} - )} - {disableFooter ? null : footer} - - )} - - - - - + + {trigger ? {trigger} : null} + + + + {disableDefaultSubComponents ? ( + children + ) : ( + <> + {disableExit ? null : exit} + {disableTitle ? null : title} + {disableBody ? ( + filteredChildren + ) : body ? ( + body + ) : ( + {filteredChildren} + )} + {disableFooter ? null : footer} + + )} + + + + ); } From 5b7d03a49203a99d623db9dd26bbd10b0f6d2abc Mon Sep 17 00:00:00 2001 From: Oksamies Date: Mon, 6 Oct 2025 21:23:31 +0300 Subject: [PATCH 5/6] Modal: Preserve non-element children in the default Body path. Primitive children (strings, numbers) are dropped because the loop only pushes valid React elements into filteredChildren. Those nodes never render, breaking legitimate content. Push all non-null children, and reserve the displayName check for elements only. --- packages/cyberstorm/src/newComponents/Modal/Modal.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/cyberstorm/src/newComponents/Modal/Modal.tsx b/packages/cyberstorm/src/newComponents/Modal/Modal.tsx index 9afa44e11..348bda5a8 100644 --- a/packages/cyberstorm/src/newComponents/Modal/Modal.tsx +++ b/packages/cyberstorm/src/newComponents/Modal/Modal.tsx @@ -204,7 +204,7 @@ export function Modal(props: ModalProps) { if (!disableDefaultSubComponents && children) { for (const child of children instanceof Array ? children : [children]) { - if (!child) continue; + if (child == null) continue; if (isValidElement(child)) { const childDisplayName = typeof child.type === "function" && @@ -228,6 +228,8 @@ export function Modal(props: ModalProps) { continue; } filteredChildren.push(child); + } else { + filteredChildren.push(child); } } } From 8ee7d267f74c2d328d683f244ab0bfe08edd32bf Mon Sep 17 00:00:00 2001 From: Oksamies Date: Tue, 7 Oct 2025 15:34:41 +0300 Subject: [PATCH 6/6] Modal: Return fallback content instead of null when asChild validation fails. Returning null when props.children is not a valid element can break the modal layout by removing the Body or Footer entirely. Provide a fallback that wraps the content in the default div so the modal structure remains intact. --- .../cyberstorm/src/newComponents/Modal/Modal.tsx | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/packages/cyberstorm/src/newComponents/Modal/Modal.tsx b/packages/cyberstorm/src/newComponents/Modal/Modal.tsx index 348bda5a8..c5dc080fc 100644 --- a/packages/cyberstorm/src/newComponents/Modal/Modal.tsx +++ b/packages/cyberstorm/src/newComponents/Modal/Modal.tsx @@ -330,7 +330,11 @@ function ModalBody( ); } else { console.warn("Modal.Body child is not valid for usage with asChild"); - return null; + return ( +
+ {props.children} +
+ ); } } @@ -360,7 +364,11 @@ function ModalFooter( ); } else { console.warn("Modal.Footer child is not valid for usage with asChild"); - return null; + return ( +
+ {props.children} +
+ ); } }