@llui/markdown-editor 0.2.1 → 0.2.3
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/dist/__llui_deps.json +21 -0
- package/dist/editor.d.ts +24 -0
- package/dist/editor.d.ts.map +1 -1
- package/dist/editor.js +33 -4
- package/dist/editor.js.map +1 -1
- package/dist/index.d.ts +3 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -2
- package/dist/index.js.map +1 -1
- package/dist/plugins/core.d.ts.map +1 -1
- package/dist/plugins/core.js +3 -40
- package/dist/plugins/core.js.map +1 -1
- package/dist/plugins/inline.d.ts +12 -0
- package/dist/plugins/inline.d.ts.map +1 -0
- package/dist/plugins/inline.js +59 -0
- package/dist/plugins/inline.js.map +1 -0
- package/dist/plugins/single-block.d.ts +29 -0
- package/dist/plugins/single-block.d.ts.map +1 -0
- package/dist/plugins/single-block.js +145 -0
- package/dist/plugins/single-block.js.map +1 -0
- package/dist/state.d.ts +23 -0
- package/dist/state.d.ts.map +1 -1
- package/dist/state.js +22 -0
- package/dist/state.js.map +1 -1
- package/dist/styles/editor.css +25 -0
- package/dist/surfaces/toolbar.d.ts +4 -1
- package/dist/surfaces/toolbar.d.ts.map +1 -1
- package/dist/surfaces/toolbar.js +21 -0
- package/dist/surfaces/toolbar.js.map +1 -1
- package/dist/transformers/gfm.d.ts +4 -0
- package/dist/transformers/gfm.d.ts.map +1 -1
- package/dist/transformers/gfm.js +15 -9
- package/dist/transformers/gfm.js.map +1 -1
- package/package.json +7 -7
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
// The single-block plugin: constrains the editor to exactly ONE paragraph that
|
|
2
|
+
// holds only inline content (bold / italic / strikethrough / code, optionally
|
|
3
|
+
// links). No headings, lists, quotes, code blocks, or multiple paragraphs.
|
|
4
|
+
//
|
|
5
|
+
// It enforces the invariant two ways, so it holds no matter how content arrives
|
|
6
|
+
// (typing, the markdown seed, or a paste):
|
|
7
|
+
// 1. The transformer set contributes ONLY inline text-format transformers, so
|
|
8
|
+
// block markdown (`# `, `- `, `> `, fenced code) is never parsed into block
|
|
9
|
+
// nodes — `registerMarkdownShortcuts` and `$convertFromMarkdownString` both
|
|
10
|
+
// read from this set.
|
|
11
|
+
// 2. A RootNode transform coalesces the document back to a single paragraph of
|
|
12
|
+
// inline leaves whenever anything slips through (a multi-paragraph paste, a
|
|
13
|
+
// multi-line seed). Enter is intercepted so it never splits the paragraph.
|
|
14
|
+
//
|
|
15
|
+
// `singleBlockPlugin()` REPLACES the default plugin set — pass it instead of
|
|
16
|
+
// `corePlugin()`. A dev-only guard warns when block-contributing plugins are
|
|
17
|
+
// composed alongside it (since a plugin is additive and can't un-register them).
|
|
18
|
+
import { $createLineBreakNode, $createParagraphNode, $createTextNode, $getSelection, $isElementNode, $isLineBreakNode, $isParagraphNode, $isRangeSelection, $isTextNode, COMMAND_PRIORITY_HIGH, KEY_ENTER_COMMAND, RootNode, } from 'lexical';
|
|
19
|
+
import { mergeRegister } from '@lexical/utils';
|
|
20
|
+
import { LinkNode } from '@lexical/link';
|
|
21
|
+
import { LINK } from '@lexical/markdown';
|
|
22
|
+
import { inlineItems } from './inline.js';
|
|
23
|
+
import { INLINE_TEXT_TRANSFORMERS } from '../transformers/gfm.js';
|
|
24
|
+
/** Is this node an inline leaf that belongs directly inside a paragraph? */
|
|
25
|
+
function isInlineLeaf(node) {
|
|
26
|
+
return $isTextNode(node) || $isLineBreakNode(node) || ($isElementNode(node) && node.isInline());
|
|
27
|
+
}
|
|
28
|
+
/** Decompose a block element into "lines" — each line is the inline content of
|
|
29
|
+
* one leaf block (paragraph, heading, list item, …). Recurses through nested
|
|
30
|
+
* block containers (a list → its items) so every leaf block becomes its own
|
|
31
|
+
* line; this is what lets adjacent list items be separator-joined rather than
|
|
32
|
+
* mashed together. */
|
|
33
|
+
function blockLines(node) {
|
|
34
|
+
const children = node.getChildren();
|
|
35
|
+
const hasBlockChildren = children.some((c) => $isElementNode(c) && !c.isInline());
|
|
36
|
+
if (!hasBlockChildren) {
|
|
37
|
+
return [children.filter(isInlineLeaf)];
|
|
38
|
+
}
|
|
39
|
+
const lines = [];
|
|
40
|
+
for (const child of children) {
|
|
41
|
+
if ($isElementNode(child) && !child.isInline())
|
|
42
|
+
lines.push(...blockLines(child));
|
|
43
|
+
else if (isInlineLeaf(child))
|
|
44
|
+
lines.push([child]);
|
|
45
|
+
}
|
|
46
|
+
return lines;
|
|
47
|
+
}
|
|
48
|
+
/** Collapse every line break in a paragraph to a single space (single-line
|
|
49
|
+
* mode) so adjacent lines don't mash into one word. */
|
|
50
|
+
function collapseLineBreaks(para) {
|
|
51
|
+
for (const child of para.getChildren()) {
|
|
52
|
+
if ($isLineBreakNode(child))
|
|
53
|
+
child.replace($createTextNode(' '));
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
/** Coalesce the document to exactly one paragraph of inline content. Returns the
|
|
57
|
+
* `RootNode` transform the plugin registers. */
|
|
58
|
+
function makeEnforce(allowLineBreaks) {
|
|
59
|
+
return (root) => {
|
|
60
|
+
const children = root.getChildren();
|
|
61
|
+
// Fast path: already a single paragraph — only fix stray line breaks.
|
|
62
|
+
if (children.length === 1 && $isParagraphNode(children[0])) {
|
|
63
|
+
if (!allowLineBreaks)
|
|
64
|
+
collapseLineBreaks(children[0]);
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
// Gather the inline content of every leaf block as a separate line.
|
|
68
|
+
const lines = [];
|
|
69
|
+
for (const child of children) {
|
|
70
|
+
if ($isElementNode(child)) {
|
|
71
|
+
if (child.isInline())
|
|
72
|
+
lines.push([child]);
|
|
73
|
+
else
|
|
74
|
+
lines.push(...blockLines(child));
|
|
75
|
+
}
|
|
76
|
+
else if (isInlineLeaf(child)) {
|
|
77
|
+
lines.push([child]);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
const para = $createParagraphNode();
|
|
81
|
+
let first = true;
|
|
82
|
+
for (const line of lines) {
|
|
83
|
+
if (line.length === 0)
|
|
84
|
+
continue;
|
|
85
|
+
if (!first)
|
|
86
|
+
para.append(allowLineBreaks ? $createLineBreakNode() : $createTextNode(' '));
|
|
87
|
+
for (const node of line)
|
|
88
|
+
para.append(node);
|
|
89
|
+
first = false;
|
|
90
|
+
}
|
|
91
|
+
if (!allowLineBreaks)
|
|
92
|
+
collapseLineBreaks(para);
|
|
93
|
+
root.clear();
|
|
94
|
+
root.append(para);
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
export function singleBlockPlugin(opts = {}) {
|
|
98
|
+
const allowLineBreaks = opts.allowLineBreaks ?? false;
|
|
99
|
+
const linkEnabled = opts.link ?? false;
|
|
100
|
+
const items = inlineItems(opts.formats);
|
|
101
|
+
const transformers = [...INLINE_TEXT_TRANSFORMERS];
|
|
102
|
+
if (linkEnabled)
|
|
103
|
+
transformers.push(LINK);
|
|
104
|
+
const enforce = makeEnforce(allowLineBreaks);
|
|
105
|
+
const onEnter = (event) => {
|
|
106
|
+
if (!allowLineBreaks) {
|
|
107
|
+
// Inert: never split the paragraph, never insert a break.
|
|
108
|
+
event?.preventDefault();
|
|
109
|
+
return true;
|
|
110
|
+
}
|
|
111
|
+
// Convert a plain Enter (which would split the paragraph) into a soft line
|
|
112
|
+
// break; let Shift+Enter fall through to the default soft-break handler.
|
|
113
|
+
if (event && event.shiftKey)
|
|
114
|
+
return false;
|
|
115
|
+
event?.preventDefault();
|
|
116
|
+
const selection = $getSelection();
|
|
117
|
+
if ($isRangeSelection(selection))
|
|
118
|
+
selection.insertLineBreak();
|
|
119
|
+
return true;
|
|
120
|
+
};
|
|
121
|
+
return {
|
|
122
|
+
name: 'single-block',
|
|
123
|
+
...(linkEnabled ? { nodes: [LinkNode] } : {}),
|
|
124
|
+
transformers,
|
|
125
|
+
items,
|
|
126
|
+
// Dev guard: single-block REPLACES the default plugin set, but a plugin is
|
|
127
|
+
// additive and can't un-register a sibling's block contributions. If another
|
|
128
|
+
// plugin's command items introduce block/structural content, warn loudly so
|
|
129
|
+
// the silently-broken `[corePlugin(), singleBlockPlugin()]` combo is caught.
|
|
130
|
+
onItems: (all) => {
|
|
131
|
+
if (import.meta.env?.DEV !== true)
|
|
132
|
+
return;
|
|
133
|
+
const structural = all.filter((i) => i.group && i.group !== 'inline' && i.group !== 'history');
|
|
134
|
+
if (structural.length === 0)
|
|
135
|
+
return;
|
|
136
|
+
const ids = structural.map((i) => i.id).join(', ');
|
|
137
|
+
console.warn(`[llui] singleBlockPlugin: other plugins contribute block/structural commands (${ids}). ` +
|
|
138
|
+
`single-block REPLACES the default plugin set — pass it INSTEAD OF corePlugin (and any ` +
|
|
139
|
+
`block/insert plugins). Block content may otherwise reappear and the single-paragraph ` +
|
|
140
|
+
`constraint won't hold.`);
|
|
141
|
+
},
|
|
142
|
+
register: (editor) => mergeRegister(editor.registerNodeTransform(RootNode, enforce), editor.registerCommand(KEY_ENTER_COMMAND, onEnter, COMMAND_PRIORITY_HIGH)),
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
//# sourceMappingURL=single-block.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"single-block.js","sourceRoot":"","sources":["../../src/plugins/single-block.ts"],"names":[],"mappings":"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","sourcesContent":["// The single-block plugin: constrains the editor to exactly ONE paragraph that\n// holds only inline content (bold / italic / strikethrough / code, optionally\n// links). No headings, lists, quotes, code blocks, or multiple paragraphs.\n//\n// It enforces the invariant two ways, so it holds no matter how content arrives\n// (typing, the markdown seed, or a paste):\n// 1. The transformer set contributes ONLY inline text-format transformers, so\n// block markdown (`# `, `- `, `> `, fenced code) is never parsed into block\n// nodes — `registerMarkdownShortcuts` and `$convertFromMarkdownString` both\n// read from this set.\n// 2. A RootNode transform coalesces the document back to a single paragraph of\n// inline leaves whenever anything slips through (a multi-paragraph paste, a\n// multi-line seed). Enter is intercepted so it never splits the paragraph.\n//\n// `singleBlockPlugin()` REPLACES the default plugin set — pass it instead of\n// `corePlugin()`. A dev-only guard warns when block-contributing plugins are\n// composed alongside it (since a plugin is additive and can't un-register them).\n\nimport {\n $createLineBreakNode,\n $createParagraphNode,\n $createTextNode,\n $getSelection,\n $isElementNode,\n $isLineBreakNode,\n $isParagraphNode,\n $isRangeSelection,\n $isTextNode,\n COMMAND_PRIORITY_HIGH,\n KEY_ENTER_COMMAND,\n RootNode,\n type ElementNode,\n type LexicalNode,\n type ParagraphNode,\n} from 'lexical'\nimport { mergeRegister } from '@lexical/utils'\nimport { LinkNode } from '@lexical/link'\nimport { LINK, type Transformer } from '@lexical/markdown'\nimport type { CommandItem, MarkdownPlugin } from './types.js'\nimport { inlineItems, type InlineFormat } from './inline.js'\nimport { INLINE_TEXT_TRANSFORMERS } from '../transformers/gfm.js'\n\nexport type { InlineFormat }\n\n// `import.meta.env.DEV` is substituted by Vite/Rollup at build time; raw tsc /\n// vitest see it as undefined, so the dev guard stays off there unless the\n// bundler sets it. (Augmented locally so the package carries no build-tool dep.)\ndeclare global {\n interface ImportMeta {\n env?: { DEV?: boolean; MODE?: string }\n }\n}\n\nexport interface SingleBlockPluginOptions {\n /** Inline formats surfaced as toolbar items.\n * Default `['bold', 'italic', 'strikethrough', 'code']`. NOTE: this limits the\n * toolbar buttons only — markdown syntax (`*x*`) and Ctrl/⌘ shortcuts still\n * apply every inline format, and all inline markdown round-trips regardless. */\n formats?: readonly InlineFormat[]\n /** Allow soft line breaks within the single paragraph. When `false` (default)\n * Enter is inert and pasted/seeded line breaks collapse to spaces — a strict\n * single-line field. When `true`, Enter inserts a `\\n` and merged lines are\n * joined with a line break instead of a space. A new paragraph is never made. */\n allowLineBreaks?: boolean\n /** Register `LinkNode` + the markdown link transformer so inline links\n * round-trip. Default `false`. Compose with `linkPlugin()` for the toolbar\n * button + insert dialog. */\n link?: boolean\n}\n\n/** Is this node an inline leaf that belongs directly inside a paragraph? */\nfunction isInlineLeaf(node: LexicalNode): boolean {\n return $isTextNode(node) || $isLineBreakNode(node) || ($isElementNode(node) && node.isInline())\n}\n\n/** Decompose a block element into \"lines\" — each line is the inline content of\n * one leaf block (paragraph, heading, list item, …). Recurses through nested\n * block containers (a list → its items) so every leaf block becomes its own\n * line; this is what lets adjacent list items be separator-joined rather than\n * mashed together. */\nfunction blockLines(node: ElementNode): LexicalNode[][] {\n const children = node.getChildren()\n const hasBlockChildren = children.some((c) => $isElementNode(c) && !c.isInline())\n if (!hasBlockChildren) {\n return [children.filter(isInlineLeaf)]\n }\n const lines: LexicalNode[][] = []\n for (const child of children) {\n if ($isElementNode(child) && !child.isInline()) lines.push(...blockLines(child))\n else if (isInlineLeaf(child)) lines.push([child])\n }\n return lines\n}\n\n/** Collapse every line break in a paragraph to a single space (single-line\n * mode) so adjacent lines don't mash into one word. */\nfunction collapseLineBreaks(para: ParagraphNode): void {\n for (const child of para.getChildren()) {\n if ($isLineBreakNode(child)) child.replace($createTextNode(' '))\n }\n}\n\n/** Coalesce the document to exactly one paragraph of inline content. Returns the\n * `RootNode` transform the plugin registers. */\nfunction makeEnforce(allowLineBreaks: boolean): (root: RootNode) => void {\n return (root) => {\n const children = root.getChildren()\n\n // Fast path: already a single paragraph — only fix stray line breaks.\n if (children.length === 1 && $isParagraphNode(children[0])) {\n if (!allowLineBreaks) collapseLineBreaks(children[0])\n return\n }\n\n // Gather the inline content of every leaf block as a separate line.\n const lines: LexicalNode[][] = []\n for (const child of children) {\n if ($isElementNode(child)) {\n if (child.isInline()) lines.push([child])\n else lines.push(...blockLines(child))\n } else if (isInlineLeaf(child)) {\n lines.push([child])\n }\n }\n\n const para = $createParagraphNode()\n let first = true\n for (const line of lines) {\n if (line.length === 0) continue\n if (!first) para.append(allowLineBreaks ? $createLineBreakNode() : $createTextNode(' '))\n for (const node of line) para.append(node)\n first = false\n }\n if (!allowLineBreaks) collapseLineBreaks(para)\n root.clear()\n root.append(para)\n }\n}\n\nexport function singleBlockPlugin(opts: SingleBlockPluginOptions = {}): MarkdownPlugin {\n const allowLineBreaks = opts.allowLineBreaks ?? false\n const linkEnabled = opts.link ?? false\n\n const items: CommandItem[] = inlineItems(opts.formats)\n\n const transformers: Transformer[] = [...INLINE_TEXT_TRANSFORMERS]\n if (linkEnabled) transformers.push(LINK)\n\n const enforce = makeEnforce(allowLineBreaks)\n\n const onEnter = (event: KeyboardEvent | null): boolean => {\n if (!allowLineBreaks) {\n // Inert: never split the paragraph, never insert a break.\n event?.preventDefault()\n return true\n }\n // Convert a plain Enter (which would split the paragraph) into a soft line\n // break; let Shift+Enter fall through to the default soft-break handler.\n if (event && event.shiftKey) return false\n event?.preventDefault()\n const selection = $getSelection()\n if ($isRangeSelection(selection)) selection.insertLineBreak()\n return true\n }\n\n return {\n name: 'single-block',\n ...(linkEnabled ? { nodes: [LinkNode] } : {}),\n transformers,\n items,\n // Dev guard: single-block REPLACES the default plugin set, but a plugin is\n // additive and can't un-register a sibling's block contributions. If another\n // plugin's command items introduce block/structural content, warn loudly so\n // the silently-broken `[corePlugin(), singleBlockPlugin()]` combo is caught.\n onItems: (all) => {\n if (import.meta.env?.DEV !== true) return\n const structural = all.filter((i) => i.group && i.group !== 'inline' && i.group !== 'history')\n if (structural.length === 0) return\n const ids = structural.map((i) => i.id).join(', ')\n console.warn(\n `[llui] singleBlockPlugin: other plugins contribute block/structural commands (${ids}). ` +\n `single-block REPLACES the default plugin set — pass it INSTEAD OF corePlugin (and any ` +\n `block/insert plugins). Block content may otherwise reappear and the single-paragraph ` +\n `constraint won't hold.`,\n )\n },\n register: (editor) =>\n mergeRegister(\n editor.registerNodeTransform(RootNode, enforce),\n editor.registerCommand(KEY_ENTER_COMMAND, onEnter, COMMAND_PRIORITY_HIGH),\n ),\n }\n}\n"]}
|
package/dist/state.d.ts
CHANGED
|
@@ -17,6 +17,16 @@ export interface FormatState {
|
|
|
17
17
|
export declare const EMPTY_FORMAT: FormatState;
|
|
18
18
|
/** Which floating surface is currently open. */
|
|
19
19
|
export type OverlayKind = 'none' | 'floating' | 'slash' | 'context' | 'link';
|
|
20
|
+
/** Live collaborative-session status (mirror of the CRDT provider state).
|
|
21
|
+
* `enabled` is false unless the editor was created with a `collab` factory. */
|
|
22
|
+
export interface CollabStatus {
|
|
23
|
+
enabled: boolean;
|
|
24
|
+
connected: boolean;
|
|
25
|
+
synced: boolean;
|
|
26
|
+
/** Remote peers currently present (excludes this client). */
|
|
27
|
+
peers: number;
|
|
28
|
+
}
|
|
29
|
+
export declare const COLLAB_OFF: CollabStatus;
|
|
20
30
|
export interface EditorState {
|
|
21
31
|
/** Last serialized markdown (mirror of the live document). */
|
|
22
32
|
value: string;
|
|
@@ -35,6 +45,8 @@ export interface EditorState {
|
|
|
35
45
|
plugins: Record<string, unknown>;
|
|
36
46
|
dirty: boolean;
|
|
37
47
|
readonly: boolean;
|
|
48
|
+
/** Collaborative-session status (always present; inert unless `collab` set). */
|
|
49
|
+
collab: CollabStatus;
|
|
38
50
|
}
|
|
39
51
|
export type EditorMsg = {
|
|
40
52
|
type: 'markdownChanged';
|
|
@@ -63,6 +75,15 @@ export type EditorMsg = {
|
|
|
63
75
|
} | {
|
|
64
76
|
type: 'setReadOnly';
|
|
65
77
|
readonly: boolean;
|
|
78
|
+
} | {
|
|
79
|
+
type: 'collabStatus';
|
|
80
|
+
connected: boolean;
|
|
81
|
+
} | {
|
|
82
|
+
type: 'collabSync';
|
|
83
|
+
synced: boolean;
|
|
84
|
+
} | {
|
|
85
|
+
type: 'collabPeers';
|
|
86
|
+
peers: number;
|
|
66
87
|
}
|
|
67
88
|
/** Route a message to a plugin's UI reducer (see {@link PluginUI}). */
|
|
68
89
|
| {
|
|
@@ -97,6 +118,8 @@ export type EditorEffect = {
|
|
|
97
118
|
export interface InitOptions {
|
|
98
119
|
value: string;
|
|
99
120
|
readonly: boolean;
|
|
121
|
+
/** Whether a collaborative session is wired (drives `collab.enabled`). */
|
|
122
|
+
collab?: boolean;
|
|
100
123
|
}
|
|
101
124
|
export declare function init(opts: InitOptions): [EditorState, EditorEffect[]];
|
|
102
125
|
export declare function update(state: EditorState, msg: EditorMsg): [EditorState, EditorEffect[]];
|
package/dist/state.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"state.d.ts","sourceRoot":"","sources":["../src/state.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,eAAe,CAAA;AAE9C;qCACqC;AACrC,MAAM,MAAM,SAAS,GACjB,WAAW,GACX,IAAI,GACJ,IAAI,GACJ,IAAI,GACJ,IAAI,GACJ,IAAI,GACJ,IAAI,GACJ,OAAO,GACP,MAAM,GACN,QAAQ,GACR,QAAQ,GACR,OAAO,GACP,OAAO,CAAA;AAEX,mFAAmF;AACnF,MAAM,WAAW,WAAW;IAC1B,IAAI,EAAE,OAAO,CAAA;IACb,MAAM,EAAE,OAAO,CAAA;IACf,aAAa,EAAE,OAAO,CAAA;IACtB,IAAI,EAAE,OAAO,CAAA;IACb,IAAI,EAAE,OAAO,CAAA;IACb,SAAS,EAAE,SAAS,CAAA;IACpB,SAAS,EAAE,SAAS,CAAA;IACpB,OAAO,EAAE,OAAO,CAAA;IAChB,OAAO,EAAE,OAAO,CAAA;CACjB;AAED,eAAO,MAAM,YAAY,EAAE,WAU1B,CAAA;AAED,gDAAgD;AAChD,MAAM,MAAM,WAAW,GAAG,MAAM,GAAG,UAAU,GAAG,OAAO,GAAG,SAAS,GAAG,MAAM,CAAA;AAE5E,MAAM,WAAW,WAAW;IAC1B,8DAA8D;IAC9D,KAAK,EAAE,MAAM,CAAA;IACb,MAAM,EAAE,WAAW,CAAA;IACnB,SAAS,EAAE,MAAM,CAAA;IACjB,SAAS,EAAE,MAAM,CAAA;IACjB,EAAE,EAAE;QACF,aAAa,EAAE,WAAW,CAAA;QAC1B,UAAU,EAAE,MAAM,CAAA;QAClB,IAAI,EAAE;YAAE,CAAC,EAAE,MAAM,CAAC;YAAC,CAAC,EAAE,MAAM,CAAA;SAAE,CAAA;KAC/B,CAAA;IACD,+EAA+E;IAC/E,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;IAChC,KAAK,EAAE,OAAO,CAAA;IACd,QAAQ,EAAE,OAAO,CAAA;
|
|
1
|
+
{"version":3,"file":"state.d.ts","sourceRoot":"","sources":["../src/state.ts"],"names":[],"mappings":"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"}
|
package/dist/state.js
CHANGED
|
@@ -12,6 +12,12 @@ export const EMPTY_FORMAT = {
|
|
|
12
12
|
canUndo: false,
|
|
13
13
|
canRedo: false,
|
|
14
14
|
};
|
|
15
|
+
export const COLLAB_OFF = {
|
|
16
|
+
enabled: false,
|
|
17
|
+
connected: false,
|
|
18
|
+
synced: false,
|
|
19
|
+
peers: 0,
|
|
20
|
+
};
|
|
15
21
|
export function init(opts) {
|
|
16
22
|
const wordCount = countWords(opts.value);
|
|
17
23
|
return [
|
|
@@ -24,6 +30,7 @@ export function init(opts) {
|
|
|
24
30
|
plugins: {},
|
|
25
31
|
dirty: false,
|
|
26
32
|
readonly: opts.readonly,
|
|
33
|
+
collab: { ...COLLAB_OFF, enabled: opts.collab ?? false },
|
|
27
34
|
},
|
|
28
35
|
[],
|
|
29
36
|
];
|
|
@@ -85,6 +92,21 @@ export function update(state, msg) {
|
|
|
85
92
|
return [state, []];
|
|
86
93
|
return [{ ...state, readonly: msg.readonly }, []];
|
|
87
94
|
}
|
|
95
|
+
case 'collabStatus': {
|
|
96
|
+
if (state.collab.connected === msg.connected)
|
|
97
|
+
return [state, []];
|
|
98
|
+
return [{ ...state, collab: { ...state.collab, connected: msg.connected } }, []];
|
|
99
|
+
}
|
|
100
|
+
case 'collabSync': {
|
|
101
|
+
if (state.collab.synced === msg.synced)
|
|
102
|
+
return [state, []];
|
|
103
|
+
return [{ ...state, collab: { ...state.collab, synced: msg.synced } }, []];
|
|
104
|
+
}
|
|
105
|
+
case 'collabPeers': {
|
|
106
|
+
if (state.collab.peers === msg.peers)
|
|
107
|
+
return [state, []];
|
|
108
|
+
return [{ ...state, collab: { ...state.collab, peers: msg.peers } }, []];
|
|
109
|
+
}
|
|
88
110
|
case 'plugin': {
|
|
89
111
|
// Plugin messages are routed by the host's composed reducer (it holds the
|
|
90
112
|
// plugin registry); the pure core reducer treats them as a no-op.
|
package/dist/state.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"state.js","sourceRoot":"","sources":["../src/state.ts"],"names":[],"mappings":"AAAA,gFAAgF;AAChF,iFAAiF;AACjF,0DAA0D;AAkC1D,MAAM,CAAC,MAAM,YAAY,GAAgB;IACvC,IAAI,EAAE,KAAK;IACX,MAAM,EAAE,KAAK;IACb,aAAa,EAAE,KAAK;IACpB,IAAI,EAAE,KAAK;IACX,IAAI,EAAE,KAAK;IACX,SAAS,EAAE,WAAW;IACtB,SAAS,EAAE,IAAI;IACf,OAAO,EAAE,KAAK;IACd,OAAO,EAAE,KAAK;CACf,CAAA;AAsDD,MAAM,UAAU,IAAI,CAAC,IAAiB;IACpC,MAAM,SAAS,GAAG,UAAU,CAAC,IAAI,CAAC,KAAK,CAAC,CAAA;IACxC,OAAO;QACL;YACE,KAAK,EAAE,IAAI,CAAC,KAAK;YACjB,MAAM,EAAE,YAAY;YACpB,SAAS;YACT,SAAS,EAAE,IAAI,CAAC,KAAK,CAAC,MAAM;YAC5B,EAAE,EAAE,EAAE,aAAa,EAAE,MAAM,EAAE,UAAU,EAAE,EAAE,EAAE,IAAI,EAAE,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,EAAE;YACnE,OAAO,EAAE,EAAE;YACX,KAAK,EAAE,KAAK;YACZ,QAAQ,EAAE,IAAI,CAAC,QAAQ;SACxB;QACD,EAAE;KACH,CAAA;AACH,CAAC;AAED,MAAM,UAAU,MAAM,CAAC,KAAkB,EAAE,GAAc;IACvD,QAAQ,GAAG,CAAC,IAAI,EAAE,CAAC;QACjB,KAAK,iBAAiB,CAAC,CAAC,CAAC;YACvB,sEAAsE;YACtE,IAAI,GAAG,CAAC,KAAK,KAAK,KAAK,CAAC,KAAK;gBAAE,OAAO,CAAC,KAAK,EAAE,EAAE,CAAC,CAAA;YACjD,OAAO;gBACL,EAAE,GAAG,KAAK,EAAE,KAAK,EAAE,GAAG,CAAC,KAAK,EAAE,KAAK,EAAE,IAAI,EAAE;gBAC3C,CAAC,EAAE,IAAI,EAAE,YAAY,EAAE,KAAK,EAAE,GAAG,CAAC,KAAK,EAAE,CAAC;aAC3C,CAAA;QACH,CAAC;QACD,KAAK,eAAe,CAAC,CAAC,CAAC;YACrB,OAAO;gBACL,EAAE,GAAG,KAAK,EAAE,MAAM,EAAE,GAAG,CAAC,MAAM,EAAE,SAAS,EAAE,GAAG,CAAC,SAAS,EAAE,SAAS,EAAE,GAAG,CAAC,SAAS,EAAE;gBACpF,CAAC,EAAE,IAAI,EAAE,YAAY,EAAE,MAAM,EAAE,GAAG,CAAC,MAAM,EAAE,CAAC;aAC7C,CAAA;QACH,CAAC;QACD,KAAK,YAAY,CAAC,CAAC,CAAC;YAClB,OAAO,CAAC,KAAK,EAAE,CAAC,EAAE,IAAI,EAAE,aAAa,EAAE,EAAE,EAAE,GAAG,CAAC,EAAE,EAAE,CAAC,CAAC,CAAA;QACvD,CAAC;QACD,KAAK,UAAU,CAAC,CAAC,CAAC;YAChB,0EAA0E;YAC1E,2DAA2D;YAC3D,IAAI,GAAG,CAAC,KAAK,KAAK,KAAK,CAAC,KAAK;gBAAE,OAAO,CAAC,KAAK,EAAE,EAAE,CAAC,CAAA;YACjD,OAAO;gBACL,EAAE,GAAG,KAAK,EAAE,KAAK,EAAE,GAAG,CAAC,KAAK,EAAE,KAAK,EAAE,IAAI,EAAE;gBAC3C,CAAC,EAAE,IAAI,EAAE,YAAY,EAAE,KAAK,EAAE,GAAG,CAAC,KAAK,EAAE,CAAC;aAC3C,CAAA;QACH,CAAC;QACD,KAAK,aAAa,CAAC,CAAC,CAAC;YACnB,OAAO;gBACL;oBACE,GAAG,KAAK;oBACR,EAAE,EAAE;wBACF,GAAG,KAAK,CAAC,EAAE;wBACX,aAAa,EAAE,GAAG,CAAC,OAAO;wBAC1B,UAAU,EAAE,GAAG,CAAC,OAAO,KAAK,OAAO,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,KAAK,CAAC,EAAE,CAAC,UAAU;wBAC9D,IAAI,EAAE,EAAE,CAAC,EAAE,GAAG,CAAC,CAAC,IAAI,KAAK,CAAC,EAAE,CAAC,IAAI,CAAC,CAAC,EAAE,CAAC,EAAE,GAAG,CAAC,CAAC,IAAI,KAAK,CAAC,EAAE,CAAC,IAAI,CAAC,CAAC,EAAE;qBACnE;iBACF;gBACD,EAAE;aACH,CAAA;QACH,CAAC;QACD,KAAK,cAAc,CAAC,CAAC,CAAC;YACpB,IAAI,KAAK,CAAC,EAAE,CAAC,aAAa,KAAK,MAAM;gBAAE,OAAO,CAAC,KAAK,EAAE,EAAE,CAAC,CAAA;YACzD,OAAO,CAAC,EAAE,GAAG,KAAK,EAAE,EAAE,EAAE,EAAE,GAAG,KAAK,CAAC,EAAE,EAAE,aAAa,EAAE,MAAM,EAAE,UAAU,EAAE,EAAE,EAAE,EAAE,EAAE,EAAE,CAAC,CAAA;QACvF,CAAC;QACD,KAAK,YAAY,CAAC,CAAC,CAAC;YAClB,OAAO,CAAC,EAAE,GAAG,KAAK,EAAE,EAAE,EAAE,EAAE,GAAG,KAAK,CAAC,EAAE,EAAE,UAAU,EAAE,GAAG,CAAC,KAAK,EAAE,EAAE,EAAE,EAAE,CAAC,CAAA;QACvE,CAAC;QACD,KAAK,aAAa,CAAC,CAAC,CAAC;YACnB,IAAI,KAAK,CAAC,QAAQ,KAAK,GAAG,CAAC,QAAQ;gBAAE,OAAO,CAAC,KAAK,EAAE,EAAE,CAAC,CAAA;YACvD,OAAO,CAAC,EAAE,GAAG,KAAK,EAAE,QAAQ,EAAE,GAAG,CAAC,QAAQ,EAAE,EAAE,EAAE,CAAC,CAAA;QACnD,CAAC;QACD,KAAK,QAAQ,CAAC,CAAC,CAAC;YACd,0EAA0E;YAC1E,kEAAkE;YAClE,OAAO,CAAC,KAAK,EAAE,EAAE,CAAC,CAAA;QACpB,CAAC;IACH,CAAC;AACH,CAAC;AAED,gFAAgF;AAChF,MAAM,UAAU,UAAU,CAAC,IAAY;IACrC,MAAM,OAAO,GAAG,IAAI,CAAC,IAAI,EAAE,CAAA;IAC3B,OAAO,OAAO,KAAK,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC,MAAM,CAAA;AACzD,CAAC","sourcesContent":["// The editor's TEA state, messages, effects, and pure reducer. Lexical owns the\n// live document; this state holds JSON-serializable mirrors/derivations only, so\n// `update` stays pure and DOM-free (fully unit-testable).\n\nimport type { Alignment } from '@llui/lexical'\n\n/** The block kind at the selection — base rich-text kinds plus list/code,\n * resolved by the markdown layer. */\nexport type BlockType =\n | 'paragraph'\n | 'h1'\n | 'h2'\n | 'h3'\n | 'h4'\n | 'h5'\n | 'h6'\n | 'quote'\n | 'code'\n | 'bullet'\n | 'number'\n | 'check'\n | 'other'\n\n/** The toolbar-facing format surface at the current selection (all primitives). */\nexport interface FormatState {\n bold: boolean\n italic: boolean\n strikethrough: boolean\n code: boolean\n link: boolean\n blockType: BlockType\n alignment: Alignment\n canUndo: boolean\n canRedo: boolean\n}\n\nexport const EMPTY_FORMAT: FormatState = {\n bold: false,\n italic: false,\n strikethrough: false,\n code: false,\n link: false,\n blockType: 'paragraph',\n alignment: null,\n canUndo: false,\n canRedo: false,\n}\n\n/** Which floating surface is currently open. */\nexport type OverlayKind = 'none' | 'floating' | 'slash' | 'context' | 'link'\n\nexport interface EditorState {\n /** Last serialized markdown (mirror of the live document). */\n value: string\n format: FormatState\n wordCount: number\n charCount: number\n ui: {\n activeOverlay: OverlayKind\n slashQuery: string\n menu: { x: number; y: number }\n }\n /** Per-plugin UI state slices, keyed by plugin name (see {@link PluginUI}). */\n plugins: Record<string, unknown>\n dirty: boolean\n readonly: boolean\n}\n\nexport type EditorMsg =\n | { type: 'markdownChanged'; value: string }\n | { type: 'formatChanged'; format: FormatState; wordCount: number; charCount: number }\n | { type: 'runCommand'; id: string }\n | { type: 'setValue'; value: string }\n | { type: 'openOverlay'; overlay: OverlayKind; x?: number; y?: number }\n | { type: 'closeOverlay' }\n | { type: 'slashQuery'; query: string }\n | { type: 'setReadOnly'; readonly: boolean }\n /** Route a message to a plugin's UI reducer (see {@link PluginUI}). */\n | { type: 'plugin'; name: string; msg: unknown }\n\n/** The subset of messages a plugin may emit through its `PluginContext` (e.g. a\n * `register` listener routing an editor event into its own plugin UI). */\nexport type EditorOutMsg = Extract<\n EditorMsg,\n { type: 'openOverlay' | 'closeOverlay' | 'slashQuery' | 'plugin' }\n>\n\nexport type EditorEffect =\n | { type: 'execCommand'; id: string }\n | { type: 'applyValue'; value: string }\n | { type: 'emitChange'; value: string }\n | { type: 'emitFormat'; format: FormatState }\n /** An effect produced by a plugin's UI reducer (see {@link PluginUI}). */\n | { type: 'pluginEffect'; name: string; effect: unknown }\n\nexport interface InitOptions {\n value: string\n readonly: boolean\n}\n\nexport function init(opts: InitOptions): [EditorState, EditorEffect[]] {\n const wordCount = countWords(opts.value)\n return [\n {\n value: opts.value,\n format: EMPTY_FORMAT,\n wordCount,\n charCount: opts.value.length,\n ui: { activeOverlay: 'none', slashQuery: '', menu: { x: 0, y: 0 } },\n plugins: {},\n dirty: false,\n readonly: opts.readonly,\n },\n [],\n ]\n}\n\nexport function update(state: EditorState, msg: EditorMsg): [EditorState, EditorEffect[]] {\n switch (msg.type) {\n case 'markdownChanged': {\n // Idempotent: re-emitting the current value is a no-op (echo safety).\n if (msg.value === state.value) return [state, []]\n return [\n { ...state, value: msg.value, dirty: true },\n [{ type: 'emitChange', value: msg.value }],\n ]\n }\n case 'formatChanged': {\n return [\n { ...state, format: msg.format, wordCount: msg.wordCount, charCount: msg.charCount },\n [{ type: 'emitFormat', format: msg.format }],\n ]\n }\n case 'runCommand': {\n return [state, [{ type: 'execCommand', id: msg.id }]]\n }\n case 'setValue': {\n // External markdown push (via the component handle). Idempotent; does not\n // re-emit onChange (the consumer already owns this value).\n if (msg.value === state.value) return [state, []]\n return [\n { ...state, value: msg.value, dirty: true },\n [{ type: 'applyValue', value: msg.value }],\n ]\n }\n case 'openOverlay': {\n return [\n {\n ...state,\n ui: {\n ...state.ui,\n activeOverlay: msg.overlay,\n slashQuery: msg.overlay === 'slash' ? '' : state.ui.slashQuery,\n menu: { x: msg.x ?? state.ui.menu.x, y: msg.y ?? state.ui.menu.y },\n },\n },\n [],\n ]\n }\n case 'closeOverlay': {\n if (state.ui.activeOverlay === 'none') return [state, []]\n return [{ ...state, ui: { ...state.ui, activeOverlay: 'none', slashQuery: '' } }, []]\n }\n case 'slashQuery': {\n return [{ ...state, ui: { ...state.ui, slashQuery: msg.query } }, []]\n }\n case 'setReadOnly': {\n if (state.readonly === msg.readonly) return [state, []]\n return [{ ...state, readonly: msg.readonly }, []]\n }\n case 'plugin': {\n // Plugin messages are routed by the host's composed reducer (it holds the\n // plugin registry); the pure core reducer treats them as a no-op.\n return [state, []]\n }\n }\n}\n\n/** Count whitespace-delimited words (shared by init and the format handler). */\nexport function countWords(text: string): number {\n const trimmed = text.trim()\n return trimmed === '' ? 0 : trimmed.split(/\\s+/).length\n}\n"]}
|
|
1
|
+
{"version":3,"file":"state.js","sourceRoot":"","sources":["../src/state.ts"],"names":[],"mappings":"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","sourcesContent":["// The editor's TEA state, messages, effects, and pure reducer. Lexical owns the\n// live document; this state holds JSON-serializable mirrors/derivations only, so\n// `update` stays pure and DOM-free (fully unit-testable).\n\nimport type { Alignment } from '@llui/lexical'\n\n/** The block kind at the selection — base rich-text kinds plus list/code,\n * resolved by the markdown layer. */\nexport type BlockType =\n | 'paragraph'\n | 'h1'\n | 'h2'\n | 'h3'\n | 'h4'\n | 'h5'\n | 'h6'\n | 'quote'\n | 'code'\n | 'bullet'\n | 'number'\n | 'check'\n | 'other'\n\n/** The toolbar-facing format surface at the current selection (all primitives). */\nexport interface FormatState {\n bold: boolean\n italic: boolean\n strikethrough: boolean\n code: boolean\n link: boolean\n blockType: BlockType\n alignment: Alignment\n canUndo: boolean\n canRedo: boolean\n}\n\nexport const EMPTY_FORMAT: FormatState = {\n bold: false,\n italic: false,\n strikethrough: false,\n code: false,\n link: false,\n blockType: 'paragraph',\n alignment: null,\n canUndo: false,\n canRedo: false,\n}\n\n/** Which floating surface is currently open. */\nexport type OverlayKind = 'none' | 'floating' | 'slash' | 'context' | 'link'\n\n/** Live collaborative-session status (mirror of the CRDT provider state).\n * `enabled` is false unless the editor was created with a `collab` factory. */\nexport interface CollabStatus {\n enabled: boolean\n connected: boolean\n synced: boolean\n /** Remote peers currently present (excludes this client). */\n peers: number\n}\n\nexport const COLLAB_OFF: CollabStatus = {\n enabled: false,\n connected: false,\n synced: false,\n peers: 0,\n}\n\nexport interface EditorState {\n /** Last serialized markdown (mirror of the live document). */\n value: string\n format: FormatState\n wordCount: number\n charCount: number\n ui: {\n activeOverlay: OverlayKind\n slashQuery: string\n menu: { x: number; y: number }\n }\n /** Per-plugin UI state slices, keyed by plugin name (see {@link PluginUI}). */\n plugins: Record<string, unknown>\n dirty: boolean\n readonly: boolean\n /** Collaborative-session status (always present; inert unless `collab` set). */\n collab: CollabStatus\n}\n\nexport type EditorMsg =\n | { type: 'markdownChanged'; value: string }\n | { type: 'formatChanged'; format: FormatState; wordCount: number; charCount: number }\n | { type: 'runCommand'; id: string }\n | { type: 'setValue'; value: string }\n | { type: 'openOverlay'; overlay: OverlayKind; x?: number; y?: number }\n | { type: 'closeOverlay' }\n | { type: 'slashQuery'; query: string }\n | { type: 'setReadOnly'; readonly: boolean }\n | { type: 'collabStatus'; connected: boolean }\n | { type: 'collabSync'; synced: boolean }\n | { type: 'collabPeers'; peers: number }\n /** Route a message to a plugin's UI reducer (see {@link PluginUI}). */\n | { type: 'plugin'; name: string; msg: unknown }\n\n/** The subset of messages a plugin may emit through its `PluginContext` (e.g. a\n * `register` listener routing an editor event into its own plugin UI). */\nexport type EditorOutMsg = Extract<\n EditorMsg,\n { type: 'openOverlay' | 'closeOverlay' | 'slashQuery' | 'plugin' }\n>\n\nexport type EditorEffect =\n | { type: 'execCommand'; id: string }\n | { type: 'applyValue'; value: string }\n | { type: 'emitChange'; value: string }\n | { type: 'emitFormat'; format: FormatState }\n /** An effect produced by a plugin's UI reducer (see {@link PluginUI}). */\n | { type: 'pluginEffect'; name: string; effect: unknown }\n\nexport interface InitOptions {\n value: string\n readonly: boolean\n /** Whether a collaborative session is wired (drives `collab.enabled`). */\n collab?: boolean\n}\n\nexport function init(opts: InitOptions): [EditorState, EditorEffect[]] {\n const wordCount = countWords(opts.value)\n return [\n {\n value: opts.value,\n format: EMPTY_FORMAT,\n wordCount,\n charCount: opts.value.length,\n ui: { activeOverlay: 'none', slashQuery: '', menu: { x: 0, y: 0 } },\n plugins: {},\n dirty: false,\n readonly: opts.readonly,\n collab: { ...COLLAB_OFF, enabled: opts.collab ?? false },\n },\n [],\n ]\n}\n\nexport function update(state: EditorState, msg: EditorMsg): [EditorState, EditorEffect[]] {\n switch (msg.type) {\n case 'markdownChanged': {\n // Idempotent: re-emitting the current value is a no-op (echo safety).\n if (msg.value === state.value) return [state, []]\n return [\n { ...state, value: msg.value, dirty: true },\n [{ type: 'emitChange', value: msg.value }],\n ]\n }\n case 'formatChanged': {\n return [\n { ...state, format: msg.format, wordCount: msg.wordCount, charCount: msg.charCount },\n [{ type: 'emitFormat', format: msg.format }],\n ]\n }\n case 'runCommand': {\n return [state, [{ type: 'execCommand', id: msg.id }]]\n }\n case 'setValue': {\n // External markdown push (via the component handle). Idempotent; does not\n // re-emit onChange (the consumer already owns this value).\n if (msg.value === state.value) return [state, []]\n return [\n { ...state, value: msg.value, dirty: true },\n [{ type: 'applyValue', value: msg.value }],\n ]\n }\n case 'openOverlay': {\n return [\n {\n ...state,\n ui: {\n ...state.ui,\n activeOverlay: msg.overlay,\n slashQuery: msg.overlay === 'slash' ? '' : state.ui.slashQuery,\n menu: { x: msg.x ?? state.ui.menu.x, y: msg.y ?? state.ui.menu.y },\n },\n },\n [],\n ]\n }\n case 'closeOverlay': {\n if (state.ui.activeOverlay === 'none') return [state, []]\n return [{ ...state, ui: { ...state.ui, activeOverlay: 'none', slashQuery: '' } }, []]\n }\n case 'slashQuery': {\n return [{ ...state, ui: { ...state.ui, slashQuery: msg.query } }, []]\n }\n case 'setReadOnly': {\n if (state.readonly === msg.readonly) return [state, []]\n return [{ ...state, readonly: msg.readonly }, []]\n }\n case 'collabStatus': {\n if (state.collab.connected === msg.connected) return [state, []]\n return [{ ...state, collab: { ...state.collab, connected: msg.connected } }, []]\n }\n case 'collabSync': {\n if (state.collab.synced === msg.synced) return [state, []]\n return [{ ...state, collab: { ...state.collab, synced: msg.synced } }, []]\n }\n case 'collabPeers': {\n if (state.collab.peers === msg.peers) return [state, []]\n return [{ ...state, collab: { ...state.collab, peers: msg.peers } }, []]\n }\n case 'plugin': {\n // Plugin messages are routed by the host's composed reducer (it holds the\n // plugin registry); the pure core reducer treats them as a no-op.\n return [state, []]\n }\n }\n}\n\n/** Count whitespace-delimited words (shared by init and the format handler). */\nexport function countWords(text: string): number {\n const trimmed = text.trim()\n return trimmed === '' ? 0 : trimmed.split(/\\s+/).length\n}\n"]}
|
package/dist/styles/editor.css
CHANGED
|
@@ -170,6 +170,31 @@
|
|
|
170
170
|
opacity: 0.4;
|
|
171
171
|
cursor: default;
|
|
172
172
|
}
|
|
173
|
+
[data-scope='md-toolbar'][data-part='presence'] {
|
|
174
|
+
display: flex;
|
|
175
|
+
align-items: center;
|
|
176
|
+
gap: 0.35rem;
|
|
177
|
+
margin-left: auto;
|
|
178
|
+
padding-left: 0.5rem;
|
|
179
|
+
font-size: 0.8rem;
|
|
180
|
+
color: var(--md-text-soft, var(--md-text));
|
|
181
|
+
}
|
|
182
|
+
[data-scope='md-toolbar'] [data-part='presence-status'] {
|
|
183
|
+
width: 0.5rem;
|
|
184
|
+
height: 0.5rem;
|
|
185
|
+
border-radius: 50%;
|
|
186
|
+
background: var(--md-border);
|
|
187
|
+
flex: none;
|
|
188
|
+
}
|
|
189
|
+
[data-scope='md-toolbar'] [data-part='presence-status'][data-status='synced'] {
|
|
190
|
+
background: #16a34a;
|
|
191
|
+
}
|
|
192
|
+
[data-scope='md-toolbar'] [data-part='presence-status'][data-status='connecting'] {
|
|
193
|
+
background: #d97706;
|
|
194
|
+
}
|
|
195
|
+
[data-scope='md-toolbar'] [data-part='presence-status'][data-status='offline'] {
|
|
196
|
+
background: #9ca3af;
|
|
197
|
+
}
|
|
173
198
|
[data-scope='md-toolbar'] [data-part='block-select'] {
|
|
174
199
|
font: inherit;
|
|
175
200
|
font-size: 0.85rem;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { type Mountable, type Send, type Signal } from '@llui/dom';
|
|
2
2
|
import type { CommandItem } from '../plugins/types.js';
|
|
3
|
-
import type { EditorMsg, FormatState } from '../state.js';
|
|
3
|
+
import type { CollabStatus, EditorMsg, FormatState } from '../state.js';
|
|
4
4
|
export interface ToolbarItemParts {
|
|
5
5
|
type: 'button';
|
|
6
6
|
'data-scope': 'md-toolbar';
|
|
@@ -41,6 +41,9 @@ export interface ToolbarOptions {
|
|
|
41
41
|
/** Render the `block` group as a `<select>` dropdown instead of buttons
|
|
42
42
|
* (default true). */
|
|
43
43
|
blockSelect?: boolean;
|
|
44
|
+
/** Collaborative-session status. When supplied AND `enabled`, the toolbar
|
|
45
|
+
* appends a presence indicator (connection dot + live peer count). */
|
|
46
|
+
collab?: Signal<CollabStatus>;
|
|
44
47
|
'aria-label'?: string;
|
|
45
48
|
}
|
|
46
49
|
/** A ready-made grouped toolbar. Items not surfaced to `'toolbar'` are dropped. */
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"toolbar.d.ts","sourceRoot":"","sources":["../../src/surfaces/toolbar.ts"],"names":[],"mappings":"AAIA,OAAO,EASL,KAAK,SAAS,EACd,KAAK,IAAI,EACT,KAAK,MAAM,EACZ,MAAM,WAAW,CAAA;AAClB,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,qBAAqB,CAAA;AACtD,OAAO,KAAK,EAAE,SAAS,EAAE,WAAW,EAAE,MAAM,aAAa,CAAA;
|
|
1
|
+
{"version":3,"file":"toolbar.d.ts","sourceRoot":"","sources":["../../src/surfaces/toolbar.ts"],"names":[],"mappings":"AAIA,OAAO,EASL,KAAK,SAAS,EACd,KAAK,IAAI,EACT,KAAK,MAAM,EACZ,MAAM,WAAW,CAAA;AAClB,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,qBAAqB,CAAA;AACtD,OAAO,KAAK,EAAE,YAAY,EAAE,SAAS,EAAE,WAAW,EAAE,MAAM,aAAa,CAAA;AAEvE,MAAM,WAAW,gBAAgB;IAC/B,IAAI,EAAE,QAAQ,CAAA;IACd,YAAY,EAAE,YAAY,CAAA;IAC1B,WAAW,EAAE,MAAM,CAAA;IACnB,SAAS,EAAE,MAAM,CAAA;IACjB,YAAY,EAAE,MAAM,CAAA;IACpB,KAAK,EAAE,MAAM,CAAA;IACb,cAAc,EAAE,MAAM,CAAC,MAAM,GAAG,OAAO,CAAC,CAAA;IACxC,eAAe,EAAE,MAAM,CAAC,MAAM,GAAG,SAAS,CAAC,CAAA;IAC3C,QAAQ,EAAE,MAAM,CAAC,OAAO,CAAC,CAAA;IACzB,aAAa,EAAE,MAAM,CAAC,EAAE,GAAG,SAAS,CAAC,CAAA;IACrC,OAAO,EAAE,CAAC,CAAC,EAAE,UAAU,KAAK,IAAI,CAAA;CACjC;AAED,MAAM,WAAW,YAAY;IAC3B,IAAI,EAAE;QACJ,IAAI,EAAE,SAAS,CAAA;QACf,YAAY,EAAE,MAAM,CAAA;QACpB,YAAY,EAAE,YAAY,CAAA;QAC1B,WAAW,EAAE,MAAM,CAAA;KACpB,CAAA;IACD,IAAI,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,gBAAgB,CAAA;CACvC;AAED;+EAC+E;AAC/E,wBAAgB,cAAc,CAC5B,MAAM,EAAE,MAAM,CAAC,WAAW,CAAC,EAC3B,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC,EACrB,KAAK,EAAE,SAAS,WAAW,EAAE,GAC5B,YAAY,CA2Bd;AAUD;;8BAE8B;AAC9B,eAAO,MAAM,cAAc,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAoB3D,CAAA;AAuBD,MAAM,WAAW,cAAc;IAC7B,MAAM,EAAE,MAAM,CAAC,WAAW,CAAC,CAAA;IAC3B,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC,CAAA;IACrB,KAAK,EAAE,SAAS,WAAW,EAAE,CAAA;IAC7B,4EAA4E;IAC5E,MAAM,CAAC,EAAE,SAAS,CAAC,SAAS,MAAM,EAAE,CAAC,EAAE,CAAA;IACvC,6EAA6E;IAC7E,MAAM,CAAC,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAA;IACzC;yBACqB;IACrB,WAAW,CAAC,EAAE,OAAO,CAAA;IACrB;0EACsE;IACtE,MAAM,CAAC,EAAE,MAAM,CAAC,YAAY,CAAC,CAAA;IAC7B,YAAY,CAAC,EAAE,MAAM,CAAA;CACtB;AAgDD,mFAAmF;AACnF,wBAAgB,OAAO,CAAC,IAAI,EAAE,cAAc,GAAG,SAAS,CAgDvD"}
|
package/dist/surfaces/toolbar.js
CHANGED
|
@@ -81,6 +81,23 @@ function groupItems(items) {
|
|
|
81
81
|
}
|
|
82
82
|
return ordered;
|
|
83
83
|
}
|
|
84
|
+
/** Connection/presence indicator for a collaborative session. */
|
|
85
|
+
function presenceIndicator(collab) {
|
|
86
|
+
const statusOf = (c) => c.connected ? (c.synced ? 'synced' : 'connecting') : 'offline';
|
|
87
|
+
return div({ 'data-scope': 'md-toolbar', 'data-part': 'presence' }, [
|
|
88
|
+
span({
|
|
89
|
+
'data-part': 'presence-status',
|
|
90
|
+
'data-status': collab.map(statusOf),
|
|
91
|
+
'aria-hidden': 'true',
|
|
92
|
+
title: collab.map(statusOf),
|
|
93
|
+
}),
|
|
94
|
+
span({
|
|
95
|
+
'data-part': 'presence-peers',
|
|
96
|
+
role: 'status',
|
|
97
|
+
'aria-live': 'polite',
|
|
98
|
+
}, [text(collab.map((c) => (c.peers === 0 ? 'Only you' : `${c.peers + 1} editing`)))]),
|
|
99
|
+
]);
|
|
100
|
+
}
|
|
84
101
|
/** Render the `block` group as a native `<select>` of block types. */
|
|
85
102
|
function blockTypeSelect(format, send, blockItems) {
|
|
86
103
|
return select({
|
|
@@ -129,6 +146,10 @@ export function toolbar(opts) {
|
|
|
129
146
|
]);
|
|
130
147
|
})));
|
|
131
148
|
}
|
|
149
|
+
// Presence indicator (constant `enabled` after init, so a one-shot peek is safe).
|
|
150
|
+
if (opts.collab && opts.collab.peek().enabled) {
|
|
151
|
+
children.push(presenceIndicator(opts.collab));
|
|
152
|
+
}
|
|
132
153
|
return div({ ...parts.root, ...(opts['aria-label'] ? { 'aria-label': opts['aria-label'] } : {}) }, children);
|
|
133
154
|
}
|
|
134
155
|
//# sourceMappingURL=toolbar.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"toolbar.js","sourceRoot":"","sources":["../../src/surfaces/toolbar.ts"],"names":[],"mappings":"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","sourcesContent":["// Toolbar surface. `connectToolbar` returns a parts bag (the @llui/components\n// idiom) driven reactively by the format signal; `toolbar` is a ready-made\n// grouped toolbar Mountable for consumers who don't want to hand-render buttons.\n\nimport {\n button,\n div,\n option,\n select,\n span,\n text,\n tagSend,\n unsafeHtml,\n type Mountable,\n type Send,\n type Signal,\n} from '@llui/dom'\nimport type { CommandItem } from '../plugins/types.js'\nimport type { EditorMsg, FormatState } from '../state.js'\n\nexport interface ToolbarItemParts {\n type: 'button'\n 'data-scope': 'md-toolbar'\n 'data-part': 'item'\n 'data-id': string\n 'aria-label': string\n title: string\n 'aria-pressed': Signal<'true' | 'false'>\n 'aria-disabled': Signal<'true' | undefined>\n disabled: Signal<boolean>\n 'data-active': Signal<'' | undefined>\n onClick: (e: MouseEvent) => void\n}\n\nexport interface ToolbarParts {\n root: {\n role: 'toolbar'\n 'aria-label': string\n 'data-scope': 'md-toolbar'\n 'data-part': 'root'\n }\n item: (id: string) => ToolbarItemParts\n}\n\n/** Build reactive toolbar parts from the format signal. Spread `item(id)` onto a\n * `<button>`; `aria-pressed` / `data-active` / `disabled` track the format. */\nexport function connectToolbar(\n format: Signal<FormatState>,\n send: Send<EditorMsg>,\n items: readonly CommandItem[],\n): ToolbarParts {\n const byId = new Map(items.map((i) => [i.id, i]))\n return {\n root: {\n role: 'toolbar',\n 'aria-label': 'Formatting',\n 'data-scope': 'md-toolbar',\n 'data-part': 'root',\n },\n item: (id) => {\n const item = byId.get(id)\n const label = item?.label ?? id\n return {\n type: 'button',\n 'data-scope': 'md-toolbar',\n 'data-part': 'item',\n 'data-id': id,\n 'aria-label': label,\n title: label,\n 'aria-pressed': format.map((f) => (item?.isActive?.(f) ? 'true' : 'false')),\n 'aria-disabled': format.map((f) => (item?.isDisabled?.(f) ? 'true' : undefined)),\n disabled: format.map((f) => item?.isDisabled?.(f) ?? false),\n 'data-active': format.map((f) => (item?.isActive?.(f) ? '' : undefined)),\n onClick: tagSend(send, ['runCommand'], () => send({ type: 'runCommand', id })),\n }\n },\n }\n}\n\n/** Inline SVG icon (monochrome, inherits `currentColor`) used where a glyph\n * reads poorly. A glyph string starting with `<svg` is rendered as markup. */\nconst LINK_ICON =\n '<svg viewBox=\"0 0 24 24\" width=\"15\" height=\"15\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\"><path d=\"M9.5 13.5a3.5 3.5 0 0 0 5 0l3-3a3.5 3.5 0 1 0-5-5l-1 1\"/><path d=\"M14.5 10.5a3.5 3.5 0 0 0-5 0l-3 3a3.5 3.5 0 1 0 5 5l1-1\"/></svg>'\n\nconst IMAGE_ICON =\n '<svg viewBox=\"0 0 24 24\" width=\"15\" height=\"15\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\"><rect x=\"3\" y=\"4\" width=\"18\" height=\"16\" rx=\"2\"/><circle cx=\"9\" cy=\"9.5\" r=\"1.6\"/><path d=\"m4 18 5-5 4 4 3-3 4 4\"/></svg>'\n\n/** Compact glyphs so the default toolbar reads as a real toolbar without icon\n * assets. SVG strings render as icons; everything else as text. Override via\n * `ToolbarOptions.glyphs`. */\nexport const DEFAULT_GLYPHS: Readonly<Record<string, string>> = {\n bold: 'B',\n italic: 'I',\n strikethrough: 'S',\n code: '</>',\n link: LINK_ICON,\n image: IMAGE_ICON,\n hr: '—',\n table: '▦',\n paragraph: '¶',\n h1: 'H1',\n h2: 'H2',\n h3: 'H3',\n quote: '❝',\n codeBlock: '{ }',\n bulletList: '•',\n numberList: '1.',\n checkList: '☑',\n undo: '↺',\n redo: '↻',\n}\n\nconst GROUP_ORDER = ['inline', 'block', 'list', 'insert', 'history']\n\nfunction groupItems(items: readonly CommandItem[]): string[][] {\n const groups = new Map<string, string[]>()\n for (const item of items) {\n const key = item.group ?? 'other'\n const list = groups.get(key) ?? []\n list.push(item.id)\n groups.set(key, list)\n }\n const ordered: string[][] = []\n for (const key of GROUP_ORDER) {\n const list = groups.get(key)\n if (list) ordered.push(list)\n }\n for (const [key, list] of groups) {\n if (!GROUP_ORDER.includes(key)) ordered.push(list)\n }\n return ordered\n}\n\nexport interface ToolbarOptions {\n format: Signal<FormatState>\n send: Send<EditorMsg>\n items: readonly CommandItem[]\n /** Explicit grouped layout of ids; defaults to grouping by `item.group`. */\n groups?: readonly (readonly string[])[]\n /** Glyph overrides (id → text/emoji). Merged over {@link DEFAULT_GLYPHS}. */\n glyphs?: Readonly<Record<string, string>>\n /** Render the `block` group as a `<select>` dropdown instead of buttons\n * (default true). */\n blockSelect?: boolean\n 'aria-label'?: string\n}\n\n/** Render the `block` group as a native `<select>` of block types. */\nfunction blockTypeSelect(\n format: Signal<FormatState>,\n send: Send<EditorMsg>,\n blockItems: readonly CommandItem[],\n): Mountable {\n return select(\n {\n 'data-scope': 'md-toolbar',\n 'data-part': 'block-select',\n 'aria-label': 'Block type',\n value: format.map((f) => blockItems.find((i) => i.isActive?.(f))?.id ?? ''),\n onChange: (e: Event) => {\n const id = (e.currentTarget as HTMLSelectElement).value\n if (id) send({ type: 'runCommand', id })\n },\n },\n [\n option({ value: '', hidden: true }, [text('—')]),\n ...blockItems.map((i) => option({ value: i.id }, [text(i.label)])),\n ],\n )\n}\n\n/** A ready-made grouped toolbar. Items not surfaced to `'toolbar'` are dropped. */\nexport function toolbar(opts: ToolbarOptions): Mountable {\n const surfaceItems = opts.items.filter((i) => i.surfaces?.includes('toolbar') ?? true)\n const byId = new Map(surfaceItems.map((i) => [i.id, i]))\n const parts = connectToolbar(opts.format, opts.send, surfaceItems)\n const glyphs = { ...DEFAULT_GLYPHS, ...(opts.glyphs ?? {}) }\n\n const useBlockSelect = opts.blockSelect !== false && !opts.groups\n const blockItems = useBlockSelect ? surfaceItems.filter((i) => i.group === 'block') : []\n const buttonItems = useBlockSelect\n ? surfaceItems.filter((i) => i.group !== 'block')\n : surfaceItems\n const groups = opts.groups ?? groupItems(buttonItems)\n\n const children: Mountable[] = []\n if (blockItems.length > 0) {\n children.push(\n div({ 'data-scope': 'md-toolbar', 'data-part': 'group' }, [\n blockTypeSelect(opts.format, opts.send, blockItems),\n ]),\n )\n }\n for (const ids of groups) {\n const present = ids.filter((id) => byId.has(id))\n if (present.length === 0) continue\n children.push(\n div(\n { 'data-scope': 'md-toolbar', 'data-part': 'group' },\n present.map((id) => {\n const item = byId.get(id)!\n const glyph = glyphs[id] ?? item.label\n const glyphNode = glyph.trimStart().startsWith('<svg') ? unsafeHtml(glyph) : text(glyph)\n return button({ ...parts.item(id) }, [\n span({ 'data-part': 'glyph', 'aria-hidden': 'true' }, [glyphNode]),\n ])\n }),\n ),\n )\n }\n\n return div(\n { ...parts.root, ...(opts['aria-label'] ? { 'aria-label': opts['aria-label'] } : {}) },\n children,\n )\n}\n"]}
|
|
1
|
+
{"version":3,"file":"toolbar.js","sourceRoot":"","sources":["../../src/surfaces/toolbar.ts"],"names":[],"mappings":"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","sourcesContent":["// Toolbar surface. `connectToolbar` returns a parts bag (the @llui/components\n// idiom) driven reactively by the format signal; `toolbar` is a ready-made\n// grouped toolbar Mountable for consumers who don't want to hand-render buttons.\n\nimport {\n button,\n div,\n option,\n select,\n span,\n text,\n tagSend,\n unsafeHtml,\n type Mountable,\n type Send,\n type Signal,\n} from '@llui/dom'\nimport type { CommandItem } from '../plugins/types.js'\nimport type { CollabStatus, EditorMsg, FormatState } from '../state.js'\n\nexport interface ToolbarItemParts {\n type: 'button'\n 'data-scope': 'md-toolbar'\n 'data-part': 'item'\n 'data-id': string\n 'aria-label': string\n title: string\n 'aria-pressed': Signal<'true' | 'false'>\n 'aria-disabled': Signal<'true' | undefined>\n disabled: Signal<boolean>\n 'data-active': Signal<'' | undefined>\n onClick: (e: MouseEvent) => void\n}\n\nexport interface ToolbarParts {\n root: {\n role: 'toolbar'\n 'aria-label': string\n 'data-scope': 'md-toolbar'\n 'data-part': 'root'\n }\n item: (id: string) => ToolbarItemParts\n}\n\n/** Build reactive toolbar parts from the format signal. Spread `item(id)` onto a\n * `<button>`; `aria-pressed` / `data-active` / `disabled` track the format. */\nexport function connectToolbar(\n format: Signal<FormatState>,\n send: Send<EditorMsg>,\n items: readonly CommandItem[],\n): ToolbarParts {\n const byId = new Map(items.map((i) => [i.id, i]))\n return {\n root: {\n role: 'toolbar',\n 'aria-label': 'Formatting',\n 'data-scope': 'md-toolbar',\n 'data-part': 'root',\n },\n item: (id) => {\n const item = byId.get(id)\n const label = item?.label ?? id\n return {\n type: 'button',\n 'data-scope': 'md-toolbar',\n 'data-part': 'item',\n 'data-id': id,\n 'aria-label': label,\n title: label,\n 'aria-pressed': format.map((f) => (item?.isActive?.(f) ? 'true' : 'false')),\n 'aria-disabled': format.map((f) => (item?.isDisabled?.(f) ? 'true' : undefined)),\n disabled: format.map((f) => item?.isDisabled?.(f) ?? false),\n 'data-active': format.map((f) => (item?.isActive?.(f) ? '' : undefined)),\n onClick: tagSend(send, ['runCommand'], () => send({ type: 'runCommand', id })),\n }\n },\n }\n}\n\n/** Inline SVG icon (monochrome, inherits `currentColor`) used where a glyph\n * reads poorly. A glyph string starting with `<svg` is rendered as markup. */\nconst LINK_ICON =\n '<svg viewBox=\"0 0 24 24\" width=\"15\" height=\"15\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\"><path d=\"M9.5 13.5a3.5 3.5 0 0 0 5 0l3-3a3.5 3.5 0 1 0-5-5l-1 1\"/><path d=\"M14.5 10.5a3.5 3.5 0 0 0-5 0l-3 3a3.5 3.5 0 1 0 5 5l1-1\"/></svg>'\n\nconst IMAGE_ICON =\n '<svg viewBox=\"0 0 24 24\" width=\"15\" height=\"15\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\"><rect x=\"3\" y=\"4\" width=\"18\" height=\"16\" rx=\"2\"/><circle cx=\"9\" cy=\"9.5\" r=\"1.6\"/><path d=\"m4 18 5-5 4 4 3-3 4 4\"/></svg>'\n\n/** Compact glyphs so the default toolbar reads as a real toolbar without icon\n * assets. SVG strings render as icons; everything else as text. Override via\n * `ToolbarOptions.glyphs`. */\nexport const DEFAULT_GLYPHS: Readonly<Record<string, string>> = {\n bold: 'B',\n italic: 'I',\n strikethrough: 'S',\n code: '</>',\n link: LINK_ICON,\n image: IMAGE_ICON,\n hr: '—',\n table: '▦',\n paragraph: '¶',\n h1: 'H1',\n h2: 'H2',\n h3: 'H3',\n quote: '❝',\n codeBlock: '{ }',\n bulletList: '•',\n numberList: '1.',\n checkList: '☑',\n undo: '↺',\n redo: '↻',\n}\n\nconst GROUP_ORDER = ['inline', 'block', 'list', 'insert', 'history']\n\nfunction groupItems(items: readonly CommandItem[]): string[][] {\n const groups = new Map<string, string[]>()\n for (const item of items) {\n const key = item.group ?? 'other'\n const list = groups.get(key) ?? []\n list.push(item.id)\n groups.set(key, list)\n }\n const ordered: string[][] = []\n for (const key of GROUP_ORDER) {\n const list = groups.get(key)\n if (list) ordered.push(list)\n }\n for (const [key, list] of groups) {\n if (!GROUP_ORDER.includes(key)) ordered.push(list)\n }\n return ordered\n}\n\nexport interface ToolbarOptions {\n format: Signal<FormatState>\n send: Send<EditorMsg>\n items: readonly CommandItem[]\n /** Explicit grouped layout of ids; defaults to grouping by `item.group`. */\n groups?: readonly (readonly string[])[]\n /** Glyph overrides (id → text/emoji). Merged over {@link DEFAULT_GLYPHS}. */\n glyphs?: Readonly<Record<string, string>>\n /** Render the `block` group as a `<select>` dropdown instead of buttons\n * (default true). */\n blockSelect?: boolean\n /** Collaborative-session status. When supplied AND `enabled`, the toolbar\n * appends a presence indicator (connection dot + live peer count). */\n collab?: Signal<CollabStatus>\n 'aria-label'?: string\n}\n\n/** Connection/presence indicator for a collaborative session. */\nfunction presenceIndicator(collab: Signal<CollabStatus>): Mountable {\n const statusOf = (c: CollabStatus): string =>\n c.connected ? (c.synced ? 'synced' : 'connecting') : 'offline'\n return div({ 'data-scope': 'md-toolbar', 'data-part': 'presence' }, [\n span({\n 'data-part': 'presence-status',\n 'data-status': collab.map(statusOf),\n 'aria-hidden': 'true',\n title: collab.map(statusOf),\n }),\n span(\n {\n 'data-part': 'presence-peers',\n role: 'status',\n 'aria-live': 'polite',\n },\n [text(collab.map((c) => (c.peers === 0 ? 'Only you' : `${c.peers + 1} editing`)))],\n ),\n ])\n}\n\n/** Render the `block` group as a native `<select>` of block types. */\nfunction blockTypeSelect(\n format: Signal<FormatState>,\n send: Send<EditorMsg>,\n blockItems: readonly CommandItem[],\n): Mountable {\n return select(\n {\n 'data-scope': 'md-toolbar',\n 'data-part': 'block-select',\n 'aria-label': 'Block type',\n value: format.map((f) => blockItems.find((i) => i.isActive?.(f))?.id ?? ''),\n onChange: (e: Event) => {\n const id = (e.currentTarget as HTMLSelectElement).value\n if (id) send({ type: 'runCommand', id })\n },\n },\n [\n option({ value: '', hidden: true }, [text('—')]),\n ...blockItems.map((i) => option({ value: i.id }, [text(i.label)])),\n ],\n )\n}\n\n/** A ready-made grouped toolbar. Items not surfaced to `'toolbar'` are dropped. */\nexport function toolbar(opts: ToolbarOptions): Mountable {\n const surfaceItems = opts.items.filter((i) => i.surfaces?.includes('toolbar') ?? true)\n const byId = new Map(surfaceItems.map((i) => [i.id, i]))\n const parts = connectToolbar(opts.format, opts.send, surfaceItems)\n const glyphs = { ...DEFAULT_GLYPHS, ...(opts.glyphs ?? {}) }\n\n const useBlockSelect = opts.blockSelect !== false && !opts.groups\n const blockItems = useBlockSelect ? surfaceItems.filter((i) => i.group === 'block') : []\n const buttonItems = useBlockSelect\n ? surfaceItems.filter((i) => i.group !== 'block')\n : surfaceItems\n const groups = opts.groups ?? groupItems(buttonItems)\n\n const children: Mountable[] = []\n if (blockItems.length > 0) {\n children.push(\n div({ 'data-scope': 'md-toolbar', 'data-part': 'group' }, [\n blockTypeSelect(opts.format, opts.send, blockItems),\n ]),\n )\n }\n for (const ids of groups) {\n const present = ids.filter((id) => byId.has(id))\n if (present.length === 0) continue\n children.push(\n div(\n { 'data-scope': 'md-toolbar', 'data-part': 'group' },\n present.map((id) => {\n const item = byId.get(id)!\n const glyph = glyphs[id] ?? item.label\n const glyphNode = glyph.trimStart().startsWith('<svg') ? unsafeHtml(glyph) : text(glyph)\n return button({ ...parts.item(id) }, [\n span({ 'data-part': 'glyph', 'aria-hidden': 'true' }, [glyphNode]),\n ])\n }),\n ),\n )\n }\n\n // Presence indicator (constant `enabled` after init, so a one-shot peek is safe).\n if (opts.collab && opts.collab.peek().enabled) {\n children.push(presenceIndicator(opts.collab))\n }\n\n return div(\n { ...parts.root, ...(opts['aria-label'] ? { 'aria-label': opts['aria-label'] } : {}) },\n children,\n )\n}\n"]}
|
|
@@ -2,6 +2,10 @@ import type { Klass, LexicalNode } from 'lexical';
|
|
|
2
2
|
import { type Transformer } from '@lexical/markdown';
|
|
3
3
|
/** Node classes required to render the GFM superset. */
|
|
4
4
|
export declare const GFM_NODES: ReadonlyArray<Klass<LexicalNode>>;
|
|
5
|
+
/** Inline text-format transformers (no block nodes, no node registration). These
|
|
6
|
+
* are the only transformers a single-block / inline-only editor needs; `LINK` is
|
|
7
|
+
* kept separate since it requires `LinkNode` to be registered. */
|
|
8
|
+
export declare const INLINE_TEXT_TRANSFORMERS: readonly Transformer[];
|
|
5
9
|
/** Markdown ↔ node transformers for the GFM superset. */
|
|
6
10
|
export declare const GFM_TRANSFORMERS: readonly Transformer[];
|
|
7
11
|
//# sourceMappingURL=gfm.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"gfm.d.ts","sourceRoot":"","sources":["../../src/transformers/gfm.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,KAAK,EAAE,WAAW,EAAE,MAAM,SAAS,CAAA;AAOjD,OAAO,EACL,KAAK,WAAW,EAiBjB,MAAM,mBAAmB,CAAA;AAE1B,wDAAwD;AACxD,eAAO,MAAM,SAAS,EAAE,aAAa,CAAC,KAAK,CAAC,WAAW,CAAC,CAQvD,CAAA;AAED,yDAAyD;AACzD,eAAO,MAAM,gBAAgB,EAAE,SAAS,WAAW,
|
|
1
|
+
{"version":3,"file":"gfm.d.ts","sourceRoot":"","sources":["../../src/transformers/gfm.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,KAAK,EAAE,WAAW,EAAE,MAAM,SAAS,CAAA;AAOjD,OAAO,EACL,KAAK,WAAW,EAiBjB,MAAM,mBAAmB,CAAA;AAE1B,wDAAwD;AACxD,eAAO,MAAM,SAAS,EAAE,aAAa,CAAC,KAAK,CAAC,WAAW,CAAC,CAQvD,CAAA;AAED;;kEAEkE;AAClE,eAAO,MAAM,wBAAwB,EAAE,SAAS,WAAW,EAU1D,CAAA;AAED,yDAAyD;AACzD,eAAO,MAAM,gBAAgB,EAAE,SAAS,WAAW,EAWlD,CAAA"}
|
package/dist/transformers/gfm.js
CHANGED
|
@@ -17,6 +17,20 @@ export const GFM_NODES = [
|
|
|
17
17
|
CodeHighlightNode,
|
|
18
18
|
LinkNode,
|
|
19
19
|
];
|
|
20
|
+
/** Inline text-format transformers (no block nodes, no node registration). These
|
|
21
|
+
* are the only transformers a single-block / inline-only editor needs; `LINK` is
|
|
22
|
+
* kept separate since it requires `LinkNode` to be registered. */
|
|
23
|
+
export const INLINE_TEXT_TRANSFORMERS = [
|
|
24
|
+
BOLD_ITALIC_STAR,
|
|
25
|
+
BOLD_ITALIC_UNDERSCORE,
|
|
26
|
+
BOLD_STAR,
|
|
27
|
+
BOLD_UNDERSCORE,
|
|
28
|
+
ITALIC_STAR,
|
|
29
|
+
ITALIC_UNDERSCORE,
|
|
30
|
+
STRIKETHROUGH,
|
|
31
|
+
INLINE_CODE,
|
|
32
|
+
HIGHLIGHT,
|
|
33
|
+
];
|
|
20
34
|
/** Markdown ↔ node transformers for the GFM superset. */
|
|
21
35
|
export const GFM_TRANSFORMERS = [
|
|
22
36
|
HEADING,
|
|
@@ -27,15 +41,7 @@ export const GFM_TRANSFORMERS = [
|
|
|
27
41
|
UNORDERED_LIST,
|
|
28
42
|
ORDERED_LIST,
|
|
29
43
|
CODE,
|
|
30
|
-
|
|
31
|
-
BOLD_ITALIC_UNDERSCORE,
|
|
32
|
-
BOLD_STAR,
|
|
33
|
-
BOLD_UNDERSCORE,
|
|
34
|
-
ITALIC_STAR,
|
|
35
|
-
ITALIC_UNDERSCORE,
|
|
36
|
-
STRIKETHROUGH,
|
|
37
|
-
INLINE_CODE,
|
|
38
|
-
HIGHLIGHT,
|
|
44
|
+
...INLINE_TEXT_TRANSFORMERS,
|
|
39
45
|
LINK,
|
|
40
46
|
];
|
|
41
47
|
//# sourceMappingURL=gfm.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"gfm.js","sourceRoot":"","sources":["../../src/transformers/gfm.ts"],"names":[],"mappings":"AAAA,8EAA8E;AAC9E,kFAAkF;AAGlF,OAAO,EAAE,WAAW,EAAE,SAAS,EAAE,MAAM,oBAAoB,CAAA;AAC3D,OAAO,EAAE,QAAQ,EAAE,YAAY,EAAE,MAAM,eAAe,CAAA;AACtD,OAAO,EAAE,QAAQ,EAAE,MAAM,eAAe,CAAA;AACxC,gFAAgF;AAChF,wEAAwE;AACxE,OAAO,EAAE,QAAQ,EAAE,iBAAiB,EAAE,MAAM,oBAAoB,CAAA;AAChE,OAAO,EAEL,OAAO,EACP,KAAK,EACL,cAAc,EACd,YAAY,EACZ,UAAU,EACV,IAAI,EACJ,gBAAgB,EAChB,sBAAsB,EACtB,SAAS,EACT,eAAe,EACf,WAAW,EACX,iBAAiB,EACjB,aAAa,EACb,WAAW,EACX,SAAS,EACT,IAAI,GACL,MAAM,mBAAmB,CAAA;AAE1B,wDAAwD;AACxD,MAAM,CAAC,MAAM,SAAS,GAAsC;IAC1D,WAAW;IACX,SAAS;IACT,QAAQ;IACR,YAAY;IACZ,QAAQ;IACR,iBAAiB;IACjB,QAAQ;CACT,CAAA;AAED,yDAAyD;AACzD,MAAM,CAAC,MAAM,gBAAgB,GAA2B;IACtD,OAAO;IACP,KAAK;IACL,4EAA4E;IAC5E,2EAA2E;IAC3E,UAAU;IACV,cAAc;IACd,YAAY;IACZ,IAAI;IACJ,
|
|
1
|
+
{"version":3,"file":"gfm.js","sourceRoot":"","sources":["../../src/transformers/gfm.ts"],"names":[],"mappings":"AAAA,8EAA8E;AAC9E,kFAAkF;AAGlF,OAAO,EAAE,WAAW,EAAE,SAAS,EAAE,MAAM,oBAAoB,CAAA;AAC3D,OAAO,EAAE,QAAQ,EAAE,YAAY,EAAE,MAAM,eAAe,CAAA;AACtD,OAAO,EAAE,QAAQ,EAAE,MAAM,eAAe,CAAA;AACxC,gFAAgF;AAChF,wEAAwE;AACxE,OAAO,EAAE,QAAQ,EAAE,iBAAiB,EAAE,MAAM,oBAAoB,CAAA;AAChE,OAAO,EAEL,OAAO,EACP,KAAK,EACL,cAAc,EACd,YAAY,EACZ,UAAU,EACV,IAAI,EACJ,gBAAgB,EAChB,sBAAsB,EACtB,SAAS,EACT,eAAe,EACf,WAAW,EACX,iBAAiB,EACjB,aAAa,EACb,WAAW,EACX,SAAS,EACT,IAAI,GACL,MAAM,mBAAmB,CAAA;AAE1B,wDAAwD;AACxD,MAAM,CAAC,MAAM,SAAS,GAAsC;IAC1D,WAAW;IACX,SAAS;IACT,QAAQ;IACR,YAAY;IACZ,QAAQ;IACR,iBAAiB;IACjB,QAAQ;CACT,CAAA;AAED;;kEAEkE;AAClE,MAAM,CAAC,MAAM,wBAAwB,GAA2B;IAC9D,gBAAgB;IAChB,sBAAsB;IACtB,SAAS;IACT,eAAe;IACf,WAAW;IACX,iBAAiB;IACjB,aAAa;IACb,WAAW;IACX,SAAS;CACV,CAAA;AAED,yDAAyD;AACzD,MAAM,CAAC,MAAM,gBAAgB,GAA2B;IACtD,OAAO;IACP,KAAK;IACL,4EAA4E;IAC5E,2EAA2E;IAC3E,UAAU;IACV,cAAc;IACd,YAAY;IACZ,IAAI;IACJ,GAAG,wBAAwB;IAC3B,IAAI;CACL,CAAA","sourcesContent":["// The built-in GFM superset: node classes + the explicit transformer set that\n// maps exactly to those nodes (no surprise nodes — HR/tables are opt-in plugins).\n\nimport type { Klass, LexicalNode } from 'lexical'\nimport { HeadingNode, QuoteNode } from '@lexical/rich-text'\nimport { ListNode, ListItemNode } from '@lexical/list'\nimport { LinkNode } from '@lexical/link'\n// `@lexical/code-core` (not `@lexical/code`) keeps Prism out of the bundle — we\n// never register syntax highlighting, so plain CodeNode is all we need.\nimport { CodeNode, CodeHighlightNode } from '@lexical/code-core'\nimport {\n type Transformer,\n HEADING,\n QUOTE,\n UNORDERED_LIST,\n ORDERED_LIST,\n CHECK_LIST,\n CODE,\n BOLD_ITALIC_STAR,\n BOLD_ITALIC_UNDERSCORE,\n BOLD_STAR,\n BOLD_UNDERSCORE,\n ITALIC_STAR,\n ITALIC_UNDERSCORE,\n STRIKETHROUGH,\n INLINE_CODE,\n HIGHLIGHT,\n LINK,\n} from '@lexical/markdown'\n\n/** Node classes required to render the GFM superset. */\nexport const GFM_NODES: ReadonlyArray<Klass<LexicalNode>> = [\n HeadingNode,\n QuoteNode,\n ListNode,\n ListItemNode,\n CodeNode,\n CodeHighlightNode,\n LinkNode,\n]\n\n/** Inline text-format transformers (no block nodes, no node registration). These\n * are the only transformers a single-block / inline-only editor needs; `LINK` is\n * kept separate since it requires `LinkNode` to be registered. */\nexport const INLINE_TEXT_TRANSFORMERS: readonly Transformer[] = [\n BOLD_ITALIC_STAR,\n BOLD_ITALIC_UNDERSCORE,\n BOLD_STAR,\n BOLD_UNDERSCORE,\n ITALIC_STAR,\n ITALIC_UNDERSCORE,\n STRIKETHROUGH,\n INLINE_CODE,\n HIGHLIGHT,\n]\n\n/** Markdown ↔ node transformers for the GFM superset. */\nexport const GFM_TRANSFORMERS: readonly Transformer[] = [\n HEADING,\n QUOTE,\n // CHECK_LIST must precede the plain list transformers: `- [ ]`/`- [x]` also\n // match `- `, so UNORDERED_LIST would otherwise swallow it as bullet text.\n CHECK_LIST,\n UNORDERED_LIST,\n ORDERED_LIST,\n CODE,\n ...INLINE_TEXT_TRANSFORMERS,\n LINK,\n]\n"]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@llui/markdown-editor",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.3",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
@@ -24,9 +24,9 @@
|
|
|
24
24
|
"./styles/editor.css": "./dist/styles/editor.css"
|
|
25
25
|
},
|
|
26
26
|
"peerDependencies": {
|
|
27
|
-
"@llui/dom": "^0.11.
|
|
28
|
-
"@llui/lexical": "^0.2.
|
|
29
|
-
"@llui/components": "^0.11.
|
|
27
|
+
"@llui/dom": "^0.11.1",
|
|
28
|
+
"@llui/lexical": "^0.2.2",
|
|
29
|
+
"@llui/components": "^0.11.2",
|
|
30
30
|
"lexical": "^0.45.0",
|
|
31
31
|
"@lexical/markdown": "^0.45.0",
|
|
32
32
|
"@lexical/rich-text": "^0.45.0",
|
|
@@ -50,9 +50,9 @@
|
|
|
50
50
|
"typescript": "^6.0.0",
|
|
51
51
|
"vitest": "^4.1.2",
|
|
52
52
|
"@lexical/code-core": "^0.45.0",
|
|
53
|
-
"@llui/dom": "0.11.
|
|
54
|
-
"@llui/lexical": "0.2.
|
|
55
|
-
"@llui/components": "0.11.
|
|
53
|
+
"@llui/dom": "0.11.1",
|
|
54
|
+
"@llui/lexical": "0.2.2",
|
|
55
|
+
"@llui/components": "0.11.2"
|
|
56
56
|
},
|
|
57
57
|
"sideEffects": [
|
|
58
58
|
"./dist/styles/editor.css"
|