@brandup/ui-richeditor 1.0.40 → 1.0.42
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +52 -2
- package/package.json +2 -2
- package/source/editing.ts +170 -13
- package/source/emoji.ts +127 -51
- package/source/format-config.ts +117 -3
- package/source/format.ts +18 -1
- package/source/index.ts +6 -1
- package/source/paragraphs.ts +79 -13
- package/source/richeditor.less +103 -10
- package/source/richeditor.ts +340 -18
- package/source/selection.ts +195 -29
- package/source/serialize.ts +479 -296
- package/source/toolbar.ts +172 -24
- package/svg/codeblock.svg +1 -0
- package/svg/mono.svg +3 -0
- package/svg/quote.svg +1 -0
- package/svg/spoiler.svg +1 -0
package/source/toolbar.ts
CHANGED
|
@@ -6,13 +6,25 @@
|
|
|
6
6
|
// т.к. тулбар находится вне привязанных UIElement).
|
|
7
7
|
|
|
8
8
|
import { DOM } from "@brandup/ui";
|
|
9
|
-
import { POPUP_CLASS, PopupManager } from "@brandup/ui-kit";
|
|
10
|
-
import {
|
|
11
|
-
|
|
9
|
+
import { POPUP_CLASS, PopupManager, SCROLLABLE_CLASS } from "@brandup/ui-kit";
|
|
10
|
+
import {
|
|
11
|
+
BLOCK_TYPES,
|
|
12
|
+
DEFAULT_BLOCK,
|
|
13
|
+
EDITOR_ACTIONS,
|
|
14
|
+
FORMAT_TOOLS,
|
|
15
|
+
type BlockType,
|
|
16
|
+
type EditorAction,
|
|
17
|
+
type FormatTool,
|
|
18
|
+
} from "./format";
|
|
19
|
+
import { EMOJI_GROUPS, type EmojiGroup } from "./emoji";
|
|
12
20
|
import boldIcon from "../svg/bold.svg";
|
|
13
21
|
import italicIcon from "../svg/italic.svg";
|
|
14
22
|
import strikeIcon from "../svg/strike.svg";
|
|
15
23
|
import underlineIcon from "../svg/underline.svg";
|
|
24
|
+
import spoilerIcon from "../svg/spoiler.svg";
|
|
25
|
+
import codeIcon from "../svg/mono.svg";
|
|
26
|
+
import quoteIcon from "../svg/quote.svg";
|
|
27
|
+
import codeblockIcon from "../svg/codeblock.svg";
|
|
16
28
|
import emojiIcon from "../svg/emoji.svg";
|
|
17
29
|
import eraseIcon from "../svg/erase.svg";
|
|
18
30
|
import undoIcon from "../svg/undo.svg";
|
|
@@ -23,6 +35,14 @@ const FORMAT_ICONS: Record<FormatTool, string> = {
|
|
|
23
35
|
italic: italicIcon,
|
|
24
36
|
strike: strikeIcon,
|
|
25
37
|
underline: underlineIcon,
|
|
38
|
+
spoiler: spoilerIcon,
|
|
39
|
+
code: codeIcon,
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
// Обычный текст кнопки не имеет: повторное нажатие активной кнопки возвращает блок к нему.
|
|
43
|
+
const BLOCK_ICONS: Partial<Record<BlockType, string>> = {
|
|
44
|
+
quote: quoteIcon,
|
|
45
|
+
code: codeblockIcon,
|
|
26
46
|
};
|
|
27
47
|
|
|
28
48
|
const ACTION_ICONS: Record<EditorAction, string> = {
|
|
@@ -32,6 +52,17 @@ const ACTION_ICONS: Record<EditorAction, string> = {
|
|
|
32
52
|
redo: redoIcon,
|
|
33
53
|
};
|
|
34
54
|
|
|
55
|
+
// Код есть и инструментом (моноширинный), и типом блока — панель сводит их в одну кнопку.
|
|
56
|
+
const CODE_TOOL: FormatTool = "code";
|
|
57
|
+
const CODE_BLOCK: BlockType = "code";
|
|
58
|
+
const MERGED_CODE_TITLE = "Код";
|
|
59
|
+
|
|
60
|
+
// Временно скрытые кнопки. Сами возможности работают: значение разбирается, показывается
|
|
61
|
+
// и сохраняется, правку можно вызвать из кода — в панель они просто не выводятся.
|
|
62
|
+
// Убрать отсюда, когда будут доведены.
|
|
63
|
+
const HIDDEN_TOOLS: FormatTool[] = ["spoiler", "code"];
|
|
64
|
+
const HIDDEN_BLOCKS: BlockType[] = ["code"];
|
|
65
|
+
|
|
35
66
|
export const TOOLBAR_CLASS = "ui-richeditor-toolbar";
|
|
36
67
|
export const EMOJI_PICKER_CLASS = "ui-richeditor-emoji";
|
|
37
68
|
|
|
@@ -61,8 +92,19 @@ export interface ToolbarHost {
|
|
|
61
92
|
readonly editorActions?: EditorAction[];
|
|
62
93
|
/** Контейнер для тулбара; null/undefined — document.body (position: fixed над редактором). */
|
|
63
94
|
readonly toolbarContainer?: HTMLElement | null;
|
|
95
|
+
/** Типы блоков многострочного режима; пусто/undefined — кнопок блоков нет. */
|
|
96
|
+
readonly blockTypes?: BlockType[];
|
|
64
97
|
applyFormat(tool: FormatTool): void;
|
|
65
98
|
isToolActive(tool: FormatTool): boolean;
|
|
99
|
+
/** false — инструмент сейчас недоступен (например, внутри кода): кнопка гасится. */
|
|
100
|
+
isToolEnabled?(tool: FormatTool): boolean;
|
|
101
|
+
/** Тип блока под кареткой — им подсвечивается активная кнопка блока. */
|
|
102
|
+
readonly currentBlock?: BlockType;
|
|
103
|
+
applyBlock?(type: BlockType): void;
|
|
104
|
+
/** Правка кода любого вида — для объединённой кнопки (моноширинный + блок кода). */
|
|
105
|
+
applyCode?(): void;
|
|
106
|
+
/** Активен ли код в любом виде — подсветка объединённой кнопки. */
|
|
107
|
+
isCodeActive?(): boolean;
|
|
66
108
|
/** Активные форматы всех инструментов сразу; нет реализации — панель опросит их поштучно. */
|
|
67
109
|
activeTools?(): ReadonlySet<FormatTool>;
|
|
68
110
|
applyAction?(action: EditorAction): void;
|
|
@@ -76,14 +118,46 @@ export interface ToolbarHost {
|
|
|
76
118
|
|
|
77
119
|
const MARGIN = 6;
|
|
78
120
|
|
|
121
|
+
// Сколько кнопок помещается в ряд при ширине панели (см. .ui-richeditor-emoji в richeditor.less).
|
|
122
|
+
// Точность нужна только для оценки высоты нерисованной группы: ошибка сдвинет ползунок прокрутки,
|
|
123
|
+
// но не саму раскладку — группа переносит кнопки сама.
|
|
124
|
+
const EMOJI_COLUMNS = 8;
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Группа смайликов: и смысловое деление в панели (отбивается линией), и кусок, к которому
|
|
128
|
+
* применяется пропуск отрисовки. Поэлементно это было бы семьсот отслеживаемых поддеревьев,
|
|
129
|
+
* и слежение за ними съедает выигрыш от пропуска.
|
|
130
|
+
*/
|
|
131
|
+
function buildEmojiGroup(group: EmojiGroup): HTMLElement {
|
|
132
|
+
const rows = Math.ceil(group.emojis.length / EMOJI_COLUMNS);
|
|
133
|
+
const elem = DOM.tag("div", {
|
|
134
|
+
class: "emoji-group",
|
|
135
|
+
role: "group",
|
|
136
|
+
"aria-label": group.title,
|
|
137
|
+
// высота, пока группа не нарисована: без неё список схлопнулся бы, а прокрутка скакала
|
|
138
|
+
style: `--emoji-rows: ${rows}`,
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
const fragment = document.createDocumentFragment();
|
|
142
|
+
for (const emoji of group.emojis)
|
|
143
|
+
fragment.appendChild(DOM.tag("button", { type: "button", class: "emoji", tabindex: "-1" }, emoji));
|
|
144
|
+
elem.appendChild(fragment);
|
|
145
|
+
|
|
146
|
+
return elem;
|
|
147
|
+
}
|
|
148
|
+
|
|
79
149
|
class FormatToolbar {
|
|
80
150
|
private __elem: HTMLElement | null = null;
|
|
81
151
|
private __emojiPicker: HTMLElement | null = null;
|
|
82
152
|
private __emojiHost: ToolbarHost | null = null; // куда уйдёт выбранный символ
|
|
83
153
|
private __emojiInitiator: HTMLElement | null = null; // кнопка, у которой открыта панель
|
|
84
154
|
private __buttons: Array<[FormatTool, HTMLButtonElement]> = [];
|
|
155
|
+
private __blockButtons: Array<[BlockType, HTMLButtonElement]> = [];
|
|
156
|
+
private __mergedCode = false; // кнопка кода делает и моноширинный, и блок (см. __build)
|
|
85
157
|
private __actionButtons: Array<[EditorAction, HTMLButtonElement]> = [];
|
|
86
|
-
|
|
158
|
+
// имя, а не сама кнопка хоста: панель одна на все редакторы и переиспользует разметку между
|
|
159
|
+
// ними, а поведение принадлежит текущему — держать здесь ссылку значит звать чужой обработчик
|
|
160
|
+
private __hostButtons: Array<[string, HTMLButtonElement]> = [];
|
|
87
161
|
private __active: ToolbarHost | null = null;
|
|
88
162
|
private __suspended: ToolbarHost | null = null; // показ придержан на время панели смайликов
|
|
89
163
|
private __toolsKey = "";
|
|
@@ -146,11 +220,16 @@ class FormatToolbar {
|
|
|
146
220
|
|
|
147
221
|
const actions = host.editorActions ?? [];
|
|
148
222
|
const buttons = host.toolbarButtons ?? [];
|
|
149
|
-
|
|
223
|
+
const tools = host.formatTools.filter((tool) => !HIDDEN_TOOLS.includes(tool));
|
|
224
|
+
// Обычный текст кнопки не имеет — он не «включается», а остаётся, когда выключены остальные.
|
|
225
|
+
const blocks = (host.blockTypes ?? []).filter(
|
|
226
|
+
(type) => type !== DEFAULT_BLOCK && !HIDDEN_BLOCKS.includes(type)
|
|
227
|
+
);
|
|
228
|
+
if (!tools.length && !blocks.length && !actions.length && !buttons.length) return;
|
|
150
229
|
|
|
151
230
|
this.__bindSelection();
|
|
152
231
|
this.__active = host;
|
|
153
|
-
this.__build(
|
|
232
|
+
this.__build(tools, blocks, actions, buttons);
|
|
154
233
|
this.refresh();
|
|
155
234
|
|
|
156
235
|
const elem = this.__ensure();
|
|
@@ -190,9 +269,12 @@ class FormatToolbar {
|
|
|
190
269
|
const emojiHost = this.__emojiHost === host;
|
|
191
270
|
if (!emojiHost && this.__active !== host) return;
|
|
192
271
|
|
|
193
|
-
|
|
194
|
-
|
|
272
|
+
// Закрываем только свою панель. Открытие у кнопки другого редактора само переводит туда
|
|
273
|
+
// фокус, и этот уход приходит уже после — панель к тому времени принадлежит соседу, и
|
|
274
|
+
// закрыть её значило бы гасить только что открытое: она требовала бы второго нажатия.
|
|
195
275
|
if (emojiHost) {
|
|
276
|
+
this.__closeEmoji();
|
|
277
|
+
|
|
196
278
|
this.__emojiHost = null;
|
|
197
279
|
this.__emojiInitiator = null;
|
|
198
280
|
}
|
|
@@ -227,13 +309,38 @@ class FormatToolbar {
|
|
|
227
309
|
|
|
228
310
|
const active = host.activeTools?.();
|
|
229
311
|
for (const [tool, btn] of this.__buttons) {
|
|
230
|
-
|
|
312
|
+
// объединённая кнопка подсвечена и на блоке кода, а не только на моноширинном
|
|
313
|
+
const isActive =
|
|
314
|
+
this.__mergedCode && tool === CODE_TOOL
|
|
315
|
+
? !!host.isCodeActive?.()
|
|
316
|
+
: active
|
|
317
|
+
? active.has(tool)
|
|
318
|
+
: host.isToolActive(tool);
|
|
319
|
+
|
|
231
320
|
if (btn.classList.contains("active") !== isActive) btn.classList.toggle("active", isActive);
|
|
321
|
+
setDisabled(btn, host.isToolEnabled?.(tool) === false);
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
// тип под кареткой спрашиваем, только если есть что подсвечивать: обновление идёт
|
|
325
|
+
// на каждое её движение, а поиск блока — обход предков
|
|
326
|
+
if (this.__blockButtons.length) {
|
|
327
|
+
const block = host.currentBlock;
|
|
328
|
+
|
|
329
|
+
for (const [type, btn] of this.__blockButtons) {
|
|
330
|
+
const isActive = block === type;
|
|
331
|
+
if (btn.classList.contains("active") !== isActive) btn.classList.toggle("active", isActive);
|
|
332
|
+
}
|
|
232
333
|
}
|
|
233
334
|
|
|
234
335
|
// хост может не реализовывать isActionEnabled — тогда кнопка всегда доступна
|
|
235
336
|
for (const [action, btn] of this.__actionButtons) setDisabled(btn, host.isActionEnabled?.(action) === false);
|
|
236
|
-
for (const [
|
|
337
|
+
for (const [name, btn] of this.__hostButtons)
|
|
338
|
+
setDisabled(btn, this.__hostButton(name)?.isEnabled?.() === false);
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
/** Кнопка хоста по имени — у активного редактора, а не у того, кто собрал разметку панели. */
|
|
342
|
+
private __hostButton(name: string): ToolbarButton | undefined {
|
|
343
|
+
return this.__active?.toolbarButtons?.find((button) => button.name === name);
|
|
237
344
|
}
|
|
238
345
|
|
|
239
346
|
/** Пересчитать позицию над активным редактором (только для режима body/fixed). */
|
|
@@ -269,8 +376,8 @@ class FormatToolbar {
|
|
|
269
376
|
return this.__elem;
|
|
270
377
|
}
|
|
271
378
|
|
|
272
|
-
private __build(tools: FormatTool[], actions: EditorAction[], buttons: ToolbarButton[]) {
|
|
273
|
-
const key = `${tools.join(",")}|${actions.join(",")}|${buttons.map((b) => b.name).join(",")}`;
|
|
379
|
+
private __build(tools: FormatTool[], blocks: BlockType[], actions: EditorAction[], buttons: ToolbarButton[]) {
|
|
380
|
+
const key = `${tools.join(",")}|${blocks.join(",")}|${actions.join(",")}|${buttons.map((b) => b.name).join(",")}`;
|
|
274
381
|
const elem = this.__ensure();
|
|
275
382
|
if (key === this.__toolsKey && elem.firstChild) return; // тот же состав — переиспользуем кнопки
|
|
276
383
|
|
|
@@ -278,29 +385,68 @@ class FormatToolbar {
|
|
|
278
385
|
const pickerInToolbar = !!this.__emojiPicker && this.__emojiPicker.parentElement === elem;
|
|
279
386
|
DOM.empty(elem);
|
|
280
387
|
this.__buttons = [];
|
|
388
|
+
this.__blockButtons = [];
|
|
281
389
|
this.__actionButtons = [];
|
|
282
390
|
this.__hostButtons = [];
|
|
283
391
|
|
|
392
|
+
// Код — одна кнопка на оба вида, как в мессенджерах: и моноширинный, и блок кода. Какой
|
|
393
|
+
// из них применить, решает редактор по выделению, поэтому отдельная кнопка блока не нужна.
|
|
394
|
+
this.__mergedCode = tools.includes(CODE_TOOL) && blocks.includes(CODE_BLOCK);
|
|
395
|
+
const blockTypes = this.__mergedCode ? blocks.filter((type) => type !== CODE_BLOCK) : blocks;
|
|
396
|
+
|
|
397
|
+
// Разделитель ставится только между непустыми группами — иначе панель начиналась бы
|
|
398
|
+
// с линии или показывала две подряд.
|
|
399
|
+
let filled = false;
|
|
400
|
+
const separate = (group: unknown[]) => {
|
|
401
|
+
if (filled && group.length) elem.appendChild(DOM.tag("div", { class: "split" }));
|
|
402
|
+
filled ||= group.length > 0;
|
|
403
|
+
};
|
|
404
|
+
|
|
405
|
+
separate(tools);
|
|
406
|
+
|
|
284
407
|
for (const tool of tools) {
|
|
408
|
+
const merged = this.__mergedCode && tool === CODE_TOOL;
|
|
285
409
|
const def = FORMAT_TOOLS[tool];
|
|
286
410
|
const btn = DOM.tag(
|
|
287
411
|
"button",
|
|
288
|
-
{
|
|
412
|
+
{
|
|
413
|
+
type: "button",
|
|
414
|
+
class: "format-button",
|
|
415
|
+
dataset: { formatTool: tool },
|
|
416
|
+
title: merged ? MERGED_CODE_TITLE : def.title,
|
|
417
|
+
},
|
|
289
418
|
FORMAT_ICONS[tool]
|
|
290
419
|
);
|
|
291
|
-
btn.addEventListener("click", () =>
|
|
420
|
+
btn.addEventListener("click", () =>
|
|
421
|
+
merged ? this.__active?.applyCode?.() : this.__active?.applyFormat(tool)
|
|
422
|
+
);
|
|
292
423
|
|
|
293
424
|
elem.appendChild(btn);
|
|
294
425
|
this.__buttons.push([tool, btn]);
|
|
295
426
|
}
|
|
296
427
|
|
|
297
|
-
|
|
428
|
+
separate(blockTypes);
|
|
429
|
+
|
|
430
|
+
for (const type of blockTypes) {
|
|
431
|
+
const def = BLOCK_TYPES[type];
|
|
432
|
+
const btn = DOM.tag(
|
|
433
|
+
"button",
|
|
434
|
+
{ type: "button", class: "block-button", dataset: { blockType: type }, title: def.title },
|
|
435
|
+
BLOCK_ICONS[type] ?? ""
|
|
436
|
+
);
|
|
437
|
+
btn.addEventListener("click", () => this.__active?.applyBlock?.(type));
|
|
438
|
+
|
|
439
|
+
elem.appendChild(btn);
|
|
440
|
+
this.__blockButtons.push([type, btn]);
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
separate(actions);
|
|
298
444
|
|
|
299
445
|
for (const action of actions) {
|
|
300
446
|
const def = EDITOR_ACTIONS[action];
|
|
301
447
|
const btn = DOM.tag(
|
|
302
448
|
"button",
|
|
303
|
-
{ type: "button", class: "action-button",
|
|
449
|
+
{ type: "button", class: "action-button", dataset: { editorAction: action }, title: def.title },
|
|
304
450
|
ACTION_ICONS[action]
|
|
305
451
|
);
|
|
306
452
|
if (action === "emoji") btn.addEventListener("click", (e) => this.__toggleEmoji(btn, e));
|
|
@@ -310,19 +456,19 @@ class FormatToolbar {
|
|
|
310
456
|
this.__actionButtons.push([action, btn]);
|
|
311
457
|
}
|
|
312
458
|
|
|
313
|
-
|
|
459
|
+
separate(buttons);
|
|
314
460
|
|
|
315
461
|
// кнопки хоста — последними, чтобы штатные не переезжали при их появлении
|
|
316
462
|
for (const button of buttons) {
|
|
317
463
|
const btn = DOM.tag(
|
|
318
464
|
"button",
|
|
319
|
-
{ type: "button", class: "host-button",
|
|
465
|
+
{ type: "button", class: "host-button", dataset: { toolbarButton: button.name }, title: button.title },
|
|
320
466
|
button.icon
|
|
321
467
|
);
|
|
322
|
-
btn.addEventListener("click", () => button.run());
|
|
468
|
+
btn.addEventListener("click", () => this.__hostButton(button.name)?.run());
|
|
323
469
|
|
|
324
470
|
elem.appendChild(btn);
|
|
325
|
-
this.__hostButtons.push([button, btn]);
|
|
471
|
+
this.__hostButtons.push([button.name, btn]);
|
|
326
472
|
}
|
|
327
473
|
|
|
328
474
|
// панель пережила перестройку кнопок — возвращаем её в тулбар, чтобы не собирать заново.
|
|
@@ -388,10 +534,12 @@ class FormatToolbar {
|
|
|
388
534
|
private __buildEmojiPicker(): HTMLElement {
|
|
389
535
|
const picker = DOM.tag("div", { class: `${POPUP_CLASS} ${EMOJI_PICKER_CLASS}` });
|
|
390
536
|
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
picker.appendChild(
|
|
537
|
+
// Прокручивается список, а не сам попап: полоса прокрутки рисуется по краю коробки
|
|
538
|
+
// и перекрывала бы скругление рамки — угол выглядел бы срезанным.
|
|
539
|
+
const list = DOM.tag("div", { class: ["emoji-list", SCROLLABLE_CLASS] });
|
|
540
|
+
picker.appendChild(list);
|
|
541
|
+
|
|
542
|
+
for (const group of EMOJI_GROUPS) list.appendChild(buildEmojiGroup(group));
|
|
395
543
|
|
|
396
544
|
// панель может висеть и вне тулбара, поэтому гасит фокус сама
|
|
397
545
|
picker.addEventListener("mousedown", (e) => e.preventDefault());
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24px" height="24px"><path d="M 5 3 C 3.895 3 3 3.895 3 5 L 3 7 L 3 19 C 3 20.093063 3.9069372 21 5 21 L 19 21 C 20.093063 21 21 20.093063 21 19 L 21 6 L 21 5 C 21 3.895 20.105 3 19 3 L 5 3 z M 5 7 L 19 7 L 19 19 L 5 19 L 5 7 z M 10 10.085938 C 9.819 10.085938 9.638 10.154969 9.5 10.292969 L 7.5 12.292969 C 7.109 12.682969 7.109 13.316031 7.5 13.707031 L 9.5 15.707031 C 9.776 15.983031 10.224 15.983031 10.5 15.707031 C 10.776 15.431031 10.776 14.983031 10.5 14.707031 L 8.7929688 13 L 10.5 11.292969 C 10.776 11.016969 10.776 10.568969 10.5 10.292969 C 10.362 10.154969 10.181 10.085938 10 10.085938 z M 14 10.085938 C 13.819 10.085938 13.638 10.154969 13.5 10.292969 C 13.224 10.568969 13.224 11.016969 13.5 11.292969 L 15.207031 13 L 13.5 14.707031 C 13.224 14.983031 13.224 15.431031 13.5 15.707031 C 13.776 15.983031 14.224 15.983031 14.5 15.707031 L 16.5 13.707031 C 16.891 13.316031 16.891 12.682969 16.5 12.292969 L 14.5 10.292969 C 14.362 10.154969 14.181 10.085938 14 10.085938 z"/></svg>
|
package/svg/mono.svg
ADDED
package/svg/quote.svg
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24px" height="24px"><path d="M20,3H4.011C2.911,3,2.01,3.9,2.01,5L2.001,16.999c0,1.105,0.895,2.001,2,2.001H5.5C5.776,19,6,19.224,6,19.5v2.086 c0,0.891,1.077,1.337,1.707,0.707l3-3C10.895,19.105,11.149,19,11.414,19H20c1.1,0,2-0.9,2-2V5C22,3.9,21.1,3,20,3z M11,11.697 c0,0.197-0.058,0.39-0.168,0.555l-1.596,2.378c-0.25,0.374-0.752,0.483-1.133,0.244c-0.398-0.249-0.512-0.777-0.251-1.167L9,12H8 c-0.552,0-1-0.448-1-1V9c0-0.552,0.448-1,1-1h2c0.552,0,1,0.448,1,1V11.697z M17,11.697c0,0.197-0.058,0.39-0.168,0.555 l-1.596,2.378c-0.25,0.374-0.752,0.483-1.133,0.244c-0.398-0.249-0.512-0.777-0.251-1.167L15,12h-1c-0.552,0-1-0.448-1-1V9 c0-0.552,0.448-1,1-1h2c0.552,0,1,0.448,1,1V11.697z"/></svg>
|
package/svg/spoiler.svg
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path fill-rule="evenodd" d="M2 6h20v12H2V6zm4 5h3v2H6v-2zm4.5 0h3v2h-3v-2zm4.5 0h3v2h-3v-2z"/></svg>
|