From bd5a1b7f0d5ea88f83c6e5541b3b069c18ded187 Mon Sep 17 00:00:00 2001 From: Imants Date: Tue, 14 Jul 2026 18:53:56 +0300 Subject: [PATCH 01/27] feat: update snippet card shell to finalized card design --- src/css/common/_cards.scss | 60 +++++++++++++++++++++--- src/css/manage/_cloud-community.scss | 2 +- src/js/components/common/SnippetCard.tsx | 16 +++++-- 3 files changed, 68 insertions(+), 10 deletions(-) diff --git a/src/css/common/_cards.scss b/src/css/common/_cards.scss index f0b53c2c6..f426c6f44 100644 --- a/src/css/common/_cards.scss +++ b/src/css/common/_cards.scss @@ -8,7 +8,7 @@ .code-snippets-card { background: #fff; border: 1px solid #c3c4c7; - border-radius: 6px; + border-radius: 5px; margin: 0; display: flex; flex-flow: column; @@ -25,15 +25,32 @@ footer { display: flex; + align-items: center; + justify-content: space-between; gap: 8px; - background: #f6f7f7; + background: #f7f7f8; margin-block-start: auto; border-block-start: 1px solid #c3c4c7; padding-inline: 24px; padding-block: 12px; + border-end-start-radius: 5px; + border-end-end-radius: 5px; + } + + .snippet-card-footer-status { + display: flex; align-items: center; - border-end-start-radius: 6px; - border-end-end-radius: 6px; + gap: 8px; + } + + .snippet-card-footer-actions { + display: flex; + align-items: center; + justify-content: flex-end; + flex-wrap: wrap; + gap: 8px; + flex: 1 1 auto; + min-inline-size: 0; } &.is-selectable, @@ -54,11 +71,42 @@ } } - &.is-selectable .card-inner h3 { + input[type='checkbox'].snippet-card-select { + appearance: none; + display: grid; + place-content: center; + inline-size: 20px; + block-size: 20px; + padding: 0; + box-sizing: border-box; + background: #fff; + border: 1.5px solid #2271b1; + border-radius: 5px; + box-shadow: 0 2px 2px rgb(0 0 0 / 5%); + cursor: pointer; + + &::before { + content: none; + } + + &:checked { + background: #2271b1; + + &::before { + content: ''; + inline-size: 14px; + block-size: 14px; + margin: 0; + background: url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20'%3E%3Cpath d='M14.83 4.89l1.34.94-5.81 8.38H9.02L5.78 9.67l1.34-1.25 2.57 2.4z' fill='%23fff'/%3E%3C/svg%3E") center/14px no-repeat; + } + } + } + + &.is-selectable .card-inner > h3 { margin-inline-end: 48px; } - &.has-corner-controls .card-inner h3 { + &.has-corner-controls .card-inner > h3 { margin-inline-end: 96px; } diff --git a/src/css/manage/_cloud-community.scss b/src/css/manage/_cloud-community.scss index c23c88d34..cd007dfd1 100644 --- a/src/css/manage/_cloud-community.scss +++ b/src/css/manage/_cloud-community.scss @@ -165,7 +165,7 @@ $status-colors: ( } } -.cloud-search-results .cloud-search-result footer > .components-spinner { +.cloud-search-results .cloud-search-result footer .components-spinner { margin: 0; } diff --git a/src/js/components/common/SnippetCard.tsx b/src/js/components/common/SnippetCard.tsx index e00748325..128fb401c 100644 --- a/src/js/components/common/SnippetCard.tsx +++ b/src/js/components/common/SnippetCard.tsx @@ -9,6 +9,7 @@ export interface SnippetCardProps { selectionLabel?: string cornerControls?: ReactNode footer?: ReactNode + footerStatus?: ReactNode children: ReactNode } @@ -16,8 +17,11 @@ export interface SnippetCardProps { * Shared card chrome for displaying a snippet in a card grid: border, inner * padding, footer strip, and a top corner holding optional extra controls * (such as an activation toggle) plus a selection checkbox for bulk actions. - * Both cloud search results and local snippet cards render inside this shell - * so the two views stay visually consistent. + * The footer is split into a status region at the inline start and an actions + * region at the inline end; the status region is always rendered so actions + * stay end-aligned even when no status is provided. Both cloud search results + * and local snippet cards render inside this shell so the two views stay + * visually consistent. */ export const SnippetCard: React.FC = ({ className, @@ -26,6 +30,7 @@ export const SnippetCard: React.FC = ({ selectionLabel, cornerControls, footer, + footerStatus, children }) =>
  • = ({ {children} - {footer ?
    {footer}
    : null} + {undefined !== footer || undefined !== footerStatus + ?
    +
    {footerStatus}
    +
    {footer}
    +
    + : null}
  • From a8c29fbdcb5507c5315e991721502d125f425c1b Mon Sep 17 00:00:00 2001 From: Imants Date: Tue, 14 Jul 2026 18:54:50 +0300 Subject: [PATCH 02/27] feat: extract snippet delete confirmation into reusable hook --- src/js/components/common/DeleteButton.tsx | 73 +++++++++++++++-------- 1 file changed, 49 insertions(+), 24 deletions(-) diff --git a/src/js/components/common/DeleteButton.tsx b/src/js/components/common/DeleteButton.tsx index 61f893706..42173bdc1 100644 --- a/src/js/components/common/DeleteButton.tsx +++ b/src/js/components/common/DeleteButton.tsx @@ -4,6 +4,7 @@ import { createInterpolateElement } from '@wordpress/element' import { useSnippetsAPI } from '../../hooks/useSnippetsAPI' import { Button } from './Button' import { ConfirmDialog } from './ConfirmDialog' +import type { ReactNode } from 'react' import type { Snippet } from '../../types/Snippet' import type { ButtonProps } from './Button' @@ -31,21 +32,25 @@ const PermanentDeleteConfirmMessage = () =>

    {__('This action cannot be undone.', 'code-snippets')}

    -export interface DeleteButtonProps extends ButtonProps { +export interface UseDeleteSnippetOptions { snippet: Snippet setIsWorking?: (isWorking: boolean) => void onSuccess?: () => Promise | void onError?: (error: unknown) => void } -export const DeleteButton: React.FC = ({ - snippet, - onSuccess, - onError, - className = 'delete-button', - setIsWorking, - ...buttonProps -}) => { +export interface UseDeleteSnippetResult { + requestDelete: () => void + confirmDialog: ReactNode +} + +/** + * Deletion flow shared by every control that removes a snippet: active and + * trashed snippets prompt for confirmation before the API call, while + * inactive snippets are trashed immediately. Callers must render the returned + * dialog somewhere that stays mounted while the confirmation is open. + */ +export const useDeleteSnippet = ({ snippet, setIsWorking, onSuccess, onError }: UseDeleteSnippetOptions): UseDeleteSnippetResult => { const snippetsAPI = useSnippetsAPI() const [isDialogOpen, setIsDialogOpen] = useState(false) @@ -58,7 +63,7 @@ export const DeleteButton: React.FC = ({ .finally(() => setIsWorking?.(false)) } - const handleButtonClick = () => { + const requestDelete = () => { if (snippet.active || snippet.trashed) { setIsDialogOpen(true) } else { @@ -66,25 +71,45 @@ export const DeleteButton: React.FC = ({ } } + const confirmDialog = + setIsDialogOpen(false)} + onConfirm={() => { + setIsDialogOpen(false) + handleDelete() + }} + > + {snippet.trashed ? : } + + + return { requestDelete, confirmDialog } +} + +export interface DeleteButtonProps extends ButtonProps, Omit { + snippet: Snippet +} + +export const DeleteButton: React.FC = ({ + snippet, + onSuccess, + onError, + className = 'delete-button', + setIsWorking, + ...buttonProps +}) => { + const { requestDelete, confirmDialog } = useDeleteSnippet({ snippet, setIsWorking, onSuccess, onError }) + return ( <> - - setIsDialogOpen(false)} - onConfirm={() => { - setIsDialogOpen(false) - handleDelete() - }} - > - {snippet.trashed ? : } - + {confirmDialog} ) } From 944f25751570e5fd0c70f49562bb6150fbfaa98c Mon Sep 17 00:00:00 2001 From: Imants Date: Tue, 14 Jul 2026 18:55:57 +0300 Subject: [PATCH 03/27] feat: reveal card selection checkbox on hover and active selection --- src/css/manage/_snippets-table.scss | 20 ++++++++++++++++--- .../SnippetsTable/SnippetsCardGrid.tsx | 7 ++++++- 2 files changed, 23 insertions(+), 4 deletions(-) diff --git a/src/css/manage/_snippets-table.scss b/src/css/manage/_snippets-table.scss index 35b28bedd..b85041665 100644 --- a/src/css/manage/_snippets-table.scss +++ b/src/css/manage/_snippets-table.scss @@ -97,13 +97,27 @@ .code-snippets-card { min-inline-size: 0; + } + + // Reveal the bulk-select checkbox on hover or keyboard focus, and keep + // every checkbox visible while any card in the grid is selected so an + // in-progress selection is never hidden. + .snippet-card-select { + opacity: 0; + transition: opacity 0.15s ease; - footer { - flex-wrap: wrap; - justify-content: flex-end; + @media (prefers-reduced-motion: reduce) { + transition-duration: 0.01s; } } + .snippet-card-select:checked, + &.has-selection .snippet-card-select, + .code-snippets-card:hover .snippet-card-select, + .code-snippets-card:focus-within .snippet-card-select { + opacity: 1; + } + // Match the light-blue tint that active snippet rows receive in the // table view (.active-snippet td/th). .code-snippets-card.active-snippet { diff --git a/src/js/components/ManageMenu/SnippetsTable/SnippetsCardGrid.tsx b/src/js/components/ManageMenu/SnippetsTable/SnippetsCardGrid.tsx index 989d59af4..47e80186e 100644 --- a/src/js/components/ManageMenu/SnippetsTable/SnippetsCardGrid.tsx +++ b/src/js/components/ManageMenu/SnippetsTable/SnippetsCardGrid.tsx @@ -1,3 +1,4 @@ +import classnames from 'classnames' import React, { useMemo, useState } from 'react' import { __ } from '@wordpress/i18n' import { getSnippetDisplayName, getSnippetType } from '../../../utils/snippets/snippets' @@ -71,7 +72,11 @@ interface CardGridProps { const CardGrid: React.FC = ({ snippets, noItems, selected, setSelected }) => 0 < snippets.length - ?