Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
43 commits
Select commit Hold shift + click to select a range
a4b016a
docs: update contributing guide to include Redis and QStash setup ins…
YungKingJayy Oct 16, 2025
30f2707
feat(cms): migrate editor from Novel to Tiptap v3
YungKingJayy Oct 16, 2025
04cf73c
feat: migrate from Novel to native Tiptap v3
YungKingJayy Oct 16, 2025
b14f333
feat: implement image upload functionality with drag-and-drop support
YungKingJayy Oct 16, 2025
0ea7454
feat: add color picker component with hex input and preset colors
YungKingJayy Oct 16, 2025
1010b16
refactor: editor components for improved readability and performance
YungKingJayy Oct 17, 2025
117c165
refactor: improve code readability and structure across various compo…
YungKingJayy Oct 17, 2025
2d673db
refactor: enhance UI components with tooltips and improved styling
YungKingJayy Oct 17, 2025
2a26339
feat(editor): add content type picker and slash command functionality
YungKingJayy Oct 19, 2025
fb9b6c7
feat(editor): add table and multi-column extensions with slash comman…
YungKingJayy Oct 23, 2025
b3bdb61
feat(table): add column and row menus with add/delete functionality
YungKingJayy Oct 23, 2025
2facbfe
feat(editor): hide placeholder and disable slash command inside tables
YungKingJayy Oct 23, 2025
bc0766e
feat: implement drag-and-drop functionality with node manipulation ac…
YungKingJayy Oct 25, 2025
7c67be1
feat(editor): update button styles to use primary theme colors and im…
YungKingJayy Oct 25, 2025
12086f6
feat(editor): add keyboard shortcuts to bubble menu tooltips
YungKingJayy Oct 25, 2025
c88a983
feat: add file handler extension for dropping or pasting images into …
YungKingJayy Oct 25, 2025
fea5d6b
feat(editor): enhance image upload with URL embed and media gallery
YungKingJayy Oct 25, 2025
e9764ba
Merge branch 'main' of https://github.com/usemarble/marble into feat/…
YungKingJayy Oct 25, 2025
a51674d
feat(editor): add columns feature to editor
YungKingJayy Oct 26, 2025
e4cece1
feat(editor): redesign link selector popup layout
YungKingJayy Oct 26, 2025
5514d48
fix: stop links from opening on click
YungKingJayy Oct 26, 2025
13d9910
feat(editor): hide bubble menu when YouTube videos are selected
YungKingJayy Oct 26, 2025
4c119a5
feat(editor): add figure extension with caption support and update im…
YungKingJayy Oct 26, 2025
eb59936
refactor(editor): improve stability of slash command and table utils
YungKingJayy Oct 26, 2025
db0bdeb
Merge branch 'main' of https://github.com/usemarble/marble into feat/…
YungKingJayy Oct 26, 2025
02d8d04
Merge branch 'main' of https://github.com/usemarble/marble into feat/…
YungKingJayy Oct 27, 2025
39e0fd4
fix: import Image
YungKingJayy Oct 27, 2025
4c6cfb8
fix: linter errors
YungKingJayy Oct 27, 2025
5e0cb4b
refactor: update biome ignore comments for clarity and consistency
YungKingJayy Oct 27, 2025
56c344e
chore: update dependencies and clean up unused code
YungKingJayy Oct 27, 2025
cf93597
fix: update ColorPicker to reset hex input on color change;
YungKingJayy Oct 27, 2025
1f520ab
refactor: improve formatting and readability in ColorPicker and useDr…
YungKingJayy Oct 27, 2025
7575c1f
feat: add markdown support with file drop and paste extensions
YungKingJayy Oct 28, 2025
c48c2f3
feat: add href support to Figure component and update markdown transf…
YungKingJayy Oct 28, 2025
6673eae
Merge branch 'main' of https://github.com/usemarble/marble into feat/…
YungKingJayy Oct 28, 2025
2da98ef
fix: drop astro preset
taqh Oct 28, 2025
668c7f7
fix: stuff
taqh Oct 29, 2025
b44e9ce
feat: add width, height, and alignment to Figure; add aria-label to I…
YungKingJayy Nov 1, 2025
be83391
refactor: simplify attribute parsing and improve markdown handling in…
YungKingJayy Nov 1, 2025
a4e77cc
Merge branch 'main' of https://github.com/usemarble/marble into feat/…
YungKingJayy Nov 1, 2025
e3539f6
refactor: standardize increment syntax in loops across various compon…
YungKingJayy Nov 1, 2025
ba0ce0e
refactor: adjust indentation for clarity in transformImageToFigure fu…
YungKingJayy Nov 1, 2025
d6c90f7
refactor: change positioning of drag handle to left-start
YungKingJayy Nov 1, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 33 additions & 5 deletions apps/cms/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
"@better-fetch/fetch": "^1.1.18",
"@databuddy/sdk": "^2.1.75",
"@discordjs/builders": "^1.11.3",
"@floating-ui/dom": "^1.6.11",
"@hookform/resolvers": "^5.2.0",
"@marble/db": "workspace:*",
"@marble/parser": "workspace:*",
Expand All @@ -28,6 +29,7 @@
"@radix-ui/react-dialog": "^1.1.4",
"@radix-ui/react-dropdown-menu": "^2.1.4",
"@radix-ui/react-label": "^2.1.0",
"@radix-ui/react-popover": "^1.1.15",
"@radix-ui/react-select": "^2.1.4",
"@radix-ui/react-separator": "^1.1.0",
"@radix-ui/react-slot": "^1.1.0",
Expand All @@ -40,10 +42,34 @@
"@tanstack/react-query-devtools": "^5.85.5",
"@tanstack/react-table": "^8.20.5",
"@tanstack/react-virtual": "^3.13.12",
"@tiptap/core": "^2.11.2",
"@tiptap/extension-text-align": "^2.11.2",
"@tiptap/extension-underline": "^2.11.2",
"@tiptap/react": "^2.11.2",
"@tiptap/core": "^3.7.2",
"@tiptap/extension-bubble-menu": "^3.7.2",
"@tiptap/extension-code-block": "^3.7.1",
"@tiptap/extension-code-block-lowlight": "^3.7.1",
"@tiptap/extension-collaboration": "^3.7.2",
"@tiptap/extension-color": "^3.7.2",
"@tiptap/extension-document": "^3.8.0",
"@tiptap/extension-drag-handle": "^3.7.2",
"@tiptap/extension-drag-handle-react": "^3.7.2",
"@tiptap/extension-file-handler": "^3.8.0",
"@tiptap/extension-highlight": "^3.7.2",
"@tiptap/extension-horizontal-rule": "^3.7.1",
"@tiptap/extension-image": "^3.7.1",
"@tiptap/extension-list": "^3.7.1",
"@tiptap/extension-node-range": "^3.7.2",
"@tiptap/extension-subscript": "^3.7.2",
"@tiptap/extension-superscript": "^3.7.2",
"@tiptap/extension-table": "^3.7.2",
"@tiptap/extension-text-align": "^3.7.1",
"@tiptap/extension-text-style": "^3.7.2",
"@tiptap/extension-underline": "^3.7.1",
"@tiptap/extension-youtube": "^3.7.1",
"@tiptap/extensions": "^3.7.1",
"@tiptap/markdown": "^3.9.0",
"@tiptap/pm": "^3.7.2",
"@tiptap/react": "^3.7.2",
"@tiptap/starter-kit": "^3.7.1",
"@tiptap/suggestion": "^3.7.2",
"@upstash/qstash": "^2.8.2",
"@upstash/ratelimit": "^2.0.5",
"@upstash/redis": "^1.35.3",
Expand All @@ -64,9 +90,9 @@
"next": "16.0.0",
"next-themes": "^0.4.4",
"node-html-markdown": "^1.3.0",
"novel": "^1.0.2",
"nuqs": "^2.6.0",
"react": "^19.2.0",
"react-colorful": "^5.6.1",
"react-dom": "^19.2.0",
"react-dropzone": "^14.3.8",
"react-hook-form": "^7.61.1",
Expand All @@ -80,6 +106,8 @@
"sonner": "^1.7.1",
"tailwindcss-animate": "^1.0.7",
"vaul": "^1.1.2",
"y-protocols": "^1.0.6",
"yjs": "^13.6.27",
"zod": "^3.25.76"
},
"devDependencies": {
Expand Down
6 changes: 3 additions & 3 deletions apps/cms/src/components/editor/ai/readability-suggestions.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

import { cn } from "@marble/ui/lib/utils";
import { CursorClickIcon } from "@phosphor-icons/react";
import type { EditorInstance } from "novel";
import type { Editor } from "@tiptap/core";
import React from "react";

export type ReadabilitySuggestion = {
Expand All @@ -12,13 +12,13 @@ export type ReadabilitySuggestion = {
};

type ReadabilitySuggestionsProps = {
editor?: EditorInstance | null;
editor?: Editor | null;
suggestions: ReadabilitySuggestion[];
isLoading?: boolean;
onRefresh?: () => void;
};

function highlightTextInEditor(editor: EditorInstance, textReference: string) {
function highlightTextInEditor(editor: Editor, textReference: string) {
const trimmed = textReference.trim();
if (!trimmed) {
return;
Expand Down
88 changes: 82 additions & 6 deletions apps/cms/src/components/editor/bubble-menu.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,88 @@
import { EditorBubble } from "novel";
"use client";

import { useCurrentEditor } from "@tiptap/react";
import { BubbleMenu as TiptapBubbleMenu } from "@tiptap/react/menus";
import { memo, useCallback, useRef } from "react";
import { FloatingPortalProvider } from "@/components/editor/floating-portal-context";
import { isColumnGripSelected } from "./extensions/table/menus/TableColumn/utils";
import { isRowGripSelected } from "./extensions/table/menus/TableRow/utils";
import { LinkSelector } from "./link-selector";
import { TextButtons } from "./text-buttons";

export function BubbleMenu() {
function BubbleMenuComponent() {
const { editor } = useCurrentEditor();
const containerRef = useRef<HTMLDivElement>(null);

const shouldShow = useCallback(
({
view,
state,
from,
}: {
view: unknown;
state: unknown;
from: number;
}) => {
if (!editor || !state) {
return false;
}

// Hide bubble menu if image, imageUpload, youtube, or youtubeUpload is selected
if (
editor.isActive("figure") ||
editor.isActive("image") ||
editor.isActive("imageUpload") ||
editor.isActive("youtube") ||
editor.isActive("youtubeUpload")
) {
return false;
}

// Hide bubble menu if table row or column grip is selected
const isRowGrip = isRowGripSelected({
editor,
view,
state,
from,
} as Parameters<typeof isRowGripSelected>[0]);

const isColumnGrip = isColumnGripSelected({
editor,
view,
state,
from: from || 0,
} as Parameters<typeof isColumnGripSelected>[0]);

if (isRowGrip || isColumnGrip) {
return false;
}

// Show for normal text selection
return !editor.state.selection.empty;
},
[editor]
);

if (!editor) {
return null;
}

return (
<EditorBubble className="flex h-fit w-fit overflow-hidden rounded-md border bg-background p-1 shadow-sm">
<TextButtons />
<LinkSelector />
</EditorBubble>
<div className="contents" ref={containerRef}>
<FloatingPortalProvider container={containerRef.current}>
<TiptapBubbleMenu
appendTo={() => document.body}
className="z-50 flex h-fit w-fit gap-0.5 overflow-hidden rounded-lg border bg-background p-1 shadow-sm"
editor={editor}
shouldShow={shouldShow}
>
<TextButtons />
<LinkSelector />
</TiptapBubbleMenu>
</FloatingPortalProvider>
</div>
);
}

// Memoize to prevent context cascade rerenders
export const BubbleMenu = memo(BubbleMenuComponent);
89 changes: 89 additions & 0 deletions apps/cms/src/components/editor/color-picker.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
import { Button } from "@marble/ui/components/button";
import { Input } from "@marble/ui/components/input";
import { ArrowCounterClockwiseIcon } from "@phosphor-icons/react";
import { useCallback, useEffect, useState } from "react";
import { HexColorPicker } from "react-colorful";

const PRESET_COLORS = [
"#fb7185", // Rose
"#fdba74", // Orange
"#d9f99d", // Lime
"#a7f3d0", // Emerald
"#a5f3fc", // Cyan
"#a5b4fc", // Indigo
];

export const ColorPicker = ({
color,
onChange,
onClear,
}: {
color?: string;
onChange: (color: string) => void;
onClear: () => void;
}) => {
const [hexInput, setHexInput] = useState(color || "");

useEffect(() => {
setHexInput(color || "");
}, [color]);

const handleHexInputChange = useCallback(
(e: React.ChangeEvent<HTMLInputElement>) => {
const value = e.target.value;
setHexInput(value);

// Validate hex color format
if (/^#[0-9A-Fa-f]{6}$/.test(value)) {
onChange(value);
}
},
[onChange]
);

const handleColorChange = useCallback(
(newColor: string) => {
setHexInput(newColor);
onChange(newColor);
},
[onChange]
);

return (
<div className="flex flex-col gap-3 p-3">
<HexColorPicker color={color || "#000000"} onChange={handleColorChange} />

<div className="flex items-center gap-2">
<Input
className="h-8 font-mono text-xs"
onChange={handleHexInputChange}
placeholder="#000000"
value={hexInput}
/>
</div>
Comment on lines +56 to +63
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Add accessible name for the hex input.

Input lacks an associated label. Provide an aria-label to satisfy a11y guideline.

-        <Input
+        <Input
           className="h-8 font-mono text-xs"
           onChange={handleHexInputChange}
           placeholder="#000000"
           value={hexInput}
+          aria-label="Hex color"
         />
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<div className="flex items-center gap-2">
<Input
className="h-8 font-mono text-xs"
onChange={handleHexInputChange}
placeholder="#000000"
value={hexInput}
/>
</div>
<div className="flex items-center gap-2">
<Input
className="h-8 font-mono text-xs"
onChange={handleHexInputChange}
placeholder="#000000"
value={hexInput}
aria-label="Hex color"
/>
</div>
🤖 Prompt for AI Agents
In apps/cms/src/components/editor/color-picker.tsx around lines 56 to 63, the
hex input has no accessible name; add an accessible label by providing an
aria-label (or aria-labelledby) on the Input element (e.g., aria-label="Hex
color" or a more descriptive string) so screen readers can identify the control;
ensure the aria-label stays in sync with purpose and keep placeholder as
presentational only.


<div className="flex items-center gap-2">
{PRESET_COLORS.map((presetColor) => (
<button
className="size-6 rounded border border-border transition-transform hover:scale-110"
key={presetColor}
onClick={() => handleColorChange(presetColor)}
style={{ backgroundColor: presetColor }}
title={presetColor}
type="button"
/>
))}
Comment on lines +66 to +75
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Icon-only preset buttons need accessible labels.

Title isn’t a reliable accessible name. Add aria-label; keep type="button".

           <button
             className="size-6 rounded border border-border transition-transform hover:scale-110"
             key={presetColor}
             onClick={() => handleColorChange(presetColor)}
             style={{ backgroundColor: presetColor }}
-            title={presetColor}
+            title={presetColor}
+            aria-label={`Set color ${presetColor}`}
             type="button"
           />
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
{PRESET_COLORS.map((presetColor) => (
<button
className="size-6 rounded border border-border transition-transform hover:scale-110"
key={presetColor}
onClick={() => handleColorChange(presetColor)}
style={{ backgroundColor: presetColor }}
title={presetColor}
type="button"
/>
))}
{PRESET_COLORS.map((presetColor) => (
<button
className="size-6 rounded border border-border transition-transform hover:scale-110"
key={presetColor}
onClick={() => handleColorChange(presetColor)}
style={{ backgroundColor: presetColor }}
title={presetColor}
aria-label={`Set color ${presetColor}`}
type="button"
/>
))}
🤖 Prompt for AI Agents
In apps/cms/src/components/editor/color-picker.tsx around lines 66 to 75, the
icon-only preset color buttons rely on title for labeling which is not a
reliable accessible name; add an explicit aria-label (for example "Select color
{presetColor}" or simply the color name) to each button, keep type="button" and
you may preserve the title if desired, so screen readers receive a proper
accessible name for each preset button.

<Button
className="size-8 shrink-0"
onClick={onClear}
size="icon"
title="Reset color"
type="button"
variant="ghost"
>
<ArrowCounterClockwiseIcon className="size-4" />
</Button>
Comment on lines +76 to +85
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Add aria-label to reset button and hide decorative SVG from AT.

Provide an explicit accessible name and mark the icon as decorative.

-        <Button
+        <Button
           className="size-8 shrink-0"
           onClick={onClear}
           size="icon"
-          title="Reset color"
+          title="Reset color"
+          aria-label="Reset color"
           type="button"
           variant="ghost"
         >
-          <ArrowCounterClockwiseIcon className="size-4" />
+          <ArrowCounterClockwiseIcon className="size-4" aria-hidden="true" focusable="false" />
         </Button>
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<Button
className="size-8 shrink-0"
onClick={onClear}
size="icon"
title="Reset color"
type="button"
variant="ghost"
>
<ArrowCounterClockwiseIcon className="size-4" />
</Button>
<Button
className="size-8 shrink-0"
onClick={onClear}
size="icon"
title="Reset color"
aria-label="Reset color"
type="button"
variant="ghost"
>
<ArrowCounterClockwiseIcon className="size-4" aria-hidden="true" focusable="false" />
</Button>
🤖 Prompt for AI Agents
In apps/cms/src/components/editor/color-picker.tsx around lines 76 to 85, the
reset Button lacks an accessible name and the SVG icon is exposed to assistive
tech; add an explicit aria-label (e.g., aria-label="Reset color") to the Button
and mark the ArrowCounterClockwiseIcon as decorative by adding
aria-hidden="true" (or role="presentation" and focusable="false") so screen
readers ignore the SVG.

</div>
</div>
);
};
Loading