修复深色说明样式归属与实名认证上传字段标签
This commit is contained in:
1 parent
56188874cb
commit
21821ec6fa
4 files changed
+383
-17
No files matched your search
@@ -10,18 +10,19 @@
|
|||||||
line-height: 1.8;
|
line-height: 1.8;
|
||||||
}
|
}
|
||||||
.description-panel[data-v-8a6cfb44] {
|
.description-panel[data-v-8a6cfb44] {
|
||||||
background: #fff;
|
/* 面板与文字使用同一主题协议,紧凑预览和完整说明都不能固定为浅色配色。 */
|
||||||
border-top: 1px solid #e2e9f3;
|
background: var(--p-content-background);
|
||||||
border-bottom: 1px solid #dfe6f0;
|
border-top: 1px solid var(--p-content-border-color);
|
||||||
|
border-bottom: 1px solid var(--p-content-border-color);
|
||||||
padding: 0;
|
padding: 0;
|
||||||
}
|
}
|
||||||
.description-header[data-v-8a6cfb44] {
|
.description-header[data-v-8a6cfb44] {
|
||||||
background: #f5f8fe;
|
background: var(--p-content-hover-background);
|
||||||
border-bottom: 1px solid #dfe6f0;
|
border-bottom: 1px solid var(--p-content-border-color);
|
||||||
padding: 8px clamp(20px, 8.6%, 110px);
|
padding: 8px clamp(20px, 8.6%, 110px);
|
||||||
}
|
}
|
||||||
.description-header h2[data-v-8a6cfb44] {
|
.description-header h2[data-v-8a6cfb44] {
|
||||||
color: #657286;
|
color: var(--p-text-muted-color);
|
||||||
margin: 0;
|
margin: 0;
|
||||||
font-size: 21px;
|
font-size: 21px;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
@@ -42,7 +43,7 @@
|
|||||||
display: grid;
|
display: grid;
|
||||||
}
|
}
|
||||||
.description-field dt[data-v-8a6cfb44] {
|
.description-field dt[data-v-8a6cfb44] {
|
||||||
color: #708096;
|
color: var(--p-text-muted-color);
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 16px;
|
gap: 16px;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
@@ -56,7 +57,7 @@
|
|||||||
font-size: 18px;
|
font-size: 18px;
|
||||||
}
|
}
|
||||||
.description-field dd[data-v-8a6cfb44] {
|
.description-field dd[data-v-8a6cfb44] {
|
||||||
color: #394658;
|
color: var(--p-text-color);
|
||||||
white-space: pre-line;
|
white-space: pre-line;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
|
|||||||
@@ -89,13 +89,13 @@
|
|||||||
.calc-row.total > b[data-v-e4be3b52] {
|
.calc-row.total > b[data-v-e4be3b52] {
|
||||||
color: var(--p-text-color);
|
color: var(--p-text-color);
|
||||||
}
|
}
|
||||||
/* 应用深色主题覆盖,保持在基础颜色之后;主题切换只改变外观,不改变业务状态。 */
|
/* 深色提示仅属于本弹窗;首页公告也使用 notice-title,不能让覆盖跨越组件作用域。 */
|
||||||
.app-dark .notice {
|
.app-dark .notice[data-v-e4be3b52] {
|
||||||
background: #172554;
|
background: #172554;
|
||||||
border-color: #1e3a8a;
|
border-color: #1e3a8a;
|
||||||
}
|
}
|
||||||
/* 应用深色主题覆盖,保持在基础颜色之后;主题切换只改变外观,不改变业务状态。 */
|
/* 应用深色主题覆盖,保持在基础颜色之后;主题切换只改变外观,不改变业务状态。 */
|
||||||
.app-dark .notice ul,
|
.app-dark .notice ul[data-v-e4be3b52],
|
||||||
.app-dark .notice-title {
|
.app-dark .notice-title[data-v-e4be3b52] {
|
||||||
color: #bfdbfe;
|
color: #bfdbfe;
|
||||||
}
|
}
|
||||||
@@ -4,6 +4,7 @@ import "~/assets/styles/UploadField.BbTCTv6C.css";
|
|||||||
import { toDisplayString } from "vue";
|
import { toDisplayString } from "vue";
|
||||||
import { mergeModels } from "vue";
|
import { mergeModels } from "vue";
|
||||||
import { useModel } from "vue";
|
import { useModel } from "vue";
|
||||||
|
import { useId } from "vue";
|
||||||
import { ref } from "vue";
|
import { ref } from "vue";
|
||||||
import { createCommentVNode } from "vue";
|
import { createCommentVNode } from "vue";
|
||||||
import { createElementBlock } from "vue";
|
import { createElementBlock } from "vue";
|
||||||
@@ -18,13 +19,10 @@ import { t as _plugin_vue_export_helper_t } from "~/lib/_plugin-vue_export-helpe
|
|||||||
var m = {
|
var m = {
|
||||||
class: `field`,
|
class: `field`,
|
||||||
},
|
},
|
||||||
h = {
|
|
||||||
key: 0,
|
|
||||||
},
|
|
||||||
g = {
|
g = {
|
||||||
class: `row`,
|
class: `row`,
|
||||||
},
|
},
|
||||||
_ = [`accept`, `disabled`],
|
_ = [`id`, `accept`, `disabled`],
|
||||||
v = {
|
v = {
|
||||||
key: 0,
|
key: 0,
|
||||||
class: `state`,
|
class: `state`,
|
||||||
@@ -62,6 +60,8 @@ var m = {
|
|||||||
let p = useModel(t, `modelValue`),
|
let p = useModel(t, `modelValue`),
|
||||||
S = u,
|
S = u,
|
||||||
C = useApi(),
|
C = useApi(),
|
||||||
|
// 每个实例有独立关联 ID,多张证件同时上传时标签仍指向各自的原生输入框。
|
||||||
|
L = useId(),
|
||||||
w = ref(!1),
|
w = ref(!1),
|
||||||
T = ref(``),
|
T = ref(``),
|
||||||
E = ref(``);
|
E = ref(``);
|
||||||
@@ -90,13 +90,20 @@ var m = {
|
|||||||
createElementBlock(`div`, m, [
|
createElementBlock(`div`, m, [
|
||||||
t.label
|
t.label
|
||||||
? (openBlock(),
|
? (openBlock(),
|
||||||
createElementBlock(`label`, h, toDisplayString(t.label), 1))
|
createElementBlock(
|
||||||
|
`label`,
|
||||||
|
{ key: 0, for: L },
|
||||||
|
toDisplayString(t.label),
|
||||||
|
9,
|
||||||
|
[`for`],
|
||||||
|
))
|
||||||
: createCommentVNode(``, !0),
|
: createCommentVNode(``, !0),
|
||||||
createElementVNode(`div`, g, [
|
createElementVNode(`div`, g, [
|
||||||
createElementVNode(
|
createElementVNode(
|
||||||
`input`,
|
`input`,
|
||||||
{
|
{
|
||||||
type: `file`,
|
type: `file`,
|
||||||
|
id: L,
|
||||||
accept: t.accept,
|
accept: t.accept,
|
||||||
disabled: unref(w),
|
disabled: unref(w),
|
||||||
onChange: D,
|
onChange: D,
|
||||||
@@ -147,6 +154,7 @@ var m = {
|
|||||||
{
|
{
|
||||||
type: `button`,
|
type: `button`,
|
||||||
class: `rm`,
|
class: `rm`,
|
||||||
|
"aria-label": `移除${t.label || `已上传文件`}`,
|
||||||
onClick: O,
|
onClick: O,
|
||||||
},
|
},
|
||||||
[
|
[
|
||||||
@@ -155,12 +163,15 @@ var m = {
|
|||||||
`i`,
|
`i`,
|
||||||
{
|
{
|
||||||
class: `pi pi-times`,
|
class: `pi pi-times`,
|
||||||
|
"aria-hidden": `true`,
|
||||||
},
|
},
|
||||||
null,
|
null,
|
||||||
-1,
|
-1,
|
||||||
),
|
),
|
||||||
]),
|
]),
|
||||||
],
|
],
|
||||||
|
8,
|
||||||
|
[`aria-label`],
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
64,
|
64,
|
||||||
|
|||||||
@@ -0,0 +1,354 @@
|
|||||||
|
/** 主题和字段回归经真实商品、首页、实名认证与实例入口验证;只替换外部 HTTP 响应。 */
|
||||||
|
import assert from "node:assert/strict";
|
||||||
|
import test from "node:test";
|
||||||
|
import { backendOrigin, login, scenario } from "./helpers/console-browser.mjs";
|
||||||
|
|
||||||
|
async function boundary(page, responses) {
|
||||||
|
const requests = [];
|
||||||
|
await page
|
||||||
|
.context()
|
||||||
|
.route(`${backendOrigin}/console/v1/**`, async (route) => {
|
||||||
|
const request = route.request();
|
||||||
|
const path = new URL(request.url()).pathname.replace(
|
||||||
|
/^\/console\/v1/,
|
||||||
|
"",
|
||||||
|
);
|
||||||
|
if (!Object.hasOwn(responses, path)) return route.fallback();
|
||||||
|
requests.push({
|
||||||
|
path,
|
||||||
|
authenticated: Boolean(request.headers().authorization),
|
||||||
|
multipart:
|
||||||
|
request
|
||||||
|
.headers()
|
||||||
|
["content-type"]?.startsWith("multipart/form-data; boundary=") ===
|
||||||
|
true,
|
||||||
|
});
|
||||||
|
await route.fulfill({
|
||||||
|
json: { status: 200, msg: "", data: responses[path] },
|
||||||
|
headers: { "Access-Control-Allow-Origin": "*" },
|
||||||
|
});
|
||||||
|
});
|
||||||
|
return requests;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function textPresentation(locator) {
|
||||||
|
return locator.evaluate((element) => {
|
||||||
|
const style = getComputedStyle(element);
|
||||||
|
const rgb = (value) => value.match(/[\d.]+/g).map(Number);
|
||||||
|
let background = [255, 255, 255];
|
||||||
|
const layers = [];
|
||||||
|
for (let owner = element; owner; owner = owner.parentElement)
|
||||||
|
layers.push(rgb(getComputedStyle(owner).backgroundColor));
|
||||||
|
for (const layer of layers.reverse()) {
|
||||||
|
const alpha = layer[3] ?? 1;
|
||||||
|
background = background.map(
|
||||||
|
(channel, index) => channel * (1 - alpha) + layer[index] * alpha,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
const foreground = rgb(style.color);
|
||||||
|
const luminance = (color) =>
|
||||||
|
color
|
||||||
|
.slice(0, 3)
|
||||||
|
.map((channel) => {
|
||||||
|
const value = channel / 255;
|
||||||
|
return value <= 0.04045
|
||||||
|
? value / 12.92
|
||||||
|
: ((value + 0.055) / 1.055) ** 2.4;
|
||||||
|
})
|
||||||
|
.reduce(
|
||||||
|
(total, value, index) =>
|
||||||
|
total + value * [0.2126, 0.7152, 0.0722][index],
|
||||||
|
0,
|
||||||
|
);
|
||||||
|
const light = luminance(foreground);
|
||||||
|
const dark = luminance(background);
|
||||||
|
const tokenColor = new Option().style;
|
||||||
|
tokenColor.color = style.getPropertyValue("--p-text-color");
|
||||||
|
return {
|
||||||
|
color: style.color,
|
||||||
|
textColor: tokenColor.color,
|
||||||
|
background: background.map(Math.round),
|
||||||
|
contrast: (Math.max(light, dark) + 0.05) / (Math.min(light, dark) + 0.05),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
test(
|
||||||
|
"dark structured catalog descriptions keep readable field text",
|
||||||
|
{ timeout: 25000 },
|
||||||
|
(t) =>
|
||||||
|
scenario(t, {}, async (page) => {
|
||||||
|
await page.emulateMedia({ colorScheme: "dark" });
|
||||||
|
await boundary(page, {
|
||||||
|
"/product/group/first": { list: [{ id: 1, name: "云产品" }] },
|
||||||
|
"/product/group/second": {
|
||||||
|
list: [{ id: 2, parent_id: 1, name: "测试分类", description: "" }],
|
||||||
|
},
|
||||||
|
"/product": {
|
||||||
|
list: [
|
||||||
|
{
|
||||||
|
id: 123,
|
||||||
|
name: "布局验收商品",
|
||||||
|
module: "idcsmart_common",
|
||||||
|
res_module: "idcsmart_common",
|
||||||
|
pay_type: "recurring_prepayment",
|
||||||
|
price: "10.00",
|
||||||
|
billing_cycle: "monthly",
|
||||||
|
stock_control: 0,
|
||||||
|
qty: 10,
|
||||||
|
order_allowed: true,
|
||||||
|
description: "配置:4GB 内存\n线路:BGP",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
count: 1,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
await login(page, "/products?g=1&sg=2");
|
||||||
|
const value = page
|
||||||
|
.locator(".description-field dd")
|
||||||
|
.filter({ hasText: "4GB" });
|
||||||
|
const label = page
|
||||||
|
.locator(".description-field dt")
|
||||||
|
.filter({ hasText: "配置" });
|
||||||
|
await value.waitFor();
|
||||||
|
await label.waitFor();
|
||||||
|
assert.ok(
|
||||||
|
(await textPresentation(value)).contrast >= 4.5,
|
||||||
|
"Structured description values must remain readable in dark mode",
|
||||||
|
);
|
||||||
|
assert.ok(
|
||||||
|
(await textPresentation(label)).contrast >= 4.5,
|
||||||
|
"Structured description labels must remain readable in dark mode",
|
||||||
|
);
|
||||||
|
return { structuredDescriptionReadable: true };
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
test(
|
||||||
|
"dark withdrawal notices do not recolor the home announcement title",
|
||||||
|
{ timeout: 25000 },
|
||||||
|
(t) =>
|
||||||
|
scenario(t, {}, async (page) => {
|
||||||
|
await page.emulateMedia({ colorScheme: "dark" });
|
||||||
|
await boundary(page, {
|
||||||
|
"/announcement": {
|
||||||
|
list: [{ id: 1, title: "本地公告", create_time: 1700000000 }],
|
||||||
|
count: 1,
|
||||||
|
},
|
||||||
|
"/withdraw/rule/credit": {
|
||||||
|
status: 1,
|
||||||
|
method: [{ id: 0, name: "账户余额", no_account: true }],
|
||||||
|
min: "1.00",
|
||||||
|
max: "100.00",
|
||||||
|
withdraw_fee_type: "fixed",
|
||||||
|
withdraw_fee: "0.00",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
await login(page, "/");
|
||||||
|
const announcement = page
|
||||||
|
.locator(".home-row-title")
|
||||||
|
.filter({ hasText: "本地公告" });
|
||||||
|
await announcement.waitFor();
|
||||||
|
const home = await textPresentation(announcement);
|
||||||
|
assert.equal(
|
||||||
|
home.color,
|
||||||
|
home.textColor,
|
||||||
|
"The announcement title owns its normal text color",
|
||||||
|
);
|
||||||
|
await page.getByRole("button", { name: "提现", exact: true }).click();
|
||||||
|
const dialog = page.getByRole("dialog", { name: "提现", exact: true });
|
||||||
|
const notice = dialog.locator(".notice-title");
|
||||||
|
await notice.waitFor();
|
||||||
|
const ownNotice = await textPresentation(notice);
|
||||||
|
assert.ok(
|
||||||
|
ownNotice.contrast >= 4.5,
|
||||||
|
"Withdrawal instructions must remain readable",
|
||||||
|
);
|
||||||
|
assert.notEqual(
|
||||||
|
ownNotice.color,
|
||||||
|
home.color,
|
||||||
|
"The withdrawal notice retains its own emphasis color",
|
||||||
|
);
|
||||||
|
assert.equal((await textPresentation(announcement)).color, home.color);
|
||||||
|
return {
|
||||||
|
homeAnnouncementColorOwned: true,
|
||||||
|
withdrawalNoticeReadable: true,
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
test(
|
||||||
|
"real-name upload fields expose distinct labels and named removal actions",
|
||||||
|
{ timeout: 25000 },
|
||||||
|
(t) =>
|
||||||
|
scenario(t, {}, async (page) => {
|
||||||
|
const requests = await boundary(page, {
|
||||||
|
"/certification/info": {
|
||||||
|
certification_open: 1,
|
||||||
|
certification_company_open: 1,
|
||||||
|
certification_upload: 1,
|
||||||
|
certification_company_need_person: 0,
|
||||||
|
is_certification: 0,
|
||||||
|
person: { status: 0 },
|
||||||
|
company: { status: 0 },
|
||||||
|
},
|
||||||
|
"/upload": { save_name: "identity-front.png" },
|
||||||
|
});
|
||||||
|
await login(page, "/account/realname/personal");
|
||||||
|
const front = page.getByLabel("身份证正面照 *", { exact: true });
|
||||||
|
const back = page.getByLabel("身份证反面照 *", { exact: true });
|
||||||
|
await front.waitFor();
|
||||||
|
await back.waitFor();
|
||||||
|
assert.notEqual(
|
||||||
|
await front.getAttribute("id"),
|
||||||
|
await back.getAttribute("id"),
|
||||||
|
);
|
||||||
|
const file = {
|
||||||
|
name: "identity-front.png",
|
||||||
|
mimeType: "image/png",
|
||||||
|
buffer: Buffer.from("local-e2e-upload-not-identity-data"),
|
||||||
|
};
|
||||||
|
await front.setInputFiles(file);
|
||||||
|
const remove = page.getByRole("button", {
|
||||||
|
name: "移除身份证正面照 *",
|
||||||
|
exact: true,
|
||||||
|
});
|
||||||
|
await remove.waitFor();
|
||||||
|
assert.deepEqual(
|
||||||
|
requests.filter((request) => request.path === "/upload"),
|
||||||
|
[{ path: "/upload", authenticated: true, multipart: true }],
|
||||||
|
);
|
||||||
|
await remove.click();
|
||||||
|
assert.equal(await remove.count(), 0);
|
||||||
|
await front.setInputFiles(file);
|
||||||
|
await remove.waitFor();
|
||||||
|
return {
|
||||||
|
separateFileInputNames: true,
|
||||||
|
namedRemoval: true,
|
||||||
|
sameFileReselection: true,
|
||||||
|
authenticatedMultipartUpload: true,
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
test(
|
||||||
|
"normal kky upgrade plans and disks fit the narrow dialog container",
|
||||||
|
{ timeout: 30000 },
|
||||||
|
(t) =>
|
||||||
|
scenario(t, {}, async (page) => {
|
||||||
|
await boundary(page, {
|
||||||
|
"/host/5": {
|
||||||
|
host: {
|
||||||
|
id: 5,
|
||||||
|
product_id: 123,
|
||||||
|
module: "kky_ecs",
|
||||||
|
name: "本地弹性云",
|
||||||
|
status: "Active",
|
||||||
|
billing_cycle: "monthly",
|
||||||
|
upgrade_allowed: true,
|
||||||
|
renew_amount: "10.00",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"/kky_ecs/5": {
|
||||||
|
cpu: 2,
|
||||||
|
memory: 4,
|
||||||
|
bw: 10,
|
||||||
|
protect: 0,
|
||||||
|
ip: "192.0.2.5",
|
||||||
|
username: "root",
|
||||||
|
port: 22,
|
||||||
|
image: { name: "Ubuntu 24.04" },
|
||||||
|
},
|
||||||
|
"/kky_ecs/5/status": { status: "on", rescue: false },
|
||||||
|
"/kky_ecs/5/traffic": {
|
||||||
|
mode: "bandwidth",
|
||||||
|
monthly_quota: 100,
|
||||||
|
traffic_show: 0,
|
||||||
|
},
|
||||||
|
"/kky_ecs/disk/5": {
|
||||||
|
list: [
|
||||||
|
{
|
||||||
|
id: "data-1",
|
||||||
|
name: "数据盘",
|
||||||
|
size: 40,
|
||||||
|
type2: "data",
|
||||||
|
fs_type: "SSD",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
"/kky_ecs/monitor/5": {
|
||||||
|
sample_info: [],
|
||||||
|
cpu: [],
|
||||||
|
memory: [],
|
||||||
|
bandwidth: [],
|
||||||
|
disk: [],
|
||||||
|
},
|
||||||
|
"/host/5/renew/auto": { status: 0 },
|
||||||
|
"/refund": { allow_refund: 0, reasons: [] },
|
||||||
|
"/refund/host/5/refund": { refund: null },
|
||||||
|
"/promo_code/host/5/promo_code": { promo_code: [] },
|
||||||
|
"/kky_ecs/upgrade/5/ins_upgrade_config": [
|
||||||
|
{
|
||||||
|
cpu_num: 4,
|
||||||
|
mem_list: [
|
||||||
|
{ id: 1, memory: 8, is_sell_out: 0, description: "常规升级规格" },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
"/kky_ecs/upgrade/5/add_disk_config": {
|
||||||
|
min: 10,
|
||||||
|
max: 1000,
|
||||||
|
step: 10,
|
||||||
|
type: ["SSD"],
|
||||||
|
remaining_num: 0,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
await login(page, "/servers/server?id=5");
|
||||||
|
await page.getByRole("tab", { name: "计费", exact: true }).click();
|
||||||
|
await page.getByRole("button", { name: "升降级", exact: true }).click();
|
||||||
|
const dialog = page.getByRole("dialog", { name: "升降级", exact: true });
|
||||||
|
await dialog.locator(".plans .plan").waitFor();
|
||||||
|
const widths = [];
|
||||||
|
const assertFits = async (gridName, cardName) => {
|
||||||
|
const measured = await dialog
|
||||||
|
.locator(gridName)
|
||||||
|
.evaluate((grid, childSelector) => {
|
||||||
|
const bounds = grid.getBoundingClientRect();
|
||||||
|
const card = grid
|
||||||
|
.querySelector(childSelector)
|
||||||
|
.getBoundingClientRect();
|
||||||
|
return {
|
||||||
|
width: bounds.width,
|
||||||
|
scroll: grid.scrollWidth,
|
||||||
|
client: grid.clientWidth,
|
||||||
|
rightOverflow: card.right - bounds.right,
|
||||||
|
};
|
||||||
|
}, cardName);
|
||||||
|
assert.ok(measured.width > 0);
|
||||||
|
assert.ok(
|
||||||
|
measured.rightOverflow <= 1 && measured.scroll <= measured.client + 1,
|
||||||
|
`${gridName} must shrink to its actual dialog container: ${JSON.stringify(measured)}`,
|
||||||
|
);
|
||||||
|
widths.push({
|
||||||
|
viewport: page.viewportSize().width,
|
||||||
|
grid: gridName,
|
||||||
|
width: Math.round(measured.width),
|
||||||
|
fits: true,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
for (const width of [375, 320]) {
|
||||||
|
await page.setViewportSize({ width, height: 900 });
|
||||||
|
await assertFits(".plans", ".plan");
|
||||||
|
}
|
||||||
|
await dialog.getByRole("button", { name: /硬盘/ }).click();
|
||||||
|
await dialog
|
||||||
|
.getByRole("button", { name: "扩容已有盘", exact: true })
|
||||||
|
.click();
|
||||||
|
await dialog.locator(".disks .disk").waitFor();
|
||||||
|
for (const width of [375, 320]) {
|
||||||
|
await page.setViewportSize({ width, height: 900 });
|
||||||
|
await assertFits(".disks", ".disk");
|
||||||
|
}
|
||||||
|
return { grids: widths };
|
||||||
|
}),
|
||||||
|
);
|
||||||
Reference in new issue
Block a user