draftly 0.1.0-alpha.0 → 1.0.0-alpha.2

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.
Files changed (74) hide show
  1. package/README.md +346 -0
  2. package/dist/chunk-2B3A3VSQ.cjs +3382 -0
  3. package/dist/chunk-2B3A3VSQ.cjs.map +1 -0
  4. package/dist/chunk-72ZYRGRT.cjs +399 -0
  5. package/dist/chunk-72ZYRGRT.cjs.map +1 -0
  6. package/dist/chunk-CG4M4TC7.js +392 -0
  7. package/dist/chunk-CG4M4TC7.js.map +1 -0
  8. package/dist/chunk-DFQYXFOP.js +86 -0
  9. package/dist/chunk-DFQYXFOP.js.map +1 -0
  10. package/dist/chunk-HPSMS2WB.js +182 -0
  11. package/dist/chunk-HPSMS2WB.js.map +1 -0
  12. package/dist/chunk-KBQDZ5IW.cjs +192 -0
  13. package/dist/chunk-KBQDZ5IW.cjs.map +1 -0
  14. package/dist/chunk-KDEDLC3D.cjs +93 -0
  15. package/dist/chunk-KDEDLC3D.cjs.map +1 -0
  16. package/dist/chunk-N3WL3XPB.js +3360 -0
  17. package/dist/chunk-N3WL3XPB.js.map +1 -0
  18. package/dist/draftly-BLnx3uGX.d.cts +293 -0
  19. package/dist/draftly-BLnx3uGX.d.ts +293 -0
  20. package/dist/editor/index.cjs +57 -0
  21. package/dist/editor/index.cjs.map +1 -0
  22. package/dist/editor/index.d.cts +15 -0
  23. package/dist/editor/index.d.ts +15 -0
  24. package/dist/editor/index.js +4 -0
  25. package/dist/editor/index.js.map +1 -0
  26. package/dist/index.cjs +120 -1129
  27. package/dist/index.cjs.map +1 -1
  28. package/dist/index.d.cts +9 -257
  29. package/dist/index.d.ts +9 -257
  30. package/dist/index.js +4 -1126
  31. package/dist/index.js.map +1 -1
  32. package/dist/plugins/index.cjs +66 -0
  33. package/dist/plugins/index.cjs.map +1 -0
  34. package/dist/plugins/index.d.cts +515 -0
  35. package/dist/plugins/index.d.ts +515 -0
  36. package/dist/plugins/index.js +5 -0
  37. package/dist/plugins/index.js.map +1 -0
  38. package/dist/preview/index.cjs +29 -0
  39. package/dist/preview/index.cjs.map +1 -0
  40. package/dist/preview/index.d.cts +143 -0
  41. package/dist/preview/index.d.ts +143 -0
  42. package/dist/preview/index.js +4 -0
  43. package/dist/preview/index.js.map +1 -0
  44. package/package.json +22 -1
  45. package/src/{draftly.ts → editor/draftly.ts} +28 -27
  46. package/src/editor/index.ts +5 -0
  47. package/src/{plugin.ts → editor/plugin.ts} +62 -34
  48. package/src/editor/theme.ts +62 -0
  49. package/src/editor/utils.ts +143 -0
  50. package/src/{view-plugin.ts → editor/view-plugin.ts} +25 -140
  51. package/src/index.ts +4 -7
  52. package/src/plugins/code-plugin.ts +1119 -0
  53. package/src/plugins/heading-plugin.ts +108 -74
  54. package/src/plugins/hr-plugin.ts +102 -0
  55. package/src/plugins/html-plugin.ts +59 -53
  56. package/src/plugins/image-plugin.ts +447 -0
  57. package/src/plugins/index.ts +57 -0
  58. package/src/plugins/inline-plugin.ts +178 -39
  59. package/src/plugins/link-plugin.ts +509 -0
  60. package/src/plugins/list-plugin.ts +492 -211
  61. package/src/plugins/math-plugin.ts +514 -0
  62. package/src/plugins/mermaid-plugin.ts +500 -0
  63. package/src/plugins/paragraph-plugin.ts +38 -0
  64. package/src/plugins/quote-plugin.ts +146 -0
  65. package/src/preview/context.ts +38 -0
  66. package/src/preview/css-generator.ts +51 -0
  67. package/src/preview/default-renderers.ts +29 -0
  68. package/src/preview/index.ts +20 -0
  69. package/src/preview/preview.ts +40 -0
  70. package/src/preview/renderer.ts +157 -0
  71. package/src/preview/types.ts +72 -0
  72. package/src/plugins/plugins.ts +0 -9
  73. package/src/theme.ts +0 -86
  74. package/src/utils.ts +0 -21
@@ -1,211 +1,492 @@
1
- import { Decoration, EditorView, WidgetType } from "@codemirror/view";
2
- import { syntaxTree } from "@codemirror/language";
3
- import { Extension } from "@codemirror/state";
4
- import { DecorationContext, DecorationPlugin } from "../plugin";
5
-
6
- /**
7
- * Mark decorations for list items
8
- */
9
- const listMarkDecorations = {
10
- "list-mark-ul": Decoration.mark({ class: "cm-draftly-list-mark-ul" }),
11
- "list-mark-ol": Decoration.mark({ class: "cm-draftly-list-mark-ol" }),
12
- "task-marker": Decoration.mark({ class: "cm-draftly-task-marker" }),
13
- };
14
-
15
- /**
16
- * Task checkbox widget
17
- */
18
- export class TaskCheckboxWidget extends WidgetType {
19
- constructor(readonly checked: boolean) {
20
- super();
21
- }
22
-
23
- eq(other: TaskCheckboxWidget): boolean {
24
- return other.checked === this.checked;
25
- }
26
-
27
- toDOM(view: EditorView): HTMLElement {
28
- const wrap = document.createElement("span");
29
- wrap.className = `cm-draftly-task-checkbox ${this.checked ? "checked" : ""}`;
30
- wrap.setAttribute("aria-hidden", "true");
31
-
32
- const checkbox = document.createElement("input");
33
- checkbox.type = "checkbox";
34
- checkbox.checked = this.checked;
35
- checkbox.tabIndex = -1;
36
-
37
- checkbox.addEventListener("mousedown", (e) => {
38
- e.preventDefault();
39
- const pos = view.posAtDOM(wrap);
40
- // Find the task marker in the document and toggle it
41
- const line = view.state.doc.lineAt(pos);
42
- const match = line.text.match(/^(\s*[-*+]\s*)\[([ xX])\]/);
43
- if (match) {
44
- const markerStart = line.from + match[1]!.length + 1;
45
- const newChar = this.checked ? " " : "x";
46
- view.dispatch({
47
- changes: { from: markerStart, to: markerStart + 1, insert: newChar },
48
- });
49
- }
50
- });
51
-
52
- wrap.appendChild(checkbox);
53
-
54
- // Add a label for better accessibility and click area
55
- // const label = document.createElement("span");
56
- // wrap.appendChild(label);
57
-
58
- return wrap;
59
- }
60
-
61
- ignoreEvent(): boolean {
62
- return false;
63
- }
64
- }
65
-
66
- /**
67
- * ListPlugin - Decorates markdown lists
68
- *
69
- * Handles:
70
- * - Unordered lists (bullet points) - Auto-styles markers (*, -, +)
71
- * - Ordered lists (numbered) - Auto-styles numbers (1., 2.)
72
- * - Task lists (checkboxes) - Replaces [ ]/[x] with interactive widget when not editing
73
- */
74
- export class ListPlugin extends DecorationPlugin {
75
- readonly name = "list";
76
- readonly version = "1.0.0";
77
-
78
- /**
79
- * Moderate priority
80
- */
81
- override get decorationPriority(): number {
82
- return 20;
83
- }
84
-
85
- /**
86
- * Get the extensions for this plugin (theme)
87
- */
88
- override getExtensions(): Extension[] {
89
- return [listTheme];
90
- }
91
-
92
- /**
93
- * Build list decorations by iterating the syntax tree
94
- */
95
- override buildDecorations(ctx: DecorationContext): void {
96
- const { view, decorations } = ctx;
97
- const tree = syntaxTree(view.state);
98
-
99
- tree.iterate({
100
- enter: (node) => {
101
- const { from, to, name } = node;
102
- const line = view.state.doc.lineAt(from);
103
- const cursorInLine = ctx.cursorInRange(line.from, line.to);
104
-
105
- // Handle list markers (bullets, numbers)
106
- if (name === "ListMark") {
107
- // Determine list type by checking grandparent (BulletList vs OrderedList)
108
- // Parent is ListItem, Grandparent is the list container
109
- const parent = node.node.parent;
110
- const grandparent = parent?.parent;
111
- const listType = grandparent?.name;
112
-
113
- if (!cursorInLine) {
114
- if (listType === "OrderedList") {
115
- decorations.push(listMarkDecorations["list-mark-ol"].range(from, to));
116
- } else {
117
- // Default to generic/unordered for BulletList or others
118
- decorations.push(listMarkDecorations["list-mark-ul"].range(from, to));
119
- }
120
- }
121
- }
122
-
123
- // Handle task lists
124
- if (name === "TaskMarker") {
125
- const text = view.state.sliceDoc(from, to);
126
- const isChecked = text.includes("x") || text.includes("X");
127
-
128
- if (cursorInLine) {
129
- // If editing the line, just style the marker text
130
- decorations.push(listMarkDecorations["task-marker"].range(from, to));
131
- } else {
132
- // If not editing, REPLACE the marker text with the widget
133
- decorations.push(
134
- Decoration.replace({
135
- widget: new TaskCheckboxWidget(isChecked),
136
- }).range(from, to)
137
- );
138
- }
139
- }
140
- },
141
- });
142
- }
143
- }
144
-
145
- /**
146
- * Theme for list styling
147
- */
148
- const listTheme = EditorView.theme({
149
- // Unordered List markers (*, -, +)
150
- ".cm-draftly-list-mark-ul": {
151
- position: "relative",
152
- },
153
-
154
- ".cm-draftly-list-mark-ul > span": {
155
- visibility: "hidden",
156
- },
157
-
158
- ".cm-draftly-list-mark-ul::after": {
159
- content: '"•"',
160
- position: "absolute",
161
- left: "50%",
162
- top: "50%",
163
- transform: "translate(-50%, -50%)",
164
- color: "var(--color-link)",
165
- fontWeight: "bold",
166
- pointerEvents: "none",
167
- },
168
-
169
- // Ordered List markers (1., 2.)
170
- ".cm-draftly-list-mark-ol": {
171
- color: "var(--draftly-highlight, #a4a4a4)",
172
- fontFamily: "monospace",
173
- marginRight: "2px",
174
- },
175
-
176
- // Task markers text ([ ] or [x]) - visible only when editing
177
- ".cm-draftly-task-marker": {
178
- color: "var(--draftly-highlight, #a4a4a4)",
179
- fontFamily: "monospace",
180
- },
181
-
182
- // Task Checkbox Widget
183
- ".cm-draftly-task-checkbox": {
184
- display: "inline-flex",
185
- verticalAlign: "middle",
186
- marginRight: "0.3em",
187
- cursor: "pointer",
188
- userSelect: "none",
189
- alignItems: "center",
190
- height: "1.2em",
191
- },
192
-
193
- ".cm-draftly-task-checkbox input": {
194
- cursor: "pointer",
195
- margin: 0,
196
- width: "1.1em",
197
- height: "1.1em",
198
- appearance: "none",
199
- border: "1px solid",
200
- borderRadius: "0.25em",
201
- backgroundColor: "transparent",
202
- position: "relative",
203
- },
204
-
205
- ".cm-draftly-task-checkbox.checked input::after": {
206
- content: '"✓"',
207
- position: "absolute",
208
- left: "1px",
209
- top: "-3px",
210
- },
211
- });
1
+ import { Decoration, EditorView, KeyBinding, WidgetType } from "@codemirror/view";
2
+ import { syntaxTree } from "@codemirror/language";
3
+ import { DecorationContext, DecorationPlugin } from "../editor/plugin";
4
+ import { createTheme } from "../editor";
5
+ import { Range } from "@codemirror/state";
6
+ import { SyntaxNode } from "@lezer/common";
7
+
8
+ // ============================================================================
9
+ // CSS Classes
10
+ // ============================================================================
11
+
12
+ const classes = {
13
+ // Unordered list classes
14
+ lineUL: "cm-draftly-list-line-ul",
15
+ markUL: "cm-draftly-list-mark-ul",
16
+
17
+ // Ordered list classes
18
+ lineOL: "cm-draftly-list-line-ol",
19
+ markOL: "cm-draftly-list-mark-ol",
20
+
21
+ // Task list classes
22
+ taskLine: "cm-draftly-task-line",
23
+ taskMarker: "cm-draftly-task-marker",
24
+
25
+ // Common classes
26
+ content: "cm-draftly-list-content",
27
+ indent: "cm-draftly-list-indent",
28
+ active: " cm-draftly-active",
29
+ preview: "cm-draftly-preview",
30
+ };
31
+
32
+ // ============================================================================
33
+ // Checkbox Widget
34
+ // ============================================================================
35
+
36
+ /**
37
+ * Interactive checkbox widget for task list items.
38
+ * Replaces `[ ]` or `[x]` markers with a clickable checkbox when not editing.
39
+ */
40
+ export class TaskCheckboxWidget extends WidgetType {
41
+ constructor(readonly checked: boolean) {
42
+ super();
43
+ }
44
+
45
+ override eq(other: TaskCheckboxWidget): boolean {
46
+ return other.checked === this.checked;
47
+ }
48
+
49
+ toDOM(view: EditorView): HTMLElement {
50
+ const wrap = document.createElement("span");
51
+ wrap.className = `cm-draftly-task-checkbox ${this.checked ? "checked" : ""}`;
52
+ wrap.setAttribute("aria-hidden", "true");
53
+
54
+ const checkbox = document.createElement("input");
55
+ checkbox.type = "checkbox";
56
+ checkbox.checked = this.checked;
57
+ checkbox.tabIndex = -1;
58
+
59
+ checkbox.addEventListener("mousedown", (e) => {
60
+ e.preventDefault();
61
+ this.toggleCheckbox(view, wrap);
62
+ });
63
+
64
+ wrap.appendChild(checkbox);
65
+ return wrap;
66
+ }
67
+
68
+ override ignoreEvent(): boolean {
69
+ return false;
70
+ }
71
+
72
+ /** Toggle the checkbox state in the document */
73
+ private toggleCheckbox(view: EditorView, wrap: HTMLElement): void {
74
+ const pos = view.posAtDOM(wrap);
75
+ const line = view.state.doc.lineAt(pos);
76
+ const match = line.text.match(/^(\s*(?:[-*+]|\d+\.)\s*)\[([ xX])\]/);
77
+
78
+ if (match) {
79
+ const markerStart = line.from + match[1]!.length + 1;
80
+ const newChar = this.checked ? " " : "x";
81
+ view.dispatch({
82
+ changes: { from: markerStart, to: markerStart + 1, insert: newChar },
83
+ });
84
+ }
85
+ }
86
+ }
87
+
88
+ // ============================================================================
89
+ // List Plugin
90
+ // ============================================================================
91
+
92
+ /**
93
+ * Decorates markdown lists with custom styling.
94
+ *
95
+ * Supports:
96
+ * - **Unordered lists** Replaces `*`, `-`, `+` markers with styled bullets
97
+ * - **Ordered lists** — Styles numbered markers (`1.`, `2.`, etc.)
98
+ * - **Task lists** — Renders `[ ]`/`[x]` as interactive checkboxes
99
+ */
100
+ export class ListPlugin extends DecorationPlugin {
101
+ readonly name = "list";
102
+ readonly version = "1.0.0";
103
+ override decorationPriority = 20;
104
+ override readonly requiredNodes = [
105
+ "BulletList",
106
+ "OrderedList",
107
+ "ListItem",
108
+ "ListMark",
109
+ "Task",
110
+ "TaskMarker",
111
+ ] as const;
112
+
113
+ override get theme() {
114
+ return theme;
115
+ }
116
+
117
+ /**
118
+ * Keyboard shortcuts for list formatting
119
+ */
120
+ override getKeymap(): KeyBinding[] {
121
+ return [
122
+ {
123
+ key: "Mod-Shift-8",
124
+ run: (view) => this.toggleListOnLines(view, "- "),
125
+ preventDefault: true,
126
+ },
127
+ {
128
+ key: "Mod-Shift-7",
129
+ run: (view) => this.toggleListOnLines(view, "1. "),
130
+ preventDefault: true,
131
+ },
132
+ {
133
+ key: "Mod-Shift-9",
134
+ run: (view) => this.toggleListOnLines(view, "- [ ] "),
135
+ preventDefault: true,
136
+ },
137
+ ];
138
+ }
139
+
140
+ /**
141
+ * Toggle list marker on current line or selected lines
142
+ */
143
+ private toggleListOnLines(view: EditorView, marker: string): boolean {
144
+ const { state } = view;
145
+ const { from, to } = state.selection.main;
146
+
147
+ // Get all lines in selection
148
+ const startLine = state.doc.lineAt(from);
149
+ const endLine = state.doc.lineAt(to);
150
+
151
+ const changes: { from: number; to: number; insert: string }[] = [];
152
+
153
+ // Regex to match existing list markers
154
+ const listMarkerRegex = /^(\s*)([-*+]|\d+\.)\s(\[[ xX]\]\s)?/;
155
+
156
+ const isOrderedMarker = marker === "1. ";
157
+ let orderNum = 1;
158
+
159
+ for (let lineNum = startLine.number; lineNum <= endLine.number; lineNum++) {
160
+ const line = state.doc.line(lineNum);
161
+ const match = line.text.match(listMarkerRegex);
162
+
163
+ // Get the actual marker to insert (incremental for ordered lists)
164
+ const actualMarker = isOrderedMarker ? `${orderNum}. ` : marker;
165
+
166
+ if (match) {
167
+ // Line already has a list marker - check if same type
168
+ const existingMarker = match[0];
169
+ const indent = match[1] || "";
170
+
171
+ // Check if this is the same marker type (toggle off)
172
+ const isUnordered = /^[-*+]$/.test(match[2]!);
173
+ const isOrdered = /^\d+\.$/.test(match[2]!);
174
+ const hasTask = !!match[3];
175
+
176
+ const wantUnordered = marker === "- ";
177
+ const wantOrdered = isOrderedMarker;
178
+ const wantTask = marker === "- [ ] ";
179
+
180
+ if (
181
+ (wantUnordered && isUnordered && !hasTask) ||
182
+ (wantOrdered && isOrdered && !hasTask) ||
183
+ (wantTask && hasTask)
184
+ ) {
185
+ // Same type - remove the marker
186
+ changes.push({
187
+ from: line.from,
188
+ to: line.from + existingMarker.length,
189
+ insert: indent,
190
+ });
191
+ } else {
192
+ // Different type - replace the marker
193
+ changes.push({
194
+ from: line.from,
195
+ to: line.from + existingMarker.length,
196
+ insert: indent + actualMarker,
197
+ });
198
+ orderNum++;
199
+ }
200
+ } else {
201
+ // No list marker - add one at start of line (after any indent)
202
+ const indentMatch = line.text.match(/^(\s*)/);
203
+ const indent = indentMatch ? indentMatch[1]! : "";
204
+ changes.push({
205
+ from: line.from + indent.length,
206
+ to: line.from + indent.length,
207
+ insert: actualMarker,
208
+ });
209
+ orderNum++;
210
+ }
211
+ }
212
+
213
+ if (changes.length > 0) {
214
+ view.dispatch({ changes });
215
+ }
216
+
217
+ return true;
218
+ }
219
+
220
+ buildDecorations(ctx: DecorationContext): void {
221
+ const { view, decorations } = ctx;
222
+ const tree = syntaxTree(view.state);
223
+
224
+ tree.iterate({
225
+ enter: (node) => {
226
+ const { from, to, name } = node;
227
+ const line = view.state.doc.lineAt(from);
228
+ const cursorInLine = ctx.cursorInRange(line.from, line.to);
229
+
230
+ switch (name) {
231
+ case "ListItem":
232
+ this.decorateListItem(node, line, decorations);
233
+ break;
234
+
235
+ case "ListMark":
236
+ this.decorateListMark(node, line, decorations, cursorInLine);
237
+ break;
238
+
239
+ case "TaskMarker":
240
+ this.decorateTaskMarker(from, to, view, decorations, cursorInLine);
241
+ break;
242
+ }
243
+ },
244
+ });
245
+ }
246
+
247
+ /** Add line decoration for list items with nesting depth */
248
+ private decorateListItem(
249
+ node: Parameters<NonNullable<Parameters<ReturnType<typeof syntaxTree>["iterate"]>[0]["enter"]>>[0],
250
+ line: { from: number },
251
+ decorations: Range<Decoration>[]
252
+ ): void {
253
+ const parent = node.node.parent;
254
+ const listType = parent?.name;
255
+
256
+ // Calculate nesting depth
257
+ let depth = 0;
258
+ let ancestor = node.node.parent;
259
+ while (ancestor) {
260
+ if (ancestor.name === "ListItem") depth++;
261
+ ancestor = ancestor.parent;
262
+ }
263
+
264
+ // Check for task marker child
265
+ const hasTask = this.hasTaskChild(node);
266
+
267
+ // Determine line class based on list type
268
+ let lineClass: string;
269
+ if (hasTask) lineClass = classes.taskLine;
270
+ else if (listType === "OrderedList") lineClass = classes.lineOL;
271
+ else lineClass = classes.lineUL;
272
+
273
+ decorations.push(
274
+ Decoration.line({
275
+ class: lineClass,
276
+ attributes: { style: `--depth: ${depth}` },
277
+ }).range(line.from)
278
+ );
279
+ }
280
+
281
+ /** Check if a ListItem node has a Task child */
282
+ private hasTaskChild(
283
+ node: Parameters<NonNullable<Parameters<ReturnType<typeof syntaxTree>["iterate"]>[0]["enter"]>>[0]
284
+ ): boolean {
285
+ const cursor = node.node.cursor();
286
+ if (cursor.firstChild()) {
287
+ do {
288
+ if (cursor.name === "Task") return true;
289
+ } while (cursor.nextSibling());
290
+ }
291
+ return false;
292
+ }
293
+
294
+ /** Decorate list markers (bullets for UL, numbers for OL) */
295
+ private decorateListMark(
296
+ node: Parameters<NonNullable<Parameters<ReturnType<typeof syntaxTree>["iterate"]>[0]["enter"]>>[0],
297
+ line: { from: number; to: number },
298
+ decorations: Range<Decoration>[],
299
+ cursorInLine: boolean
300
+ ): void {
301
+ const { from, to } = node;
302
+ const parent = node.node.parent;
303
+ const grandparent = parent?.parent;
304
+ const listType = grandparent?.name;
305
+ const activeClass = cursorInLine ? classes.active : "";
306
+
307
+ // Add indent decoration for nested items
308
+ if (from > line.from) {
309
+ decorations.push(Decoration.mark({ class: classes.indent + activeClass }).range(line.from, from));
310
+ }
311
+
312
+ // Add marker decoration based on list type
313
+ const markClass = listType === "OrderedList" ? classes.markOL : classes.markUL;
314
+ decorations.push(Decoration.mark({ class: markClass + activeClass }).range(from, to + 1));
315
+
316
+ // Wrap remaining line content
317
+ const contentStart = to + 1;
318
+ if (contentStart < line.to) {
319
+ decorations.push(Decoration.mark({ class: classes.content }).range(contentStart, line.to));
320
+ }
321
+ }
322
+
323
+ /** Decorate task markers - show checkbox widget or raw text based on cursor */
324
+ private decorateTaskMarker(
325
+ from: number,
326
+ to: number,
327
+ view: EditorView,
328
+ decorations: Range<Decoration>[],
329
+ cursorInLine: boolean
330
+ ): void {
331
+ const text = view.state.sliceDoc(from, to);
332
+ const isChecked = text.includes("x") || text.includes("X");
333
+
334
+ if (cursorInLine) {
335
+ // Show raw marker when editing
336
+ decorations.push(Decoration.mark({ class: classes.taskMarker }).range(from, to));
337
+ } else {
338
+ // Replace with interactive checkbox
339
+ decorations.push(
340
+ Decoration.replace({
341
+ widget: new TaskCheckboxWidget(isChecked),
342
+ }).range(from, to)
343
+ );
344
+ }
345
+ }
346
+
347
+ /** Render list nodes to HTML */
348
+ override renderToHTML(
349
+ node: SyntaxNode,
350
+ children: string,
351
+ ctx: { sliceDoc(from: number, to: number): string; sanitize(html: string): string }
352
+ ): string | null {
353
+ switch (node.name) {
354
+ case "BulletList":
355
+ return `<ul class="${classes.lineUL} ${classes.preview}">${children}</ul>\n`;
356
+
357
+ case "OrderedList":
358
+ return `<ol class="${classes.lineOL} ${classes.preview}">${children}</ol>\n`;
359
+
360
+ case "ListItem":
361
+ return `<li>${children}</li>\n`;
362
+
363
+ case "Task":
364
+ return children;
365
+
366
+ case "TaskMarker": {
367
+ const text = ctx.sliceDoc(node.from, node.to);
368
+ const isChecked = text.includes("x") || text.includes("X");
369
+ return `<input type="checkbox" class="cm-draftly-task-checkbox" disabled ${isChecked ? "checked" : ""} />`;
370
+ }
371
+
372
+ case "ListMark":
373
+ return "";
374
+
375
+ default:
376
+ return null;
377
+ }
378
+ }
379
+ }
380
+
381
+ // ============================================================================
382
+ // Theme
383
+ // ============================================================================
384
+
385
+ const theme = createTheme({
386
+ default: {
387
+ // Indentation marker positioning
388
+ ".cm-draftly-list-indent": {
389
+ overflow: "hidden",
390
+ display: "inline-block",
391
+ position: "absolute",
392
+ left: "calc(1rem * (var(--depth, 0) + 1))",
393
+ transform: "translateX(-100%)",
394
+ },
395
+
396
+ // List line layout (flexbox for marker alignment)
397
+ ".cm-draftly-list-line-ul, .cm-draftly-list-line-ol": {
398
+ position: "relative",
399
+ paddingLeft: "calc(1rem * (var(--depth, 0) + 1)) !important",
400
+ display: "flex",
401
+ alignItems: "start",
402
+ },
403
+ ".cm-draftly-list-line-ul > :first-child, .cm-draftly-list-line-ol > :first-child": {
404
+ flexShrink: 0,
405
+ },
406
+
407
+ // List marker sizing
408
+ ".cm-draftly-list-line-ul .cm-draftly-list-mark-ul, .cm-draftly-list-line-ol .cm-draftly-list-mark-ol": {
409
+ whiteSpace: "pre",
410
+ position: "relative",
411
+ width: "1rem",
412
+ flexShrink: 0,
413
+ },
414
+
415
+ // Hide raw marker text when not active
416
+ ".cm-draftly-list-mark-ul:not(.cm-draftly-active) > span, .cm-draftly-task-line .cm-draftly-list-mark-ol:not(.cm-draftly-active) > span":
417
+ {
418
+ visibility: "hidden",
419
+ display: "none",
420
+ },
421
+
422
+ // Styled bullet for unordered lists
423
+ ".cm-draftly-list-line-ul .cm-draftly-list-mark-ul:not(.cm-draftly-active)::after": {
424
+ content: '"•"',
425
+ color: "var(--color-link)",
426
+ fontWeight: "bold",
427
+ pointerEvents: "none",
428
+ },
429
+
430
+ // Task marker styling (visible when editing)
431
+ ".cm-draftly-task-marker": {
432
+ color: "var(--draftly-highlight, #a4a4a4)",
433
+ fontFamily: "monospace",
434
+ },
435
+
436
+ // Task checkbox container
437
+ ".cm-draftly-task-checkbox": {
438
+ display: "inline-flex",
439
+ verticalAlign: "middle",
440
+ marginRight: "0.3em",
441
+ cursor: "pointer",
442
+ userSelect: "none",
443
+ alignItems: "center",
444
+ height: "1.2em",
445
+ },
446
+
447
+ // Task checkbox input styling
448
+ ".cm-draftly-task-checkbox input": {
449
+ cursor: "pointer",
450
+ margin: 0,
451
+ width: "1.1em",
452
+ height: "1.1em",
453
+ appearance: "none",
454
+ border: "1px solid",
455
+ borderRadius: "0.25em",
456
+ backgroundColor: "transparent",
457
+ position: "relative",
458
+ },
459
+
460
+ // Checkmark for completed tasks
461
+ ".cm-draftly-task-checkbox.checked input::after": {
462
+ content: '"✓"',
463
+ position: "absolute",
464
+ left: "1px",
465
+ top: "-3px",
466
+ },
467
+
468
+ // Preview styles (override editor-specific layout)
469
+ ".cm-draftly-preview": {
470
+ display: "block",
471
+ paddingLeft: "1.5rem",
472
+ margin: "0.5rem 0",
473
+ },
474
+ ".cm-draftly-preview li": {
475
+ display: "list-item",
476
+ marginBottom: "0.25rem",
477
+ },
478
+ "ul.cm-draftly-preview": {
479
+ listStyleType: "disc",
480
+ },
481
+ "ol.cm-draftly-preview": {
482
+ listStyleType: "decimal",
483
+ },
484
+ // Hide list marker for task items
485
+ ".cm-draftly-preview li:has(.cm-draftly-task-checkbox)": {
486
+ listStyleType: "none",
487
+ },
488
+ ".cm-draftly-preview li .cm-draftly-paragraph": {
489
+ padding: "0",
490
+ },
491
+ },
492
+ });