diff --git a/src/css/common/_badges.scss b/src/css/common/_badges.scss index a2ad6bd90..0ccdbf5e2 100644 --- a/src/css/common/_badges.scss +++ b/src/css/common/_badges.scss @@ -125,3 +125,31 @@ } } } + +// Override WordPress' .wp-core-ui .button .dashicons { line-height: 1.9 } — +// inside a button the inflated line-box would shift the glyph below the +// badge's bottom border. Placed at end-of-file so it follows all other +// .dashicons rules in source order (no-descending-specificity). +.wp-core-ui .button .badge .dashicons { + line-height: 1; +} + + +// Pro badge consolidated to the navigation pro-chip style: a light accent pill +// in every context, overriding the badge colour map and the inverted/nav-tab +// variants (whose white !important border must be beaten here). This is a +// deliberate final override, so it intentionally follows higher-specificity +// context rules above. +/* stylelint-disable no-descending-specificity */ +.badge.pro-badge, +.inverted-badges .badge.pro-badge, +.nav-tab-inactive .badge.pro-badge { + color: #2271b1; + background-color: #eff5f9; + border: 1px solid rgb(34 113 177 / 10%) !important; + border-radius: 999px; + padding-block: 3px; + padding-inline: 10px; + line-height: normal; +} +/* stylelint-enable no-descending-specificity */ diff --git a/src/css/common/_cards.scss b/src/css/common/_cards.scss index f0b53c2c6..bb2d08469 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,19 +25,46 @@ 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; + } + + // The status label shrinks and clips away under pressure rather than + // pushing the action buttons onto a second line. + .snippet-card-footer-status { + display: flex; align-items: center; - border-end-start-radius: 6px; - border-end-end-radius: 6px; + gap: 8px; + flex: 0 1 auto; + min-inline-size: 0; + overflow: hidden; + white-space: nowrap; } - &.is-selectable, - &.has-corner-controls { + .snippet-card-footer-actions { + display: flex; + align-items: center; + justify-content: flex-end; + flex-wrap: nowrap; + gap: 8px; + flex: 0 0 auto; + + .button:not(.kebab-menu-trigger) { + min-inline-size: 103px; + text-align: center; + justify-content: center; + } + } + + &.is-selectable { position: relative; } @@ -54,12 +81,39 @@ } } - &.is-selectable .card-inner h3 { - margin-inline-end: 48px; + 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; + } + } } - &.has-corner-controls .card-inner h3 { - margin-inline-end: 96px; + &.is-selectable .card-inner > h3 { + margin-inline-end: 48px; } &.is-selected { diff --git a/src/css/common/_kebab-menu.scss b/src/css/common/_kebab-menu.scss new file mode 100644 index 000000000..061bc0eb7 --- /dev/null +++ b/src/css/common/_kebab-menu.scss @@ -0,0 +1,104 @@ +// Generic kebab ("more actions") menu: a square accent-outlined trigger +// button that opens a small popover of actions anchored to its +// bottom-end corner, flipping above the trigger near the viewport edge. +.kebab-menu { + position: relative; + display: inline-flex; +} + +.kebab-menu-trigger { + display: flex; + align-items: center; + justify-content: center; + inline-size: 38px; + block-size: 38px; + box-sizing: border-box; + padding: 0; + background: #fff; + color: #2271b1; + border: 1px solid #2271b1; + border-radius: 5px; + cursor: pointer; + + svg { + display: block; + } + + &:hover, + &:focus { + background: #f0f6fc; + } +} + +.kebab-menu-popover { + position: absolute; + inset-block-start: calc(100% + 4px); + inset-inline-end: 0; + z-index: 1000; + inline-size: 224px; + box-sizing: border-box; + margin: 0; + padding-block: 6px; + padding-inline: 0; + list-style: none; + background: #fff; + border: 1px solid #e2e2e4; + border-radius: 5px; + box-shadow: 0 3px 16px rgb(0 0 0 / 15%); + + &.kebab-menu-popover-top { + inset-block: auto calc(100% + 4px); + } + + li { + margin: 0; + } +} + +.kebab-menu-item { + display: block; + inline-size: 100%; + box-sizing: border-box; + padding-block: 10px; + padding-inline: 14px; + background: none; + border: none; + font-family: inherit; + font-size: 14px; + line-height: 1.4; + color: #2c3337; + text-align: start; + cursor: pointer; + + &:hover, + &:focus { + background: #f0f0f1; + } + + &:disabled { + color: #a7aaad; + cursor: default; + background: none; + } + + &.kebab-menu-item-destructive { + color: #d63638; + } +} + +.kebab-menu-divider { + margin-block: 6px; + border-block-start: 1px solid #e2e2e4; +} + +.kebab-menu-row { + display: flex; + align-items: center; + gap: 8px; + box-sizing: border-box; + padding-block: 10px; + padding-inline: 14px; + font-size: 14px; + line-height: 1.4; + color: #2c3337; +} diff --git a/src/css/common/_list-table.scss b/src/css/common/_list-table.scss index 341cc2e1c..89c5eb866 100644 --- a/src/css/common/_list-table.scss +++ b/src/css/common/_list-table.scss @@ -1,313 +1,4 @@ -@use 'theme'; - -.column-name { - .extra-icons { - float: inline-end; - display: flex; - flex-wrap: wrap; - gap: 5px; - } - - .dashicons-lock { - color: #646970; - opacity: 0.7; - - &:hover { - opacity: 1; - } - } -} - -.active-snippet { - td, th { - background-color: rgba(#78c8e6, 0.06); - } - - th.check-column { - border-inline-start: 2px solid #2ea2cc; - } - - .column-name > .snippet-name { - font-weight: 600; - } -} - -.inactive-snippet { - @include theme.link-colors(#579); -} - -.paging-input { - display: inline-flex; - align-items: center; - gap: 2px; -} - -.wp-list-table { - td.column-id { - text-align: center; - } - - tr { - background: #fff; - } - - ol, ul { - margin-block: 0 1.5em; - margin-inline: 1.5em 0; - } - - ul { - list-style: disc; - } - - .sortable-column-title { - display: flex; - } - - th.sortable .list-table-sort-button, - th.sorted .list-table-sort-button { - display: flex; - flex-direction: row; - align-items: center; - inline-size: 100%; - margin: 0; - border: none; - background: none; - font: inherit; - color: #2271b1; - cursor: pointer; - text-align: start; - overflow: hidden; - padding: 8px; - } - - th.sortable .list-table-sort-button:focus-visible, - th.sorted .list-table-sort-button:focus-visible { - outline: 2px solid #2271b1; - outline-offset: 2px; - border-radius: 2px; - } - - .row-actions { - color: #646970; - position: relative; - inset-inline-start: 0; - - // 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 - // at equal specificity, so it wins). Non-delete links inherit the accent text colour. - .button-link { - block-size: auto; - min-block-size: 0; - padding: 0; - border: 0; - border-radius: 0; - background: none; - box-shadow: none; - line-height: inherit; - font-weight: 400; - - &:hover:not(:disabled), - &:focus:not(:disabled) { - background: none; - border: 0; - box-shadow: none; - } - - &.delete { - color: #b32d2e; - } - - &.snippet-cloud-update { - color: #ff851b; - } - } - - .snippet-row-action-feedback, - .snippet-row-action-error { - display: inline-flex; - align-items: center; - gap: .5em; - - .components-spinner { - margin: 0; - inline-size: 1em; - block-size: 1em; - } - - } - - .snippet-row-action-error { - color: #b32d2e; - - } - - .delete.disabled { - color: #a7aaad; - cursor: not-allowed; - pointer-events: none; - } - } - - .column-activate { - padding-inline-end: 0 !important; - } - - .clear-filters { - vertical-align: middle; - } - - tfoot th.check-column { - padding: 13px 0 0 3px; - } - - thead th.check-column, - tfoot th.check-column, - .inactive-snippet th.check-column { - padding-inline-start: 5px; - } - - .active-snippet, .inactive-snippet { - td, th { - padding: 10px 9px; - box-shadow: inset 0 -1px 0 rgb(0 0 0 / 10%); - } - } - - tr.active-snippet + tr.inactive-snippet th, - tr.active-snippet + tr.inactive-snippet td { - border-block-start: 1px solid rgb(0 0 0 / 3%); - box-shadow: inset 0 1px 0 rgb(0 0 0 / 2%), inset 0 -1px 0 #e1e1e1; - } - - .delete { - color: #b32d2e; - } - - a.delete:not(.disabled) { - &:hover, &:focus, &:active { - border-block-end: 1px solid #f00; - color: #f00; - } - } - - td.column-date, th.column-date { - white-space: nowrap; - inline-size: 130px; /* fixed column width */ - min-inline-size: 130px; - max-inline-size: 130px; - text-align: end; - overflow: hidden; - text-overflow: ellipsis; - } - - td.column-date .modified-column-content { - display: block; - text-align: start; - } - - &.truncate-row-values { - td.column-name > .snippet-name, - td.column-desc .snippet-description-content { - display: block; - overflow: hidden; - text-overflow: ellipsis; - } - - td.column-name > .snippet-name { - max-inline-size: min(15rem, 30vw); - } - - td.column-desc .snippet-description-content { - max-inline-size: min(25rem, 45vw); - overflow: clip; - } - } -} - -.snippets-table-toolbar { - display: flex; - justify-content: space-between; - align-items: center; - margin: 12px 0; - - .subsubsub { - display: flex; - gap: 5px; - margin: 0; - padding: 0; - } -} - -.tablenav { - margin: 20px 0; - display: flex; - align-items: center; -} - -/* WP 7.0 changes the default admin link colour to the modern blue. Pin the classic accent on active rows (names and - row-action links) so links keep their original colour; inactive rows are handled by the muted link-colors mixin, - and the trash button keeps its dark red from the `.delete` rules. */ -.wp-list-table .active-snippet a { - color: theme.$accent; - - &:hover, - &:focus { - color: theme.$accent-hover; - } -} - -/* Status filter links (All | Active | Inactive | …): WP 7.0 recolours these with the modern blue. Restore the classic - accent for non-current links; the current one keeps its own colour. */ -/* stylelint-disable-next-line no-descending-specificity -- unrelated element; ordering is fine. */ -.subsubsub a:not(.current) { - color: theme.$accent; - - &:hover, - &:focus { - color: theme.$accent-hover; - } -} - -.wp-core-ui .button.clear-filters { - vertical-align: baseline; -} - -.snippet-type-description { - border-block-end: 1px solid #ccc; - margin: 0; - padding-block: 1em; - padding-inline: 0; -} - -.snippets-search-area, -.tablenav .displaying-num { - margin-inline-start: auto; -} - -.snippets-search-area { - display: flex; - align-items: center; - gap: 12px; - - .search-box { - float: none; - margin: 0; - - input[type='search'] { - inline-size: 260px; - min-block-size: 32px; - } - } -} - -@media (width <= 782px) { - p.search-box { - float: inline-start; - position: initial; - margin-block: 1em 0; - margin-inline: 0; - block-size: auto; - } -} +@use 'list-table/layout'; +@use 'list-table/navigation'; +@use 'list-table/pagination'; +@use 'list-table/responsive'; diff --git a/src/css/common/_modal.scss b/src/css/common/_modal.scss index fad7a3835..3a90d67f8 100644 --- a/src/css/common/_modal.scss +++ b/src/css/common/_modal.scss @@ -55,15 +55,15 @@ .components-modal__frame.code-snippets-preview-modal { min-inline-size: 520px; min-block-size: 240px; - max-inline-size: 80vw; - max-block-size: 80vh; + inline-size: min(900px, 90vw); + max-block-size: 85vh; @media (width <= 600px) { min-inline-size: 90vw; } - .components-modal__content { - overflow: auto; + .components-modal__header { + border-block-end-color: #ddd; } // The badge lives in the modal content because the minimum-supported @@ -75,12 +75,89 @@ inset-inline-end: 56px; } - // Shown when the code editor is unavailable (syntax highlighting preference - // off): the raw read-only textarea is the functional fallback. - textarea { - inline-size: 100%; - min-block-size: 360px; - resize: none; - font-family: monospace; + // Header and footer stay pinned; the CodeMirror editor is the only + // scroll region, scrolling long code both vertically and horizontally. + .components-modal__content { + display: flex; + flex-flow: column; + min-block-size: 0; + padding: 0; + overflow: hidden; + } + + // The modal component wraps its children in an unstyled focus container; + // it must join the flex chain or the editor grows to its content height. + .components-modal__header + div { + display: flex; + flex-flow: column; + flex: 1; + min-block-size: 0; + overflow: hidden; + } + + .code-snippets-preview-modal__editor { + display: flex; + flex-flow: column; + flex: 1; + min-block-size: 0; + + .CodeMirror { + flex: 1; + min-block-size: 0; + block-size: 100%; + } + + textarea { + flex: 1; + min-block-size: 0; + inline-size: 100%; + resize: none; + font-family: monospace; + } + } + + .code-snippets-preview-modal__footer { + display: flex; + align-items: center; + justify-content: space-between; + flex-shrink: 0; + gap: 16px; + padding: 11px 19px; + border-block-start: 1px solid #e2e2e4; + background: #f6f7f7; + } + + .code-snippets-preview-modal__priority { + display: flex; + align-items: center; + gap: 8px; + font-size: 14px; + color: #646970; + + .code-snippets-preview-modal__priority-value { + font-weight: 600; + color: #2c3337; + } + } + + .code-snippets-preview-modal__buttons { + display: flex; + align-items: center; + gap: 8px; + + .button { + border-radius: 5px; + font-size: 14px; + } + + .button-link.code-snippets-preview-modal__trash { + color: #b32d2e; + text-decoration: none; + + &:hover, + &:focus { + color: #d63638; + } + } } } diff --git a/src/css/common/_page-header.scss b/src/css/common/_page-header.scss new file mode 100644 index 000000000..81a8c10e0 --- /dev/null +++ b/src/css/common/_page-header.scss @@ -0,0 +1,45 @@ +@use 'theme'; + +// Contextual page header row shared by the plugin admin screens: the page +// title on the inline-start side and the primary page action, if any, +// aligned to the inline-end side of the same row. +.snippets-page-header { + display: flex; + align-items: center; + justify-content: space-between; + flex-wrap: wrap; + gap: 16px 24px; + margin-block: 8px 16px; + + h1, + h2 { + margin: 0; + padding: 0; + font-size: 32px; + font-weight: 510; + line-height: 1.25; + color: #000; + } +} + +.wrap .snippets-page-header .button.button-primary { + padding: 12px; + min-block-size: 0; + border-radius: 5px; + line-height: 1; + font-size: 14px; + font-weight: 700; +} + +// One-line supporting description shown directly beneath the page title. +.snippets-page-description { + margin-block: 0 16px; + font-size: 14px; + line-height: 1.5; + color: #646970; + + a { + color: theme.$accent; + text-decoration: underline; + } +} diff --git a/src/css/common/_subnav.scss b/src/css/common/_subnav.scss index 08ea58f78..088efd410 100644 --- a/src/css/common/_subnav.scss +++ b/src/css/common/_subnav.scss @@ -1,10 +1,16 @@ -// Snippet-type navigation styled to match the main plugin toolbar: a white -// bar with tab links showing an icon beside black text, highlighting blue -// with a blue bottom border on hover and for the active type. The create -// button sits at the far end of the bar. +// Snippet-type navigation: a boxed segmented control on a white band, with +// each tab separated by a vertical rule and the active tab shown as a +// filled tile. .snippet-type-nav { + // The tab band scrolls sideways instead of wrapping at any width. + overflow-x: auto; + scrollbar-width: thin; background: #fff; - border-block-end: 1px solid #c3c4c7; + + // 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; + min-block-size: 53px; // Bleed across the full admin content width, flush against the plugin // toolbar above: counteract the .wrap top margin (10px) plus its 2px / @@ -16,16 +22,32 @@ // Core admin switches to 10px #wpcontent padding, 0/12px .wrap margins // below 782px; mirror them so the bar stays flush without overflowing - // the viewport. + // the viewport. The tabs scroll horizontally on a single row rather than + // stacking, with the scrollbar chrome hidden. @media screen and (width <= 782px) { margin-inline-start: -10px; inline-size: calc(100% + 22px); + overflow-x: auto; + scrollbar-width: none; + + &::-webkit-scrollbar { + display: none; + } + + ul { + flex-wrap: nowrap; + } + + .snippet-type-link { + white-space: nowrap; + } } ul { display: flex; align-items: stretch; - flex-wrap: wrap; + flex-wrap: nowrap; + min-block-size: 52px; margin: 0; padding: 0; list-style: none; @@ -38,11 +60,9 @@ button.snippet-type-link { background: none; - border-inline: none; - border-block-start: none; + border-block: none; + border-inline-start: none; font-family: inherit; - font-size: 15px; - font-weight: 600; } .snippet-type-icon { @@ -54,19 +74,25 @@ .snippet-type-link { display: flex; align-items: center; - gap: 10px; - padding: 18px 20px 14px; + justify-content: center; + gap: 8px; + min-inline-size: 100px; + padding-block: 0; + padding-inline: 24px; cursor: pointer; - - // Overlap the nav's black bottom border so the blue active/hover - // border paints directly on top of it with no gap between the two. - margin-block-end: -1px; - font-size: 15px; + font-size: 14px; font-weight: 600; - color: #1d2327; + line-height: 1.5; + color: #646970; text-decoration: none; box-sizing: border-box; - border-block-end: 4px solid transparent; + border-inline-end: 1px solid #e2e2e4; + + // 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. + &.all-type-link { + gap: 12px; + } svg { display: block; @@ -76,10 +102,18 @@ &:hover, &:focus, - &:active, + &:active { + color: #2c3337; + } + &.active-type { + background: #f0f0f1; + border-color: #f0f0f1; color: #2271b1; - border-block-end-color: currentcolor; + + // Children paint over the parent nav's border, so this recolours + // the 1px band separator beneath the active tab to match its fill. + box-shadow: 0 1px 0 0 #f0f0f1; } // Suppress the persistent focus ring left behind after a mouse click; @@ -100,19 +134,51 @@ } } - .snippet-view-toggle-nav-item { - margin-inline-start: auto; - align-items: center; - } +} + +// Card/table view toggle: a two-icon segmented pair of square tiles with +// the active view filled in the accent colour. +.snippet-view-toggle { + display: flex; + align-items: center; + gap: 4px; - .create-snippet-nav-item { - margin-inline-start: 24px; - padding-inline-end: 16px; + .snippet-view-toggle-option { + display: flex; align-items: center; + justify-content: center; + inline-size: 38px; + block-size: 38px; + box-sizing: border-box; + padding: 0; + margin: 0; + background: #f0f0f1; + color: #646970; + border: none; + border-radius: 4.5px; + cursor: pointer; + + svg { + display: block; + } + + &:hover, + &:focus { + color: #2c3337; + } + + &.active-view { + background: #2271b1; + color: #fff; + + &:hover, + &:focus { + color: #fff; + } + } } } - // Slot that adopts the WordPress Screen Options / Help tabs so they appear // directly below the snippet-type nav instead of above the page content. .snippets-screen-meta-slot { @@ -128,29 +194,46 @@ } } - -// Count pill shown beside a subnav tab's label (e.g. total on the "All" tab). +// Item count shown after a subnav tab's label, as muted text. .snippet-type-nav .subnav-count { - display: inline-flex; - align-items: center; - justify-content: center; - min-inline-size: 20px; - block-size: 20px; - padding-inline: 7px; - border-radius: 999px; - background: #2271b1; - color: #fff; - font-size: 11px; + font-size: 14px; font-weight: 600; - line-height: 1; - box-sizing: border-box; + line-height: 1.5; + color: #646970; } -.snippet-type-nav .snippet-type-link:not(.active-type) .subnav-count { - background: #dbebf7; +// The active tab's count adopts the accent alongside its label. +.snippet-type-nav .snippet-type-link.active-type .subnav-count { color: #2271b1; } +// The short "All" form of the all-snippets label only replaces "All Snippets" +// once the bar collapses; the full form is shown at every wider width. +.snippet-type-name-short { + display: none; +} + +// Collapse at mid-width and below (through mobile): drop the type names so each +// tab shows just its badge and count, and shorten "All Snippets" to "All". Only +// the badged manage nav owns a .snippet-type-name; the text-only import/settings +// subnav tabs are untouched. Below 782px the bar still scrolls sideways (above), +// now with the compact collapsed tabs. +@media (width <= 1210px) { + .snippet-type-nav .snippet-type-link:not(.all-type-link) .snippet-type-name { + display: none; + } + + .snippet-type-nav .all-type-link .snippet-type-name-full { + display: none; + } + + .snippet-type-nav .all-type-link .snippet-type-name-short { + display: inline; + } +} + +// When an admin notice pushes the page content down, restore the gap and top +// border above the subnav bar. .notice + .wrap .snippet-type-nav { margin-block-start: 20px; border-block-start: 1px solid #c3c4c7; diff --git a/src/css/common/_switch.scss b/src/css/common/_switch.scss index 3d47ad39b..9823e71a4 100644 --- a/src/css/common/_switch.scss +++ b/src/css/common/_switch.scss @@ -1,7 +1,5 @@ @use 'theme'; -$off-color: #789; - .snippet-execution-button, .snippet-activation-switch, input[type='checkbox'].switch { @@ -9,17 +7,19 @@ input[type='checkbox'].switch { position: relative; } +// Activation switch: an accent-coloured 36x19 pill shared by the list-table +// activate column, snippet cards and settings toggles. .snippet-activation-switch, input[type='checkbox'].switch { appearance: none; outline: 0; cursor: pointer; margin: 0; - inline-size: 32px; + inline-size: 36px; block-size: 19px; border-radius: 34px; text-align: start; - border: 1px solid $off-color; + border: 1px solid theme.$accent; box-sizing: border-box; &::before { @@ -28,7 +28,7 @@ input[type='checkbox'].switch { block-size: 13px; inline-size: 13px; display: inline-block; - background-color: $off-color; + background-color: theme.$accent; border-radius: 50%; margin: 2px; @@ -40,11 +40,12 @@ input[type='checkbox'].switch { .active-snippet .snippet-activation-switch, input[type='checkbox'].switch:checked { - background-color: #0073aa; + background-color: theme.$accent; + // Travel distance: pill width minus the knob, its margins and the borders. &::before { background-color: white; - transform: translateX(calc(100% * var(--cs-direction-multiplier))); + transform: translateX(calc((100% + 4px) * var(--cs-direction-multiplier))); } } @@ -112,14 +113,3 @@ a.snippet-condition-count { } } } - -.snippet-view-toggle { - display: flex; - align-items: center; - gap: 8px; - padding-inline-end: 16px; - - input[type='checkbox'].switch { - display: inline-block; - } -} diff --git a/src/css/common/_theme.scss b/src/css/common/_theme.scss index 9f376cede..a836dce51 100644 --- a/src/css/common/_theme.scss +++ b/src/css/common/_theme.scss @@ -9,13 +9,13 @@ $brand-facebook: #3b5998; $cloud: #00bcd4; $cloud-update: #ff9800; -// WordPress classic admin control palette. WP 7.0's "modern" redesign restyles native form -// controls; these restore the classic metrics our admin UI is designed around (see -// common/_wp-admin.scss). +// 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; -$control-border: #8c8f94; +$control-border: #c3c4c7; $control-text: #2c3338; -$control-height: 30px; +$control-height: 38px; +$control-radius: 5px; /* format: background-color [color] */ $badges: ( @@ -24,7 +24,7 @@ $badges: ( php: $accent, html: #cd4510, css: #9b59b6, - js: #ffeb3b #1c1f20, + js: #f7d67a #1c1f20, cond: #22826f, core: #61c5cb, pro: #f7e8e3 #df9279, diff --git a/src/css/common/_toolbar.scss b/src/css/common/_toolbar.scss index b05cd0699..7b1abaa5d 100644 --- a/src/css/common/_toolbar.scss +++ b/src/css/common/_toolbar.scss @@ -1,6 +1,6 @@ @use 'upsell'; -$toolbar-block-size: 150px; +$toolbar-block-size: 136px; $wpcontent-inline-start-indent: 20px; #wpbody { @@ -28,8 +28,11 @@ $wpcontent-inline-start-indent: 20px; .code-snippets-toolbar-upper { justify-content: space-between; - block-size: 77px; + block-size: 76px; + box-sizing: border-box; + padding-block: 16px; padding-inline: 24px; + border-block-end: 1px solid rgb(195 196 199 / 50%); display: flex; align-items: center; @@ -62,37 +65,102 @@ $wpcontent-inline-start-indent: 20px; } .code-snippets-toolbar-lower { - border-block: 1px solid #c3c4c7; + border-block-end: 1px solid #e2e2e4; + block-size: 60px; + box-sizing: border-box; + + nav, + ul { + block-size: 100%; + } ul { display: flex; - align-items: end; + align-items: stretch; + } + + li { + display: flex; } + // Items fill the 60px band; the active tab is indicated by accent colour on + // its icon and label, with no underline. li a { display: flex; margin: 0; - padding: 24px 24px 20px; - color: inherit; + block-size: 100%; + padding-block: 0; + padding-inline: 24px; + color: #2c3337; gap: 12px; align-items: center; font-size: 16px; + font-weight: 600; + letter-spacing: -0.16px; text-decoration: none; - font-weight: bold; box-sizing: border-box; - transition: border unset; - border-block-end: 4px solid transparent; cursor: pointer; + svg { + inline-size: 26px; + block-size: 26px; + } + &.active-link, &:hover, &:focus, &:active { color: #2271b1; - border-block-end-color: currentcolor; } } - li:last-of-type { + li:not(.toolbar-end-item) + li.toolbar-end-item { margin-inline-start: auto; } + + // Narrow widths: centre each item's label directly beneath its icon and + // shrink the type so the nav still fits on one row. The pro chip becomes a + // small badge pinned to the item's top corner, out of the flow so it never + // shifts the icon off-centre from its label. Below 782px the row scrolls + // sideways (see the mobile block below) rather than wrapping. + @media (width <= 1140px) { + li a { + position: relative; + flex-direction: column; + justify-content: center; + align-items: center; + gap: 4px; + padding-inline: 14px; + font-size: 13px; + text-align: center; + + .pro-chip { + position: absolute; + inset-block-start: 7px; + inset-inline-end: 4px; + padding-block: 1px; + padding-inline: 5px; + font-size: 8px; + line-height: normal; + } + } + } + + // Mobile: the primary navigation scrolls horizontally on its band rather + // than wrapping, with the scrollbar chrome hidden. + @media screen and (width <= 782px) { + overflow-x: auto; + scrollbar-width: none; + + &::-webkit-scrollbar { + display: none; + } + + ul { + flex-wrap: nowrap; + } + + li a { + white-space: nowrap; + } + } } .code-snippets-toolbar-upper li a.active-link, @@ -105,16 +173,18 @@ $wpcontent-inline-start-indent: 20px; .code-snippets-toolbar-lower .pro-chip, .nav-tab .pro-chip, .snippet-type-link .pro-chip { - color: #d46f4d; + color: #2271b1; + background: #eff5f9; + border: 1px solid rgb(34 113 177 / 10%); text-transform: uppercase; - border: 2px solid currentcolor; border-radius: 999px; display: flex; justify-content: center; align-items: center; box-sizing: border-box; - padding: 3px 9px; - font-weight: 600; + padding: 5px 12px; + font-weight: 700; + line-height: 1; } @@ -125,7 +195,6 @@ $wpcontent-inline-start-indent: 20px; .nav-tab .pro-chip, .snippet-type-link .pro-chip { font-size: 10px; - padding: 0 6px; } .code-snippets-return-link { diff --git a/src/css/common/_wp-admin.scss b/src/css/common/_wp-admin.scss index f511cef57..b94f57a6d 100644 --- a/src/css/common/_wp-admin.scss +++ b/src/css/common/_wp-admin.scss @@ -1,18 +1,16 @@ @use 'theme'; /** - * WordPress 7.0 admin compatibility. + * Native form control styling. * - * WP 7.0 ships the "modern" admin redesign, which enlarges native form controls - * (40px tall vs. 30/32px), reduces their corner radius (2px vs. 3/4px) and recolours - * secondary buttons with a transparent fill and the modern accent (#3858E9). Our admin - * UI was designed against the classic control metrics and the brand accent (#2271b1), - * so the redesign throws our spacing and colours out of alignment. - * - * These rules restore the classic control metrics for native elements inside our admin - * pages so the layout renders consistently on both WP 6.9 and WP 7.0. They deliberately - * leave the admin menu, admin bar and `@wordpress/components` React widgets untouched — - * only native inputs/buttons rendered inside our own markup are normalised. + * WordPress core ships different control metrics between versions (30/32px classic, + * 40px with a 2px radius in the WP 7.0 "modern" redesign) and recolours secondary + * buttons with the modern accent (#3858E9). These rules apply the plugin's own control + * tokens — 38px height, 5px radius, #c3c4c7 borders and the brand accent (#2271b1) — + * to native elements inside our admin pages so the layout renders consistently across + * WP versions. They deliberately leave the admin menu, admin bar and + * `@wordpress/components` React widgets untouched — only native inputs/buttons + * rendered inside our own markup are normalised. */ // Scope to our admin page content. We intentionally keep this to a single `.wrap` class so @@ -21,7 +19,7 @@ // win on equal specificity. This lets bespoke controls (conditions button, row-action links, // priority input, etc.) keep their intended styling without per-element exclusions here. .wrap { - // Native single-line text controls and selects: restore 30px height + classic border. + // Native single-line text controls and selects: token height + border. // `@wordpress/components` inputs/selects (`.components-*`) keep their own design — they // are excluded so React widgets such as the tags token field are left untouched. // `.snippet-priority` (the borderless list-table priority field) is excluded so it keeps @@ -33,13 +31,13 @@ input[type='url']:not([class*='components-']), input[type='password']:not([class*='components-']), input[type='tel']:not([class*='components-']) { - // `min-block-size` (not a fixed `block-size`) so controls floor at 30px but can still - // flex-stretch where the layout calls for it (e.g. the 54px cloud search bar). + // `min-block-size` (not a fixed `block-size`) so controls floor at the token height but + // can still flex-stretch where the layout calls for it (e.g. the 54px cloud search bar). min-block-size: theme.$control-height; padding-block: 0; padding-inline: 8px; border: 1px solid theme.$control-border; - border-radius: 4px; + border-radius: theme.$control-radius; line-height: 2; color: theme.$control-text; font-size: 14px; @@ -59,10 +57,10 @@ padding-block: 0; padding-inline: 8px 24px; border: 1px solid theme.$control-border; - border-radius: 3px; + border-radius: theme.$control-radius; - // WP 7.0 leaves a tall (≈38px) line-height on selects, pushing the value off-centre in - // the 30px control. Pin it so the text stays vertically centred. + // WP 7.0 leaves a tall (≈38px) line-height on selects, pushing the value off-centre. + // Pin it so the text stays vertically centred within the token height. line-height: 2; color: theme.$control-text; font-size: 14px; @@ -74,17 +72,18 @@ } } - // Secondary buttons: restore classic geometry, the grey fill and the brand accent - // border/text in place of WP 7.0's transparent + modern-blue treatment. `.button-primary` - // is excluded so its solid fill below is not overwritten. + // Secondary buttons: token geometry, the grey fill and the brand accent border/text in + // place of WP 7.0's transparent + modern-blue treatment. `.button-primary` is excluded + // so its solid fill below is not overwritten. The line-height centres text vertically + // in anchor-based buttons, which lack a button element's automatic centring. .button:not(.button-primary, .button-link), .button-secondary { min-block-size: theme.$control-height; padding-block: 0; padding-inline: 12px; - border-radius: 3px; - line-height: 2.3077; - font-size: 13px; + border-radius: theme.$control-radius; + line-height: 2.5715; + font-size: 14px; font-weight: 400; background: #f6f7f7; border-color: theme.$accent; @@ -104,15 +103,17 @@ } // Primary buttons: WP 7.0 recolours these with the modern accent and drops the solid - // fill. Restore the classic solid brand-accent fill with white text. - .button-primary { + // fill. Restore the classic solid brand-accent fill with white text. The `:not` + // matches the secondary rule's specificity so a `.button-small.button-primary` + // keeps the token height instead of collapsing to the 26px small size. + .button-primary:not(.button-link) { min-block-size: theme.$control-height; padding-block: 0; padding-inline: 12px; - border-radius: 3px; - line-height: 2.3077; - font-size: 13px; - font-weight: 400; + border-radius: theme.$control-radius; + line-height: 2.5715; + font-size: 14px; + font-weight: 700; background: theme.$accent; border-color: theme.$accent; color: #fff; diff --git a/src/css/common/list-table/_layout.scss b/src/css/common/list-table/_layout.scss new file mode 100644 index 000000000..8141219f2 --- /dev/null +++ b/src/css/common/list-table/_layout.scss @@ -0,0 +1,264 @@ +@use '../theme'; + +.column-name { + .extra-icons { + float: inline-end; + display: flex; + flex-wrap: wrap; + gap: 5px; + } + + .dashicons-lock { + color: #646970; + opacity: 0.7; + + &:hover { + opacity: 1; + } + } +} + +.active-snippet { + td, th { + background-color: rgba(#78c8e6, 0.06); + } + + th.check-column { + border-inline-start: 2px solid #2ea2cc; + } + + .column-name > .snippet-name { + font-weight: 600; + } +} + +.inactive-snippet { + @include theme.link-colors(#579); +} + +.paging-input { + display: inline-flex; + align-items: center; + gap: 2px; +} + +.wp-list-table { + td.column-id { + text-align: center; + } + + tr { + background: #fff; + } + + ol, ul { + margin-block: 0 1.5em; + margin-inline: 1.5em 0; + } + + ul { + list-style: disc; + } + + .sortable-column-title { + display: flex; + } + + th.sortable .list-table-sort-button, + th.sorted .list-table-sort-button { + display: flex; + flex-direction: row; + align-items: center; + inline-size: 100%; + margin: 0; + border: none; + background: none; + font: inherit; + color: #2271b1; + cursor: pointer; + text-align: start; + overflow: hidden; + padding: 8px; + } + + th.sortable .list-table-sort-button:focus-visible, + th.sorted .list-table-sort-button:focus-visible { + outline: 2px solid #2271b1; + outline-offset: 2px; + border-radius: 2px; + } + + .row-actions { + color: #646970; + position: relative; + inset-inline-start: 0; + + // 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 + // at equal specificity, so it wins). Non-delete links inherit the accent text colour. + .button-link { + block-size: auto; + min-block-size: 0; + padding: 0; + border: 0; + border-radius: 0; + background: none; + box-shadow: none; + line-height: inherit; + font-weight: 400; + + &:hover:not(:disabled), + &:focus:not(:disabled) { + background: none; + border: 0; + box-shadow: none; + } + + &.delete { + color: #b32d2e; + } + + &.snippet-cloud-update { + color: #ff851b; + } + } + + .snippet-row-action-feedback, + .snippet-row-action-error { + display: inline-flex; + align-items: center; + gap: .5em; + + .components-spinner { + margin: 0; + inline-size: 1em; + block-size: 1em; + } + + } + + .snippet-row-action-error { + color: #b32d2e; + + } + + .delete.disabled { + color: #a7aaad; + cursor: not-allowed; + pointer-events: none; + } + } + + .column-activate { + padding-inline-end: 0 !important; + } + + .clear-filters { + vertical-align: middle; + } + + tfoot th.check-column { + padding: 13px 0 0 3px; + } + + thead th.check-column, + tfoot th.check-column, + .inactive-snippet th.check-column { + padding-inline-start: 5px; + } + + .active-snippet, .inactive-snippet { + td, th { + padding: 10px 9px; + box-shadow: inset 0 -1px 0 rgb(0 0 0 / 10%); + } + } + + tr.active-snippet + tr.inactive-snippet th, + tr.active-snippet + tr.inactive-snippet td { + border-block-start: 1px solid rgb(0 0 0 / 3%); + box-shadow: inset 0 1px 0 rgb(0 0 0 / 2%), inset 0 -1px 0 #e1e1e1; + } + + .delete { + color: #b32d2e; + } + + a.delete:not(.disabled) { + &:hover, &:focus, &:active { + border-block-end: 1px solid #f00; + color: #f00; + } + } + + td.column-date, th.column-date { + white-space: nowrap; + inline-size: 130px; /* fixed column width */ + min-inline-size: 130px; + max-inline-size: 130px; + text-align: end; + overflow: hidden; + text-overflow: ellipsis; + } + + td.column-date .modified-column-content { + display: block; + text-align: start; + } + + // Snippet names always stay on a single line, truncating gracefully; + // the full name is exposed through the title attribute. + td.column-name > .snippet-name { + display: block; + max-inline-size: min(15rem, 30vw); + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + } + + &.truncate-row-values td.column-desc .snippet-description-content { + display: block; + max-inline-size: min(25rem, 45vw); + overflow: clip; + text-overflow: ellipsis; + } +} + +.snippets-table-toolbar { + display: flex; + justify-content: space-between; + align-items: center; + + // The preceding page header owns the 16px gap above; a `wp-header-end`
+ // sits between them and prevents margin collapse, so this row keeps only its + // own 16px gap below (to the tablenav) and no top margin of its own. + margin-block: 0 16px; + + .subsubsub { + display: flex; + align-items: center; + gap: 9px; + margin: 0; + padding: 0; + font-size: 13px; + line-height: 1.5; + + li { + display: flex; + align-items: center; + gap: 9px; + margin: 0; + padding: 0; + } + + // A thin vertical rule precedes every status after the first, sized to the + // 13px text rather than the taller line box. + li + li::before { + content: ''; + inline-size: 1px; + block-size: 13px; + background: #c3c4c7; + } + } +} diff --git a/src/css/common/list-table/_navigation.scss b/src/css/common/list-table/_navigation.scss new file mode 100644 index 000000000..ffd19344e --- /dev/null +++ b/src/css/common/list-table/_navigation.scss @@ -0,0 +1,58 @@ +@use '../theme'; + +.tablenav { + margin: 16px 0; + display: flex; + align-items: center; + flex-wrap: wrap; + + // The row gap only shows once controls wrap onto a second row (see the + // tablet collapse below, where the selection + pagination cluster drops to + // its own row); the 18px column gap spaces the left cluster on one line. + gap: 12px 18px; +} + +/* WP 7.0 changes the default admin link colour to the modern blue. Pin the classic accent on active rows (names and + row-action links) so links keep their original colour; inactive rows are handled by the muted link-colors mixin, + and the trash button keeps its dark red from the `.delete` rules. */ +.wp-list-table .active-snippet a { + color: theme.$accent; + + &:hover, + &:focus { + color: theme.$accent-hover; + } +} + +/* Status filter links (All | Active | Inactive | …): the current view reads as plain + body text while the remaining views are accent-coloured links; every count stays + in the body-text colour regardless of its link state. */ +/* stylelint-disable no-descending-specificity -- unrelated element; ordering is fine. */ +.subsubsub a.current { + color: #2c3337; +} + +.subsubsub a:not(.current) { + color: theme.$accent; + + &:hover, + &:focus { + color: theme.$accent-hover; + } +} + +.snippets-table-toolbar .subsubsub a .count { + color: #2c3337; +} +/* stylelint-enable no-descending-specificity */ + +.wp-core-ui .button.clear-filters { + vertical-align: baseline; +} + +.snippet-type-description { + border-block-end: 1px solid #ccc; + margin: 0; + padding-block: 1em; + padding-inline: 0; +} diff --git a/src/css/common/list-table/_pagination.scss b/src/css/common/list-table/_pagination.scss new file mode 100644 index 000000000..a278bb920 --- /dev/null +++ b/src/css/common/list-table/_pagination.scss @@ -0,0 +1,102 @@ +// The pagination group and view toggle wrap together as one end-pinned +// cluster so narrow viewports never split them across rows. +.tablenav .tablenav-end-group { + display: flex; + align-items: center; + gap: 24px; + margin-inline-start: auto; +} + +.tablenav .tablenav-pages-nav { + display: flex; + align-items: center; + gap: 8px; + + .tablenav-pages { + display: flex; + align-items: center; + gap: 8px; + margin: 0; + } +} + +// "Select all" control shown in the toolbar beside the bulk actions. +.tablenav .tablenav-select-all { + display: flex; + align-items: center; + gap: 8px; + font-size: 14px; + + input[type='checkbox'] { + inline-size: 20px; + block-size: 20px; + margin: 0; + border: 1px solid #c3c4c7; + border-radius: 5px; + box-shadow: 0 2px 2px rgb(0 0 0 / 5%); + } +} + +.snippets-search-area { + display: flex; + align-items: center; + gap: 12px; + + search { + flex: 1 1 auto; + min-inline-size: 0; + } + + .search-box { + float: none; + display: flex; + align-items: center; + gap: 8px; + margin: 0; + + input[type='search'] { + flex: 1 1 auto; + min-inline-size: 0; + } + } +} + +// Inside the results toolbar the search area is fixed to the design width, +// with the input flexing to fill the available space. +.tablenav .snippets-search-area { + inline-size: 237px; +} + +.wrap .snippets-search-area input[type='search'] { + border-color: #e3e3e4; +} + +// Tablet widths: let the search area grow to fill the remainder of its row +// while the fixed-width selects keep their sizing and groups wrap naturally. +@media (width <= 1024px) { + .tablenav .snippets-search-area { + inline-size: auto; + flex: 1 1 237px; + max-inline-size: 100%; + } +} + +@media (width <= 782px) { + p.search-box { + float: inline-start; + position: initial; + margin-block: 1em 0; + margin-inline: 0; + block-size: auto; + } + + // Mobile: the search area takes a full row of its own, and the action + // groups stay visible (core admin hides .tablenav.top .actions here). + .tablenav .snippets-search-area { + flex-basis: 100%; + } + + .tablenav .alignleft.actions { + display: flex; + } +} diff --git a/src/css/common/list-table/_responsive.scss b/src/css/common/list-table/_responsive.scss new file mode 100644 index 000000000..a4e5967f9 --- /dev/null +++ b/src/css/common/list-table/_responsive.scss @@ -0,0 +1,188 @@ +// Toolbar squeeze: above the mobile breakpoint the row never wraps — +// components shrink gracefully instead (flex-wrap would otherwise win +// before flex-shrink ever engages). Wrapping remains the small-screen +// fallback only. +.snippets-list-view .tablenav { + flex-wrap: wrap; + + @media (width > 782px) { + flex-wrap: nowrap; + } + + .alignleft.actions { + display: flex; + align-items: center; + gap: 8px; + float: none; + margin: 0; + padding: 0; + flex: 0 1 auto; + min-inline-size: 0; + + select { + inline-size: auto; + flex: 1 1 154px; + min-inline-size: 104px; + max-inline-size: 154px; + } + } + + .tablenav-select-all { + flex-shrink: 0; + } + + .snippets-search-area { + inline-size: auto; + flex: 1 1 237px; + min-inline-size: 150px; + max-inline-size: 237px; + + // The inner search row never wraps — the field shrinks instead. + search, + form, + p.search-box { + display: flex; + align-items: center; + flex-wrap: nowrap; + gap: 8px; + min-inline-size: 0; + margin: 0; + } + + input[type='search'] { + min-inline-size: 110px; + } + + .button { + flex: 0 0 auto; + } + } + + // Pagination labels stay on one line while the row squeezes. + .tablenav-pages, + .tablenav-pages .paging-input, + .tablenav-pages .tablenav-paging-text { + white-space: nowrap; + flex-wrap: nowrap; + } + + // Square pagination tiles with a compact page-number box between them. + // Enabled arrows pick up the accent border/text from the shared control + // styling; disabled arrows keep the native WordPress treatment. + .pagination-links { + display: flex; + align-items: center; + gap: 4px; + + .button { + display: inline-flex; + align-items: center; + justify-content: center; + inline-size: 38px; + block-size: 38px; + min-block-size: 38px; + box-sizing: border-box; + padding: 0; + margin: 0; + border-radius: 5.6px; + } + + a.button { + color: #2271b1; + } + + .paging-input { + gap: 8px; + margin-inline: 4px; + } + + .current-page { + inline-size: 45px; + min-block-size: 38px; + box-sizing: border-box; + margin: 0; + text-align: center; + color: #2c3337; + } + + .tablenav-paging-text { + font-size: 14px; + color: #646970; + } + } + + .tablenav-pages .displaying-num { + font-size: 14px; + color: #646970; + white-space: nowrap; + } + + // The trailing float-clearing break would otherwise register as an + // empty flex item and contribute a stray column gap. + br.clear { + display: none; + } + + .tablenav-end-group { + flex: 0 1 auto; + min-inline-size: 0; + } + + // The item count is the first thing to give way when space runs short. + @media (width <= 1240px) { + .tablenav-pages .displaying-num { + display: none; + } + } +} + +// Tablet collapse (iPad-class widths): rather than let the single toolbar row +// overflow, break the top toolbar onto two rows. Row one keeps the working +// controls — bulk actions, the tag filter and the search box (stretched to +// fill the remainder). Row two carries the selection + navigation cluster: +// "Select all" at the start, the pagination + view-toggle group pinned to the +// end. A forced full-width break keeps the split deterministic regardless of +// content width. +@media (782px < width <= 1210px) { + .snippets-list-view .tablenav.top { + flex-wrap: wrap; + row-gap: 8px; + + // Both rows spread edge-to-edge: the first control sits at the start, + // the last at the end, with the slack distributed between. + justify-content: space-between; + + // Row one: bulk actions, tag filter, search — at its design width so the + // space-between slack lands in the gaps rather than stretching the field. + .bulkactions { + order: 1; + } + + .alignleft.actions:not(.bulkactions) { + order: 2; + } + + .snippets-search-area { + order: 3; + flex: 0 1 237px; + inline-size: 237px; + max-inline-size: 100%; + } + + &::after { + content: ''; + order: 4; + flex-basis: 100%; + block-size: 0; + } + + // Row two: select all at the start, pagination + view-toggle at the end. + .tablenav-select-all { + order: 5; + } + + .tablenav-end-group { + order: 6; + } + } +} diff --git a/src/css/edit/_form.scss b/src/css/edit/_form.scss index f11387dad..59a97a24f 100644 --- a/src/css/edit/_form.scss +++ b/src/css/edit/_form.scss @@ -9,6 +9,28 @@ $sidebar-gap: 30px; block-size: 45px } +// The type selector is a react-select control that sizes to its content; pin +// its bordered control box to the same 45px as the adjacent title field and +// keep the selected value vertically centred within it. +.snippet-type-container .code-snippets-select { + block-size: 100%; + + // Target the control box only (:first-of-type). The open menu is a later + // sibling div; matching it here would clamp and flex-row the option list. + > div:first-of-type { + min-block-size: 45px; + block-size: 45px; + max-block-size: 45px; + } + + > div:first-of-type > div:first-child { + display: flex; + align-items: center; + block-size: 45px; + padding-block: 0; + } +} + .snippet-type-option { display: flex; align-items: center; @@ -16,6 +38,12 @@ $sidebar-gap: 30px; flex-flow: row; gap: 2em; + .snippet-type-option-main { + display: flex; + align-items: center; + gap: 8px; + } + .small-badge { margin-inline-start: 0.5em; } @@ -23,12 +51,18 @@ $sidebar-gap: 30px; .conditions-editor-open { + .conditions-editor-header { + display: flex; + align-items: center; + justify-content: space-between; + gap: 8px; + } + .button.button-large { - block-size: 100%; display: flex; align-items: center; gap: 8px; - flex: 1; + inline-size: 100%; border-color: #ccc; padding-block: 6px; padding-inline: 12px; @@ -117,3 +151,12 @@ $sidebar-gap: 30px; min-inline-size: 180px; } } + +// Match the location selector's control height to the adjacent conditions +// button so the two sidebar controls line up. This targets a different +// container than the type selector rules above, so their relative order does +// not affect the cascade. +// stylelint-disable-next-line no-descending-specificity +.snippet-editor-sidebar .code-snippets-select.code-snippets-select-location > div:first-of-type { + min-block-size: 48px; +} diff --git a/src/css/edit/_sidebar.scss b/src/css/edit/_sidebar.scss index a964a38cc..888307702 100644 --- a/src/css/edit/_sidebar.scss +++ b/src/css/edit/_sidebar.scss @@ -82,8 +82,22 @@ .inline-form-field.row-actions { display: flex; + + // Keep the export/download, trash and lock controls on a single row; the + // base .inline-form-field sets `row wrap`, which would otherwise drop the + // lock control onto a second line in the narrow sidebar. + flex-wrap: nowrap; gap: 0; + // Lay the export + download pair out inline too — the group is a plain + // block by default, so its two links would otherwise stack vertically. + .snippet-export-buttons { + display: flex; + flex-wrap: nowrap; + gap: 12px; + min-inline-size: 0; + } + // Row-action buttons render as plain inline links. Neutralise the generic `.button` // WP 7.0 compatibility styling (fill/border/radius/40px height) here — emitted after // the wp-admin layer at equal specificity, so it wins. Export/Download inherit the accent. @@ -91,7 +105,7 @@ block-size: auto; min-block-size: theme.$control-height; padding-block: 0; - padding-inline: 10px; + padding-inline: 8px; font-weight: 400; display: inline-flex; justify-content: center; @@ -106,11 +120,21 @@ &:not(.snippet-lock-button) { border: 0; border-radius: 0; + padding-inline: 0; background: none; box-shadow: none; + // WordPress paints a fill on disabled buttons with !important; + // keep the disabled trash flat like the rest of the row. + &:disabled { + background: none !important; + border: 0; + box-shadow: none; + } + &:hover:not(:disabled), - &:focus:not(:disabled) { + &:focus:not(:disabled), + &:active:not(:disabled) { background: none; border: 0; box-shadow: none; diff --git a/src/css/import.scss b/src/css/import.scss index c53285e62..978446da9 100644 --- a/src/css/import.scss +++ b/src/css/import.scss @@ -1,5 +1,7 @@ @use 'common/toolbar'; +@use 'common/subnav'; @use 'common/wp-admin'; +@use 'common/page-header'; @use 'import/page'; @use 'import/card'; @use 'import/upload'; diff --git a/src/css/import/_page.scss b/src/css/import/_page.scss index 428016bcf..885701a62 100644 --- a/src/css/import/_page.scss +++ b/src/css/import/_page.scss @@ -1,15 +1,12 @@ .wrap > h2:first-of-type { - font-size: 1.5rem; - font-weight: 400; - line-height: 1.3; - margin-block: 50px 1rem; + font-size: 32px; + font-weight: 510; + line-height: 1.25; + color: #2c3337; + margin-block: 24px 16px; padding: 0; } -.nav-tab-wrapper { - margin-block-end: 20px; -} - .import-snippets-section { padding-block-start: 0; display: none; diff --git a/src/css/manage.scss b/src/css/manage.scss index 6aab50913..e8567542b 100644 --- a/src/css/manage.scss +++ b/src/css/manage.scss @@ -10,8 +10,11 @@ @use 'common/toolbar'; @use 'common/subnav'; @use 'common/wp-admin'; +@use 'common/page-header'; +@use 'common/kebab-menu'; @use 'manage/snippets-table'; @use 'manage/cloud-community'; +@use 'manage/cloud-community-cards'; .tablenav .tablenav-pages { margin-block-end: 0; diff --git a/src/css/manage/_cloud-community-cards.scss b/src/css/manage/_cloud-community-cards.scss new file mode 100644 index 000000000..f110dc700 --- /dev/null +++ b/src/css/manage/_cloud-community-cards.scss @@ -0,0 +1,179 @@ +@use 'sass:color'; +@use '../common/theme'; +@use '../common/cards'; + +.cloud-search-result { + p:last-child { + margin-block-end: 0; + } + + .card-inner { + display: flex; + flex-flow: column; + gap: 16px; + } + + .snippet-card-header { + display: flex; + align-items: center; + gap: 10px; + + // Keep header content clear of the absolutely-positioned selection checkbox. + padding-inline-end: 32px; + + h3 { + margin: 0; + font-size: 18px; + font-weight: 700; + line-height: 1.5; + min-inline-size: 0; + } + + .cloud-snippet-title-button { + display: block; + max-inline-size: 100%; + margin: 0; + padding: 0; + border: none; + background: none; + font: inherit; + color: #2271b1; + cursor: pointer; + text-align: start; + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; + + &:hover, &:focus { + text-decoration: underline; + } + + &:focus-visible { + outline: 2px solid #2271b1; + outline-offset: 2px; + border-radius: 2px; + } + } + } + + .snippet-card-meta { + display: flex; + align-items: center; + flex-wrap: wrap; + gap: 16px; + font-size: 14px; + + .cloud-snippet-tags { + color: #2271b1; + } + + .snippet-card-modified { + color: #6c7e7e; + margin-inline-start: auto; + } + } + + .snippet-description-content { + display: -webkit-box; + -webkit-box-orient: vertical; + -webkit-line-clamp: 2; + line-clamp: 2; + overflow: hidden; + margin: 0; + font-size: 14px; + color: #646970; + border-block-start: 1px solid #dcdcde; + padding-block-start: 16px; + } + + .cloud-snippet-author { + margin: 0; + font-size: 14px; + color: #646970; + + a { + color: theme.$accent; + } + } + + footer { + .dashicons-warning { + color: #b32d2e; + } + + .cloud-snippet-update svg { + display: block; + } + + .button { + font-size: 14px; + line-height: 2.5714; + border-radius: 5px; + } + + .button:not(.button-primary, .cloud-pro-button) { + background: #fff; + } + } +} + +.cloud-search-results .cloud-search-result footer .components-spinner { + margin: 0; +} + +// Anchored to the card container so this keeps its brand-orange fill above the WP-admin +// compatibility layer's secondary-button styling (equal specificity, resolved by source order). +.cloud-search-result .cloud-pro-button.button { + background-color: theme.$secondary; + border-color: theme.$secondary; + color: #fff; + + &:hover, + &:focus { + background-color: color.adjust(theme.$secondary, $lightness: -10%); + border-color: color.adjust(theme.$secondary, $lightness: -10%); + color: #fff; + } +} + +// Three equal-height snippet cards per row, matching the manage snippets +// grid. minmax(0, 1fr) lets columns shrink below their content width so +// the grid never overflows the page. +ul.cloud-search-results.code-snippets-cards { + grid-template-columns: repeat(3, minmax(0, 1fr)); + + @media screen and (width <= 1100px) { + grid-template-columns: repeat(2, minmax(0, 1fr)); + } + + @media screen and (width <= 782px) { + grid-template-columns: minmax(0, 1fr); + } + + .code-snippets-card { + min-inline-size: 0; + + footer { + flex-wrap: wrap; + } + } + + // 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; + + @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; + } +} diff --git a/src/css/manage/_cloud-community.scss b/src/css/manage/_cloud-community.scss index c23c88d34..e24197863 100644 --- a/src/css/manage/_cloud-community.scss +++ b/src/css/manage/_cloud-community.scss @@ -1,13 +1,12 @@ -@use 'sass:color'; -@use '../common/theme'; @use '../common/banners'; -@use '../common/cards'; .cloud-snippet-status { - text-transform: uppercase; + display: inline-flex; + align-items: center; + gap: 8px; color: #646970; font-size: 12px; - font-weight: 700; + font-weight: 500; white-space: nowrap; &::before { @@ -16,7 +15,6 @@ inline-size: 10px; border-radius: 50%; display: inline-block; - margin-inline-end: 8px; } } @@ -25,7 +23,7 @@ $status-colors: ( private #cc96fb, unverified #ea835e, ai-verified #1cabcf, - pro-verified #7cd68a, + pro-verified #41a269, ); @each $status, $color in $status-colors { @@ -84,136 +82,19 @@ $status-colors: ( .banner { justify-content: center; } - - .tablenav.top { - display: flex; - gap: 20px; - block-size: 40px; - margin-block-end: 31px; - align-items: center; - - select { - inline-size: 245px; - block-size: 100%; - } - - nav { - margin-inline-start: auto; - } - - .tablenav-pages { - margin: 0; - margin-inline-start: auto; - } - } } .bundle-share-code-form { max-inline-size: 60%; } -.cloud-search-result { - p:last-child { - margin-block-end: 0; - } - - .cloud-snippet { - h3 .cloud-snippet-title-button { - display: inline; - margin: 0; - padding: 0; - border: none; - background: none; - font: inherit; - color: #2271b1; - cursor: pointer; - text-align: start; - - &:hover, &:focus { - text-decoration: underline; - } - - &:focus-visible { - outline: 2px solid #2271b1; - outline-offset: 2px; - border-radius: 2px; - } - } - } - - .cloud-snippet-meta { - display: flex; - gap: 16px; - margin-block-start: 16px; - } - - footer { - .dashicons-warning { - color: #b32d2e; - } - - .cloud-snippet-status { - margin-inline-end: auto; - } - } - - .cloud-snippet-votes { - .dashicons { - color: theme.$accent; - padding-inline-end: 8px; - } - } -} - -.cloud-search-results .cloud-search-result footer > .components-spinner { - margin: 0; -} - -// Anchored to the card container so this keeps its brand-orange fill above the WP-admin -// compatibility layer's secondary-button styling (equal specificity, resolved by source order). -.cloud-search-result .cloud-pro-button.button { - background-color: theme.$secondary; - border-color: theme.$secondary; - color: #fff; - - &:hover, - &:focus { - background-color: color.adjust(theme.$secondary, $lightness: -10%); - border-color: color.adjust(theme.$secondary, $lightness: -10%); - color: #fff; - } -} - -// Three equal-height snippet cards per row, matching the manage snippets -// grid. minmax(0, 1fr) lets columns shrink below their content width so -// the grid never overflows the page. -ul.cloud-search-results.code-snippets-cards { - grid-template-columns: repeat(3, minmax(0, 1fr)); - - @media screen and (width <= 1100px) { - grid-template-columns: repeat(2, minmax(0, 1fr)); - } - - @media screen and (width <= 782px) { - grid-template-columns: minmax(0, 1fr); - } - - .code-snippets-card { - min-inline-size: 0; - - footer { - flex-wrap: wrap; - } - } -} - .cloud-snippet-action-buttons { display: flex; justify-content: flex-end; align-items: center; gap: 8px; - // Keep the Download / View / Pro Only button a consistent width so + // Keep the Download / Edit / Pro Only button a consistent width so // rows line up regardless of which state the snippet is in. .button-primary, .cloud-pro-button { diff --git a/src/css/manage/_snippets-table.scss b/src/css/manage/_snippets-table.scss index c3c900e5a..6c82a6a78 100644 --- a/src/css/manage/_snippets-table.scss +++ b/src/css/manage/_snippets-table.scss @@ -1,4 +1,3 @@ -@use '../common/theme'; @use '../common/list-table'; .column-name, @@ -15,16 +14,18 @@ } } -// `.wp-list-table` prefix keeps this above WP 7.0's 40px control height so the borderless -// priority field stays 30px tall and vertically aligned with the rest of the row. +// `.wp-list-table` prefix keeps this above WP 7.0's 40px control height so the boxed +// priority field stays 30px tall (below the 38px control token, which would stretch the +// table rows) and vertically aligned with the rest of the row. .wp-list-table .priority-column input { appearance: none; - background: none; - border: none; + background: #fff; + border: 1px solid #c3c4c7; + border-radius: 5px; box-shadow: none; box-sizing: border-box; - min-block-size: theme.$control-height; - block-size: theme.$control-height; + min-block-size: 30px; + block-size: 30px; inline-size: 4em; // Pin the box model so padding/margins/line-height match WP 6.9 regardless of WP 7.0's @@ -35,7 +36,7 @@ margin-block: 0; margin-inline: 1px; line-height: 2; - color: #666; + color: #2c3337; text-align: center; &:disabled { @@ -60,21 +61,6 @@ display: table-cell !important; } -.snippets-card-grid-container { - .snippets-cards-toolbar { - display: flex; - align-items: center; - gap: 16px; - - .snippets-cards-select-all, - .snippets-cards-sort { - display: flex; - align-items: center; - gap: 6px; - } - } -} - .snippets-card-grid { // Three equal-height cards per row; descriptions are clamped so height // stays uniform regardless of text length. The compound selector wins @@ -97,39 +83,71 @@ .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; } } - // Match the light-blue tint that active snippet rows receive in the - // table view (.active-snippet td/th). - .code-snippets-card.active-snippet { - background-color: rgb(120 200 230 / 6%); + .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; } - .snippet-card-more-toggle { - display: inline-flex; - align-items: center; - gap: 4px; + // Card body bands: header row, meta row, then a separated description. + .card-inner { + display: flex; + flex-flow: column; + gap: 16px; + } - .dashicons { - font-size: 16px; - block-size: 16px; - inline-size: 16px; - transition: transform 0.25s ease; + .snippet-card-header { + display: flex; + align-items: center; + gap: 10px; + + // Keep header content clear of the absolutely-positioned selection checkbox. + padding-inline-end: 32px; + + h3 { + margin: 0; + font-size: 18px; + font-weight: 700; + line-height: 1.5; + + // Let the heading shrink inside the flex row so long names + // truncate instead of pushing the header icons out of the card. + min-inline-size: 0; + + .snippet-name { + display: block; + color: #2271b1; + text-decoration: none; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + } } - &[aria-expanded='true'] .dashicons { - transform: rotate(90deg); + .extra-icons:empty { + display: none; } - } - .card-inner h3 { - margin: 0; - font-size: 1.1em; + // The accent pill styling lives in common/_switch.scss; keep the + // switch from shrinking inside the flex card header. + input[type='checkbox'].switch { + flex-shrink: 0; + } } .snippet-card-meta { @@ -137,107 +155,77 @@ align-items: center; flex-wrap: wrap; gap: 16px; - margin-block-start: 16px; + font-size: 14px; + + .snippet-card-tags-label { + color: #646970; + } + + .snippet-card-tags a { + color: #2271b1; + text-decoration: none; + } - .snippet-card-tags .dashicons { - color: #787c82; - margin-inline-end: 2px; + .snippet-card-modified { + color: #646970; + margin-inline-start: auto; } } .snippet-description-content { display: -webkit-box; -webkit-box-orient: vertical; - -webkit-line-clamp: 3; - line-clamp: 3; + -webkit-line-clamp: 2; + line-clamp: 2; overflow: hidden; - margin-block-start: 12px; + font-size: 14px; + color: #646970; + border-block-start: 1px solid #dcdcde; + padding-block-start: 16px; } - // Enlarge the activation switch when shown in the card corner so it - // reads clearly next to the selection checkbox. - .snippet-card-corner input[type='checkbox'].switch { - inline-size: 38px; - block-size: 22px; - - &::before { - block-size: 16px; - inline-size: 16px; - } + // Align the selection checkbox with the taller card header row. + .snippet-card-corner { + inset-block-start: 28px; } - .snippet-card-priority { - display: flex; - align-items: center; - gap: 6px; - - input.snippet-priority { - inline-size: 4em; + .code-snippets-card footer { + .button { + font-size: 14px; + line-height: 2.5714; + border-radius: 5px; } - } - - .snippet-card-actions { - display: flex; - align-items: center; - flex-wrap: wrap; - gap: 8px; - margin-inline-start: auto; - } - - // Expanding actions row: animating grid-template-rows from 0fr to 1fr - // gives a smooth height transition without a hard-coded max height. - .snippet-card-more { - display: grid; - grid-template-rows: 0fr; - transition: grid-template-rows 0.25s ease; - background: #f6f7f7; - &.is-expanded { - grid-template-rows: 1fr; + .button:not(.button-primary) { + background: #fff; } } - .snippet-card-more-inner { - overflow: hidden; - min-block-size: 0; - } - - .snippet-card-more-row { - display: flex; - align-items: center; - flex-wrap: wrap; - gap: 8px; - background: #f6f7f7; - border-block-start: 1px solid #dcdcde; - padding-inline: 24px; - padding-block: 12px; - } + .kebab-menu-priority { + justify-content: space-between; - @media (prefers-reduced-motion: reduce) { - .snippet-card-more, - .snippet-card-more-toggle .dashicons { - transition-duration: 0.01s; + input.snippet-priority { + inline-size: 52px; + block-size: 30px; + box-sizing: border-box; + margin: 0; + padding-block: 0; + padding-inline: 4px; + border: 1px solid #e2e2e4; + border-radius: 4px; + text-align: center; + color: #2c3337; } } } -// Shared top table-navigation layout for both snippet views: bulk actions -// and filters on the left, with the search field filling the remaining -// width of the row. -.snippets-list-view .tablenav.top { - display: flex; - align-items: center; - flex-wrap: wrap; +// Shared table-navigation layout for both snippet views and both toolbars: +// bulk actions, select all, tag filter and search on the left, with the item +// count, pagination and view toggle pinned to the far end of the row. The +// bottom toolbar repeats only the bulk actions and pagination group. +.snippets-list-view .tablenav { block-size: auto; - margin-block-end: 20px; - -} - -.tablenav-pages { - .displaying-num { - color: #646970; - white-space: nowrap; - } + margin-block-end: 16px; } .snippets-search-subtitle { @@ -252,12 +240,6 @@ gap: 1em; } -// The visible 'N items found' count beside the search box already reports -// the total, so hide the pagination's duplicate item count in this view. -.snippets-list-view .tablenav-pages .displaying-num { - display: none; -} - // Keep every table row the same height by clamping the description cell // to two lines, matching the two-line name + row-actions column. .snippets-list-view .wp-list-table td.column-desc .snippet-description-content { @@ -267,10 +249,3 @@ line-clamp: 2; overflow: hidden; } - -// The All tab keeps the width it had with its longer label, with the -// icon and text centred inside it. -.snippet-type-nav .all-type-link { - min-inline-size: 165px; - justify-content: center; -} diff --git a/src/css/menu.scss b/src/css/menu.scss index e329afa5c..87d5c1dbe 100644 --- a/src/css/menu.scss +++ b/src/css/menu.scss @@ -1,4 +1,3 @@ -@use 'common/theme'; #adminmenu { .toplevel_page_snippets div.wp-menu-image::before { @@ -15,12 +14,13 @@ border: none; // WP 7.0 restyles `.button`/`.button-primary` with wider padding, a 2px radius and a - // 40px min-height; pin these so the menu pill keeps its classic dimensions and stays - // aligned with the surrounding submenu items across WP versions. + // 40px min-height; pin these (independent of the plugin control tokens, which are + // sized for page content rather than the admin menu) so the pill keeps its compact + // dimensions and stays aligned with the surrounding submenu items across WP versions. border-radius: 3px; padding-block: 0; padding-inline: 10px; - min-block-size: theme.$control-height; + min-block-size: 30px; block-size: auto; text-align: center; font-weight: bold; diff --git a/src/css/settings.scss b/src/css/settings.scss index df3285f11..43567de82 100644 --- a/src/css/settings.scss +++ b/src/css/settings.scss @@ -2,20 +2,29 @@ @use 'common/subnav'; @use 'common/toolbar'; @use 'common/wp-admin'; +@use 'common/page-header'; $sections: general, editor, debug, version-switch; -.wrap > h2:first-of-type { - font-size: 1.5rem; - font-weight: 400; - line-height: 1.3; - padding: 0; - margin-block: 50px 1rem; +.snippets-page-header h1 { + color: #2c3337; } p.submit { display: flex; - justify-content: space-between; + justify-content: flex-start; + gap: 14px; + + .button-secondary { + background: transparent; + } +} + +.settings-section .form-table th { + inline-size: 261px; + font-size: 14px; + font-weight: 700; + color: #2c3337; } .settings-section, diff --git a/src/css/welcome.scss b/src/css/welcome.scss index 9d4c0702c..d15e7c3f8 100644 --- a/src/css/welcome.scss +++ b/src/css/welcome.scss @@ -8,9 +8,10 @@ $breakpoint: 1060px; .code-snippets-welcome { h2 { - font-size: 1.5rem; - font-weight: 400; - line-height: 1.3; + font-size: 32px; + font-weight: 510; + line-height: 1.25; + color: #2c3337; padding: 0; margin-block: 50px 1rem; } diff --git a/src/js/components/EditMenu/ConditionModal/ConditionModalButton.tsx b/src/js/components/EditMenu/ConditionModal/ConditionModalButton.tsx index 7d1eaaa49..38c3faab9 100644 --- a/src/js/components/EditMenu/ConditionModal/ConditionModalButton.tsx +++ b/src/js/components/EditMenu/ConditionModal/ConditionModalButton.tsx @@ -18,14 +18,16 @@ export const ConditionModalButton: React.FC = ({ setI const hasCondition = 0 !== snippet.conditionId return ( -
+
{isCondition(snippet) ? null : <> - {__('Conditions', 'code-snippets')} - - {__('beta', 'code-snippets')} - - {!isLicensed() && {__('Pro', 'code-snippets')}} +
+ {__('Conditions', 'code-snippets')} + {!isLicensed() && {__('Pro', 'code-snippets')}} +
- - -
+
- - - - {snippet.vote_count} - {` ${__('votes', 'code-snippets')}`} - - - {0 < snippet.tags.length - ? - {__('Category: ', 'code-snippets')} - {snippet.tags[0]} - - : null} + +

+ +

- {snippet.description && ( -

+ {0 < snippet.tags.length || snippet.updated + ?

+ {0 < snippet.tags.length + ? {snippet.tags.join(', ')} + : null} + + {snippet.updated + ? + : null} +
+ : null} + + {snippet.description + ?

{truncateChars(stripTags(snippet.description))}

- )} + : null} -

- {_x('by ', 'snippet author', 'code-snippets')} - - {snippet.codevault} - -

+ {author}
export interface SearchResultProps { snippet: CloudSnippetSchema + author?: ReactNode isSelected?: boolean onSelectedChange?: (isSelected: boolean) => void } -export const SearchResult: React.FC = ({ snippet, isSelected = false, onSelectedChange }) => { +export const SearchResult: React.FC = ({ + snippet, + author, + isSelected = false, + onSelectedChange +}) => { const [isPreviewOpen, setIsPreviewOpen] = useState(false) return ( @@ -81,23 +113,30 @@ export const SearchResult: React.FC = ({ snippet, isSelected __('Select %s', 'code-snippets'), snippet.name )} - footer={<> + footerStatus={<> + + {snippet.update_available ? : null} - - - + } + footer={<> + + } > - + - + ) } diff --git a/src/js/components/ManageMenu/SnippetsTable/ManageSnippetCard.tsx b/src/js/components/ManageMenu/SnippetsTable/ManageSnippetCard.tsx index 950e62d05..690b003b3 100644 --- a/src/js/components/ManageMenu/SnippetsTable/ManageSnippetCard.tsx +++ b/src/js/components/ManageMenu/SnippetsTable/ManageSnippetCard.tsx @@ -1,19 +1,34 @@ -import React, { useState } from 'react' -import classnames from 'classnames' -import { __, sprintf } from '@wordpress/i18n' +import { humanTimeDiff } from '@wordpress/date' import { RawHTML } from '@wordpress/element' +import { __, sprintf } from '@wordpress/i18n' +import React, { useState } from 'react' import { useSnippetsAPI } from '../../../hooks/useSnippetsAPI' import { useSnippetsList } from '../../../hooks/useSnippetsList' import { handleUnknownError } from '../../../utils/errors' import { downloadSnippetExportFile } from '../../../utils/files' -import { isNetworkAdmin } from '../../../utils/screen' -import { cloneSnippetObject, getSnippetDisplayName, getSnippetEditUrl, getSnippetType, isSnippetActive } from '../../../utils/snippets/snippets' +import { + canModifySnippet, + cloneSnippetObject, + getSnippetDisplayName, + getSnippetEditUrl, + getSnippetType, + isNetworkOnlySnippet, + isSnippetActive +} from '../../../utils/snippets/snippets' import { Button } from '../../common/Button' -import { DeleteButton } from '../../common/DeleteButton' +import { useDeleteSnippet } from '../../common/DeleteButton' +import { KebabMenu, KebabMenuDivider, KebabMenuItem, KebabMenuRow } from '../../common/KebabMenu' import { SnippetCard } from '../../common/SnippetCard' import { SnippetPreviewModal } from '../../common/SnippetPreviewModal' import { useFilteredSnippets } from './WithFilteredSnippetsContext' -import { ActivateColumn, DateColumn, PriorityColumn, SnippetExtraIcons, SnippetName, TagsColumn, TypeColumn } from './TableColumns' +import { + ActivateColumn, + PriorityColumn, + SnippetExtraIcons, + SnippetName, + TagsColumn, + TypeColumn +} from './TableColumns' import type { Snippet } from '../../../types/Snippet' interface SnippetCardActionsProps { @@ -35,128 +50,153 @@ const CardPreviewButton: React.FC = ({ snippet }) => { type={getSnippetType(snippet)} isOpen={isPreviewOpen} setIsOpen={setIsPreviewOpen} + snippet={snippet} /> ) } -const EditCloneExportButtons: React.FC = ({ snippet }) => { +const CloneExportMenuItems: React.FC = ({ snippet }) => { const api = useSnippetsAPI() const { refreshSnippetsList } = useSnippetsList() return ( <> - - {snippet.locked ? __('View', 'code-snippets') : __('Edit', 'code-snippets')} - - - + - + + + ) } -const SnippetCardActions: React.FC = ({ snippet }) => { +interface RestoreDeleteMenuItemsProps extends SnippetCardActionsProps { + requestDelete: () => void +} + +const RestoreDeleteMenuItems: React.FC = ({ + snippet, + requestDelete +}) => { const api = useSnippetsAPI() const { refreshSnippetsList } = useSnippetsList() - if (!isNetworkAdmin() && snippet.network && !snippet.shared_network) { - return ( - - {snippet.active - ? {__('Network Active', 'code-snippets')} - : {__('Network Only', 'code-snippets')}} - - ) - } - - if (snippet.shared_network && !window.CODE_SNIPPETS_MANAGE?.hasNetworkCap) { - return null - } - return ( - - {!snippet.trashed && } + <> + - {snippet.trashed && ( - )} + + : null} - {(!snippet.locked || snippet.trashed) && ( - )} - + + {snippet.trashed ? __('Delete Permanently', 'code-snippets') : __('Trash', 'code-snippets')} + + ) } -const CardActionsArea: React.FC = ({ snippet }) => { - const [isExpanded, setIsExpanded] = useState(false) +const CardActionsMenu: React.FC = ({ snippet }) => { + const { refreshSnippetsList } = useSnippetsList() + const canModify = canModifySnippet(snippet) + const { requestDelete, confirmDialog } = useDeleteSnippet({ + snippet, + onSuccess: refreshSnippetsList, + onError: handleUnknownError + }) return ( <> -
- - - -
- -
-
-
- - - - - - -
-
-
+ + {canModify && !snippet.trashed ? : null} + + + + + + + {canModify && (snippet.trashed || !snippet.locked) + ? + : null} + + + {confirmDialog} ) } +const CardFooterActions: React.FC = ({ snippet }) => + <> + {isNetworkOnlySnippet(snippet) + ? snippet.active + ? {__('Network Active', 'code-snippets')} + : {__('Network Only', 'code-snippets')} + : null} + + {!snippet.trashed ? : null} + + {!snippet.trashed && canModifySnippet(snippet) + ? + {snippet.locked ? __('View', 'code-snippets') : __('Edit', 'code-snippets')} + + : null} + + + + +const CardModifiedDate: React.FC = ({ snippet }) => + snippet.modified + ? + : null + export interface ManageSnippetCardProps { snippet: Snippet isSelected: boolean onSelectedChange: (isSelected: boolean) => void } -export const ManageSnippetCard: React.FC = ({ snippet, isSelected, onSelectedChange }) => { +export const ManageSnippetCard: React.FC = ({ + snippet, + isSelected, + onSelectedChange +}) => { const { activeByCondition } = useFilteredSnippets() return ( @@ -174,34 +214,34 @@ export const ManageSnippetCard: React.FC = ({ snippet, i __('Select %s', 'code-snippets'), getSnippetDisplayName(snippet) )} - cornerControls={} + footer={} >
-

- -
+
+ - - - {__('Last Modified: ', 'code-snippets')} - - - - {0 < snippet.tags.length && ( - - - - )} - +

+ {0 < snippet.tags.length || snippet.modified + ?
+ {0 < snippet.tags.length + ? + {__('Tags:', 'code-snippets')} + {' '} + + + : null} + + +
+ : null} + {snippet.desc ?
{snippet.desc}
: null}
- - ) } diff --git a/src/js/components/ManageMenu/SnippetsTable/RowActions.tsx b/src/js/components/ManageMenu/SnippetsTable/RowActions.tsx index f55ce5689..9bf2d4298 100644 --- a/src/js/components/ManageMenu/SnippetsTable/RowActions.tsx +++ b/src/js/components/ManageMenu/SnippetsTable/RowActions.tsx @@ -32,6 +32,7 @@ const PreviewLink: React.FC = ({ snippet }) => { type={getSnippetType(snippet)} isOpen={isPreviewOpen} setIsOpen={setIsPreviewOpen} + snippet={snippet} /> ) diff --git a/src/js/components/ManageMenu/SnippetsTable/SnippetsCardGrid.tsx b/src/js/components/ManageMenu/SnippetsTable/SnippetsCardGrid.tsx index 989d59af4..5f88e5cf2 100644 --- a/src/js/components/ManageMenu/SnippetsTable/SnippetsCardGrid.tsx +++ b/src/js/components/ManageMenu/SnippetsTable/SnippetsCardGrid.tsx @@ -1,66 +1,13 @@ +import classnames from 'classnames' import React, { useMemo, useState } from 'react' -import { __ } from '@wordpress/i18n' -import { getSnippetDisplayName, getSnippetType } from '../../../utils/snippets/snippets' +import { fetchQueryParam } from '../../../utils/urls' import { TableNavigation } from '../../common/ListTable/TableNavigation' import { ManageSnippetCard } from './ManageSnippetCard' import type { ListTableAction, ListTableNavProps } from '../../common/ListTable' import type { Snippet } from '../../../types/Snippet' import type { Dispatch, ReactNode, SetStateAction } from 'react' -type SortKey = 'name' | 'type' | 'modified' | 'priority' - -const SORT_LABELS: Record = { - name: __('Name', 'code-snippets'), - type: __('Type', 'code-snippets'), - modified: __('Recently Modified', 'code-snippets'), - priority: __('Priority', 'code-snippets') -} - -const SORT_COMPARATORS: Record number> = { - name: (snippetA, snippetB) => - getSnippetDisplayName(snippetA).localeCompare(getSnippetDisplayName(snippetB), undefined, { sensitivity: 'base' }), - type: (snippetA, snippetB) => getSnippetType(snippetA).localeCompare(getSnippetType(snippetB)), - modified: (snippetA, snippetB) => - new Date(snippetB.modified ?? 0).getTime() - new Date(snippetA.modified ?? 0).getTime(), - priority: (snippetA, snippetB) => snippetA.priority - snippetB.priority -} - -const isSortKey = (value: string): value is SortKey => value in SORT_COMPARATORS - -interface CardsToolbarProps { - sortKey: SortKey - setSortKey: (sortKey: SortKey) => void - allSelected: boolean - toggleSelectAll: (isSelected: boolean) => void -} - -const CardsToolbar: React.FC = ({ sortKey, setSortKey, allSelected, toggleSelectAll }) => -
- - - -
+const PAGE_SEARCH_PARAM = 'paged' interface CardGridProps { snippets: Snippet[] @@ -71,7 +18,11 @@ interface CardGridProps { const CardGrid: React.FC = ({ snippets, noItems, selected, setSelected }) => 0 < snippets.length - ?
    + ?
      selected.has(snippet.id)) + })} + > {snippets.map(snippet => = ({ snippets, noItems, selected, setSel
    :

    {noItems}

    -interface ToggleSnippetsSelectionParams { - previous: Set - snippets: Snippet[] - isSelected: boolean -} - -const toggleSnippetsSelection = ({ previous, snippets, isSelected }: ToggleSnippetsSelectionParams): Set => { - const updated = new Set(previous) - - snippets.forEach(snippet => { - if (isSelected) { - updated.add(snippet.id) - } else { - updated.delete(snippet.id) - } - }) - - return updated -} - const getVisibleSelection = (selected: Set, visible: Snippet[]): Set => new Set([...selected].filter(id => visible.some(snippet => snippet.id === id))) -const composeExtraTableNav = ( - toolbar: ReactNode, - extraTableNav: ((which: 'top' | 'bottom') => ReactNode) | undefined -) => - function extraCardsTableNav(which: 'top' | 'bottom'): ReactNode { - return <> - {'top' === which ? toolbar : null} - {extraTableNav?.(which)} - - } - -interface SortedPagedSnippetsParams { +interface PagedSnippetsParams { snippets: Snippet[] - sortKey: SortKey itemsPerPage: number | undefined currentPage: number } interface PagedSnippets { - sortedSnippets: Snippet[] totalPages: number safePage: number visibleSnippets: Snippet[] } -const useSortedPagedSnippets = ({ snippets, sortKey, itemsPerPage, currentPage }: SortedPagedSnippetsParams): PagedSnippets => { - const sortedSnippets = useMemo( - () => snippets.toSorted(SORT_COMPARATORS[sortKey]), - [snippets, sortKey] - ) - - const totalPages = itemsPerPage ? Math.ceil(sortedSnippets.length / itemsPerPage) : 0 +const usePagedSnippets = ({ + snippets, + itemsPerPage, + currentPage +}: PagedSnippetsParams): PagedSnippets => { + const totalPages = itemsPerPage ? Math.ceil(snippets.length / itemsPerPage) : 0 const safePage = totalPages ? Math.min(currentPage, totalPages) : 1 const visibleSnippets = useMemo( () => itemsPerPage - ? sortedSnippets.slice((safePage - 1) * itemsPerPage, safePage * itemsPerPage) - : sortedSnippets, - [sortedSnippets, itemsPerPage, safePage] + ? snippets.slice((safePage - 1) * itemsPerPage, safePage * itemsPerPage) + : snippets, + [snippets, itemsPerPage, safePage] ) - return { sortedSnippets, totalPages, safePage, visibleSnippets } + return { totalPages, safePage, visibleSnippets } } export interface SnippetsCardGridProps - extends Pick, 'extraTableNav'> { + extends Pick, 'extraTableNav' | 'endTableNav'> { snippets: Snippet[] actions: ListTableAction[] doAction: (action: A, selected: Set) => Promise @@ -176,41 +93,37 @@ export const SnippetsCardGrid = ({ actions, doAction, extraTableNav, + endTableNav, itemsPerPage, noItems, beforeGrid }: SnippetsCardGridProps) => { const [selected, setSelected] = useState(() => new Set()) - const [sortKey, setSortKey] = useState('name') - const [currentPage, setCurrentPage] = useState(1) + const [currentPage, setCurrentPage] = useState( + () => Number(fetchQueryParam(PAGE_SEARCH_PARAM)) || 1 + ) - const { sortedSnippets, totalPages, safePage, visibleSnippets } = - useSortedPagedSnippets({ snippets, sortKey, itemsPerPage, currentPage }) - const allSelected = 0 < visibleSnippets.length && visibleSnippets.every(snippet => selected.has(snippet.id)) + const { totalPages, safePage, visibleSnippets } = usePagedSnippets({ + snippets, + itemsPerPage, + currentPage + }) return (
    snippet.id)} actions={actions} doAction={doAction} - extraTableNav={composeExtraTableNav( - { - setSelected(previous => toggleSnippetsSelection({ previous, snippets: visibleSnippets, isSelected })) - }} - />, - extraTableNav - )} + extraTableNav={extraTableNav} + endTableNav={endTableNav} > {beforeGrid} diff --git a/src/js/components/ManageMenu/SnippetsTable/SnippetsListTable.tsx b/src/js/components/ManageMenu/SnippetsTable/SnippetsListTable.tsx index 39c9f9fe0..d91d0e7ba 100644 --- a/src/js/components/ManageMenu/SnippetsTable/SnippetsListTable.tsx +++ b/src/js/components/ManageMenu/SnippetsTable/SnippetsListTable.tsx @@ -1,105 +1,32 @@ -import { __, _n, _x, sprintf } from '@wordpress/i18n' -import React, { Fragment, useEffect, useMemo, useState } from 'react' +import { __ } from '@wordpress/i18n' +import React, { useEffect, useMemo, useState } from 'react' import classnames from 'classnames' -import { createInterpolateElement } from '@wordpress/element' -import { useRestAPI } from '../../../hooks/useRestAPI' -import { useSnippetsList } from '../../../hooks/useSnippetsList' -import { handleUnknownError } from '../../../utils/errors' -import { REST_BASES } from '../../../utils/restAPI' import { getSnippetType, isSnippetActive } from '../../../utils/snippets/snippets' import { buildUrl } from '../../../utils/urls' import { ListTable } from '../../common/ListTable' -import { SubmitButton } from '../../common/SubmitButton' -import { INDEX_STATUS, useSnippetsFilters } from './WithSnippetsTableFilters' -import { useFilteredSnippets } from './WithFilteredSnippetsContext' +import { SnippetViewToggle } from '../../common/SnippetViewToggle' import { SnippetsCardGrid } from './SnippetsCardGrid' -import { SearchArea, SearchResultsIndicator } from './SnippetsTableSearch' -import { BULK_ACTIONS, TRASHED_BULK_ACTIONS, useApplyBulkAction } from './useApplyBulkAction' +import { SnippetsTableNavigation, SnippetsTableToolbar } from './SnippetsTableControls' +import { SearchResultsIndicator } from './SnippetsTableSearch' import { getTableColumns } from './TableColumns' +import { useFilteredSnippets } from './WithFilteredSnippetsContext' +import { INDEX_STATUS, useSnippetsFilters } from './WithSnippetsTableFilters' +import { BULK_ACTIONS, TRASHED_BULK_ACTIONS, useApplyBulkAction } from './useApplyBulkAction' import type { ListTableAction } from '../../common/ListTable' -import type { SnippetStatus } from './WithSnippetsTableFilters' import type { SnippetsTableAction } from './useApplyBulkAction' import type { Snippet } from '../../../types/Snippet' import type { SnippetView } from '../../../types/SnippetView' import type { ReactNode } from 'react' -const STATUS_LABELS: [SnippetStatus, string][] = [ - ['all', __('All', 'code-snippets')], - ['active', __('Active', 'code-snippets')], - ['inactive', __('Inactive', 'code-snippets')], - ['recently_active', __('Recently Active', 'code-snippets')], - ['locked', __('Locked', 'code-snippets')], - ['unlocked', __('Unlocked', 'code-snippets')], - ['trashed', __('Trashed', 'code-snippets')] -] - -const SnippetStatusCounts = () => { - const { currentStatus, setCurrentStatus } = useSnippetsFilters() - const { snippetsByStatus } = useFilteredSnippets() - - const visibleStatuses = STATUS_LABELS.filter(([status]) => - snippetsByStatus.has(status) && ('unlocked' !== status || snippetsByStatus.has('locked'))) - - return ( - - ) -} - -const ClearRecentlyActiveButton: React.FC = () => { - const { api } = useRestAPI() - const { refreshSnippetsList } = useSnippetsList() - const { currentStatus } = useSnippetsFilters() - - return 'recently_active' === currentStatus - ?
    - { - event.preventDefault() - api.del(REST_BASES.recentlyActive) - .then(refreshSnippetsList) - .catch(handleUnknownError) - }} - /> -
    - : null -} - -interface ExtraTableNavProps { - visibleSnippets: Snippet[] -} - interface ManageTableSettings { hiddenColumns: Set truncateRowValues: boolean } const useManageTableSettings = (): ManageTableSettings => { - const [hiddenColumns, setHiddenColumns] = useState(() => new Set(window.CODE_SNIPPETS_MANAGE?.hiddenColumns ?? [])) + const [hiddenColumns, setHiddenColumns] = useState( + () => new Set(window.CODE_SNIPPETS_MANAGE?.hiddenColumns ?? []) + ) const [truncateRowValues, setTruncateRowValues] = useState( () => 0 !== Number(window.CODE_SNIPPETS_MANAGE?.truncateRowValues ?? 1) ) @@ -113,12 +40,16 @@ const useManageTableSettings = (): ManageTableSettings => { const updateHiddenColumns = () => { setHiddenColumns( - new Set(Array.from(screenOptions.querySelectorAll('.hide-column-tog:not(:checked)')) + new Set(Array.from(screenOptions.querySelectorAll( + '.hide-column-tog:not(:checked)' + )) .map(toggle => toggle.value)) ) setTruncateRowValues( - screenOptions.querySelector('#snippets-table-truncate-row-values')?.checked ?? true + screenOptions.querySelector( + '#snippets-table-truncate-row-values' + )?.checked ?? true ) } @@ -133,57 +64,29 @@ const useManageTableSettings = (): ManageTableSettings => { return { hiddenColumns, truncateRowValues } } -const FilterByTagControl: React.FC = ({ visibleSnippets }) => { - const { currentTag, setCurrentTag } = useSnippetsFilters() - - const tagsList: Set = useMemo( - () => visibleSnippets.reduce((tags, snippet) => { - snippet.tags.forEach(tag => tags.add(tag)) - return tags - }, new Set()), - [visibleSnippets]) - - return 0 < tagsList.size - ?
    - - -
    - : null -} - const NoItemsMessage = () => { const { currentType, currentTag, searchQuery } = useSnippetsFilters() + const emptyMessage = currentType + ? __("You don't have snippets of this type yet.", 'code-snippets') + : __("You don't have any snippets yet.", 'code-snippets') return searchQuery || currentTag ? <> {__('No snippets were found matching the current search query.', 'code-snippets')} {__(' Please enter a new query or use the "Clear Filters" button above.', 'code-snippets')} - : <>{createInterpolateElement( - currentType - ? __("It looks like you don't have any snippets of this type. Perhaps you would like to add a new one?", 'code-snippets') - : __("It looks like you don't have any snippets. Perhaps you would like to add a new one?", 'code-snippets'), - { - // eslint-disable-next-line jsx-a11y/anchor-has-content, jsx-a11y/control-has-associated-label - a: - } - )} + : <> + {emptyMessage}{' '} + + {__('Add a new snippet.', 'code-snippets')} + } -const getRowClassName = (snippet: Snippet, activeByCondition: Map): string => +const getRowClassName = ( + snippet: Snippet, + activeByCondition: Map +): string => classnames( 'snippet', `snippet ${isSnippetActive(snippet, activeByCondition) ? 'active' : 'inactive'}-snippet`, @@ -196,6 +99,7 @@ const getRowClassName = (snippet: Snippet, activeByCondition: Map void snippets: Snippet[] actions: ListTableAction[] doAction: (action: SnippetsTableAction, selected: Set) => Promise @@ -206,6 +110,7 @@ interface SnippetsViewProps { const SnippetsView: React.FC = ({ snippetView, + setSnippetView, snippets, actions, doAction, @@ -217,6 +122,10 @@ const SnippetsView: React.FC = ({ const columns = useMemo(() => getTableColumns(hiddenColumns), [hiddenColumns]) const itemsPerPage = window.CODE_SNIPPETS_MANAGE?.snippetsPerPage const pageCount = itemsPerPage && Math.ceil(snippets.length / itemsPerPage) + const endTableNav = (which: 'top' | 'bottom') => + 'top' === which + ? + : null return 'card' === snippetView ? = ({ doAction={doAction} itemsPerPage={itemsPerPage} extraTableNav={extraTableNav} + endTableNav={endTableNav} noItems={} beforeGrid={} /> @@ -237,6 +147,8 @@ const SnippetsView: React.FC = ({ doAction={doAction} totalPages={pageCount} extraTableNav={extraTableNav} + selectAllControl + endTableNav={endTableNav} rowClassName={snippet => getRowClassName(snippet, activeByCondition)} noItems={} beforeTable={} @@ -245,9 +157,13 @@ const SnippetsView: React.FC = ({ export interface SnippetsListTableProps { snippetView: SnippetView + setSnippetView: (view: SnippetView) => void } -export const SnippetsListTable: React.FC = ({ snippetView }) => { +export const SnippetsListTable: React.FC = ({ + snippetView, + setSnippetView +}) => { const { snippetsByStatus } = useFilteredSnippets() const { currentStatus, setCurrentStatus } = useSnippetsFilters() const { hiddenColumns, truncateRowValues } = useManageTableSettings() @@ -265,29 +181,16 @@ export const SnippetsListTable: React.FC = ({ snippetVie }, [currentStatus, setCurrentStatus, snippetsByStatus]) const extraTableNav = (which: 'top' | 'bottom') => - <> - {'top' === which && } - - - - {sprintf( - // translators: %d: total number of snippets across all pages. - _n('%d item', '%d items', currentSnippets.length), - currentSnippets.length - )} - - + return ( <> -
    - - -
    +
    = ({ type, count, setIsUpgra } }} > - {type ? :