mirror of
https://github.com/multica-ai/multica.git
synced 2026-06-27 01:19:26 +02:00
Compare commits
4 Commits
agent/lamb
...
agent/j/0d
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c8e6fc67b5 | ||
|
|
12dbfa8673 | ||
|
|
9a6e029ff3 | ||
|
|
281faa7087 |
@@ -60,6 +60,7 @@ import {
|
||||
Link2,
|
||||
List,
|
||||
ListOrdered,
|
||||
ListTodo,
|
||||
Quote,
|
||||
ChevronDown,
|
||||
Check,
|
||||
@@ -297,7 +298,7 @@ function HeadingDropdown({ editor, onOpenChange, activeLevel }: { editor: Editor
|
||||
// List Dropdown
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
function ListDropdown({ editor, onOpenChange, isBullet, isOrdered }: { editor: Editor; onOpenChange: (open: boolean) => void; isBullet: boolean; isOrdered: boolean }) {
|
||||
function ListDropdown({ editor, onOpenChange, isBullet, isOrdered, isTask }: { editor: Editor; onOpenChange: (open: boolean) => void; isBullet: boolean; isOrdered: boolean; isTask: boolean }) {
|
||||
const { t } = useT("editor");
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
@@ -310,7 +311,7 @@ function ListDropdown({ editor, onOpenChange, isBullet, isOrdered }: { editor: E
|
||||
<Popover modal={false} open={open} onOpenChange={handleOpenChange}>
|
||||
<Tooltip>
|
||||
<TooltipTrigger render={
|
||||
<PopoverTrigger className="inline-flex h-7 items-center gap-0.5 rounded-md px-1.5 text-xs font-medium hover:bg-muted aria-pressed:bg-muted" aria-pressed={isBullet || isOrdered} onMouseDown={(e) => e.preventDefault()} />
|
||||
<PopoverTrigger className="inline-flex h-7 items-center gap-0.5 rounded-md px-1.5 text-xs font-medium hover:bg-muted aria-pressed:bg-muted" aria-pressed={isBullet || isOrdered || isTask} onMouseDown={(e) => e.preventDefault()} />
|
||||
}>
|
||||
<List className="size-3.5" />
|
||||
<ChevronDown className="size-3" />
|
||||
@@ -349,6 +350,18 @@ function ListDropdown({ editor, onOpenChange, isBullet, isOrdered }: { editor: E
|
||||
<ListOrdered className="size-3.5" /> {t(($) => $.bubble_menu.list_dropdown.ordered_list)}
|
||||
{isOrdered && <Check className="ml-auto size-3.5" />}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="flex w-full cursor-default items-center gap-2 rounded-md px-1.5 py-1 text-xs outline-hidden select-none hover:bg-accent hover:text-accent-foreground"
|
||||
onMouseDown={(e) => {
|
||||
e.preventDefault();
|
||||
editor.chain().focus().toggleTaskList().run();
|
||||
handleOpenChange(false);
|
||||
}}
|
||||
>
|
||||
<ListTodo className="size-3.5" /> {t(($) => $.bubble_menu.list_dropdown.task_list)}
|
||||
{isTask && <Check className="ml-auto size-3.5" />}
|
||||
</button>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
);
|
||||
@@ -478,6 +491,7 @@ function EditorBubbleMenu({
|
||||
blockquote: e.isActive("blockquote"),
|
||||
bulletList: e.isActive("bulletList"),
|
||||
orderedList: e.isActive("orderedList"),
|
||||
taskList: e.isActive("taskList"),
|
||||
heading1: e.isActive("heading", { level: 1 }),
|
||||
heading2: e.isActive("heading", { level: 2 }),
|
||||
heading3: e.isActive("heading", { level: 3 }),
|
||||
@@ -606,7 +620,7 @@ function EditorBubbleMenu({
|
||||
</Tooltip>
|
||||
<Separator orientation="vertical" className="mx-0.5 h-5" />
|
||||
<HeadingDropdown editor={editor} onOpenChange={handleMenuOpenChange} activeLevel={fmt.heading1 ? 1 : fmt.heading2 ? 2 : fmt.heading3 ? 3 : undefined} />
|
||||
<ListDropdown editor={editor} onOpenChange={handleMenuOpenChange} isBullet={fmt.bulletList} isOrdered={fmt.orderedList} />
|
||||
<ListDropdown editor={editor} onOpenChange={handleMenuOpenChange} isBullet={fmt.bulletList} isOrdered={fmt.orderedList} isTask={fmt.taskList} />
|
||||
<Tooltip>
|
||||
<TooltipTrigger render={
|
||||
<Toggle size="sm" pressed={fmt.blockquote} onPressedChange={() => editor.chain().focus().toggleBlockquote().run()} onMouseDown={(e) => e.preventDefault()} />
|
||||
|
||||
@@ -31,6 +31,7 @@ import TableRow from "@tiptap/extension-table-row";
|
||||
import TableHeader from "@tiptap/extension-table-header";
|
||||
import TableCell from "@tiptap/extension-table-cell";
|
||||
import { Table } from "@tiptap/extension-table";
|
||||
import { TaskList } from "@tiptap/extension-list";
|
||||
import { Markdown } from "@tiptap/markdown";
|
||||
import { ReactNodeViewRenderer } from "@tiptap/react";
|
||||
import type { AnyExtension } from "@tiptap/core";
|
||||
@@ -41,7 +42,7 @@ import { createMentionSuggestion } from "./mention-suggestion";
|
||||
import { SlashCommandExtension } from "./slash-command-extension";
|
||||
import { createSlashCommandSuggestion } from "./slash-command-suggestion";
|
||||
import { CodeBlockView } from "./code-block-view";
|
||||
import { PatchedListItem } from "./list-item";
|
||||
import { PatchedListItem, PatchedTaskItem } from "./list-item";
|
||||
import { createMarkdownPasteExtension } from "./markdown-paste";
|
||||
import { createMarkdownCopyExtension } from "./markdown-copy";
|
||||
import { createSubmitExtension } from "./submit-shortcut";
|
||||
@@ -129,6 +130,13 @@ export function createEditorExtensions(
|
||||
listItem: false,
|
||||
}),
|
||||
PatchedListItem,
|
||||
// Checkbox task lists: `- [ ]` / `- [x]`. TaskList + TaskItem ship their own
|
||||
// markdown tokenizer / renderMarkdown, an input rule (typing `[] ` / `[x] `),
|
||||
// and a checkbox NodeView. The taskList tokenizer is consulted before
|
||||
// marked's built-in list tokenizer, so `- [ ]` becomes a task while a plain
|
||||
// `- ` still falls through to PatchedListItem's bullet list.
|
||||
TaskList,
|
||||
PatchedTaskItem,
|
||||
CodeBlockLowlight.extend({
|
||||
addNodeView() {
|
||||
return ReactNodeViewRenderer(CodeBlockView);
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { ListItem } from "@tiptap/extension-list";
|
||||
import { type Editor, InputRule } from "@tiptap/core";
|
||||
import { ListItem, TaskItem } from "@tiptap/extension-list";
|
||||
|
||||
/**
|
||||
* Patched ListItem with proper "double-Enter exits list" behaviour.
|
||||
* Shared list keymap with proper "double-Enter exits list" behaviour.
|
||||
*
|
||||
* Tiptap's stock `Enter: splitListItem` is incomplete. `splitListItem` itself
|
||||
* returns false (without dispatching) when the cursor sits in an empty
|
||||
@@ -17,17 +18,67 @@ import { ListItem } from "@tiptap/extension-list";
|
||||
* empty bullet exits the list as a plain paragraph. Non-empty and nested
|
||||
* empty items are unaffected because `splitListItem` handles them correctly
|
||||
* and returns true.
|
||||
*
|
||||
* Tab / Shift-Tab indent / dedent the item.
|
||||
*/
|
||||
function listItemKeymap(editor: Editor, name: string) {
|
||||
return {
|
||||
Enter: () =>
|
||||
editor.commands.first(({ commands }) => [
|
||||
() => commands.splitListItem(name),
|
||||
() => commands.liftListItem(name),
|
||||
]),
|
||||
Tab: () => editor.commands.sinkListItem(name),
|
||||
"Shift-Tab": () => editor.commands.liftListItem(name),
|
||||
};
|
||||
}
|
||||
|
||||
export const PatchedListItem = ListItem.extend({
|
||||
addKeyboardShortcuts() {
|
||||
return {
|
||||
Enter: () =>
|
||||
this.editor.commands.first(({ commands }) => [
|
||||
() => commands.splitListItem(this.name),
|
||||
() => commands.liftListItem(this.name),
|
||||
]),
|
||||
Tab: () => this.editor.commands.sinkListItem(this.name),
|
||||
"Shift-Tab": () => this.editor.commands.liftListItem(this.name),
|
||||
};
|
||||
return listItemKeymap(this.editor, this.name);
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* Patched TaskItem — same "double-Enter exits list" fix as PatchedListItem,
|
||||
* applied to checkbox task items so they behave identically to bullet/ordered
|
||||
* lists. `nested: true` lets a task item hold nested lists (so Tab indents into
|
||||
* a sub-task and nested markdown round-trips), matching GitHub / Notion.
|
||||
*
|
||||
* It also adds the GitHub-style `- [ ] ` typing flow. TaskItem's built-in input
|
||||
* rule only converts `[ ] ` / `[x] ` typed at the start of a PLAIN paragraph;
|
||||
* once a leading `- ` (or `1. `) has turned the line into a bullet/ordered
|
||||
* item, that rule no longer fires and `[ ]` stays as literal text. The extra
|
||||
* rule below catches the checkbox token when it is the entire content of a
|
||||
* freshly-typed list item and converts just that item into a task item —
|
||||
* sibling items in the same list are left untouched (lift then re-wrap).
|
||||
*/
|
||||
export const PatchedTaskItem = TaskItem.extend({
|
||||
addKeyboardShortcuts() {
|
||||
return listItemKeymap(this.editor, this.name);
|
||||
},
|
||||
addInputRules() {
|
||||
return [
|
||||
...(this.parent?.() ?? []),
|
||||
new InputRule({
|
||||
find: /^\[([ xX])?\]\s$/,
|
||||
handler: ({ state, range, match, chain }) => {
|
||||
// Only when the checkbox token is the whole content of a list item.
|
||||
// Plain-paragraph typing is handled by the inherited rule above; the
|
||||
// anchored regex guarantees there is no other text before it. When
|
||||
// the guard fails the handler leaves the transaction untouched, so
|
||||
// the rule is a no-op and ProseMirror falls through.
|
||||
if (state.selection.$from.node(-1)?.type.name === "listItem") {
|
||||
const checked = (match[1] ?? "").toLowerCase() === "x";
|
||||
chain()
|
||||
.deleteRange(range)
|
||||
.liftListItem("listItem")
|
||||
.toggleList("taskList", "taskItem")
|
||||
.updateAttributes("taskItem", { checked })
|
||||
.run();
|
||||
}
|
||||
},
|
||||
}),
|
||||
];
|
||||
},
|
||||
}).configure({ nested: true });
|
||||
|
||||
169
packages/views/editor/extensions/task-list-markdown.test.ts
Normal file
169
packages/views/editor/extensions/task-list-markdown.test.ts
Normal file
@@ -0,0 +1,169 @@
|
||||
import { describe, it, expect, afterEach } from "vitest";
|
||||
import { Editor } from "@tiptap/core";
|
||||
import StarterKit from "@tiptap/starter-kit";
|
||||
import { Markdown } from "@tiptap/markdown";
|
||||
import { PatchedListItem, PatchedTaskItem } from "./list-item";
|
||||
import { TaskList } from "@tiptap/extension-list";
|
||||
|
||||
// A minimal editor mirroring the production list config: StarterKit's stock
|
||||
// ListItem disabled in favor of PatchedListItem, plus the checkbox TaskList /
|
||||
// TaskItem pair, all serialized through @tiptap/markdown.
|
||||
function makeEditor() {
|
||||
const element = document.createElement("div");
|
||||
document.body.appendChild(element);
|
||||
return new Editor({
|
||||
element,
|
||||
extensions: [
|
||||
StarterKit.configure({ listItem: false }),
|
||||
PatchedListItem,
|
||||
TaskList,
|
||||
PatchedTaskItem,
|
||||
Markdown.configure({ indentation: { style: "space", size: 3 } }),
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
interface JsonNode {
|
||||
type?: string;
|
||||
text?: string;
|
||||
attrs?: Record<string, unknown>;
|
||||
content?: JsonNode[];
|
||||
}
|
||||
|
||||
function findAll(node: JsonNode, type: string, acc: JsonNode[] = []): JsonNode[] {
|
||||
if (node.type === type) acc.push(node);
|
||||
for (const child of node.content ?? []) findAll(child, type, acc);
|
||||
return acc;
|
||||
}
|
||||
|
||||
function nodeText(node: JsonNode): string {
|
||||
if (node.text !== undefined) return node.text;
|
||||
return (node.content ?? []).map(nodeText).join("");
|
||||
}
|
||||
|
||||
function loadMarkdown(editor: Editor, md: string) {
|
||||
editor.commands.setContent(md, { contentType: "markdown" });
|
||||
}
|
||||
|
||||
// Faithfully simulate typing: each character gets a chance to fire an input
|
||||
// rule (handleTextInput) before falling back to a plain insert — exactly how
|
||||
// ProseMirror processes keyboard input. Lets us exercise the live `[ ] ` /
|
||||
// `- [ ] ` shortcuts, which setContent (the markdown path) bypasses.
|
||||
function typeText(ed: Editor, text: string) {
|
||||
for (const ch of text) {
|
||||
const { from, to } = ed.state.selection;
|
||||
const handled = ed.view.someProp("handleTextInput", (f) =>
|
||||
f(ed.view, from, to, ch, () => ed.state.tr),
|
||||
);
|
||||
if (!handled) ed.view.dispatch(ed.state.tr.insertText(ch, from, to));
|
||||
}
|
||||
}
|
||||
|
||||
let editor: Editor;
|
||||
afterEach(() => editor?.destroy());
|
||||
|
||||
describe("task list markdown parsing", () => {
|
||||
it("parses `- [ ]` / `- [x]` into a taskList with checked flags", () => {
|
||||
editor = makeEditor();
|
||||
loadMarkdown(editor, "- [ ] todo\n- [x] done");
|
||||
|
||||
const json = editor.getJSON() as JsonNode;
|
||||
const taskLists = findAll(json, "taskList");
|
||||
expect(taskLists).toHaveLength(1);
|
||||
|
||||
const items = findAll(json, "taskItem");
|
||||
expect(items).toHaveLength(2);
|
||||
expect(items[0]!.attrs?.checked).toBe(false);
|
||||
expect(nodeText(items[0]!)).toBe("todo");
|
||||
expect(items[1]!.attrs?.checked).toBe(true);
|
||||
expect(nodeText(items[1]!)).toBe("done");
|
||||
});
|
||||
|
||||
it("accepts an uppercase `- [X]` as checked", () => {
|
||||
editor = makeEditor();
|
||||
loadMarkdown(editor, "- [X] done");
|
||||
|
||||
const items = findAll(editor.getJSON() as JsonNode, "taskItem");
|
||||
expect(items).toHaveLength(1);
|
||||
expect(items[0]!.attrs?.checked).toBe(true);
|
||||
});
|
||||
|
||||
it("leaves a plain bullet as a bulletList, not a taskList", () => {
|
||||
editor = makeEditor();
|
||||
loadMarkdown(editor, "- just a bullet");
|
||||
|
||||
const json = editor.getJSON() as JsonNode;
|
||||
expect(findAll(json, "taskList")).toHaveLength(0);
|
||||
expect(findAll(json, "bulletList")).toHaveLength(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe("task list markdown serialization", () => {
|
||||
it("round-trips checked and unchecked items", () => {
|
||||
editor = makeEditor();
|
||||
loadMarkdown(editor, "- [ ] todo\n- [x] done");
|
||||
expect(editor.getMarkdown().trim()).toBe("- [ ] todo\n- [x] done");
|
||||
});
|
||||
|
||||
it("serializes a checkbox toggled in the editor back to `- [x]`", () => {
|
||||
editor = makeEditor();
|
||||
loadMarkdown(editor, "- [ ] todo");
|
||||
|
||||
// Flip the single task item's checked attr the way the checkbox NodeView does.
|
||||
editor.commands.command(({ tr, state }) => {
|
||||
state.doc.descendants((node, pos) => {
|
||||
if (node.type.name === "taskItem") {
|
||||
tr.setNodeMarkup(pos, undefined, { ...node.attrs, checked: true });
|
||||
}
|
||||
return true;
|
||||
});
|
||||
return true;
|
||||
});
|
||||
|
||||
expect(editor.getMarkdown().trim()).toBe("- [x] todo");
|
||||
});
|
||||
});
|
||||
|
||||
describe("task list typing input rules", () => {
|
||||
it("converts `[ ] ` typed in a plain paragraph into an unchecked task", () => {
|
||||
editor = makeEditor();
|
||||
typeText(editor, "[ ] buy milk");
|
||||
expect(editor.getMarkdown().trim()).toBe("- [ ] buy milk");
|
||||
});
|
||||
|
||||
it("converts `[x] ` into a checked task", () => {
|
||||
editor = makeEditor();
|
||||
typeText(editor, "[x] shipped");
|
||||
expect(editor.getMarkdown().trim()).toBe("- [x] shipped");
|
||||
});
|
||||
|
||||
// The GitHub-style flow the feature request showed: `- ` makes a bullet, then
|
||||
// `[ ] ` turns that item into a checkbox.
|
||||
it("converts `- [ ] ` typed as a bullet into a task", () => {
|
||||
editor = makeEditor();
|
||||
typeText(editor, "- [ ] write tests");
|
||||
expect(editor.getMarkdown().trim()).toBe("- [ ] write tests");
|
||||
});
|
||||
|
||||
it("converts `- [x] ` typed as a bullet into a checked task", () => {
|
||||
editor = makeEditor();
|
||||
typeText(editor, "- [x] done");
|
||||
expect(editor.getMarkdown().trim()).toBe("- [x] done");
|
||||
});
|
||||
|
||||
it("only converts the current bullet item, leaving siblings as bullets", () => {
|
||||
editor = makeEditor();
|
||||
typeText(editor, "- apple");
|
||||
editor.commands.enter();
|
||||
typeText(editor, "[ ] task");
|
||||
expect(editor.getMarkdown().trim()).toBe("- apple\n\n- [ ] task");
|
||||
});
|
||||
|
||||
it("leaves a plain `- ` bullet alone (no false conversion)", () => {
|
||||
editor = makeEditor();
|
||||
typeText(editor, "- hello");
|
||||
const json = editor.getJSON() as JsonNode;
|
||||
expect(findAll(json, "taskList")).toHaveLength(0);
|
||||
expect(findAll(json, "bulletList")).toHaveLength(1);
|
||||
});
|
||||
});
|
||||
@@ -126,6 +126,52 @@ describe("ReadonlyContent line breaks", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("ReadonlyContent task lists", () => {
|
||||
it("renders `- [ ]` / `- [x]` as checkboxes and preserves the checked state", () => {
|
||||
const { container } = render(
|
||||
<ReadonlyContent content={"- [ ] todo\n- [x] done"} />,
|
||||
);
|
||||
|
||||
const boxes = container.querySelectorAll<HTMLInputElement>(
|
||||
'input[type="checkbox"]',
|
||||
);
|
||||
expect(boxes).toHaveLength(2);
|
||||
// The completed item must render checked, not just present.
|
||||
expect(boxes[0]!.checked).toBe(false);
|
||||
expect(boxes[1]!.checked).toBe(true);
|
||||
// Checkboxes are display-only in readonly mode.
|
||||
expect(boxes[1]!.disabled).toBe(true);
|
||||
});
|
||||
|
||||
it("nests a child task list inside its parent item (not as a sibling)", () => {
|
||||
const { container } = render(
|
||||
<ReadonlyContent content={"- [ ] parent\n - [x] child\n - [ ] child2"} />,
|
||||
);
|
||||
|
||||
// One top-level list with a single parent item.
|
||||
const root = container.querySelector("ul.contains-task-list");
|
||||
expect(root).not.toBeNull();
|
||||
const topItems = root!.querySelectorAll(":scope > li.task-list-item");
|
||||
expect(topItems).toHaveLength(1);
|
||||
|
||||
// The child list lives INSIDE the parent <li> — this is the structural
|
||||
// assumption the readonly CSS depends on (no <div> body wrapper, so the
|
||||
// parent item must stay a block, not flex, or the nested <ul> shares the
|
||||
// parent's row). If remark-gfm ever wrapped the body, this fails loudly.
|
||||
const parent = topItems[0]!;
|
||||
const nested = parent.querySelector(":scope > ul.contains-task-list");
|
||||
expect(nested).not.toBeNull();
|
||||
|
||||
const childItems = nested!.querySelectorAll(":scope > li.task-list-item");
|
||||
expect(childItems).toHaveLength(2);
|
||||
const childBoxes = nested!.querySelectorAll<HTMLInputElement>(
|
||||
'input[type="checkbox"]',
|
||||
);
|
||||
expect(childBoxes[0]!.checked).toBe(true);
|
||||
expect(childBoxes[1]!.checked).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("ReadonlyContent highlight Markdown", () => {
|
||||
// `==text==` is lowered to a raw <mark> by highlightToHtml; rehype-raw turns
|
||||
// it into an element and the sanitize schema must whitelist <mark> or it gets
|
||||
|
||||
@@ -149,6 +149,109 @@
|
||||
list-style-type: lower-roman;
|
||||
}
|
||||
|
||||
/*
|
||||
* Task lists (checkboxes) — `- [ ]` / `- [x]`.
|
||||
*
|
||||
* The editor and readonly views emit DIFFERENT DOM, so they are styled
|
||||
* separately rather than sharing a flex rule:
|
||||
*
|
||||
* - Editor (Tiptap NodeView):
|
||||
* ul[data-type="taskList"] > li[data-checked]
|
||||
* > label(contenteditable) > input + span
|
||||
* > div(content) > p, [nested ul]
|
||||
* The item body is wrapped in a <div>, so the row is a flex layout and a
|
||||
* nested task list inside that <div> still stacks below the text.
|
||||
*
|
||||
* - Readonly (remark-gfm):
|
||||
* ul.contains-task-list > li.task-list-item
|
||||
* > input[disabled] + text [ + nested ul ]
|
||||
* There is NO body wrapper — a nested <ul> is a direct sibling of the
|
||||
* checkbox and text. Flex here would pull the nested list onto the same
|
||||
* row, so the item stays a block and the checkbox is positioned inline.
|
||||
*/
|
||||
|
||||
/* Shared: checkbox appearance. */
|
||||
.rich-text-editor input[type="checkbox"] {
|
||||
width: 0.95rem;
|
||||
height: 0.95rem;
|
||||
accent-color: var(--brand);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
/* --- Editor (Tiptap) --- */
|
||||
.rich-text-editor ul[data-type="taskList"] {
|
||||
list-style: none;
|
||||
padding-inline: 0;
|
||||
margin: 0.5rem 0;
|
||||
}
|
||||
|
||||
/* Child combinators keep these rules off nested plain bullet/ordered lists. */
|
||||
.rich-text-editor ul[data-type="taskList"] > li {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 0.5rem;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
/* Checkbox column. The top margin nudges the box onto the first text line
|
||||
(line-height 1.625). */
|
||||
.rich-text-editor ul[data-type="taskList"] > li > label {
|
||||
flex: 0 0 auto;
|
||||
margin: 0.28rem 0 0;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
/* Content column fills the remaining width and may wrap/scroll. */
|
||||
.rich-text-editor ul[data-type="taskList"] > li > div {
|
||||
flex: 1 1 auto;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.rich-text-editor ul[data-type="taskList"] > li > div > p {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
/* Completed editor item: muted text (no strikethrough), matching Linear. The
|
||||
NodeView keeps `data-checked` on the <li> (but not data-type). */
|
||||
.rich-text-editor ul[data-type="taskList"] > li[data-checked="true"] > div {
|
||||
color: var(--muted-foreground);
|
||||
}
|
||||
|
||||
/* --- Readonly (remark-gfm) --- */
|
||||
.rich-text-editor ul.contains-task-list {
|
||||
list-style: none;
|
||||
padding-inline: 0;
|
||||
margin: 0.5rem 0;
|
||||
}
|
||||
|
||||
.rich-text-editor li.task-list-item {
|
||||
padding-inline: 0;
|
||||
}
|
||||
|
||||
/* Inline checkbox on the first text line; a nested <ul> stays block and drops
|
||||
below the item instead of sharing the row. */
|
||||
.rich-text-editor li.task-list-item > input[type="checkbox"] {
|
||||
margin: 0 0.5rem 0 0;
|
||||
vertical-align: -0.1em;
|
||||
}
|
||||
|
||||
/* Indent nested readonly task lists under their parent item. */
|
||||
.rich-text-editor li.task-list-item .contains-task-list {
|
||||
padding-inline-start: 1.45rem;
|
||||
margin: 0.25rem 0;
|
||||
}
|
||||
|
||||
/* Completed readonly item: muted text, matched via :has(input:checked). The
|
||||
reset keeps a completed parent from bleeding its muted color into the
|
||||
(independently evaluated) nested items below it. */
|
||||
.rich-text-editor li.task-list-item:has(> input:checked) {
|
||||
color: var(--muted-foreground);
|
||||
}
|
||||
|
||||
.rich-text-editor li.task-list-item:has(> input:checked) .contains-task-list {
|
||||
color: var(--foreground);
|
||||
}
|
||||
|
||||
/* Tables */
|
||||
.rich-text-editor .tableWrapper {
|
||||
overflow-x: auto;
|
||||
|
||||
@@ -18,7 +18,8 @@
|
||||
},
|
||||
"list_dropdown": {
|
||||
"bullet_list": "Bullet List",
|
||||
"ordered_list": "Ordered List"
|
||||
"ordered_list": "Ordered List",
|
||||
"task_list": "Task List"
|
||||
},
|
||||
"sub_issue": {
|
||||
"tooltip": "Create sub-issue from selection",
|
||||
|
||||
@@ -18,7 +18,8 @@
|
||||
},
|
||||
"list_dropdown": {
|
||||
"bullet_list": "箇条書きリスト",
|
||||
"ordered_list": "番号付きリスト"
|
||||
"ordered_list": "番号付きリスト",
|
||||
"task_list": "タスクリスト"
|
||||
},
|
||||
"sub_issue": {
|
||||
"tooltip": "選択範囲からサブイシューを作成",
|
||||
|
||||
@@ -18,7 +18,8 @@
|
||||
},
|
||||
"list_dropdown": {
|
||||
"bullet_list": "글머리 기호 목록",
|
||||
"ordered_list": "번호 목록"
|
||||
"ordered_list": "번호 목록",
|
||||
"task_list": "체크리스트"
|
||||
},
|
||||
"sub_issue": {
|
||||
"tooltip": "선택 영역으로 하위 이슈 만들기",
|
||||
|
||||
@@ -18,7 +18,8 @@
|
||||
},
|
||||
"list_dropdown": {
|
||||
"bullet_list": "无序列表",
|
||||
"ordered_list": "有序列表"
|
||||
"ordered_list": "有序列表",
|
||||
"task_list": "任务列表"
|
||||
},
|
||||
"sub_issue": {
|
||||
"tooltip": "用选中内容创建子 issue",
|
||||
|
||||
Reference in New Issue
Block a user