From 54989e28ce142c9b32755deff80d21ee5a1f2ca9 Mon Sep 17 00:00:00 2001 From: "daily.dev Bot" Date: Tue, 27 Jan 2026 19:30:11 +0000 Subject: [PATCH 1/3] feat(ENG-483): add Stack & Tools frontend for Squads This commit adds the frontend implementation for Squad stack/tools feature, allowing users to view and manage the technologies and tools associated with a Squad. Frontend changes: - Add GraphQL types and queries for SourceStack - Add useSourceStack hook for data management - Add SourceStackItem component for individual items - Add SourceStackSection component for grouping by section - Add SourceStackModal for adding/editing items - Add SquadStack component as main container - Integrate SquadStack into SquadPageHeader - Add SourceStack RequestKey to query cache Co-Authored-By: Claude Opus 4.5 --- .../src/components/squads/SquadPageHeader.tsx | 4 + .../squads/stack/SourceStackItem.tsx | 83 ++++++ .../squads/stack/SourceStackModal.tsx | 254 ++++++++++++++++++ .../squads/stack/SourceStackSection.tsx | 56 ++++ .../components/squads/stack/SquadStack.tsx | 201 ++++++++++++++ .../src/components/squads/stack/index.ts | 4 + .../shared/src/graphql/source/sourceStack.ts | 157 +++++++++++ packages/shared/src/hooks/source/index.ts | 1 + .../shared/src/hooks/source/useSourceStack.ts | 95 +++++++ packages/shared/src/lib/query.ts | 1 + 10 files changed, 856 insertions(+) create mode 100644 packages/shared/src/components/squads/stack/SourceStackItem.tsx create mode 100644 packages/shared/src/components/squads/stack/SourceStackModal.tsx create mode 100644 packages/shared/src/components/squads/stack/SourceStackSection.tsx create mode 100644 packages/shared/src/components/squads/stack/SquadStack.tsx create mode 100644 packages/shared/src/components/squads/stack/index.ts create mode 100644 packages/shared/src/graphql/source/sourceStack.ts create mode 100644 packages/shared/src/hooks/source/useSourceStack.ts diff --git a/packages/shared/src/components/squads/SquadPageHeader.tsx b/packages/shared/src/components/squads/SquadPageHeader.tsx index 24ba30afb2..3990d193cc 100644 --- a/packages/shared/src/components/squads/SquadPageHeader.tsx +++ b/packages/shared/src/components/squads/SquadPageHeader.tsx @@ -33,6 +33,7 @@ import { TypographyType, } from '../typography/Typography'; import { ClickableText } from '../buttons/ClickableText'; +import { SquadStack } from './stack/SquadStack'; interface SquadPageHeaderProps { squad: Squad; @@ -178,6 +179,9 @@ export function SquadPageHeader({ )} +
+ +
void; + onDelete?: (item: SourceStack) => void; +} + +export function SourceStackItem({ + item, + canEdit, + onEdit, + onDelete, +}: SourceStackItemProps): ReactElement { + const { tool } = item; + const title = item.title ?? tool.title; + + return ( +
+
+ {tool.faviconUrl ? ( + + ) : ( + + )} + {!!title && ( +
+ + {title} + +
+ )} +
+ {canEdit && ( +
+ {onEdit && ( +
+ )} +
+ ); +} diff --git a/packages/shared/src/components/squads/stack/SourceStackModal.tsx b/packages/shared/src/components/squads/stack/SourceStackModal.tsx new file mode 100644 index 0000000000..80c0898025 --- /dev/null +++ b/packages/shared/src/components/squads/stack/SourceStackModal.tsx @@ -0,0 +1,254 @@ +import type { ReactElement } from 'react'; +import React, { useMemo, useState } from 'react'; +import { FormProvider, useForm } from 'react-hook-form'; +import { z } from 'zod'; +import { zodResolver } from '@hookform/resolvers/zod'; +import type { ModalProps } from '../../modals/common/Modal'; +import { Modal } from '../../modals/common/Modal'; +import { TextField } from '../../fields/TextField'; +import { Typography, TypographyType } from '../../typography/Typography'; +import { Button, ButtonVariant } from '../../buttons/Button'; +import { ModalHeader } from '../../modals/common/ModalHeader'; +import { useViewSize, ViewSize } from '../../../hooks'; +import type { + SourceStack, + AddSourceStackInput, +} from '../../../graphql/source/sourceStack'; +import type { DatasetTool } from '../../../graphql/user/userStack'; +import { useStackSearch } from '../../../features/profile/hooks/useStackSearch'; +import { PlusIcon } from '../../icons'; + +const SECTION_OPTIONS = ['Primary', 'Backend', 'Frontend', 'DevOps', 'AI'] as const; + +const sourceStackFormSchema = z.object({ + title: z.string().min(1, 'Title is required').max(255), + section: z.string().min(1, 'Section is required').max(100), + customSection: z.string().max(100).optional(), +}); + +type SourceStackFormData = z.infer; + +type SourceStackModalProps = Omit & { + onSubmit: (input: AddSourceStackInput) => Promise; + existingItem?: SourceStack; +}; + +export function SourceStackModal({ + onSubmit, + existingItem, + ...rest +}: SourceStackModalProps): ReactElement { + const [showSuggestions, setShowSuggestions] = useState(false); + const isMobile = useViewSize(ViewSize.MobileL); + const isEditing = !!existingItem; + + const methods = useForm({ + resolver: zodResolver(sourceStackFormSchema), + defaultValues: { + title: existingItem?.title ?? existingItem?.tool.title ?? '', + section: existingItem?.section || 'Primary', + customSection: '', + }, + }); + + const { + register, + handleSubmit, + watch, + setValue, + formState: { errors, isSubmitting }, + } = methods; + + const title = watch('title'); + const section = watch('section'); + const customSection = watch('customSection'); + + const { results: suggestions } = useStackSearch(title); + + const isCustomSection = !SECTION_OPTIONS.includes( + section as (typeof SECTION_OPTIONS)[number], + ); + const finalSection = isCustomSection ? customSection || section : section; + const canSubmit = title.trim().length > 0 && finalSection.trim().length > 0; + + const handleSelectSuggestion = (suggestion: DatasetTool) => { + setValue('title', suggestion.title); + setShowSuggestions(false); + }; + + const onFormSubmit = handleSubmit(async (data) => { + const effectiveSection = isCustomSection + ? data.customSection || data.section + : data.section; + + await onSubmit({ + title: data.title.trim(), + section: effectiveSection.trim(), + }); + rest.onRequestClose?.(null); + }); + + const filteredSuggestions = useMemo(() => { + if (!showSuggestions || title.length < 1) { + return []; + } + return suggestions; + }, [showSuggestions, suggestions, title.length]); + + return ( + + + + {isEditing ? 'Edit Stack Item' : 'Add to Squad Stack'} + + + ), + rightButtonProps: { + variant: ButtonVariant.Primary, + disabled: !canSubmit || isSubmitting, + loading: isSubmitting, + }, + copy: { right: isEditing ? 'Save' : 'Add' }, + }} + kind={Modal.Kind.FlexibleCenter} + size={Modal.Size.Small} + {...rest} + > +
+ + + {isEditing ? 'Edit Stack Item' : 'Add to Squad Stack'} + + + + {/* Title with autocomplete */} +
+ { + setValue('title', e.target.value); + if (!isEditing) { + setShowSuggestions(true); + } + }} + onFocus={() => { + if (!isEditing) { + setShowSuggestions(true); + } + }} + /> + {!isEditing && showSuggestions && title.trim() && ( +
+ {filteredSuggestions.map((suggestion) => ( + + ))} + {!filteredSuggestions.some( + (s) => s.title.toLowerCase() === title.trim().toLowerCase(), + ) && ( + + )} +
+ )} +
+ + {/* Section selector */} +
+ + Section + +
+ {SECTION_OPTIONS.map((opt) => ( + + ))} + +
+ {isCustomSection && ( + + )} +
+ + {!isMobile && ( + + )} +
+
+
+
+ ); +} diff --git a/packages/shared/src/components/squads/stack/SourceStackSection.tsx b/packages/shared/src/components/squads/stack/SourceStackSection.tsx new file mode 100644 index 0000000000..441c961305 --- /dev/null +++ b/packages/shared/src/components/squads/stack/SourceStackSection.tsx @@ -0,0 +1,56 @@ +import type { ReactElement } from 'react'; +import React from 'react'; +import type { SourceStack } from '../../../graphql/source/sourceStack'; +import { + Typography, + TypographyType, + TypographyColor, +} from '../../typography/Typography'; +import { Pill, PillSize } from '../../Pill'; +import { SourceStackItem } from './SourceStackItem'; + +interface SourceStackSectionProps { + section: string; + items: SourceStack[]; + canEdit: boolean; + onEdit?: (item: SourceStack) => void; + onDelete?: (item: SourceStack) => void; +} + +export function SourceStackSection({ + section, + items, + canEdit, + onEdit, + onDelete, +}: SourceStackSectionProps): ReactElement { + return ( +
+
+ + {section} + + +
+
+ {items.map((item) => ( + + ))} +
+
+ ); +} diff --git a/packages/shared/src/components/squads/stack/SquadStack.tsx b/packages/shared/src/components/squads/stack/SquadStack.tsx new file mode 100644 index 0000000000..0e6c5995fb --- /dev/null +++ b/packages/shared/src/components/squads/stack/SquadStack.tsx @@ -0,0 +1,201 @@ +import type { ReactElement } from 'react'; +import React, { useState, useCallback } from 'react'; +import type { Squad } from '../../../graphql/sources'; +import { useSourceStack } from '../../../hooks/source/useSourceStack'; +import { + Typography, + TypographyType, + TypographyColor, +} from '../../typography/Typography'; +import { Button, ButtonSize, ButtonVariant } from '../../buttons/Button'; +import { PlusIcon } from '../../icons'; +import { SourceStackSection } from './SourceStackSection'; +import { SourceStackModal } from './SourceStackModal'; +import type { + SourceStack, + AddSourceStackInput, +} from '../../../graphql/source/sourceStack'; +import { useToastNotification } from '../../../hooks/useToastNotification'; +import { usePrompt } from '../../../hooks/usePrompt'; + +interface SquadStackProps { + squad: Squad; +} + +// Predefined section order +const SECTION_ORDER = [ + 'Primary', + 'Backend', + 'Frontend', + 'DevOps', + 'AI', + 'Development', + 'Design', + 'Productivity', +]; + +export function SquadStack({ squad }: SquadStackProps): ReactElement | null { + const { stackItems, groupedBySection, canEdit, add, update, remove } = + useSourceStack(squad); + const { displayToast } = useToastNotification(); + const { showPrompt } = usePrompt(); + + const [isModalOpen, setIsModalOpen] = useState(false); + const [editingItem, setEditingItem] = useState(null); + + const handleAdd = useCallback( + async (input: AddSourceStackInput) => { + try { + await add(input); + displayToast('Added to squad stack'); + } catch (error) { + displayToast('Failed to add item'); + throw error; + } + }, + [add, displayToast], + ); + + const handleEdit = useCallback((item: SourceStack) => { + setEditingItem(item); + setIsModalOpen(true); + }, []); + + const handleUpdate = useCallback( + async (input: AddSourceStackInput) => { + if (!editingItem) { + return; + } + try { + await update({ + id: editingItem.id, + input: { + section: input.section, + title: input.title, + }, + }); + displayToast('Stack item updated'); + } catch (error) { + displayToast('Failed to update item'); + throw error; + } + }, + [editingItem, update, displayToast], + ); + + const handleDelete = useCallback( + async (item: SourceStack) => { + const displayTitle = item.title ?? item.tool.title; + const confirmed = await showPrompt({ + title: 'Remove from stack?', + description: `Are you sure you want to remove "${displayTitle}" from the squad stack?`, + okButton: { title: 'Remove', variant: ButtonVariant.Primary }, + }); + if (!confirmed) { + return; + } + + try { + await remove(item.id); + displayToast('Removed from squad stack'); + } catch (error) { + displayToast('Failed to remove item'); + } + }, + [remove, displayToast, showPrompt], + ); + + const handleCloseModal = useCallback(() => { + setIsModalOpen(false); + setEditingItem(null); + }, []); + + // Sort sections: predefined first, then custom alphabetically + const sortedSections = Object.keys(groupedBySection).sort((a, b) => { + const aIndex = SECTION_ORDER.indexOf(a); + const bIndex = SECTION_ORDER.indexOf(b); + if (aIndex !== -1 && bIndex !== -1) { + return aIndex - bIndex; + } + if (aIndex !== -1) { + return -1; + } + if (bIndex !== -1) { + return 1; + } + return a.localeCompare(b); + }); + + const hasItems = stackItems.length > 0; + + if (!hasItems && !canEdit) { + return null; + } + + return ( +
+
+ + Stack & Tools + + {canEdit && ( + + )} +
+ + {hasItems ? ( +
+ {sortedSections.map((section) => ( + + ))} +
+ ) : ( + canEdit && ( +
+ + Share your squad's stack & tools + + +
+ ) + )} + + {isModalOpen && ( + + )} +
+ ); +} diff --git a/packages/shared/src/components/squads/stack/index.ts b/packages/shared/src/components/squads/stack/index.ts new file mode 100644 index 0000000000..ff3e731e21 --- /dev/null +++ b/packages/shared/src/components/squads/stack/index.ts @@ -0,0 +1,4 @@ +export { SquadStack } from './SquadStack'; +export { SourceStackSection } from './SourceStackSection'; +export { SourceStackItem } from './SourceStackItem'; +export { SourceStackModal } from './SourceStackModal'; diff --git a/packages/shared/src/graphql/source/sourceStack.ts b/packages/shared/src/graphql/source/sourceStack.ts new file mode 100644 index 0000000000..8b5b76224e --- /dev/null +++ b/packages/shared/src/graphql/source/sourceStack.ts @@ -0,0 +1,157 @@ +import { gql } from 'graphql-request'; +import type { Connection } from '../common'; +import { gqlClient } from '../common'; +import type { DatasetTool } from '../user/userStack'; + +export interface SourceStackCreatedBy { + id: string; + name: string; + image: string; +} + +export interface SourceStack { + id: string; + tool: DatasetTool; + section: string; + position: number; + icon: string | null; + title: string | null; + createdAt: string; + createdBy: SourceStackCreatedBy; +} + +export interface AddSourceStackInput { + title: string; + section: string; +} + +export interface UpdateSourceStackInput { + section?: string; + icon?: string; + title?: string; +} + +export interface ReorderSourceStackInput { + id: string; + position: number; +} + +const SOURCE_STACK_FRAGMENT = gql` + fragment SourceStackFragment on SourceStack { + id + section + position + icon + title + createdAt + tool { + id + title + faviconUrl + } + createdBy { + id + name + image + } + } +`; + +export const SOURCE_STACK_QUERY = gql` + query SourceStack($sourceId: ID!, $first: Int, $after: String) { + sourceStack(sourceId: $sourceId, first: $first, after: $after) { + edges { + node { + ...SourceStackFragment + } + } + pageInfo { + hasNextPage + endCursor + } + } + } + ${SOURCE_STACK_FRAGMENT} +`; + +const ADD_SOURCE_STACK_MUTATION = gql` + mutation AddSourceStack($sourceId: ID!, $input: AddSourceStackInput!) { + addSourceStack(sourceId: $sourceId, input: $input) { + ...SourceStackFragment + } + } + ${SOURCE_STACK_FRAGMENT} +`; + +const UPDATE_SOURCE_STACK_MUTATION = gql` + mutation UpdateSourceStack($id: ID!, $input: UpdateSourceStackInput!) { + updateSourceStack(id: $id, input: $input) { + ...SourceStackFragment + } + } + ${SOURCE_STACK_FRAGMENT} +`; + +const DELETE_SOURCE_STACK_MUTATION = gql` + mutation DeleteSourceStack($id: ID!) { + deleteSourceStack(id: $id) { + _ + } + } +`; + +const REORDER_SOURCE_STACK_MUTATION = gql` + mutation ReorderSourceStack( + $sourceId: ID! + $items: [ReorderSourceStackInput!]! + ) { + reorderSourceStack(sourceId: $sourceId, items: $items) { + ...SourceStackFragment + } + } + ${SOURCE_STACK_FRAGMENT} +`; + +export const getSourceStack = async ( + sourceId: string, + first = 50, +): Promise> => { + const result = await gqlClient.request<{ + sourceStack: Connection; + }>(SOURCE_STACK_QUERY, { sourceId, first }); + return result.sourceStack; +}; + +export const addSourceStack = async ( + sourceId: string, + input: AddSourceStackInput, +): Promise => { + const result = await gqlClient.request<{ + addSourceStack: SourceStack; + }>(ADD_SOURCE_STACK_MUTATION, { sourceId, input }); + return result.addSourceStack; +}; + +export const updateSourceStack = async ( + id: string, + input: UpdateSourceStackInput, +): Promise => { + const result = await gqlClient.request<{ + updateSourceStack: SourceStack; + }>(UPDATE_SOURCE_STACK_MUTATION, { id, input }); + return result.updateSourceStack; +}; + +export const deleteSourceStack = async (id: string): Promise => { + await gqlClient.request(DELETE_SOURCE_STACK_MUTATION, { id }); +}; + +export const reorderSourceStack = async ( + sourceId: string, + items: ReorderSourceStackInput[], +): Promise => { + const result = await gqlClient.request<{ + reorderSourceStack: SourceStack[]; + }>(REORDER_SOURCE_STACK_MUTATION, { sourceId, items }); + return result.reorderSourceStack; +}; diff --git a/packages/shared/src/hooks/source/index.ts b/packages/shared/src/hooks/source/index.ts index b09f7f2403..8147f2ee00 100644 --- a/packages/shared/src/hooks/source/index.ts +++ b/packages/shared/src/hooks/source/index.ts @@ -1,2 +1,3 @@ export * from './useSourceActions'; export * from './useSourceActionsNotify'; +export * from './useSourceStack'; diff --git a/packages/shared/src/hooks/source/useSourceStack.ts b/packages/shared/src/hooks/source/useSourceStack.ts new file mode 100644 index 0000000000..a9a508bdbf --- /dev/null +++ b/packages/shared/src/hooks/source/useSourceStack.ts @@ -0,0 +1,95 @@ +import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; +import { useMemo, useCallback } from 'react'; +import type { Squad, SourcePermissions } from '../../graphql/sources'; +import type { + SourceStack, + AddSourceStackInput, + UpdateSourceStackInput, + ReorderSourceStackInput, +} from '../../graphql/source/sourceStack'; +import { + getSourceStack, + addSourceStack, + updateSourceStack, + deleteSourceStack, + reorderSourceStack, +} from '../../graphql/source/sourceStack'; +import { generateQueryKey, RequestKey, StaleTime } from '../../lib/query'; +import { verifyPermission } from '../../graphql/squads'; +import { SourcePermissions as SourcePermissionsEnum } from '../../graphql/sources'; + +export function useSourceStack(squad: Squad | null) { + const queryClient = useQueryClient(); + const canEdit = squad + ? verifyPermission(squad, SourcePermissionsEnum.Edit) + : false; + + const queryKey = generateQueryKey(RequestKey.SourceStack, null, squad?.id); + + const query = useQuery({ + queryKey, + queryFn: () => getSourceStack(squad?.id as string), + staleTime: StaleTime.Default, + enabled: !!squad?.id, + }); + + const stackItems = useMemo( + () => query.data?.edges?.map(({ node }) => node) ?? [], + [query.data], + ); + + // Group items by section + const groupedBySection = useMemo(() => { + const groups: Record = {}; + stackItems.forEach((item) => { + if (!groups[item.section]) { + groups[item.section] = []; + } + groups[item.section].push(item); + }); + return groups; + }, [stackItems]); + + const invalidateQuery = useCallback(() => { + queryClient.invalidateQueries({ queryKey }); + }, [queryClient, queryKey]); + + const addMutation = useMutation({ + mutationFn: (input: AddSourceStackInput) => + addSourceStack(squad?.id as string, input), + onSuccess: invalidateQuery, + }); + + const updateMutation = useMutation({ + mutationFn: ({ id, input }: { id: string; input: UpdateSourceStackInput }) => + updateSourceStack(id, input), + onSuccess: invalidateQuery, + }); + + const deleteMutation = useMutation({ + mutationFn: (id: string) => deleteSourceStack(id), + onSuccess: invalidateQuery, + }); + + const reorderMutation = useMutation({ + mutationFn: (items: ReorderSourceStackInput[]) => + reorderSourceStack(squad?.id as string, items), + onSuccess: invalidateQuery, + }); + + return { + ...query, + stackItems, + groupedBySection, + canEdit, + queryKey, + add: addMutation.mutateAsync, + update: updateMutation.mutateAsync, + remove: deleteMutation.mutateAsync, + reorder: reorderMutation.mutateAsync, + isAdding: addMutation.isPending, + isUpdating: updateMutation.isPending, + isDeleting: deleteMutation.isPending, + isReordering: reorderMutation.isPending, + }; +} diff --git a/packages/shared/src/lib/query.ts b/packages/shared/src/lib/query.ts index d2a9e630cb..838d236596 100644 --- a/packages/shared/src/lib/query.ts +++ b/packages/shared/src/lib/query.ts @@ -231,6 +231,7 @@ export enum RequestKey { Autocomplete = 'autocomplete', UserExperience = 'user_experience', UserStack = 'user_stack', + SourceStack = 'source_stack', StackSearch = 'stack_search', UserHotTakes = 'user_hot_takes', UserTools = 'user_tools', From a3a7095e4149c7c601d3ce020723b1ab0bf6991c Mon Sep 17 00:00:00 2001 From: Chris Bongers Date: Wed, 28 Jan 2026 14:02:50 +0200 Subject: [PATCH 2/3] fix: remove section --- .../squads/stack/SourceStackModal.tsx | 70 +------------------ .../components/squads/stack/SquadStack.tsx | 45 ++---------- .../shared/src/graphql/source/sourceStack.ts | 4 -- .../shared/src/hooks/source/useSourceStack.ts | 13 ---- 4 files changed, 9 insertions(+), 123 deletions(-) diff --git a/packages/shared/src/components/squads/stack/SourceStackModal.tsx b/packages/shared/src/components/squads/stack/SourceStackModal.tsx index 80c0898025..9858a086df 100644 --- a/packages/shared/src/components/squads/stack/SourceStackModal.tsx +++ b/packages/shared/src/components/squads/stack/SourceStackModal.tsx @@ -6,7 +6,6 @@ import { zodResolver } from '@hookform/resolvers/zod'; import type { ModalProps } from '../../modals/common/Modal'; import { Modal } from '../../modals/common/Modal'; import { TextField } from '../../fields/TextField'; -import { Typography, TypographyType } from '../../typography/Typography'; import { Button, ButtonVariant } from '../../buttons/Button'; import { ModalHeader } from '../../modals/common/ModalHeader'; import { useViewSize, ViewSize } from '../../../hooks'; @@ -18,12 +17,8 @@ import type { DatasetTool } from '../../../graphql/user/userStack'; import { useStackSearch } from '../../../features/profile/hooks/useStackSearch'; import { PlusIcon } from '../../icons'; -const SECTION_OPTIONS = ['Primary', 'Backend', 'Frontend', 'DevOps', 'AI'] as const; - const sourceStackFormSchema = z.object({ title: z.string().min(1, 'Title is required').max(255), - section: z.string().min(1, 'Section is required').max(100), - customSection: z.string().max(100).optional(), }); type SourceStackFormData = z.infer; @@ -46,8 +41,6 @@ export function SourceStackModal({ resolver: zodResolver(sourceStackFormSchema), defaultValues: { title: existingItem?.title ?? existingItem?.tool.title ?? '', - section: existingItem?.section || 'Primary', - customSection: '', }, }); @@ -60,16 +53,10 @@ export function SourceStackModal({ } = methods; const title = watch('title'); - const section = watch('section'); - const customSection = watch('customSection'); const { results: suggestions } = useStackSearch(title); - const isCustomSection = !SECTION_OPTIONS.includes( - section as (typeof SECTION_OPTIONS)[number], - ); - const finalSection = isCustomSection ? customSection || section : section; - const canSubmit = title.trim().length > 0 && finalSection.trim().length > 0; + const canSubmit = title.trim().length > 0; const handleSelectSuggestion = (suggestion: DatasetTool) => { setValue('title', suggestion.title); @@ -77,13 +64,8 @@ export function SourceStackModal({ }; const onFormSubmit = handleSubmit(async (data) => { - const effectiveSection = isCustomSection - ? data.customSection || data.section - : data.section; - await onSubmit({ title: data.title.trim(), - section: effectiveSection.trim(), }); rest.onRequestClose?.(null); }); @@ -118,7 +100,7 @@ export function SourceStackModal({ size={Modal.Size.Small} {...rest} > -
+ {isEditing ? 'Edit Stack Item' : 'Add to Squad Stack'} @@ -188,54 +170,6 @@ export function SourceStackModal({ )} - {/* Section selector */} -
- - Section - -
- {SECTION_OPTIONS.map((opt) => ( - - ))} - -
- {isCustomSection && ( - - )} -
- {!isMobile && (