Compare commits

..
32 changed files with 825 additions and 1462 deletions
-6
View File
@@ -20,12 +20,6 @@ http {
server {
listen 3000;
# Security headers
add_header X-Frame-Options "DENY" always;
add_header X-Content-Type-Options "nosniff" always;
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
add_header X-XSS-Protection "1; mode=block" always;
location / {
root /usr/share/nginx/html;
index index.html index.htm;
+1
View File
@@ -45,6 +45,7 @@
"react-dom": "catalog:",
"react-hook-form": "7.51.5",
"react-router": "catalog:",
"react-router-dom": "catalog:",
"serve": "14.2.5",
"swr": "catalog:",
"uuid": "catalog:"
+3 -2
View File
@@ -219,7 +219,7 @@ def create_pages(workspace, project, user_id, pages_count):
Faker.seed(0)
pages = []
for _ in range(0, pages_count):
for index in range(0, pages_count):
text = fake.text(max_nb_chars=60000)
pages.append(
Page(
@@ -231,13 +231,14 @@ def create_pages(workspace, project, user_id, pages_count):
description_html=f"<p>{text}</p>",
archived_at=None,
is_locked=False,
sort_order=index * 10000,
)
)
# Bulk create pages
pages = Page.objects.bulk_create(pages, ignore_conflicts=True)
# Add Page to project
ProjectPage.objects.bulk_create(
[ProjectPage(page=page, project=project, workspace=workspace) for page in pages],
[ProjectPage(page=page, project=project, workspace=workspace, sort_order=page.sort_order) for page in pages],
batch_size=1000,
)
@@ -0,0 +1,32 @@
# Generated by Django 4.2.22 on 2026-01-05 13:28
from django.db import migrations, models
def update_projectpage_sort_order(apps, schema_editor):
ProjectPage = apps.get_model("db", "ProjectPage")
Project = apps.get_model("db", "Project")
for project in Project.objects.all():
pages = list(
ProjectPage.objects.filter(project=project).order_by("created_at")
)
for index, page in enumerate(pages):
page.sort_order = index * 10000
ProjectPage.objects.bulk_update(pages, ["sort_order"], batch_size=3000)
class Migration(migrations.Migration):
dependencies = [
('db', '0113_webhook_version'),
]
operations = [
migrations.AddField(
model_name='projectpage',
name='sort_order',
field=models.FloatField(default=65535),
),
migrations.RunPython(update_projectpage_sort_order, reverse_code=migrations.RunPython.noop),
]
@@ -1,38 +0,0 @@
# Generated by Django 4.2.27 on 2026-01-13 10:38
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('db', '0115_auto_20260105_1406'),
]
operations = [
migrations.AddField(
model_name='profile',
name='notification_view_mode',
field=models.CharField(choices=[('full', 'Full'), ('compact', 'Compact')], default='full', max_length=255),
),
migrations.AddField(
model_name='user',
name='is_password_reset_required',
field=models.BooleanField(default=False),
),
migrations.AddField(
model_name='workspacemember',
name='explored_features',
field=models.JSONField(default=dict),
),
migrations.AddField(
model_name='workspacemember',
name='getting_started_checklist',
field=models.JSONField(default=dict),
),
migrations.AddField(
model_name='workspacemember',
name='tips',
field=models.JSONField(default=dict),
),
]
+78 -2
View File
@@ -4,10 +4,11 @@ from django.conf import settings
from django.utils import timezone
# Django imports
from django.db import models
from django.db import connection, models, transaction
# Module imports
from plane.utils.html_processor import strip_tags
from plane.utils.uuid import convert_uuid_to_integer
from .base import BaseModel
@@ -63,6 +64,22 @@ class Page(BaseModel):
"""Return owner email and page name"""
return f"{self.owned_by.email} <{self.name}>"
def _get_sort_order(self, project):
"""Get the next sort order for the page within a specific project."""
if self.access == Page.PRIVATE_ACCESS:
largest = ProjectPage.objects.filter(
page__access=Page.PRIVATE_ACCESS,
page__owned_by=self.owned_by,
project=project,
).aggregate(largest=models.Max("sort_order"))["largest"]
else:
largest = ProjectPage.objects.filter(
page__access=Page.PUBLIC_ACCESS,
project=project,
).aggregate(largest=models.Max("sort_order"))["largest"]
return (largest or self.DEFAULT_SORT_ORDER) + 10000
def save(self, *args, **kwargs):
# Strip the html tags using html parser
self.description_stripped = (
@@ -70,7 +87,27 @@ class Page(BaseModel):
if (self.description_html == "" or self.description_html is None)
else strip_tags(self.description_html)
)
super(Page, self).save(*args, **kwargs)
if not self._state.adding:
original = Page.objects.get(pk=self.pk)
if original.access != self.access:
with transaction.atomic():
# Get the project pages for the page and update the sort order
project_pages = list(ProjectPage.objects.filter(page=self).select_related("project"))
# Acquire advisory locks for all projects to prevent race conditions
for project_page in project_pages:
lock_key = convert_uuid_to_integer(project_page.project_id)
with connection.cursor() as cursor:
cursor.execute("SELECT pg_advisory_xact_lock(%s)", [lock_key])
for project_page in project_pages:
project_page.sort_order = self._get_sort_order(project_page.project)
# Bulk update all project pages in a single query
if project_pages:
ProjectPage.objects.bulk_update(project_pages, ["sort_order"])
super(Page, self).save(*args, **kwargs)
class PageLog(BaseModel):
@@ -129,9 +166,12 @@ class PageLabel(BaseModel):
class ProjectPage(BaseModel):
DEFAULT_SORT_ORDER = 65535
project = models.ForeignKey("db.Project", on_delete=models.CASCADE, related_name="project_pages")
page = models.ForeignKey("db.Page", on_delete=models.CASCADE, related_name="project_pages")
workspace = models.ForeignKey("db.Workspace", on_delete=models.CASCADE, related_name="project_pages")
sort_order = models.FloatField(default=DEFAULT_SORT_ORDER)
class Meta:
unique_together = ["project", "page", "deleted_at"]
@@ -150,6 +190,42 @@ class ProjectPage(BaseModel):
def __str__(self):
return f"{self.project.name} {self.page.name}"
def _get_sort_order(self):
"""Get the next sort order for the project page based on page access type."""
if self.page.access == Page.PRIVATE_ACCESS:
# For private pages, get max sort_order among pages owned by same user in same project
largest = ProjectPage.objects.filter(
page__access=Page.PRIVATE_ACCESS,
page__owned_by=self.page.owned_by,
project=self.project,
).aggregate(largest=models.Max("sort_order"))["largest"]
else:
# For public pages, get max sort_order among all public pages in same project
largest = ProjectPage.objects.filter(
page__access=Page.PUBLIC_ACCESS,
project=self.project,
).aggregate(largest=models.Max("sort_order"))["largest"]
return (largest or self.DEFAULT_SORT_ORDER) + 10000
def save(self, *args, **kwargs):
# Set sort_order for new project pages
if self._state.adding:
with transaction.atomic():
# Create a lock for this specific project using a transaction-level advisory lock
# This ensures only one transaction per project can execute this code at a time
# The lock is automatically released when the transaction ends
lock_key = convert_uuid_to_integer(self.project_id)
with connection.cursor() as cursor:
# Get an exclusive transaction-level lock using the project ID as the lock key
cursor.execute("SELECT pg_advisory_xact_lock(%s)", [lock_key])
self.sort_order = self._get_sort_order()
super(ProjectPage, self).save(*args, **kwargs)
else:
super(ProjectPage, self).save(*args, **kwargs)
class PageVersion(BaseModel):
workspace = models.ForeignKey("db.Workspace", on_delete=models.CASCADE, related_name="page_versions")
+2 -8
View File
@@ -84,7 +84,7 @@ class User(AbstractBaseUser, PermissionsMixin):
is_staff = models.BooleanField(default=False)
is_email_verified = models.BooleanField(default=False)
is_password_autoset = models.BooleanField(default=False)
is_password_reset_required = models.BooleanField(default=False)
# random token generated
token = models.CharField(max_length=64, blank=True)
@@ -192,10 +192,6 @@ class Profile(TimeAuditModel):
FRIDAY = 5
SATURDAY = 6
class NotificationViewMode(models.TextChoices):
FULL = "full", "Full"
COMPACT = "compact", "Compact"
START_OF_THE_WEEK_CHOICES = (
(SUNDAY, "Sunday"),
(MONDAY, "Monday"),
@@ -225,9 +221,7 @@ class Profile(TimeAuditModel):
billing_address = models.JSONField(null=True)
has_billing_address = models.BooleanField(default=False)
company_name = models.CharField(max_length=255, blank=True)
notification_view_mode = models.CharField(
max_length=255, choices=NotificationViewMode.choices, default=NotificationViewMode.FULL
)
is_smooth_cursor_enabled = models.BooleanField(default=False)
# mobile
is_mobile_onboarded = models.BooleanField(default=False)
-3
View File
@@ -214,9 +214,6 @@ class WorkspaceMember(BaseModel):
default_props = models.JSONField(default=get_default_props)
issue_props = models.JSONField(default=get_issue_props)
is_active = models.BooleanField(default=True)
getting_started_checklist = models.JSONField(default=dict)
tips = models.JSONField(default=dict)
explored_features = models.JSONField(default=dict)
class Meta:
unique_together = ["workspace", "member", "deleted_at"]
@@ -98,7 +98,7 @@ export const AuthRoot = observer(function AuthRoot() {
}
if (currentAuthMode === EAuthModes.SIGN_IN) {
if (isSMTPConfigured && isMagicLoginEnabled && response.status === "MAGIC_CODE") {
if (response.is_password_autoset && isSMTPConfigured && isMagicLoginEnabled) {
setAuthStep(EAuthSteps.UNIQUE_CODE);
generateEmailUniqueCode(data.email);
} else if (isEmailPasswordEnabled) {
@@ -109,7 +109,7 @@ export const AuthRoot = observer(function AuthRoot() {
setErrorInfo(errorhandler);
}
} else {
if (isSMTPConfigured && isMagicLoginEnabled && response.status === "MAGIC_CODE") {
if (isSMTPConfigured && isMagicLoginEnabled) {
setAuthStep(EAuthSteps.UNIQUE_CODE);
generateEmailUniqueCode(data.email);
} else if (isEmailPasswordEnabled) {
@@ -119,7 +119,6 @@ export const AuthRoot = observer(function AuthRoot() {
setErrorInfo(errorhandler);
}
}
return;
})
.catch((error) => {
const errorhandler = authErrorHandler(error?.error_code?.toString(), data?.email || undefined);
-1
View File
@@ -19,7 +19,6 @@ export interface IEmailCheckData {
}
export interface IEmailCheckResponse {
status: "MAGIC_CODE" | "CREDENTIAL";
is_password_autoset: boolean;
existing: boolean;
}
+1
View File
@@ -50,6 +50,7 @@
"react-hook-form": "7.51.5",
"react-popper": "^2.3.0",
"react-router": "catalog:",
"react-router-dom": "catalog:",
"swr": "catalog:",
"uuid": "catalog:"
},
-6
View File
@@ -20,12 +20,6 @@ http {
server {
listen 3000;
# Security headers
add_header X-Frame-Options "DENY" always;
add_header X-Content-Type-Options "nosniff" always;
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
add_header X-XSS-Protection "1; mode=block" always;
location / {
root /usr/share/nginx/html;
index index.html index.htm;
+1
View File
@@ -67,6 +67,7 @@
"react-pdf-html": "^2.1.2",
"react-popper": "^2.3.0",
"react-router": "catalog:",
"react-router-dom": "catalog:",
"recharts": "^2.12.7",
"serve": "14.2.5",
"smooth-scroll-into-view-if-needed": "^2.0.2",
+1 -2
View File
@@ -69,8 +69,7 @@
"prosemirror-view": "1.40.0",
"@types/express": "4.17.23",
"typescript": "catalog:",
"vite": "catalog:",
"qs": "6.14.1"
"vite": "catalog:"
},
"onlyBuiltDependencies": [
"@sentry/cli",
+9 -28
View File
@@ -62,9 +62,7 @@ Retrieves variable declarators from the current collection.
- **Parameters**: `callback` (Function)
- **Example**:
```javascript
const variableDeclarators = j
.find(j.Identifier)
.getVariableDeclarators((path) => path.value.name);
const variableDeclarators = j.find(j.Identifier).getVariableDeclarators((path) => path.value.name);
```
#### `findVariableDeclarators`
@@ -84,9 +82,7 @@ Filters nodes based on a predicate function.
- **Parameters**: `predicate` (Function)
- **Example**:
```javascript
const constDeclarations = j
.find(j.VariableDeclaration)
.filter((path) => path.node.kind === "const");
const constDeclarations = j.find(j.VariableDeclaration).filter((path) => path.node.kind === "const");
```
#### `forEach`
@@ -108,9 +104,7 @@ Checks if at least one element in the collection passes the test.
- **Parameters**: `callback` (Function)
- **Example**:
```javascript
const hasVariableA = root
.find(j.VariableDeclarator)
.some((path) => path.node.id.name === "a");
const hasVariableA = root.find(j.VariableDeclarator).some((path) => path.node.id.name === "a");
```
#### `every`
@@ -120,9 +114,7 @@ Checks if all elements in the collection pass the test.
- **Parameters**: `callback` (Function)
- **Example**:
```javascript
const allAreConst = root
.find(j.VariableDeclaration)
.every((path) => path.node.kind === "const");
const allAreConst = root.find(j.VariableDeclaration).every((path) => path.node.kind === "const");
```
#### `map`
@@ -132,9 +124,7 @@ Maps each node in the collection to a new value.
- **Parameters**: `callback` (Function)
- **Example**:
```javascript
const variableNames = j
.find(j.VariableDeclaration)
.map((path) => path.node.declarations.map((decl) => decl.id.name));
const variableNames = j.find(j.VariableDeclaration).map((path) => path.node.declarations.map((decl) => decl.id.name));
```
#### `size`
@@ -217,10 +207,7 @@ Checks if the node in the collection is of a specific type.
- **Parameters**: `type` (String)
- **Example**:
```javascript
const isVariableDeclarator = root
.find(j.VariableDeclarator)
.at(0)
.isOfType("VariableDeclarator");
const isVariableDeclarator = root.find(j.VariableDeclarator).at(0).isOfType("VariableDeclarator");
```
### Node Transformation APIs
@@ -232,9 +219,7 @@ Replaces the current node(s) with a new node.
- **Parameters**: `newNode` (Node or Function)
- **Example**:
```javascript
j.find(j.Identifier).replaceWith((path) =>
j.identifier(path.node.name.toUpperCase())
);
j.find(j.Identifier).replaceWith((path) => j.identifier(path.node.name.toUpperCase()));
```
#### `insertBefore`
@@ -244,9 +229,7 @@ Inserts a node before the current node.
- **Parameters**: `newNode` (Node)
- **Example**:
```javascript
j.find(j.FunctionDeclaration).insertBefore(
j.expressionStatement(j.stringLiteral("Inserted before"))
);
j.find(j.FunctionDeclaration).insertBefore(j.expressionStatement(j.stringLiteral("Inserted before")));
```
#### `insertAfter`
@@ -256,9 +239,7 @@ Inserts a node after the current node.
- **Parameters**: `newNode` (Node)
- **Example**:
```javascript
j.find(j.FunctionDeclaration).insertAfter(
j.expressionStatement(j.stringLiteral("Inserted after"))
);
j.find(j.FunctionDeclaration).insertAfter(j.expressionStatement(j.stringLiteral("Inserted after")));
```
#### `remove`
@@ -35,7 +35,6 @@ export function CustomCalloutBlock(props: CustomCalloutNodeViewProps) {
}}
>
<CalloutBlockLogoSelector
key={node.attrs["id"]}
blockAttributes={node.attrs}
disabled={!editor.isEditable}
isOpen={isEmojiPickerOpen}
@@ -33,7 +33,7 @@ export function CalloutBlockLogoSelector(props: Props) {
return (
<div contentEditable={false}>
<EmojiPicker
closeOnSelect={true}
closeOnSelect={false}
isOpen={isOpen}
handleToggle={handleOpen}
className="flex-shrink-0 grid place-items-center"
@@ -1,459 +0,0 @@
import { computePosition, flip, shift, autoUpdate } from "@floating-ui/dom";
import type { Editor } from "@tiptap/core";
import { TableMap } from "@tiptap/pm/tables";
// constants
import { CORE_EXTENSIONS } from "@/constants/extension";
// icons
import { DRAG_HANDLE_ICONS, createSvgElement } from "../icons";
// dropdown
import { createDropdownContent, handleDropdownAction } from "../dropdown-content";
import type { DropdownOption } from "../dropdown-content";
// extensions
import {
findTable,
getTableHeightPx,
getTableWidthPx,
isCellSelection,
selectColumn,
getSelectedColumns,
} from "@/extensions/table/table/utilities/helpers";
// local imports
import { moveSelectedColumns, duplicateColumns } from "../actions";
import {
DROP_MARKER_THICKNESS,
getDropMarker,
getColDragMarker,
hideDragMarker,
hideDropMarker,
updateColDragMarker,
updateColDropMarker,
} from "../marker-utils";
import { updateCellContentVisibility } from "../utils";
import { calculateColumnDropIndex, constructColumnDragPreview, getTableColumnNodesInfo } from "./utils";
export type ColumnDragHandleConfig = {
editor: Editor;
col: number;
};
/**
* Creates a vanilla JS column drag handle element with dropdown functionality
*/
export function createColumnDragHandle(config: ColumnDragHandleConfig): {
element: HTMLElement;
destroy: () => void;
} {
const { editor, col } = config;
// Create container
const container = document.createElement("div");
container.className =
"table-col-handle-container absolute z-20 top-0 left-0 flex justify-center items-center w-full -translate-y-1/2";
// Create button
const button = document.createElement("button");
button.type = "button";
button.className = "default-state";
// Create icon (Ellipsis lucide icon as SVG)
const icon = createSvgElement(DRAG_HANDLE_ICONS.ellipsis, "size-4 text-primary");
button.appendChild(icon);
container.appendChild(button);
// State for dropdown
let isDropdownOpen = false;
let dropdownElement: HTMLElement | null = null;
let backdropElement: HTMLElement | null = null;
let cleanupFloating: (() => void) | null = null;
let backdropClickHandler: (() => void) | null = null;
// Track drag event listeners for cleanup
let dragListeners: {
mouseup?: (e: MouseEvent) => void;
mousemove?: (e: MouseEvent) => void;
} = {};
// Dropdown toggle function
const toggleDropdown = () => {
if (isDropdownOpen) {
closeDropdown();
} else {
openDropdown();
}
};
const closeDropdown = () => {
if (!isDropdownOpen) return;
isDropdownOpen = false;
// Reset button to default state
button.className = "default-state";
// Remove dropdown and backdrop
if (dropdownElement) {
dropdownElement.remove();
dropdownElement = null;
}
if (backdropElement) {
// Remove backdrop listener before removing element
if (backdropClickHandler) {
backdropElement.removeEventListener("click", backdropClickHandler);
backdropClickHandler = null;
}
backdropElement.remove();
backdropElement = null;
}
// Cleanup floating UI (this also removes keydown listener)
if (cleanupFloating) {
cleanupFloating();
cleanupFloating = null;
}
// Remove active dropdown extension
setTimeout(() => {
editor.commands.removeActiveDropbarExtension(CORE_EXTENSIONS.TABLE);
}, 0);
};
const openDropdown = () => {
if (isDropdownOpen) return;
isDropdownOpen = true;
// Update button to open state
button.className = "open-state";
// Add active dropdown extension
editor.commands.addActiveDropbarExtension(CORE_EXTENSIONS.TABLE);
// Create backdrop
backdropElement = document.createElement("div");
backdropElement.style.position = "fixed";
backdropElement.style.inset = "0";
backdropElement.style.zIndex = "99";
backdropClickHandler = closeDropdown;
backdropElement.addEventListener("click", backdropClickHandler);
document.body.appendChild(backdropElement);
// Create dropdown
dropdownElement = document.createElement("div");
dropdownElement.className =
"max-h-[90vh] w-[12rem] overflow-y-auto rounded-md border-[0.5px] border-strong bg-surface-1 px-2 py-2.5 shadow-raised-200";
dropdownElement.style.position = "fixed";
dropdownElement.style.zIndex = "100";
// Create and append dropdown content
const content = createDropdownContent(getDropdownOptions());
dropdownElement.appendChild(content);
document.body.appendChild(dropdownElement);
// Attach dropdown event listeners
attachDropdownEventListeners(dropdownElement);
// Setup floating UI positioning
cleanupFloating = autoUpdate(button, dropdownElement, () => {
void computePosition(button, dropdownElement!, {
placement: "bottom-start",
middleware: [
flip({
fallbackPlacements: ["top-start", "bottom-start", "top-end", "bottom-end"],
}),
shift({
padding: 8,
}),
],
}).then(({ x, y }) => {
if (dropdownElement) {
dropdownElement.style.left = `${x}px`;
dropdownElement.style.top = `${y}px`;
}
return;
});
});
// Handle keyboard events
const handleKeyDown = (event: KeyboardEvent) => {
closeDropdown();
event.preventDefault();
event.stopPropagation();
};
document.addEventListener("keydown", handleKeyDown);
// Store cleanup for this specific dropdown instance
const originalCleanup = cleanupFloating;
cleanupFloating = () => {
document.removeEventListener("keydown", handleKeyDown);
if (originalCleanup) originalCleanup();
};
};
const getDropdownOptions = (): DropdownOption[] => [
{
key: "toggle-header",
label: "Header column",
icon: DRAG_HANDLE_ICONS.toggleRight,
showRightIcon: true,
},
{
key: "insert-left",
label: "Insert left",
icon: DRAG_HANDLE_ICONS.arrowLeft,
showRightIcon: false,
},
{
key: "insert-right",
label: "Insert right",
icon: DRAG_HANDLE_ICONS.arrowRight,
showRightIcon: false,
},
{
key: "duplicate",
label: "Duplicate",
icon: DRAG_HANDLE_ICONS.duplicate,
showRightIcon: false,
},
{
key: "clear-contents",
label: "Clear contents",
icon: DRAG_HANDLE_ICONS.close,
showRightIcon: false,
},
{
key: "delete",
label: "Delete",
icon: DRAG_HANDLE_ICONS.trash,
showRightIcon: false,
},
];
const attachDropdownEventListeners = (dropdown: HTMLElement) => {
const buttons = dropdown.querySelectorAll("button[data-action]");
const colorPanel = dropdown.querySelector(".color-panel");
const colorChevron = dropdown.querySelector(".color-chevron");
buttons.forEach((btn) => {
const action = btn.getAttribute("data-action");
if (!action) return;
btn.addEventListener("click", (e) => {
e.preventDefault();
e.stopPropagation();
// Handle common actions
handleDropdownAction(action, editor, closeDropdown, colorPanel, colorChevron);
// Handle column-specific actions
switch (action) {
case "toggle-header":
editor.chain().focus().toggleHeaderColumn().run();
closeDropdown();
break;
case "set-bg-color": {
const color = btn.getAttribute("data-color");
if (color) {
editor
.chain()
.focus()
.updateAttributes(CORE_EXTENSIONS.TABLE_CELL, {
background: color,
})
.run();
}
closeDropdown();
break;
}
case "insert-left":
editor.chain().focus().addColumnBefore().run();
closeDropdown();
break;
case "insert-right":
editor.chain().focus().addColumnAfter().run();
closeDropdown();
break;
case "duplicate": {
const table = findTable(editor.state.selection);
if (table) {
const tableMap = TableMap.get(table.node);
let tr = editor.state.tr;
const selectedColumns = getSelectedColumns(editor.state.selection, tableMap);
tr = duplicateColumns(table, selectedColumns, tr);
editor.view.dispatch(tr);
}
closeDropdown();
break;
}
case "clear-contents":
editor.chain().focus().clearSelectedCells().run();
closeDropdown();
break;
case "delete":
editor.chain().focus().deleteColumn().run();
closeDropdown();
break;
}
});
});
};
// Handle mousedown for dragging
const handleMouseDown = (e: MouseEvent) => {
// Prevent dropdown from opening during drag
if (e.button !== 0) return; // Only left click
e.stopPropagation();
e.preventDefault();
// Check if this is a click (will be determined by mouseup without much movement)
const startX = e.clientX;
const startY = e.clientY;
let hasMoved = false;
// Clean up any existing drag listeners
if (dragListeners.mouseup) {
window.removeEventListener("mouseup", dragListeners.mouseup);
}
if (dragListeners.mousemove) {
window.removeEventListener("mousemove", dragListeners.mousemove);
}
dragListeners = {};
const table = findTable(editor.state.selection);
if (!table) return;
editor.view.dispatch(selectColumn(table, col, editor.state.tr));
// Drag column logic
const tableWidthPx = getTableWidthPx(table, editor);
const columns = getTableColumnNodesInfo(table, editor);
let dropIndex = col;
const startLeft = columns[col].left ?? 0;
const startXPos = e.clientX;
const tableElement = editor.view.nodeDOM(table.pos);
const dropMarker = tableElement instanceof HTMLElement ? getDropMarker(tableElement) : null;
const dragMarker = tableElement instanceof HTMLElement ? getColDragMarker(tableElement) : null;
const handleFinish = (): void => {
// Clean up markers if they exist
if (dropMarker && dragMarker) {
hideDropMarker(dropMarker);
hideDragMarker(dragMarker);
}
if (isCellSelection(editor.state.selection)) {
updateCellContentVisibility(editor, false);
}
// Perform drag operation if user moved
if (col !== dropIndex && hasMoved) {
let tr = editor.state.tr;
const selection = editor.state.selection;
if (isCellSelection(selection)) {
const table = findTable(selection);
if (table) {
tr = moveSelectedColumns(editor, table, selection, dropIndex, tr);
}
}
editor.view.dispatch(tr);
}
window.removeEventListener("mouseup", handleFinish);
window.removeEventListener("mousemove", handleMove);
dragListeners.mouseup = undefined;
dragListeners.mousemove = undefined;
// If it was just a click (no movement), toggle dropdown
if (!hasMoved) {
toggleDropdown();
}
};
let pseudoColumn: HTMLElement | undefined;
const handleMove = (moveEvent: MouseEvent): void => {
// Mark that we've moved
const deltaX = Math.abs(moveEvent.clientX - startX);
const deltaY = Math.abs(moveEvent.clientY - startY);
if (deltaX > 3 || deltaY > 3) {
hasMoved = true;
}
// Calculate drop index
const currentLeft = startLeft + moveEvent.clientX - startXPos;
dropIndex = calculateColumnDropIndex(col, columns, currentLeft);
// Update visual markers if they exist
if (dropMarker && dragMarker) {
if (!pseudoColumn) {
pseudoColumn = constructColumnDragPreview(editor, editor.state.selection, table);
const tableHeightPx = getTableHeightPx(table, editor);
if (pseudoColumn) {
pseudoColumn.style.height = `${tableHeightPx}px`;
}
}
const dragMarkerWidthPx = columns[col].width;
const dragMarkerLeftPx = Math.max(0, Math.min(currentLeft, tableWidthPx - dragMarkerWidthPx));
const dropMarkerLeftPx =
dropIndex <= col ? columns[dropIndex].left : columns[dropIndex].left + columns[dropIndex].width;
updateColDropMarker({
element: dropMarker,
left: dropMarkerLeftPx - Math.floor(DROP_MARKER_THICKNESS / 2) - 1,
width: DROP_MARKER_THICKNESS,
});
updateColDragMarker({
element: dragMarker,
left: dragMarkerLeftPx,
width: dragMarkerWidthPx,
pseudoColumn,
});
}
};
try {
dragListeners.mouseup = handleFinish;
dragListeners.mousemove = handleMove;
window.addEventListener("mouseup", handleFinish);
window.addEventListener("mousemove", handleMove);
} catch (error) {
console.error("Error in ColumnDragHandle:", error);
handleFinish();
}
};
// Attach mousedown listener
button.addEventListener("mousedown", handleMouseDown);
// Cleanup function
const destroy = () => {
// Close dropdown if open
if (isDropdownOpen) {
closeDropdown();
}
// Remove drag listeners
if (dragListeners.mouseup) {
window.removeEventListener("mouseup", dragListeners.mouseup);
}
if (dragListeners.mousemove) {
window.removeEventListener("mousemove", dragListeners.mousemove);
}
// Remove mousedown listener
button.removeEventListener("mousedown", handleMouseDown);
// Remove DOM elements
container.remove();
};
return {
element: container,
destroy,
};
}
@@ -0,0 +1,258 @@
import {
shift,
flip,
useDismiss,
useFloating,
useInteractions,
autoUpdate,
useClick,
useRole,
FloatingOverlay,
FloatingPortal,
} from "@floating-ui/react";
import type { Editor } from "@tiptap/core";
import { Ellipsis } from "lucide-react";
import { useCallback, useEffect, useRef, useState } from "react";
// plane imports
import { cn } from "@plane/utils";
// constants
import { CORE_EXTENSIONS } from "@/constants/extension";
// extensions
import {
findTable,
getTableHeightPx,
getTableWidthPx,
isCellSelection,
selectColumn,
} from "@/extensions/table/table/utilities/helpers";
// local imports
import { moveSelectedColumns } from "../actions";
import {
DROP_MARKER_THICKNESS,
getColDragMarker,
getDropMarker,
hideDragMarker,
hideDropMarker,
updateColDragMarker,
updateColDropMarker,
} from "../marker-utils";
import { updateCellContentVisibility } from "../utils";
import { ColumnOptionsDropdown } from "./dropdown";
import { calculateColumnDropIndex, constructColumnDragPreview, getTableColumnNodesInfo } from "./utils";
export type ColumnDragHandleProps = {
col: number;
editor: Editor;
};
export function ColumnDragHandle(props: ColumnDragHandleProps) {
const { col, editor } = props;
// states
const [isDropdownOpen, setIsDropdownOpen] = useState(false);
// Track active event listeners for cleanup
const activeListenersRef = useRef<{
mouseup?: (e: MouseEvent) => void;
mousemove?: (e: MouseEvent) => void;
}>({});
// Cleanup window event listeners on unmount
useEffect(() => {
const listenersRef = activeListenersRef.current;
return () => {
// Remove any lingering window event listeners when component unmounts
if (listenersRef.mouseup) {
window.removeEventListener("mouseup", listenersRef.mouseup);
}
if (listenersRef.mousemove) {
window.removeEventListener("mousemove", listenersRef.mousemove);
}
};
}, []);
// floating ui
const { refs, floatingStyles, context } = useFloating({
placement: "bottom-start",
middleware: [
flip({
fallbackPlacements: ["top-start", "bottom-start", "top-end", "bottom-end"],
}),
shift({
padding: 8,
}),
],
open: isDropdownOpen,
onOpenChange: (open) => {
setIsDropdownOpen(open);
if (open) {
editor.commands.addActiveDropbarExtension(CORE_EXTENSIONS.TABLE);
} else {
setTimeout(() => {
editor.commands.removeActiveDropbarExtension(CORE_EXTENSIONS.TABLE);
}, 0);
}
},
whileElementsMounted: autoUpdate,
});
const click = useClick(context);
const dismiss = useDismiss(context);
const role = useRole(context);
const { getReferenceProps, getFloatingProps } = useInteractions([dismiss, click, role]);
useEffect(() => {
if (!isDropdownOpen) return;
const handleKeyDown = (event: KeyboardEvent) => {
context.onOpenChange(false);
event.preventDefault();
event.stopPropagation();
};
document.addEventListener("keydown", handleKeyDown);
return () => document.removeEventListener("keydown", handleKeyDown);
}, [isDropdownOpen, context]);
const handleMouseDown = useCallback(
(e: React.MouseEvent<HTMLButtonElement>) => {
e.stopPropagation();
e.preventDefault();
// Prevent multiple simultaneous drag operations
// If there are already listeners attached, remove them first
if (activeListenersRef.current.mouseup) {
window.removeEventListener("mouseup", activeListenersRef.current.mouseup);
}
if (activeListenersRef.current.mousemove) {
window.removeEventListener("mousemove", activeListenersRef.current.mousemove);
}
activeListenersRef.current.mouseup = undefined;
activeListenersRef.current.mousemove = undefined;
const table = findTable(editor.state.selection);
if (!table) return;
editor.view.dispatch(selectColumn(table, col, editor.state.tr));
// drag column
const tableWidthPx = getTableWidthPx(table, editor);
const columns = getTableColumnNodesInfo(table, editor);
let dropIndex = col;
const startLeft = columns[col].left ?? 0;
const startX = e.clientX;
const tableElement = editor.view.nodeDOM(table.pos);
const dropMarker = tableElement instanceof HTMLElement ? getDropMarker(tableElement) : null;
const dragMarker = tableElement instanceof HTMLElement ? getColDragMarker(tableElement) : null;
const handleFinish = () => {
if (!dropMarker || !dragMarker) return;
hideDropMarker(dropMarker);
hideDragMarker(dragMarker);
if (isCellSelection(editor.state.selection)) {
updateCellContentVisibility(editor, false);
}
if (col !== dropIndex) {
let tr = editor.state.tr;
const selection = editor.state.selection;
if (isCellSelection(selection)) {
const table = findTable(selection);
if (table) {
tr = moveSelectedColumns(editor, table, selection, dropIndex, tr);
}
}
editor.view.dispatch(tr);
}
window.removeEventListener("mouseup", handleFinish);
window.removeEventListener("mousemove", handleMove);
// Clear the ref
activeListenersRef.current.mouseup = undefined;
activeListenersRef.current.mousemove = undefined;
};
let pseudoColumn: HTMLElement | undefined;
const handleMove = (moveEvent: MouseEvent) => {
if (!dropMarker || !dragMarker) return;
const currentLeft = startLeft + moveEvent.clientX - startX;
dropIndex = calculateColumnDropIndex(col, columns, currentLeft);
if (!pseudoColumn) {
pseudoColumn = constructColumnDragPreview(editor, editor.state.selection, table);
const tableHeightPx = getTableHeightPx(table, editor);
if (pseudoColumn) {
pseudoColumn.style.height = `${tableHeightPx}px`;
}
}
const dragMarkerWidthPx = columns[col].width;
const dragMarkerLeftPx = Math.max(0, Math.min(currentLeft, tableWidthPx - dragMarkerWidthPx));
const dropMarkerLeftPx =
dropIndex <= col ? columns[dropIndex].left : columns[dropIndex].left + columns[dropIndex].width;
updateColDropMarker({
element: dropMarker,
left: dropMarkerLeftPx - Math.floor(DROP_MARKER_THICKNESS / 2) - 1,
width: DROP_MARKER_THICKNESS,
});
updateColDragMarker({
element: dragMarker,
left: dragMarkerLeftPx,
width: dragMarkerWidthPx,
pseudoColumn,
});
};
try {
// Store references for cleanup
activeListenersRef.current.mouseup = handleFinish;
activeListenersRef.current.mousemove = handleMove;
window.addEventListener("mouseup", handleFinish);
window.addEventListener("mousemove", handleMove);
} catch (error) {
console.error("Error in ColumnDragHandle:", error);
handleFinish();
}
},
[col, editor]
);
return (
<>
<div className="table-col-handle-container absolute z-20 top-0 left-0 flex justify-center items-center w-full -translate-y-1/2">
<button
ref={refs.setReference}
{...getReferenceProps()}
type="button"
onMouseDown={handleMouseDown}
className={cn("px-1 bg-layer-1 border border-strong-1 rounded-sm outline-none transition-all duration-200", {
"!opacity-100 bg-accent-primary border-accent-strong": isDropdownOpen,
"hover:bg-layer-1-hover": !isDropdownOpen,
})}
>
<Ellipsis className="size-4 text-primary" />
</button>
</div>
{isDropdownOpen && (
<FloatingPortal>
{/* Backdrop */}
<FloatingOverlay
style={{
zIndex: 99,
}}
lockScroll
/>
<div
className="max-h-[90vh] w-[12rem] overflow-y-auto rounded-md border-[0.5px] border-strong bg-surface-1 px-2 py-2.5 shadow-raised-200"
ref={refs.setFloating}
{...getFloatingProps()}
style={{
...floatingStyles,
zIndex: 100,
}}
>
<ColumnOptionsDropdown editor={editor} onClose={() => context.onOpenChange(false)} />
</div>
</FloatingPortal>
)}
</>
);
}
@@ -2,6 +2,7 @@ import type { Editor } from "@tiptap/core";
import { Plugin, PluginKey } from "@tiptap/pm/state";
import { TableMap } from "@tiptap/pm/tables";
import { Decoration, DecorationSet } from "@tiptap/pm/view";
import { ReactRenderer } from "@tiptap/react";
// extensions
import {
findTable,
@@ -9,20 +10,16 @@ import {
haveTableRelatedChanges,
} from "@/extensions/table/table/utilities/helpers";
// local imports
import { createColumnDragHandle } from "./drag-handle";
type DragHandleInstance = {
element: HTMLElement;
destroy: () => void;
};
import type { ColumnDragHandleProps } from "./drag-handle";
import { ColumnDragHandle } from "./drag-handle";
type TableColumnDragHandlePluginState = {
decorations?: DecorationSet;
// track table structure to detect changes
tableWidth?: number;
tableNodePos?: number;
// track drag handle instances for cleanup
dragHandles?: DragHandleInstance[];
// track renderers for cleanup
renderers?: ReactRenderer[];
};
const TABLE_COLUMN_DRAG_HANDLE_PLUGIN_KEY = new PluginKey("tableColumnHandlerDecorationPlugin");
@@ -63,40 +60,43 @@ export const TableColumnDragHandlePlugin = (editor: Editor): Plugin<TableColumnD
decorations: mapped,
tableWidth: tableMap.width,
tableNodePos: table.pos,
dragHandles: prev.dragHandles,
renderers: prev.renderers,
};
}
// Clean up old drag handles before creating new ones
prev.dragHandles?.forEach((handle) => {
// Clean up old renderers before creating new ones
prev.renderers?.forEach((renderer) => {
try {
handle.destroy();
renderer.destroy();
} catch (error) {
console.error("Error destroying drag handle:", error);
console.error("Error destroying renderer:", error);
}
});
// recreate all decorations
const decorations: Decoration[] = [];
const dragHandles: DragHandleInstance[] = [];
const renderers: ReactRenderer[] = [];
for (let col = 0; col < tableMap.width; col++) {
const pos = getTableCellWidgetDecorationPos(table, tableMap, col);
const dragHandle = createColumnDragHandle({
const dragHandleComponent = new ReactRenderer(ColumnDragHandle, {
props: {
col,
editor,
} satisfies ColumnDragHandleProps,
editor,
col,
});
dragHandles.push(dragHandle);
decorations.push(Decoration.widget(pos, () => dragHandle.element));
renderers.push(dragHandleComponent);
decorations.push(Decoration.widget(pos, () => dragHandleComponent.element));
}
return {
decorations: DecorationSet.create(newState.doc, decorations),
tableWidth: tableMap.width,
tableNodePos: table.pos,
dragHandles,
renderers,
};
},
},
@@ -107,15 +107,15 @@ export const TableColumnDragHandlePlugin = (editor: Editor): Plugin<TableColumnD
},
},
destroy() {
// Clean up all drag handles when plugin is destroyed
// Clean up all renderers when plugin is destroyed
const state =
editor.state &&
(TABLE_COLUMN_DRAG_HANDLE_PLUGIN_KEY.getState(editor.state) as TableColumnDragHandlePluginState | undefined);
state?.dragHandles?.forEach((handle: DragHandleInstance) => {
state?.renderers?.forEach((renderer: ReactRenderer) => {
try {
handle.destroy();
renderer.destroy();
} catch (error) {
console.error("Error destroying drag handle:", error);
console.error("Error destroying renderer:", error);
}
});
},
@@ -1,179 +0,0 @@
import type { Editor } from "@tiptap/core";
// constants
import { COLORS_LIST } from "@/constants/common";
import { CORE_EXTENSIONS } from "@/constants/extension";
// icons
import { DRAG_HANDLE_ICONS, createSvgElement } from "./icons";
export type DropdownOption = {
key: string;
label: string;
icon: string;
showRightIcon: boolean;
};
/**
* Creates the color selector section for the dropdown
*/
export function createColorSelector(): HTMLElement {
const container = document.createElement("div");
container.className = "mb-2";
// Create toggle button
const toggleBtn = document.createElement("button");
toggleBtn.type = "button";
toggleBtn.className =
"flex items-center justify-between gap-2 w-full rounded-sm px-1 py-1.5 text-11 text-left truncate text-secondary hover:bg-layer-transparent-hover";
toggleBtn.setAttribute("data-action", "toggle-color-selector");
// Left span with icon and text
const leftSpan = document.createElement("span");
leftSpan.className = "flex items-center gap-2";
const paletteIcon = createSvgElement(DRAG_HANDLE_ICONS.palette, "shrink-0 size-3");
leftSpan.appendChild(paletteIcon);
leftSpan.appendChild(document.createTextNode("Color"));
// Right chevron icon
const chevronIcon = createSvgElement(
DRAG_HANDLE_ICONS.chevronRight,
"shrink-0 size-3 transition-transform duration-200 color-chevron"
);
toggleBtn.appendChild(leftSpan);
toggleBtn.appendChild(chevronIcon);
container.appendChild(toggleBtn);
// Create color panel
const colorPanel = document.createElement("div");
colorPanel.className = "p-1 space-y-2 mb-1.5 hidden color-panel";
const innerDiv = document.createElement("div");
innerDiv.className = "space-y-1";
const title = document.createElement("p");
title.className = "text-11 text-tertiary font-semibold";
title.textContent = "Background colors";
innerDiv.appendChild(title);
const colorsContainer = document.createElement("div");
colorsContainer.className = "flex items-center flex-wrap gap-2";
// Create color buttons
COLORS_LIST.forEach((color) => {
const colorBtn = document.createElement("button");
colorBtn.type = "button";
colorBtn.className =
"flex-shrink-0 size-6 rounded-sm border-[0.5px] border-strong-1 hover:opacity-60 transition-opacity";
colorBtn.style.backgroundColor = color.backgroundColor;
colorBtn.setAttribute("data-action", "set-bg-color");
colorBtn.setAttribute("data-color", color.backgroundColor);
colorsContainer.appendChild(colorBtn);
});
// Create clear color button
const clearBtn = document.createElement("button");
clearBtn.type = "button";
clearBtn.className =
"flex-shrink-0 size-6 grid place-items-center rounded-sm text-tertiary border-[0.5px] border-strong-1 hover:bg-layer-transparent-hover transition-colors";
clearBtn.setAttribute("data-action", "clear-bg-color");
const banIcon = createSvgElement(DRAG_HANDLE_ICONS.ban, "size-4");
clearBtn.appendChild(banIcon);
colorsContainer.appendChild(clearBtn);
innerDiv.appendChild(colorsContainer);
colorPanel.appendChild(innerDiv);
container.appendChild(colorPanel);
return container;
}
/**
* Creates the dropdown content with options and color selector
*/
export function createDropdownContent(options: DropdownOption[]): DocumentFragment {
const fragment = document.createDocumentFragment();
// Create option buttons
options.forEach((option, index) => {
const btn = document.createElement("button");
btn.type = "button";
btn.className = `flex items-center ${option.showRightIcon ? "justify-between" : ""} gap-2 w-full rounded-sm px-1 py-1.5 text-11 text-left truncate text-secondary hover:bg-layer-transparent-hover`;
btn.setAttribute("data-action", option.key);
// Create icon element
const iconElement = createSvgElement(option.icon, "shrink-0 size-3");
// Create label
const labelDiv = document.createElement("div");
labelDiv.className = "flex-grow truncate";
labelDiv.textContent = option.label;
// Append in correct order
if (option.showRightIcon) {
btn.appendChild(labelDiv);
btn.appendChild(iconElement);
} else {
btn.appendChild(iconElement);
btn.appendChild(labelDiv);
}
fragment.appendChild(btn);
// Add divider after first option (header toggle)
if (index === 0) {
const hr = document.createElement("hr");
hr.className = "my-2 border-subtle";
fragment.appendChild(hr);
// Add color selector after divider
const colorSection = createColorSelector();
fragment.appendChild(colorSection);
}
});
return fragment;
}
/**
* Handles dropdown action events
*/
export function handleDropdownAction(
action: string,
editor: Editor,
onClose: () => void,
colorPanel?: Element | null,
colorChevron?: Element | null
): void {
switch (action) {
case "toggle-color-selector":
if (colorPanel && colorChevron) {
const isHidden = colorPanel.classList.contains("hidden");
if (isHidden) {
colorPanel.classList.remove("hidden");
colorChevron.classList.add("rotate-90");
} else {
colorPanel.classList.add("hidden");
colorChevron.classList.remove("rotate-90");
}
}
break;
case "set-bg-color": {
// Color is handled by the button's data-color attribute
// This case is handled in the event listener
break;
}
case "clear-bg-color":
editor
.chain()
.focus()
.updateAttributes(CORE_EXTENSIONS.TABLE_CELL, {
background: null,
})
.run();
onClose();
break;
default:
// Other actions are handled by specific implementations
break;
}
}
@@ -1,49 +0,0 @@
/**
* SVG icon paths for table drag handle dropdowns
*/
export const DRAG_HANDLE_ICONS = {
// Toggle icons
toggleRight: '<rect width="20" height="12" x="2" y="6" rx="6" ry="6"/><circle cx="16" cy="12" r="2"/>',
// Arrow icons
arrowUp: '<path d="m5 12 7-7 7 7"/><path d="M12 19V5"/>',
arrowDown: '<path d="M12 5v14"/><path d="m19 12-7 7-7-7"/>',
arrowLeft: '<path d="m12 19-7-7 7-7"/><path d="M19 12H5"/>',
arrowRight: '<path d="M5 12h14"/><path d="m12 5 7 7-7 7"/>',
// Action icons
duplicate:
'<rect width="14" height="14" x="8" y="8" rx="2" ry="2"/><path d="M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2"/>',
close: '<path d="M18 6 6 18"/><path d="m6 6 12 12"/>',
trash:
'<path d="M3 6h18"/><path d="M19 6v14c0 1-1 2-2 2H7c-1 0-2-1-2-2V6"/><path d="M8 6V4c0-1 1-2 2-2h4c1 0 2 1 2 2v2"/>',
// Color icons
palette:
'<circle cx="13.5" cy="6.5" r=".5" fill="currentColor"/><circle cx="17.5" cy="10.5" r=".5" fill="currentColor"/><circle cx="8.5" cy="7.5" r=".5" fill="currentColor"/><circle cx="6.5" cy="12.5" r=".5" fill="currentColor"/><path d="M12 2C6.5 2 2 6.5 2 12s4.5 10 10 10c.926 0 1.648-.746 1.648-1.688 0-.437-.18-.835-.437-1.125-.29-.289-.438-.652-.438-1.125a1.64 1.64 0 0 1 1.668-1.668h1.996c3.051 0 5.555-2.503 5.555-5.554C21.965 6.012 17.461 2 12 2z"/>',
chevronRight: '<path d="m9 18 6-6-6-6"/>',
ban: '<circle cx="12" cy="12" r="10"/><path d="m4.9 4.9 14.2 14.2"/>',
// Ellipsis (drag handle button)
ellipsis: '<circle cx="12" cy="12" r="1"/><circle cx="12" cy="5" r="1"/><circle cx="12" cy="19" r="1"/>',
} as const;
/**
* Creates an SVG element (DOM element) with the given icon path
*/
export function createSvgElement(iconPath: string, className = "size-3"): SVGSVGElement {
const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
svg.setAttribute("class", className);
svg.setAttribute("xmlns", "http://www.w3.org/2000/svg");
svg.setAttribute("width", "24");
svg.setAttribute("height", "24");
svg.setAttribute("viewBox", "0 0 24 24");
svg.setAttribute("fill", "none");
svg.setAttribute("stroke", "currentColor");
svg.setAttribute("stroke-width", "2");
svg.setAttribute("stroke-linecap", "round");
svg.setAttribute("stroke-linejoin", "round");
svg.innerHTML = iconPath;
return svg;
}
@@ -1,458 +0,0 @@
import { computePosition, flip, shift, autoUpdate } from "@floating-ui/dom";
import type { Editor } from "@tiptap/core";
import { TableMap } from "@tiptap/pm/tables";
// constants
import { CORE_EXTENSIONS } from "@/constants/extension";
// icons
import { DRAG_HANDLE_ICONS, createSvgElement } from "../icons";
// dropdown
import { createDropdownContent, handleDropdownAction } from "../dropdown-content";
import type { DropdownOption } from "../dropdown-content";
// extensions
import {
findTable,
getTableHeightPx,
getTableWidthPx,
isCellSelection,
selectRow,
getSelectedRows,
} from "@/extensions/table/table/utilities/helpers";
// local imports
import { moveSelectedRows, duplicateRows } from "../actions";
import {
DROP_MARKER_THICKNESS,
getDropMarker,
getRowDragMarker,
hideDragMarker,
hideDropMarker,
updateRowDragMarker,
updateRowDropMarker,
} from "../marker-utils";
import { updateCellContentVisibility } from "../utils";
import { calculateRowDropIndex, constructRowDragPreview, getTableRowNodesInfo } from "./utils";
export type RowDragHandleConfig = {
editor: Editor;
row: number;
};
/**
* Creates a vanilla JS row drag handle element with dropdown functionality
*/
export function createRowDragHandle(config: RowDragHandleConfig): {
element: HTMLElement;
destroy: () => void;
} {
const { editor, row } = config;
// Create container
const container = document.createElement("div");
container.className =
"table-row-handle-container absolute z-20 top-0 left-0 flex justify-center items-center h-full -translate-x-1/2";
// Create button
const button = document.createElement("button");
button.type = "button";
button.className = "default-state";
// Create icon (Ellipsis lucide icon as SVG)
const icon = createSvgElement(DRAG_HANDLE_ICONS.ellipsis, "size-4 text-primary");
button.appendChild(icon);
container.appendChild(button);
// State for dropdown
let isDropdownOpen = false;
let dropdownElement: HTMLElement | null = null;
let backdropElement: HTMLElement | null = null;
let cleanupFloating: (() => void) | null = null;
let backdropClickHandler: (() => void) | null = null;
// Track drag event listeners for cleanup
let dragListeners: {
mouseup?: (e: MouseEvent) => void;
mousemove?: (e: MouseEvent) => void;
} = {};
// Dropdown toggle function
const toggleDropdown = () => {
if (isDropdownOpen) {
closeDropdown();
} else {
openDropdown();
}
};
const closeDropdown = () => {
if (!isDropdownOpen) return;
isDropdownOpen = false;
// Reset button to default state
button.className = "default-state";
// Remove dropdown and backdrop
if (dropdownElement) {
dropdownElement.remove();
dropdownElement = null;
}
if (backdropElement) {
// Remove backdrop listener before removing element
if (backdropClickHandler) {
backdropElement.removeEventListener("click", backdropClickHandler);
backdropClickHandler = null;
}
backdropElement.remove();
backdropElement = null;
}
// Cleanup floating UI (this also removes keydown listener)
if (cleanupFloating) {
cleanupFloating();
cleanupFloating = null;
}
// Remove active dropdown extension
setTimeout(() => {
editor.commands.removeActiveDropbarExtension(CORE_EXTENSIONS.TABLE);
}, 0);
};
const openDropdown = () => {
if (isDropdownOpen) return;
isDropdownOpen = true;
// Update button to open state
button.className = "open-state";
// Add active dropdown extension
editor.commands.addActiveDropbarExtension(CORE_EXTENSIONS.TABLE);
// Create backdrop
backdropElement = document.createElement("div");
backdropElement.style.position = "fixed";
backdropElement.style.inset = "0";
backdropElement.style.zIndex = "99";
backdropClickHandler = closeDropdown;
backdropElement.addEventListener("click", backdropClickHandler);
document.body.appendChild(backdropElement);
// Create dropdown
dropdownElement = document.createElement("div");
dropdownElement.className =
"max-h-[90vh] w-[12rem] overflow-y-auto rounded-md border-[0.5px] border-strong bg-surface-1 px-2 py-2.5 shadow-raised-200";
dropdownElement.style.position = "fixed";
dropdownElement.style.zIndex = "100";
// Create and append dropdown content
const content = createDropdownContent(getDropdownOptions());
dropdownElement.appendChild(content);
document.body.appendChild(dropdownElement);
// Attach dropdown event listeners
attachDropdownEventListeners(dropdownElement);
// Setup floating UI positioning
cleanupFloating = autoUpdate(button, dropdownElement, () => {
void computePosition(button, dropdownElement!, {
placement: "bottom-start",
middleware: [
flip({
fallbackPlacements: ["top-start", "bottom-start", "top-end", "bottom-end"],
}),
shift({
padding: 8,
}),
],
}).then(({ x, y }) => {
if (dropdownElement) {
dropdownElement.style.left = `${x}px`;
dropdownElement.style.top = `${y}px`;
}
return;
});
});
// Handle keyboard events
const handleKeyDown = (event: KeyboardEvent) => {
closeDropdown();
event.preventDefault();
event.stopPropagation();
};
document.addEventListener("keydown", handleKeyDown);
// Store cleanup for this specific dropdown instance
const originalCleanup = cleanupFloating;
cleanupFloating = () => {
document.removeEventListener("keydown", handleKeyDown);
if (originalCleanup) originalCleanup();
};
};
const getDropdownOptions = (): DropdownOption[] => [
{
key: "toggle-header",
label: "Header row",
icon: DRAG_HANDLE_ICONS.toggleRight,
showRightIcon: true,
},
{
key: "insert-above",
label: "Insert above",
icon: DRAG_HANDLE_ICONS.arrowUp,
showRightIcon: false,
},
{
key: "insert-below",
label: "Insert below",
icon: DRAG_HANDLE_ICONS.arrowDown,
showRightIcon: false,
},
{
key: "duplicate",
label: "Duplicate",
icon: DRAG_HANDLE_ICONS.duplicate,
showRightIcon: false,
},
{
key: "clear-contents",
label: "Clear contents",
icon: DRAG_HANDLE_ICONS.close,
showRightIcon: false,
},
{
key: "delete",
label: "Delete",
icon: DRAG_HANDLE_ICONS.trash,
showRightIcon: false,
},
];
const attachDropdownEventListeners = (dropdown: HTMLElement) => {
const buttons = dropdown.querySelectorAll("button[data-action]");
const colorPanel = dropdown.querySelector(".color-panel");
const colorChevron = dropdown.querySelector(".color-chevron");
buttons.forEach((btn) => {
const action = btn.getAttribute("data-action");
if (!action) return;
btn.addEventListener("click", (e) => {
e.preventDefault();
e.stopPropagation();
// Handle common actions
handleDropdownAction(action, editor, closeDropdown, colorPanel, colorChevron);
// Handle row-specific actions
switch (action) {
case "toggle-header":
editor.chain().focus().toggleHeaderRow().run();
closeDropdown();
break;
case "set-bg-color": {
const color = btn.getAttribute("data-color");
if (color) {
editor
.chain()
.focus()
.updateAttributes(CORE_EXTENSIONS.TABLE_CELL, {
background: color,
})
.run();
}
closeDropdown();
break;
}
case "insert-above":
editor.chain().focus().addRowBefore().run();
closeDropdown();
break;
case "insert-below":
editor.chain().focus().addRowAfter().run();
closeDropdown();
break;
case "duplicate": {
const table = findTable(editor.state.selection);
if (table) {
const tableMap = TableMap.get(table.node);
let tr = editor.state.tr;
const selectedRows = getSelectedRows(editor.state.selection, tableMap);
tr = duplicateRows(table, selectedRows, tr);
editor.view.dispatch(tr);
}
closeDropdown();
break;
}
case "clear-contents":
editor.chain().focus().clearSelectedCells().run();
closeDropdown();
break;
case "delete":
editor.chain().focus().deleteRow().run();
closeDropdown();
break;
}
});
});
};
// Handle mousedown for dragging
const handleMouseDown = (e: MouseEvent) => {
// Prevent dropdown from opening during drag
if (e.button !== 0) return; // Only left click
e.stopPropagation();
e.preventDefault();
// Check if this is a click (will be determined by mouseup without much movement)
const startX = e.clientX;
const startY = e.clientY;
let hasMoved = false;
// Clean up any existing drag listeners
if (dragListeners.mouseup) {
window.removeEventListener("mouseup", dragListeners.mouseup);
}
if (dragListeners.mousemove) {
window.removeEventListener("mousemove", dragListeners.mousemove);
}
dragListeners = {};
const table = findTable(editor.state.selection);
if (!table) return;
editor.view.dispatch(selectRow(table, row, editor.state.tr));
// Drag row logic
const tableHeightPx = getTableHeightPx(table, editor);
const rows = getTableRowNodesInfo(table, editor);
let dropIndex = row;
const startTop = rows[row].top ?? 0;
const startYPos = e.clientY;
const tableElement = editor.view.nodeDOM(table.pos);
const dropMarker = tableElement instanceof HTMLElement ? getDropMarker(tableElement) : null;
const dragMarker = tableElement instanceof HTMLElement ? getRowDragMarker(tableElement) : null;
const handleFinish = (): void => {
// Clean up markers if they exist
if (dropMarker && dragMarker) {
hideDropMarker(dropMarker);
hideDragMarker(dragMarker);
}
if (isCellSelection(editor.state.selection)) {
updateCellContentVisibility(editor, false);
}
// Perform drag operation if user moved
if (row !== dropIndex && hasMoved) {
let tr = editor.state.tr;
const selection = editor.state.selection;
if (isCellSelection(selection)) {
const table = findTable(selection);
if (table) {
tr = moveSelectedRows(editor, table, selection, dropIndex, tr);
}
}
editor.view.dispatch(tr);
}
window.removeEventListener("mouseup", handleFinish);
window.removeEventListener("mousemove", handleMove);
dragListeners.mouseup = undefined;
dragListeners.mousemove = undefined;
// If it was just a click (no movement), toggle dropdown
if (!hasMoved) {
toggleDropdown();
}
};
let pseudoRow: HTMLElement | undefined;
const handleMove = (moveEvent: MouseEvent): void => {
// Mark that we've moved
const deltaX = Math.abs(moveEvent.clientX - startX);
const deltaY = Math.abs(moveEvent.clientY - startY);
if (deltaX > 3 || deltaY > 3) {
hasMoved = true;
}
// Calculate drop index
const cursorTop = startTop + moveEvent.clientY - startYPos;
dropIndex = calculateRowDropIndex(row, rows, cursorTop);
// Update visual markers if they exist
if (dropMarker && dragMarker) {
if (!pseudoRow) {
pseudoRow = constructRowDragPreview(editor, editor.state.selection, table);
const tableWidthPx = getTableWidthPx(table, editor);
if (pseudoRow) {
pseudoRow.style.width = `${tableWidthPx}px`;
}
}
const dragMarkerHeightPx = rows[row].height;
const dragMarkerTopPx = Math.max(0, Math.min(cursorTop, tableHeightPx - dragMarkerHeightPx));
const dropMarkerTopPx = dropIndex <= row ? rows[dropIndex].top : rows[dropIndex].top + rows[dropIndex].height;
updateRowDropMarker({
element: dropMarker,
top: dropMarkerTopPx - DROP_MARKER_THICKNESS / 2,
height: DROP_MARKER_THICKNESS,
});
updateRowDragMarker({
element: dragMarker,
top: dragMarkerTopPx,
height: dragMarkerHeightPx,
pseudoRow,
});
}
};
try {
dragListeners.mouseup = handleFinish;
dragListeners.mousemove = handleMove;
window.addEventListener("mouseup", handleFinish);
window.addEventListener("mousemove", handleMove);
} catch (error) {
console.error("Error in RowDragHandle:", error);
handleFinish();
}
};
// Attach mousedown listener
button.addEventListener("mousedown", handleMouseDown);
// Cleanup function
const destroy = () => {
// Close dropdown if open
if (isDropdownOpen) {
closeDropdown();
}
// Remove drag listeners
if (dragListeners.mouseup) {
window.removeEventListener("mouseup", dragListeners.mouseup);
}
if (dragListeners.mousemove) {
window.removeEventListener("mousemove", dragListeners.mousemove);
}
// Remove mousedown listener
button.removeEventListener("mousedown", handleMouseDown);
// Remove DOM elements
container.remove();
};
return {
element: container,
destroy,
};
}
@@ -0,0 +1,257 @@
import {
autoUpdate,
flip,
FloatingOverlay,
FloatingPortal,
shift,
useClick,
useDismiss,
useFloating,
useInteractions,
useRole,
} from "@floating-ui/react";
import type { Editor } from "@tiptap/core";
import { Ellipsis } from "lucide-react";
import { useCallback, useEffect, useRef, useState } from "react";
// plane imports
import { cn } from "@plane/utils";
// constants
import { CORE_EXTENSIONS } from "@/constants/extension";
// extensions
import {
findTable,
getTableHeightPx,
getTableWidthPx,
isCellSelection,
selectRow,
} from "@/extensions/table/table/utilities/helpers";
// local imports
import { moveSelectedRows } from "../actions";
import {
DROP_MARKER_THICKNESS,
getDropMarker,
getRowDragMarker,
hideDragMarker,
hideDropMarker,
updateRowDragMarker,
updateRowDropMarker,
} from "../marker-utils";
import { updateCellContentVisibility } from "../utils";
import { RowOptionsDropdown } from "./dropdown";
import { calculateRowDropIndex, constructRowDragPreview, getTableRowNodesInfo } from "./utils";
export type RowDragHandleProps = {
editor: Editor;
row: number;
};
export function RowDragHandle(props: RowDragHandleProps) {
const { editor, row } = props;
// states
const [isDropdownOpen, setIsDropdownOpen] = useState(false);
// Track active event listeners for cleanup
const activeListenersRef = useRef<{
mouseup?: (e: MouseEvent) => void;
mousemove?: (e: MouseEvent) => void;
}>({});
// Cleanup window event listeners on unmount
useEffect(() => {
const listenersRef = activeListenersRef.current;
return () => {
// Remove any lingering window event listeners when component unmounts
if (listenersRef.mouseup) {
window.removeEventListener("mouseup", listenersRef.mouseup);
}
if (listenersRef.mousemove) {
window.removeEventListener("mousemove", listenersRef.mousemove);
}
};
}, []);
// floating ui
const { refs, floatingStyles, context } = useFloating({
placement: "bottom-start",
middleware: [
flip({
fallbackPlacements: ["top-start", "bottom-start", "top-end", "bottom-end"],
}),
shift({
padding: 8,
}),
],
open: isDropdownOpen,
onOpenChange: (open) => {
setIsDropdownOpen(open);
if (open) {
editor.commands.addActiveDropbarExtension(CORE_EXTENSIONS.TABLE);
} else {
setTimeout(() => {
editor.commands.removeActiveDropbarExtension(CORE_EXTENSIONS.TABLE);
}, 0);
}
},
whileElementsMounted: autoUpdate,
});
const click = useClick(context);
const dismiss = useDismiss(context);
const role = useRole(context);
const { getReferenceProps, getFloatingProps } = useInteractions([dismiss, click, role]);
useEffect(() => {
if (!isDropdownOpen) return;
const handleKeyDown = (event: KeyboardEvent) => {
context.onOpenChange(false);
event.preventDefault();
event.stopPropagation();
};
document.addEventListener("keydown", handleKeyDown);
return () => document.removeEventListener("keydown", handleKeyDown);
}, [isDropdownOpen, context]);
const handleMouseDown = useCallback(
(e: React.MouseEvent<HTMLButtonElement>) => {
e.stopPropagation();
e.preventDefault();
// Prevent multiple simultaneous drag operations
// If there are already listeners attached, remove them first
if (activeListenersRef.current.mouseup) {
window.removeEventListener("mouseup", activeListenersRef.current.mouseup);
}
if (activeListenersRef.current.mousemove) {
window.removeEventListener("mousemove", activeListenersRef.current.mousemove);
}
activeListenersRef.current.mouseup = undefined;
activeListenersRef.current.mousemove = undefined;
const table = findTable(editor.state.selection);
if (!table) return;
editor.view.dispatch(selectRow(table, row, editor.state.tr));
// drag row
const tableHeightPx = getTableHeightPx(table, editor);
const rows = getTableRowNodesInfo(table, editor);
let dropIndex = row;
const startTop = rows[row].top ?? 0;
const startY = e.clientY;
const tableElement = editor.view.nodeDOM(table.pos);
const dropMarker = tableElement instanceof HTMLElement ? getDropMarker(tableElement) : null;
const dragMarker = tableElement instanceof HTMLElement ? getRowDragMarker(tableElement) : null;
const handleFinish = (): void => {
if (!dropMarker || !dragMarker) return;
hideDropMarker(dropMarker);
hideDragMarker(dragMarker);
if (isCellSelection(editor.state.selection)) {
updateCellContentVisibility(editor, false);
}
if (row !== dropIndex) {
let tr = editor.state.tr;
const selection = editor.state.selection;
if (isCellSelection(selection)) {
const table = findTable(selection);
if (table) {
tr = moveSelectedRows(editor, table, selection, dropIndex, tr);
}
}
editor.view.dispatch(tr);
}
window.removeEventListener("mouseup", handleFinish);
window.removeEventListener("mousemove", handleMove);
// Clear the ref
activeListenersRef.current.mouseup = undefined;
activeListenersRef.current.mousemove = undefined;
};
let pseudoRow: HTMLElement | undefined;
const handleMove = (moveEvent: MouseEvent): void => {
if (!dropMarker || !dragMarker) return;
const cursorTop = startTop + moveEvent.clientY - startY;
dropIndex = calculateRowDropIndex(row, rows, cursorTop);
if (!pseudoRow) {
pseudoRow = constructRowDragPreview(editor, editor.state.selection, table);
const tableWidthPx = getTableWidthPx(table, editor);
if (pseudoRow) {
pseudoRow.style.width = `${tableWidthPx}px`;
}
}
const dragMarkerHeightPx = rows[row].height;
const dragMarkerTopPx = Math.max(0, Math.min(cursorTop, tableHeightPx - dragMarkerHeightPx));
const dropMarkerTopPx = dropIndex <= row ? rows[dropIndex].top : rows[dropIndex].top + rows[dropIndex].height;
updateRowDropMarker({
element: dropMarker,
top: dropMarkerTopPx - DROP_MARKER_THICKNESS / 2,
height: DROP_MARKER_THICKNESS,
});
updateRowDragMarker({
element: dragMarker,
top: dragMarkerTopPx,
height: dragMarkerHeightPx,
pseudoRow,
});
};
try {
// Store references for cleanup
activeListenersRef.current.mouseup = handleFinish;
activeListenersRef.current.mousemove = handleMove;
window.addEventListener("mouseup", handleFinish);
window.addEventListener("mousemove", handleMove);
} catch (error) {
console.error("Error in RowDragHandle:", error);
handleFinish();
}
},
[editor, row]
);
return (
<>
<div className="table-row-handle-container absolute z-20 top-0 left-0 flex justify-center items-center h-full -translate-x-1/2">
<button
ref={refs.setReference}
{...getReferenceProps()}
type="button"
onMouseDown={handleMouseDown}
className={cn("py-1 bg-layer-1 border border-strong-1 rounded-sm outline-none transition-all duration-200", {
"!opacity-100 bg-accent-primary border-accent-strong": isDropdownOpen,
"hover:bg-layer-1-hover": !isDropdownOpen,
})}
>
<Ellipsis className="size-4 text-primary rotate-90" />
</button>
</div>
{isDropdownOpen && (
<FloatingPortal>
{/* Backdrop */}
<FloatingOverlay
style={{
zIndex: 99,
}}
lockScroll
/>
<div
className="max-h-[90vh] w-[12rem] overflow-y-auto rounded-md border-[0.5px] border-strong bg-surface-1 px-2 py-2.5 shadow-raised-200"
ref={refs.setFloating}
{...getFloatingProps()}
style={{
...floatingStyles,
zIndex: 100,
}}
>
<RowOptionsDropdown editor={editor} onClose={() => context.onOpenChange(false)} />
</div>
</FloatingPortal>
)}
</>
);
}
@@ -2,6 +2,7 @@ import type { Editor } from "@tiptap/core";
import { Plugin, PluginKey } from "@tiptap/pm/state";
import { TableMap } from "@tiptap/pm/tables";
import { Decoration, DecorationSet } from "@tiptap/pm/view";
import { ReactRenderer } from "@tiptap/react";
// extensions
import {
findTable,
@@ -9,20 +10,16 @@ import {
haveTableRelatedChanges,
} from "@/extensions/table/table/utilities/helpers";
// local imports
import { createRowDragHandle } from "./drag-handle";
type DragHandleInstance = {
element: HTMLElement;
destroy: () => void;
};
import type { RowDragHandleProps } from "./drag-handle";
import { RowDragHandle } from "./drag-handle";
type TableRowDragHandlePluginState = {
decorations?: DecorationSet;
// track table structure to detect changes
tableHeight?: number;
tableNodePos?: number;
// track drag handle instances for cleanup
dragHandles?: DragHandleInstance[];
// track renderers for cleanup
renderers?: ReactRenderer[];
};
const TABLE_ROW_DRAG_HANDLE_PLUGIN_KEY = new PluginKey("tableRowDragHandlePlugin");
@@ -63,40 +60,43 @@ export const TableRowDragHandlePlugin = (editor: Editor): Plugin<TableRowDragHan
decorations: mapped,
tableHeight: tableMap.height,
tableNodePos: table.pos,
dragHandles: prev.dragHandles,
renderers: prev.renderers,
};
}
// Clean up old drag handles before creating new ones
prev.dragHandles?.forEach((handle) => {
// Clean up old renderers before creating new ones
prev.renderers?.forEach((renderer) => {
try {
handle.destroy();
renderer.destroy();
} catch (error) {
console.error("Error destroying drag handle:", error);
console.error("Error destroying renderer:", error);
}
});
// recreate all decorations
const decorations: Decoration[] = [];
const dragHandles: DragHandleInstance[] = [];
const renderers: ReactRenderer[] = [];
for (let row = 0; row < tableMap.height; row++) {
const pos = getTableCellWidgetDecorationPos(table, tableMap, row * tableMap.width);
const dragHandle = createRowDragHandle({
const dragHandleComponent = new ReactRenderer(RowDragHandle, {
props: {
editor,
row,
} satisfies RowDragHandleProps,
editor,
row,
});
dragHandles.push(dragHandle);
decorations.push(Decoration.widget(pos, () => dragHandle.element));
renderers.push(dragHandleComponent);
decorations.push(Decoration.widget(pos, () => dragHandleComponent.element));
}
return {
decorations: DecorationSet.create(newState.doc, decorations),
tableHeight: tableMap.height,
tableNodePos: table.pos,
dragHandles,
renderers,
};
},
},
@@ -107,15 +107,15 @@ export const TableRowDragHandlePlugin = (editor: Editor): Plugin<TableRowDragHan
},
},
destroy() {
// Clean up all drag handles when plugin is destroyed
// Clean up all renderers when plugin is destroyed
const state =
editor.state &&
(TABLE_ROW_DRAG_HANDLE_PLUGIN_KEY.getState(editor.state) as TableRowDragHandlePluginState | undefined);
state?.dragHandles?.forEach((handle: DragHandleInstance) => {
state?.renderers?.forEach((renderer: ReactRenderer) => {
try {
handle.destroy();
renderer.destroy();
} catch (error) {
console.error("Error destroying drag handle:", error);
console.error("Error destroying renderer:", error);
}
});
},
@@ -72,18 +72,6 @@ export const TableInsertPlugin = (editor: Editor): Plugin => {
});
};
let updateScheduled = false;
const scheduleUpdate = () => {
if (updateScheduled) return;
updateScheduled = true;
requestAnimationFrame(() => {
updateScheduled = false;
updateAllTables();
});
};
return new Plugin({
key: TABLE_INSERT_PLUGIN_KEY,
@@ -92,9 +80,9 @@ export const TableInsertPlugin = (editor: Editor): Plugin => {
return {
update(view, prevState) {
// Debounce updates using RAF to batch multiple changes
// Update when document changes
if (!prevState.doc.eq(view.state.doc)) {
scheduleUpdate();
updateAllTables();
}
},
destroy() {
@@ -219,11 +219,16 @@ export const createRowInsertButton = (editor: Editor, tableInfo: TableInfo): HTM
export const findAllTables = (editor: Editor): TableInfo[] => {
const tables: TableInfo[] = [];
const tableElements = editor.view.dom.querySelectorAll("table");
// Iterate through document to look for tables
editor.state.doc.descendants((node, pos) => {
if (node.type.spec.tableRole === "table") {
try {
tableElements.forEach((tableElement) => {
// Find the table's ProseMirror position
let tablePos = -1;
let tableNode: ProseMirrorNode | null = null;
// Walk through the document to find matching table nodes
editor.state.doc.descendants((node, pos) => {
if (node.type.spec.tableRole === "table") {
const domAtPos = editor.view.domAtPos(pos + 1);
let domTable = domAtPos.node;
@@ -236,17 +241,20 @@ export const findAllTables = (editor: Editor): TableInfo[] => {
domTable = domTable.parentNode;
}
if (domTable instanceof HTMLElement && domTable.tagName === "TABLE") {
tables.push({
tableElement: domTable,
tableNode: node,
tablePos: pos,
});
if (domTable === tableElement) {
tablePos = pos;
tableNode = node;
return false; // Stop iteration
}
} catch (error) {
// Skip tables that fail to resolve
console.error("Error finding table:", error);
}
});
if (tablePos !== -1 && tableNode) {
tables.push({
tableElement,
tableNode,
tablePos,
});
}
});
+2 -26
View File
@@ -57,31 +57,7 @@
.table-col-handle-container,
.table-row-handle-container {
& > button {
@apply opacity-0 rounded-sm outline-none;
&.default-state {
@apply bg-layer-1 hover:bg-layer-1-hover border border-strong-1;
}
&.open-state {
@apply opacity-100! bg-accent-primary border-accent-strong;
}
}
}
.table-col-handle-container {
& > button {
@apply px-1;
svg {
@apply rotate-90;
}
}
}
.table-row-handle-container {
& > button {
@apply py-1;
opacity: 0;
}
}
@@ -184,7 +160,7 @@
opacity: 0;
pointer-events: none;
outline: none;
z-index: 9;
z-index: 10;
transition: all 0.2s ease;
&:hover {
+33 -75
View File
@@ -321,9 +321,9 @@
--txt-link-primary-hover: var(--brand-900);
--txt-link-secondary: var(--neutral-900);
/* Label colors */
--label-indigo-bg: var(--extended-color-indigo-50);
--label-indigo-bg: var(--extended-color-indigo-100);
--label-indigo-bg-strong: var(--extended-color-indigo-700);
--label-indigo-hover: var(--extended-color-indigo-100);
--label-indigo-hover: var(--extended-color-indigo-300);
--label-indigo-icon: var(--extended-color-indigo-700);
--label-indigo-text: var(--extended-color-indigo-700);
--label-indigo-border: var(--extended-color-indigo-700);
@@ -331,7 +331,7 @@
--label-emerald-bg: var(--extended-color-emerald-50);
--label-emerald-bg-strong: var(--extended-color-emerald-600);
--label-emerald-hover: var(--extended-color-emerald-200);
--label-emerald-icon: var(--extended-color-emerald-800);
--label-emerald-icon: var(--extended-color-emerald-600);
--label-emerald-text: var(--extended-color-emerald-800);
--label-emerald-border: var(--extended-color-emerald-800);
--label-emerald-focus: var(--extended-color-emerald-700);
@@ -352,31 +352,10 @@
--label-yellow-bg: var(--extended-color-yellow-50);
--label-yellow-bg-strong: var(--extended-color-yellow-600);
--label-yellow-hover: var(--extended-color-yellow-100);
--label-yellow-icon: var(--extended-color-yellow-700);
--label-yellow-text: var(--extended-color-yellow-700);
--label-yellow-icon: var(--extended-color-yellow-600);
--label-yellow-text: var(--extended-color-yellow-600);
--label-yellow-border: var(--extended-color-yellow-600);
--label-yellow-focus: var(--extended-color-yellow-400);
--label-orange-bg: var(--extended-color-orange-50);
--label-orange-bg-strong: var(--extended-color-orange-600);
--label-orange-hover: var(--extended-color-orange-100);
--label-orange-icon: var(--extended-color-orange-600);
--label-orange-text: var(--extended-color-orange-600);
--label-orange-border: var(--extended-color-orange-600);
--label-orange-focus: var(--extended-color-orange-300);
--label-pink-bg: var(--extended-color-pink-50);
--label-pink-bg-strong: var(--extended-color-pink-600);
--label-pink-hover: var(--extended-color-pink-100);
--label-pink-icon: var(--extended-color-pink-600);
--label-pink-text: var(--extended-color-pink-600);
--label-pink-border: var(--extended-color-pink-600);
--label-pink-focus: var(--extended-color-pink-400);
--label-purple-bg: var(--extended-color-purple-50);
--label-purple-bg-strong: var(--extended-color-purple-500);
--label-purple-hover: var(--extended-color-purple-100);
--label-purple-icon: var(--extended-color-purple-500);
--label-purple-text: var(--extended-color-purple-600);
--label-purple-border: var(--extended-color-purple-600);
--label-purple-focus: var(--extended-color-purple-300);
/* Illustration colors */
--illustration-fill-primary: var(--neutral-white);
--illustration-fill-secondary: var(--neutral-200);
@@ -571,62 +550,41 @@
--txt-link-primary-hover: var(--brand-700);
--txt-link-secondary: var(--neutral-1100);
/* Label colors */
--label-indigo-bg: var(--extended-color-indigo-900);
--label-indigo-bg: var(--extended-color-indigo-800);
--label-indigo-bg-strong: var(--extended-color-indigo-500);
--label-indigo-hover: var(--extended-color-indigo-800);
--label-indigo-icon: var(--extended-color-indigo-400);
--label-indigo-text: var(--extended-color-indigo-400);
--label-indigo-border: var(--extended-color-indigo-400);
--label-indigo-hover: var(--extended-color-indigo-700);
--label-indigo-icon: var(--extended-color-indigo-500);
--label-indigo-text: var(--extended-color-indigo-500);
--label-indigo-border: var(--extended-color-indigo-500);
--label-indigo-focus: var(--extended-color-indigo-400);
--label-emerald-bg: var(--extended-color-emerald-950);
--label-emerald-bg: var(--extended-color-emerald-700);
--label-emerald-bg-strong: var(--extended-color-emerald-600);
--label-emerald-hover: var(--extended-color-emerald-900);
--label-emerald-icon: var(--extended-color-emerald-400);
--label-emerald-hover: var(--extended-color-emerald-800);
--label-emerald-icon: var(--extended-color-emerald-600);
--label-emerald-text: var(--extended-color-emerald-400);
--label-emerald-border: var(--extended-color-emerald-400);
--label-emerald-focus: var(--extended-color-emerald-700);
--label-grey-bg: var(--extended-color-grey-900);
--label-grey-bg: var(--extended-color-grey-800);
--label-grey-bg-strong: var(--extended-color-grey-500);
--label-grey-hover: var(--extended-color-grey-800);
--label-grey-icon: var(--extended-color-grey-400);
--label-grey-text: var(--extended-color-grey-400);
--label-grey-border: var(--extended-color-grey-400);
--label-grey-hover: var(--extended-color-grey-700);
--label-grey-icon: var(--extended-color-grey-500);
--label-grey-text: var(--extended-color-grey-500);
--label-grey-border: var(--extended-color-grey-500);
--label-grey-focus: var(--extended-color-grey-400);
--label-crimson-bg: var(--extended-color-crimson-950);
--label-crimson-bg: var(--extended-color-crimson-800);
--label-crimson-bg-strong: var(--extended-color-crimson-500);
--label-crimson-hover: var(--extended-color-crimson-900);
--label-crimson-icon: var(--extended-color-crimson-400);
--label-crimson-text: var(--extended-color-crimson-400);
--label-crimson-border: var(--extended-color-crimson-400);
--label-crimson-hover: var(--extended-color-crimson-700);
--label-crimson-icon: var(--extended-color-crimson-500);
--label-crimson-text: var(--extended-color-crimson-500);
--label-crimson-border: var(--extended-color-crimson-500);
--label-crimson-focus: var(--extended-color-crimson-400);
--label-yellow-bg: var(--extended-color-yellow-950);
--label-yellow-bg: var(--extended-color-yellow-900);
--label-yellow-bg-strong: var(--extended-color-yellow-500);
--label-yellow-hover: var(--extended-color-yellow-900);
--label-yellow-hover: var(--extended-color-yellow-800);
--label-yellow-icon: var(--extended-color-yellow-500);
--label-yellow-text: var(--extended-color-yellow-500);
--label-yellow-border: var(--extended-color-yellow-500);
--label-yellow-focus: var(--extended-color-yellow-400);
--label-orange-bg: var(--extended-color-orange-950);
--label-orange-bg-strong: var(--extended-color-orange-400);
--label-orange-hover: var(--extended-color-orange-900);
--label-orange-icon: var(--extended-color-orange-300);
--label-orange-text: var(--extended-color-orange-300);
--label-orange-border: var(--extended-color-orange-300);
--label-orange-focus: var(--extended-color-orange-300);
--label-pink-bg: var(--extended-color-pink-900);
--label-pink-bg-strong: var(--extended-color-pink-500);
--label-pink-hover: var(--extended-color-pink-800);
--label-pink-icon: var(--extended-color-pink-400);
--label-pink-text: var(--extended-color-pink-400);
--label-pink-border: var(--extended-color-pink-400);
--label-pink-focus: var(--extended-color-pink-400);
--label-purple-bg: var(--extended-color-purple-900);
--label-purple-bg-strong: var(--extended-color-purple-400);
--label-purple-hover: var(--extended-color-purple-800);
--label-purple-icon: var(--extended-color-purple-400);
--label-purple-text: var(--extended-color-purple-400);
--label-purple-border: var(--extended-color-purple-400);
--label-purple-focus: var(--extended-color-purple-300);
/* Illustration colors */
--illustration-fill-primary: var(--neutral-400);
--illustration-fill-secondary: var(--neutral-500);
@@ -1101,42 +1059,42 @@
--text-h4-bold--letter-spacing: var(--tracking-default);
--text-h4-bold--font-weight: var(--font-weight-bold);
--text-h5-regular: var(--text-18);
--text-h5-regular: var(--text-16);
--text-h5-regular--line-height: 1.2;
--text-h5-regular--letter-spacing: var(--tracking-default);
--text-h5-regular--font-weight: var(--font-weight-regular);
--text-h5-medium: var(--text-18);
--text-h5-medium: var(--text-16);
--text-h5-medium--line-height: 1.2;
--text-h5-medium--letter-spacing: var(--tracking-default);
--text-h5-medium--font-weight: var(--font-weight-medium);
--text-h5-semibold: var(--text-18);
--text-h5-semibold: var(--text-16);
--text-h5-semibold--line-height: 1.2;
--text-h5-semibold--letter-spacing: var(--tracking-default);
--text-h5-semibold--font-weight: var(--font-weight-semibold);
--text-h5-bold: var(--text-18);
--text-h5-bold: var(--text-16);
--text-h5-bold--line-height: 1.2;
--text-h5-bold--letter-spacing: var(--tracking-default);
--text-h5-bold--font-weight: var(--font-weight-bold);
--text-h6-regular: var(--text-16);
--text-h6-regular: var(--text-14);
--text-h6-regular--line-height: 1.2;
--text-h6-regular--letter-spacing: var(--tracking-default);
--text-h6-regular--font-weight: var(--font-weight-regular);
--text-h6-medium: var(--text-16);
--text-h6-medium: var(--text-14);
--text-h6-medium--line-height: 1.2;
--text-h6-medium--letter-spacing: var(--tracking-default);
--text-h6-medium--font-weight: var(--font-weight-medium);
--text-h6-semibold: var(--text-16);
--text-h6-semibold: var(--text-14);
--text-h6-semibold--line-height: 1.2;
--text-h6-semibold--letter-spacing: var(--tracking-default);
--text-h6-semibold--font-weight: var(--font-weight-semibold);
--text-h6-bold: var(--text-16);
--text-h6-bold: var(--text-14);
--text-h6-bold--line-height: 1.2;
--text-h6-bold--letter-spacing: var(--tracking-default);
--text-h6-bold--font-weight: var(--font-weight-bold);
+73 -41
View File
@@ -82,8 +82,11 @@ catalogs:
specifier: 18.3.1
version: 18.3.1
react-router:
specifier: 7.12.0
version: 7.12.0
specifier: 7.9.5
version: 7.9.5
react-router-dom:
specifier: 7.9.5
version: 7.9.5
swr:
specifier: 2.2.4
version: 2.2.4
@@ -112,7 +115,6 @@ overrides:
'@types/express': 4.17.23
typescript: 5.8.3
vite: 7.1.11
qs: 6.14.1
importers:
@@ -225,10 +227,10 @@ importers:
version: link:../../packages/utils
'@react-router/node':
specifier: 'catalog:'
version: 7.9.5(react-router@7.12.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(typescript@5.8.3)
version: 7.9.5(react-router@7.9.5(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(typescript@5.8.3)
'@sentry/react-router':
specifier: 'catalog:'
version: 10.27.0(@react-router/node@7.9.5(react-router@7.12.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(typescript@5.8.3))(react-router@7.12.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1)
version: 10.27.0(@react-router/node@7.9.5(react-router@7.9.5(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(typescript@5.8.3))(react-router@7.9.5(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1)
'@tanstack/react-virtual':
specifier: ^3.13.12
version: 3.13.12(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
@@ -267,7 +269,10 @@ importers:
version: 7.51.5(react@18.3.1)
react-router:
specifier: 'catalog:'
version: 7.12.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
version: 7.9.5(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
react-router-dom:
specifier: 'catalog:'
version: 7.9.5(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
serve:
specifier: 14.2.5
version: 14.2.5
@@ -289,7 +294,7 @@ importers:
version: link:../../packages/typescript-config
'@react-router/dev':
specifier: 'catalog:'
version: 7.9.5(@react-router/serve@7.9.5(react-router@7.12.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(typescript@5.8.3))(@types/node@22.12.0)(babel-plugin-macros@3.1.0)(jiti@2.6.1)(lightningcss@1.30.2)(react-router@7.12.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(terser@5.43.1)(typescript@5.8.3)(vite@7.1.11(@types/node@22.12.0)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.43.1)(yaml@2.8.1))(yaml@2.8.1)
version: 7.9.5(@react-router/serve@7.9.5(react-router@7.9.5(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(typescript@5.8.3))(@types/node@22.12.0)(babel-plugin-macros@3.1.0)(jiti@2.6.1)(lightningcss@1.30.2)(react-router@7.9.5(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(terser@5.43.1)(typescript@5.8.3)(vite@7.1.11(@types/node@22.12.0)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.43.1)(yaml@2.8.1))(yaml@2.8.1)
'@types/lodash-es':
specifier: 'catalog:'
version: 4.17.12
@@ -470,13 +475,13 @@ importers:
version: 2.11.8
'@react-router/node':
specifier: 'catalog:'
version: 7.9.5(react-router@7.12.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(typescript@5.8.3)
version: 7.9.5(react-router@7.9.5(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(typescript@5.8.3)
'@react-router/serve':
specifier: 'catalog:'
version: 7.9.5(react-router@7.12.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(typescript@5.8.3)
version: 7.9.5(react-router@7.9.5(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(typescript@5.8.3)
'@sentry/react-router':
specifier: 'catalog:'
version: 10.27.0(@react-router/node@7.9.5(react-router@7.12.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(typescript@5.8.3))(react-router@7.12.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1)
version: 10.27.0(@react-router/node@7.9.5(react-router@7.9.5(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(typescript@5.8.3))(react-router@7.9.5(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1)
axios:
specifier: 'catalog:'
version: 1.12.0
@@ -524,7 +529,10 @@ importers:
version: 2.3.0(@popperjs/core@2.11.8)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
react-router:
specifier: 'catalog:'
version: 7.12.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
version: 7.9.5(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
react-router-dom:
specifier: 'catalog:'
version: 7.9.5(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
swr:
specifier: 'catalog:'
version: 2.2.4(react@18.3.1)
@@ -543,7 +551,7 @@ importers:
version: link:../../packages/typescript-config
'@react-router/dev':
specifier: 'catalog:'
version: 7.9.5(@react-router/serve@7.9.5(react-router@7.12.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(typescript@5.8.3))(@types/node@22.12.0)(babel-plugin-macros@3.1.0)(jiti@2.6.1)(lightningcss@1.30.2)(react-router@7.12.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(terser@5.43.1)(typescript@5.8.3)(vite@7.1.11(@types/node@22.12.0)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.43.1)(yaml@2.8.1))(yaml@2.8.1)
version: 7.9.5(@react-router/serve@7.9.5(react-router@7.9.5(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(typescript@5.8.3))(@types/node@22.12.0)(babel-plugin-macros@3.1.0)(jiti@2.6.1)(lightningcss@1.30.2)(react-router@7.9.5(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(terser@5.43.1)(typescript@5.8.3)(vite@7.1.11(@types/node@22.12.0)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.43.1)(yaml@2.8.1))(yaml@2.8.1)
'@tailwindcss/typography':
specifier: 0.5.19
version: 0.5.19
@@ -636,10 +644,10 @@ importers:
version: 3.4.5(react@18.3.1)
'@react-router/node':
specifier: 'catalog:'
version: 7.9.5(react-router@7.12.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(typescript@5.8.3)
version: 7.9.5(react-router@7.9.5(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(typescript@5.8.3)
'@sentry/react-router':
specifier: 'catalog:'
version: 10.27.0(@react-router/node@7.9.5(react-router@7.12.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(typescript@5.8.3))(react-router@7.12.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1)
version: 10.27.0(@react-router/node@7.9.5(react-router@7.9.5(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(typescript@5.8.3))(react-router@7.9.5(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1)
'@tanstack/react-table':
specifier: ^8.21.3
version: 8.21.3(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
@@ -720,7 +728,10 @@ importers:
version: 2.3.0(@popperjs/core@2.11.8)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
react-router:
specifier: 'catalog:'
version: 7.12.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
version: 7.9.5(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
react-router-dom:
specifier: 'catalog:'
version: 7.9.5(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
recharts:
specifier: ^2.12.7
version: 2.15.4(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
@@ -751,7 +762,7 @@ importers:
version: link:../../packages/typescript-config
'@react-router/dev':
specifier: 'catalog:'
version: 7.9.5(@react-router/serve@7.9.5(react-router@7.12.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(typescript@5.8.3))(@types/node@22.12.0)(babel-plugin-macros@3.1.0)(jiti@2.6.1)(lightningcss@1.30.2)(react-router@7.12.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(terser@5.43.1)(typescript@5.8.3)(vite@7.1.11(@types/node@22.12.0)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.43.1)(yaml@2.8.1))(yaml@2.8.1)
version: 7.9.5(@react-router/serve@7.9.5(react-router@7.9.5(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(typescript@5.8.3))(@types/node@22.12.0)(babel-plugin-macros@3.1.0)(jiti@2.6.1)(lightningcss@1.30.2)(react-router@7.9.5(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(terser@5.43.1)(typescript@5.8.3)(vite@7.1.11(@types/node@22.12.0)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.43.1)(yaml@2.8.1))(yaml@2.8.1)
'@tailwindcss/typography':
specifier: 0.5.19
version: 0.5.19
@@ -7860,8 +7871,12 @@ packages:
resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==}
engines: {node: '>=6'}
qs@6.14.1:
resolution: {integrity: sha512-4EK3+xJl8Ts67nLYNwqw/dsFVnCf+qR7RgXSK9jEEm9unao3njwMDdmsdvoKBKHzxd7tCYz5e5M+SnMjdtXGQQ==}
qs@6.13.0:
resolution: {integrity: sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==}
engines: {node: '>=0.6'}
qs@6.14.0:
resolution: {integrity: sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==}
engines: {node: '>=0.6'}
quansync@0.2.11:
@@ -8005,8 +8020,15 @@ packages:
'@types/react':
optional: true
react-router@7.12.0:
resolution: {integrity: sha512-kTPDYPFzDVGIIGNLS5VJykK0HfHLY5MF3b+xj0/tTyNYL1gF1qs7u67Z9jEhQk2sQ98SUaHxlG31g1JtF7IfVw==}
react-router-dom@7.9.5:
resolution: {integrity: sha512-mkEmq/K8tKN63Ae2M7Xgz3c9l9YNbY+NHH6NNeUmLA3kDkhKXRsNb/ZpxaEunvGo2/3YXdk5EJU3Hxp3ocaBPw==}
engines: {node: '>=20.0.0'}
peerDependencies:
react: '>=18'
react-dom: '>=18'
react-router@7.9.5:
resolution: {integrity: sha512-JmxqrnBZ6E9hWmf02jzNn9Jm3UqyeimyiwzD69NjxGySG6lIz/1LVPsoTCwN7NBX2XjCEa1LIX5EMz1j2b6u6A==}
engines: {node: '>=20.0.0'}
peerDependencies:
react: '>=18'
@@ -10983,7 +11005,7 @@ snapshots:
'@react-pdf/primitives': 4.1.1
'@react-pdf/stylesheet': 6.1.0
'@react-router/dev@7.9.5(@react-router/serve@7.9.5(react-router@7.12.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(typescript@5.8.3))(@types/node@22.12.0)(babel-plugin-macros@3.1.0)(jiti@2.6.1)(lightningcss@1.30.2)(react-router@7.12.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(terser@5.43.1)(typescript@5.8.3)(vite@7.1.11(@types/node@22.12.0)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.43.1)(yaml@2.8.1))(yaml@2.8.1)':
'@react-router/dev@7.9.5(@react-router/serve@7.9.5(react-router@7.9.5(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(typescript@5.8.3))(@types/node@22.12.0)(babel-plugin-macros@3.1.0)(jiti@2.6.1)(lightningcss@1.30.2)(react-router@7.9.5(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(terser@5.43.1)(typescript@5.8.3)(vite@7.1.11(@types/node@22.12.0)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.43.1)(yaml@2.8.1))(yaml@2.8.1)':
dependencies:
'@babel/core': 7.28.4
'@babel/generator': 7.28.5
@@ -10993,7 +11015,7 @@ snapshots:
'@babel/traverse': 7.28.4
'@babel/types': 7.28.5
'@npmcli/package-json': 4.0.1
'@react-router/node': 7.9.5(react-router@7.12.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(typescript@5.8.3)
'@react-router/node': 7.9.5(react-router@7.9.5(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(typescript@5.8.3)
'@remix-run/node-fetch-server': 0.9.0
arg: 5.0.2
babel-dead-code-elimination: 1.0.10
@@ -11009,14 +11031,14 @@ snapshots:
picocolors: 1.1.1
prettier: 3.7.4
react-refresh: 0.14.2
react-router: 7.12.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
react-router: 7.9.5(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
semver: 7.7.3
tinyglobby: 0.2.15
valibot: 1.2.0(typescript@5.8.3)
vite: 7.1.11(@types/node@22.12.0)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.43.1)(yaml@2.8.1)
vite-node: 3.2.4(@types/node@22.12.0)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.43.1)(yaml@2.8.1)
optionalDependencies:
'@react-router/serve': 7.9.5(react-router@7.12.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(typescript@5.8.3)
'@react-router/serve': 7.9.5(react-router@7.9.5(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(typescript@5.8.3)
typescript: 5.8.3
transitivePeerDependencies:
- '@types/node'
@@ -11034,31 +11056,31 @@ snapshots:
- tsx
- yaml
'@react-router/express@7.9.5(express@4.22.0)(react-router@7.12.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(typescript@5.8.3)':
'@react-router/express@7.9.5(express@4.22.0)(react-router@7.9.5(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(typescript@5.8.3)':
dependencies:
'@react-router/node': 7.9.5(react-router@7.12.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(typescript@5.8.3)
'@react-router/node': 7.9.5(react-router@7.9.5(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(typescript@5.8.3)
express: 4.22.0
react-router: 7.12.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
react-router: 7.9.5(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
optionalDependencies:
typescript: 5.8.3
'@react-router/node@7.9.5(react-router@7.12.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(typescript@5.8.3)':
'@react-router/node@7.9.5(react-router@7.9.5(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(typescript@5.8.3)':
dependencies:
'@mjackson/node-fetch-server': 0.2.0
react-router: 7.12.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
react-router: 7.9.5(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
optionalDependencies:
typescript: 5.8.3
'@react-router/serve@7.9.5(react-router@7.12.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(typescript@5.8.3)':
'@react-router/serve@7.9.5(react-router@7.9.5(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(typescript@5.8.3)':
dependencies:
'@mjackson/node-fetch-server': 0.2.0
'@react-router/express': 7.9.5(express@4.22.0)(react-router@7.12.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(typescript@5.8.3)
'@react-router/node': 7.9.5(react-router@7.12.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(typescript@5.8.3)
'@react-router/express': 7.9.5(express@4.22.0)(react-router@7.9.5(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(typescript@5.8.3)
'@react-router/node': 7.9.5(react-router@7.9.5(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(typescript@5.8.3)
compression: 1.8.1
express: 4.22.0
get-port: 5.1.1
morgan: 1.10.1
react-router: 7.12.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
react-router: 7.9.5(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
source-map-support: 0.5.21
transitivePeerDependencies:
- supports-color
@@ -11356,13 +11378,13 @@ snapshots:
transitivePeerDependencies:
- supports-color
'@sentry/react-router@10.27.0(@react-router/node@7.9.5(react-router@7.12.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(typescript@5.8.3))(react-router@7.12.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1)':
'@sentry/react-router@10.27.0(@react-router/node@7.9.5(react-router@7.9.5(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(typescript@5.8.3))(react-router@7.9.5(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1)':
dependencies:
'@opentelemetry/api': 1.9.0
'@opentelemetry/core': 2.2.0(@opentelemetry/api@1.9.0)
'@opentelemetry/instrumentation': 0.208.0(@opentelemetry/api@1.9.0)
'@opentelemetry/semantic-conventions': 1.37.0
'@react-router/node': 7.9.5(react-router@7.12.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(typescript@5.8.3)
'@react-router/node': 7.9.5(react-router@7.9.5(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(typescript@5.8.3)
'@sentry/browser': 10.27.0
'@sentry/cli': 2.58.2
'@sentry/core': 10.27.0
@@ -11371,7 +11393,7 @@ snapshots:
'@sentry/vite-plugin': 4.6.0
glob: 11.1.0
react: 18.3.1
react-router: 7.12.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
react-router: 7.9.5(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
transitivePeerDependencies:
- encoding
- supports-color
@@ -13158,7 +13180,7 @@ snapshots:
http-errors: 2.0.0
iconv-lite: 0.4.24
on-finished: 2.4.1
qs: 6.14.1
qs: 6.13.0
raw-body: 2.5.2
type-is: 1.6.18
unpipe: 1.0.0
@@ -14335,7 +14357,7 @@ snapshots:
parseurl: 1.3.3
path-to-regexp: 0.1.12
proxy-addr: 2.0.7
qs: 6.14.1
qs: 6.14.0
range-parser: 1.2.1
safe-buffer: 5.2.1
send: 0.19.0
@@ -16724,7 +16746,11 @@ snapshots:
punycode@2.3.1: {}
qs@6.14.1:
qs@6.13.0:
dependencies:
side-channel: 1.1.0
qs@6.14.0:
dependencies:
side-channel: 1.1.0
@@ -16919,7 +16945,13 @@ snapshots:
optionalDependencies:
'@types/react': 18.3.11
react-router@7.12.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1):
react-router-dom@7.9.5(react-dom@18.3.1(react@18.3.1))(react@18.3.1):
dependencies:
react: 18.3.1
react-dom: 18.3.1(react@18.3.1)
react-router: 7.9.5(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
react-router@7.9.5(react-dom@18.3.1(react@18.3.1))(react@18.3.1):
dependencies:
cookie: 1.0.2
react: 18.3.1
@@ -18104,7 +18136,7 @@ snapshots:
url@0.11.4:
dependencies:
punycode: 1.4.1
qs: 6.14.1
qs: 6.14.0
use-callback-ref@1.3.3(@types/react@18.3.11)(react@18.3.1):
dependencies:
+2 -1
View File
@@ -30,7 +30,8 @@ catalog:
mobx-utils: 6.0.8
mobx: 6.12.0
react-dom: 18.3.1
react-router: 7.12.0
react-router-dom: 7.9.5
react-router: 7.9.5
react: 18.3.1
swr: 2.2.4
tsdown: 0.16.0