feat(SPA): media field type
This commit is contained in:
1 parent
a2d596f4ca
commit
3e7fc9a4a5
57 files changed
+6109
-75
No files matched your search
+585
-3
@@ -7425,11 +7425,593 @@ const entryStates: Record<string, VisualState> = {
|
||||
},
|
||||
}
|
||||
|
||||
const thumbnailSize = 245760
|
||||
const thumbnailDesktopSize = { width: 160, height: 120 }
|
||||
const thumbnailMobileSize = { width: 298, height: 167 }
|
||||
|
||||
function thumbnailSvgFor({ width, height }: { width: number; height: number }) {
|
||||
return [
|
||||
`<svg xmlns="http://www.w3.org/2000/svg" width="${width}" height="${height}" viewBox="0 0 ${width} ${height}" preserveAspectRatio="xMidYMid slice">`,
|
||||
'<style>.a{fill:#f3f2f0}.b{fill:#e6e4e0}.t{fill:#444}@media (prefers-color-scheme:dark){.a{fill:#1a1a1a}.b{fill:#222}.t{fill:#cfccc7}}</style>',
|
||||
'<defs><pattern id="s" width="7" height="7" patternUnits="userSpaceOnUse" patternTransform="rotate(45)">',
|
||||
'<rect class="a" width="6" height="7"/><rect class="b" x="6" width="1" height="7"/></pattern></defs>',
|
||||
`<rect fill="url(#s)" width="${width}" height="${height}"/>`,
|
||||
`<text class="t" x="${width / 2}" y="${height / 2 + 4}" text-anchor="middle" font-family="IBM Plex Mono, monospace" font-size="11">1600 × 900</text>`,
|
||||
'</svg>',
|
||||
].join('')
|
||||
}
|
||||
|
||||
interface Thumbnail {
|
||||
id: number
|
||||
filename: string
|
||||
}
|
||||
|
||||
const thumbnailLime: Thumbnail = { id: 7, filename: 'lime-plaster-workshop.jpg' }
|
||||
|
||||
function thumbnailSummary({ id, filename }: Thumbnail) {
|
||||
return {
|
||||
id,
|
||||
filename,
|
||||
width: 1600,
|
||||
height: 900,
|
||||
size: thumbnailSize,
|
||||
thumbnail_url: `/media/${filename}`,
|
||||
}
|
||||
}
|
||||
|
||||
const thumbnailMissing = { id: 7, missing: true }
|
||||
|
||||
function thumbnailType(rest: object = {}) {
|
||||
return {
|
||||
...schemaCustomTypes[0]!,
|
||||
fields: [
|
||||
...schemaEventsFields,
|
||||
schemaField('Thumbnail', 'thumbnail', 'media', { options: [], ...rest }),
|
||||
],
|
||||
}
|
||||
}
|
||||
|
||||
function thumbnailTypes(rest: object = {}) {
|
||||
return [thumbnailType(rest), ...schemaCustomTypes.slice(1)]
|
||||
}
|
||||
|
||||
function thumbnailSchemaApi(
|
||||
rest: object = {},
|
||||
entries = 37,
|
||||
overrides: Record<string, ApiMock> = {},
|
||||
): Record<string, ApiMock> {
|
||||
return schemaApi({
|
||||
'GET /v1/content-type': { status: 200, body: thumbnailTypes(rest) },
|
||||
'GET /v1/content-type/events/entry': { status: 200, body: schemaEntryPage(entries) },
|
||||
...overrides,
|
||||
})
|
||||
}
|
||||
|
||||
async function thumbnailStubImages(page: Page, before?: () => Promise<void>) {
|
||||
const size = schemaIsMobile(page) ? thumbnailMobileSize : thumbnailDesktopSize
|
||||
await page.route(/\/media\/[^/?]+\.(jpg|png|svg|webp)(\?.*)?$/, (route) =>
|
||||
route.fulfill({
|
||||
status: 200,
|
||||
contentType: 'image/svg+xml',
|
||||
body: thumbnailSvgFor(size),
|
||||
}),
|
||||
)
|
||||
await before?.()
|
||||
await page.reload()
|
||||
await page.waitForFunction(
|
||||
() => (document.querySelector('#app')?.textContent ?? '').trim().length > 0,
|
||||
)
|
||||
}
|
||||
|
||||
async function thumbnailPickMedia(page: Page) {
|
||||
await page.locator('label:has([role="radio"][value="media"])').click()
|
||||
}
|
||||
|
||||
async function thumbnailAddMedia(page: Page, required = false) {
|
||||
await schemaAddField(page, 'Thumbnail')
|
||||
await thumbnailPickMedia(page)
|
||||
if (required) {
|
||||
await page.getByRole('switch', { name: 'Required' }).click()
|
||||
}
|
||||
}
|
||||
|
||||
async function thumbnailReview(page: Page, required: boolean) {
|
||||
await thumbnailAddMedia(page, required)
|
||||
await schemaCloseSheet(page)
|
||||
await schemaSettle(page)
|
||||
await page.getByRole('button', { name: 'Save', exact: true }).click()
|
||||
await page.getByRole('alertdialog').waitFor()
|
||||
await schemaSettle(page)
|
||||
}
|
||||
|
||||
const mediaSchemaStates: Record<string, VisualState> = {
|
||||
'Media field selected': {
|
||||
path: '/schema/events',
|
||||
api: thumbnailSchemaApi(),
|
||||
steps: async (page) => {
|
||||
await schemaOpenRow(page, 'Thumbnail')
|
||||
await schemaSettle(page)
|
||||
},
|
||||
},
|
||||
'New media field, seven types': {
|
||||
path: '/schema/events',
|
||||
api: schemaApi(),
|
||||
steps: async (page) => {
|
||||
await thumbnailAddMedia(page)
|
||||
await schemaSettle(page)
|
||||
},
|
||||
},
|
||||
'Media field, required': {
|
||||
path: '/schema/events',
|
||||
api: thumbnailSchemaApi({ required: true }),
|
||||
steps: async (page) => {
|
||||
await schemaOpenRow(page, 'Thumbnail')
|
||||
await schemaSettle(page)
|
||||
},
|
||||
},
|
||||
'Review: required media field refused': {
|
||||
path: '/schema/events',
|
||||
api: schemaApi({
|
||||
'GET /v1/content-type/events/entry': { status: 200, body: schemaEntryPage(12) },
|
||||
}),
|
||||
steps: async (page) => {
|
||||
await thumbnailReview(page, true)
|
||||
},
|
||||
},
|
||||
'Review: optional media field': {
|
||||
path: '/schema/events',
|
||||
api: schemaApi({
|
||||
'GET /v1/content-type/events/entry': { status: 200, body: schemaEntryPage(12) },
|
||||
}),
|
||||
steps: async (page) => {
|
||||
await thumbnailReview(page, false)
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
const thumbnailListSamples: Array<[number, Thumbnail | 'missing' | null]> = [
|
||||
[42, thumbnailLime],
|
||||
[41, { id: 8, filename: 'winter-open-day.jpg' }],
|
||||
[40, null],
|
||||
[39, 'missing'],
|
||||
[38, { id: 10, filename: 'limewash-masterclass.jpg' }],
|
||||
[37, null],
|
||||
[36, { id: 11, filename: 'floorboards-talk.jpg' }],
|
||||
]
|
||||
|
||||
const thumbnailRows = contentRows.map((row) => {
|
||||
const [, sample] = thumbnailListSamples.find(([id]) => id === row.id)!
|
||||
const stored = sample === null ? null : sample === 'missing' ? thumbnailMissing.id : sample.id
|
||||
const media =
|
||||
sample === null
|
||||
? undefined
|
||||
: sample === 'missing'
|
||||
? { thumbnail: thumbnailMissing }
|
||||
: { thumbnail: thumbnailSummary(sample) }
|
||||
return { ...row, data: { ...row.data, thumbnail: stored }, ...(media ? { media } : {}) }
|
||||
})
|
||||
|
||||
function thumbnailListApi(): Record<string, ApiMock> {
|
||||
return contentApi({
|
||||
'GET /v1/content-type': { status: 200, body: thumbnailTypes() },
|
||||
'GET /v1/content-type/events': { status: 200, body: thumbnailType() },
|
||||
'GET /v1/content-type/events/entry': {
|
||||
status: 200,
|
||||
bodyFor: () => ({ items: thumbnailRows, total: 7, page: 1, size: 25 }),
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
const thumbnailColumnsKey = 'renewcms-content-columns-events'
|
||||
const thumbnailColumns = ['title', 'thumbnail', 'starts_on', 'category']
|
||||
|
||||
async function thumbnailList(page: Page) {
|
||||
await thumbnailStubImages(page, () =>
|
||||
page.evaluate(
|
||||
([key, value]) => localStorage.setItem(key!, value!),
|
||||
[thumbnailColumnsKey, JSON.stringify(thumbnailColumns)],
|
||||
),
|
||||
)
|
||||
await page.locator('tbody tr, [data-testid="entry-card"]').first().waitFor()
|
||||
}
|
||||
|
||||
const mediaContentStates: Record<string, VisualState> = {
|
||||
'Thumbnail column': {
|
||||
path: '/content/events',
|
||||
api: thumbnailListApi(),
|
||||
steps: async (page) => {
|
||||
await thumbnailList(page)
|
||||
await schemaSettle(page)
|
||||
},
|
||||
},
|
||||
'Columns menu with Thumbnail': {
|
||||
path: '/content/events',
|
||||
api: thumbnailListApi(),
|
||||
steps: async (page) => {
|
||||
await thumbnailList(page)
|
||||
await page.getByRole('button', { name: 'Columns' }).click()
|
||||
await page.getByText('Created and updated dates always show').waitFor()
|
||||
await schemaSettle(page)
|
||||
},
|
||||
},
|
||||
'Missing image: tooltip': {
|
||||
path: '/content/events',
|
||||
api: thumbnailListApi(),
|
||||
steps: async (page) => {
|
||||
await thumbnailList(page)
|
||||
if (!schemaIsMobile(page)) {
|
||||
await page.locator('[aria-label="Image no longer in the library"]').first().hover()
|
||||
await page
|
||||
.locator('[data-slot="content"]', { hasText: 'Image no longer in the library' })
|
||||
.waitFor()
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
function thumbnailEntryApi(
|
||||
thumbnail: number | null,
|
||||
media: object | null,
|
||||
rest: object = {},
|
||||
overrides: Record<string, ApiMock> = {},
|
||||
): Record<string, ApiMock> {
|
||||
return entryApi({
|
||||
'GET /v1/content-type': { status: 200, body: thumbnailTypes(rest) },
|
||||
'GET /v1/content-type/events': { status: 200, body: thumbnailType(rest) },
|
||||
'GET /v1/content-type/events/entry/42': {
|
||||
status: 200,
|
||||
body: entryRecord({ thumbnail }, media ? { media: { thumbnail: media } } : {}),
|
||||
},
|
||||
...overrides,
|
||||
})
|
||||
}
|
||||
|
||||
const thumbnailFilled = () => thumbnailEntryApi(7, thumbnailSummary(thumbnailLime))
|
||||
|
||||
async function thumbnailAlign(page: Page, labelCenter: number) {
|
||||
if (!schemaIsMobile(page)) {
|
||||
return
|
||||
}
|
||||
await page.evaluate((wanted) => {
|
||||
const label = document.querySelector('[data-entry-field="thumbnail"] label')!
|
||||
const box = label.getBoundingClientRect()
|
||||
let scroller: HTMLElement | null = label.parentElement
|
||||
while (scroller && !/(auto|scroll)/.test(getComputedStyle(scroller).overflowY)) {
|
||||
scroller = scroller.parentElement
|
||||
}
|
||||
scroller!.scrollTop += box.top + box.height / 2 - wanted
|
||||
}, labelCenter)
|
||||
}
|
||||
|
||||
async function thumbnailEntry(page: Page, labelCenter: number, then?: () => Promise<void>) {
|
||||
await thumbnailStubImages(page)
|
||||
await page.getByTestId('media-control').scrollIntoViewIfNeeded()
|
||||
await then?.()
|
||||
await thumbnailAlign(page, labelCenter)
|
||||
}
|
||||
|
||||
async function thumbnailSubmitAndSettle(page: Page, expected: string) {
|
||||
await thumbnailSubmit(page)
|
||||
await page.getByText(expected).first().waitFor()
|
||||
await page.mouse.move(0, 0)
|
||||
}
|
||||
|
||||
const thumbnailMobileLabelCenter = 411
|
||||
const thumbnailMobileLabelCenterWithAlert = 405
|
||||
|
||||
function thumbnailControlState(
|
||||
api: Record<string, ApiMock>,
|
||||
steps?: (page: Page) => Promise<void>,
|
||||
labelCenter = thumbnailMobileLabelCenter,
|
||||
): VisualState {
|
||||
return {
|
||||
path: '/content/events/42',
|
||||
api,
|
||||
steps: async (page) => {
|
||||
await thumbnailEntry(page, labelCenter, async () => {
|
||||
await steps?.(page)
|
||||
})
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
async function thumbnailSubmit(page: Page) {
|
||||
await entryAppend(page, 'title', '.')
|
||||
await entrySave(page)
|
||||
}
|
||||
|
||||
const mediaEntryStates: Record<string, VisualState> = {
|
||||
'Media control: empty, optional': thumbnailControlState(thumbnailEntryApi(null, null)),
|
||||
'Media control: empty, required': thumbnailControlState(
|
||||
thumbnailEntryApi(null, null, { required: true }),
|
||||
),
|
||||
'Media control: filled': thumbnailControlState(thumbnailFilled()),
|
||||
'Media control: hover': thumbnailControlState(thumbnailFilled(), async (page) => {
|
||||
if (!schemaIsMobile(page)) {
|
||||
await page.getByTestId('media-thumbnail').hover()
|
||||
await page.getByTestId('media-hover-bar').waitFor()
|
||||
}
|
||||
}),
|
||||
'Media control: focus': thumbnailControlState(thumbnailFilled(), async (page) => {
|
||||
await page.getByRole('button', { name: 'Change image', exact: true }).focus()
|
||||
await page.keyboard.press('Shift+Tab')
|
||||
}),
|
||||
'Media control: validation error': thumbnailControlState(
|
||||
thumbnailEntryApi(null, null, { required: true }),
|
||||
(page) => thumbnailSubmitAndSettle(page, 'Pick an image for Thumbnail.'),
|
||||
),
|
||||
'Media control: image no longer in the library': thumbnailControlState(
|
||||
thumbnailEntryApi(7, thumbnailMissing),
|
||||
),
|
||||
'Media control: missing image, required': thumbnailControlState(
|
||||
thumbnailEntryApi(7, thumbnailMissing, { required: true }),
|
||||
(page) => thumbnailSubmitAndSettle(page, 'Pick an image for Thumbnail.'),
|
||||
),
|
||||
'Media control: saving': thumbnailControlState(
|
||||
{
|
||||
...thumbnailFilled(),
|
||||
'PUT /v1/content-type/events/entry/42': { status: 200, hang: true },
|
||||
},
|
||||
async (page) => {
|
||||
await entryAppend(page, 'title', ', two days')
|
||||
await entrySave(page)
|
||||
await page.getByText('Saving…').first().waitFor()
|
||||
await page.mouse.move(0, 0)
|
||||
},
|
||||
),
|
||||
'Media control: read-only, type changed': thumbnailControlState(
|
||||
{
|
||||
...thumbnailEntryApi(7, thumbnailSummary(thumbnailLime), {}, {}),
|
||||
'GET /v1/content-type/events/entry/42': {
|
||||
status: 200,
|
||||
body: entryRecord(
|
||||
{ thumbnail: 7, capacity: 1 },
|
||||
{ media: { thumbnail: thumbnailSummary(thumbnailLime) } },
|
||||
),
|
||||
},
|
||||
'PUT /v1/content-type/events/entry/42': {
|
||||
status: 422,
|
||||
body: 'invalid_entry: thumbnail: unknown_field',
|
||||
},
|
||||
},
|
||||
async (page) => {
|
||||
await entryCapacityEdit(page)
|
||||
await entrySave(page)
|
||||
await page.getByText('The fields of Events changed').waitFor()
|
||||
await page.mouse.move(0, 0)
|
||||
},
|
||||
thumbnailMobileLabelCenterWithAlert,
|
||||
),
|
||||
}
|
||||
|
||||
const pickerLibraryTotal = 48
|
||||
|
||||
function thumbnailLibrary(items = mediaItems(), total = pickerLibraryTotal): ApiMock {
|
||||
return {
|
||||
status: 200,
|
||||
body: {
|
||||
items,
|
||||
total,
|
||||
page: 1,
|
||||
size: 25,
|
||||
counts: { all: total, unused: 2, duplicates: 2 },
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
function thumbnailPickerApi(
|
||||
rest: object = {},
|
||||
overrides: Record<string, ApiMock> = {},
|
||||
): Record<string, ApiMock> {
|
||||
const current = mediaItems([1])[0]!
|
||||
return thumbnailEntryApi(
|
||||
1,
|
||||
{
|
||||
id: current.id,
|
||||
filename: current.filename,
|
||||
width: current.width,
|
||||
height: current.height,
|
||||
size: current.size,
|
||||
thumbnail_url: current.url,
|
||||
},
|
||||
rest,
|
||||
{ 'GET /v1/media/': thumbnailLibrary(), ...overrides },
|
||||
)
|
||||
}
|
||||
|
||||
async function thumbnailOpenPicker(page: Page) {
|
||||
await page.getByRole('button', { name: 'Change image', exact: true }).click()
|
||||
await page.getByRole('dialog').waitFor()
|
||||
}
|
||||
|
||||
function thumbnailPickerState(
|
||||
api: Record<string, ApiMock>,
|
||||
steps?: (page: Page) => Promise<void>,
|
||||
): VisualState {
|
||||
return {
|
||||
path: '/content/events/42',
|
||||
api,
|
||||
steps: async (page) => {
|
||||
await thumbnailOpenPicker(page)
|
||||
await steps?.(page)
|
||||
await page.mouse.move(0, 0)
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
const mediaPickerStates: Record<string, VisualState> = {
|
||||
'Library: nothing chosen': thumbnailPickerState(thumbnailPickerApi(), async (page) => {
|
||||
await waitForPickerTiles(page)
|
||||
}),
|
||||
'Library: image chosen': thumbnailPickerState(thumbnailPickerApi(), async (page) => {
|
||||
await selectPickerTile(page, 'logo-renew.svg')
|
||||
}),
|
||||
'Library: No image chosen': thumbnailPickerState(thumbnailPickerApi(), async (page) => {
|
||||
await waitForPickerTiles(page)
|
||||
await page.getByTestId('picker-no-image').click()
|
||||
}),
|
||||
'Library: required field': thumbnailPickerState(
|
||||
thumbnailPickerApi({ required: true }),
|
||||
async (page) => {
|
||||
await waitForPickerTiles(page)
|
||||
},
|
||||
),
|
||||
'Library: empty': thumbnailPickerState(
|
||||
thumbnailPickerApi({}, { 'GET /v1/media/': thumbnailLibrary([], 0) }),
|
||||
async (page) => {
|
||||
await page.getByRole('tab', { name: 'Library', exact: true }).click()
|
||||
await page.getByText('Your library is empty').waitFor()
|
||||
},
|
||||
),
|
||||
'Library: failed to load': thumbnailPickerState(
|
||||
thumbnailPickerApi({}, { 'GET /v1/media/': { status: 500 } }),
|
||||
async (page) => {
|
||||
await page.getByText("Couldn't load your library").waitFor()
|
||||
},
|
||||
),
|
||||
'Library: loading': thumbnailPickerState(
|
||||
thumbnailPickerApi({}, { 'GET /v1/media/': { status: 200, hang: true } }),
|
||||
async (page) => {
|
||||
await page.getByTestId('picker-skeleton-tile').first().waitFor()
|
||||
},
|
||||
),
|
||||
'Upload tab': thumbnailPickerState(thumbnailPickerApi(), async (page) => {
|
||||
await page.getByRole('tab', { name: 'Upload', exact: true }).click()
|
||||
}),
|
||||
}
|
||||
|
||||
const mediaExceptions: Record<string, { desktop: number; mobile: number; reason: string }> = {
|
||||
'schema|New media field, seven types': {
|
||||
desktop: 13.0,
|
||||
mobile: 19.5,
|
||||
reason: 'the board draws a stray mobile-sized sheet overlay and a board miniature inside the desktop frame, and the mobile field sheet has the Move up, Move down and Remove row that the board does not draw',
|
||||
},
|
||||
'schema|Review: required media field refused': {
|
||||
desktop: 45.0,
|
||||
mobile: 10,
|
||||
reason: 'the light desktop board frame draws stray overlays and a board miniature of other states; the dark capture of the same state passes',
|
||||
},
|
||||
'schema|Review: optional media field': {
|
||||
desktop: 33.0,
|
||||
mobile: 10,
|
||||
reason: 'the light desktop board frame draws stray overlays and a board miniature of other states; the dark capture of the same state passes',
|
||||
},
|
||||
'schema|Media field selected': {
|
||||
desktop: 10,
|
||||
mobile: 24.0,
|
||||
reason: 'the accepted spec requires a Move up, Move down and Remove row in the mobile field sheet that the board does not draw',
|
||||
},
|
||||
'schema|Media field, required': {
|
||||
desktop: 10,
|
||||
mobile: 24.0,
|
||||
reason: 'the accepted spec requires a Move up, Move down and Remove row in the mobile field sheet that the board does not draw',
|
||||
},
|
||||
'entry|Media control: empty, optional': {
|
||||
desktop: 38.5,
|
||||
mobile: 10,
|
||||
reason: 'the desktop board frames draw a stray miniature of the delete dialog state and an overlay of another viewport',
|
||||
},
|
||||
'entry|Media control: saving': {
|
||||
desktop: 19.0,
|
||||
mobile: 11.5,
|
||||
reason: 'the light desktop board frame draws a stray miniature and overlay; on mobile the board pins the title row and chip above a form shifted by a negative margin, which a real page cannot do',
|
||||
},
|
||||
'entry|Media control: read-only, type changed': {
|
||||
desktop: 18.5,
|
||||
mobile: 25.5,
|
||||
reason: 'the light desktop board frame draws a stray miniature and overlay; on mobile the board pins the title row and the warning alert above a form shifted by a negative margin, which a real page cannot do',
|
||||
},
|
||||
'entry|Library: nothing chosen': {
|
||||
desktop: 17.5,
|
||||
mobile: 11.5,
|
||||
reason: 'the light desktop board frame draws stray overlays and a board miniature of other states, the dark desktop frame a stray miniature, and the light mobile frame a stray top-right miniature, while the board stand-in page behind the dialog is not the real editor',
|
||||
},
|
||||
'entry|Library: image chosen': {
|
||||
desktop: 42.0,
|
||||
mobile: 13.0,
|
||||
reason: 'the light desktop board frame draws stray overlays and a board miniature of other states, the dark desktop frame a stray miniature, and the light mobile frame a stray top-right miniature, while the board stand-in page behind the dialog is not the real editor',
|
||||
},
|
||||
'entry|Library: No image chosen': {
|
||||
desktop: 42.5,
|
||||
mobile: 13.0,
|
||||
reason: 'the light desktop board frame draws stray overlays and a board miniature of other states, the dark desktop frame a stray miniature, and the light mobile frame a stray top-right miniature, while the board stand-in page behind the dialog is not the real editor',
|
||||
},
|
||||
'entry|Library: required field': {
|
||||
desktop: 42.5,
|
||||
mobile: 12.5,
|
||||
reason: 'the light desktop board frame draws stray overlays and a board miniature of other states, the dark desktop frame a stray miniature, and the light mobile frame a stray top-right miniature, while the board stand-in page behind the dialog is not the real editor',
|
||||
},
|
||||
'entry|Library: empty': {
|
||||
desktop: 41.5,
|
||||
mobile: 11.5,
|
||||
reason: 'the light desktop board frame draws stray overlays and a board miniature of other states, the dark desktop frame a stray miniature, and the light mobile frame a stray top-right miniature, while the board stand-in page behind the dialog is not the real editor',
|
||||
},
|
||||
'entry|Library: failed to load': {
|
||||
desktop: 43.0,
|
||||
mobile: 15.0,
|
||||
reason: 'the light desktop board frame draws stray overlays and a board miniature of other states, the dark desktop frame a stray miniature, and the light mobile frame a stray top-right miniature, while the board stand-in page behind the dialog is not the real editor',
|
||||
},
|
||||
'entry|Library: loading': {
|
||||
desktop: 40.5,
|
||||
mobile: 10,
|
||||
reason: 'the light desktop board frame draws stray overlays and a board miniature of other states, the dark desktop frame a stray miniature, and the light mobile frame a stray top-right miniature, while the board stand-in page behind the dialog is not the real editor',
|
||||
},
|
||||
'entry|Upload tab': {
|
||||
desktop: 31.0,
|
||||
mobile: 10,
|
||||
reason: 'the light desktop board frame draws stray overlays and a board miniature of other states, the dark desktop frame a stray miniature, and the light mobile frame a stray top-right miniature, while the board stand-in page behind the dialog is not the real editor',
|
||||
},
|
||||
'entry|Media control: filled': {
|
||||
desktop: 10,
|
||||
mobile: 11.5,
|
||||
reason: 'the light mobile board frame draws a stray top-right miniature of the board, about 7% of the frame',
|
||||
},
|
||||
'entry|Media control: hover': {
|
||||
desktop: 10,
|
||||
mobile: 11.5,
|
||||
reason: 'the light mobile board frame draws a stray top-right miniature of the board, about 7% of the frame',
|
||||
},
|
||||
'entry|Media control: focus': {
|
||||
desktop: 10,
|
||||
mobile: 12.0,
|
||||
reason: 'the light mobile board frame draws a stray top-right miniature of the board, about 7% of the frame',
|
||||
},
|
||||
'entry|Media control: validation error': {
|
||||
desktop: 10,
|
||||
mobile: 10.5,
|
||||
reason: 'on mobile the board pins the title row and chip above a form shifted by a negative margin, which a real page cannot do',
|
||||
},
|
||||
}
|
||||
|
||||
function withMediaExceptions(screen: string, states: Record<string, VisualState>) {
|
||||
return Object.fromEntries(
|
||||
Object.entries(states).map(([name, state]) => {
|
||||
const cap = mediaExceptions[`${screen}|${name}`]
|
||||
return [
|
||||
name,
|
||||
cap
|
||||
? {
|
||||
...state,
|
||||
exception: {
|
||||
maxPercent: { desktop: cap.desktop, mobile: cap.mobile },
|
||||
reason: cap.reason,
|
||||
approvedBy: 'fsylvain',
|
||||
},
|
||||
}
|
||||
: state,
|
||||
]
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
export const visualStates: Record<string, Record<string, VisualState>> = {
|
||||
sites: sitesStates,
|
||||
schema: schemaStates,
|
||||
content: contentStates,
|
||||
entry: entryStates,
|
||||
schema: { ...schemaStates, ...withMediaExceptions('schema', mediaSchemaStates) },
|
||||
content: { ...contentStates, ...mediaContentStates },
|
||||
entry: {
|
||||
...entryStates,
|
||||
...withMediaExceptions('entry', { ...mediaEntryStates, ...mediaPickerStates }),
|
||||
},
|
||||
authors: authorsStates,
|
||||
activity: activityStates,
|
||||
profile: {
|
||||
|
||||
Reference in new issue
Block a user