diff --git a/src/wp-admin/css/on-this-day.css b/src/wp-admin/css/on-this-day.css new file mode 100644 index 0000000000000..a7a7b4d29d49b --- /dev/null +++ b/src/wp-admin/css/on-this-day.css @@ -0,0 +1,321 @@ +/* ============================================================================= + On This Day dashboard widget + ============================================================================= + Organised as: + 1. Design tokens (custom properties on the widget root) + 2. Postbox chrome (#dashboard_on_this_day) + 3. Widget title accents (calendar icon + date pill in `.hndle`) + 4. Timeline (year headers, vertical line, post rows) + 5. Post row (icon circle, title, excerpt, meta row) + 6. Empty state + 7. Adaptive rules (reduced motion, small viewports) + ============================================================================= */ + +/* ----------------------------------------------------------------------------- + 1. Design tokens + + Scoped to the postbox so the title spans (rendered in `.hndle`, outside the + `.on-this-day-widget` wrapper) can reference the same variables as the body. + + Accent: `--wp-admin-theme-color*` are the only color custom properties core + exposes at runtime (see src/wp-admin/css/colors/_tokens.scss), so the widget + follows the user's selected admin color scheme (Blue, Modern, Coffee, etc.). + Fallback values match the classic "Fresh" scheme. + ----------------------------------------------------------------------------- */ +#dashboard_on_this_day, +.on-this-day-widget { + /* Accent — theme-color aware, follows the user's admin color scheme. */ + --otd-accent: var(--wp-admin-theme-color, #2271b1); + --otd-accent-dark: var(--wp-admin-theme-color-darker-10, #135e96); + --otd-accent-rgb: var(--wp-admin-theme-color--rgb, 34, 113, 177); + --otd-accent-8: rgba(var(--otd-accent-rgb), 0.08); + --otd-accent-15: rgba(var(--otd-accent-rgb), 0.15); + + /* Neutrals — classic wp-admin palette. */ + --otd-ink: #1d2327; + --otd-text: #2c3338; + --otd-muted: #646970; + --otd-subtle: #8c8f94; + --otd-line: #dcdcde; + + /* Semantic (theme-independent). */ + --otd-private: #b32d2e; + + /* Shape + motion. */ + --otd-pill: 9999px; + --otd-ease: 0.15s ease; +} + +/* ----------------------------------------------------------------------------- + 2. Postbox chrome + ----------------------------------------------------------------------------- */ +#dashboard_on_this_day { + & .inside { + margin: 0; + padding: 0; + max-height: 560px; + overflow: auto; + } + + & .hndle { + gap: 0; + } +} + +.on-this-day-widget { + font-size: 13px; + color: var(--otd-ink); + line-height: 1.5; +} + +/* ----------------------------------------------------------------------------- + 3. Widget title (in .hndle) + ----------------------------------------------------------------------------- */ +.on-this-day-title-main { + display: inline-flex; + align-items: center; + gap: 6px; +} + +.on-this-day-title-date { + display: inline-block; + margin-left: 10px; + padding: 2px 9px; + font-weight: 600; + font-size: 11px; + letter-spacing: 0.3px; + text-transform: uppercase; + color: var(--otd-accent-dark); + background: var(--otd-accent-8); + border-radius: var(--otd-pill); + white-space: nowrap; + vertical-align: 1px; +} + +/* ----------------------------------------------------------------------------- + 4. Timeline + ----------------------------------------------------------------------------- + Layout math (reused below): + padding-left of .on-this-day-timeline = 20px + icon width = 28px -> centers at 20 + 14 = 34px + gap between icon and body = 14px -> body column starts at 20 + 28 + 14 = 62px + ----------------------------------------------------------------------------- */ +.on-this-day-timeline { + margin: 0; + padding: 12px 20px 16px; + list-style: none; +} + +.on-this-day-year-group { + list-style: none; + margin: 0; + padding: 0; +} + +.on-this-day-year-header { + margin: 0 0 8px; + padding-left: 42px; /* aligns with the body text column */ + font-size: 12px; + font-weight: 500; + line-height: 1.4; + color: var(--otd-subtle); +} + +.on-this-day-year-number { + color: var(--otd-muted); +} + +.on-this-day-year-ago { + margin-left: 6px; + color: var(--otd-subtle); +} + +.on-this-day-post-list { + position: relative; + margin: 0; + padding: 0; + list-style: none; + + /* Vertical guide line, scoped per year group so the year header + naturally interrupts the line between groups. The halo on each + icon circle (see below) punches ~3px gaps around every circle. */ + &::before { + content: ""; + position: absolute; + left: 14px; /* center of the 28px icon column */ + top: 6px; + bottom: 14px; + width: 1px; + background: var(--otd-line); + } +} + +/* ----------------------------------------------------------------------------- + 5. Post row + ----------------------------------------------------------------------------- */ +.on-this-day-post { + display: grid; + grid-template-columns: 28px 1fr; + gap: 14px; + padding: 6px 0 14px; + + /* Icon circle on the left. */ + & .on-this-day-post-icon { + position: relative; + z-index: 1; /* sits above the vertical line */ + display: inline-flex; + align-items: center; + justify-content: center; + width: 28px; + height: 28px; + border-radius: 50%; + background: #fff; + border: 1px solid var(--otd-line); + color: var(--otd-muted); + + /* 3px white halo so the timeline line appears to stop short of + the circle on both sides rather than butting right into it. */ + box-shadow: 0 0 0 3px #fff; + + & svg { + display: block; + } + } + + &.is-private .on-this-day-post-icon { + color: var(--otd-private); + border-color: #f5c9cc; + background: #fcf0f1; + } +} + +.on-this-day-post-body { + min-width: 0; +} + +.on-this-day-post-title { + margin: 0 0 2px; + font-size: 13px; + font-weight: 600; + line-height: 1.4; + + & a { + color: var(--otd-ink); + text-decoration: none; + box-shadow: none; + + &:hover, + &:focus { + color: var(--otd-accent); + } + } +} + +.on-this-day-post-excerpt { + margin: 0 0 6px; + color: var(--otd-text); + line-height: 1.5; + display: -webkit-box; + -webkit-box-orient: vertical; + -webkit-line-clamp: 2; + line-clamp: 2; + overflow: hidden; +} + +.on-this-day-post-meta { + display: flex; + align-items: center; + flex-wrap: wrap; + gap: 6px; + font-size: 12px; + color: var(--otd-subtle); + line-height: 1.5; +} + +.on-this-day-post-time { + color: var(--otd-muted); +} + +.on-this-day-post-sep { + color: var(--otd-line); +} + +.on-this-day-post-categories { + max-width: 240px; + color: var(--otd-muted); + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.on-this-day-post-private { + color: var(--otd-private); + font-weight: 500; +} + +.on-this-day-post-actions { + display: inline-flex; + gap: 12px; + margin-left: auto; /* push to the right edge of the meta row */ +} + +.on-this-day-post-action { + color: var(--otd-accent); + text-decoration: none; + + &:hover, + &:focus { + color: var(--otd-accent-dark); + text-decoration: underline; + } +} + +/* ----------------------------------------------------------------------------- + 6. Empty state + ----------------------------------------------------------------------------- */ +.on-this-day-empty { + text-align: center; + padding: 28px 20px 24px; +} + +.on-this-day-empty-icon { + display: inline-flex; + align-items: center; + justify-content: center; + width: 60px; + height: 60px; + margin-bottom: 10px; + color: var(--otd-accent); + background: var(--otd-accent-8); + border-radius: 50%; + box-shadow: inset 0 0 0 1px var(--otd-accent-15); +} + +.on-this-day-empty-title { + margin: 6px 0 4px; + font-size: 15px; + font-weight: 600; + color: var(--otd-ink); +} + +.on-this-day-empty-text { + max-width: 340px; + margin: 0 auto 12px; + color: var(--otd-text); + line-height: 1.55; +} + +.on-this-day-empty-cta { + margin: 0; +} + +/* ----------------------------------------------------------------------------- + 7. Adaptive rules + ----------------------------------------------------------------------------- */ +@media (prefers-reduced-motion: reduce) { + .on-this-day-post-title a, + .on-this-day-post-action { + transition: none; + } +} + diff --git a/src/wp-admin/includes/class-wp-on-this-day.php b/src/wp-admin/includes/class-wp-on-this-day.php new file mode 100644 index 0000000000000..d86d3fd111566 --- /dev/null +++ b/src/wp-admin/includes/class-wp-on-this-day.php @@ -0,0 +1,339 @@ +'; + if ( empty( $posts ) ) { + self::render_empty_state(); + } else { + self::render_posts( $posts ); + } + echo ''; + $html = ob_get_clean(); + + wp_cache_set( $cache_key, $html, self::CACHE_GROUP, DAY_IN_SECONDS ); + + // Already escaped at write time by the render_* methods below. + echo $html; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped + } + + /** + * Retrieves posts by a given author that were published on this + * same month and day in previous years. + * + * @since 7.1.0 + * + * @param int $user_id Author ID to query posts for. + * @return WP_Post[] Array of posts ordered by newest first. + */ + public static function get_posts( $user_id ) { + $args = array( + 'author' => (int) $user_id, + 'post_type' => 'post', + 'post_status' => array( 'publish', 'private' ), + 'posts_per_page' => self::POSTS_PER_PAGE, + 'ignore_sticky_posts' => true, + 'orderby' => 'date', + 'order' => 'DESC', + 'no_found_rows' => true, + ); + + /** + * Filters the arguments used to query posts for the On This Day dashboard widget. + * + * @since 7.1.0 + * + * @param array $args WP_Query arguments. + * @param int $user_id The author ID the query is scoped to. + */ + $args = apply_filters( 'dashboard_on_this_day_query_args', $args, $user_id ); + + add_filter( 'posts_where', array( __CLASS__, 'filter_posts_where' ) ); + $query = new WP_Query( $args ); + remove_filter( 'posts_where', array( __CLASS__, 'filter_posts_where' ) ); + + return $query->posts; + } + + /** + * Restricts the widget's query to the current month and day in prior years. + * + * @since 7.1.0 + * + * @global wpdb $wpdb WordPress database abstraction object. + * + * @param string $where SQL WHERE clause. + * @return string Filtered WHERE clause. + */ + public static function filter_posts_where( $where ) { + global $wpdb; + + $month = (int) current_time( 'n' ); + $day = (int) current_time( 'j' ); + $year = (int) current_time( 'Y' ); + + $where .= $wpdb->prepare( + " AND MONTH({$wpdb->posts}.post_date) = %d AND DAY({$wpdb->posts}.post_date) = %d AND YEAR({$wpdb->posts}.post_date) < %d", + $month, + $day, + $year + ); + + return $where; + } + + /** + * Renders the empty state shown when no matching posts exist. + * + * @since 7.1.0 + */ + protected static function render_empty_state() { + ?> +
+ +

+

+ ' . esc_html( date_i18n( 'F j' ) ) . '' + ); + ?> +

+

+ + + +

+
+ + + ID ); + $view_link = get_permalink( $post->ID ); + $status = get_post_status( $post ); + $is_private = ( 'private' === $status ); + + $title = get_the_title( $post ); + if ( '' === trim( $title ) ) { + $title = __( '(no title)' ); + } + + $excerpt = has_excerpt( $post ) ? $post->post_excerpt : $post->post_content; + $excerpt = wp_strip_all_tags( strip_shortcodes( $excerpt ) ); + $excerpt = preg_replace( '/\s+/', ' ', $excerpt ); + $excerpt = wp_trim_words( trim( $excerpt ), 24, '…' ); + + $time_str = get_the_time( get_option( 'time_format' ), $post ); + $time_iso = get_the_time( 'Y-m-d H:i', $post ); + $categories = get_the_category( $post->ID ); + + $row_classes = 'on-this-day-post'; + if ( $is_private ) { + $row_classes .= ' is-private'; + } + ?> +
  • + +
  • + %1$s%2$s', + __( 'On This Day' ), + esc_html( date_i18n( 'F j' ) ) + ); + + wp_add_dashboard_widget( 'dashboard_on_this_day', $on_this_day_title, array( 'WP_On_This_Day', 'render_dashboard_widget' ) ); + } + // WordPress Events and News. wp_add_dashboard_widget( 'dashboard_primary', __( 'WordPress Events and News' ), 'wp_dashboard_events_news' ); diff --git a/src/wp-includes/script-loader.php b/src/wp-includes/script-loader.php index 42d42b3f8781d..74754f413c1ee 100644 --- a/src/wp-includes/script-loader.php +++ b/src/wp-includes/script-loader.php @@ -1634,6 +1634,7 @@ function wp_default_styles( $styles ) { $styles->add( 'l10n', "/wp-admin/css/l10n$suffix.css" ); $styles->add( 'code-editor', "/wp-admin/css/code-editor$suffix.css", array( 'wp-codemirror' ) ); $styles->add( 'site-health', "/wp-admin/css/site-health$suffix.css" ); + $styles->add( 'on-this-day', "/wp-admin/css/on-this-day$suffix.css" ); $styles->add( 'wp-admin', false, array( 'dashicons', 'common', 'forms', 'admin-menu', 'dashboard', 'list-tables', 'edit', 'revisions', 'media', 'themes', 'about', 'nav-menus', 'widgets', 'site-icon', 'l10n', 'wp-base-styles' ) ); @@ -1855,6 +1856,7 @@ function wp_default_styles( $styles ) { 'customize-preview', 'login', 'site-health', + 'on-this-day', 'wp-empty-template-alert', // Includes CSS. 'buttons',