Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
05fa436
[skeleton, spin-container, spin] redesign
ilyabrower Aug 5, 2026
a8af5ac
[spin] fixed sizes
ilyabrower Aug 6, 2026
9dfa503
[skeleton] removed redundant logic
ilyabrower Aug 6, 2026
087eed9
[skeleton] fixed fills
ilyabrower Aug 10, 2026
2020d29
[spin] adjusted animation, small layout fixes in examples
sheila-semrush Aug 10, 2026
874a8ed
[stories] updated spin alignment in notice-bubble
sheila-semrush Aug 10, 2026
279b252
[skeleton, spin, spin-container] fixed themes
ilyabrower Aug 12, 2026
564f19e
[skeleton, spin, spin-container] fixed themes
ilyabrower Aug 12, 2026
18e59fb
Merge branch 'UIK-5351/add-more-variables-to-theme' into UIK-5667/spi…
Valeria-Zimnitskaya Aug 12, 2026
68c2142
[skeleton] fixed defaul props
ilyabrower Aug 13, 2026
69ffeb5
[chore] update all test related
Valeria-Zimnitskaya Aug 13, 2026
9f10dbb
[spin] removed Box props, left only margins
ilyabrower Aug 13, 2026
d66e34e
[skeleton] fixed invert theme for charts
ilyabrower Aug 13, 2026
7d3c333
[spin] update tests
Valeria-Zimnitskaya Aug 13, 2026
ec0f472
[skeleton] fixed invert theme for charts
ilyabrower Aug 13, 2026
6256ad4
[chore] Merge branch 'UIK-5667/spin-redesign' of github.com:semrush/i…
ilyabrower Aug 13, 2026
1130b53
[chore] update tests
Valeria-Zimnitskaya Aug 13, 2026
e5b436e
[chore] update snapshots tests
Valeria-Zimnitskaya Aug 13, 2026
6bed1f5
[skeleton] fixed line chart svgs
ilyabrower Aug 14, 2026
0bffdc4
[chore] Merge branch 'UIK-5667/spin-redesign' of github.com:semrush/i…
ilyabrower Aug 14, 2026
e92650e
[base-trigger] removed unnecessary theme=false on loading spin
ilyabrower Aug 14, 2026
244715f
[chore] update snapshots
Valeria-Zimnitskaya Aug 14, 2026
4888d0b
[chore] Merge branch 'UIK-5351/add-more-variables-to-theme' into UIK-…
ilyabrower Aug 14, 2026
ebf073d
[chore] Merge remote-tracking branch 'origin/UIK-5351/add-more-variab…
Valeria-Zimnitskaya Aug 14, 2026
b2a2cc8
[chore] update snapshots
Valeria-Zimnitskaya Aug 14, 2026
152b61d
[spin-container] change overlay to primary
sheila-semrush Aug 14, 2026
f78ac62
[skeleton] fixed texts skeletons
ilyabrower Aug 17, 2026
106557f
[chore] Merge branch 'UIK-5667/spin-redesign' of github.com:semrush/i…
ilyabrower Aug 17, 2026
2265343
Merge branch 'UIK-5351/add-more-variables-to-theme' into UIK-5667/spi…
Valeria-Zimnitskaya Aug 17, 2026
e596f78
[chore] update snapshots
Valeria-Zimnitskaya Aug 17, 2026
a2188a6
[chore] update snapshots
Valeria-Zimnitskaya Aug 17, 2026
a0a4e73
Merge branch 'UIK-5351/add-more-variables-to-theme' into UIK-5667/spi…
Valeria-Zimnitskaya Aug 17, 2026
2952c3e
[spin] fixed texts in docs
ilyabrower Aug 18, 2026
740774d
[docs] replaced dark theme mentions in spin, spin-container, skeleton
sheila-semrush Aug 18, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
18 changes: 9 additions & 9 deletions playground/entries/Spin.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,9 @@ type AdditionalJSXProps = {
};
export type SpinJSXProps = JSXProps<NSSpin.Props> & AdditionalJSXProps;

const margins = {
xs: 1,
s: 1,
const gaps = {
xs: 1.5,
s: 1.5,
m: 2,
l: 2,
xl: 4,
Expand All @@ -32,10 +32,10 @@ const margins = {
const textSize: { [key in SpinJSXProps['size']]: NSText.Props['size'] } = {
xs: 200,
s: 200,
m: 300,
m: 200,
l: 300,
xl: 200,
xxl: 200,
xl: 300,
xxl: 300,
};

const textPlacement: { [key in SpinJSXProps['textPlacement']]: NSFlex.Props['direction'] } = {
Expand All @@ -46,7 +46,7 @@ const textPlacement: { [key in SpinJSXProps['textPlacement']]: NSFlex.Props['dir
function getJSX(props: SpinJSXProps) {
return props.text.length
? (
<Flex alignItems='center' gap={margins[props.size]} direction={textPlacement[props.textPlacement]}>
<Flex alignItems='center' gap={gaps[props.size]} direction={textPlacement[props.textPlacement]}>
<Spin size={props.size} theme={props.theme} />
<Text size={textSize[props.size]} color='text-secondary'>
{props.text}
Expand All @@ -69,8 +69,8 @@ const entry: PlaygroundEntry<SpinJSXProps> = {
},
theme: {
type: 'inline-radio',
value: 'dark',
options: ['dark', 'invert'],
value: 'default',
options: ['default', 'invert'],
displayName: 'Theme',
},
text: {
Expand Down
4 changes: 2 additions & 2 deletions playground/entries/SpinContainer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -48,8 +48,8 @@ const entry: PlaygroundEntry<SpinContainerJSXProps> = {
},
theme: {
type: 'inline-radio',
value: 'dark',
options: ['dark', 'invert'],
value: 'default',
options: ['default', 'invert'],
displayName: 'Theme',
},
size: {
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 1 addition & 1 deletion semcore/base-trigger/src/ButtonTrigger.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ class RootButtonTrigger extends Component {
{addonTextChildren(Children, ButtonTrigger.Text, ButtonTrigger.Addon, empty)}
{(chevron || loading) && (
<SButtonAddon>
{loading ? <SButtonTriggerSpin size='xs' theme={false} /> : <ChevronDown />}
{loading ? <SButtonTriggerSpin size='xs' /> : <ChevronDown />}
</SButtonAddon>
)}
</Root>,
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
Expand Up @@ -694,11 +694,13 @@ test.describe(`${TAG.VISUAL}`, () => {
test('Verify Notice with Loading state', {
tag: [TAG.PRIORITY_HIGH, '@notice-bubble'],
}, async ({ page, browserName }) => {
test.setTimeout(60000);
await loadPage(page, 'stories/components/notice-bubble/docs/examples/dynamic_notice.tsx', 'en', {
initialAnimation: true,
duration: 0,
type: 'info',
focusLock: false,
loadingDuration: 10000,
});
await page.keyboard.press('Tab');
await page.keyboard.press('Enter');
Expand All @@ -710,7 +712,7 @@ test.describe(`${TAG.VISUAL}`, () => {
await page.keyboard.press('Enter');
await page.locator('[data-ui-name="Spin"]').waitFor({ state: 'visible' });
await expect(page).toHaveScreenshot();
await page.waitForSelector('text="Try again"');
await page.waitForSelector('text="Try again"', { timeout: 30000 });
await page.keyboard.press('Tab');
await locators.closeHint(page).waitFor({ state: 'visible' });
await expect(page).toHaveScreenshot();
Expand Down
257 changes: 198 additions & 59 deletions semcore/skeleton/__tests__/skeleton.browser-test.tsx
Original file line number Diff line number Diff line change
@@ -1,108 +1,247 @@
import type { Page } from '@semcore/testing-utils/playwright';
import type { Locator, Page } from '@semcore/testing-utils/playwright';
import { expect, test } from '@semcore/testing-utils/playwright';
import { loadPage } from '@semcore/testing-utils/shared/helpers';
import { TAG } from '@semcore/testing-utils/shared/tags';

const BASE_EXAMPLE = 'stories/components/skeleton/tests/examples/chart-skeletons.tsx';

/** every chart skeleton rendered by the example, in DOM order */
const CHARTS = [
'LineChartSkeleton',
'AreaChartSkeleton',
'BarChartSkeleton',
'HistogramChartSkeleton',
'CompactHorizontalBarChartSkeleton',
'DonutChartSkeleton',
'ScatterPlotChartSkeleton',
'BubbleChartSkeleton',
'VennChartSkeleton',
'RadialTreeChartSkeleton',
] as const;

export const locators = {
skeleton: (page: Page, index?: number) => {
const base = page.locator('[data-ui-name="SkeletonSVG"]');
return typeof index === 'number' ? base.nth(index) : base;
},
button: (page: Page, index?: number) => {
const base = page.locator('[data-ui-name="Button"]');
chartSkeleton: (page: Page, index?: number) => {
const base = page.locator('[data-ui-name="Skeleton"]');
return typeof index === 'number' ? base.nth(index) : base;
},
// areas of the example, so every screenshot is scoped to its own skeleton
area: (page: Page, testId: string) => page.locator(`[data-testid="${testId}"]`),
chart: (page: Page, name: string) => page.locator(`[data-testid="chart-${name}"]`),
};

const computed = (locator: Locator, property: string) =>
locator.evaluate((el, prop) => getComputedStyle(el).getPropertyValue(prop), property);

/* =====================================================
@visual
Visual states, hover and focus styles, paddings, margins, and snapshots.
Visual states and snapshots.
Every snapshot is taken from a [data-testid] area, never from the whole page.
The pulse animation is frozen with duration=0 to keep screenshots stable.
===================================================== */
test.describe(`${TAG.VISUAL}`, () => {
test('Verify text initial loading styles', {
test('Verify every chart skeleton pattern', {
tag: [TAG.PRIORITY_HIGH, '@skeleton'],
}, async ({ page }) => {
await loadPage(page, 'stories/components/skeleton/docs/examples/text_initial_loading.tsx', 'en');
await loadPage(page, BASE_EXAMPLE, 'en', { duration: 0 });

for (const name of CHARTS) {
await test.step(`Verify ${name}`, async () => {
const chart = locators.chart(page, name);

await chart.scrollIntoViewIfNeeded();
await expect(chart).toBeVisible();
await expect(chart).toHaveScreenshot(`${name}.png`);
});
}
});

test('Verify type=monotone for Line and Area', {
tag: [TAG.PRIORITY_MEDIUM, '@skeleton'],
}, async ({ page }) => {
await loadPage(page, BASE_EXAMPLE, 'en', { duration: 0, type: 'monotone' });

await expect(locators.chart(page, 'LineChartSkeleton')).toHaveScreenshot(
'LineChartSkeleton-monotone.png',
);
await expect(locators.chart(page, 'AreaChartSkeleton')).toHaveScreenshot(
'AreaChartSkeleton-monotone.png',
);
});

test('Verify layout=vertical for Bar and Histogram', {
tag: [TAG.PRIORITY_MEDIUM, '@skeleton'],
}, async ({ page }) => {
await loadPage(page, BASE_EXAMPLE, 'en', { duration: 0, layout: 'vertical' });

await expect(locators.chart(page, 'BarChartSkeleton')).toHaveScreenshot(
'BarChartSkeleton-vertical.png',
);
await expect(locators.chart(page, 'HistogramChartSkeleton')).toHaveScreenshot(
'HistogramChartSkeleton-vertical.png',
);
});

test('Verify halfsize donut', {
tag: [TAG.PRIORITY_MEDIUM, '@skeleton'],
}, async ({ page }) => {
await loadPage(page, BASE_EXAMPLE, 'en', { duration: 0, halfsize: true });

await locators.skeleton(page).waitFor({ state: 'visible' });
await expect(page).toHaveScreenshot();
await expect(locators.chart(page, 'DonutChartSkeleton')).toHaveScreenshot(
'DonutChartSkeleton-halfsize.png',
);
});

await expect(locators.skeleton(page)).toHaveAttribute('preserveAspectRatio', 'none');
const textCases = [
{ theme: 'default', amount: 1, textWidth: '60%' },
{ theme: 'invert', amount: 1, textWidth: '60%' },
{ theme: 'default', amount: 3, textWidth: '100%' },
{ theme: 'invert', amount: 5, textWidth: '30%' },
] as const;

const styleAttr = await locators.skeleton(page).getAttribute('style');
expect(styleAttr).toContain('2000ms');
textCases.forEach((item) => {
test(`Verify theme=${item.theme}, amount=${item.amount}`, {
tag: [TAG.PRIORITY_HIGH, '@skeleton'],
}, async ({ page }) => {
await loadPage(page, BASE_EXAMPLE, 'en', {
...item,
duration: 0,
invertedBackground: item.theme === 'invert',
});

await locators.button(page).click();
await expect(locators.skeleton(page)).not.toBeVisible();
await expect(locators.skeleton(page, 0)).toBeVisible();
await expect(locators.area(page, 'plain-skeleton')).toHaveScreenshot();
});
});

test('Verify skeleton for charts', {
test('Verify chart skeletons on inverted background', {
tag: [TAG.PRIORITY_HIGH, '@skeleton'],
}, async ({ page }) => {
await loadPage(page, 'stories/components/skeleton/docs/examples/skeleton_examples_for_charts.tsx', 'en');
await loadPage(page, BASE_EXAMPLE, 'en', {
duration: 0,
theme: 'invert',
invertedBackground: true,
});

const card = page.locator('[data-ui-name="Card"]');
const count = await card.count();
await expect(locators.area(page, 'chart-skeletons')).toHaveScreenshot();
});
});

for (let i = 0; i < count; i++) {
const currentCard = card.nth(i);
const title = await currentCard.locator('[data-ui-name="Card.Title"]').textContent();
/* =====================================================
@functional
Attributes, props forwarding, locales and resizing - no snapshots here.
===================================================== */
test.describe(`${TAG.FUNCTIONAL}`, () => {
test('Verify root attributes', {
tag: [TAG.PRIORITY_HIGH, '@skeleton'],
}, async ({ page }) => {
await loadPage(page, BASE_EXAMPLE, 'en');

await currentCard.scrollIntoViewIfNeeded();
await expect(currentCard).toBeVisible();
const skeleton = locators.skeleton(page, 0);

const clip = await currentCard.boundingBox();
if (!clip) {
throw new Error(`Card "${title}" is not available for screenshot`);
}
await expect(skeleton).toBeVisible();
await expect(skeleton).toHaveAttribute('role', 'img');
await expect(skeleton).toHaveAttribute('aria-label', 'Loading…');
await expect(skeleton).toHaveAttribute('preserveAspectRatio', 'none');
});

await expect(page).toHaveScreenshot(`${title}.png`, { clip });
}
test('Verify duration prop drives the pulse animation', {
tag: [TAG.PRIORITY_MEDIUM, '@skeleton'],
}, async ({ page }) => {
await loadPage(page, BASE_EXAMPLE, 'en', { duration: 500 });

const skeleton = locators.skeleton(page, 0);

expect(await skeleton.getAttribute('style')).toContain('500ms');
expect(await computed(skeleton, 'animation-duration')).toBe('0.5s');
});

const themeVariables = [
{ theme: 'dark', amount: 1, duration: 0 },
{ theme: 'invert', amount: 1, duration: 0 },
{ theme: 'dark', amount: 3, duration: 0 },
{ theme: 'invert', amount: 5, width: '60%', duration: 0 },
];
themeVariables.forEach((item) => {
test(`Verify theme=${item.theme}, amount=${item.amount}`, {
tag: [TAG.PRIORITY_HIGH, '@skeleton'],
}, async ({ page }) => {
await loadPage(page, 'stories/components/skeleton/tests/examples/skeleton-themes.tsx', 'en', item);
test('Verify hidden prop renders nothing', {
tag: [TAG.PRIORITY_HIGH, '@skeleton'],
}, async ({ page }) => {
await loadPage(page, BASE_EXAMPLE, 'en', { hidden: true });

await test.step('Verify skeleton visibility', async () => {
await expect(locators.skeleton(page, 0)).toBeVisible();
});
await expect(locators.skeleton(page)).toHaveCount(0);
await expect(locators.chartSkeleton(page)).toHaveCount(0);
});

await test.step('Verify screenshot', async () => {
await expect(page).toHaveScreenshot();
});
});
test('Verify aria-label is localized', {
tag: [TAG.PRIORITY_MEDIUM, '@skeleton'],
}, async ({ page }) => {
await loadPage(page, BASE_EXAMPLE, 'de', { locale: 'de' });

expect(await locators.skeleton(page, 0).getAttribute('aria-label')).not.toBe('Loading…');
});

test('Verify Skeleton.Text amount renders the requested number of lines', {
tag: [TAG.PRIORITY_MEDIUM, '@skeleton'],
}, async ({ page }) => {
await loadPage(page, BASE_EXAMPLE, 'en', { amount: 4 });

// 4 lines from the first Skeleton.Text plus a single line from the second one
await expect(locators.area(page, 'plain-skeleton').locator('rect[rx="4"]')).toHaveCount(5);
});

test('Verify theme changes the fill of Skeleton', {
tag: [TAG.PRIORITY_HIGH, '@skeleton'],
}, async ({ page }) => {
await loadPage(page, BASE_EXAMPLE, 'en', { theme: 'default' });
const defaultFill = await computed(locators.skeleton(page, 0), 'fill');

await loadPage(page, BASE_EXAMPLE, 'en', { theme: 'invert' });
const invertFill = await computed(locators.skeleton(page, 0), 'fill');

expect(defaultFill).not.toBe('');
expect(invertFill).not.toBe(defaultFill);
});

test('Verify observeParentSize prop styles', {
test('Verify theme changes the color of chart skeletons', {
tag: [TAG.PRIORITY_HIGH, '@skeleton'],
}, async ({ page }) => {
await loadPage(page, BASE_EXAMPLE, 'en', { theme: 'default' });
const defaultChart = locators.chart(page, 'LineChartSkeleton').getByRole('img');
await expect(defaultChart).toBeVisible();
const defaultBackground = await computed(defaultChart, 'background-color');

await loadPage(page, BASE_EXAMPLE, 'en', { theme: 'invert' });
const invertChart = locators.chart(page, 'LineChartSkeleton').getByRole('img');
await expect(invertChart).toBeVisible();
const invertBackground = await computed(invertChart, 'background-color');

expect(defaultBackground).not.toBe('');
expect(invertBackground).not.toBe(defaultBackground);
});

test('Verify observeParentSize follows the parent width', {
tag: [TAG.PRIORITY_MEDIUM, '@skeleton'],
}, async ({ page }) => {
await loadPage(page, 'stories/components/skeleton/tests/examples/observe-parent-size.tsx', 'en');
await loadPage(page, BASE_EXAMPLE, 'en', { observeParentSize: true, parentWidth: 200 });

const skeleton = locators.skeleton(page, 0);
await expect(skeleton).toHaveAttribute('width', '100%');

const narrow = (await skeleton.boundingBox())!.width;

await locators.area(page, 'skeleton-parent').evaluate((el) => {
(el as HTMLElement).style.width = '600px';
});

const styleAttr = await locators.skeleton(page, 1).getAttribute('style');
await expect(locators.skeleton(page, 0)).toHaveAttribute('width', '100%');
expect(styleAttr).toContain('300px');
await expect(async () => {
const wide = (await skeleton.boundingBox())!.width;
expect(wide).toBeGreaterThan(narrow);
}).toPass();

await locators.button(page).click();
await page.waitForTimeout(100);
await expect(locators.skeleton(page, 0)).toHaveAttribute('width', '100%');
expect(styleAttr).toContain('300px');
await expect(page).toHaveScreenshot();
await expect(skeleton).toHaveAttribute('width', '100%');
});

test('Verify usage with other elements styles', {
test('Verify w prop pins the skeleton width regardless of the parent', {
tag: [TAG.PRIORITY_MEDIUM, '@skeleton'],
}, async ({ page }) => {
await loadPage(page, 'stories/components/skeleton/docs/examples/usage_with_other_elements.tsx', 'en');
await loadPage(page, BASE_EXAMPLE, 'en', { w: 300, parentWidth: 200 });

await expect(page).toHaveScreenshot();
expect(await computed(locators.skeleton(page, 0), 'width')).toBe('300px');
});
});
Diff not rendered.
Diff not rendered.
Diff not rendered.
Diff not rendered.
Diff not rendered.
Diff not rendered.
Diff not rendered.
Diff not rendered.
Diff not rendered.
Diff not rendered.
Diff not rendered.
Diff not rendered.
Diff not rendered.
Diff not rendered.
Diff not rendered.
Diff not rendered.
Diff not rendered.
Diff not rendered.
Diff not rendered.
Diff not rendered.
Diff not rendered.
Diff not rendered.
Diff not rendered.
Diff not rendered.
Diff not rendered.
Diff not rendered.
Diff not rendered.
Diff not rendered.
Diff not rendered.
Diff not rendered.
Diff not rendered.
Diff not rendered.
Diff not rendered.
Diff not rendered.
Diff not rendered.
Diff not rendered.
Diff not rendered.
Diff not rendered.
Diff not rendered.
Diff not rendered.
Diff not rendered.
Diff not rendered.
Diff not rendered.
Diff not rendered.
Diff not rendered.
Diff not rendered.
Diff not rendered.
Diff not rendered.
Diff not rendered.
Diff not rendered.
Diff not rendered.
Diff not rendered.
Diff not rendered.
Diff not rendered.
Diff not rendered.
Loading
Loading