diff --git a/CHANGELOG.md b/CHANGELOG.md index c63c2d9c..936db7c2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [0.7.21.0] - 2026-10-18 + +### Changed + +- Settings now has the same sidebar as the other signed-in pages. +- Closing a project note puts the keyboard back on that card. +- The Add Repositories visibility filter is a list like the organization filter, and star counts use the same star icon as the rest of the app. + ## [0.7.20.0] - 2026-10-18 ### Changed diff --git a/TODOS.md b/TODOS.md index 1f8aa5cd..3f8fc4f1 100644 --- a/TODOS.md +++ b/TODOS.md @@ -45,20 +45,10 @@ From the `/ship` review of `fix/owner-filter-rollback-guard-sentry-tunnel`. None From `/design-review` of the change for issue #213. The review found 27 issues and fixed 21; these are what is left. All of them are in the app as it was before that change. The full report with screenshots is in `~/.gstack/projects/laststance-gitbox/designs/design-audit-20261012/`. -### Smaller design gaps left from the 2026-10-12 review - -**What:** (1) Replace `transition-all` (18 places) with the properties that change, and stop animating the sidebar's `width`, which from 768px moves the whole page on every frame (below, the sidebar lies over the page). (2) Return the focus to the card after every dialog that a card opens: `useCardDialogFocusReturn` is used by Move to Another Board and Remove from Board only. (3) Use one component for inline error messages and one pattern for a pending button; they are built by hand in each form. (4) `/settings` renders without the sidebar every other signed-in page has. (5) A card's menu has separators and a column's menu has none. (6) The picker has a native ` - setVisibilityFilter( - e.target.value as 'all' | 'public' | 'private', - ) + onValueChange={(value) => + setVisibilityFilter(value as 'all' | 'public' | 'private') } - className="border-input bg-background text-foreground focus:ring-primary rounded-md border px-3 py-1.5 text-sm focus:ring-1 focus:outline-none" - aria-label="Visibility filter" > - - - - + + + + + All + Public + Private + + {/* Selected repositories badges */} @@ -816,7 +822,13 @@ export const AddRepositoryCombobox = memo(function AddRepositoryCombobox({

)}
- ⭐ {repo.stargazers_count} + + {repo.language && ( {repo.language} @@ -869,7 +881,13 @@ export const AddRepositoryCombobox = memo(function AddRepositoryCombobox({

)}
- ⭐ {repo.stargazers_count} + + {repo.language && ( {repo.language} diff --git a/src/components/Board/ColumnInsertZone.tsx b/src/components/Board/ColumnInsertZone.tsx index 406843dc..cd867758 100644 --- a/src/components/Board/ColumnInsertZone.tsx +++ b/src/components/Board/ColumnInsertZone.tsx @@ -55,7 +55,7 @@ export const ColumnInsertZone = memo(
(
diff --git a/src/components/Board/CommentDisplay.tsx b/src/components/Board/CommentDisplay.tsx index 793d3058..e88cf568 100644 --- a/src/components/Board/CommentDisplay.tsx +++ b/src/components/Board/CommentDisplay.tsx @@ -174,7 +174,7 @@ export const CommentDisplay = memo( className={cn( 'border-muted-foreground/30 rounded-md border-2 border-dashed p-3', 'hover:border-muted-foreground/50 hover:bg-muted/10', - 'cursor-pointer transition-all duration-200', + 'cursor-pointer transition-colors duration-200', 'focus:ring-ring focus:ring-2 focus:ring-offset-2 focus:outline-none', className, )} @@ -205,7 +205,7 @@ export const CommentDisplay = memo( data-testid="comment-display" className={cn( // Base styles - full rounded card with background fill - 'cursor-pointer rounded-md p-3 transition-all duration-200', + 'cursor-pointer rounded-md p-3 transition-colors duration-200', // Combined card color (bg + border from COMMENT_CARD_COLORS) cardColorClass, // Focus styles diff --git a/src/components/Board/MoveHereDialog.tsx b/src/components/Board/MoveHereDialog.tsx index 932c0731..af00b283 100644 --- a/src/components/Board/MoveHereDialog.tsx +++ b/src/components/Board/MoveHereDialog.tsx @@ -37,6 +37,7 @@ import { import { useAppDispatch, useAppSelector } from '@/lib/redux/store' import type { BoardId, StatusListId } from '@/lib/types/brands' import { toMoveFailure } from '@/lib/utils/move-failure' +import { pendingButtonLabel } from '@/lib/utils/pending-button-label' import { reloadPage } from '@/lib/utils/reload-page' import { settleWithin } from '@/lib/utils/settle-within' @@ -247,7 +248,7 @@ export const MoveHereDialog = memo(function MoveHereDialog({ (failure !== null && !failure.isOutcomeKnown) } > - {isBusy ? 'Moving…' : 'Move Here'} + {pendingButtonLabel(isBusy, 'Move Here', 'Moving…')} diff --git a/src/components/Board/NewRowDropZone.tsx b/src/components/Board/NewRowDropZone.tsx index a7f9b3aa..a8c7c7ec 100644 --- a/src/components/Board/NewRowDropZone.tsx +++ b/src/components/Board/NewRowDropZone.tsx @@ -51,7 +51,7 @@ export const NewRowDropZone = memo( 'h-20 rounded-xl border-2 border-dashed', 'flex items-center justify-center', 'text-muted-foreground text-sm font-medium', - 'transition-all duration-200', + 'transition-[color,background-color,border-color,transform] duration-200', isOver ? 'border-primary bg-primary/10 text-link scale-[1.02]' : 'border-border/50 bg-background/30', diff --git a/src/components/Board/RepoCard.tsx b/src/components/Board/RepoCard.tsx index 8f095d90..8a2915cc 100644 --- a/src/components/Board/RepoCard.tsx +++ b/src/components/Board/RepoCard.tsx @@ -224,7 +224,7 @@ const RepoCardView = memo(function RepoCardView({ className={sortable ? 'cursor-grab active:cursor-grabbing' : undefined} > (
{/* StatusColumn renders the column content with draggable header */} diff --git a/src/components/Board/StatusColumn.tsx b/src/components/Board/StatusColumn.tsx index 08a82074..2828429b 100644 --- a/src/components/Board/StatusColumn.tsx +++ b/src/components/Board/StatusColumn.tsx @@ -11,6 +11,7 @@ import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, + DropdownMenuSeparator, DropdownMenuTrigger, } from '@/components/ui/dropdown-menu' import type { CommentData } from '@/lib/actions/project-info' @@ -159,6 +160,7 @@ export const StatusColumn = memo( Edit Column )} + {onEditStatus && onDeleteStatus && } {onDeleteStatus && ( onDeleteStatus(status.id)} diff --git a/src/components/Boards/BoardCard.tsx b/src/components/Boards/BoardCard.tsx index 097a209d..a8a00fa5 100644 --- a/src/components/Boards/BoardCard.tsx +++ b/src/components/Boards/BoardCard.tsx @@ -138,7 +138,7 @@ export const BoardCard = memo(function BoardCard({ <>
{/* Board link - covers the card but not the menu */} {hasRenameError ? ( - + message={renameState.errors?.name?.[0]} + /> ) : ( )} @@ -526,7 +525,7 @@ export const BoardSettingsDialog = memo(function BoardSettingsDialog({ disabled={isRenamePending || name.trim() === ''} aria-label={`Rename board to ${name}`} > - {isRenamePending ? 'Renaming…' : 'Rename'} + {pendingButtonLabel(isRenamePending, 'Rename', 'Renaming…')} @@ -823,7 +822,11 @@ export const BoardSettingsDialog = memo(function BoardSettingsDialog({ disabled={isDeletePending} aria-label={`Delete board ${boardName}`} > - {isDeletePending ? 'Deleting…' : 'Delete Board'} + {pendingButtonLabel( + isDeletePending, + 'Delete Board', + 'Deleting…', + )} diff --git a/src/components/Boards/DeleteBoardDialog.tsx b/src/components/Boards/DeleteBoardDialog.tsx index 616545b2..f86473d7 100644 --- a/src/components/Boards/DeleteBoardDialog.tsx +++ b/src/components/Boards/DeleteBoardDialog.tsx @@ -21,6 +21,7 @@ import { } from '@/components/ui/alert-dialog' import { Button } from '@/components/ui/button' import { deleteBoardAction, type DeleteBoardState } from '@/lib/actions/board' +import { pendingButtonLabel } from '@/lib/utils/pending-button-label' interface DeleteBoardDialogProps { /** Whether the dialog is open */ @@ -101,7 +102,7 @@ export const DeleteBoardDialog = memo(function DeleteBoardDialog({ disabled={isPending} aria-label={`Delete board ${boardName}`} > - {isPending ? 'Deleting…' : 'Delete Board'} + {pendingButtonLabel(isPending, 'Delete Board', 'Deleting…')} diff --git a/src/components/Boards/RenameBoardDialog.tsx b/src/components/Boards/RenameBoardDialog.tsx index cf0c7df0..8d470d0d 100644 --- a/src/components/Boards/RenameBoardDialog.tsx +++ b/src/components/Boards/RenameBoardDialog.tsx @@ -19,9 +19,11 @@ import { DialogHeader, DialogTitle, } from '@/components/ui/dialog' +import { FormError } from '@/components/ui/form-error' import { Input } from '@/components/ui/input' import { renameBoardAction, type RenameBoardState } from '@/lib/actions/board' import { NEAR_LIMIT_COUNTER_TEXT_CLASS } from '@/lib/constants/character-counter' +import { pendingButtonLabel } from '@/lib/utils/pending-button-label' import { BOARD_NAME_MAX_LENGTH } from '@/lib/validations/board' interface RenameBoardDialogProps { @@ -127,13 +129,7 @@ export const RenameBoardDialog = memo(function RenameBoardDialog({
{hasError ? ( - + ) : ( )} @@ -157,7 +153,7 @@ export const RenameBoardDialog = memo(function RenameBoardDialog({ disabled={isPending || name.trim() === ''} aria-label={`Rename board to ${name}`} > - {isPending ? 'Renaming…' : 'Rename'} + {pendingButtonLabel(isPending, 'Rename', 'Renaming…')} diff --git a/src/components/Boards/SortableBoardCard.tsx b/src/components/Boards/SortableBoardCard.tsx index d5ed6370..ef216e4c 100644 --- a/src/components/Boards/SortableBoardCard.tsx +++ b/src/components/Boards/SortableBoardCard.tsx @@ -67,7 +67,7 @@ export const SortableBoardCard = memo(function SortableBoardCard({
{/* Drag handle — visible on hover, positioned at left edge */} diff --git a/src/components/Modals/CreateDestinationBoardForm.tsx b/src/components/Modals/CreateDestinationBoardForm.tsx index 0be67233..399c3819 100644 --- a/src/components/Modals/CreateDestinationBoardForm.tsx +++ b/src/components/Modals/CreateDestinationBoardForm.tsx @@ -18,6 +18,7 @@ import { Button } from '@/components/ui/button' import { Input } from '@/components/ui/input' import { Label } from '@/components/ui/label' import { OUTCOME_UNKNOWN_TEXT_CLASS } from '@/lib/constants/outcome-unknown' +import { pendingButtonLabel } from '@/lib/utils/pending-button-label' import { reloadPage } from '@/lib/utils/reload-page' import type { BoardCreationAttempt } from '@/lib/utils/request-board-creation' import { BOARD_NAME_MAX_LENGTH, boardNameSchema } from '@/lib/validations/board' @@ -197,13 +198,9 @@ export const CreateDestinationBoardForm = memo( {recovery === 'create' ? ( ) : (
@@ -208,7 +205,7 @@ export const CreateLinkTypeDialog = memo(function CreateLinkTypeDialog({ disabled={isPending || !label.trim()} data-testid="submit-create-link-type" > - {isPending ? 'Creating…' : 'Create'} + {pendingButtonLabel(isPending, 'Create', 'Creating…')} diff --git a/src/components/Modals/DeleteAccountDialog.tsx b/src/components/Modals/DeleteAccountDialog.tsx index 592df604..771ca31d 100644 --- a/src/components/Modals/DeleteAccountDialog.tsx +++ b/src/components/Modals/DeleteAccountDialog.tsx @@ -27,6 +27,7 @@ import { Label } from '@/components/ui/label' import { deleteAccount } from '@/lib/actions/auth' import { forgetAccountOnThisBrowser } from '@/lib/redux/forget-account-on-this-browser' import { store } from '@/lib/redux/store' +import { pendingButtonLabel } from '@/lib/utils/pending-button-label' /** Confirmation text required to enable delete button */ const CONFIRMATION_TEXT = 'DELETE' @@ -136,7 +137,7 @@ export const DeleteAccountDialog = memo(function DeleteAccountDialog({ disabled={!isConfirmed || isPending} data-testid="confirm-delete-button" > - {isPending ? 'Deleting…' : 'Delete Account'} + {pendingButtonLabel(isPending, 'Delete Account', 'Deleting…')} diff --git a/src/components/Modals/MoveToAnotherBoardDialog.tsx b/src/components/Modals/MoveToAnotherBoardDialog.tsx index 24ddfe07..4c25b1b6 100644 --- a/src/components/Modals/MoveToAnotherBoardDialog.tsx +++ b/src/components/Modals/MoveToAnotherBoardDialog.tsx @@ -48,6 +48,7 @@ import { DialogHeader, DialogTitle, } from '@/components/ui/dialog' +import { FormError } from '@/components/ui/form-error' import { Label } from '@/components/ui/label' import { Select, @@ -76,6 +77,7 @@ import { import { useAppDispatch, useAppSelector } from '@/lib/redux/store' import type { RepoCardId } from '@/lib/types/brands' import { toMoveFailure } from '@/lib/utils/move-failure' +import { pendingButtonLabel } from '@/lib/utils/pending-button-label' import { reloadPage } from '@/lib/utils/reload-page' import { requestBoardCreation, @@ -392,13 +394,7 @@ export const MoveToAnotherBoardDialog = memo(function MoveToAnotherBoardDialog({ )) .with({ kind: 'load-failed' }, ({ message }) => (
- + diff --git a/src/components/Modals/NoteModal.stories.tsx b/src/components/Modals/NoteModal.stories.tsx index 7ad0ee17..5f9e80b0 100644 --- a/src/components/Modals/NoteModal.stories.tsx +++ b/src/components/Modals/NoteModal.stories.tsx @@ -9,7 +9,7 @@ */ import type { Meta, StoryObj } from '@storybook/nextjs-vite' -import { memo, useEffect } from 'react' +import { memo, useEffect, useState, type ComponentProps } from 'react' import { expect, userEvent, waitFor, fn } from 'storybook/test' import { updateDraftNote } from '@/lib/redux/slices/draftSlice' @@ -261,3 +261,135 @@ export const CharacterCountDisplays: Story = { }) }, } + +/** + * Card the note was opened from, plus a note that can actually close. + * Story args keep `isOpen` true; without this wrapper Cancel, the overlay + * and Save never unmount the dialog, so focus never returns. + */ +const ClosableNoteOnPage = memo(function ClosableNoteOnPage( + props: ComponentProps, +) { + const [open, setOpen] = useState(props.isOpen) + return ( + <> +
+ Fixture card +
+ { + setOpen(false) + props.onClose() + }} + /> + + ) +}) + +/** + * Cancel hands the keyboard back to the card, not the page. + */ +export const CancelReturnsFocusToTheCard: Story = { + args: { + isOpen: true, + onClose: fn(), + onSave: fn(), + cardId: toRepoCardId('card-test'), + initialNote: 'Test note', + initialLinks: [], + cardTitle: 'test/repo', + }, + render: (args) => , + play: async () => { + await waitFor(() => { + expect(document.querySelector('[role="dialog"]')).toBeTruthy() + }) + + await userEvent.click( + document.querySelector('[data-testid="cancel-button"]') as HTMLElement, + ) + + await waitFor(() => { + expect(document.querySelector('[role="dialog"]')).toBeNull() + }) + await waitFor(() => { + expect(document.activeElement).toHaveAttribute( + 'data-repo-card-id', + 'card-test', + ) + }) + }, +} + +/** + * A click on the overlay closes the note and focuses the card. + */ +export const OverlayReturnsFocusToTheCard: Story = { + args: { + isOpen: true, + onClose: fn(), + onSave: fn(), + cardId: toRepoCardId('card-test'), + initialNote: 'Test note', + initialLinks: [], + cardTitle: 'test/repo', + }, + render: (args) => , + play: async () => { + await waitFor(() => { + expect(document.querySelector('[role="dialog"]')).toBeTruthy() + }) + + const overlay = document.querySelector( + '[data-state="open"].fixed.inset-0', + ) as HTMLElement + await userEvent.click(overlay) + + await waitFor(() => { + expect(document.querySelector('[role="dialog"]')).toBeNull() + }) + await waitFor(() => { + expect(document.activeElement).toHaveAttribute( + 'data-repo-card-id', + 'card-test', + ) + }) + }, +} + +/** + * Save closes the note and focuses the card. + */ +export const SaveReturnsFocusToTheCard: Story = { + args: { + isOpen: true, + onClose: fn(), + onSave: fn().mockResolvedValue(undefined), + cardId: toRepoCardId('card-test'), + initialNote: 'Test note', + initialLinks: [], + cardTitle: 'test/repo', + }, + render: (args) => , + play: async () => { + await waitFor(() => { + expect(document.querySelector('[role="dialog"]')).toBeTruthy() + }) + + await userEvent.click( + document.querySelector('[data-testid="save-button"]') as HTMLElement, + ) + + await waitFor(() => { + expect(document.querySelector('[role="dialog"]')).toBeNull() + }) + await waitFor(() => { + expect(document.activeElement).toHaveAttribute( + 'data-repo-card-id', + 'card-test', + ) + }) + }, +} diff --git a/src/components/Modals/NoteModal.tsx b/src/components/Modals/NoteModal.tsx index df052c53..1105564d 100644 --- a/src/components/Modals/NoteModal.tsx +++ b/src/components/Modals/NoteModal.tsx @@ -15,9 +15,11 @@ import { DialogTitle, } from '@/components/ui/dialog' import { type ProjectLink } from '@/components/ui/editable-url-item' +import { useCardDialogFocusReturn } from '@/hooks/board/useCardDialogFocusReturn' import { useNoteModalDraft } from '@/hooks/board/useNoteModalDraft' import type { RepoCardId } from '@/lib/types/brands' import type { RepoIdentifier } from '@/lib/types/domain-primitives' +import { pendingButtonLabel } from '@/lib/utils/pending-button-label' interface NoteModalProps { /** Whether the modal is open */ @@ -104,12 +106,14 @@ export const NoteModal = memo(function NoteModal({ onClose, cardTitle, }) + const { handleCloseAutoFocus } = useCardDialogFocusReturn(cardId) return ( @@ -155,7 +159,7 @@ export const NoteModal = memo(function NoteModal({ disabled={isSaving || !isFormValid} data-testid="save-button" > - {isSaving ? 'Saving…' : 'Save'} + {pendingButtonLabel(isSaving, 'Save', 'Saving…')}
diff --git a/src/components/Modals/StatusListDialog.tsx b/src/components/Modals/StatusListDialog.tsx index 9878e5bf..601ffdd8 100644 --- a/src/components/Modals/StatusListDialog.tsx +++ b/src/components/Modals/StatusListDialog.tsx @@ -11,6 +11,7 @@ import { DialogFooter, DialogDescription, } from '@/components/ui/dialog' +import { FormError } from '@/components/ui/form-error' import { Input } from '@/components/ui/input' import { Label } from '@/components/ui/label' import type { StatusListDomain } from '@/lib/models/domain' @@ -136,7 +137,7 @@ const StatusListForm = memo(function StatusListForm({ type="button" onClick={() => setColor(preset.value)} data-color={preset.value} - className={`h-8 w-8 rounded-full border-2 transition-all ${color === preset.value ? 'border-foreground scale-110' : 'border-transparent'} `} + className={`h-8 w-8 rounded-full border-2 transition-[border-color,transform] ${color === preset.value ? 'border-foreground scale-110' : 'border-transparent'} `} style={{ backgroundColor: preset.value }} title={preset.name} aria-label={preset.name} @@ -164,7 +165,7 @@ const StatusListForm = memo(function StatusListForm({
{/* Error */} - {error &&

{error}

} +
)} - {error && ( - - )} +
) }) diff --git a/src/components/ui/font-color-toolbar-button.tsx b/src/components/ui/font-color-toolbar-button.tsx index e67e6faf..3108ec04 100644 --- a/src/components/ui/font-color-toolbar-button.tsx +++ b/src/components/ui/font-color-toolbar-button.tsx @@ -41,7 +41,7 @@ function ColorDropdownMenuItem({ size: 'icon', variant: 'outline', }), - 'border-muted my-1 flex size-6 items-center justify-center rounded-full border border-solid p-0 transition-all hover:scale-125', + 'border-muted my-1 flex size-6 items-center justify-center rounded-full border border-solid p-0 transition-transform hover:scale-125', !isBrightColor && 'border-transparent', isSelected && 'border-primary border-2', className, diff --git a/src/components/ui/form-error.tsx b/src/components/ui/form-error.tsx new file mode 100644 index 00000000..d542dba7 --- /dev/null +++ b/src/components/ui/form-error.tsx @@ -0,0 +1,47 @@ +'use client' + +import { memo } from 'react' + +/** + * Destructive inline error under a form field. + * + * One class and `role="alert"` for every refused write. Unconfirmed / + * unknown-outcome copy stays `text-warning` and must not use this. + * + * Called by rename, create-link, board settings, column edit, URL items, + * and Move to Another Board. + * + * @param message - The sentence to show. Empty or missing renders nothing. + * @param id - Optional id for `aria-describedby` on the field that failed. + * @param testId - Optional `data-testid`. + * + * @example + * ```ts + * + * ``` + */ +export const FormError = memo(function FormError({ + message, + id, + testId, +}: { + message?: string | null + /** For `aria-describedby` on the field that failed. */ + id?: string + testId?: string +}) { + if (!message) { + return null + } + + return ( + + ) +}) diff --git a/src/hooks/board/useCardDialogFocusReturn.ts b/src/hooks/board/useCardDialogFocusReturn.ts index 78c60e7d..0451ef59 100644 --- a/src/hooks/board/useCardDialogFocusReturn.ts +++ b/src/hooks/board/useCardDialogFocusReturn.ts @@ -18,8 +18,8 @@ import { /** * Keeps a keyboard user's place on the board when a card dialog closes: on the * card when it stays, on its neighbor when it was moved away or removed. - * Called by {@link MoveToAnotherBoardDialog} and by {@link OverflowMenu} for - * the Remove from Board confirmation. + * Called by {@link MoveToAnotherBoardDialog}, {@link NoteModal}, and by + * {@link OverflowMenu} for the Remove from Board confirmation. * * @param cardId - Card the dialog was opened for. * @returns diff --git a/src/lib/utils/pending-button-label.ts b/src/lib/utils/pending-button-label.ts new file mode 100644 index 00000000..4600dbed --- /dev/null +++ b/src/lib/utils/pending-button-label.ts @@ -0,0 +1,23 @@ +/** + * Idle or pending label for a submit control. + * + * Forms used to write `{isPending ? 'Creating…' : 'Create Board'}` by hand. + * {@link pendingButtonLabel} keeps the Title Case idle command and the + * ellipsis pending form in one place. Callers that show a spinner still + * render that icon themselves. + * + * @param isPending - Whether the request is in flight. + * @param idle - Title Case command, e.g. `Create Board`. + * @param pending - Ellipsis form, e.g. `Creating…`. + * @returns {@link pending} when in flight, otherwise {@link idle}. + * + * @example + * pendingButtonLabel(true, 'Create Board', 'Creating…') + */ +export function pendingButtonLabel( + isPending: boolean, + idle: string, + pending: string, +): string { + return isPending ? pending : idle +} diff --git a/src/tests/unit/components/Board/AddRepositoryCombobox.placements.test.tsx b/src/tests/unit/components/Board/AddRepositoryCombobox.placements.test.tsx index 91b42183..6f3c6288 100644 --- a/src/tests/unit/components/Board/AddRepositoryCombobox.placements.test.tsx +++ b/src/tests/unit/components/Board/AddRepositoryCombobox.placements.test.tsx @@ -1319,10 +1319,10 @@ describe('AddRepositoryCombobox with repositories placed elsewhere', () => { await findRepositoryOptions() // Act - await user.selectOptions( + await user.click( screen.getByRole('combobox', { name: 'Visibility filter' }), - 'Private', ) + await user.click(screen.getByRole('option', { name: 'Private' })) // Assert expect( @@ -1334,6 +1334,35 @@ describe('AddRepositoryCombobox with repositories placed elsewhere', () => { ).toBeNull() }) + // Value: protects=star counts use a lucide icon and Visibility is a Radix + // select; fails_when=the row paints ⭐ or the filter is a native