From 6150b2eac822746964138aeaae640b67c98fa9ec Mon Sep 17 00:00:00 2001 From: Jessica Rynkar Date: Fri, 22 May 2026 12:44:33 +0100 Subject: [PATCH 01/16] feat(ui): update version diff view to v4 design --- .../next/src/views/Version/Default/index.css | 189 ++++++++++++++++ .../next/src/views/Version/Default/index.scss | 170 -------------- .../next/src/views/Version/Default/index.tsx | 23 +- .../DiffCollapser/index.css | 76 +++++++ .../DiffCollapser/index.scss | 81 ------- .../DiffCollapser/index.tsx | 14 +- .../RenderVersionFieldsToDiff.tsx | 2 +- .../RenderFieldsToDiff/buildVersionFields.tsx | 6 +- .../fields/Checkbox/index.css | 100 ++++++++ .../fields/Checkbox/index.tsx | 76 +++++++ .../fields/Date/{index.scss => index.css} | 7 + .../RenderFieldsToDiff/fields/Date/index.tsx | 27 ++- .../RenderFieldsToDiff/fields/Group/index.css | 5 + .../fields/Group/index.scss | 9 - .../RenderFieldsToDiff/fields/Group/index.tsx | 2 +- .../fields/Iterable/index.css | 40 ++++ .../fields/Iterable/index.scss | 59 ----- .../fields/Iterable/index.tsx | 4 +- .../fields/Relationship/index.css | 189 ++++++++++++++++ .../fields/Relationship/index.scss | 90 -------- .../fields/Relationship/index.tsx | 78 ++++--- .../fields/Select/{index.scss => index.css} | 0 .../fields/Select/index.tsx | 27 +-- .../RenderFieldsToDiff/fields/Tabs/index.css | 6 + .../RenderFieldsToDiff/fields/Tabs/index.scss | 9 - .../RenderFieldsToDiff/fields/Tabs/index.tsx | 2 +- .../fields/Text/{index.scss => index.css} | 0 .../RenderFieldsToDiff/fields/Text/index.tsx | 24 +- .../fields/Upload/index.css | 142 ++++++++++++ .../fields/Upload/index.scss | 120 ---------- .../fields/Upload/index.tsx | 213 +++++++++++------- .../RenderFieldsToDiff/fields/index.ts | 3 +- .../Version/RenderFieldsToDiff/index.css | 24 ++ .../Version/RenderFieldsToDiff/index.scss | 24 -- .../next/src/views/Version/Restore/index.css | 26 +++ .../next/src/views/Version/Restore/index.scss | 83 ------- .../next/src/views/Version/Restore/index.tsx | 7 +- .../VersionPillLabel/VersionPillLabel.tsx | 25 +- .../views/Version/VersionPillLabel/index.css | 31 +++ .../views/Version/VersionPillLabel/index.scss | 26 --- packages/ui/src/css/typography.css | 2 +- packages/ui/src/elements/Button/index.css | 49 +++- packages/ui/src/elements/Button/index.tsx | 5 +- packages/ui/src/elements/Button/types.ts | 5 + .../src/elements/FieldDiffContainer/index.css | 43 ++++ .../elements/FieldDiffContainer/index.scss | 45 ---- .../src/elements/FieldDiffContainer/index.tsx | 19 +- .../ui/src/elements/FieldDiffLabel/index.css | 15 ++ .../ui/src/elements/FieldDiffLabel/index.scss | 11 - .../ui/src/elements/FieldDiffLabel/index.tsx | 2 +- packages/ui/src/elements/HTMLDiff/colors.css | 17 ++ packages/ui/src/elements/HTMLDiff/colors.scss | 35 --- packages/ui/src/elements/HTMLDiff/index.css | 181 +++++++++++++++ packages/ui/src/elements/HTMLDiff/index.scss | 170 -------------- packages/ui/src/elements/HTMLDiff/index.tsx | 3 +- packages/ui/src/elements/Popup/index.css | 2 +- .../ReactSelect/DropdownIndicator/index.tsx | 2 +- test/v4/baseConfig.ts | 6 + test/v4/collections/VersionsDiff/index.ts | 191 ++++++++++++++++ test/v4/seed/versionsDiffData.ts | 79 +++++++ test/v4/slugs.ts | 2 + 61 files changed, 1787 insertions(+), 1136 deletions(-) create mode 100644 packages/next/src/views/Version/Default/index.css delete mode 100644 packages/next/src/views/Version/Default/index.scss create mode 100644 packages/next/src/views/Version/RenderFieldsToDiff/DiffCollapser/index.css delete mode 100644 packages/next/src/views/Version/RenderFieldsToDiff/DiffCollapser/index.scss create mode 100644 packages/next/src/views/Version/RenderFieldsToDiff/fields/Checkbox/index.css create mode 100644 packages/next/src/views/Version/RenderFieldsToDiff/fields/Checkbox/index.tsx rename packages/next/src/views/Version/RenderFieldsToDiff/fields/Date/{index.scss => index.css} (66%) create mode 100644 packages/next/src/views/Version/RenderFieldsToDiff/fields/Group/index.css delete mode 100644 packages/next/src/views/Version/RenderFieldsToDiff/fields/Group/index.scss create mode 100644 packages/next/src/views/Version/RenderFieldsToDiff/fields/Iterable/index.css delete mode 100644 packages/next/src/views/Version/RenderFieldsToDiff/fields/Iterable/index.scss create mode 100644 packages/next/src/views/Version/RenderFieldsToDiff/fields/Relationship/index.css delete mode 100644 packages/next/src/views/Version/RenderFieldsToDiff/fields/Relationship/index.scss rename packages/next/src/views/Version/RenderFieldsToDiff/fields/Select/{index.scss => index.css} (100%) create mode 100644 packages/next/src/views/Version/RenderFieldsToDiff/fields/Tabs/index.css delete mode 100644 packages/next/src/views/Version/RenderFieldsToDiff/fields/Tabs/index.scss rename packages/next/src/views/Version/RenderFieldsToDiff/fields/Text/{index.scss => index.css} (100%) create mode 100644 packages/next/src/views/Version/RenderFieldsToDiff/fields/Upload/index.css delete mode 100644 packages/next/src/views/Version/RenderFieldsToDiff/fields/Upload/index.scss create mode 100644 packages/next/src/views/Version/RenderFieldsToDiff/index.css delete mode 100644 packages/next/src/views/Version/RenderFieldsToDiff/index.scss create mode 100644 packages/next/src/views/Version/Restore/index.css delete mode 100644 packages/next/src/views/Version/Restore/index.scss create mode 100644 packages/next/src/views/Version/VersionPillLabel/index.css delete mode 100644 packages/next/src/views/Version/VersionPillLabel/index.scss create mode 100644 packages/ui/src/elements/FieldDiffContainer/index.css delete mode 100644 packages/ui/src/elements/FieldDiffContainer/index.scss create mode 100644 packages/ui/src/elements/FieldDiffLabel/index.css delete mode 100644 packages/ui/src/elements/FieldDiffLabel/index.scss create mode 100644 packages/ui/src/elements/HTMLDiff/colors.css delete mode 100644 packages/ui/src/elements/HTMLDiff/colors.scss create mode 100644 packages/ui/src/elements/HTMLDiff/index.css delete mode 100644 packages/ui/src/elements/HTMLDiff/index.scss create mode 100644 test/v4/collections/VersionsDiff/index.ts create mode 100644 test/v4/seed/versionsDiffData.ts diff --git a/packages/next/src/views/Version/Default/index.css b/packages/next/src/views/Version/Default/index.css new file mode 100644 index 00000000000..9a245e5f962 --- /dev/null +++ b/packages/next/src/views/Version/Default/index.css @@ -0,0 +1,189 @@ +@layer payload-default { + .view-version { + width: 100%; + padding-bottom: var(--spacing-view-bottom); + } + + .view-version__toggle-locales-label { + color: var(--color-text-secondary); + } + + .view-version-controls-top { + border-bottom: var(--stroke-width-small) solid var(--color-border); + padding: var(--spacer-2) var(--spacer-4); + } + + .view-version-controls-top__wrapper { + display: flex; + flex-direction: row; + justify-content: space-between; + align-items: center; + } + + .view-version-controls-top__wrapper-actions { + display: flex; + flex-direction: row; + align-items: center; + gap: var(--spacer-4); + } + + .view-version-controls-top h2 { + color: var(--color-text); + font-family: var(--text-body-large-strong-font-family); + font-size: var(--text-body-large-strong-font-size); + font-style: normal; + font-weight: var(--text-body-large-strong-font-weight); + line-height: var(--text-body-large-strong-line-height); + letter-spacing: var(--text-body-large-strong-letter-spacing); + } + + .view-version-controls-bottom { + border-bottom: var(--stroke-width-small) solid var(--color-border); + padding: var(--spacer-2-5) var(--spacer-4); + position: relative; + + /* Vertical separator line */ + &::after { + content: ''; + position: absolute; + top: 0; + bottom: 0; + left: 50%; + width: var(--stroke-width-small); + background-color: var(--color-border); + transform: translateX(-50%); + } + } + + .view-version-controls-bottom__wrapper { + display: grid; + grid-template-columns: 1fr 1fr; + gap: var(--spacer-4); + } + + .view-version__time-elapsed { + color: var(--color-text-secondary); + font-weight: var(--text-body-medium-font-weight); + } + + .view-version__version-from, + .view-version__version-to { + display: flex; + flex-direction: column; + gap: var(--spacer-2); + } + + .view-version__version-from-labels, + .view-version__version-to-labels { + display: flex; + flex-direction: row; + justify-content: space-between; + color: var(--color-text); + font-family: var(--text-body-medium-strong-font-family); + font-size: var(--text-body-medium-strong-font-size); + font-style: normal; + font-weight: var(--text-body-medium-strong-font-weight); + line-height: var(--text-body-medium-strong-line-height); + letter-spacing: var(--text-body-medium-letter-spacing); + } + + .view-version__version-to-version { + display: flex; + flex-direction: row; + align-items: center; + justify-content: space-between; + border: var(--stroke-width-small) solid var(--color-border); + border-radius: var(--radius-medium); + padding: var(--spacer-1) var(--spacer-2); + gap: var(--spacer-2); + } + + .view-version__version-to-version h2 { + font-family: var(--text-body-medium-font-family); + font-size: var(--text-body-medium-font-size); + font-weight: var(--text-body-medium-font-weight); + line-height: var(--text-body-medium-line-height); + letter-spacing: var(--text-body-medium-letter-spacing); + color: var(--color-text-secondary); + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + } + + .view-version__restore div { + margin-block: 0; + } + + .view-version__modifiedCheckBox { + margin: 0 0 0 var(--spacer-4); + display: flex; + align-items: center; + overflow: hidden; + color: var(--color-text); + text-overflow: ellipsis; + font-family: var(--text-body-medium-font-family); + font-size: var(--text-body-medium-font-size); + font-style: normal; + font-weight: var(--text-body-medium-font-weight); + line-height: var(--text-body-medium-line-height); + letter-spacing: var(--text-body-medium-letter-spacing); + } + + .view-version__modifiedCheckBox label.field-label { + font-weight: var(--text-body-medium-font-weight); + } + + .compare-version .rs__control { + padding: var(--spacer-2); + } + + .view-version__diff-wrap { + padding: var(--spacer-3) var(--spacer-4); + display: flex; + flex-direction: column; + gap: var(--spacer-3); + position: relative; + border-bottom: var(--stroke-width-small) solid var(--color-border); + + /* Vertical separator line */ + &::after { + content: ''; + position: absolute; + top: 0; + bottom: 0; + left: 50%; + width: var(--stroke-width-small); + background-color: var(--color-border); + transform: translateX(-50%); + z-index: 2; + } + } + + @media (max-width: 1024px) { + .view-version__version-to-version { + flex-direction: column; + align-items: flex-start; + } + } + + @media (max-width: 768px) { + .view-version__diff-wrap { + padding: var(--spacer-2) var(--spacer-4); + } + + .view-version__version-to-labels, + .view-version__version-from-labels { + flex-direction: column; + align-items: flex-start; + } + + .view-version-controls-top__wrapper { + flex-direction: column; + align-items: flex-start; + } + + .view-version-controls-top__wrapper .view-version__modifiedCheckBox { + margin-left: 0; + } + } +} diff --git a/packages/next/src/views/Version/Default/index.scss b/packages/next/src/views/Version/Default/index.scss deleted file mode 100644 index f9a3f23f7ad..00000000000 --- a/packages/next/src/views/Version/Default/index.scss +++ /dev/null @@ -1,170 +0,0 @@ -@import '~@payloadcms/ui/scss'; - -@layer payload-default { - .view-version { - width: 100%; - padding-bottom: var(--spacing-view-bottom); - - &__toggle-locales-label { - color: var(--theme-elevation-500); - } - - &-controls-top { - border-bottom: 1px solid var(--theme-elevation-100); - padding: 16px var(--gutter-h) 16px var(--gutter-h); - - &__wrapper { - display: flex; - flex-direction: row; - justify-content: space-between; - align-items: center; - - &-actions { - display: flex; - flex-direction: row; - align-items: center; - gap: var(--base); - } - } - - h2 { - font-size: 18px; - } - } - - &-controls-bottom { - border-bottom: 1px solid var(--theme-elevation-100); - padding: 16px var(--gutter-h) 16px var(--gutter-h); - position: relative; - - // Vertical separator line - &::after { - content: ''; - position: absolute; - top: 0; - bottom: 0; - left: 50%; - width: 1px; - background-color: var(--theme-elevation-100); - transform: translateX(-50%); // Center the line - } - - &__wrapper { - display: grid; - grid-template-columns: 1fr 1fr; - grid-gap: var(--base); - gap: var(--base); - } - } - - &__time-elapsed { - color: var(--theme-elevation-500); - } - - &__version-from { - display: flex; - flex-direction: column; - gap: 5px; - - &-labels { - display: flex; - flex-direction: row; - justify-content: space-between; - } - } - - &__version-to { - display: flex; - flex-direction: column; - gap: 5px; - - &-labels { - display: flex; - flex-direction: row; - justify-content: space-between; - } - - &-version { - display: flex; - flex-direction: row; - align-items: center; - justify-content: space-between; - background: var(--theme-elevation-50); - padding: 8px 12px; - gap: calc(var(--base) / 2); - - h2 { - font-size: 13px; - font-weight: 400; - } - } - } - - &__restore { - div { - margin-block: 0; - } - } - - &__modifiedCheckBox { - margin: 0 0 0 var(--base); - display: flex; - align-items: center; - } - - &__diff-wrap { - padding-top: var(--base); - display: flex; - flex-direction: column; - gap: var(--base); - position: relative; - - // Vertical separator line - &::after { - content: ''; - position: absolute; - top: 0; - bottom: 0; - left: 50%; - width: 1px; - background-color: var(--theme-elevation-100); - transform: translateX(-50%); // Center the line - z-index: 2; - } - } - - @include mid-break { - &__version-to { - &-version { - flex-direction: column; - align-items: flex-start; - } - } - } - - @include small-break { - &__diff-wrap { - padding-top: calc(var(--base) / 2); - } - - &__version-to, - &__version-from { - &-labels { - flex-direction: column; - align-items: flex-start; - } - } - - &-controls-top { - &__wrapper { - flex-direction: column; - align-items: flex-start; - - .view-version__modifiedCheckBox { - margin-left: 0; - } - } - } - } - } -} diff --git a/packages/next/src/views/Version/Default/index.tsx b/packages/next/src/views/Version/Default/index.tsx index 521919bd5ee..05f6fead1b0 100644 --- a/packages/next/src/views/Version/Default/index.tsx +++ b/packages/next/src/views/Version/Default/index.tsx @@ -5,7 +5,6 @@ import { CheckboxInput, ChevronIcon, formatTimeToNow, - Gutter, Pill, type SelectablePill, useConfig, @@ -20,7 +19,7 @@ import React, { type FormEventHandler, useCallback, useEffect, useMemo, useState import type { CompareOption, DefaultVersionsViewProps } from './types.js' import { Restore } from '../Restore/index.js' -import './index.scss' +import './index.css' import { SelectComparison } from '../SelectComparison/index.js' import { type SelectedLocaleOnChange, SelectLocales } from '../SelectLocales/index.js' import { SelectedLocalesContext } from './SelectedLocalesContext.js' @@ -180,9 +179,15 @@ export const DefaultVersionView: React.FC = ({ [versionFromCreatedAt, i18n, t], ) + const selectedLocaleNames = useMemo(() => locales.map((locale) => locale.name), [locales]) + const selectedLocalesContextValue = useMemo( + () => ({ selectedLocales: selectedLocaleNames }), + [selectedLocaleNames], + ) + return (
- +

{i18n.t('version:compareVersions')}

@@ -225,8 +230,8 @@ export const DefaultVersionView: React.FC = ({ onChange={onChangeSelectedLocales} /> )} - - +
+
@@ -267,7 +272,7 @@ export const DefaultVersionView: React.FC = ({
- +
= ({ versionToCreatedAtFormatted={versionToCreatedAtFormatted} versionToID={versionToID} /> - - locale.name) }}> +
+ {versionToCreatedAt && RenderedDiff} - +
) } diff --git a/packages/next/src/views/Version/RenderFieldsToDiff/DiffCollapser/index.css b/packages/next/src/views/Version/RenderFieldsToDiff/DiffCollapser/index.css new file mode 100644 index 00000000000..cb14fcc0dd6 --- /dev/null +++ b/packages/next/src/views/Version/RenderFieldsToDiff/DiffCollapser/index.css @@ -0,0 +1,76 @@ +@layer payload-default { + .diff-collapser__toggle-button { + all: unset; + cursor: pointer; + position: relative; + z-index: 1; + display: flex; + align-items: center; + + .icon { + color: var(--color-icon-tertiary); + } + + &:hover { + &::before { + content: ''; + position: absolute; + top: 0; + left: calc(var(--spacer-1) * -1); + right: 0; + bottom: 0; + background-color: var(--color-bg-hover); + border-radius: var(--radius-medium); + z-index: -1; + } + } + + &:active { + &::before { + content: ''; + position: absolute; + top: 0; + left: calc(var(--spacer-1) * -1); + right: 0; + bottom: 0; + background-color: var(--color-bg-pressed); + border-radius: var(--radius-medium); + z-index: -1; + } + } + } + + .diff-collapser__label { + margin: 0; + display: inline-flex; + height: 100%; + } + + .diff-collapser__field-change-count { + margin-left: var(--spacer-2); + padding: 0 var(--spacer-1); + background: var(--color-bg-tertiary); + border-radius: var(--radius-medium); + font-family: var(--text-body-medium-font-family); + font-size: var(--text-body-medium-font-size); + font-weight: var(--text-body-medium-font-weight); + line-height: var(--text-body-medium-line-height); + letter-spacing: var(--text-body-medium-letter-spacing); + } + + .diff-collapser__content:not(.diff-collapser__content--hide-gutter) { + [dir='ltr'] & { + border-left: var(--stroke-width-small) solid var(--color-border); + padding-left: var(--spacer-2-5); + } + + [dir='rtl'] & { + border-right: var(--stroke-width-small) solid var(--color-border); + padding-right: var(--spacer-2-5); + } + } + + .diff-collapser__content--is-collapsed { + display: none; + } +} diff --git a/packages/next/src/views/Version/RenderFieldsToDiff/DiffCollapser/index.scss b/packages/next/src/views/Version/RenderFieldsToDiff/DiffCollapser/index.scss deleted file mode 100644 index d73cac43a46..00000000000 --- a/packages/next/src/views/Version/RenderFieldsToDiff/DiffCollapser/index.scss +++ /dev/null @@ -1,81 +0,0 @@ -@import '~@payloadcms/ui/scss'; - -@layer payload-default { - .diff-collapser { - &__toggle-button { - all: unset; - cursor: pointer; - position: relative; - z-index: 1; - display: flex; - align-items: center; - - .icon { - color: var(--theme-elevation-500); - } - - &:hover { - // Apply background color but with padding, thus we use after - &::before { - content: ''; - position: absolute; - top: -(base(0.15)); - left: -(base(0.15)); - right: -(base(0.15)); - bottom: -(base(0.15)); - background-color: var(--theme-elevation-50); - border-radius: var(--style-radius-s); - z-index: -1; - } - - .iterable-diff__label { - background-color: var(--theme-elevation-50); - z-index: 1; - } - } - } - - &__label { - // Add space between label, chevron, and change count - margin: 0 calc(var(--base) * 0.3) 0 0; - display: inline-flex; - height: 100%; - } - - &__field-change-count { - // Reset the font weight of the change count to normal - font-weight: normal; - margin-left: calc(var(--base) * 0.3); - padding: calc(var(--base) * 0.1) calc(var(--base) * 0.2); - background: var(--theme-elevation-100); - border-radius: var(--style-radius-s); - font-size: 0.8rem; - } - - &__content:not(.diff-collapser__content--hide-gutter) { - [dir='ltr'] & { - // Vertical gutter - border-left: 2px solid var(--theme-elevation-100); - // Center-align the gutter with the chevron - margin-left: 3px; - // Content indentation - padding-left: calc(var(--base) * 0.5); - } - [dir='rtl'] & { - // Vertical gutter - border-right: 2px solid var(--theme-elevation-100); - // Center-align the gutter with the chevron - margin-right: 3px; - // Content indentation - padding-right: calc(var(--base) * 0.5); - } - } - - &__content--is-collapsed { - // Hide the content when collapsed. We use display: none instead of - // conditional rendering to avoid loosing children's collapsed state when - // remounting. - display: none; - } - } -} diff --git a/packages/next/src/views/Version/RenderFieldsToDiff/DiffCollapser/index.tsx b/packages/next/src/views/Version/RenderFieldsToDiff/DiffCollapser/index.tsx index 5e4111edf09..3b28756e74b 100644 --- a/packages/next/src/views/Version/RenderFieldsToDiff/DiffCollapser/index.tsx +++ b/packages/next/src/views/Version/RenderFieldsToDiff/DiffCollapser/index.tsx @@ -5,12 +5,13 @@ import { ChevronIcon, FieldDiffLabel, useConfig, useTranslation } from '@payload import { fieldIsArrayType, fieldIsBlockType } from 'payload/shared' import React, { useState } from 'react' -import './index.scss' +import './index.css' import { countChangedFields, countChangedFieldsInRows } from '../utilities/countChangedFields.js' const baseClass = 'diff-collapser' type Props = { + changeCountOverride?: number hideGutter?: boolean initCollapsed?: boolean Label: React.ReactNode @@ -37,6 +38,7 @@ type Props = { ) export const DiffCollapser: React.FC = ({ + changeCountOverride, children, field, fields, @@ -53,9 +55,9 @@ export const DiffCollapser: React.FC = ({ const [isCollapsed, setIsCollapsed] = useState(initCollapsed) const { config } = useConfig() - let changeCount = 0 + let changeCount = changeCountOverride ?? 0 - if (isIterable) { + if (changeCountOverride === undefined && isIterable) { if (!fieldIsArrayType(field) && !fieldIsBlockType(field)) { throw new Error( 'DiffCollapser: field must be an array or blocks field when isIterable is true', @@ -65,9 +67,7 @@ export const DiffCollapser: React.FC = ({ const valueToRows = valueTo ?? [] if (!Array.isArray(valueFromRows) || !Array.isArray(valueToRows)) { - throw new Error( - 'DiffCollapser: valueFrom and valueTro must be arrays when isIterable is true', - ) + throw new Error('DiffCollapser: valueFrom and valueTo must be arrays when isIterable is true') } changeCount = countChangedFieldsInRows({ @@ -78,7 +78,7 @@ export const DiffCollapser: React.FC = ({ valueFromRows, valueToRows, }) - } else { + } else if (changeCountOverride === undefined) { changeCount = countChangedFields({ config, fields, diff --git a/packages/next/src/views/Version/RenderFieldsToDiff/RenderVersionFieldsToDiff.tsx b/packages/next/src/views/Version/RenderFieldsToDiff/RenderVersionFieldsToDiff.tsx index 3f8ec527880..7e4f2208f3f 100644 --- a/packages/next/src/views/Version/RenderFieldsToDiff/RenderVersionFieldsToDiff.tsx +++ b/packages/next/src/views/Version/RenderFieldsToDiff/RenderVersionFieldsToDiff.tsx @@ -2,7 +2,7 @@ const baseClass = 'render-field-diffs' import type { VersionField } from 'payload' -import './index.scss' +import './index.css' import { ShimmerEffect } from '@payloadcms/ui' import React, { Fragment, useEffect } from 'react' diff --git a/packages/next/src/views/Version/RenderFieldsToDiff/buildVersionFields.tsx b/packages/next/src/views/Version/RenderFieldsToDiff/buildVersionFields.tsx index 1b8bbcd362c..52139c63c92 100644 --- a/packages/next/src/views/Version/RenderFieldsToDiff/buildVersionFields.tsx +++ b/packages/next/src/views/Version/RenderFieldsToDiff/buildVersionFields.tsx @@ -388,7 +388,7 @@ const buildVersionField = ({ fieldsPermissions: subFieldsPermissions, i18n, modifiedOnly, - nestingLevel: nestingLevel + 1, + nestingLevel: nestingLevel + 2, parentIndexPath: 'name' in field ? '' : indexPath, parentIsLocalized: parentIsLocalized || field.localized, parentPath: ('name' in field ? path : parentPath) + '.' + i, @@ -455,7 +455,7 @@ const buildVersionField = ({ if (toRow.blockType === fromRow.blockType) { fields = toBlock.fields } else { - const fromBlockSlugToMatch: string = toRow?.blockType ?? fromRow?.blockType + const fromBlockSlugToMatch: string = fromRow?.blockType ?? toRow?.blockType const fromBlock = req.payload.blocks[fromBlockSlugToMatch] ?? @@ -494,7 +494,7 @@ const buildVersionField = ({ fieldsPermissions: blockFieldsPermissions, i18n, modifiedOnly, - nestingLevel: nestingLevel + 1, + nestingLevel: nestingLevel + 2, parentIndexPath: 'name' in field ? '' : indexPath, parentIsLocalized: parentIsLocalized || ('localized' in field && field.localized), parentPath: ('name' in field ? path : parentPath) + '.' + i, diff --git a/packages/next/src/views/Version/RenderFieldsToDiff/fields/Checkbox/index.css b/packages/next/src/views/Version/RenderFieldsToDiff/fields/Checkbox/index.css new file mode 100644 index 00000000000..b0eb3784ebc --- /dev/null +++ b/packages/next/src/views/Version/RenderFieldsToDiff/fields/Checkbox/index.css @@ -0,0 +1,100 @@ +@layer payload-default { + .checkbox-diff__value { + display: flex; + align-items: center; + gap: var(--spacer-2); + position: relative; + z-index: 1; + } + + .checkbox-diff__value--create::before, + .checkbox-diff__value--delete::before { + content: ''; + position: absolute; + top: calc(var(--spacer-2) * -1); + bottom: calc(var(--spacer-2) * -1); + left: calc(var(--spacer-2-5) * -1); + right: calc(var(--spacer-2-5) * -1); + display: block; + z-index: -1; + } + + .checkbox-diff__value--create::before { + background-color: var(--diff-create-parent-bg); + } + + .checkbox-diff__value--delete::before { + background-color: var(--diff-delete-parent-bg); + } + + .checkbox-diff__indicator { + display: flex; + align-items: center; + justify-content: center; + width: var(--spacer-3); + height: var(--spacer-3); + border: var(--stroke-width-small) solid var(--color-border-strong); + border-radius: var(--radius-medium); + background: transparent; + flex-shrink: 0; + position: relative; + } + + .checkbox-diff__indicator--checked { + background-color: var(--color-border-strong); + border-color: var(--color-border-strong); + } + + .checkbox-diff__indicator--create { + border-color: var(--color-border-brand); + background-color: var(--diff-create-parent-bg); + } + + .checkbox-diff__indicator--create.checkbox-diff__indicator--checked { + background-color: var(--color-bg-brand); + border-color: var(--color-border-selected); + } + + .checkbox-diff__indicator--delete { + border-color: var(--color-border-danger); + background-color: var(--diff-delete-parent-bg); + } + + .checkbox-diff__indicator--delete.checkbox-diff__indicator--checked { + background-color: var(--color-bg-danger); + border-color: var(--color-bg-danger); + } + + .checkbox-diff__icon { + position: absolute; + inset: 0; + display: flex; + align-items: center; + justify-content: center; + } + + .checkbox-diff__icon svg { + opacity: 0; + color: var(--color-icon-onbrand); + } + + .checkbox-diff__indicator--checked .checkbox-diff__icon svg { + opacity: 1; + } + + .checkbox-diff__label { + font-family: var(--text-body-medium-font-family); + font-size: var(--text-body-medium-font-size); + font-weight: var(--text-body-medium-font-weight); + line-height: var(--text-body-medium-line-height); + color: var(--color-text); + } + + .checkbox-diff__label--create { + color: var(--diff-create-parent-color); + } + + .checkbox-diff__label--delete { + color: var(--color-text-danger); + } +} diff --git a/packages/next/src/views/Version/RenderFieldsToDiff/fields/Checkbox/index.tsx b/packages/next/src/views/Version/RenderFieldsToDiff/fields/Checkbox/index.tsx new file mode 100644 index 00000000000..1334a605dfc --- /dev/null +++ b/packages/next/src/views/Version/RenderFieldsToDiff/fields/Checkbox/index.tsx @@ -0,0 +1,76 @@ +'use client' +import type { CheckboxFieldDiffClientComponent } from 'payload' + +import { CheckIcon, FieldDiffContainer, useTranslation } from '@payloadcms/ui' +import React from 'react' + +import './index.css' + +const baseClass = 'checkbox-diff' + +const CheckboxIndicator: React.FC<{ + checked: boolean + variant?: 'create' | 'default' | 'delete' +}> = ({ checked, variant = 'default' }) => { + const classNames = [ + `${baseClass}__indicator`, + `${baseClass}__indicator--${variant}`, + checked && `${baseClass}__indicator--checked`, + ] + .filter(Boolean) + .join(' ') + + return ( + + + + + + ) +} + +export const Checkbox: CheckboxFieldDiffClientComponent = ({ + comparisonValue: valueFrom, + field, + locale, + nestingLevel, + versionValue: valueTo, +}) => { + const { i18n } = useTranslation() + + const checkedFrom = Boolean(valueFrom) + const checkedTo = Boolean(valueTo) + const hasChanged = checkedFrom !== checkedTo + + const fromVariant = hasChanged ? 'delete' : 'default' + const toVariant = hasChanged ? 'create' : 'default' + + const From = ( +
+ + + {checkedFrom ? 'Checked' : 'Unchecked'} + +
+ ) + + const To = ( +
+ + + {checkedTo ? 'Checked' : 'Unchecked'} + +
+ ) + + return ( + + ) +} diff --git a/packages/next/src/views/Version/RenderFieldsToDiff/fields/Date/index.scss b/packages/next/src/views/Version/RenderFieldsToDiff/fields/Date/index.css similarity index 66% rename from packages/next/src/views/Version/RenderFieldsToDiff/fields/Date/index.scss rename to packages/next/src/views/Version/RenderFieldsToDiff/fields/Date/index.css index 3b41515ca11..5489aec557f 100644 --- a/packages/next/src/views/Version/RenderFieldsToDiff/fields/Date/index.scss +++ b/packages/next/src/views/Version/RenderFieldsToDiff/fields/Date/index.css @@ -1,9 +1,16 @@ @layer payload-default { + .date-diff[data-match-type] { + background-color: unset !important; + border-radius: unset !important; + padding: 0 !important; + } + .date-diff { p *[data-match-type='delete'] { color: unset !important; background-color: unset !important; } + p *[data-match-type='create'] { color: unset !important; background-color: unset !important; diff --git a/packages/next/src/views/Version/RenderFieldsToDiff/fields/Date/index.tsx b/packages/next/src/views/Version/RenderFieldsToDiff/fields/Date/index.tsx index c108ff120d5..ba5659119be 100644 --- a/packages/next/src/views/Version/RenderFieldsToDiff/fields/Date/index.tsx +++ b/packages/next/src/views/Version/RenderFieldsToDiff/fields/Date/index.tsx @@ -12,7 +12,7 @@ import { import { formatDate } from '@payloadcms/ui/shared' import React from 'react' -import './index.scss' +import './index.css' const baseClass = 'date-diff' @@ -46,33 +46,38 @@ export const DateDiffComponent: DateFieldDiffClientComponent = ({ }) : '' + // TODO: translate 'No value' + const NoValue =
No value
+ const escapedFromDate = escapeDiffHTML(formattedFromDate) const escapedToDate = escapeDiffHTML(formattedToDate) const { From, To } = getHTMLDiffComponents({ - fromHTML: - `

` + - escapedFromDate + - '

', + fromHTML: formattedFromDate + ? `

` + + escapedFromDate + + '

' + : '

', postProcess: unescapeDiffHTML, - toHTML: - `

` + - escapedToDate + - '

', + toHTML: formattedToDate + ? `

` + + escapedToDate + + '

' + : '

', tokenizeByCharacter: false, }) return ( ) } diff --git a/packages/next/src/views/Version/RenderFieldsToDiff/fields/Group/index.css b/packages/next/src/views/Version/RenderFieldsToDiff/fields/Group/index.css new file mode 100644 index 00000000000..8993f10ea97 --- /dev/null +++ b/packages/next/src/views/Version/RenderFieldsToDiff/fields/Group/index.css @@ -0,0 +1,5 @@ +@layer payload-default { + .group-diff__locale-label--no-label { + color: var(--color-text-secondary); + } +} diff --git a/packages/next/src/views/Version/RenderFieldsToDiff/fields/Group/index.scss b/packages/next/src/views/Version/RenderFieldsToDiff/fields/Group/index.scss deleted file mode 100644 index 6a2af115c2e..00000000000 --- a/packages/next/src/views/Version/RenderFieldsToDiff/fields/Group/index.scss +++ /dev/null @@ -1,9 +0,0 @@ -@layer payload-default { - .group-diff { - &__locale-label { - &--no-label { - color: var(--theme-elevation-600); - } - } - } -} diff --git a/packages/next/src/views/Version/RenderFieldsToDiff/fields/Group/index.tsx b/packages/next/src/views/Version/RenderFieldsToDiff/fields/Group/index.tsx index 95761c47da2..926e84dbac3 100644 --- a/packages/next/src/views/Version/RenderFieldsToDiff/fields/Group/index.tsx +++ b/packages/next/src/views/Version/RenderFieldsToDiff/fields/Group/index.tsx @@ -3,7 +3,7 @@ import type { GroupFieldDiffClientComponent } from 'payload' import { getTranslation } from '@payloadcms/translations' -import './index.scss' +import './index.css' import { useTranslation } from '@payloadcms/ui' import React from 'react' diff --git a/packages/next/src/views/Version/RenderFieldsToDiff/fields/Iterable/index.css b/packages/next/src/views/Version/RenderFieldsToDiff/fields/Iterable/index.css new file mode 100644 index 00000000000..af342949049 --- /dev/null +++ b/packages/next/src/views/Version/RenderFieldsToDiff/fields/Iterable/index.css @@ -0,0 +1,40 @@ +@layer payload-default { + .iterable-diff-label-container { + position: relative; + display: flex; + flex-direction: row; + height: 100%; + } + + .iterable-diff__label { + color: var(--color-text-secondary); + font-family: var(--text-body-medium-strong-font-family); + font-size: var(--text-body-medium-strong-font-size); + font-style: normal; + font-weight: var(--font-weight-strong); + line-height: var(--text-body-medium-strong-line-height); + letter-spacing: var(--text-body-medium-strong-letter-spacing); + } + + .iterable-diff__locale-label { + background: var(--color-bg-secondary); + border-radius: var(--radius-medium); + padding: var(--spacer-1); + + [dir='ltr'] & { + margin-right: var(--spacer-1); + } + + [dir='rtl'] & { + margin-left: var(--spacer-1); + } + } + + .iterable-diff__row:not(:first-of-type) { + margin-top: var(--spacer-2); + } + + .iterable-diff__no-rows { + color: var(--color-text-tertiary); + } +} diff --git a/packages/next/src/views/Version/RenderFieldsToDiff/fields/Iterable/index.scss b/packages/next/src/views/Version/RenderFieldsToDiff/fields/Iterable/index.scss deleted file mode 100644 index 96b430610b0..00000000000 --- a/packages/next/src/views/Version/RenderFieldsToDiff/fields/Iterable/index.scss +++ /dev/null @@ -1,59 +0,0 @@ -@layer payload-default { - .iterable-diff { - &-label-container { - position: relative; - height: 20px; - display: flex; - flex-direction: row; - height: 100%; - } - - &-label-prefix { - background-color: var(--theme-bg); - position: relative; - width: calc(var(--base) * 0.5); - height: 16px; - margin-left: calc((var(--base) * -0.5) - 5px); - margin-right: calc(var(--base) * 0.5); - - &::before { - content: ''; - position: absolute; - left: 1px; - top: 8px; - transform: translateY(-50%); - width: 6px; - height: 6px; - background-color: var(--theme-elevation-200); - border-radius: 50%; - margin-right: 5px; - } - } - &__label { - font-weight: 400; - color: var(--theme-elevation-600); - } - - &__locale-label { - background: var(--theme-elevation-100); - border-radius: var(--style-radius-s); - padding: calc(var(--base) * 0.2); - // border-radius: $style-radius-m; - [dir='ltr'] & { - margin-right: calc(var(--base) * 0.25); - } - [dir='rtl'] & { - margin-left: calc(var(--base) * 0.25); - } - } - - // Space between each row - &__row:not(:first-of-type) { - margin-top: calc(var(--base) * 0.5); - } - - &__no-rows { - color: var(--theme-elevation-400); - } - } -} diff --git a/packages/next/src/views/Version/RenderFieldsToDiff/fields/Iterable/index.tsx b/packages/next/src/views/Version/RenderFieldsToDiff/fields/Iterable/index.tsx index 2f0a9201072..ab7a0310ad0 100644 --- a/packages/next/src/views/Version/RenderFieldsToDiff/fields/Iterable/index.tsx +++ b/packages/next/src/views/Version/RenderFieldsToDiff/fields/Iterable/index.tsx @@ -5,7 +5,7 @@ import type { FieldDiffClientProps } from 'payload' import { getTranslation } from '@payloadcms/translations' import { useConfig, useTranslation } from '@payloadcms/ui' -import './index.scss' +import './index.css' import { fieldIsArrayType, fieldIsBlockType } from 'payload/shared' import React from 'react' @@ -87,10 +87,8 @@ export const Iterable: React.FC = ({
-
{rowLabel}
} diff --git a/packages/next/src/views/Version/RenderFieldsToDiff/fields/Relationship/index.css b/packages/next/src/views/Version/RenderFieldsToDiff/fields/Relationship/index.css new file mode 100644 index 00000000000..e5b6a4ca79b --- /dev/null +++ b/packages/next/src/views/Version/RenderFieldsToDiff/fields/Relationship/index.css @@ -0,0 +1,189 @@ +@layer payload-default { + .relationship-diff-container .field-diff-content { + padding: 0; + background: unset; + } + + .relationship-diff-container .diff-no-value { + padding: 0; + } + + .relationship-diff-container--hasOne .relationship-diff { + min-width: 100%; + max-width: fit-content; + } + + .relationship-diff-container--hasOne .relationship-diff[data-match-type] { + outline: none; + background-color: transparent; + } + + .relationship-diff-container--hasOne .field-diff-content { + padding: var(--spacer-2) var(--spacer-2-5); + background: var(--color-bg-secondary); + } + + .relationship-diff-container--hasOne .field-diff-content > .html-diff { + position: relative; + z-index: 1; + flex: 1; + } + + .relationship-diff-container--hasOne + .field-diff-content + > .html-diff:has(.relationship-diff[data-match-type='create'])::before { + content: ''; + position: absolute; + top: calc(var(--spacer-2) * -1); + bottom: calc(var(--spacer-2) * -1); + left: calc(var(--spacer-2-5) * -1); + right: calc(var(--spacer-2-5) * -1); + display: block; + background-color: var(--diff-create-parent-bg); + z-index: -1; + } + + .relationship-diff-container--hasOne + .field-diff-content + > .html-diff:has(.relationship-diff[data-match-type='delete'])::before { + content: ''; + position: absolute; + top: calc(var(--spacer-2) * -1); + bottom: calc(var(--spacer-2) * -1); + left: calc(var(--spacer-2-5) * -1); + right: calc(var(--spacer-2-5) * -1); + display: block; + background-color: var(--diff-delete-parent-bg); + z-index: -1; + } + + .relationship-diff-container--hasMany .field-diff-content { + background: var(--color-bg-secondary); + padding: var(--spacer-1) var(--spacer-2); + + .html-diff { + display: flex; + flex-direction: row; + min-width: 0; + max-width: unset; + flex-wrap: wrap; + gap: var(--spacer-1); + } + + .relationship-diff { + padding: var(--spacer-1); + } + } + + .relationship-diff { + display: flex; + align-items: center; + gap: var(--spacer-1); + border-radius: var(--radius-medium); + outline: var(--stroke-width-small) solid var(--color-border); + outline-offset: calc(var(--stroke-width-small) * -1); + background-color: var(--color-bg); + position: relative; + font-family: var(--text-body-medium-font-family); + font-size: var(--text-body-medium-font-size); + font-weight: var(--text-body-medium-font-weight); + line-height: var(--text-body-medium-line-height); + padding: var(--spacer-2) var(--spacer-1); + height: min-content; + + &[data-match-type='create'] { + background-color: var(--diff-create-parent-bg); + outline-color: var(--diff-create-pill-border); + } + + &[data-match-type='delete'] { + background-color: var(--diff-delete-parent-bg); + outline-color: var(--diff-delete-pill-border); + text-decoration-line: none; + } + } + + /* Override HTMLDiff generic [data-match-type] styles (specificity 0,4,1 from :not(img)) */ + .html-diff .relationship-diff.relationship-diff.relationship-diff[data-match-type='create'], + .html-diff .relationship-diff.relationship-diff.relationship-diff[data-match-type='delete'] { + border-radius: var(--radius-medium); + padding: var(--spacer-2) var(--spacer-1); + text-decoration-line: none; + } + + /* hasMany pills stay compact at 24px */ + .relationship-diff-container--hasMany + .html-diff + .relationship-diff.relationship-diff.relationship-diff[data-match-type='create'], + .relationship-diff-container--hasMany + .html-diff + .relationship-diff.relationship-diff.relationship-diff[data-match-type='delete'] { + padding: var(--spacer-1); + } + + /* hasOne pills: no padding, field-diff-content provides row height */ + .relationship-diff-container--hasOne + .html-diff + .relationship-diff.relationship-diff.relationship-diff[data-match-type='create'], + .relationship-diff-container--hasOne + .html-diff + .relationship-diff.relationship-diff.relationship-diff[data-match-type='delete'] { + padding: 0; + } + + .html-diff .relationship-diff.relationship-diff.relationship-diff[data-match-type='create'] { + background-color: var(--diff-create-parent-bg); + outline-color: var(--diff-create-pill-border); + color: inherit; + } + + .html-diff .relationship-diff.relationship-diff.relationship-diff[data-match-type='delete'] { + background-color: var(--diff-delete-parent-bg); + outline-color: var(--diff-delete-pill-border); + color: inherit; + } + + .relationship-diff__pill { + display: inline-flex; + align-items: center; + height: var(--spacer-3); + border-radius: var(--radius-medium); + padding: 0 var(--spacer-1); + background-color: var(--color-bg-tertiary); + color: var(--color-text); + white-space: nowrap; + flex-shrink: 0; + } + + .relationship-diff[data-match-type='create'] .relationship-diff__pill { + background-color: var(--color-bg-brand); + color: var(--color-text-onbrand); + } + + .relationship-diff[data-match-type='delete'] .relationship-diff__pill { + background-color: var(--color-bg-danger); + color: var(--color-text-oncomponent); + + * { + text-decoration-line: none; + color: var(--color-text-oncomponent); + background-color: transparent; + } + } + + .relationship-diff__info { + padding-inline-end: var(--spacer-1); + font-weight: var(--text-body-medium-font-weight); + color: var(--color-text); + white-space: nowrap; + } + + .relationship-diff[data-match-type='create'] .relationship-diff__info { + color: var(--diff-create-parent-color); + } + + .relationship-diff[data-match-type='delete'] .relationship-diff__info { + color: var(--color-text-danger); + text-decoration-line: line-through; + } +} diff --git a/packages/next/src/views/Version/RenderFieldsToDiff/fields/Relationship/index.scss b/packages/next/src/views/Version/RenderFieldsToDiff/fields/Relationship/index.scss deleted file mode 100644 index 932e6199243..00000000000 --- a/packages/next/src/views/Version/RenderFieldsToDiff/fields/Relationship/index.scss +++ /dev/null @@ -1,90 +0,0 @@ -@import '~@payloadcms/ui/scss'; - -@layer payload-default { - .relationship-diff-container .field-diff-content { - padding: 0; - background: unset; - } - - .relationship-diff-container--hasOne { - .relationship-diff { - min-width: 100%; - max-width: fit-content; - } - } - - .relationship-diff-container--hasMany .field-diff-content { - background: var(--theme-elevation-50); - padding: 10px; - - .html-diff { - display: flex; - min-width: 0; - max-width: max-content; - flex-wrap: wrap; - gap: calc(var(--base) * 0.5); - } - - .relationship-diff { - padding: calc(var(--base) * 0.15) calc(var(--base) * 0.3); - } - } - - .relationship-diff { - display: flex; - align-items: center; - border-radius: $style-radius-s; - border: 1px solid var(--theme-elevation-150); - position: relative; - font-family: var(--font-body); - max-height: calc(var(--base) * 3); - padding: calc(var(--base) * 0.35); - - &[data-match-type='create'] { - border-color: var(--diff-create-pill-border); - color: var(--diff-create-parent-color); - - * { - color: var(--diff-create-parent-color); - } - } - - &[data-match-type='delete'] { - border-color: var(--diff-delete-pill-border); - color: var(--diff-delete-parent-color); - background-color: var(--diff-delete-pill-bg); - text-decoration-line: none !important; - - * { - color: var(--diff-delete-parent-color); - text-decoration-line: none; - } - - .relationship-diff__info { - text-decoration-line: line-through; - } - } - - &__info { - font-weight: 500; - } - - &__pill { - border-radius: $style-radius-s; - margin: 0 calc(var(--base) * 0.4) 0 calc(var(--base) * 0.2); - padding: 0 calc(var(--base) * 0.1); - background-color: var(--theme-elevation-150); - color: var(--theme-elevation-750); - } - - &[data-match-type='create'] .relationship-diff__pill { - background-color: var(--diff-create-parent-bg); - color: var(--diff-create-pill-color); - } - - &[data-match-type='delete'] .relationship-diff__pill { - background-color: var(--diff-delete-parent-bg); - color: var(--diff-delete-pill-color); - } - } -} diff --git a/packages/next/src/views/Version/RenderFieldsToDiff/fields/Relationship/index.tsx b/packages/next/src/views/Version/RenderFieldsToDiff/fields/Relationship/index.tsx index 3a1eb90e68c..b5ca2aa2ee4 100644 --- a/packages/next/src/views/Version/RenderFieldsToDiff/fields/Relationship/index.tsx +++ b/packages/next/src/views/Version/RenderFieldsToDiff/fields/Relationship/index.tsx @@ -8,7 +8,7 @@ import type { import { getTranslation, type I18nClient } from '@payloadcms/translations' import { FieldDiffContainer, getHTMLDiffComponents } from '@payloadcms/ui/rsc' -import './index.scss' +import './index.css' import React from 'react' @@ -157,23 +157,34 @@ export const SingleRelationshipDiff: React.FC<{ /> ) : null - const fromHTML = FromComponent ? ReactDOMServer.renderToStaticMarkup(FromComponent) : `

` - const toHTML = ToComponent ? ReactDOMServer.renderToStaticMarkup(ToComponent) : `

` + // TODO: translate 'No value' + const NoValue =
No value
- const diff = getHTMLDiffComponents({ - fromHTML, - toHTML, - tokenizeByCharacter: false, - }) + let From: React.ReactNode = NoValue + let To: React.ReactNode = NoValue + + if (FromComponent || ToComponent) { + const fromHTML = FromComponent ? ReactDOMServer.renderToStaticMarkup(FromComponent) : '

' + const toHTML = ToComponent ? ReactDOMServer.renderToStaticMarkup(ToComponent) : '

' + + const diff = getHTMLDiffComponents({ + fromHTML, + toHTML, + tokenizeByCharacter: false, + }) + + From = FromComponent ? diff.From : NoValue + To = ToComponent ? diff.To : NoValue + } return ( ) } @@ -250,35 +261,46 @@ const ManyRelationshipDiff: React.FC<{ : (field.relationTo as string) } req={req} - showPill={polymorphic} + showPill={true} title={titles[idx]} value={val} /> )) - const fromNodes = - fromArr.length > 0 ? makeNodes(fromArr, titlesFrom) :

+ // TODO: translate 'No value' + const NoValue =
No value
- const toNodes = - toArr.length > 0 ? makeNodes(toArr, titlesTo) :

+ const hasFrom = fromArr.length > 0 + const hasTo = toArr.length > 0 - const fromHTML = ReactDOMServer.renderToStaticMarkup(fromNodes) - const toHTML = ReactDOMServer.renderToStaticMarkup(toNodes) + let From: React.ReactNode = NoValue + let To: React.ReactNode = NoValue - const diff = getHTMLDiffComponents({ - fromHTML, - toHTML, - tokenizeByCharacter: false, - }) + if (hasFrom || hasTo) { + const fromNodes = hasFrom ? makeNodes(fromArr, titlesFrom) : [] + const toNodes = hasTo ? makeNodes(toArr, titlesTo) : [] + + const fromHTML = hasFrom ? ReactDOMServer.renderToStaticMarkup(<>{fromNodes}) : '' + const toHTML = hasTo ? ReactDOMServer.renderToStaticMarkup(<>{toNodes}) : '' + + const diff = getHTMLDiffComponents({ + fromHTML, + toHTML, + tokenizeByCharacter: false, + }) + + From = hasFrom ? diff.From : NoValue + To = hasTo ? diff.To : NoValue + } return ( ) } @@ -320,8 +342,12 @@ const RelationshipDocumentDiff = ({ data-enable-match="true" data-id={ polymorphic - ? (value as { relationTo: string; value: TypeWithID }).value.id - : (value as TypeWithID).id + ? typeof (value as { relationTo: string; value: unknown }).value === 'object' + ? ((value as { relationTo: string; value: TypeWithID }).value?.id ?? '') + : (value as { relationTo: string; value: unknown }).value + : typeof value === 'object' && value !== null + ? (value as TypeWithID).id + : value } data-relation-to={relationTo} > diff --git a/packages/next/src/views/Version/RenderFieldsToDiff/fields/Select/index.scss b/packages/next/src/views/Version/RenderFieldsToDiff/fields/Select/index.css similarity index 100% rename from packages/next/src/views/Version/RenderFieldsToDiff/fields/Select/index.scss rename to packages/next/src/views/Version/RenderFieldsToDiff/fields/Select/index.css diff --git a/packages/next/src/views/Version/RenderFieldsToDiff/fields/Select/index.tsx b/packages/next/src/views/Version/RenderFieldsToDiff/fields/Select/index.tsx index 0c7b3a5c711..fecc262d151 100644 --- a/packages/next/src/views/Version/RenderFieldsToDiff/fields/Select/index.tsx +++ b/packages/next/src/views/Version/RenderFieldsToDiff/fields/Select/index.tsx @@ -12,12 +12,12 @@ import { } from '@payloadcms/ui' import React from 'react' -import './index.scss' +import './index.css' const baseClass = 'select-diff' const getOptionsToRender = ( - value: string, + value: string | string[], options: SelectField['options'], hasMany: boolean, ): Option | Option[] => { @@ -78,11 +78,7 @@ export const Select: SelectFieldDiffClientComponent = ({ const renderedValueFrom = typeof valueFrom !== 'undefined' ? getTranslatedOptions( - getOptionsToRender( - typeof valueFrom === 'string' ? valueFrom : JSON.stringify(valueFrom), - options, - field.hasMany, - ), + getOptionsToRender(valueFrom as string | string[], options, field.hasMany), i18n, ) : '' @@ -90,33 +86,32 @@ export const Select: SelectFieldDiffClientComponent = ({ const renderedValueTo = typeof valueTo !== 'undefined' ? getTranslatedOptions( - getOptionsToRender( - typeof valueTo === 'string' ? valueTo : JSON.stringify(valueTo), - options, - field.hasMany, - ), + getOptionsToRender(valueTo as string | string[], options, field.hasMany), i18n, ) : '' + // TODO: translate 'No value' + const NoValue =
No value
+ const { From, To } = getHTMLDiffComponents({ - fromHTML: '

' + escapeDiffHTML(renderedValueFrom) + '

', + fromHTML: renderedValueFrom ? '

' + escapeDiffHTML(renderedValueFrom) + '

' : '

', postProcess: unescapeDiffHTML, - toHTML: '

' + escapeDiffHTML(renderedValueTo) + '

', + toHTML: renderedValueTo ? '

' + escapeDiffHTML(renderedValueTo) + '

' : '

', tokenizeByCharacter: true, }) return ( ) } diff --git a/packages/next/src/views/Version/RenderFieldsToDiff/fields/Tabs/index.css b/packages/next/src/views/Version/RenderFieldsToDiff/fields/Tabs/index.css new file mode 100644 index 00000000000..7a5790b5c2b --- /dev/null +++ b/packages/next/src/views/Version/RenderFieldsToDiff/fields/Tabs/index.css @@ -0,0 +1,6 @@ +@layer payload-default { + .tabs-diff__tab:not(:first-of-type), + .tabs-diff__tab-locale:not(:first-of-type) { + margin-top: var(--spacer-3); + } +} diff --git a/packages/next/src/views/Version/RenderFieldsToDiff/fields/Tabs/index.scss b/packages/next/src/views/Version/RenderFieldsToDiff/fields/Tabs/index.scss deleted file mode 100644 index 02f067d9c9b..00000000000 --- a/packages/next/src/views/Version/RenderFieldsToDiff/fields/Tabs/index.scss +++ /dev/null @@ -1,9 +0,0 @@ -@layer payload-default { - .tabs-diff { - // Space between each tab or tab locale - &__tab:not(:first-of-type), - &__tab-locale:not(:first-of-type) { - margin-top: var(--base); - } - } -} diff --git a/packages/next/src/views/Version/RenderFieldsToDiff/fields/Tabs/index.tsx b/packages/next/src/views/Version/RenderFieldsToDiff/fields/Tabs/index.tsx index 91c41170ef9..7e9c31f77a2 100644 --- a/packages/next/src/views/Version/RenderFieldsToDiff/fields/Tabs/index.tsx +++ b/packages/next/src/views/Version/RenderFieldsToDiff/fields/Tabs/index.tsx @@ -11,7 +11,7 @@ import { getTranslation } from '@payloadcms/translations' import { useTranslation } from '@payloadcms/ui' import React from 'react' -import './index.scss' +import './index.css' import { useSelectedLocales } from '../../../Default/SelectedLocalesContext.js' import { DiffCollapser } from '../../DiffCollapser/index.js' import { RenderVersionFieldsToDiff } from '../../RenderVersionFieldsToDiff.js' diff --git a/packages/next/src/views/Version/RenderFieldsToDiff/fields/Text/index.scss b/packages/next/src/views/Version/RenderFieldsToDiff/fields/Text/index.css similarity index 100% rename from packages/next/src/views/Version/RenderFieldsToDiff/fields/Text/index.scss rename to packages/next/src/views/Version/RenderFieldsToDiff/fields/Text/index.css diff --git a/packages/next/src/views/Version/RenderFieldsToDiff/fields/Text/index.tsx b/packages/next/src/views/Version/RenderFieldsToDiff/fields/Text/index.tsx index d968c584120..0d2deebd133 100644 --- a/packages/next/src/views/Version/RenderFieldsToDiff/fields/Text/index.tsx +++ b/packages/next/src/views/Version/RenderFieldsToDiff/fields/Text/index.tsx @@ -10,13 +10,13 @@ import { } from '@payloadcms/ui' import React from 'react' -import './index.scss' +import './index.css' const baseClass = 'text-diff' function formatValue(value: unknown): { tokenizeByCharacter: boolean - value: string + value: string | undefined } { if (typeof value === 'string') { return { tokenizeByCharacter: true, value: escapeDiffHTML(value) } @@ -58,10 +58,13 @@ export const Text: TextFieldDiffClientComponent = ({ let placeholder = '' - if (valueTo == valueFrom) { - placeholder = `` + if (valueTo === valueFrom) { + placeholder = `` } + // TODO: translate 'No value' + const NoValue =
No value
+ const formattedValueFrom = formatValue(valueFrom) const formattedValueTo = formatValue(valueTo) @@ -72,16 +75,19 @@ export const Text: TextFieldDiffClientComponent = ({ tokenizeByCharacter = formattedValueTo.tokenizeByCharacter } - const renderedValueFrom = formattedValueFrom.value ?? placeholder - const renderedValueTo: string = formattedValueTo.value ?? placeholder + const fromHTML = formattedValueFrom.value ?? placeholder + const toHTML = formattedValueTo.value ?? placeholder - const { From, To } = getHTMLDiffComponents({ - fromHTML: '

' + renderedValueFrom + '

', + const { From: DiffFrom, To: DiffTo } = getHTMLDiffComponents({ + fromHTML: `

${fromHTML}

`, postProcess: unescapeDiffHTML, - toHTML: '

' + renderedValueTo + '

', + toHTML: `

${toHTML}

`, tokenizeByCharacter, }) + const From = fromHTML ? DiffFrom : NoValue + const To = toHTML ? DiffTo : NoValue + return ( { req, versionValue: valueTo, } = args - const hasMany = 'hasMany' in field && field.hasMany && Array.isArray(valueTo) + const hasMany = + 'hasMany' in field && field.hasMany && (Array.isArray(valueTo) || Array.isArray(valueFrom)) const polymorphic = Array.isArray(field.relationTo) if (hasMany) { @@ -78,7 +81,11 @@ export const HasManyUploadDiff: React.FC<{ let From: React.ReactNode = '' let To: React.ReactNode = '' - const showCollectionSlug = Array.isArray(field.relationTo) + // TODO: translate 'No value' + const NoValue =
No value
+ + const hasFrom = valueFrom && valueFrom.length > 0 + const hasTo = valueTo && valueTo.length > 0 const getUploadDocKey = (uploadDoc: UploadDoc): number | string => { if (typeof uploadDoc === 'object' && 'relationTo' in uploadDoc) { @@ -90,67 +97,104 @@ export const HasManyUploadDiff: React.FC<{ return typeof uploadDoc === 'object' ? uploadDoc.id : uploadDoc } - const FromComponents = valueFrom - ? valueFrom.map((uploadDoc) => ( - - )) - : null - const ToComponents = valueTo - ? valueTo.map((uploadDoc) => ( - - )) - : null - - const diffResult = getHTMLDiffComponents({ - fromHTML: - `
` + - (FromComponents - ? FromComponents.map( - (component) => `
${ReactDOMServer.renderToStaticMarkup(component)}
`, - ).join('') - : '') + - '
', - toHTML: - `
` + - (ToComponents - ? ToComponents.map( - (component) => `
${ReactDOMServer.renderToStaticMarkup(component)}
`, - ).join('') - : '') + - '
', - tokenizeByCharacter: false, - }) - From = diffResult.From - To = diffResult.To + if (hasFrom || hasTo) { + const FromComponents = hasFrom + ? valueFrom.map((uploadDoc) => ( + + )) + : null + const ToComponents = hasTo + ? valueTo.map((uploadDoc) => ( + + )) + : null + + const diffResult = getHTMLDiffComponents({ + fromHTML: + `
` + + (FromComponents + ? FromComponents.map( + (component) => `
${ReactDOMServer.renderToStaticMarkup(component)}
`, + ).join('') + : '') + + '
', + toHTML: + `
` + + (ToComponents + ? ToComponents.map( + (component) => `
${ReactDOMServer.renderToStaticMarkup(component)}
`, + ).join('') + : '') + + '
', + tokenizeByCharacter: false, + }) + + From = hasFrom ? diffResult.From : NoValue + To = hasTo ? diffResult.To : NoValue + } + + const effectiveNesting = nestingLevel || 0 + + // Count individual upload changes + const fromLength = valueFrom?.length || 0 + const toLength = valueTo?.length || 0 + const maxLength = Math.max(fromLength, toLength) + let uploadChangeCount = 0 + + for (let i = 0; i < maxLength; i++) { + const fromKey = i < fromLength ? getUploadDocKey(valueFrom[i]) : undefined + const toKey = i < toLength ? getUploadDocKey(valueTo[i]) : undefined + + if (fromKey !== toKey) { + uploadChangeCount++ + } + } + + const uploadGutterOffset = (effectiveNesting + 1) * 6.5 return ( - +
+ + {locale && {locale}} + {typeof field.label !== 'function' && + field.label !== false && + getTranslation(field.label, i18n)} + + } + locales={undefined} + parentIsLocalized={false} + valueFrom={valueFrom} + valueTo={valueTo} + > +
+ {From} + {To} +
+
+
) } @@ -171,7 +215,8 @@ export const SingleUploadDiff: React.FC<{ let From: React.ReactNode = '' let To: React.ReactNode = '' - const showCollectionSlug = Array.isArray(field.relationTo) + // TODO: translate 'No value' + const NoValue =
No value
const FromComponent = valueFrom ? ( ) : null @@ -189,25 +233,23 @@ export const SingleUploadDiff: React.FC<{ polymorphic={polymorphic} relationTo={field.relationTo} req={req} - showCollectionSlug={showCollectionSlug} uploadDoc={valueTo} /> ) : null - const fromHtml = FromComponent - ? ReactDOMServer.renderToStaticMarkup(FromComponent) - : '

' + '' + '

' - const toHtml = ToComponent - ? ReactDOMServer.renderToStaticMarkup(ToComponent) - : '

' + '' + '

' - - const diffResult = getHTMLDiffComponents({ - fromHTML: fromHtml, - toHTML: toHtml, - tokenizeByCharacter: false, - }) - From = diffResult.From - To = diffResult.To + if (FromComponent || ToComponent) { + const fromHtml = FromComponent ? ReactDOMServer.renderToStaticMarkup(FromComponent) : '

' + const toHtml = ToComponent ? ReactDOMServer.renderToStaticMarkup(ToComponent) : '

' + + const diffResult = getHTMLDiffComponents({ + fromHTML: fromHtml, + toHTML: toHtml, + tokenizeByCharacter: false, + }) + + From = FromComponent ? diffResult.From : NoValue + To = ToComponent ? diffResult.To : NoValue + } return (
@@ -299,7 +348,7 @@ const UploadDocumentDiff = (args: {
)}
- {filename} + {filename}
diff --git a/packages/next/src/views/Version/RenderFieldsToDiff/fields/index.ts b/packages/next/src/views/Version/RenderFieldsToDiff/fields/index.ts index cc00eeab186..7bc250158ea 100644 --- a/packages/next/src/views/Version/RenderFieldsToDiff/fields/index.ts +++ b/packages/next/src/views/Version/RenderFieldsToDiff/fields/index.ts @@ -1,5 +1,6 @@ import type { FieldDiffClientProps, FieldDiffServerProps, FieldTypes } from 'payload' +import { Checkbox } from './Checkbox/index.js' import { Collapsible } from './Collapsible/index.js' import { DateDiffComponent } from './Date/index.js' import { Group } from './Group/index.js' @@ -17,7 +18,7 @@ export const diffComponents: Record< > = { array: Iterable, blocks: Iterable, - checkbox: Text, + checkbox: Checkbox, code: Text, collapsible: Collapsible, date: DateDiffComponent, diff --git a/packages/next/src/views/Version/RenderFieldsToDiff/index.css b/packages/next/src/views/Version/RenderFieldsToDiff/index.css new file mode 100644 index 00000000000..240571af5c5 --- /dev/null +++ b/packages/next/src/views/Version/RenderFieldsToDiff/index.css @@ -0,0 +1,24 @@ +@layer payload-default { + .render-field-diffs { + display: flex; + flex-direction: column; + gap: var(--spacer-3); + + [role='banner'] { + display: none !important; + } + } + + .render-field-diffs__field { + overflow-wrap: anywhere; + display: flex; + flex-direction: column; + gap: var(--spacer-3); + } + + @media (max-width: 768px) { + .render-field-diffs { + gap: var(--spacer-2); + } + } +} diff --git a/packages/next/src/views/Version/RenderFieldsToDiff/index.scss b/packages/next/src/views/Version/RenderFieldsToDiff/index.scss deleted file mode 100644 index 03eed0412be..00000000000 --- a/packages/next/src/views/Version/RenderFieldsToDiff/index.scss +++ /dev/null @@ -1,24 +0,0 @@ -@import '~@payloadcms/ui/scss'; - -@layer payload-default { - .render-field-diffs { - display: flex; - flex-direction: column; - gap: var(--base); - - [role='banner'] { - display: none !important; - } - - &__field { - overflow-wrap: anywhere; - display: flex; - flex-direction: column; - gap: var(--base); - } - - @include small-break { - gap: calc(var(--base) / 2); - } - } -} diff --git a/packages/next/src/views/Version/Restore/index.css b/packages/next/src/views/Version/Restore/index.css new file mode 100644 index 00000000000..67647a2c1bc --- /dev/null +++ b/packages/next/src/views/Version/Restore/index.css @@ -0,0 +1,26 @@ +@layer payload-default { + .restore-version { + cursor: pointer; + display: flex; + min-width: max-content; + } + + .restore-version .btn { + margin-block: 0; + } + + .restore-version .btn__content { + gap: 0; + } + + .restore-version__restore-as-draft-button { + border-top-right-radius: 0; + border-bottom-right-radius: 0; + padding: 0; + } + + .restore-version__restore-as-draft-button:focus { + border-radius: 0; + outline-offset: 0; + } +} diff --git a/packages/next/src/views/Version/Restore/index.scss b/packages/next/src/views/Version/Restore/index.scss deleted file mode 100644 index f2b2ce708fe..00000000000 --- a/packages/next/src/views/Version/Restore/index.scss +++ /dev/null @@ -1,83 +0,0 @@ -@import '~@payloadcms/ui/scss'; - -@layer payload-default { - .restore-version { - cursor: pointer; - display: flex; - min-width: max-content; - - .popup-button { - display: flex; - } - - &__chevron { - background-color: var(--theme-elevation-150); - border-top-left-radius: 0; - border-bottom-left-radius: 0; - cursor: pointer; - - .stroke { - stroke-width: 1px; - } - - &:hover { - background: var(--theme-elevation-100); - } - } - - .btn { - margin-block: 0; - } - - &__restore-as-draft-button { - border-top-right-radius: 0px; - border-bottom-right-radius: 0px; - margin-right: 2px; - - &:focus { - border-radius: 0; - outline-offset: 0; - } - } - - &__modal { - @include blur-bg; - display: flex; - align-items: center; - justify-content: center; - height: 100%; - - &__toggle { - } - } - - &__wrapper { - z-index: 1; - position: relative; - display: flex; - flex-direction: column; - gap: base(0.8); - padding: base(2); - max-width: base(36); - } - - &__content { - display: flex; - flex-direction: column; - gap: base(0.4); - - > * { - margin: 0; - } - } - - &__controls { - display: flex; - gap: base(0.4); - - .btn { - margin: 0; - } - } - } -} diff --git a/packages/next/src/views/Version/Restore/index.tsx b/packages/next/src/views/Version/Restore/index.tsx index 6c36df823eb..ea756bdce51 100644 --- a/packages/next/src/views/Version/Restore/index.tsx +++ b/packages/next/src/views/Version/Restore/index.tsx @@ -17,7 +17,7 @@ import { requests } from '@payloadcms/ui/shared' import { useRouter } from 'next/navigation.js' import { formatAdminURL } from 'payload/shared' -import './index.scss' +import './index.css' import React, { Fragment, useCallback, useState } from 'react' @@ -121,11 +121,12 @@ export const Restore: React.FC = ({
[0]['pillStyle']) => { - return ( - - {label} - - ) +const statusModifierMap: Record = { + currentDraft: 'draft', + currentlyPublished: 'published', + draft: 'draft', + previouslyPublished: 'previously-published', + published: 'published', +} + +const renderStatus = (label: React.ReactNode, name: string) => { + const modifier = statusModifierMap[name] || 'draft' + return {label} } export const VersionPillLabel: React.FC<{ @@ -65,7 +70,7 @@ export const VersionPillLabel: React.FC<{ const { i18n, t } = useTranslation() const { code: currentLocale } = useLocale() - const { label, pillStyle } = getVersionLabel({ + const { name, label } = getVersionLabel({ currentLocale, currentlyPublishedVersion, latestDraftVersion, @@ -99,7 +104,7 @@ export const VersionPillLabel: React.FC<{ {labelFirst ? ( {labelStyle === 'pill' ? ( - renderPill(labelText, pillStyle) + renderStatus(labelText, name) ) : ( {labelText} )} @@ -109,7 +114,7 @@ export const VersionPillLabel: React.FC<{ {showDate && {formattedDate}} {labelStyle === 'pill' ? ( - renderPill(labelText, pillStyle) + renderStatus(labelText, name) ) : ( {labelText} )} diff --git a/packages/next/src/views/Version/VersionPillLabel/index.css b/packages/next/src/views/Version/VersionPillLabel/index.css new file mode 100644 index 00000000000..3fc64316c59 --- /dev/null +++ b/packages/next/src/views/Version/VersionPillLabel/index.css @@ -0,0 +1,31 @@ +@layer payload-default { + .version-pill-label { + display: flex; + align-items: center; + gap: var(--spacer-2); + } + + .version-pill-label-text { + font-weight: var(--text-body-medium-strong-font-weight); + } + + .version-pill-label-date { + overflow: hidden; + color: var(--color-text-secondary); + text-overflow: ellipsis; + font-family: var(--text-body-medium-font-family); + font-size: var(--text-body-medium-font-size); + font-style: normal; + font-weight: var(--text-body-medium-font-weight); + line-height: var(--text-body-medium-line-height); + letter-spacing: var(--text-body-medium-letter-spacing); + } + + @media (max-width: 768px) { + .version-pill-label { + flex-direction: column; + align-items: flex-start; + gap: 0; + } + } +} diff --git a/packages/next/src/views/Version/VersionPillLabel/index.scss b/packages/next/src/views/Version/VersionPillLabel/index.scss deleted file mode 100644 index fb6a15c6b39..00000000000 --- a/packages/next/src/views/Version/VersionPillLabel/index.scss +++ /dev/null @@ -1,26 +0,0 @@ -@import '~@payloadcms/ui/scss'; - -@layer payload-default { - .version-pill-label { - display: flex; - align-items: center; - gap: calc(var(--base) / 2); - - &-text { - font-weight: 500; - } - - &-date { - color: var(--theme-elevation-500); - } - } - - @include small-break { - .version-pill-label { - // Column - flex-direction: column; - align-items: flex-start; - gap: 0; - } - } -} diff --git a/packages/ui/src/css/typography.css b/packages/ui/src/css/typography.css index ea8afd28e5c..63cb4256d00 100644 --- a/packages/ui/src/css/typography.css +++ b/packages/ui/src/css/typography.css @@ -28,7 +28,7 @@ --text-body-medium-strong-font-size: 0.6875rem; /* 11px */ --text-body-medium-strong-font-weight: 550; --text-body-medium-strong-line-height: 1rem; /* 16px */ - --text-body-medium-strong-letter-spacing: 0.5px; + --text-body-medium-strong-letter-spacing: 0.055px; /* Body — Large */ --text-body-large-font-family: var(--font-family-sans); diff --git a/packages/ui/src/elements/Button/index.css b/packages/ui/src/elements/Button/index.css index c4dd75a0336..28e2e0153ea 100644 --- a/packages/ui/src/elements/Button/index.css +++ b/packages/ui/src/elements/Button/index.css @@ -289,6 +289,7 @@ .popup-button { border-radius: var(--radius-medium); align-items: center; + justify-content: center; html:not([dir='RTL']) & { border-left: 1px solid var(--separator-color); @@ -403,14 +404,60 @@ .btn--withPopup.btn--style-ghost { --separator-color: transparent; + border-radius: var(--radius-medium); + padding-left: var(--spacer-2); + padding-right: var(--spacer-1); + .btn, .popup-button { color: var(--color-text); background-color: transparent; + } + + .btn { + padding: 0; &:hover, &:active { - background-color: var(--color-bg-transparent-hover); + background-color: transparent; + } + } + + .popup-button { + align-self: center; + border-radius: var(--radius-medium); + border: none; + height: auto; + margin-left: var(--spacer-1); + + html:not([dir='RTL']) & { + border-top-left-radius: var(--radius-medium); + border-bottom-left-radius: var(--radius-medium); + } + + html[dir='RTL'] & { + border-top-right-radius: var(--radius-medium); + border-bottom-right-radius: var(--radius-medium); + } + } + + &:hover, + &:active { + background-color: var(--color-bg-transparent-hover); + } + + .popup-button:hover { + background-color: var(--color-bg-transparent-hover); + } + + .popup-button:active { + background-color: var(--color-bg-transparent-pressed); + } + + &.btn--disabled { + &:hover, + &:active { + background-color: transparent; } } diff --git a/packages/ui/src/elements/Button/index.tsx b/packages/ui/src/elements/Button/index.tsx index c6421974b5d..dd484713c02 100644 --- a/packages/ui/src/elements/Button/index.tsx +++ b/packages/ui/src/elements/Button/index.tsx @@ -75,6 +75,7 @@ export const Button: React.FC = (props) => { newTab, onClick, onMouseDown, + popupIconSize, ref, round, size = 'medium', @@ -202,7 +203,7 @@ export const Button: React.FC = (props) => {
{buttonElement} } + button={} buttonSize={size} className={disabled && !enableSubMenu ? `${baseClass}--popup-disabled` : ''} disabled={disabled && !enableSubMenu} @@ -210,7 +211,7 @@ export const Button: React.FC = (props) => { id={`${id}-popup`} noBackground render={({ close }) => SubMenuPopupContent({ close: () => close() })} - size="large" + size="small" verticalAlign="bottom" />
diff --git a/packages/ui/src/elements/Button/types.ts b/packages/ui/src/elements/Button/types.ts index 87366e3ca34..7f7af42bcc8 100644 --- a/packages/ui/src/elements/Button/types.ts +++ b/packages/ui/src/elements/Button/types.ts @@ -36,6 +36,11 @@ export type Props = { newTab?: boolean onClick?: (event: MouseEvent) => void onMouseDown?: (event: MouseEvent) => void + /** + * Size of the chevron icon in the split-button popup trigger. + * @default 24 + */ + popupIconSize?: 16 | 24 /** * Enables form submission via an onClick handler. This is only needed if * type="submit" does not trigger form submission, e.g. if the button DOM diff --git a/packages/ui/src/elements/FieldDiffContainer/index.css b/packages/ui/src/elements/FieldDiffContainer/index.css new file mode 100644 index 00000000000..9d92bcac8a6 --- /dev/null +++ b/packages/ui/src/elements/FieldDiffContainer/index.css @@ -0,0 +1,43 @@ +@layer payload-default { + .field-diff__locale-label { + background: var(--color-bg-secondary); + border-radius: var(--radius-medium); + padding: 0 var(--spacer-1); + + [dir='ltr'] & { + margin-right: var(--spacer-1); + } + + [dir='rtl'] & { + margin-left: var(--spacer-1); + } + } + + .field-diff-container { + position: relative; + } + + .field-diff-content { + display: grid; + grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); + gap: calc(var(--spacer-2-5) * 2); + background: var(--color-bg-secondary); + padding: var(--spacer-2) var(--spacer-2-5); + border-radius: var(--radius-medium); + overflow: clip; + } + + .diff-no-value { + display: flex; + align-items: center; + height: 100%; + max-height: var(--spacer-5); + background-color: var(--color-bg-secondary); + border-radius: var(--radius-medium); + color: var(--color-text-secondary); + font-family: var(--text-body-medium-font-family); + font-size: var(--text-body-medium-font-size); + font-weight: var(--text-body-medium-font-weight); + line-height: var(--text-body-medium-line-height); + } +} diff --git a/packages/ui/src/elements/FieldDiffContainer/index.scss b/packages/ui/src/elements/FieldDiffContainer/index.scss deleted file mode 100644 index 60b1265a09e..00000000000 --- a/packages/ui/src/elements/FieldDiffContainer/index.scss +++ /dev/null @@ -1,45 +0,0 @@ -@import '../../scss/styles.scss'; - -@layer payload-default { - .field-diff { - &__locale-label { - background: var(--theme-elevation-100); - border-radius: var(--style-radius-s); - padding: calc(var(--base) * 0.2); - // border-radius: $style-radius-m; - [dir='ltr'] & { - margin-right: calc(var(--base) * 0.25); - } - [dir='rtl'] & { - margin-left: calc(var(--base) * 0.25); - } - } - - &-container { - position: relative; - - // Vertical separator line - not needed anymore, as the parent version view container adds a vertical line that spans the entire height of the container. - /* - &::after { - content: ''; - position: absolute; - top: 0; - bottom: 0; - left: var(--left-offset); - width: 1px; - background-color: var(--theme-elevation-100); - transform: translateX(-50%); // Center the line - }*/ - } - - &-content { - display: grid; - // Need to use 50% 50% so that we can apply overflow-x without the column shrinking to the content width. - // Need -base(0.5) to enure the gap is center aligned - this is required when using 50% over 1fr. - grid-template-columns: calc(50% - base(0.5)) calc(50% - base(0.5)); - gap: base(1); - background: var(--theme-elevation-50); - padding: base(0.5); - } - } -} diff --git a/packages/ui/src/elements/FieldDiffContainer/index.tsx b/packages/ui/src/elements/FieldDiffContainer/index.tsx index 6c9bcc826f0..fe4aa4ffa28 100644 --- a/packages/ui/src/elements/FieldDiffContainer/index.tsx +++ b/packages/ui/src/elements/FieldDiffContainer/index.tsx @@ -1,13 +1,16 @@ import type { LabelFunction, StaticLabel } from 'payload' -import './index.scss' +import './index.css' import { getTranslation, type I18nClient } from '@payloadcms/translations' +import React from 'react' import { FieldDiffLabel } from '../FieldDiffLabel/index.js' const baseClass = 'field-diff' +const gutterOffset = 6.5 + export const FieldDiffContainer: React.FC<{ className?: string From: React.ReactNode @@ -31,27 +34,17 @@ export const FieldDiffContainer: React.FC<{ return (
{locale && {locale}} - {typeof label !== 'function' && getTranslation(label || '', i18n)} + {label !== false && typeof label !== 'function' && getTranslation(label || '', i18n)}
- + ) } diff --git a/test/v4/baseConfig.ts b/test/v4/baseConfig.ts index a5c5d2a777a..44576a3b0bc 100644 --- a/test/v4/baseConfig.ts +++ b/test/v4/baseConfig.ts @@ -6,6 +6,7 @@ import { type CollectionConfig, type Config } from 'payload' import { resetDB } from '../__helpers/shared/clearAndSeed/reset.js' import { devUser } from '../credentials.js' import { blocksSeedData } from './seed/blocksSeedData.js' +import { seedVersionsDiff } from './seed/versionsDiffData.js' import { blocksFieldsSlug, collectionSlugs, @@ -65,6 +66,7 @@ import Rubbish from './collections/Trash/index.js' import Unauthorized from './collections/Unauthorized/index.js' import Uploads from './collections/Upload/index.js' import UploadFields from './collections/UploadField/index.js' +import { VersionsDiff } from './collections/VersionsDiff/index.js' import { codeContent, getRichTextContent, @@ -131,6 +133,7 @@ export const collections: CollectionConfig[] = [ Autosave, Rubbish, Unauthorized, + VersionsDiff, ] export const baseConfig: Partial = { @@ -424,6 +427,9 @@ export const baseConfig: Partial = { }, }, }) + + // Seed versions-diff collection with two versions for diff testing + await seedVersionsDiff(payload) }, typescript: { outputFile: path.resolve(dirname, 'payload-types.ts'), diff --git a/test/v4/collections/VersionsDiff/index.ts b/test/v4/collections/VersionsDiff/index.ts new file mode 100644 index 00000000000..c85140681d7 --- /dev/null +++ b/test/v4/collections/VersionsDiff/index.ts @@ -0,0 +1,191 @@ +import type { CollectionConfig } from 'payload' + +import { tagsSlug, uploadsSlug, versionsDiffSlug } from '../../slugs.js' + +export const VersionsDiff: CollectionConfig = { + slug: versionsDiffSlug, + admin: { + useAsTitle: 'title', + group: 'Versions', + }, + versions: { + drafts: true, + }, + fields: [ + { + name: 'title', + type: 'text', + localized: true, + required: true, + }, + { + name: 'array', + type: 'array', + fields: [ + { + name: 'arrayText', + type: 'text', + }, + { + name: 'nestedArray', + type: 'array', + fields: [ + { + name: 'nestedText', + type: 'text', + }, + ], + }, + ], + }, + { + name: 'blocks', + type: 'blocks', + blocks: [ + { + slug: 'textBlock', + fields: [ + { + name: 'blockText', + type: 'text', + }, + ], + }, + { + slug: 'numberBlock', + fields: [ + { + name: 'blockNumber', + type: 'number', + }, + ], + }, + ], + }, + { + name: 'checkbox', + type: 'checkbox', + }, + { + name: 'code', + type: 'code', + }, + { + name: 'date', + type: 'date', + }, + { + name: 'description', + type: 'textarea', + }, + { + name: 'email', + type: 'email', + }, + { + name: 'group', + type: 'group', + fields: [ + { + name: 'nestedText', + type: 'text', + }, + { + name: 'nestedNumber', + type: 'number', + }, + ], + }, + { + name: 'json', + type: 'json', + }, + { + name: 'number', + type: 'number', + }, + { + name: 'numberMany', + type: 'number', + hasMany: true, + }, + { + name: 'point', + type: 'point', + }, + { + name: 'radio', + type: 'radio', + options: [ + { label: 'Small', value: 'small' }, + { label: 'Medium', value: 'medium' }, + { label: 'Large', value: 'large' }, + ], + }, + { + name: 'relationship', + type: 'relationship', + relationTo: tagsSlug, + }, + { + name: 'relationshipMany', + type: 'relationship', + relationTo: tagsSlug, + hasMany: true, + }, + { + name: 'select', + type: 'select', + options: [ + { label: 'Option 1', value: 'option-1' }, + { label: 'Option 2', value: 'option-2' }, + { label: 'Option 3', value: 'option-3' }, + ], + }, + { + name: 'selectMany', + type: 'select', + hasMany: true, + options: [ + { label: 'Option 1', value: 'option-1' }, + { label: 'Option 2', value: 'option-2' }, + { label: 'Option 3', value: 'option-3' }, + ], + }, + { + name: 'tabs', + type: 'tabs', + tabs: [ + { + label: 'Tab One', + fields: [ + { + name: 'tabText', + type: 'text', + }, + ], + }, + { + label: 'Tab Two', + fields: [ + { + name: 'tabNumber', + type: 'number', + }, + ], + }, + ], + }, + { + name: 'upload', + type: 'upload', + relationTo: uploadsSlug, + }, + { + name: 'uploadMany', + type: 'upload', + relationTo: uploadsSlug, + hasMany: true, + }, + ], +} diff --git a/test/v4/seed/versionsDiffData.ts b/test/v4/seed/versionsDiffData.ts new file mode 100644 index 00000000000..7cf3035f6fc --- /dev/null +++ b/test/v4/seed/versionsDiffData.ts @@ -0,0 +1,79 @@ +import type { Payload } from 'payload' + +import { tagsSlug, uploadsSlug, versionsDiffSlug } from '../slugs.js' + +export async function seedVersionsDiff(payload: Payload) { + const tagDocs = await payload.find({ collection: tagsSlug, limit: 3 }) + const tagIds = tagDocs.docs.map((d) => d.id) + + const uploadDocs = await payload.find({ collection: uploadsSlug, limit: 2 }) + const uploadIds = uploadDocs.docs.map((d) => d.id) + + const versionsDiffDoc = await payload.create({ + collection: versionsDiffSlug, + data: { + title: 'Original Title', + array: [{ arrayText: 'First item' }, { arrayText: 'Second item' }], + blocks: [{ blockType: 'textBlock', blockText: 'Original block text' }], + checkbox: false, + code: 'const x = 1;', + date: '2024-01-15T00:00:00.000Z', + description: 'Original description text', + email: 'original@example.com', + group: { + nestedText: 'Original nested', + nestedNumber: 100, + }, + json: { key: 'original' }, + number: 42, + numberMany: [1, 2, 3], + point: [10, 20], + radio: 'small', + relationship: tagIds[0], + relationshipMany: [tagIds[0], tagIds[1]], + select: 'option-1', + selectMany: ['option-1', 'option-2'], + tabText: 'Original tab text', + tabNumber: 10, + upload: uploadIds[0], + uploadMany: uploadIds.length > 1 ? [uploadIds[0], uploadIds[1]] : [uploadIds[0]], + _status: 'published', + }, + }) + + await payload.update({ + collection: versionsDiffSlug, + id: versionsDiffDoc.id, + data: { + title: 'Updated Title', + array: [{ arrayText: 'Updated first item' }, { arrayText: 'New third item' }], + blocks: [ + { blockType: 'textBlock', blockText: 'Updated block text' }, + { blockType: 'numberBlock', blockNumber: 42 }, + ], + checkbox: true, + code: 'const x = 2;\nconst y = 3;', + date: '2025-06-01T00:00:00.000Z', + description: 'Updated description with more content', + email: 'updated@example.com', + group: { + nestedText: 'Updated nested', + nestedNumber: 200, + }, + json: { key: 'updated', extra: true }, + number: 99, + numberMany: [4, 5], + point: [30, 40], + radio: 'large', + relationship: tagIds[1], + relationshipMany: [tagIds[1], tagIds[2]], + select: 'option-2', + selectMany: ['option-2', 'option-3'], + tabText: 'Updated tab text', + tabNumber: 50, + upload: uploadIds[1] || uploadIds[0], + uploadMany: uploadIds.length > 1 ? [uploadIds[1]] : [uploadIds[0]], + _status: 'published', + }, + }) +} diff --git a/test/v4/slugs.ts b/test/v4/slugs.ts index c72bc956b08..b646372a071 100644 --- a/test/v4/slugs.ts +++ b/test/v4/slugs.ts @@ -32,6 +32,7 @@ export const joinPostsSlug = 'join-posts' export const rubbishSlug = 'rubbish' export const searchBarTestSlug = 'search-bar-test' export const unauthorizedSlug = 'unauthorized-test' +export const versionsDiffSlug = 'versions-diff' export const collectionSlugs = [ 'users', @@ -68,5 +69,6 @@ export const collectionSlugs = [ rubbishSlug, searchBarTestSlug, unauthorizedSlug, + versionsDiffSlug, 'payload-query-presets', ] From 6e4ec6eb0158303c378f999401b0841491d56dfe Mon Sep 17 00:00:00 2001 From: Jessica Rynkar Date: Fri, 22 May 2026 15:09:44 +0100 Subject: [PATCH 02/16] chore: fix tests --- test/localization/e2e.spec.ts | 22 +++++++++++----------- test/versions/e2e.spec.ts | 12 ++++++------ 2 files changed, 17 insertions(+), 17 deletions(-) diff --git a/test/localization/e2e.spec.ts b/test/localization/e2e.spec.ts index 326923e03b5..63f613efdd0 100644 --- a/test/localization/e2e.spec.ts +++ b/test/localization/e2e.spec.ts @@ -1,22 +1,16 @@ import type { BrowserContext, Page } from '@playwright/test' -import type { GeneratedTypes } from '../__helpers/shared/sdk/types.js' import { expect, test } from '@playwright/test' -import { addArrayRow } from '../__helpers/e2e/fields/array/index.js' -import { addBlock } from '../__helpers/e2e/fields/blocks/addBlock.js' -import { navigateToDoc } from '../__helpers/e2e/navigateToDoc.js' -import { openDocControls } from '../__helpers/e2e/openDocControls.js' -import { upsertPreferences } from '../__helpers/e2e/preferences.js' -import { runAxeScan } from '../__helpers/e2e/runAxeScan.js' -import { openDocDrawer } from '../__helpers/e2e/toggleDocDrawer.js' -import { waitForAutoSaveToRunAndComplete } from '../__helpers/e2e/waitForAutoSaveToRunAndComplete.js' import path from 'path' import { formatAdminURL } from 'payload/shared' import { fileURLToPath } from 'url' import type { PayloadTestSDK } from '../__helpers/shared/sdk/index.js' +import type { GeneratedTypes } from '../__helpers/shared/sdk/types.js' import type { Config, LocalizedPost } from './payload-types.js' +import { addArrayRow } from '../__helpers/e2e/fields/array/index.js' +import { addBlock } from '../__helpers/e2e/fields/blocks/addBlock.js' import { changeLocale, closeAllToasts, @@ -29,6 +23,12 @@ import { throttleTest, waitForFormReady, } from '../__helpers/e2e/helpers.js' +import { navigateToDoc } from '../__helpers/e2e/navigateToDoc.js' +import { openDocControls } from '../__helpers/e2e/openDocControls.js' +import { upsertPreferences } from '../__helpers/e2e/preferences.js' +import { runAxeScan } from '../__helpers/e2e/runAxeScan.js' +import { openDocDrawer } from '../__helpers/e2e/toggleDocDrawer.js' +import { waitForAutoSaveToRunAndComplete } from '../__helpers/e2e/waitForAutoSaveToRunAndComplete.js' import { AdminUrlUtil } from '../__helpers/shared/adminUrlUtil.js' import { initPayloadE2ENoConfig } from '../__helpers/shared/initPayloadE2ENoConfig.js' import { RESTClient } from '../__helpers/shared/rest.js' @@ -934,7 +934,7 @@ describe('Localization', () => { await page.goto(urlAllFieldsLocalized.versions(docID)) const firstRow = page.locator('tbody tr').first() - await expect(firstRow.locator('.pill__label span')).toHaveText('Currently Published') + await expect(firstRow.locator('.status-cell span')).toHaveText('Currently Published') }) test('should only show published status when viewing the published locale', async () => { @@ -961,7 +961,7 @@ describe('Localization', () => { await changeLocale(page, defaultLocale) const firstRow = page.locator('tbody tr').first() - await expect(firstRow.locator('.pill__label span')).toHaveText('Current Draft') + await expect(firstRow.locator('.status-cell span')).toHaveText('Current Draft') }) }) }) diff --git a/test/versions/e2e.spec.ts b/test/versions/e2e.spec.ts index 95311c74ff7..60ffef724db 100644 --- a/test/versions/e2e.spec.ts +++ b/test/versions/e2e.spec.ts @@ -26,9 +26,6 @@ import type { BrowserContext, Dialog, Page } from '@playwright/test' import type { TypeWithID } from 'payload' import { expect, test } from '@playwright/test' -import { checkFocusIndicators } from '../__helpers/e2e/checkFocusIndicators.js' -import { runAxeScan } from '../__helpers/e2e/runAxeScan.js' -import { postsCollectionSlug } from '../admin/slugs.js' import mongoose from 'mongoose' import path from 'path' import { formatAdminURL, wait } from 'payload/shared' @@ -38,6 +35,7 @@ import type { PayloadTestSDK } from '../__helpers/shared/sdk/index.js' import type { Config, Diff } from './payload-types.js' import { assertNetworkRequests } from '../__helpers/e2e/assertNetworkRequests.js' +import { checkFocusIndicators } from '../__helpers/e2e/checkFocusIndicators.js' import { changeLocale, ensureCompilationIsDone, @@ -51,10 +49,12 @@ import { } from '../__helpers/e2e/helpers.js' import { navigateToDiffVersionView as _navigateToDiffVersionView } from '../__helpers/e2e/navigateToDiffVersionView.js' import { openDocControls } from '../__helpers/e2e/openDocControls.js' +import { runAxeScan } from '../__helpers/e2e/runAxeScan.js' import { waitForAutoSaveToRunAndComplete } from '../__helpers/e2e/waitForAutoSaveToRunAndComplete.js' import { AdminUrlUtil } from '../__helpers/shared/adminUrlUtil.js' import { reInitializeDB } from '../__helpers/shared/clearAndSeed/reInitializeDB.js' import { initPayloadE2ENoConfig } from '../__helpers/shared/initPayloadE2ENoConfig.js' +import { postsCollectionSlug } from '../admin/slugs.js' import { POLL_TOPASS_TIMEOUT, TEST_TIMEOUT_LONG } from '../playwright.config.js' import { draftWithCustomUnpublishSlug } from './collections/DraftsWithCustomUnpublish.js' import { BASE_PATH } from './shared.js' @@ -2304,8 +2304,8 @@ describe('Versions', () => { const checkbox = page.locator('[data-field-path="checkbox"]') - await expect(checkbox.locator('.html-diff__diff-old')).toHaveText('true') - await expect(checkbox.locator('.html-diff__diff-new')).toHaveText('false') + await expect(checkbox.locator('.checkbox-diff__label--delete')).toHaveText('Checked') + await expect(checkbox.locator('.checkbox-diff__label--create')).toHaveText('Unchecked') }) test('correctly renders diff for code fields', async () => { @@ -2437,7 +2437,7 @@ describe('Versions', () => { const zeroDepthRelationship = page.locator('[data-field-path="zeroDepthRelationship"]') - await expect(zeroDepthRelationship.locator('.html-diff__diff-old')).toBeEmpty() + await expect(zeroDepthRelationship.locator('.diff-no-value')).toHaveText('No value') await expect( zeroDepthRelationship.locator('.html-diff__diff-new .relationship-diff__info'), ).toHaveText('dev@payloadcms.com') From 45d9fced24e6e111de3e047a1cbc0399ae00cc44 Mon Sep 17 00:00:00 2001 From: Jessica Rynkar Date: Fri, 22 May 2026 15:19:51 +0100 Subject: [PATCH 03/16] chore: add translations --- .../DiffCollapser/index.tsx | 2 +- .../fields/Checkbox/index.tsx | 6 +- .../RenderFieldsToDiff/fields/Date/index.tsx | 5 +- .../fields/Relationship/index.tsx | 6 +- .../fields/Select/index.tsx | 6 +- .../RenderFieldsToDiff/fields/Text/index.tsx | 5 +- .../fields/Upload/index.tsx | 6 +- packages/translations/src/clientKeys.ts | 2 + packages/translations/src/languages/ar.ts | 2 + packages/translations/src/languages/az.ts | 2 + packages/translations/src/languages/bg.ts | 2 + packages/translations/src/languages/bnBd.ts | 2 + packages/translations/src/languages/bnIn.ts | 2 + packages/translations/src/languages/ca.ts | 2 + packages/translations/src/languages/cs.ts | 2 + packages/translations/src/languages/da.ts | 2 + packages/translations/src/languages/de.ts | 2 + packages/translations/src/languages/en.ts | 2 + packages/translations/src/languages/es.ts | 2 + packages/translations/src/languages/et.ts | 2 + packages/translations/src/languages/fa.ts | 2 + packages/translations/src/languages/fr.ts | 2 + packages/translations/src/languages/he.ts | 2 + packages/translations/src/languages/hr.ts | 2 + packages/translations/src/languages/hu.ts | 2 + packages/translations/src/languages/hy.ts | 2 + packages/translations/src/languages/id.ts | 2 + packages/translations/src/languages/is.ts | 2 + packages/translations/src/languages/it.ts | 2 + packages/translations/src/languages/ja.ts | 2 + packages/translations/src/languages/ko.ts | 2 + packages/translations/src/languages/lt.ts | 2 + packages/translations/src/languages/lv.ts | 2 + packages/translations/src/languages/my.ts | 2 + packages/translations/src/languages/nb.ts | 2 + packages/translations/src/languages/nl.ts | 2 + packages/translations/src/languages/pl.ts | 2 + packages/translations/src/languages/pt.ts | 2 + packages/translations/src/languages/ro.ts | 2 + packages/translations/src/languages/rs.ts | 2 + .../translations/src/languages/rsLatin.ts | 2 + packages/translations/src/languages/ru.ts | 2 + packages/translations/src/languages/sk.ts | 2 + packages/translations/src/languages/sl.ts | 2 + packages/translations/src/languages/sv.ts | 2 + packages/translations/src/languages/ta.ts | 2 + packages/translations/src/languages/th.ts | 2 + packages/translations/src/languages/tr.ts | 2 + packages/translations/src/languages/uk.ts | 2 + packages/translations/src/languages/vi.ts | 2 + packages/translations/src/languages/zh.ts | 2 + packages/translations/src/languages/zhTw.ts | 2 + test/v4/payload-types.ts | 143 ++++++++++++++++++ 53 files changed, 247 insertions(+), 22 deletions(-) diff --git a/packages/next/src/views/Version/RenderFieldsToDiff/DiffCollapser/index.tsx b/packages/next/src/views/Version/RenderFieldsToDiff/DiffCollapser/index.tsx index 3b28756e74b..757ca5be6f9 100644 --- a/packages/next/src/views/Version/RenderFieldsToDiff/DiffCollapser/index.tsx +++ b/packages/next/src/views/Version/RenderFieldsToDiff/DiffCollapser/index.tsx @@ -101,7 +101,7 @@ export const DiffCollapser: React.FC = ({
) diff --git a/packages/richtext-lexical/src/field/Diff/converters/upload/index.css b/packages/richtext-lexical/src/field/Diff/converters/upload/index.css index 32cd45cbe34..231c8a82e08 100644 --- a/packages/richtext-lexical/src/field/Diff/converters/upload/index.css +++ b/packages/richtext-lexical/src/field/Diff/converters/upload/index.css @@ -1,83 +1,95 @@ @layer payload-default { .lexical-diff { .lexical-upload-diff { - box-shadow: 0 2px 2px -1px rgba(0, 0, 0, 0.1); - min-width: calc(var(--spacer-5) * 6.25); + min-width: 100%; max-width: fit-content; display: flex; align-items: center; background-color: var(--color-bg); border-radius: var(--radius-medium); - border: var(--stroke-width-small) solid var(--color-border); + outline: var(--stroke-width-small) solid var(--color-border); + outline-offset: calc(var(--stroke-width-small) * -1); position: relative; - font-family: var(--font-body); - margin-block: calc(var(--spacer-1) * 2.5); - max-height: calc(var(--spacer-4) * 2.5); - padding: var(--spacer-2-5); + z-index: 2; + font-family: var(--text-body-medium-font-family); + margin-block: var(--spacer-1); + padding: var(--spacer-2); &[data-match-type='create'] { - border-color: var(--diff-create-pill-border); + background-color: var(--diff-create-parent-bg); + outline-color: var(--diff-create-pill-border); color: var(--diff-create-parent-color); - * { + .lexical-upload-diff__info { color: var(--diff-create-parent-color); } .lexical-upload-diff__meta { - color: var(--diff-create-link-color); - * { - color: var(--diff-create-link-color); - } + color: var(--diff-create-parent-color); } .lexical-upload-diff__thumbnail { - border-radius: 0; - border-color: var(--diff-create-pill-border); - background-color: none; + background-color: transparent; } } &[data-match-type='delete'] { - border-color: var(--diff-delete-pill-border); text-decoration-line: none; + background-color: var(--diff-delete-parent-bg); + outline-color: var(--diff-delete-pill-border); color: var(--diff-delete-parent-color); - background-color: var(--diff-delete-pill-bg); - .lexical-upload-diff__meta { - color: var(--diff-delete-link-color); - * { - color: var(--diff-delete-link-color); - } + .lexical-upload-diff__info { + text-decoration-line: line-through; + text-decoration-color: var(--diff-delete-parent-color); + color: var(--diff-delete-parent-color); } - * { - text-decoration-line: none; + .lexical-upload-diff__meta { + text-decoration-line: line-through; color: var(--diff-delete-parent-color); } .lexical-upload-diff__thumbnail { - border-radius: 0; - border-color: var(--diff-delete-pill-border); - background-color: none; + background-color: transparent; } } } + /* Override HTMLDiff generic [data-match-type] styles - specificity must beat 0,4,1 from HTMLDiff */ + .lexical-upload-diff.lexical-upload-diff[data-enable-match='true'][data-match-type='create'], + .lexical-upload-diff.lexical-upload-diff[data-enable-match='true'][data-match-type='delete'] { + border-radius: var(--radius-medium); + padding: var(--spacer-2); + text-decoration-line: none; + } + + .lexical-upload-diff.lexical-upload-diff[data-enable-match='true'][data-match-type='create'] { + background-color: var(--diff-create-parent-bg); + color: var(--diff-create-parent-color); + } + + .lexical-upload-diff.lexical-upload-diff[data-enable-match='true'][data-match-type='delete'] { + background-color: var(--diff-delete-parent-bg); + color: var(--diff-delete-parent-color); + } + .lexical-upload-diff__card { display: flex; flex-direction: row; align-items: center; width: 100%; + gap: var(--spacer-2-5); } .lexical-upload-diff__thumbnail { - width: calc(var(--spacer-4) * 1.5); - height: calc(var(--spacer-4) * 1.5); + width: var(--spacer-6); + height: var(--spacer-6); position: relative; overflow: hidden; flex-shrink: 0; - border-radius: 0; - border: var(--stroke-width-small) solid var(--color-border); + border-radius: var(--radius-medium); + background-color: var(--color-bg-secondary); img, svg { @@ -85,28 +97,34 @@ object-fit: cover; width: 100%; height: 100%; - border-radius: 0; } } .lexical-upload-diff__info { - flex-grow: 1; + flex: 1 0 0; display: flex; - align-items: flex-start; flex-direction: column; - padding: calc(var(--spacer-1) * 1.25) calc(var(--spacer-2-5) * 1.25); - justify-content: space-between; - font-weight: 400; + justify-content: center; + gap: var(--spacer-1); + font-size: var(--text-body-medium-font-size); + line-height: var(--text-body-medium-line-height); + overflow: hidden; + min-width: 1px; strong { font-weight: 600; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; } } .lexical-upload-diff__meta { + font-size: var(--text-body-medium-font-size); + color: var(--color-text-secondary); + white-space: nowrap; overflow: hidden; text-overflow: ellipsis; - white-space: nowrap; } } } diff --git a/packages/richtext-lexical/src/field/Diff/converters/upload/index.tsx b/packages/richtext-lexical/src/field/Diff/converters/upload/index.tsx index 687d6c5a8d6..effde9acd29 100644 --- a/packages/richtext-lexical/src/field/Diff/converters/upload/index.tsx +++ b/packages/richtext-lexical/src/field/Diff/converters/upload/index.tsx @@ -18,7 +18,7 @@ const baseClass = 'lexical-upload-diff' export const UploadDiffHTMLConverterAsync: (args: { i18n: I18nClient req: PayloadRequest -}) => HTMLConvertersAsync = () => { +}) => HTMLConvertersAsync = ({ i18n, req }) => { return { upload: async ({ node, populate, providedCSSString }) => { const uploadNode = node as UploadDataImproved @@ -54,6 +54,10 @@ export const UploadDiffHTMLConverterAsync: (args: { .update(JSON.stringify(node.fields ?? {})) .digest('hex') + const filename = + uploadDoc?.filename || + `${i18n.t('general:untitled')} - ID: ${typeof uploadNode.value === 'object' ? uploadNode.value : uploadNode.value}` + const JSX = (
: }
- {uploadDoc?.filename} + {filename}
{formatFilesize(uploadDoc?.filesize)} {typeof uploadDoc?.width === 'number' && typeof uploadDoc?.height === 'number' && ( diff --git a/packages/ui/src/elements/FieldDiffContainer/index.css b/packages/ui/src/elements/FieldDiffContainer/index.css index 9d92bcac8a6..b755360b5af 100644 --- a/packages/ui/src/elements/FieldDiffContainer/index.css +++ b/packages/ui/src/elements/FieldDiffContainer/index.css @@ -27,6 +27,23 @@ overflow: clip; } + @media (max-width: 768px) { + .field-diff-content { + grid-template-columns: 1fr !important; + gap: 2px; + padding: 0; + background: transparent; + isolation: isolate; + } + + .field-diff-content > * { + background: var(--color-bg-secondary); + padding: var(--spacer-1) var(--spacer-2); + min-height: var(--spacer-5); + overflow: hidden; + } + } + .diff-no-value { display: flex; align-items: center; diff --git a/packages/ui/src/elements/HTMLDiff/index.css b/packages/ui/src/elements/HTMLDiff/index.css index 048f588e7c0..a3bd442344c 100644 --- a/packages/ui/src/elements/HTMLDiff/index.css +++ b/packages/ui/src/elements/HTMLDiff/index.css @@ -178,4 +178,40 @@ content: ''; } } + + @media (max-width: 768px) { + .html-diff p:not([data-enable-match='false']):has([data-match-type='create']), + .html-diff h1:not([data-enable-match='false']):has([data-match-type='create']), + .html-diff h2:not([data-enable-match='false']):has([data-match-type='create']), + .html-diff h3:not([data-enable-match='false']):has([data-match-type='create']), + .html-diff h4:not([data-enable-match='false']):has([data-match-type='create']), + .html-diff h5:not([data-enable-match='false']):has([data-match-type='create']), + .html-diff h6:not([data-enable-match='false']):has([data-match-type='create']), + .html-diff blockquote:not([data-enable-match='false']):has([data-match-type='create']), + .html-diff pre:not([data-enable-match='false']):has([data-match-type='create']), + .html-diff p:not([data-enable-match='false']):has([data-match-type='delete']), + .html-diff h1:not([data-enable-match='false']):has([data-match-type='delete']), + .html-diff h2:not([data-enable-match='false']):has([data-match-type='delete']), + .html-diff h3:not([data-enable-match='false']):has([data-match-type='delete']), + .html-diff h4:not([data-enable-match='false']):has([data-match-type='delete']), + .html-diff h5:not([data-enable-match='false']):has([data-match-type='delete']), + .html-diff h6:not([data-enable-match='false']):has([data-match-type='delete']), + .html-diff blockquote:not([data-enable-match='false']):has([data-match-type='delete']), + .html-diff pre:not([data-enable-match='false']):has([data-match-type='delete']) { + &::before { + top: calc(var(--spacer-1) * -1); + bottom: calc(var(--spacer-1) * -1); + left: calc(var(--spacer-2) * -1); + right: calc(var(--spacer-2) * -1); + } + } + + .html-diff li:not([data-enable-match='false']):has([data-match-type='create']), + .html-diff li:not([data-enable-match='false']):has([data-match-type='delete']) { + &::before { + left: calc(var(--spacer-2) * -1); + right: calc(var(--spacer-2) * -1); + } + } + } } diff --git a/test/v4/collections/RichText/index.ts b/test/v4/collections/RichText/index.ts index 5ccc7820640..007651b4093 100644 --- a/test/v4/collections/RichText/index.ts +++ b/test/v4/collections/RichText/index.ts @@ -13,6 +13,9 @@ import { richTextFieldsSlug } from '../../slugs.js' const RichTextFields: CollectionConfig = { slug: richTextFieldsSlug, + versions: { + drafts: true, + }, fields: [ { name: 'content', diff --git a/test/v4/payload-types.ts b/test/v4/payload-types.ts index 4344c180325..cc791dd4ba4 100644 --- a/test/v4/payload-types.ts +++ b/test/v4/payload-types.ts @@ -976,6 +976,7 @@ export interface RichTextField { } | null; updatedAt: string; createdAt: string; + _status?: ('draft' | 'published') | null; } /** * This interface was referenced by `Config`'s JSON-Schema @@ -2054,6 +2055,7 @@ export interface RichTextFieldsSelect { lists?: T; updatedAt?: T; createdAt?: T; + _status?: T; } /** * This interface was referenced by `Config`'s JSON-Schema From a4d739e46fe26b44b4f8f868e0ebe1e221dc0ec8 Mon Sep 17 00:00:00 2001 From: Jessica Rynkar Date: Tue, 26 May 2026 12:41:59 +0100 Subject: [PATCH 05/16] chore: merge with main --- test/v4/baseConfig.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/test/v4/baseConfig.ts b/test/v4/baseConfig.ts index 5d89f07b3e4..c72ec0e550f 100644 --- a/test/v4/baseConfig.ts +++ b/test/v4/baseConfig.ts @@ -429,10 +429,9 @@ export const baseConfig: Partial = { }, }) -<<<<<<< HEAD // Seed versions-diff collection with two versions for diff testing await seedVersionsDiff(payload) -======= + // Seed draft-versions collection with many versions for pagination testing const { id: draftVersionsDocID } = await payload.create({ collection: draftVersionsSlug, @@ -453,7 +452,6 @@ export const baseConfig: Partial = { }, }) } ->>>>>>> main }, typescript: { outputFile: path.resolve(dirname, 'payload-types.ts'), From f6084f43ba79db65bd3b8cafb2a503be5c23ee0e Mon Sep 17 00:00:00 2001 From: Jessica Rynkar Date: Tue, 26 May 2026 14:15:01 +0100 Subject: [PATCH 06/16] chore: update borders to stroke width var --- .../Version/RenderFieldsToDiff/fields/Upload/index.css | 2 +- .../src/field/Diff/converters/listitem/index.css | 10 +++++----- .../src/field/Diff/converters/relationship/index.css | 8 ++++---- .../src/field/Diff/converters/upload/index.css | 2 +- packages/ui/src/elements/FieldDiffContainer/index.css | 2 +- packages/ui/src/elements/HTMLDiff/index.css | 4 ++-- 6 files changed, 14 insertions(+), 14 deletions(-) diff --git a/packages/next/src/views/Version/RenderFieldsToDiff/fields/Upload/index.css b/packages/next/src/views/Version/RenderFieldsToDiff/fields/Upload/index.css index 1a4225eac83..6c48a9cd501 100644 --- a/packages/next/src/views/Version/RenderFieldsToDiff/fields/Upload/index.css +++ b/packages/next/src/views/Version/RenderFieldsToDiff/fields/Upload/index.css @@ -108,7 +108,7 @@ overflow: hidden; white-space: nowrap; text-overflow: ellipsis; - min-width: 1px; + min-width: var(--stroke-width-small); } .upload-diff__pill { diff --git a/packages/richtext-lexical/src/field/Diff/converters/listitem/index.css b/packages/richtext-lexical/src/field/Diff/converters/listitem/index.css index 6b0c45171ef..898c773c711 100644 --- a/packages/richtext-lexical/src/field/Diff/converters/listitem/index.css +++ b/packages/richtext-lexical/src/field/Diff/converters/listitem/index.css @@ -25,9 +25,9 @@ pointer-events: none; &[data-match-type='create'] { - background-color: var(--ramp-blue-200); - border-color: var(--ramp-blue-600); - color: var(--ramp-blue-600); + background-color: var(--color-bg-brand-tertiary); + border-color: var(--color-border-brand-strong); + color: var(--color-text-brand); } &[data-match-type='delete'] { @@ -43,8 +43,8 @@ } .checkboxItem__icon.checkboxItem__icon[data-enable-match='true'][data-match-type='create'] { - background-color: var(--ramp-blue-200); - color: var(--ramp-blue-600); + background-color: var(--color-bg-brand-tertiary); + color: var(--color-text-brand); } .checkboxItem__icon.checkboxItem__icon[data-enable-match='true'][data-match-type='delete'] { diff --git a/packages/richtext-lexical/src/field/Diff/converters/relationship/index.css b/packages/richtext-lexical/src/field/Diff/converters/relationship/index.css index 91a7c764a23..fc2a7612111 100644 --- a/packages/richtext-lexical/src/field/Diff/converters/relationship/index.css +++ b/packages/richtext-lexical/src/field/Diff/converters/relationship/index.css @@ -3,9 +3,9 @@ max-width: fit-content; display: inline-flex; align-items: center; - background-color: var(--ramp-blue-200); - outline: 1px solid var(--ramp-blue-300); - outline-offset: -1px; + background-color: var(--color-bg-brand-tertiary); + outline: var(--stroke-width-small) solid var(--color-border-brand); + outline-offset: calc(var(--stroke-width-small) * -1); border-radius: var(--radius-medium); position: relative; z-index: 2; @@ -90,7 +90,7 @@ .lexical-diff .lexical-relationship-diff__info { padding-inline: var(--spacer-1); font-weight: var(--text-body-medium-font-weight); - color: var(--ramp-blue-600); + color: var(--color-text-brand); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; diff --git a/packages/richtext-lexical/src/field/Diff/converters/upload/index.css b/packages/richtext-lexical/src/field/Diff/converters/upload/index.css index 231c8a82e08..9557ee67422 100644 --- a/packages/richtext-lexical/src/field/Diff/converters/upload/index.css +++ b/packages/richtext-lexical/src/field/Diff/converters/upload/index.css @@ -109,7 +109,7 @@ font-size: var(--text-body-medium-font-size); line-height: var(--text-body-medium-line-height); overflow: hidden; - min-width: 1px; + min-width: var(--stroke-width-small); strong { font-weight: 600; diff --git a/packages/ui/src/elements/FieldDiffContainer/index.css b/packages/ui/src/elements/FieldDiffContainer/index.css index b755360b5af..6c97c0265a6 100644 --- a/packages/ui/src/elements/FieldDiffContainer/index.css +++ b/packages/ui/src/elements/FieldDiffContainer/index.css @@ -30,7 +30,7 @@ @media (max-width: 768px) { .field-diff-content { grid-template-columns: 1fr !important; - gap: 2px; + gap: calc(var(--stroke-width-small) * 2); padding: 0; background: transparent; isolation: isolate; diff --git a/packages/ui/src/elements/HTMLDiff/index.css b/packages/ui/src/elements/HTMLDiff/index.css index a3bd442344c..182195c6e51 100644 --- a/packages/ui/src/elements/HTMLDiff/index.css +++ b/packages/ui/src/elements/HTMLDiff/index.css @@ -122,7 +122,7 @@ text-decoration-line: line-through; background-color: var(--diff-delete-pill-bg); border-radius: var(--radius-medium); - padding: 1.5px 2px; + padding: calc(var(--stroke-width-small) * 1.5) calc(var(--stroke-width-small) * 2); text-decoration-thickness: var(--stroke-width-small); } @@ -147,7 +147,7 @@ background-color: var(--diff-create-pill-bg); color: var(--diff-create-pill-color); border-radius: var(--radius-medium); - padding: 1.5px 2px; + padding: calc(var(--stroke-width-small) * 1.5) calc(var(--stroke-width-small) * 2); } .html-diff-create-inline-wrapper, From 5bcacd9fc188aa150ecb261f18079390af1b8d1a Mon Sep 17 00:00:00 2001 From: Jessica Rynkar Date: Tue, 26 May 2026 15:45:43 +0100 Subject: [PATCH 07/16] chore: mobile version diff tweaks --- .../next/src/views/Version/Default/index.css | 2 +- .../RenderFieldsToDiff/fields/Date/index.css | 10 +++++++ .../fields/Relationship/index.css | 30 +++++++++++++++++++ .../Version/RenderFieldsToDiff/index.css | 2 +- .../Diff/converters/relationship/index.css | 10 +++++++ .../src/elements/FieldDiffContainer/index.css | 17 +++++++++++ .../ui/src/elements/FieldDiffLabel/index.css | 6 ++++ packages/ui/src/elements/HTMLDiff/index.css | 4 +++ 8 files changed, 79 insertions(+), 2 deletions(-) diff --git a/packages/next/src/views/Version/Default/index.css b/packages/next/src/views/Version/Default/index.css index cb57cab39e3..196e0f7f162 100644 --- a/packages/next/src/views/Version/Default/index.css +++ b/packages/next/src/views/Version/Default/index.css @@ -195,7 +195,7 @@ .view-version__diff-wrap { order: 3; - padding: var(--spacer-2) var(--spacer-3); + padding: var(--spacer-3); /* Hide vertical separator on mobile */ &::after { diff --git a/packages/next/src/views/Version/RenderFieldsToDiff/fields/Date/index.css b/packages/next/src/views/Version/RenderFieldsToDiff/fields/Date/index.css index 5489aec557f..925f518937e 100644 --- a/packages/next/src/views/Version/RenderFieldsToDiff/fields/Date/index.css +++ b/packages/next/src/views/Version/RenderFieldsToDiff/fields/Date/index.css @@ -16,4 +16,14 @@ background-color: unset !important; } } + + @media (max-width: 768px) { + .field-diff-content .date-diff { + display: contents; + } + + .date-diff .field-diff-content > .html-diff { + justify-content: center; + } + } } diff --git a/packages/next/src/views/Version/RenderFieldsToDiff/fields/Relationship/index.css b/packages/next/src/views/Version/RenderFieldsToDiff/fields/Relationship/index.css index e5b6a4ca79b..f14f2e7a98c 100644 --- a/packages/next/src/views/Version/RenderFieldsToDiff/fields/Relationship/index.css +++ b/packages/next/src/views/Version/RenderFieldsToDiff/fields/Relationship/index.css @@ -186,4 +186,34 @@ color: var(--color-text-danger); text-decoration-line: line-through; } + + @media (max-width: 768px) { + .relationship-diff-container--hasOne .field-diff-content { + padding: 0; + background: transparent; + } + + .relationship-diff-container--hasOne .field-diff-content > .html-diff { + justify-content: center; + } + + .relationship-diff-container--hasOne + .field-diff-content + > .html-diff:has(.relationship-diff[data-match-type='create'])::before, + .relationship-diff-container--hasOne + .field-diff-content + > .html-diff:has(.relationship-diff[data-match-type='delete'])::before { + top: calc(var(--spacer-1) * -1); + bottom: calc(var(--spacer-1) * -1); + left: calc(var(--spacer-2) * -1); + right: calc(var(--spacer-2) * -1); + } + + .relationship-diff-container--hasMany .field-diff-content { + padding: 0; + background: transparent; + border-radius: 0; + gap: calc(var(--stroke-width-small) * 2); + } + } } diff --git a/packages/next/src/views/Version/RenderFieldsToDiff/index.css b/packages/next/src/views/Version/RenderFieldsToDiff/index.css index 240571af5c5..7f59794b45e 100644 --- a/packages/next/src/views/Version/RenderFieldsToDiff/index.css +++ b/packages/next/src/views/Version/RenderFieldsToDiff/index.css @@ -18,7 +18,7 @@ @media (max-width: 768px) { .render-field-diffs { - gap: var(--spacer-2); + gap: calc(var(--spacer-4) - var(--spacer-1)); } } } diff --git a/packages/richtext-lexical/src/field/Diff/converters/relationship/index.css b/packages/richtext-lexical/src/field/Diff/converters/relationship/index.css index fc2a7612111..a3ff24f74de 100644 --- a/packages/richtext-lexical/src/field/Diff/converters/relationship/index.css +++ b/packages/richtext-lexical/src/field/Diff/converters/relationship/index.css @@ -95,4 +95,14 @@ overflow: hidden; text-overflow: ellipsis; } + + @media (max-width: 768px) { + .lexical-diff .html-diff .lexical-relationship-diff { + max-width: unset; + display: flex; + border-radius: 0; + outline: none; + margin: 0; + } + } } diff --git a/packages/ui/src/elements/FieldDiffContainer/index.css b/packages/ui/src/elements/FieldDiffContainer/index.css index 6c97c0265a6..dc6da1216d3 100644 --- a/packages/ui/src/elements/FieldDiffContainer/index.css +++ b/packages/ui/src/elements/FieldDiffContainer/index.css @@ -41,6 +41,23 @@ padding: var(--spacer-1) var(--spacer-2); min-height: var(--spacer-5); overflow: hidden; + align-content: center; + } + + .field-diff-content > *:first-child { + border-radius: var(--radius-medium) var(--radius-medium) 0 0; + } + + .field-diff-content > *:last-child { + border-radius: 0 0 var(--radius-medium) var(--radius-medium); + } + + .field-diff-content > .html-diff__diff-old:has([data-match-type]) { + background: var(--diff-delete-parent-bg); + } + + .field-diff-content > .html-diff__diff-new:has([data-match-type]) { + background: var(--diff-create-parent-bg); } } diff --git a/packages/ui/src/elements/FieldDiffLabel/index.css b/packages/ui/src/elements/FieldDiffLabel/index.css index 5380d0f8341..8270ffd53bd 100644 --- a/packages/ui/src/elements/FieldDiffLabel/index.css +++ b/packages/ui/src/elements/FieldDiffLabel/index.css @@ -12,4 +12,10 @@ align-items: center; color: var(--color-text); } + + @media (max-width: 768px) { + .field-diff-label { + height: min-content; + } + } } diff --git a/packages/ui/src/elements/HTMLDiff/index.css b/packages/ui/src/elements/HTMLDiff/index.css index 182195c6e51..27d0a2e058b 100644 --- a/packages/ui/src/elements/HTMLDiff/index.css +++ b/packages/ui/src/elements/HTMLDiff/index.css @@ -180,6 +180,10 @@ } @media (max-width: 768px) { + .html-diff p { + align-content: center; + } + .html-diff p:not([data-enable-match='false']):has([data-match-type='create']), .html-diff h1:not([data-enable-match='false']):has([data-match-type='create']), .html-diff h2:not([data-enable-match='false']):has([data-match-type='create']), From 642eb95703871bcb94188bd0030f94146a2ba4af Mon Sep 17 00:00:00 2001 From: Jessica Rynkar Date: Wed, 27 May 2026 12:52:10 +0100 Subject: [PATCH 08/16] chore: diff view tweaks and seed lexical version --- .../fields/Relationship/index.css | 20 +- .../fields/Upload/index.css | 62 +- .../fields/Upload/index.tsx | 8 +- .../field/Diff/converters/upload/index.css | 14 +- .../src/elements/FieldDiffContainer/index.css | 14 +- .../src/elements/FieldDiffContainer/index.tsx | 6 +- packages/ui/src/elements/HTMLDiff/index.css | 9 +- test/v4/baseConfig.ts | 18 +- test/v4/seed/richTextData.ts | 738 ++++++++++++++++++ test/v4/seed/versionsDiffData.ts | 34 +- 10 files changed, 856 insertions(+), 67 deletions(-) diff --git a/packages/next/src/views/Version/RenderFieldsToDiff/fields/Relationship/index.css b/packages/next/src/views/Version/RenderFieldsToDiff/fields/Relationship/index.css index f14f2e7a98c..5d9a5ef9b15 100644 --- a/packages/next/src/views/Version/RenderFieldsToDiff/fields/Relationship/index.css +++ b/packages/next/src/views/Version/RenderFieldsToDiff/fields/Relationship/index.css @@ -31,7 +31,10 @@ .relationship-diff-container--hasOne .field-diff-content - > .html-diff:has(.relationship-diff[data-match-type='create'])::before { + > .html-diff:is( + :has(.relationship-diff[data-match-type='create']), + :has(.relationship-diff[data-match-type='delete']) + )::before { content: ''; position: absolute; top: calc(var(--spacer-2) * -1); @@ -39,22 +42,19 @@ left: calc(var(--spacer-2-5) * -1); right: calc(var(--spacer-2-5) * -1); display: block; - background-color: var(--diff-create-parent-bg); z-index: -1; } + .relationship-diff-container--hasOne + .field-diff-content + > .html-diff:has(.relationship-diff[data-match-type='create'])::before { + background-color: var(--diff-create-parent-bg); + } + .relationship-diff-container--hasOne .field-diff-content > .html-diff:has(.relationship-diff[data-match-type='delete'])::before { - content: ''; - position: absolute; - top: calc(var(--spacer-2) * -1); - bottom: calc(var(--spacer-2) * -1); - left: calc(var(--spacer-2-5) * -1); - right: calc(var(--spacer-2-5) * -1); - display: block; background-color: var(--diff-delete-parent-bg); - z-index: -1; } .relationship-diff-container--hasMany .field-diff-content { diff --git a/packages/next/src/views/Version/RenderFieldsToDiff/fields/Upload/index.css b/packages/next/src/views/Version/RenderFieldsToDiff/fields/Upload/index.css index 6c48a9cd501..77daf47f37d 100644 --- a/packages/next/src/views/Version/RenderFieldsToDiff/fields/Upload/index.css +++ b/packages/next/src/views/Version/RenderFieldsToDiff/fields/Upload/index.css @@ -8,6 +8,38 @@ padding: var(--spacer-2) var(--spacer-2-5); } + @media (max-width: 768px) { + /* Stack hasMany columns vertically */ + .upload-diff-container--hasMany .field-diff-content { + grid-template-columns: 1fr; + gap: var(--spacer-2); + } + + /* + * Suppress the generic mobile column-level styling for uploads. + * Individual .upload-diff elements handle their own create/delete backgrounds. + */ + .upload-diff-container .field-diff-content > .html-diff__diff-old, + .upload-diff-container .field-diff-content > .html-diff__diff-new { + background: transparent; + padding: 0; + border-radius: 0; + min-height: unset; + overflow: visible; + } + + /* On mobile, remove .upload-diff padding so it sits flush inside the stacked column */ + .upload-diff-container--hasOne .upload-diff, + .upload-diff-container--hasOne + .html-diff + .upload-diff.upload-diff.upload-diff[data-match-type='create'], + .upload-diff-container--hasOne + .html-diff + .upload-diff.upload-diff.upload-diff[data-match-type='delete'] { + padding: 0; + } + } + .upload-diff-hasMany { display: flex; flex-direction: column; @@ -23,7 +55,13 @@ border-radius: var(--radius-medium); position: relative; font-family: var(--text-body-medium-font-family); - padding: var(--spacer-1) !important; + padding: var(--spacer-1); + + &[data-match-type] { + .upload-diff__thumbnail { + background-color: transparent; + } + } &[data-match-type='create'] { background-color: var(--diff-create-parent-bg); @@ -32,10 +70,6 @@ .upload-diff__info { color: var(--diff-create-parent-color); } - - .upload-diff__thumbnail { - background-color: transparent; - } } &[data-match-type='delete'] { @@ -48,27 +82,23 @@ text-decoration-color: var(--diff-delete-parent-color); color: var(--diff-delete-parent-color); } - - .upload-diff__thumbnail { - background-color: transparent; - } } } /* Override HTMLDiff generic [data-match-type] styles which have higher specificity */ - .html-diff .upload-diff.upload-diff[data-match-type='create'], - .html-diff .upload-diff.upload-diff[data-match-type='delete'] { + .html-diff .upload-diff.upload-diff.upload-diff[data-match-type='create'], + .html-diff .upload-diff.upload-diff.upload-diff[data-match-type='delete'] { border-radius: var(--radius-medium); padding: var(--spacer-1); text-decoration-line: none; } - .html-diff .upload-diff.upload-diff[data-match-type='create'] { + .html-diff .upload-diff.upload-diff.upload-diff[data-match-type='create'] { background-color: var(--diff-create-parent-bg); color: var(--diff-create-parent-color); } - .html-diff .upload-diff.upload-diff[data-match-type='delete'] { + .html-diff .upload-diff.upload-diff.upload-diff[data-match-type='delete'] { background-color: var(--diff-delete-parent-bg); color: var(--diff-delete-parent-color); } @@ -122,16 +152,18 @@ flex-shrink: 0; } + .upload-diff[data-match-type] .upload-diff__pill { + text-decoration-line: none; + } + .upload-diff[data-match-type='create'] .upload-diff__pill { background-color: var(--color-bg-brand); color: var(--color-text-onbrand); - text-decoration-line: none; } .upload-diff[data-match-type='delete'] .upload-diff__pill { background-color: var(--color-bg-danger); color: var(--color-text-oncomponent); - text-decoration-line: none; * { text-decoration-line: none; diff --git a/packages/next/src/views/Version/RenderFieldsToDiff/fields/Upload/index.tsx b/packages/next/src/views/Version/RenderFieldsToDiff/fields/Upload/index.tsx index e3678acdde0..a847e0589f4 100644 --- a/packages/next/src/views/Version/RenderFieldsToDiff/fields/Upload/index.tsx +++ b/packages/next/src/views/Version/RenderFieldsToDiff/fields/Upload/index.tsx @@ -185,9 +185,11 @@ export const HasManyUploadDiff: React.FC<{ >
{From} {To} diff --git a/packages/richtext-lexical/src/field/Diff/converters/upload/index.css b/packages/richtext-lexical/src/field/Diff/converters/upload/index.css index 9557ee67422..aeb26316241 100644 --- a/packages/richtext-lexical/src/field/Diff/converters/upload/index.css +++ b/packages/richtext-lexical/src/field/Diff/converters/upload/index.css @@ -15,6 +15,12 @@ margin-block: var(--spacer-1); padding: var(--spacer-2); + &[data-match-type] { + .lexical-upload-diff__thumbnail { + background-color: transparent; + } + } + &[data-match-type='create'] { background-color: var(--diff-create-parent-bg); outline-color: var(--diff-create-pill-border); @@ -27,10 +33,6 @@ .lexical-upload-diff__meta { color: var(--diff-create-parent-color); } - - .lexical-upload-diff__thumbnail { - background-color: transparent; - } } &[data-match-type='delete'] { @@ -49,10 +51,6 @@ text-decoration-line: line-through; color: var(--diff-delete-parent-color); } - - .lexical-upload-diff__thumbnail { - background-color: transparent; - } } } diff --git a/packages/ui/src/elements/FieldDiffContainer/index.css b/packages/ui/src/elements/FieldDiffContainer/index.css index dc6da1216d3..d02f4cef308 100644 --- a/packages/ui/src/elements/FieldDiffContainer/index.css +++ b/packages/ui/src/elements/FieldDiffContainer/index.css @@ -19,17 +19,19 @@ .field-diff-content { display: grid; - grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); - gap: calc(var(--spacer-2-5) * 2); + grid-template-columns: var(--field-diff-columns, minmax(0, 1fr) minmax(0, 1fr)); background: var(--color-bg-secondary); - padding: var(--spacer-2) var(--spacer-2-5); + padding-block: var(--spacer-2); border-radius: var(--radius-medium); - overflow: clip; + } + + .field-diff-content > * { + padding-inline: var(--spacer-2-5); } @media (max-width: 768px) { - .field-diff-content { - grid-template-columns: 1fr !important; + .field-diff-container .field-diff-content { + grid-template-columns: 1fr; gap: calc(var(--stroke-width-small) * 2); padding: 0; background: transparent; diff --git a/packages/ui/src/elements/FieldDiffContainer/index.tsx b/packages/ui/src/elements/FieldDiffContainer/index.tsx index fe4aa4ffa28..ea420c6bf72 100644 --- a/packages/ui/src/elements/FieldDiffContainer/index.tsx +++ b/packages/ui/src/elements/FieldDiffContainer/index.tsx @@ -43,9 +43,9 @@ export const FieldDiffContainer: React.FC<{ className={`${baseClass}-content`} style={ nestingLevel - ? { - gridTemplateColumns: `calc(50% - ${nestingLevel * gutterOffset}px - var(--spacer-2-5)) calc(50% + ${nestingLevel * gutterOffset}px - var(--spacer-2-5))`, - } + ? ({ + '--field-diff-columns': `calc(50% - ${nestingLevel * gutterOffset}px - var(--spacer-2-5)) calc(50% + ${nestingLevel * gutterOffset}px - var(--spacer-2-5))`, + } as React.CSSProperties) : undefined } > diff --git a/packages/ui/src/elements/HTMLDiff/index.css b/packages/ui/src/elements/HTMLDiff/index.css index 27d0a2e058b..79956086f89 100644 --- a/packages/ui/src/elements/HTMLDiff/index.css +++ b/packages/ui/src/elements/HTMLDiff/index.css @@ -2,6 +2,7 @@ .html-diff { display: flex; flex-direction: column; + overflow: clip; font-family: var(--text-body-medium-font-family); font-size: var(--text-body-medium-font-size); font-weight: var(--text-body-medium-font-weight); @@ -82,8 +83,8 @@ position: absolute; top: 0; bottom: 0; - left: calc(var(--spacer-2-5) * -1); - right: calc(var(--spacer-2-5) * -1); + left: -9999px; + right: -9999px; display: block; background-color: var(--diff-create-parent-bg); color: var(--diff-create-parent-color); @@ -100,8 +101,8 @@ position: absolute; top: 0; bottom: 0; - left: calc(var(--spacer-2-5) * -1); - right: calc(var(--spacer-2-5) * -1); + left: -9999px; + right: -9999px; display: block; background-color: var(--diff-delete-parent-bg); color: var(--diff-delete-parent-color); diff --git a/test/v4/baseConfig.ts b/test/v4/baseConfig.ts index c72ec0e550f..b91a735d01e 100644 --- a/test/v4/baseConfig.ts +++ b/test/v4/baseConfig.ts @@ -74,8 +74,12 @@ import { codeContent, getRichTextContent, getTypographyContent, + getUpdatedRichTextContent, + getUpdatedTypographyContent, listsContent, tableContent, + updatedCodeContent, + updatedListsContent, } from './seed/richTextData.js' export const collections: CollectionConfig[] = [ @@ -253,7 +257,7 @@ export const baseConfig: Partial = { const richTextContent = getRichTextContent(formattedUploadID, formattedUserID) - await payload.create({ + const richTextDoc = await payload.create({ collection: richTextFieldsSlug, data: { title: 'Data harvest \u2013 how AI and sensors are revolutionizing farming', @@ -264,6 +268,18 @@ export const baseConfig: Partial = { code: codeContent, }, }) + + // Create a second version with updated content for diff testing + await payload.update({ + id: richTextDoc.id, + collection: richTextFieldsSlug, + data: { + content: getUpdatedRichTextContent(formattedUploadID, formattedUserID), + lists: updatedListsContent, + typography: getUpdatedTypographyContent(formattedUserID), + code: updatedCodeContent, + }, + }) } // Seed relationship-fields to test join field diff --git a/test/v4/seed/richTextData.ts b/test/v4/seed/richTextData.ts index 63a4be891ea..4ee002caffc 100644 --- a/test/v4/seed/richTextData.ts +++ b/test/v4/seed/richTextData.ts @@ -1,5 +1,280 @@ import { uploadsSlug } from '../slugs.js' +export function getUpdatedRichTextContent( + formattedUploadID: number | string, + formattedUserID?: number | string, +) { + return JSON.parse( + JSON.stringify({ + root: { + type: 'root', + format: '', + indent: 0, + version: 1, + children: [ + { + type: 'heading', + tag: 'h1', + version: 1, + direction: 'ltr', + format: '', + indent: 0, + children: [ + { + type: 'text', + version: 1, + text: 'Smart farming \u2013 the future of precision agriculture', + detail: 0, + format: 0, + mode: 'normal', + style: '', + }, + ], + }, + { + type: 'paragraph', + version: 1, + direction: 'ltr', + format: '', + indent: 0, + textFormat: 0, + children: [ + { + type: 'text', + version: 1, + text: 'Modern agriculture is undergoing a transformation driven by data and automation. Farms of the future will rely on real-time analytics, autonomous equipment, and sustainable practices to feed a growing population.', + detail: 0, + format: 0, + mode: 'normal', + style: '', + }, + ], + }, + { + type: 'upload', + version: 3, + format: '', + relationTo: uploadsSlug, + value: '{{UPLOAD_ID}}', + }, + { + type: 'horizontalrule', + version: 1, + }, + { + type: 'heading', + tag: 'h3', + version: 1, + direction: 'ltr', + format: '', + indent: 0, + children: [ + { + type: 'text', + version: 1, + text: 'Key technologies', + detail: 0, + format: 0, + mode: 'normal', + style: '', + }, + ], + }, + { + type: 'list', + listType: 'check', + start: 1, + tag: 'ul', + version: 1, + direction: null, + format: '', + indent: 0, + children: [ + { + type: 'listitem', + version: 1, + direction: 'ltr', + format: '', + indent: 0, + value: 1, + checked: true, + children: [ + { + type: 'text', + version: 1, + text: 'Soil moisture sensors deployed', + detail: 0, + format: 0, + mode: 'normal', + style: '', + }, + ], + }, + { + type: 'listitem', + version: 1, + direction: 'ltr', + format: '', + indent: 0, + value: 2, + checked: true, + children: [ + { + type: 'text', + version: 1, + text: 'Drone mapping integrated', + detail: 0, + format: 0, + mode: 'normal', + style: '', + }, + ], + }, + { + type: 'listitem', + version: 1, + direction: 'ltr', + format: '', + indent: 0, + value: 3, + checked: false, + children: [ + { + type: 'text', + version: 1, + text: 'Autonomous tractor navigation', + detail: 0, + format: 0, + mode: 'normal', + style: '', + }, + ], + }, + { + type: 'listitem', + version: 1, + direction: 'ltr', + format: '', + indent: 0, + value: 4, + checked: false, + children: [ + { + type: 'text', + version: 1, + text: 'AI-powered crop disease detection', + detail: 0, + format: 0, + mode: 'normal', + style: '', + }, + ], + }, + ], + }, + { + type: 'block', + version: 2, + format: '', + fields: { + id: 'seed-banner-1', + blockType: 'banner', + style: 'info', + content: { + root: { + type: 'root', + format: '', + indent: 0, + version: 1, + children: [ + { + type: 'paragraph', + version: 1, + direction: 'ltr', + format: '', + indent: 0, + textFormat: 0, + children: [ + { + type: 'text', + version: 1, + text: 'Did you know? Precision farming can reduce water usage by up to 30% while maintaining crop yields.', + detail: 0, + format: 0, + mode: 'normal', + style: '', + }, + ], + }, + ], + }, + }, + }, + }, + { + type: 'heading', + tag: 'h6', + version: 1, + direction: 'ltr', + format: '', + indent: 0, + children: [ + { + type: 'text', + version: 1, + text: 'SUSTAINABILITY GOALS', + detail: 0, + format: 0, + mode: 'normal', + style: '', + }, + ], + }, + { + type: 'paragraph', + version: 1, + direction: 'ltr', + format: '', + indent: 0, + textFormat: 0, + children: [ + { + type: 'text', + version: 1, + text: 'Eco-friendly and organic farming not only benefit the environment, but they also support the local economy. By using sustainable practices, farmers can reduce their dependence on expensive synthetic inputs, resulting in lower production costs and higher profits.', + detail: 0, + format: 0, + mode: 'normal', + style: '', + }, + ], + }, + { + type: 'quote', + version: 1, + direction: 'ltr', + format: '', + indent: 0, + children: [ + { + type: 'text', + version: 1, + text: 'The greatest threat to our planet is the belief that someone else will save it.', + detail: 0, + format: 2, + mode: 'normal', + style: '', + }, + ], + }, + ], + }, + }) + .replace(/"\{\{UPLOAD_ID\}\}"/g, `${formattedUploadID}`) + .replace(/"\{\{USER_ID\}\}"/g, formattedUserID !== undefined ? `${formattedUserID}` : '""'), + ) +} + export function getRichTextContent( formattedUploadID: number | string, formattedUserID?: number | string, @@ -822,3 +1097,466 @@ export const codeContent = { ], }, } + +// --- Updated V2 variants for version diff testing --- + +export function getUpdatedTypographyContent(formattedUserID?: number | string) { + return JSON.parse( + JSON.stringify({ + root: { + type: 'root', + format: '', + indent: 0, + version: 1, + children: [ + { + type: 'heading', + tag: 'h1', + version: 1, + direction: 'ltr', + format: '', + indent: 0, + children: [ + { + type: 'text', + version: 1, + text: 'Heading 1 (updated)', + detail: 0, + format: 0, + mode: 'normal', + style: '', + }, + ], + }, + { + type: 'heading', + tag: 'h2', + version: 1, + direction: 'ltr', + format: '', + indent: 0, + children: [ + { + type: 'text', + version: 1, + text: 'Heading 2', + detail: 0, + format: 0, + mode: 'normal', + style: '', + }, + ], + }, + { + type: 'heading', + tag: 'h3', + version: 1, + direction: 'ltr', + format: '', + indent: 0, + children: [ + { + type: 'text', + version: 1, + text: 'New Heading 3', + detail: 0, + format: 0, + mode: 'normal', + style: '', + }, + ], + }, + { + type: 'heading', + tag: 'h4', + version: 1, + direction: 'ltr', + format: '', + indent: 0, + children: [ + { + type: 'text', + version: 1, + text: 'Heading 4', + detail: 0, + format: 0, + mode: 'normal', + style: '', + }, + ], + }, + // h5 removed + { + type: 'heading', + tag: 'h6', + version: 1, + direction: 'ltr', + format: '', + indent: 0, + children: [ + { + type: 'text', + version: 1, + text: 'Heading 6', + detail: 0, + format: 0, + mode: 'normal', + style: '', + }, + ], + }, + { + type: 'horizontalrule', + version: 1, + }, + { + type: 'paragraph', + version: 1, + direction: 'ltr', + format: '', + indent: 0, + textFormat: 0, + children: [ + { + type: 'text', + version: 1, + text: 'Updated paragraph replacing the original precision agriculture text with new content about vertical farming and controlled environment agriculture.', + detail: 0, + format: 0, + mode: 'normal', + style: '', + }, + ], + }, + { + type: 'paragraph', + version: 1, + direction: 'ltr', + format: '', + indent: 0, + textFormat: 0, + children: [ + { + type: 'text', + version: 1, + text: 'Bold and italic', + detail: 0, + format: 3, + mode: 'normal', + style: '', + }, + ], + }, + { + type: 'paragraph', + version: 1, + direction: 'ltr', + format: '', + indent: 0, + textFormat: 0, + children: [ + { + type: 'text', + version: 1, + text: 'Super', + detail: 0, + format: 64, + mode: 'normal', + style: '', + }, + { + type: 'text', + version: 1, + text: 'script, ', + detail: 0, + format: 0, + mode: 'normal', + style: '', + }, + { + type: 'text', + version: 1, + text: 'Sub', + detail: 0, + format: 32, + mode: 'normal', + style: '', + }, + { + type: 'text', + version: 1, + text: 'script', + detail: 0, + format: 0, + mode: 'normal', + style: '', + }, + ], + }, + { + type: 'paragraph', + version: 1, + direction: 'ltr', + format: '', + indent: 0, + textFormat: 0, + children: [ + { + type: 'text', + version: 1, + text: 'inline code updated', + detail: 0, + format: 16, + mode: 'normal', + style: '', + }, + ], + }, + { + type: 'paragraph', + version: 1, + direction: 'ltr', + format: '', + indent: 0, + textFormat: 0, + children: [ + { + type: 'link', + version: 3, + direction: 'ltr', + format: '', + indent: 0, + fields: { + linkType: 'custom', + newTab: false, + url: 'https://payloadcms.com', + }, + children: [ + { + type: 'text', + version: 1, + text: 'Updated external link', + detail: 0, + format: 0, + mode: 'normal', + style: '', + }, + ], + }, + ], + }, + ...(formattedUserID !== undefined + ? [ + { + type: 'paragraph', + version: 1, + direction: 'ltr', + format: '', + indent: 0, + textFormat: 0, + children: [ + { + type: 'link', + version: 3, + direction: 'ltr', + format: '', + indent: 0, + fields: { + linkType: 'internal', + newTab: false, + doc: { + value: '{{USER_ID}}', + relationTo: 'users', + }, + }, + children: [ + { + type: 'text', + version: 1, + text: 'Internal link', + detail: 0, + format: 0, + mode: 'normal', + style: '', + }, + ], + }, + ], + }, + ] + : []), + { + type: 'quote', + version: 1, + direction: 'ltr', + format: '', + indent: 0, + children: [ + { + type: 'text', + version: 1, + text: 'A new blockquote added in the typography update.', + detail: 0, + format: 2, + mode: 'normal', + style: '', + }, + ], + }, + ], + }, + }).replace(/"\{\{USER_ID\}\}"/g, formattedUserID !== undefined ? `${formattedUserID}` : '""'), + ) +} + +export const updatedListsContent = { + root: { + type: 'root', + format: '', + indent: 0, + version: 1, + direction: null, + children: [ + // Ordered list — changed items, removed deepest nesting + nestedList('number', 'ol', [ + listItem(0, 1, [textNode('Lorem (updated)')]), + listItem(0, 2, [ + nestedList('number', 'ol', [ + listItem(1, 1, [textNode('Ipsum')]), + listItem(1, 2, [ + nestedList('number', 'ol', [listItem(2, 1, [textNode('Dolor sit amet')])]), + ]), + ]), + ]), + listItem(0, 3, [textNode('New third item')]), + ]), + // Unordered list — reordered and changed + nestedList('bullet', 'ul', [ + listItem(0, 1, [textNode('Ipsum')]), + listItem(0, 2, [textNode('Lorem')]), + listItem(0, 3, [ + nestedList('bullet', 'ul', [ + listItem(1, 1, [textNode('Dolor')]), + listItem(1, 2, [textNode('New nested item')]), + ]), + ]), + ]), + // Checklist — toggled states and added items + nestedList('check', 'ul', [ + checkListItem(0, 1, true, [textNode('Lorem')]), + checkListItem(0, 2, false, [textNode('Ipsum')]), + checkListItem(0, 3, true, [textNode('New checked item')]), + checkListItem(0, 4, false, [ + nestedList('check', 'ul', [ + checkListItem(1, 1, true, [textNode('Dolor')]), + checkListItem(1, 2, false, [textNode('Sit')]), + ]), + ]), + ]), + ], + }, +} + +export const updatedCodeContent = { + root: { + type: 'root', + format: '', + indent: 0, + version: 1, + direction: null, + children: [ + { + type: 'heading', + tag: 'h2', + version: 1, + direction: 'ltr', + format: '', + indent: 0, + children: [ + { + type: 'text', + version: 1, + text: 'Smart farming \u2013 configuration and setup', + detail: 0, + format: 0, + mode: 'normal', + style: '', + }, + ], + }, + { + type: 'paragraph', + version: 1, + direction: 'ltr', + format: '', + indent: 0, + textFormat: 0, + children: [ + { + type: 'text', + version: 1, + text: 'Modern Payload configurations leverage database adapters, plugins, and rich text editors for flexible content management.', + detail: 0, + format: 0, + mode: 'normal', + style: '', + }, + ], + }, + { + type: 'block', + version: 2, + format: '', + fields: { + id: '6a7b8c9d0e1f2a3b4c5d6e7f', + blockName: '', + blockType: 'Code', + code: "import { postgresAdapter } from '@payloadcms/db-postgres'\nimport sharp from 'sharp'\nimport path from 'path'\nimport { buildConfig } from 'payload'\nimport { fileURLToPath } from 'url'\n\nimport { Categories } from './collections/Categories'\nimport { Media } from './collections/Media'\nimport { Pages } from './collections/Pages'\nimport { Posts } from './collections/Posts'\nimport { Users } from './collections/Users'\nimport { Footer } from './Footer/config'\nimport { Header } from './Header/config'\nimport { plugins } from './plugins'\nimport { defaultLexical } from '@/fields/defaultLexical'\nimport { getServerSideURL } from './utilities/getURL'\n\nconst filename = fileURLToPath(import.meta.url)\nconst dirname = path.dirname(filename)\n\nexport default buildConfig({\n admin: {\n components: {\n beforeLogin: ['@/components/BeforeLogin'],\n },\n importMap: {\n baseDir: path.resolve(dirname),\n },\n user: Users.slug,\n },\n editor: defaultLexical,\n db: postgresAdapter({\n pool: { connectionString: process.env.DATABASE_URI },\n }),\n collections: [Pages, Posts, Media, Categories, Users],\n cors: [getServerSideURL()].filter(Boolean),\n globals: [Header, Footer],\n plugins: [\n ...plugins,\n ],\n secret: process.env.PAYLOAD_SECRET,\n sharp,\n typescript: {\n outputFile: path.resolve(dirname, 'payload-types.ts'),\n },\n})", + language: 'typescript', + }, + }, + { + type: 'horizontalrule', + version: 1, + }, + { + type: 'heading', + tag: 'h6', + version: 1, + direction: 'ltr', + format: '', + indent: 0, + children: [ + { + type: 'text', + version: 1, + text: 'DEPLOYMENT NOTES', + detail: 0, + format: 0, + mode: 'normal', + style: '', + }, + ], + }, + { + type: 'paragraph', + version: 1, + direction: 'ltr', + format: '', + indent: 0, + textFormat: 0, + children: [ + { + type: 'text', + version: 1, + text: 'When deploying to production, ensure environment variables are set and the database connection string is configured correctly. Use connection pooling for optimal performance.', + detail: 0, + format: 0, + mode: 'normal', + style: '', + }, + ], + }, + ], + }, +} diff --git a/test/v4/seed/versionsDiffData.ts b/test/v4/seed/versionsDiffData.ts index 7cf3035f6fc..3c1e5471070 100644 --- a/test/v4/seed/versionsDiffData.ts +++ b/test/v4/seed/versionsDiffData.ts @@ -12,17 +12,17 @@ export async function seedVersionsDiff(payload: Payload) { const versionsDiffDoc = await payload.create({ collection: versionsDiffSlug, data: { - title: 'Original Title', + _status: 'published', array: [{ arrayText: 'First item' }, { arrayText: 'Second item' }], - blocks: [{ blockType: 'textBlock', blockText: 'Original block text' }], + blocks: [{ blockText: 'Original block text', blockType: 'textBlock' }], checkbox: false, code: 'const x = 1;', date: '2024-01-15T00:00:00.000Z', description: 'Original description text', email: 'original@example.com', group: { - nestedText: 'Original nested', nestedNumber: 100, + nestedText: 'Original nested', }, json: { key: 'original' }, number: 42, @@ -30,26 +30,26 @@ export async function seedVersionsDiff(payload: Payload) { point: [10, 20], radio: 'small', relationship: tagIds[0], - relationshipMany: [tagIds[0], tagIds[1]], + relationshipMany: [tagIds[0]!, tagIds[1]!], select: 'option-1', selectMany: ['option-1', 'option-2'], - tabText: 'Original tab text', tabNumber: 10, + tabText: 'Original tab text', + title: 'Original Title', upload: uploadIds[0], - uploadMany: uploadIds.length > 1 ? [uploadIds[0], uploadIds[1]] : [uploadIds[0]], - _status: 'published', + uploadMany: uploadIds.length > 1 ? [uploadIds[0]!, uploadIds[1]!] : [uploadIds[0]!], }, }) await payload.update({ - collection: versionsDiffSlug, id: versionsDiffDoc.id, + collection: versionsDiffSlug, data: { - title: 'Updated Title', + _status: 'published', array: [{ arrayText: 'Updated first item' }, { arrayText: 'New third item' }], blocks: [ - { blockType: 'textBlock', blockText: 'Updated block text' }, - { blockType: 'numberBlock', blockNumber: 42 }, + { blockText: 'Updated block text', blockType: 'textBlock' }, + { blockNumber: 42, blockType: 'numberBlock' }, ], checkbox: true, code: 'const x = 2;\nconst y = 3;', @@ -57,23 +57,23 @@ export async function seedVersionsDiff(payload: Payload) { description: 'Updated description with more content', email: 'updated@example.com', group: { - nestedText: 'Updated nested', nestedNumber: 200, + nestedText: 'Updated nested', }, - json: { key: 'updated', extra: true }, + json: { extra: true, key: 'updated' }, number: 99, numberMany: [4, 5], point: [30, 40], radio: 'large', relationship: tagIds[1], - relationshipMany: [tagIds[1], tagIds[2]], + relationshipMany: [tagIds[1]!, tagIds[2]!], select: 'option-2', selectMany: ['option-2', 'option-3'], - tabText: 'Updated tab text', tabNumber: 50, + tabText: 'Updated tab text', + title: 'Updated Title', upload: uploadIds[1] || uploadIds[0], - uploadMany: uploadIds.length > 1 ? [uploadIds[1]] : [uploadIds[0]], - _status: 'published', + uploadMany: uploadIds.length > 1 ? [uploadIds[1]!] : [uploadIds[0]!], }, }) } From d238a49797b99d3cde294e238114d6303ec628df Mon Sep 17 00:00:00 2001 From: Jessica Rynkar Date: Wed, 27 May 2026 12:55:01 +0100 Subject: [PATCH 09/16] chore: update lexical diff typography --- .../richtext-lexical/src/field/Diff/index.css | 128 ++++++++++-------- 1 file changed, 71 insertions(+), 57 deletions(-) diff --git a/packages/richtext-lexical/src/field/Diff/index.css b/packages/richtext-lexical/src/field/Diff/index.css index a5f2430a970..aac670eb23d 100644 --- a/packages/richtext-lexical/src/field/Diff/index.css +++ b/packages/richtext-lexical/src/field/Diff/index.css @@ -1,101 +1,115 @@ @layer payload-default { .lexical-diff .field-diff-content { .html-diff { - font-family: var(--font-serif); + font-family: var(--text-body-large-font-family); font-size: var(--text-body-large-font-size); + font-weight: var(--text-body-large-font-weight); line-height: var(--text-body-large-line-height); - letter-spacing: 0.02em; + letter-spacing: -0.0325px; } blockquote { font-size: var(--text-body-large-font-size); - line-height: var(--text-body-large-line-height); + line-height: 1.5; + letter-spacing: normal; margin-block: var(--spacer-3); - margin-inline: 0; + margin-inline: var(--spacer-1); padding-inline-start: var(--spacer-2-5); padding-block: var(--spacer-1); position: relative; + border-inline-start: var(--spacer-1) solid var(--color-border); - &::after { - content: ''; - position: absolute; - top: 0; - bottom: 0; - inset-inline-start: 0; - width: var(--spacer-1); - background-color: var(--color-bg-secondary); - } - - &:has([data-match-type='create'])::after { - background-color: var(--color-bg-success-tertiary); + &:has([data-match-type='create']) { + border-inline-start-color: var(--color-bg-success-tertiary); } - &:has([data-match-type='delete'])::after { - background-color: var(--color-bg-danger-tertiary); + &:has([data-match-type='delete']) { + border-inline-start-color: var(--color-bg-danger-tertiary); } } a { - border-bottom: var(--stroke-width-small) dotted; - text-decoration: none; + color: var(--color-text-brand); + text-decoration: underline; + } + + h1, + h2, + h3, + h4, + h5, + h6 { + font-weight: 550; + line-height: 1.2; + margin-block: var(--spacer-3); } h1 { - padding: calc(var(--spacer-2) * 1.75) 0px calc(var(--spacer-2) * 1.375); - line-height: var(--text-heading-large-line-height); - font-weight: 600; - font-size: var(--text-heading-large-font-size); - font-family: var(--font-body); + font-size: 2rem; + letter-spacing: -0.544px; } h2 { - padding: calc(var(--spacer-2) * 1.75) 0px calc(var(--spacer-1) * 2.5); - line-height: var(--text-heading-medium-line-height); - font-weight: 600; - font-size: var(--text-heading-medium-font-size); - font-family: var(--font-body); + font-size: 1.5rem; + line-height: 2rem; + letter-spacing: var(--text-heading-large-letter-spacing); } h3 { - padding: var(--spacer-2-5) 0px calc(var(--spacer-1) * 2.25); - line-height: var(--text-heading-small-line-height); - font-weight: 600; - font-size: var(--text-heading-small-font-size); - font-family: var(--font-body); + font-size: 1.25rem; + letter-spacing: -0.34px; } h4 { - padding: var(--spacer-2) 0px calc(var(--spacer-1) * 1.75); - line-height: var(--text-body-large-line-height); - font-weight: 600; - font-size: var(--text-body-large-font-size); - font-family: var(--font-body); + font-size: 1rem; + letter-spacing: -0.272px; } h5 { - padding: calc(var(--spacer-1) * 1.5) 0px calc(var(--spacer-1) * 1.5); - line-height: var(--text-body-medium-line-height); - font-weight: 600; - font-size: var(--text-body-medium-font-size); - font-family: var(--font-body); + font-size: 0.8125rem; + letter-spacing: -0.221px; } - h6 { - padding: calc(var(--spacer-2) * 1.375) 0px calc(var(--spacer-1) * 1.25); - line-height: var(--text-body-small-line-height); - font-weight: 600; - font-size: var(--text-body-small-font-size); - font-family: var(--font-body); + font-size: 0.6875rem; + letter-spacing: -0.187px; + } + + > .html-diff > :first-child { + margin-top: 0; + } + + > .html-diff > :last-child { + margin-bottom: 0; } p { - padding: var(--spacer-2) 0 var(--spacer-2); + margin-block: var(--spacer-3); + } - &:first-child { - padding: 0 0 var(--spacer-2); - } + code { + border: var(--stroke-width-small) solid var(--color-border); + color: var(--color-text); + padding: var(--spacer-1); + font-family: 'SF Mono', Menlo, Consolas, Monaco, monospace; + font-size: var(--text-body-medium-font-size); + font-weight: var(--text-body-medium-font-weight); + border-radius: var(--radius-small); + box-decoration-break: clone; + -webkit-box-decoration-break: clone; + } + + hr { + margin: var(--spacer-3) 0; + border: none; + border-top: var(--stroke-width-small) solid var(--color-border); } ul, ol { - padding-top: var(--spacer-2); - padding-bottom: var(--spacer-2); + padding: 0; + margin-block: var(--spacer-3); + } + + li { + font-family: var(--text-body-large-font-family); + font-size: var(--text-body-large-font-size); + margin: 0 0 var(--spacer-3) var(--spacer-6); } } } From 56292daa605340776f7e39f667a671e5b0dc9061 Mon Sep 17 00:00:00 2001 From: Jessica Rynkar Date: Wed, 27 May 2026 13:19:18 +0100 Subject: [PATCH 10/16] chore: lexical version diff tweaks --- .../richtext-lexical/src/field/Diff/index.css | 11 +-- packages/ui/src/elements/HTMLDiff/index.css | 67 +++++-------------- 2 files changed, 22 insertions(+), 56 deletions(-) diff --git a/packages/richtext-lexical/src/field/Diff/index.css b/packages/richtext-lexical/src/field/Diff/index.css index aac670eb23d..8f51456ea17 100644 --- a/packages/richtext-lexical/src/field/Diff/index.css +++ b/packages/richtext-lexical/src/field/Diff/index.css @@ -95,21 +95,24 @@ } hr { - margin: var(--spacer-3) 0; + margin: var(--spacer-2-5) 0; border: none; border-top: var(--stroke-width-small) solid var(--color-border); } - ul, ol { padding: 0; - margin-block: var(--spacer-3); + margin-block: var(--spacer-2); + } + + ul { + padding-left: 0; } li { font-family: var(--text-body-large-font-family); font-size: var(--text-body-large-font-size); - margin: 0 0 var(--spacer-3) var(--spacer-6); + margin: 0 0 var(--spacer-1) var(--spacer-4); } } } diff --git a/packages/ui/src/elements/HTMLDiff/index.css b/packages/ui/src/elements/HTMLDiff/index.css index 79956086f89..3f53d435cd9 100644 --- a/packages/ui/src/elements/HTMLDiff/index.css +++ b/packages/ui/src/elements/HTMLDiff/index.css @@ -19,7 +19,8 @@ margin-bottom: 0; } - /* Apply background color to parents that have children with diffs */ + /* Apply background color to parents that have children with diffs. + ::before extends full-width via -100vw, clipped by overflow:clip on .html-diff */ .html-diff p:not([data-enable-match='false']):has([data-match-type='create']), .html-diff h1:not([data-enable-match='false']):has([data-match-type='create']), .html-diff h2:not([data-enable-match='false']):has([data-match-type='create']), @@ -28,21 +29,20 @@ .html-diff h5:not([data-enable-match='false']):has([data-match-type='create']), .html-diff h6:not([data-enable-match='false']):has([data-match-type='create']), .html-diff blockquote:not([data-enable-match='false']):has([data-match-type='create']), - .html-diff pre:not([data-enable-match='false']):has([data-match-type='create']) { + .html-diff pre:not([data-enable-match='false']):has([data-match-type='create']), + .html-diff li:not([data-enable-match='false']):has([data-match-type='create']) { position: relative; z-index: 1; - flex: 1; &::before { content: ''; position: absolute; - top: calc(var(--spacer-2) * -1); - bottom: calc(var(--spacer-2) * -1); - left: calc(var(--spacer-2-5) * -1); - right: calc(var(--spacer-2-5) * -1); - display: block; + top: 50%; + left: -100vw; + right: -100vw; + min-height: max(100%, var(--spacer-5)); + transform: translateY(-50%); background-color: var(--diff-create-parent-bg); - color: var(--diff-create-parent-color); z-index: -1; } } @@ -55,43 +55,7 @@ .html-diff h5:not([data-enable-match='false']):has([data-match-type='delete']), .html-diff h6:not([data-enable-match='false']):has([data-match-type='delete']), .html-diff blockquote:not([data-enable-match='false']):has([data-match-type='delete']), - .html-diff pre:not([data-enable-match='false']):has([data-match-type='delete']) { - position: relative; - z-index: 1; - flex: 1; - - &::before { - content: ''; - position: absolute; - top: calc(var(--spacer-2) * -1); - bottom: calc(var(--spacer-2) * -1); - left: calc(var(--spacer-2-5) * -1); - right: calc(var(--spacer-2-5) * -1); - display: block; - background-color: var(--diff-delete-parent-bg); - color: var(--diff-delete-parent-color); - z-index: -1; - } - } - - .html-diff li:not([data-enable-match='false']):has([data-match-type='create']) { - position: relative; - z-index: 1; - - &::before { - content: ''; - position: absolute; - top: 0; - bottom: 0; - left: -9999px; - right: -9999px; - display: block; - background-color: var(--diff-create-parent-bg); - color: var(--diff-create-parent-color); - z-index: -1; - } - } - + .html-diff pre:not([data-enable-match='false']):has([data-match-type='delete']), .html-diff li:not([data-enable-match='false']):has([data-match-type='delete']) { position: relative; z-index: 1; @@ -99,13 +63,12 @@ &::before { content: ''; position: absolute; - top: 0; - bottom: 0; - left: -9999px; - right: -9999px; - display: block; + top: 50%; + left: -100vw; + right: -100vw; + min-height: max(100%, var(--spacer-5)); + transform: translateY(-50%); background-color: var(--diff-delete-parent-bg); - color: var(--diff-delete-parent-color); z-index: -1; } } From 4186b56c0b2d574464c0bc35436bf84d36fe8c41 Mon Sep 17 00:00:00 2001 From: Jessica Rynkar Date: Wed, 27 May 2026 14:36:52 +0100 Subject: [PATCH 11/16] chore: fix version diff regressions --- .../DiffCollapser/index.css | 40 ++++------ .../fields/Checkbox/index.css | 5 +- .../fields/Relationship/index.css | 19 ++--- .../fields/Upload/index.css | 2 +- .../field/Diff/converters/listitem/index.css | 6 +- .../Diff/converters/relationship/index.css | 4 +- .../richtext-lexical/src/field/Diff/index.css | 10 +-- .../src/elements/FieldDiffContainer/index.css | 12 +-- .../src/elements/FieldDiffContainer/index.tsx | 2 +- packages/ui/src/elements/HTMLDiff/index.css | 73 +++++++++++++++---- 10 files changed, 92 insertions(+), 81 deletions(-) diff --git a/packages/next/src/views/Version/RenderFieldsToDiff/DiffCollapser/index.css b/packages/next/src/views/Version/RenderFieldsToDiff/DiffCollapser/index.css index cb14fcc0dd6..f6d7247c211 100644 --- a/packages/next/src/views/Version/RenderFieldsToDiff/DiffCollapser/index.css +++ b/packages/next/src/views/Version/RenderFieldsToDiff/DiffCollapser/index.css @@ -11,32 +11,20 @@ color: var(--color-icon-tertiary); } - &:hover { - &::before { - content: ''; - position: absolute; - top: 0; - left: calc(var(--spacer-1) * -1); - right: 0; - bottom: 0; - background-color: var(--color-bg-hover); - border-radius: var(--radius-medium); - z-index: -1; - } + &::before { + content: ''; + position: absolute; + inset: 0 0 0 calc(var(--spacer-1) * -1); + border-radius: var(--radius-medium); + z-index: -1; } - &:active { - &::before { - content: ''; - position: absolute; - top: 0; - left: calc(var(--spacer-1) * -1); - right: 0; - bottom: 0; - background-color: var(--color-bg-pressed); - border-radius: var(--radius-medium); - z-index: -1; - } + &:hover::before { + background-color: var(--color-bg-hover); + } + + &:active::before { + background-color: var(--color-bg-pressed); } } @@ -47,8 +35,8 @@ } .diff-collapser__field-change-count { - margin-left: var(--spacer-2); - padding: 0 var(--spacer-1); + margin-inline-start: var(--spacer-2); + padding-inline: var(--spacer-1); background: var(--color-bg-tertiary); border-radius: var(--radius-medium); font-family: var(--text-body-medium-font-family); diff --git a/packages/next/src/views/Version/RenderFieldsToDiff/fields/Checkbox/index.css b/packages/next/src/views/Version/RenderFieldsToDiff/fields/Checkbox/index.css index b0eb3784ebc..60a4f5d21f2 100644 --- a/packages/next/src/views/Version/RenderFieldsToDiff/fields/Checkbox/index.css +++ b/packages/next/src/views/Version/RenderFieldsToDiff/fields/Checkbox/index.css @@ -11,10 +11,7 @@ .checkbox-diff__value--delete::before { content: ''; position: absolute; - top: calc(var(--spacer-2) * -1); - bottom: calc(var(--spacer-2) * -1); - left: calc(var(--spacer-2-5) * -1); - right: calc(var(--spacer-2-5) * -1); + inset: calc(var(--spacer-2) * -1) 0; display: block; z-index: -1; } diff --git a/packages/next/src/views/Version/RenderFieldsToDiff/fields/Relationship/index.css b/packages/next/src/views/Version/RenderFieldsToDiff/fields/Relationship/index.css index 5d9a5ef9b15..559b97008d0 100644 --- a/packages/next/src/views/Version/RenderFieldsToDiff/fields/Relationship/index.css +++ b/packages/next/src/views/Version/RenderFieldsToDiff/fields/Relationship/index.css @@ -19,7 +19,7 @@ } .relationship-diff-container--hasOne .field-diff-content { - padding: var(--spacer-2) var(--spacer-2-5); + padding-block: var(--spacer-2); background: var(--color-bg-secondary); } @@ -37,10 +37,7 @@ )::before { content: ''; position: absolute; - top: calc(var(--spacer-2) * -1); - bottom: calc(var(--spacer-2) * -1); - left: calc(var(--spacer-2-5) * -1); - right: calc(var(--spacer-2-5) * -1); + inset: calc(var(--spacer-2) * -1) 0; display: block; z-index: -1; } @@ -59,7 +56,7 @@ .relationship-diff-container--hasMany .field-diff-content { background: var(--color-bg-secondary); - padding: var(--spacer-1) var(--spacer-2); + padding-block: var(--spacer-1); .html-diff { display: flex; @@ -109,6 +106,7 @@ border-radius: var(--radius-medium); padding: var(--spacer-2) var(--spacer-1); text-decoration-line: none; + color: inherit; } /* hasMany pills stay compact at 24px */ @@ -134,13 +132,11 @@ .html-diff .relationship-diff.relationship-diff.relationship-diff[data-match-type='create'] { background-color: var(--diff-create-parent-bg); outline-color: var(--diff-create-pill-border); - color: inherit; } .html-diff .relationship-diff.relationship-diff.relationship-diff[data-match-type='delete'] { background-color: var(--diff-delete-parent-bg); outline-color: var(--diff-delete-pill-border); - color: inherit; } .relationship-diff__pill { @@ -148,7 +144,7 @@ align-items: center; height: var(--spacer-3); border-radius: var(--radius-medium); - padding: 0 var(--spacer-1); + padding-inline: var(--spacer-1); background-color: var(--color-bg-tertiary); color: var(--color-text); white-space: nowrap; @@ -203,10 +199,7 @@ .relationship-diff-container--hasOne .field-diff-content > .html-diff:has(.relationship-diff[data-match-type='delete'])::before { - top: calc(var(--spacer-1) * -1); - bottom: calc(var(--spacer-1) * -1); - left: calc(var(--spacer-2) * -1); - right: calc(var(--spacer-2) * -1); + inset: calc(var(--spacer-1) * -1) calc(var(--spacer-2) * -1); } .relationship-diff-container--hasMany .field-diff-content { diff --git a/packages/next/src/views/Version/RenderFieldsToDiff/fields/Upload/index.css b/packages/next/src/views/Version/RenderFieldsToDiff/fields/Upload/index.css index 77daf47f37d..0194633b534 100644 --- a/packages/next/src/views/Version/RenderFieldsToDiff/fields/Upload/index.css +++ b/packages/next/src/views/Version/RenderFieldsToDiff/fields/Upload/index.css @@ -143,7 +143,7 @@ .upload-diff__pill { border-radius: var(--radius-medium); - padding: 0 var(--spacer-1); + padding-inline: var(--spacer-1); background-color: var(--color-bg-brand); color: var(--color-text-onbrand); font-size: var(--text-body-medium-font-size); diff --git a/packages/richtext-lexical/src/field/Diff/converters/listitem/index.css b/packages/richtext-lexical/src/field/Diff/converters/listitem/index.css index 898c773c711..c5e8539465a 100644 --- a/packages/richtext-lexical/src/field/Diff/converters/listitem/index.css +++ b/packages/richtext-lexical/src/field/Diff/converters/listitem/index.css @@ -1,7 +1,7 @@ @layer payload-default { .lexical-diff { ul.list-check { - padding-left: 0; + padding-inline-start: 0; } .checkboxItem { @@ -16,7 +16,7 @@ .checkboxItem__icon { width: var(--spacer-3); height: var(--spacer-3); - margin-right: var(--spacer-2); + margin-inline-end: var(--spacer-2); border: var(--stroke-width-small) solid var(--color-text); border-radius: var(--radius-medium); display: flex; @@ -53,7 +53,7 @@ } .checkboxItem--nested { - margin-left: var(--spacer-4); + margin-inline-start: var(--spacer-4); } } } diff --git a/packages/richtext-lexical/src/field/Diff/converters/relationship/index.css b/packages/richtext-lexical/src/field/Diff/converters/relationship/index.css index a3ff24f74de..f9fa9364c4e 100644 --- a/packages/richtext-lexical/src/field/Diff/converters/relationship/index.css +++ b/packages/richtext-lexical/src/field/Diff/converters/relationship/index.css @@ -13,7 +13,7 @@ font-size: var(--text-body-medium-font-size); font-weight: var(--text-body-medium-font-weight); line-height: var(--text-body-medium-line-height); - margin: var(--spacer-2) 0; + margin-block: var(--spacer-2); padding: var(--spacer-1); &[data-match-type='create'] { @@ -80,7 +80,7 @@ display: inline-flex; align-items: center; border-radius: var(--radius-medium); - padding: 0 var(--spacer-1); + padding-inline: var(--spacer-1); background-color: var(--color-bg-brand); color: var(--color-text-onbrand); white-space: nowrap; diff --git a/packages/richtext-lexical/src/field/Diff/index.css b/packages/richtext-lexical/src/field/Diff/index.css index 8f51456ea17..c6ac0e69dde 100644 --- a/packages/richtext-lexical/src/field/Diff/index.css +++ b/packages/richtext-lexical/src/field/Diff/index.css @@ -95,24 +95,22 @@ } hr { - margin: var(--spacer-2-5) 0; + margin-block: var(--spacer-2-5); border: none; border-top: var(--stroke-width-small) solid var(--color-border); } + ul, ol { padding: 0; margin-block: var(--spacer-2); } - ul { - padding-left: 0; - } - li { font-family: var(--text-body-large-font-family); font-size: var(--text-body-large-font-size); - margin: 0 0 var(--spacer-1) var(--spacer-4); + margin-block: 0 var(--spacer-1); + margin-inline-start: var(--spacer-4); } } } diff --git a/packages/ui/src/elements/FieldDiffContainer/index.css b/packages/ui/src/elements/FieldDiffContainer/index.css index d02f4cef308..926da3d9a29 100644 --- a/packages/ui/src/elements/FieldDiffContainer/index.css +++ b/packages/ui/src/elements/FieldDiffContainer/index.css @@ -2,15 +2,8 @@ .field-diff__locale-label { background: var(--color-bg-secondary); border-radius: var(--radius-medium); - padding: 0 var(--spacer-1); - - [dir='ltr'] & { - margin-right: var(--spacer-1); - } - - [dir='rtl'] & { - margin-left: var(--spacer-1); - } + padding-inline: var(--spacer-1); + margin-inline-end: var(--spacer-1); } .field-diff-container { @@ -23,6 +16,7 @@ background: var(--color-bg-secondary); padding-block: var(--spacer-2); border-radius: var(--radius-medium); + overflow: clip; } .field-diff-content > * { diff --git a/packages/ui/src/elements/FieldDiffContainer/index.tsx b/packages/ui/src/elements/FieldDiffContainer/index.tsx index ea420c6bf72..2be5a6d079d 100644 --- a/packages/ui/src/elements/FieldDiffContainer/index.tsx +++ b/packages/ui/src/elements/FieldDiffContainer/index.tsx @@ -44,7 +44,7 @@ export const FieldDiffContainer: React.FC<{ style={ nestingLevel ? ({ - '--field-diff-columns': `calc(50% - ${nestingLevel * gutterOffset}px - var(--spacer-2-5)) calc(50% + ${nestingLevel * gutterOffset}px - var(--spacer-2-5))`, + '--field-diff-columns': `calc(50% - ${nestingLevel * gutterOffset}px) calc(50% + ${nestingLevel * gutterOffset}px)`, } as React.CSSProperties) : undefined } diff --git a/packages/ui/src/elements/HTMLDiff/index.css b/packages/ui/src/elements/HTMLDiff/index.css index 3f53d435cd9..83eea424d1e 100644 --- a/packages/ui/src/elements/HTMLDiff/index.css +++ b/packages/ui/src/elements/HTMLDiff/index.css @@ -2,7 +2,6 @@ .html-diff { display: flex; flex-direction: column; - overflow: clip; font-family: var(--text-body-medium-font-family); font-size: var(--text-body-medium-font-size); font-weight: var(--text-body-medium-font-weight); @@ -14,13 +13,17 @@ color: var(--color-text-tertiary); } + .html-diff p { + margin: 0; + align-content: center; + } + .html-diff pre { margin-top: 0; margin-bottom: 0; } - /* Apply background color to parents that have children with diffs. - ::before extends full-width via -100vw, clipped by overflow:clip on .html-diff */ + /* Apply background color to parents that have children with diffs */ .html-diff p:not([data-enable-match='false']):has([data-match-type='create']), .html-diff h1:not([data-enable-match='false']):has([data-match-type='create']), .html-diff h2:not([data-enable-match='false']):has([data-match-type='create']), @@ -29,20 +32,21 @@ .html-diff h5:not([data-enable-match='false']):has([data-match-type='create']), .html-diff h6:not([data-enable-match='false']):has([data-match-type='create']), .html-diff blockquote:not([data-enable-match='false']):has([data-match-type='create']), - .html-diff pre:not([data-enable-match='false']):has([data-match-type='create']), - .html-diff li:not([data-enable-match='false']):has([data-match-type='create']) { + .html-diff pre:not([data-enable-match='false']):has([data-match-type='create']) { position: relative; z-index: 1; + flex: 1; &::before { content: ''; position: absolute; - top: 50%; - left: -100vw; - right: -100vw; - min-height: max(100%, var(--spacer-5)); - transform: translateY(-50%); + top: calc(var(--spacer-2) * -1); + bottom: calc(var(--spacer-2) * -1); + left: calc(var(--spacer-2-5) * -1); + right: calc(var(--spacer-2-5) * -1); + display: block; background-color: var(--diff-create-parent-bg); + color: var(--diff-create-parent-color); z-index: -1; } } @@ -55,7 +59,43 @@ .html-diff h5:not([data-enable-match='false']):has([data-match-type='delete']), .html-diff h6:not([data-enable-match='false']):has([data-match-type='delete']), .html-diff blockquote:not([data-enable-match='false']):has([data-match-type='delete']), - .html-diff pre:not([data-enable-match='false']):has([data-match-type='delete']), + .html-diff pre:not([data-enable-match='false']):has([data-match-type='delete']) { + position: relative; + z-index: 1; + flex: 1; + + &::before { + content: ''; + position: absolute; + top: calc(var(--spacer-2) * -1); + bottom: calc(var(--spacer-2) * -1); + left: calc(var(--spacer-2-5) * -1); + right: calc(var(--spacer-2-5) * -1); + display: block; + background-color: var(--diff-delete-parent-bg); + color: var(--diff-delete-parent-color); + z-index: -1; + } + } + + .html-diff li:not([data-enable-match='false']):has([data-match-type='create']) { + position: relative; + z-index: 1; + + &::before { + content: ''; + position: absolute; + top: 0; + bottom: 0; + left: -9999px; + right: -9999px; + display: block; + background-color: var(--diff-create-parent-bg); + color: var(--diff-create-parent-color); + z-index: -1; + } + } + .html-diff li:not([data-enable-match='false']):has([data-match-type='delete']) { position: relative; z-index: 1; @@ -63,12 +103,13 @@ &::before { content: ''; position: absolute; - top: 50%; - left: -100vw; - right: -100vw; - min-height: max(100%, var(--spacer-5)); - transform: translateY(-50%); + top: 0; + bottom: 0; + left: -9999px; + right: -9999px; + display: block; background-color: var(--diff-delete-parent-bg); + color: var(--diff-delete-parent-color); z-index: -1; } } From 055865fb5c828d3334c75d486469d3289fd5aad9 Mon Sep 17 00:00:00 2001 From: Jessica Rynkar Date: Wed, 27 May 2026 14:39:57 +0100 Subject: [PATCH 12/16] chore: fix lexical overflow --- packages/richtext-lexical/src/field/Diff/index.css | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/richtext-lexical/src/field/Diff/index.css b/packages/richtext-lexical/src/field/Diff/index.css index c6ac0e69dde..dc4c44e6aef 100644 --- a/packages/richtext-lexical/src/field/Diff/index.css +++ b/packages/richtext-lexical/src/field/Diff/index.css @@ -1,6 +1,7 @@ @layer payload-default { .lexical-diff .field-diff-content { .html-diff { + overflow: clip; font-family: var(--text-body-large-font-family); font-size: var(--text-body-large-font-size); font-weight: var(--text-body-large-font-weight); From bf89324b432cb7a800f1d872971cb185fbc0e4ea Mon Sep 17 00:00:00 2001 From: Jessica Rynkar Date: Wed, 27 May 2026 18:32:49 +0100 Subject: [PATCH 13/16] chore: fix lexical diff --- .../fields/Relationship/index.css | 1 + .../field/Diff/converters/upload/index.css | 65 ++++++++-- .../richtext-lexical/src/field/Diff/index.css | 69 ++++++++--- packages/ui/src/elements/HTMLDiff/index.css | 114 +++++------------- 4 files changed, 140 insertions(+), 109 deletions(-) diff --git a/packages/next/src/views/Version/RenderFieldsToDiff/fields/Relationship/index.css b/packages/next/src/views/Version/RenderFieldsToDiff/fields/Relationship/index.css index 559b97008d0..f948df2e419 100644 --- a/packages/next/src/views/Version/RenderFieldsToDiff/fields/Relationship/index.css +++ b/packages/next/src/views/Version/RenderFieldsToDiff/fields/Relationship/index.css @@ -65,6 +65,7 @@ max-width: unset; flex-wrap: wrap; gap: var(--spacer-1); + justify-content: flex-start; } .relationship-diff { diff --git a/packages/richtext-lexical/src/field/Diff/converters/upload/index.css b/packages/richtext-lexical/src/field/Diff/converters/upload/index.css index aeb26316241..9c4b52bd56a 100644 --- a/packages/richtext-lexical/src/field/Diff/converters/upload/index.css +++ b/packages/richtext-lexical/src/field/Diff/converters/upload/index.css @@ -12,20 +12,17 @@ position: relative; z-index: 2; font-family: var(--text-body-medium-font-family); - margin-block: var(--spacer-1); padding: var(--spacer-2); - &[data-match-type] { - .lexical-upload-diff__thumbnail { - background-color: transparent; - } - } - &[data-match-type='create'] { background-color: var(--diff-create-parent-bg); outline-color: var(--diff-create-pill-border); color: var(--diff-create-parent-color); + .lexical-upload-diff__thumbnail { + background-color: var(--diff-create-pill-bg); + } + .lexical-upload-diff__info { color: var(--diff-create-parent-color); } @@ -41,14 +38,22 @@ outline-color: var(--diff-delete-pill-border); color: var(--diff-delete-parent-color); + .lexical-upload-diff__thumbnail { + background-color: var(--diff-delete-pill-bg); + } + .lexical-upload-diff__info { - text-decoration-line: line-through; - text-decoration-color: var(--diff-delete-parent-color); color: var(--diff-delete-parent-color); + + strong { + text-decoration-line: line-through; + text-decoration-color: var(--diff-delete-parent-color); + } } .lexical-upload-diff__meta { text-decoration-line: line-through; + text-decoration-color: var(--diff-delete-parent-color); color: var(--diff-delete-parent-color); } } @@ -124,5 +129,47 @@ overflow: hidden; text-overflow: ellipsis; } + + /* Style uploads based on ancestor data-match-type (e.g. from diff wrappers) */ + [data-match-type='create'] .lexical-upload-diff__info { + color: var(--diff-create-parent-color); + } + + [data-match-type='create'] .lexical-upload-diff__meta.lexical-upload-diff__meta { + color: var(--diff-create-parent-color); + } + + [data-match-type='create'] .lexical-upload-diff__thumbnail { + background-color: var(--diff-create-pill-bg); + } + + [data-match-type='create'] .lexical-upload-diff { + background-color: var(--diff-create-parent-bg); + outline-color: var(--diff-create-pill-border); + } + + [data-match-type='delete'] .lexical-upload-diff__info { + color: var(--diff-delete-parent-color); + + strong { + text-decoration-line: line-through; + text-decoration-color: var(--diff-delete-parent-color); + } + } + + [data-match-type='delete'] .lexical-upload-diff__meta.lexical-upload-diff__meta { + text-decoration-line: line-through; + text-decoration-color: var(--diff-delete-parent-color); + color: var(--diff-delete-parent-color); + } + + [data-match-type='delete'] .lexical-upload-diff__thumbnail { + background-color: var(--diff-delete-pill-bg); + } + + [data-match-type='delete'] .lexical-upload-diff { + background-color: var(--diff-delete-parent-bg); + outline-color: var(--diff-delete-pill-border); + } } } diff --git a/packages/richtext-lexical/src/field/Diff/index.css b/packages/richtext-lexical/src/field/Diff/index.css index dc4c44e6aef..1ae4176f1b5 100644 --- a/packages/richtext-lexical/src/field/Diff/index.css +++ b/packages/richtext-lexical/src/field/Diff/index.css @@ -1,7 +1,6 @@ @layer payload-default { .lexical-diff .field-diff-content { .html-diff { - overflow: clip; font-family: var(--text-body-large-font-family); font-size: var(--text-body-large-font-size); font-weight: var(--text-body-large-font-weight); @@ -13,20 +12,19 @@ font-size: var(--text-body-large-font-size); line-height: 1.5; letter-spacing: normal; - margin-block: var(--spacer-3); margin-inline: var(--spacer-1); padding-inline-start: var(--spacer-2-5); padding-block: var(--spacer-1); position: relative; - border-inline-start: var(--spacer-1) solid var(--color-border); + border-inline-start: var(--stroke-width-medium) solid currentColor; + } - &:has([data-match-type='create']) { - border-inline-start-color: var(--color-bg-success-tertiary); - } + .html-diff:has([data-match-type='create']) blockquote { + color: var(--diff-create-pill-color); + } - &:has([data-match-type='delete']) { - border-inline-start-color: var(--color-bg-danger-tertiary); - } + .html-diff:has([data-match-type='delete']) blockquote { + color: var(--diff-delete-pill-color); } a { @@ -42,7 +40,6 @@ h6 { font-weight: 550; line-height: 1.2; - margin-block: var(--spacer-3); } h1 { @@ -80,7 +77,7 @@ } p { - margin-block: var(--spacer-3); + margin: 0; } code { @@ -96,15 +93,29 @@ } hr { - margin-block: var(--spacer-2-5); + margin: 0; border: none; - border-top: var(--stroke-width-small) solid var(--color-border); + border-top: var(--stroke-width-small) solid currentColor; + } + + .html-diff:has([data-match-type='create']) hr { + color: var(--diff-create-pill-color); + } + + .html-diff:has([data-match-type='delete']) hr { + color: var(--diff-delete-pill-color); + } + + ul { + list-style: disc; + padding: 0; + margin: 0; } - ul, ol { + list-style: decimal; padding: 0; - margin-block: var(--spacer-2); + margin: 0; } li { @@ -112,6 +123,34 @@ font-size: var(--text-body-large-font-size); margin-block: 0 var(--spacer-1); margin-inline-start: var(--spacer-4); + position: relative; + z-index: 0; + + &:has(> [data-match-type='create'])::before, + &:has(> [data-match-type='delete'])::before { + content: ''; + position: absolute; + inset: 0; + inset-inline-start: calc(var(--spacer-4) * -1); + border-radius: var(--radius-medium); + z-index: -1; + } + + &:has(> [data-match-type='create'])::before { + background-color: var(--diff-create-pill-bg); + } + + &:has(> [data-match-type='delete'])::before { + background-color: var(--diff-delete-pill-bg); + } + + &.nestedListItem:last-child { + margin-block-end: 0; + } + + &:last-child { + margin-block-end: 0; + } } } } diff --git a/packages/ui/src/elements/HTMLDiff/index.css b/packages/ui/src/elements/HTMLDiff/index.css index 83eea424d1e..6f226b06e42 100644 --- a/packages/ui/src/elements/HTMLDiff/index.css +++ b/packages/ui/src/elements/HTMLDiff/index.css @@ -2,6 +2,7 @@ .html-diff { display: flex; flex-direction: column; + gap: var(--spacer-3); font-family: var(--text-body-medium-font-family); font-size: var(--text-body-medium-font-size); font-weight: var(--text-body-medium-font-weight); @@ -23,99 +24,29 @@ margin-bottom: 0; } - /* Apply background color to parents that have children with diffs */ - .html-diff p:not([data-enable-match='false']):has([data-match-type='create']), - .html-diff h1:not([data-enable-match='false']):has([data-match-type='create']), - .html-diff h2:not([data-enable-match='false']):has([data-match-type='create']), - .html-diff h3:not([data-enable-match='false']):has([data-match-type='create']), - .html-diff h4:not([data-enable-match='false']):has([data-match-type='create']), - .html-diff h5:not([data-enable-match='false']):has([data-match-type='create']), - .html-diff h6:not([data-enable-match='false']):has([data-match-type='create']), - .html-diff blockquote:not([data-enable-match='false']):has([data-match-type='create']), - .html-diff pre:not([data-enable-match='false']):has([data-match-type='create']) { - position: relative; - z-index: 1; - flex: 1; - - &::before { - content: ''; - position: absolute; - top: calc(var(--spacer-2) * -1); - bottom: calc(var(--spacer-2) * -1); - left: calc(var(--spacer-2-5) * -1); - right: calc(var(--spacer-2-5) * -1); - display: block; - background-color: var(--diff-create-parent-bg); - color: var(--diff-create-parent-color); - z-index: -1; - } + /* Apply background color to the whole field when it has diffs */ + .html-diff:has([data-match-type='create']) { + background-color: var(--diff-create-parent-bg); + margin-block: calc(var(--spacer-2) * -1); + padding-block: var(--spacer-2); } - .html-diff p:not([data-enable-match='false']):has([data-match-type='delete']), - .html-diff h1:not([data-enable-match='false']):has([data-match-type='delete']), - .html-diff h2:not([data-enable-match='false']):has([data-match-type='delete']), - .html-diff h3:not([data-enable-match='false']):has([data-match-type='delete']), - .html-diff h4:not([data-enable-match='false']):has([data-match-type='delete']), - .html-diff h5:not([data-enable-match='false']):has([data-match-type='delete']), - .html-diff h6:not([data-enable-match='false']):has([data-match-type='delete']), - .html-diff blockquote:not([data-enable-match='false']):has([data-match-type='delete']), - .html-diff pre:not([data-enable-match='false']):has([data-match-type='delete']) { - position: relative; - z-index: 1; - flex: 1; - - &::before { - content: ''; - position: absolute; - top: calc(var(--spacer-2) * -1); - bottom: calc(var(--spacer-2) * -1); - left: calc(var(--spacer-2-5) * -1); - right: calc(var(--spacer-2-5) * -1); - display: block; - background-color: var(--diff-delete-parent-bg); - color: var(--diff-delete-parent-color); - z-index: -1; - } + .html-diff:has([data-match-type='delete']) { + background-color: var(--diff-delete-parent-bg); + margin-block: calc(var(--spacer-2) * -1); + padding-block: var(--spacer-2); } - .html-diff li:not([data-enable-match='false']):has([data-match-type='create']) { - position: relative; - z-index: 1; - - &::before { - content: ''; - position: absolute; - top: 0; - bottom: 0; - left: -9999px; - right: -9999px; - display: block; - background-color: var(--diff-create-parent-bg); - color: var(--diff-create-parent-color); - z-index: -1; - } + .html-diff li::marker { + color: var(--color-text); } - .html-diff li:not([data-enable-match='false']):has([data-match-type='delete']) { - position: relative; - z-index: 1; - - &::before { - content: ''; - position: absolute; - top: 0; - bottom: 0; - left: -9999px; - right: -9999px; - display: block; - background-color: var(--diff-delete-parent-bg); - color: var(--diff-delete-parent-color); - z-index: -1; - } + .html-diff li:has(> [data-match-type='create'])::marker { + color: var(--diff-create-pill-color); } - .html-diff li::marker { - color: var(--color-text); + .html-diff li:has(> [data-match-type='delete'])::marker { + color: var(--diff-delete-pill-color); } .html-diff @@ -129,6 +60,7 @@ border-radius: var(--radius-medium); padding: calc(var(--stroke-width-small) * 1.5) calc(var(--stroke-width-small) * 2); text-decoration-thickness: var(--stroke-width-small); + vertical-align: baseline; } .html-diff @@ -153,6 +85,7 @@ color: var(--diff-create-pill-color); border-radius: var(--radius-medium); padding: calc(var(--stroke-width-small) * 1.5) calc(var(--stroke-width-small) * 2); + vertical-align: baseline; } .html-diff-create-inline-wrapper, @@ -185,6 +118,17 @@ } @media (max-width: 768px) { + .html-diff:has([data-match-type='create']), + .html-diff:has([data-match-type='delete']) { + margin-block: 0; + padding-block: 0; + background-color: transparent; + } + + .html-diff { + justify-content: center; + } + .html-diff p { align-content: center; } From 40f509648e2605e45279c3a2f50a87c52b5ca4ff Mon Sep 17 00:00:00 2001 From: Patrik Kozak <35232443+PatrikKozak@users.noreply.github.com> Date: Thu, 28 May 2026 11:12:14 -0400 Subject: [PATCH 14/16] fix(ui): adjust top and bottom padding on htlm diff pills --- packages/ui/src/elements/HTMLDiff/index.css | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/ui/src/elements/HTMLDiff/index.css b/packages/ui/src/elements/HTMLDiff/index.css index 6f226b06e42..74c057dc0c0 100644 --- a/packages/ui/src/elements/HTMLDiff/index.css +++ b/packages/ui/src/elements/HTMLDiff/index.css @@ -58,7 +58,7 @@ text-decoration-line: line-through; background-color: var(--diff-delete-pill-bg); border-radius: var(--radius-medium); - padding: calc(var(--stroke-width-small) * 1.5) calc(var(--stroke-width-small) * 2); + padding: calc(var(--stroke-width-small) * 1) calc(var(--stroke-width-small) * 2); text-decoration-thickness: var(--stroke-width-small); vertical-align: baseline; } @@ -84,7 +84,7 @@ background-color: var(--diff-create-pill-bg); color: var(--diff-create-pill-color); border-radius: var(--radius-medium); - padding: calc(var(--stroke-width-small) * 1.5) calc(var(--stroke-width-small) * 2); + padding: calc(var(--stroke-width-small) * 1) calc(var(--stroke-width-small) * 2); vertical-align: baseline; } From bdeb69d1866a1819f2286cd3872b06fc5f11e9a5 Mon Sep 17 00:00:00 2001 From: Jessica Rynkar Date: Thu, 28 May 2026 17:57:36 +0100 Subject: [PATCH 15/16] chore(ui): fix upload hasMany diff and list items --- .../fields/Upload/index.css | 6 ++++++ .../fields/Upload/index.tsx | 2 +- .../field/Diff/converters/listitem/index.css | 21 +++++++++++++++++++ 3 files changed, 28 insertions(+), 1 deletion(-) diff --git a/packages/next/src/views/Version/RenderFieldsToDiff/fields/Upload/index.css b/packages/next/src/views/Version/RenderFieldsToDiff/fields/Upload/index.css index 0194633b534..bfdaa1f518f 100644 --- a/packages/next/src/views/Version/RenderFieldsToDiff/fields/Upload/index.css +++ b/packages/next/src/views/Version/RenderFieldsToDiff/fields/Upload/index.css @@ -4,6 +4,12 @@ background: unset; } + .upload-diff-container .html-diff { + background-color: transparent; + margin-block: 0; + padding-block: 0; + } + .upload-diff-container .diff-no-value { padding: var(--spacer-2) var(--spacer-2-5); } diff --git a/packages/next/src/views/Version/RenderFieldsToDiff/fields/Upload/index.tsx b/packages/next/src/views/Version/RenderFieldsToDiff/fields/Upload/index.tsx index a847e0589f4..0f139d877b3 100644 --- a/packages/next/src/views/Version/RenderFieldsToDiff/fields/Upload/index.tsx +++ b/packages/next/src/views/Version/RenderFieldsToDiff/fields/Upload/index.tsx @@ -187,7 +187,7 @@ export const HasManyUploadDiff: React.FC<{ className="field-diff-content" style={ { - '--field-diff-columns': `calc(50% - ${uploadGutterOffset}px - var(--spacer-2-5)) calc(50% + ${uploadGutterOffset}px - var(--spacer-2-5))`, + '--field-diff-columns': `calc(50% - ${uploadGutterOffset}px) calc(50% + ${uploadGutterOffset}px)`, } as React.CSSProperties } > diff --git a/packages/richtext-lexical/src/field/Diff/converters/listitem/index.css b/packages/richtext-lexical/src/field/Diff/converters/listitem/index.css index c5e8539465a..695d3bbdd10 100644 --- a/packages/richtext-lexical/src/field/Diff/converters/listitem/index.css +++ b/packages/richtext-lexical/src/field/Diff/converters/listitem/index.css @@ -55,5 +55,26 @@ .checkboxItem--nested { margin-inline-start: var(--spacer-4); } + + /* Match editor's hierarchical ordered list numbering (1, A, a, I, i) */ + ol.list-number { + list-style-type: decimal; + } + + ol.list-number ol.list-number { + list-style-type: upper-alpha; + } + + ol.list-number ol.list-number ol.list-number { + list-style-type: lower-alpha; + } + + ol.list-number ol.list-number ol.list-number ol.list-number { + list-style-type: upper-roman; + } + + ol.list-number ol.list-number ol.list-number ol.list-number ol.list-number { + list-style-type: lower-roman; + } } } From 9cf5071c82b3c36edb0b4eb1d8eec39361edcbec Mon Sep 17 00:00:00 2001 From: Jessica Rynkar Date: Fri, 29 May 2026 17:47:38 +0100 Subject: [PATCH 16/16] chore: update upload diffs --- .../Version/RenderFieldsToDiff/fields/Upload/index.css | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/packages/next/src/views/Version/RenderFieldsToDiff/fields/Upload/index.css b/packages/next/src/views/Version/RenderFieldsToDiff/fields/Upload/index.css index bfdaa1f518f..e8a1d91d78f 100644 --- a/packages/next/src/views/Version/RenderFieldsToDiff/fields/Upload/index.css +++ b/packages/next/src/views/Version/RenderFieldsToDiff/fields/Upload/index.css @@ -14,6 +14,14 @@ padding: var(--spacer-2) var(--spacer-2-5); } + .upload-diff-container .field-diff-content > .html-diff__diff-old { + padding-left: 0; + } + + .upload-diff-container .field-diff-content > .html-diff__diff-new { + padding-right: 0; + } + @media (max-width: 768px) { /* Stack hasMany columns vertically */ .upload-diff-container--hasMany .field-diff-content {