diff --git a/src/css/common/_cards.scss b/src/css/common/_cards.scss index 57efd0ba..cd816bdc 100644 --- a/src/css/common/_cards.scss +++ b/src/css/common/_cards.scss @@ -30,7 +30,7 @@ align-items: center; justify-content: space-between; gap: 8px; - background: #f7f7f8; + background: theme.$background-light; margin-block-start: auto; border-block-start: 1px solid theme.$control-border; padding-inline: 24px; diff --git a/src/css/common/_modal.scss b/src/css/common/_modal.scss index 3a90d67f..07aac5aa 100644 --- a/src/css/common/_modal.scss +++ b/src/css/common/_modal.scss @@ -53,13 +53,31 @@ } .components-modal__frame.code-snippets-preview-modal { - min-inline-size: 520px; + min-inline-size: min(520px, 90vw); min-block-size: 240px; inline-size: min(900px, 90vw); max-block-size: 85vh; - @media (width <= 600px) { - min-inline-size: 90vw; + @media (width <= 480px) { + min-inline-size: 0; + min-block-size: 0; + inline-size: calc(100vw - 36px); + block-size: calc(100vh - 36px); + max-inline-size: calc(100vw - 36px); + max-block-size: calc(100vh - 36px); + margin: auto; + + .components-modal__header-heading-container { + min-inline-size: 0; + box-sizing: border-box; + padding-inline-end: 64px; + } + + .components-modal__header-heading { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + } } .components-modal__header { diff --git a/src/css/common/_page-header.scss b/src/css/common/_page-header.scss index 81a8c10e..d25a0ae7 100644 --- a/src/css/common/_page-header.scss +++ b/src/css/common/_page-header.scss @@ -43,3 +43,20 @@ text-decoration: underline; } } + +@media (width <= 480px) { + .snippets-page-header { + align-items: flex-start; + flex-direction: column; + gap: 16px; + margin-block: 17px 24px; + margin-inline: 8px; + + h1, + h2 { + font-size: 28px; + font-weight: 590; + letter-spacing: -0.56px; + } + } +} diff --git a/src/css/common/_subnav.scss b/src/css/common/_subnav.scss index 4b5ecf5f..15be18a2 100644 --- a/src/css/common/_subnav.scss +++ b/src/css/common/_subnav.scss @@ -11,7 +11,7 @@ // The toolbar band above already draws the 1px separator, so only a // bottom border here — a top border would double the line weight. - border-block-end: 1px solid #e2e2e4; + border-block-end: 1px solid theme.$border-light; min-block-size: 53px; // Bleed across the full admin content width, flush against the plugin @@ -88,7 +88,7 @@ color: #646970; text-decoration: none; box-sizing: border-box; - border-inline-end: 1px solid #e2e2e4; + border-inline-end: 1px solid theme.$border-light; // The text-only "All Snippets" tab uses a slightly wider gap between // its label and count than tabs that lead with a badge or icon. @@ -138,6 +138,14 @@ } +.snippet-type-nav-container { + position: relative; +} + +.snippet-type-nav-fade { + display: none; +} + // Card/table view toggle: a two-icon segmented pair of square tiles with // the active view filled in the accent colour. .snippet-view-toggle { @@ -240,3 +248,22 @@ margin-block-start: 20px; border-block-start: 1px solid theme.$control-border; } + +@media (width <= 480px) { + .snippet-type-nav-fade { + display: block; + position: absolute; + z-index: 1; + inset-block-start: -10px; + inset-inline-end: -12px; + inline-size: 66px; + block-size: 52px; + background: linear-gradient(90deg, rgb(255 255 255 / 0%), #fff); + pointer-events: none; + transform: scaleX(var(--cs-direction-multiplier)); + } + + .snippets-screen-meta-slot { + display: none; + } +} diff --git a/src/css/common/_theme.scss b/src/css/common/_theme.scss index a836dce5..3c266010 100644 --- a/src/css/common/_theme.scss +++ b/src/css/common/_theme.scss @@ -12,6 +12,8 @@ $cloud-update: #ff9800; // Plugin admin control tokens, applied to native form controls on all plugin // screens regardless of WordPress version (see common/_wp-admin.scss). $accent-hover: #0a4b78; +$background-light: #f7f7f8; +$border-light: #e2e2e4; $control-border: #c3c4c7; $control-text: #2c3338; $control-height: 38px; diff --git a/src/css/common/_toolbar.scss b/src/css/common/_toolbar.scss index 2fe2b16d..935e8895 100644 --- a/src/css/common/_toolbar.scss +++ b/src/css/common/_toolbar.scss @@ -66,7 +66,7 @@ $wpcontent-inline-start-indent: 20px; } .code-snippets-toolbar-lower { - border-block-end: 1px solid #e2e2e4; + border-block-end: 1px solid theme.$border-light; block-size: 60px; box-sizing: border-box; @@ -201,3 +201,60 @@ $wpcontent-inline-start-indent: 20px; .code-snippets-return-link { float: inline-end; } + +@media (width <= 480px) { + #wpbody { + padding-block-start: 143px; + } + + .code-snippets-toolbar { + block-size: 143px; + } + + .code-snippets-toolbar-upper nav { + display: none; + } + + .code-snippets-toolbar-lower { + block-size: 67px; + + nav { + overflow-x: auto; + scrollbar-width: none; + + &::-webkit-scrollbar { + display: none; + } + } + + ul { + min-inline-size: 100%; + } + + li { + flex: 1 0 92px; + } + + li a { + inline-size: 100%; + padding-block: 8px; + padding-inline: 12px; + gap: 4px; + border-block-end: 3px solid transparent; + font-size: 14px; + font-weight: 700; + + &.active-link { + border-block-end-color: theme.$accent; + } + } + + li:not(.toolbar-end-item) + li.toolbar-end-item { + margin-inline-start: 0; + } + + .cloud-library-item { + display: none; + } + } +} diff --git a/src/css/common/list-table/_layout.scss b/src/css/common/list-table/_layout.scss index 46a72eb4..8ca83156 100644 --- a/src/css/common/list-table/_layout.scss +++ b/src/css/common/list-table/_layout.scss @@ -93,6 +93,11 @@ position: relative; inset-inline-start: 0; + .row-action, + .row-action-separator { + font-size: inherit; + } + // Row-action buttons render as plain inline links. On WP 7.0 the generic `.button` // compatibility styling would otherwise give them a fill, border, radius and fixed // height, so fully neutralise it here (this rule is emitted after the wp-admin layer diff --git a/src/css/common/list-table/_pagination.scss b/src/css/common/list-table/_pagination.scss index fd42d4d3..ff1c8923 100644 --- a/src/css/common/list-table/_pagination.scss +++ b/src/css/common/list-table/_pagination.scss @@ -70,7 +70,7 @@ } .wrap .snippets-search-area input[type='search'] { - border-color: #e3e3e4; + border-color: theme.$border-light; } // Tablet widths: let the search area grow to fill the remainder of its row diff --git a/src/css/manage.scss b/src/css/manage.scss index 79c48128..a34945f2 100644 --- a/src/css/manage.scss +++ b/src/css/manage.scss @@ -15,6 +15,9 @@ @use 'manage/snippets-table'; @use 'manage/cloud-community'; @use 'manage/cloud-community-cards'; +@use 'manage/mobile-cards'; +@use 'manage/mobile-list'; +@use 'manage/mobile-controls'; .tablenav .tablenav-pages { margin-block-end: 0; diff --git a/src/css/manage/_mobile-cards.scss b/src/css/manage/_mobile-cards.scss new file mode 100644 index 00000000..d2c63325 --- /dev/null +++ b/src/css/manage/_mobile-cards.scss @@ -0,0 +1,20 @@ +@media (width <= 480px) { + .snippets-card-grid-container { + margin-inline: 8px 6px; + } + + .snippets-card-grid { + .snippet-card-corner { + display: none; + } + + .snippet-card-header { + padding-inline-end: 0; + + h3 { + font-size: 16px; + font-weight: 590; + } + } + } +} diff --git a/src/css/manage/_mobile-controls.scss b/src/css/manage/_mobile-controls.scss new file mode 100644 index 00000000..c13be018 --- /dev/null +++ b/src/css/manage/_mobile-controls.scss @@ -0,0 +1,178 @@ +@use '../common/theme'; + +.snippets-list-view .snippets-search-submit, +.mobile-clear-recently-active, +.mobile-paging-input { + display: none; +} + +.snippets-list-view .tablenav .snippets-tag-filter-bottom { + display: none; +} + +@media (width <= 480px) { + .snippets-table-toolbar { + flex-direction: column; + align-items: flex-start; + gap: 8px; + margin-block-end: 0; + margin-inline: 8px 6px; + + .subsubsub { + overflow-x: auto; + flex-wrap: nowrap; + max-inline-size: 100%; + white-space: nowrap; + scrollbar-width: none; + + &::-webkit-scrollbar { + display: none; + } + } + + .mobile-clear-recently-active { + display: block; + + .button { + block-size: auto; + min-block-size: 0; + padding: 0; + border: 0; + background: transparent; + box-shadow: none; + color: theme.$accent; + font-size: 13px; + font-weight: 400; + line-height: 1.5; + + &:hover, + &:focus { + border: 0; + background: transparent; + box-shadow: none; + color: theme.$accent-hover; + } + } + } + } + + .snippets-list-view .tablenav.top { + flex-wrap: nowrap; + gap: 12px; + margin-block: 16px; + + .bulkactions, + .tablenav-select-all, + .snippets-tag-filter-top, + .desktop-clear-recently-active, + .tablenav-pages-nav { + display: none; + } + + .snippets-search-area { + flex: 1 1 0; + inline-size: auto; + min-inline-size: 0; + max-inline-size: none; + + search { + display: flex; + flex: 1 1 auto; + align-items: center; + gap: 8px; + min-inline-size: 0; + } + + input[type='search'] { + flex: 1 1 0; + min-inline-size: 0; + border-color: theme.$border-light; + } + } + + .snippets-search-submit { + display: inline-flex; + flex: 0 0 auto; + align-items: center; + justify-content: center; + background: #fff; + } + + .tablenav-end-group { + flex: 0 0 auto; + gap: 0; + margin-inline-start: 0; + } + } + + .snippets-table-view .tablenav.bottom { + align-items: center; + gap: 24px 12px; + margin-block: 16px 0; + + .bulkactions { + display: flex; + flex: 0 0 200px; + gap: 8px; + + select { + flex: 0 0 154px; + inline-size: 154px; + min-inline-size: 0; + max-inline-size: 154px; + } + + .button.action { + flex: 0 0 38px; + inline-size: 38px; + padding: 0; + } + } + + .snippets-tag-filter-bottom { + display: flex; + flex: 0 0 154px; + + select { + flex: 0 0 154px; + inline-size: 154px; + min-inline-size: 0; + max-inline-size: 154px; + } + } + + .tablenav-end-group { + flex: 1 0 100%; + order: 2; + margin-inline-start: 0; + } + + .tablenav-pages .displaying-num { + display: inline; + } + + .tablenav-pages-nav { + inline-size: 100%; + min-inline-size: 0; + } + + .tablenav-pages { + flex-wrap: wrap; + } + } + + .snippets-card-view .tablenav.bottom { + .bulkactions, + .snippets-tag-filter-bottom { + display: none; + } + } + + .desktop-paging-input { + display: none; + } + + .mobile-paging-input { + display: inline-flex; + } +} diff --git a/src/css/manage/_mobile-list.scss b/src/css/manage/_mobile-list.scss new file mode 100644 index 00000000..54fd1487 --- /dev/null +++ b/src/css/manage/_mobile-list.scss @@ -0,0 +1,275 @@ +@use '../common/theme'; + +.mobile-column-title, +.mobile-cell-label, +.mobile-row-toggle { + display: none; +} + +@media (width <= 480px) { + .snippets-table-view { + margin-inline: 8px 6px; + } + + .snippets-list-view .wp-list-table { + display: block; + overflow: hidden; + inline-size: 100%; + border: 1px solid theme.$control-border; + border-radius: 5px; + box-sizing: border-box; + + thead, + tbody, + tfoot { + display: block; + } + + thead tr, + tfoot tr { + display: grid; + grid-template-columns: 20px minmax(0, 1fr); + align-items: center; + gap: 16px; + min-block-size: 37px; + padding-block: 8px; + padding-inline: 16px; + background: theme.$background-light; + box-sizing: border-box; + } + + thead td, + thead th, + tfoot td, + tfoot th { + display: none; + padding: 0; + background: theme.$background-light; + } + + thead .check-column, + tfoot .check-column { + display: flex; + grid-column: 1; + align-items: center; + inline-size: 20px; + border: 0; + } + + thead .column-name, + tfoot .column-name { + display: block; + grid-column: 2; + inline-size: auto; + min-inline-size: 0; + font-size: 14px; + font-weight: 590; + line-height: 1.5; + color: #2c3337; + } + + thead th.sortable .list-table-sort-button, + thead th.sorted .list-table-sort-button, + tfoot th.sortable .list-table-sort-button, + tfoot th.sorted .list-table-sort-button { + gap: 4px; + padding: 0; + color: #2c3337; + } + + thead input[type='checkbox'], + tfoot input[type='checkbox'], + tbody input[type='checkbox']:not(.switch) { + inline-size: 20px; + block-size: 20px; + margin: 0; + } + + .desktop-column-title { + display: none; + } + + .mobile-column-title { + display: inline; + } + + tbody tr.snippet { + position: relative; + display: grid; + grid-template-columns: 20px 36px minmax(0, 1fr); + column-gap: 16px; + align-items: start; + overflow: hidden; + block-size: 64px; + padding-block: 20px; + padding-inline: 16px; + border-block-end: 1px solid theme.$border-light; + background: #fff; + box-sizing: border-box; + } + + tbody tr.snippet > th, + tbody tr.snippet > td { + display: none; + padding: 0 !important; + background: transparent !important; + border: 0; + box-shadow: none !important; + } + + tbody tr.snippet > .check-column, + tbody tr.snippet > .column-activate, + tbody tr.snippet > .column-name { + display: block; + } + + tbody tr.snippet > .check-column { + grid-column: 1; + inline-size: 20px; + } + + tbody tr.snippet > .column-activate { + grid-column: 2; + inline-size: 36px; + + input.switch { + margin: 0; + } + } + + tbody tr.snippet > .column-name { + grid-column: 3; + min-inline-size: 0; + padding-inline-end: 32px !important; + font-size: 16px; + font-weight: 590; + line-height: 1.5; + + > .snippet-name { + display: block; + overflow: hidden; + max-inline-size: 100%; + color: theme.$accent; + text-overflow: ellipsis; + white-space: nowrap; + } + + .row-actions { + display: none; + margin-block-start: 16px; + font-size: 13px; + font-weight: 400; + line-height: 20px; + white-space: nowrap; + } + } + + .mobile-row-toggle { + position: absolute; + inset-block-start: 12px; + inset-inline-end: 8px; + display: flex; + align-items: center; + justify-content: center; + inline-size: 40px; + block-size: 40px; + padding: 0; + border: 0; + background: transparent; + color: #646970; + cursor: pointer; + + .dashicons { + inline-size: 24px; + block-size: 24px; + font-size: 24px; + } + + &:focus-visible { + outline: 2px solid theme.$accent; + outline-offset: 2px; + } + } + + tbody tr.is-mobile-expanded { + block-size: auto; + row-gap: 16px; + + .mobile-row-toggle .dashicons { + transform: rotate(180deg); + } + + > .column-name .row-actions { + display: block; + } + + > td:not(.column-activate, .column-name, .hidden) { + display: grid; + grid-column: 1 / -1; + grid-template-columns: 76px minmax(0, 1fr); + gap: 10px; + inline-size: auto; + min-inline-size: 0; + max-inline-size: none; + font-size: 13px; + line-height: 20px; + text-align: start; + white-space: normal; + + .mobile-cell-label { + display: block; + font-weight: 590; + color: #2c3337; + } + + .mobile-cell-value { + min-inline-size: 0; + } + } + + > td.column-date { + inline-size: auto; + min-inline-size: 0; + max-inline-size: none; + + .modified-column-content { + text-align: start; + } + } + + > td.column-desc .snippet-description-content { + max-inline-size: none; + } + + > td.priority-column input { + inline-size: 65px; + block-size: 29px; + min-block-size: 29px; + } + } + + tbody tr.no-items { + display: block; + padding-block: 20px; + padding-inline: 16px; + + .colspanchange { + display: block; + padding: 0; + } + } + + .row-action-preview { + display: none; + } + + .row-action-separator { + margin-inline: 8px; + color: #646970; + } + + .row-action-edit .row-action-separator { + display: none; + } + } +} diff --git a/src/css/manage/_snippets-table.scss b/src/css/manage/_snippets-table.scss index 615597f7..c7729644 100644 --- a/src/css/manage/_snippets-table.scss +++ b/src/css/manage/_snippets-table.scss @@ -77,8 +77,9 @@ grid-template-columns: repeat(2, minmax(0, 1fr)); } - @media (width <= 782px) { + @media (width <= 480px) { grid-template-columns: minmax(0, 1fr); + gap: 16px; } } diff --git a/src/js/components/ManageMenu/SnippetsTable/RowActions.tsx b/src/js/components/ManageMenu/SnippetsTable/RowActions.tsx index 9bf2d429..09bdf86a 100644 --- a/src/js/components/ManageMenu/SnippetsTable/RowActions.tsx +++ b/src/js/components/ManageMenu/SnippetsTable/RowActions.tsx @@ -123,6 +123,23 @@ const DeleteActionLink: React.FC<{ snippet: Snippet; onSuccess: () => Promise = ({ actions }) => + <> + {actions + .filter(action => action.content) + .map((action, index) => + + {0 < index + ? + : null} + {action.content} + )} + + const ActionLinks = ({ snippet }: { snippet: Snippet }) => { const api = useSnippetsAPI() const { refreshSnippetsList } = useSnippetsList() @@ -165,16 +182,16 @@ const ActionLinks = ({ snippet }: { snippet: Snippet }) => { ? : null - return ( - <> - {[Preview, Edit, Clone, Restore, Export, Delete] - .filter(Action => Action) - .reduce( - (Actions, Action) => - null === Actions ? <>{Action} : <>{Actions} | {Action}, - null)} - - ) + return } export const RowActions: React.FC = ({ snippet }) => { diff --git a/src/js/components/ManageMenu/SnippetsTable/SnippetsListTable.tsx b/src/js/components/ManageMenu/SnippetsTable/SnippetsListTable.tsx index d91d0e7b..a1c4ad57 100644 --- a/src/js/components/ManageMenu/SnippetsTable/SnippetsListTable.tsx +++ b/src/js/components/ManageMenu/SnippetsTable/SnippetsListTable.tsx @@ -1,7 +1,7 @@ -import { __ } from '@wordpress/i18n' +import { __, sprintf } from '@wordpress/i18n' import React, { useEffect, useMemo, useState } from 'react' import classnames from 'classnames' -import { getSnippetType, isSnippetActive } from '../../../utils/snippets/snippets' +import { getSnippetDisplayName, getSnippetType, isSnippetActive } from '../../../utils/snippets/snippets' import { buildUrl } from '../../../utils/urls' import { ListTable } from '../../common/ListTable' import { SnippetViewToggle } from '../../common/SnippetViewToggle' @@ -97,6 +97,19 @@ const getRowClassName = ( } ) +const getRowExpansionLabel = (snippet: Snippet, isExpanded: boolean): string => + isExpanded + ? sprintf( + /* translators: %s: snippet name. */ + __('Collapse details for %s', 'code-snippets'), + getSnippetDisplayName(snippet) + ) + : sprintf( + /* translators: %s: snippet name. */ + __('Expand details for %s', 'code-snippets'), + getSnippetDisplayName(snippet) + ) + interface SnippetsViewProps { snippetView: SnippetView setSnippetView: (view: SnippetView) => void @@ -150,6 +163,7 @@ const SnippetsView: React.FC = ({ selectAllControl endTableNav={endTableNav} rowClassName={snippet => getRowClassName(snippet, activeByCondition)} + getRowExpansionLabel={getRowExpansionLabel} noItems={} beforeTable={} /> @@ -187,7 +201,7 @@ export const SnippetsListTable: React.FC = ({ <> -
+
{ return ( <> - +
diff --git a/src/js/components/ManageMenu/SnippetsTable/SnippetsTableControls.tsx b/src/js/components/ManageMenu/SnippetsTable/SnippetsTableControls.tsx index 524b6ae3..581f4bb2 100644 --- a/src/js/components/ManageMenu/SnippetsTable/SnippetsTableControls.tsx +++ b/src/js/components/ManageMenu/SnippetsTable/SnippetsTableControls.tsx @@ -1,5 +1,6 @@ import { __, _x, sprintf } from '@wordpress/i18n' import React, { Fragment, useMemo } from 'react' +import classnames from 'classnames' import { useRestAPI } from '../../../hooks/useRestAPI' import { useSnippetsList } from '../../../hooks/useSnippetsList' import { handleUnknownError } from '../../../utils/errors' @@ -60,16 +61,24 @@ const SnippetStatusCounts = () => { ) } -const ClearRecentlyActiveButton: React.FC = () => { +interface ClearRecentlyActiveButtonProps { + className?: string + name?: string +} + +const ClearRecentlyActiveButton: React.FC = ({ + className, + name = 'clear-recent-list' +}) => { const { api } = useRestAPI() const { refreshSnippetsList } = useSnippetsList() const { currentStatus } = useSnippetsFilters() return 'recently_active' === currentStatus - ?
+ ?
{ event.preventDefault() @@ -83,11 +92,13 @@ const ClearRecentlyActiveButton: React.FC = () => { } interface FilterByTagControlProps { + which: 'top' | 'bottom' visibleSnippets: Snippet[] } -const FilterByTagControl: React.FC = ({ visibleSnippets }) => { +const FilterByTagControl: React.FC = ({ which, visibleSnippets }) => { const { currentTag, setCurrentTag } = useSnippetsFilters() + const controlId = 'top' === which ? 'snippets-tag-filter' : 'snippets-tag-filter-bottom' const tagsList: Set = useMemo( () => visibleSnippets.reduce((tags, snippet) => { @@ -97,12 +108,12 @@ const FilterByTagControl: React.FC = ({ visibleSnippets [visibleSnippets]) return 0 < tagsList.size - ?
-