mirror of
https://github.com/Ku6epXBOCTuK/easy-png-tools.git
synced 2026-11-13 17:08:20 +00:00
chore: update lint rules for comments
This commit is contained in:
@@ -142,13 +142,21 @@ report-only (`warn`), baseline — 0 срабатываний.
|
||||
списку**: комментарий обязан быть ASCII, поэтому ловятся любые письменности
|
||||
(кириллица, CJK, арабский), а не один конкретный алфавит; осознанные глифы —
|
||||
через опцию `allowChars`. Строковые литералы не трогаем — локализованные
|
||||
значения легальны в словарях.
|
||||
значения легальны в словарях. В `.svelte` проверяются оба вида комментариев:
|
||||
`<script>` и HTML-комментарии шаблона `<!-- -->` (последние — узлы AST, а не
|
||||
comment-токены, поэтому обходятся отдельным визитором).
|
||||
- `conventions/comment-format` — два мягких потолка: `maxBlockLines` (по
|
||||
умолчанию 5) строк на один блок комментариев и `maxRatio` (по умолчанию 0.15)
|
||||
— доля комментарных строк от непустых строк файла; density проверяется только
|
||||
в файлах от `minLines` непустых строк (в конфиге 100), иначе любая шапка в
|
||||
маленьком файле даёт ложное превышение. Превышение — сигнал к рефакторингу
|
||||
кода, а не к сжатию комментария.
|
||||
кода, а не к сжатию комментария. В `.svelte` считаются все три вида
|
||||
комментариев (и в строках блока, и в плотности): `<script>`, HTML-комментарии
|
||||
шаблона `<!-- -->` — отдельным визитором, как в
|
||||
`conventions/comments-english`, и CSS в `<style>`, который парсится в
|
||||
postcss-AST и берётся хелпером `design-tokens/style-context.js`. Комментарии
|
||||
разных видов не склеиваются в один блок: соседние строки продолжают блок
|
||||
только внутри `<script>`.
|
||||
|
||||
Scope правил комментариев — не только `src/`: сами плагины, `scripts/` и
|
||||
`*.config.*` живут по тем же конвенциям (dogfooding). Фикстуры и тесты правил
|
||||
|
||||
@@ -1,8 +1,12 @@
|
||||
// Tests for the conventions/comment-format rule
|
||||
// (web/eslint-plugins/conventions/comment-format.js).
|
||||
//
|
||||
// Pure AST check (comment nodes + line counts), standard RuleTester.
|
||||
// Pure AST check (comment nodes + line counts), standard RuleTester. The
|
||||
// svelte pass pins the two comment kinds the token stream never sees: template
|
||||
// `<!-- -->` comments (AST nodes) and CSS in `<style>` (a postcss AST); both
|
||||
// need a svelte parser to exist at all.
|
||||
import { RuleTester } from "eslint";
|
||||
import svelteParser from "svelte-eslint-parser";
|
||||
import tseslint from "typescript-eslint";
|
||||
import { describe, it } from "vitest";
|
||||
import commentFormat from "../conventions/comment-format.js";
|
||||
@@ -17,6 +21,13 @@ const tester = new RuleTester({
|
||||
},
|
||||
});
|
||||
|
||||
const svelteTester = new RuleTester({
|
||||
languageOptions: {
|
||||
parser: svelteParser,
|
||||
parserOptions: { parser: tseslint.parser },
|
||||
},
|
||||
});
|
||||
|
||||
const longBlock = [
|
||||
"// one",
|
||||
"// two",
|
||||
@@ -96,3 +107,101 @@ tester.run("comment-format", asRuleModule(commentFormat), {
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
svelteTester.run("comment-format (svelte)", asRuleModule(commentFormat), {
|
||||
valid: [
|
||||
{
|
||||
code: "<!-- why -->\n<main>Hello</main>",
|
||||
filename: "Component.svelte",
|
||||
options: [{ maxRatio: 1 }],
|
||||
},
|
||||
{
|
||||
// A template comment at the cap is fine, same as in a .ts file.
|
||||
code: "<!--\na\nb\nc\n-->\n<main>Hello</main>",
|
||||
filename: "Component.svelte",
|
||||
options: [{ maxRatio: 1 }],
|
||||
},
|
||||
{
|
||||
// Script and template comments are separate blocks, not one run.
|
||||
code: '<!-- why -->\n<main>Hello</main>\n<script lang="ts">\n// why\n</script>',
|
||||
filename: "Component.svelte",
|
||||
options: [{ maxRatio: 1 }],
|
||||
},
|
||||
{
|
||||
// A CSS comment at the cap is fine, same as a script comment.
|
||||
code: "<style>\n/*\na\nb\nc\n*/\n.x {\n\tcolor: red;\n}\n</style>",
|
||||
filename: "Component.svelte",
|
||||
options: [{ maxRatio: 1 }],
|
||||
},
|
||||
{
|
||||
// Script and <style> comments live in different ASTs: no run across,
|
||||
// so 2 + 5 lines pass where a merged block of 7 would not.
|
||||
code: '<script lang="ts">\n// one\n// two\n</script>\n<style>\n/*\na\nb\nc\n*/\n.x {\n\tcolor: red;\n}\n</style>',
|
||||
filename: "Component.svelte",
|
||||
options: [{ maxRatio: 1 }],
|
||||
},
|
||||
],
|
||||
invalid: [
|
||||
{
|
||||
// The template comment the ts pass could never see, over the cap.
|
||||
code: "<!--\na\nb\nc\nd\n-->\n<main>Hello</main>",
|
||||
filename: "Component.svelte",
|
||||
options: [{ maxRatio: 1 }],
|
||||
errors: [{ messageId: "blockTooLong" }],
|
||||
},
|
||||
{
|
||||
// <style> is a postcss AST, so its comments are invisible to
|
||||
// getAllComments(); the loc must still point at the real line.
|
||||
code: "<style>\n/*\na\nb\nc\nd\n*/\n.x {\n\tcolor: red;\n}\n</style>",
|
||||
filename: "Component.svelte",
|
||||
options: [{ maxRatio: 1 }],
|
||||
errors: [{ messageId: "blockTooLong", line: 2 }],
|
||||
},
|
||||
{
|
||||
// A comment nested in markup is collected too (not Program.body).
|
||||
code: "<div>\n\t<!--\n\ta\n\tb\n\tc\n\td\n\t-->\n</div>",
|
||||
filename: "Component.svelte",
|
||||
options: [{ maxRatio: 1 }],
|
||||
errors: [{ messageId: "blockTooLong" }],
|
||||
},
|
||||
{
|
||||
// Template comment lines count toward density: 2 of 4 non-blank.
|
||||
code: "<!-- a -->\n<!-- b -->\n<p>x</p>\n<p>y</p>",
|
||||
filename: "Component.svelte",
|
||||
options: [{ maxBlockLines: 5, maxRatio: 0.4 }],
|
||||
errors: [{ messageId: "tooDense" }],
|
||||
},
|
||||
{
|
||||
// Script and template comments share one density budget.
|
||||
code: '<!-- a -->\n<script lang="ts">\n// b\n</script>\n<p>c</p>',
|
||||
filename: "Component.svelte",
|
||||
options: [{ maxBlockLines: 5, maxRatio: 0.3 }],
|
||||
errors: [{ messageId: "tooDense" }],
|
||||
},
|
||||
{
|
||||
// CSS lines count toward density too: 2 of 8 non-blank. Consecutive
|
||||
// CSS comments stay separate blocks (each one line, under the cap).
|
||||
code: "<p>x</p>\n<style>\n/* a */\n/* b */\n.x {\n\tcolor: red;\n}\n</style>",
|
||||
filename: "Component.svelte",
|
||||
options: [{ maxBlockLines: 5, maxRatio: 0.2 }],
|
||||
errors: [{ messageId: "tooDense" }],
|
||||
},
|
||||
{
|
||||
// All three kinds over the cap: one report per block, by position.
|
||||
code: "<!--\na\nb\nc\nd\ne\n-->\n<style>\n/*\nx\ny\nz\nw\nv\n*/\n.x {\n\tcolor: red;\n}\n</style>",
|
||||
filename: "Component.svelte",
|
||||
options: [{ maxRatio: 1 }],
|
||||
errors: [
|
||||
{ messageId: "blockTooLong", line: 1 },
|
||||
{ messageId: "blockTooLong", line: 9 },
|
||||
],
|
||||
},
|
||||
{
|
||||
// Both violations in one file produce two reports.
|
||||
code: "<!--\na\nb\nc\nd\n-->\n<p>x</p>",
|
||||
filename: "Component.svelte",
|
||||
options: [{ maxBlockLines: 5, maxRatio: 0.5 }],
|
||||
errors: [{ messageId: "blockTooLong" }, { messageId: "tooDense" }],
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
@@ -3,12 +3,14 @@
|
||||
//
|
||||
// Pure AST check over comment nodes with an ASCII whitelist; string literals
|
||||
// are ignored by construction (they are not comments), which the cases below
|
||||
// pin down.
|
||||
// pin down. .svelte gets its own pass: template `<!-- -->` comments are AST
|
||||
// nodes (SvelteHTMLComment), not comment tokens, so they need a svelte parser.
|
||||
import { RuleTester } from "eslint";
|
||||
import svelteParser from "svelte-eslint-parser";
|
||||
import tseslint from "typescript-eslint";
|
||||
import { describe, it } from "vitest";
|
||||
import commentsEnglish from "../conventions/comments-english.js";
|
||||
import { asRuleModule } from "./helpers.js";
|
||||
import { asRuleModule, svelteComponent } from "./helpers.js";
|
||||
|
||||
RuleTester.describe = describe;
|
||||
RuleTester.it = it;
|
||||
@@ -19,6 +21,13 @@ const tester = new RuleTester({
|
||||
},
|
||||
});
|
||||
|
||||
const svelteTester = new RuleTester({
|
||||
languageOptions: {
|
||||
parser: svelteParser,
|
||||
parserOptions: { parser: tseslint.parser },
|
||||
},
|
||||
});
|
||||
|
||||
tester.run("comments-english", asRuleModule(commentsEnglish), {
|
||||
valid: [
|
||||
{ code: "// resize the canvas\nconst x = 1;", filename: "a.ts" },
|
||||
@@ -94,3 +103,77 @@ tester.run("comments-english", asRuleModule(commentsEnglish), {
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
// Svelte components: both comment kinds are in scope. `<script>` comments
|
||||
// come in as ordinary comment tokens; template `<!-- -->` comments only exist
|
||||
// as SvelteHTMLComment nodes, which is the case the ts pass cannot reach.
|
||||
svelteTester.run("comments-english (svelte)", asRuleModule(commentsEnglish), {
|
||||
valid: [
|
||||
{
|
||||
code: "<!-- resize the canvas -->\n<main>Hello</main>",
|
||||
filename: "Component.svelte",
|
||||
},
|
||||
{
|
||||
code: svelteComponent("// resize the canvas"),
|
||||
filename: "Component.svelte",
|
||||
},
|
||||
{
|
||||
code: '<!-- svelte-ignore a11y_missing_attribute -->\n<main lang="en">Hello</main>',
|
||||
filename: "Component.svelte",
|
||||
},
|
||||
{
|
||||
// allowChars applies to template comments too.
|
||||
code: "<!-- size 512×512 -->\n<main>Hello</main>",
|
||||
filename: "Component.svelte",
|
||||
options: [{ allowChars: ["×"] }],
|
||||
},
|
||||
{
|
||||
// CSS comments in <style> are checked as well, ASCII passes.
|
||||
code: '<div class="x">hi</div>\n<style>\n/* keep the row height stable */\n.x {\n\tcolor: red;\n}\n</style>',
|
||||
filename: "Component.svelte",
|
||||
},
|
||||
],
|
||||
invalid: [
|
||||
{
|
||||
// The template comment the ts pass could never see.
|
||||
code: "<!-- русский комментарий -->\n<main>Hello</main>",
|
||||
filename: "Component.svelte",
|
||||
errors: [{ messageId: "nonAscii" }],
|
||||
},
|
||||
{
|
||||
// <style> is a postcss AST, so its comments are invisible to
|
||||
// getAllComments(); the loc must still point at the real line.
|
||||
code: '<div class="x">hi</div>\n<style>\n/* русский комментарий в стилях */\n.x {\n\tcolor: red;\n}\n</style>',
|
||||
filename: "Component.svelte",
|
||||
errors: [{ messageId: "nonAscii", line: 3 }],
|
||||
},
|
||||
{
|
||||
// All three comment kinds in one component, reported by position.
|
||||
code: '<!-- раз -->\n<style>\n/* два */\n.x {\n\tcolor: red;\n}\n</style>\n<script lang="ts">\n// три\n</script>',
|
||||
filename: "Component.svelte",
|
||||
errors: [
|
||||
{ messageId: "nonAscii", line: 1 },
|
||||
{ messageId: "nonAscii", line: 3 },
|
||||
{ messageId: "nonAscii", line: 9 },
|
||||
],
|
||||
},
|
||||
{
|
||||
code: svelteComponent("// ресайз холста"),
|
||||
filename: "Component.svelte",
|
||||
errors: [{ messageId: "nonAscii" }],
|
||||
},
|
||||
{
|
||||
// One report per comment, script and template alike.
|
||||
code: '<!-- раз -->\n<main>два</main>\n<script lang="ts">\n// три\n</script>',
|
||||
filename: "Component.svelte",
|
||||
errors: [{ messageId: "nonAscii" }, { messageId: "nonAscii" }],
|
||||
},
|
||||
{
|
||||
// allowChars does not whitelist other glyphs in the same comment.
|
||||
code: "<!-- 512×512 → done -->\n<main>Hello</main>",
|
||||
filename: "Component.svelte",
|
||||
options: [{ allowChars: ["×"] }],
|
||||
errors: [{ messageId: "nonAscii" }],
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
@@ -1,9 +1,14 @@
|
||||
// Rule: COMMENT FORMAT: bounded blocks and bounded density.
|
||||
// maxBlockLines caps one comment block; maxRatio caps the share of comment
|
||||
// lines in files with minLines+ non-blank lines. Warn-only tripwires:
|
||||
// excess means refactor the code, not squeeze the comment.
|
||||
// lines in files with minLines+ non-blank lines. In .svelte, <script>,
|
||||
// template <!-- --> and <style> CSS (postcss AST) count alike.
|
||||
// Details: web/eslint-plugins/README.md.
|
||||
|
||||
import {
|
||||
getStyleNodeLoc,
|
||||
getStyleRoot,
|
||||
} from "../design-tokens/style-context.js";
|
||||
|
||||
function lineSpan(comment) {
|
||||
return comment.loc.end.line - comment.loc.start.line + 1;
|
||||
}
|
||||
@@ -40,10 +45,42 @@ export default {
|
||||
const maxRatio = context.options[0]?.maxRatio ?? 0.15;
|
||||
const minLines = context.options[0]?.minLines ?? 0;
|
||||
|
||||
const htmlComments = [];
|
||||
|
||||
// `<style>` is a postcss AST, not a token stream, so ESLint's comment
|
||||
// tokens never include those comments; they come from the shared helper
|
||||
// the design-tokens rules use (null outside Svelte components).
|
||||
const styleRoot = getStyleRoot(context);
|
||||
const styleNodeLoc = getStyleNodeLoc(context);
|
||||
|
||||
return {
|
||||
Program(node) {
|
||||
// svelte-eslint-parser keeps `<!-- -->` template comments out of the
|
||||
// token stream (they are AST nodes, not comment tokens), so
|
||||
// getAllComments() below never sees them. Collected during traversal
|
||||
// to catch template comments nested inside markup elements too.
|
||||
SvelteHTMLComment(node) {
|
||||
htmlComments.push(node);
|
||||
},
|
||||
"Program:exit"(node) {
|
||||
const sourceCode = context.sourceCode;
|
||||
const comments = sourceCode.getAllComments();
|
||||
// `type: "Block"` keeps a CSS comment from merging with the run
|
||||
// above it: only adjacent `//` lines continue one block.
|
||||
const styleComments = [];
|
||||
if (styleRoot && styleNodeLoc) {
|
||||
styleRoot.walkComments((comment) => {
|
||||
const loc = styleNodeLoc(comment);
|
||||
if (loc.start) styleComments.push({ loc, type: "Block" });
|
||||
});
|
||||
}
|
||||
const comments = [
|
||||
...sourceCode.getAllComments(),
|
||||
...htmlComments,
|
||||
...styleComments,
|
||||
].sort(
|
||||
(a, b) =>
|
||||
a.loc.start.line - b.loc.start.line ||
|
||||
a.loc.start.column - b.loc.start.column,
|
||||
);
|
||||
if (comments.length === 0) return;
|
||||
|
||||
const commentLines = new Set();
|
||||
@@ -67,7 +104,8 @@ export default {
|
||||
commentLines.add(l);
|
||||
}
|
||||
if (i === 0) continue;
|
||||
// Adjacent `//` lines form one block; anything else starts a new one.
|
||||
// Adjacent `//` lines form one block; HTML template comments and
|
||||
// everything else start a new one (their own span is the block).
|
||||
if (
|
||||
comment.type === "Line" &&
|
||||
comments[i - 1].type === "Line" &&
|
||||
|
||||
@@ -4,6 +4,11 @@
|
||||
// checked: localized dictionary values legitimately contain other scripts.
|
||||
// Details: web/eslint-plugins/README.md.
|
||||
|
||||
import {
|
||||
getStyleNodeLoc,
|
||||
getStyleRoot,
|
||||
} from "../design-tokens/style-context.js";
|
||||
|
||||
export default {
|
||||
meta: {
|
||||
type: "suggestion",
|
||||
@@ -33,19 +38,18 @@ export default {
|
||||
},
|
||||
create(context) {
|
||||
const allowed = new Set(context.options[0]?.allowChars ?? []);
|
||||
return {
|
||||
Program() {
|
||||
for (const comment of context.sourceCode.getAllComments()) {
|
||||
|
||||
const check = (text, loc) => {
|
||||
const offenders = new Set();
|
||||
for (const match of comment.value.matchAll(
|
||||
for (const match of text.matchAll(
|
||||
// eslint-disable-next-line no-control-regex -- the printable-ASCII whitelist needs explicit control escapes
|
||||
/[^\x09\x0A\x0D\x20-\x7E]/g,
|
||||
)) {
|
||||
if (!allowed.has(match[0])) offenders.add(match[0]);
|
||||
}
|
||||
if (offenders.size === 0) continue;
|
||||
if (offenders.size === 0) return;
|
||||
context.report({
|
||||
loc: comment.loc,
|
||||
loc,
|
||||
messageId: "nonAscii",
|
||||
data: {
|
||||
chars: [...offenders]
|
||||
@@ -56,7 +60,34 @@ export default {
|
||||
.join(", "),
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
// `<style>` is a postcss AST, not a token stream, so ESLint's comment
|
||||
// tokens never include those comments; they come from the shared helper
|
||||
// the design-tokens rules use (null outside Svelte components).
|
||||
const styleRoot = getStyleRoot(context);
|
||||
const styleNodeLoc = getStyleNodeLoc(context);
|
||||
const htmlComments = [];
|
||||
|
||||
return {
|
||||
// svelte-eslint-parser keeps `<!-- -->` template comments out of the
|
||||
// token stream (they are AST nodes, not comment tokens), so
|
||||
// getAllComments() below never sees them. Collected during traversal
|
||||
// to catch template comments nested inside markup elements too.
|
||||
SvelteHTMLComment(node) {
|
||||
htmlComments.push(node);
|
||||
},
|
||||
"Program:exit"() {
|
||||
for (const comment of context.sourceCode.getAllComments()) {
|
||||
check(comment.value, comment.loc);
|
||||
}
|
||||
for (const node of htmlComments) {
|
||||
check(node.value, node.loc);
|
||||
}
|
||||
if (!styleRoot || !styleNodeLoc) return;
|
||||
styleRoot.walkComments((comment) => {
|
||||
check(comment.text ?? "", styleNodeLoc(comment));
|
||||
});
|
||||
},
|
||||
};
|
||||
},
|
||||
|
||||
@@ -90,7 +90,7 @@
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: var(--space-m);
|
||||
/* Резерв под бейдж: строка не прыгает, когда заметка появляется. */
|
||||
/* Reserve for the badge: the row does not jump when the note appears. */
|
||||
min-height: var(--space-xxl);
|
||||
margin-bottom: var(--space-m);
|
||||
font: var(--font-size-s) var(--font-mono);
|
||||
@@ -105,7 +105,7 @@
|
||||
align-items: center;
|
||||
gap: var(--space-m);
|
||||
}
|
||||
/* Место под заметки результата: бейджи-предупреждения. */
|
||||
/* Slot for result notes: warning badges. */
|
||||
.tile-note {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
|
||||
@@ -177,12 +177,12 @@
|
||||
cursor: not-allowed;
|
||||
}
|
||||
.menu {
|
||||
/* Native popover: top layer + light dismiss + Escape из коробки;
|
||||
позиционирование — trackPopover (anchor() поддержан не везде). */
|
||||
/* Native popover: top layer + light dismiss + Escape out of the box;
|
||||
positioning is trackPopover (anchor() is not supported everywhere). */
|
||||
position: fixed;
|
||||
margin: 0;
|
||||
translate: 0 var(--space-s);
|
||||
/* Ширина под строку качества: числовое поле + степперы + сброс. */
|
||||
/* Width for the quality row: number field + steppers + reset. */
|
||||
min-width: calc(var(--space-xxxl) * 9);
|
||||
gap: var(--space-s);
|
||||
padding: var(--space-m);
|
||||
@@ -190,8 +190,8 @@
|
||||
border-radius: var(--radius-m);
|
||||
background: var(--color-panel);
|
||||
}
|
||||
/* display задаётся только в открытом состоянии: авторский display
|
||||
перебивает UA-правило [popover]:not(:popover-open) { display: none }. */
|
||||
/* display applies only while open: the author display overrides the
|
||||
UA rule [popover]:not(:popover-open) { display: none }. */
|
||||
.menu:popover-open {
|
||||
display: grid;
|
||||
}
|
||||
|
||||
@@ -128,8 +128,8 @@
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
/* Вложен в PreviewTile.canvas: без растяжения сжимался по контенту и
|
||||
ломал parts-grid в одну колонку. */
|
||||
/* Nested in PreviewTile.canvas: without stretch it shrank to its
|
||||
content and collapsed parts-grid into a single column. */
|
||||
width: 100%;
|
||||
align-self: stretch;
|
||||
min-height: var(--size-tile-canvas-min);
|
||||
|
||||
@@ -105,8 +105,8 @@
|
||||
color: var(--color-text);
|
||||
}
|
||||
.menu {
|
||||
/* Native popover: top layer + light dismiss + Escape из коробки;
|
||||
позиционирование — trackPopover (anchor() поддержан не везде). */
|
||||
/* Native popover: top layer + light dismiss + Escape out of the box;
|
||||
positioning is trackPopover (anchor() is not supported everywhere). */
|
||||
position: fixed;
|
||||
margin: 0;
|
||||
translate: 0 var(--space-s);
|
||||
@@ -119,8 +119,8 @@
|
||||
border-radius: var(--radius-m);
|
||||
background: var(--color-panel);
|
||||
}
|
||||
/* display задаётся только в открытом состоянии: авторский display
|
||||
перебивает UA-правило [popover]:not(:popover-open) { display: none }. */
|
||||
/* display applies only while open: the author display overrides the
|
||||
UA rule [popover]:not(:popover-open) { display: none }. */
|
||||
.menu:popover-open {
|
||||
display: grid;
|
||||
}
|
||||
|
||||
@@ -60,7 +60,7 @@
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
/* Мини-размер цели в 24px: попадание пальцем без промаха. */
|
||||
/* Mini target size at 24px: tap without missing. */
|
||||
width: calc(var(--space-xxxl) * 0.66);
|
||||
height: calc(var(--space-xxxl) * 0.66);
|
||||
padding: 0;
|
||||
@@ -78,9 +78,9 @@
|
||||
color: var(--color-text);
|
||||
border-color: var(--color-main);
|
||||
}
|
||||
/* Нативный popover живёт в top layer: light dismiss, Escape и раскладка
|
||||
поверх всего — из коробки. Позиционирование — trackPopover: anchor()
|
||||
поддержан не во всех браузерах. */
|
||||
/* The native popover lives in the top layer: light dismiss, Escape and
|
||||
stacking above everything come for free. Positioning is trackPopover:
|
||||
anchor() is not supported in every browser. */
|
||||
.popover {
|
||||
position: fixed;
|
||||
margin: 0;
|
||||
|
||||
Reference in New Issue
Block a user