Compare commits

...
Author SHA1 Message Date
David Nguyen 6a8fe6f1ad chore: remove planning skills (#3177) 2026-08-18 12:23:28 +10:00
Lucas Smith 779de01fe8 feat: migrate to react 19 (#3107) 2026-08-17 16:58:11 +10:00
Lucas Smith 283c6d274b fix: use documenso fork of skia-canvas for rendering (#3214)
Use our fork of `skia-canvas` for rendering which handles
encoding characters correctly with the caveat font and other
similar fonts that can group glyphs like ligatures.

This resolves issues with pdf text extraction where characters
were unable to be extracted due to lacking any data within the cmaps.
2026-08-17 15:23:19 +10:00
38 changed files with 1572 additions and 2899 deletions
@@ -1,56 +0,0 @@
---
name: create-justification
description: Create a new justification file in .agents/justifications/ with a unique three-word ID, frontmatter, and formatted title
license: MIT
compatibility: opencode
metadata:
audience: agents
workflow: decision-making
---
## What I do
I help you create new justification files in the `.agents/justifications/` directory. Each justification file gets:
- A unique three-word identifier (e.g., `swift-emerald-river`)
- Frontmatter with the current date and formatted title
- Content you provide
## How to use
Run the script with a slug and content:
```bash
npx tsx scripts/create-justification.ts "decision-name" "Justification content here"
```
Or use heredoc for multi-line content:
```bash
npx tsx scripts/create-justification.ts "decision-name" << HEREDOC
Multi-line
justification content
goes here
HEREDOC
```
## File format
Files are created as: `{three-word-id}-{slug}.md`
Example: `swift-emerald-river-decision-name.md`
The file includes frontmatter:
```markdown
---
date: 2026-01-13
title: Decision Name
---
Your content here
```
## When to use me
Use this skill when you need to document the reasoning or justification for a decision, approach, or architectural choice. The unique ID ensures no filename conflicts, and the frontmatter provides metadata for organization.
-56
View File
@@ -1,56 +0,0 @@
---
name: create-plan
description: Create a new plan file in .agents/plans/ with a unique three-word ID, frontmatter, and formatted title
license: MIT
compatibility: opencode
metadata:
audience: agents
workflow: planning
---
## What I do
I help you create new plan files in the `.agents/plans/` directory. Each plan file gets:
- A unique three-word identifier (e.g., `happy-blue-moon`)
- Frontmatter with the current date and formatted title
- Content you provide
## How to use
Run the script with a slug and content:
```bash
npx tsx scripts/create-plan.ts "feature-name" "Plan content here"
```
Or use heredoc for multi-line content:
```bash
npx tsx scripts/create-plan.ts "feature-name" << HEREDOC
Multi-line
plan content
goes here
HEREDOC
```
## File format
Files are created as: `{three-word-id}-{slug}.md`
Example: `happy-blue-moon-feature-name.md`
The file includes frontmatter:
```markdown
---
date: 2026-01-13
title: Feature Name
---
Your content here
```
## When to use me
Use this skill when you need to create a new plan document for a feature, task, or project. The unique ID ensures no filename conflicts, and the frontmatter provides metadata for organization.
-56
View File
@@ -1,56 +0,0 @@
---
name: create-scratch
description: Create a new scratch file in .agents/scratches/ with a unique three-word ID, frontmatter, and formatted title
license: MIT
compatibility: opencode
metadata:
audience: agents
workflow: exploration
---
## What I do
I help you create new scratch files in the `.agents/scratches/` directory. Each scratch file gets:
- A unique three-word identifier (e.g., `calm-teal-cloud`)
- Frontmatter with the current date and formatted title
- Content you provide
## How to use
Run the script with a slug and content:
```bash
npx tsx scripts/create-scratch.ts "note-name" "Scratch content here"
```
Or use heredoc for multi-line content:
```bash
npx tsx scripts/create-scratch.ts "note-name" << HEREDOC
Multi-line
scratch content
goes here
HEREDOC
```
## File format
Files are created as: `{three-word-id}-{slug}.md`
Example: `calm-teal-cloud-note-name.md`
The file includes frontmatter:
```markdown
---
date: 2026-01-13
title: Note Name
---
Your content here
```
## When to use me
Use this skill when you need to create a temporary note, exploration document, or scratch pad for ideas. The unique ID ensures no filename conflicts, and the frontmatter provides metadata for organization.
+1 -1
View File
@@ -1,3 +1,3 @@
legacy-peer-deps = true
prefer-dedupe = true
# min-release-age = 7
min-release-age = 7
-1
View File
@@ -10,7 +10,6 @@
"postinstall": "fumadocs-mdx"
},
"dependencies": {
"@radix-ui/react-tabs": "^1.1.13",
"fumadocs-core": "16.5.0",
"fumadocs-mdx": "14.2.6",
"fumadocs-ui": "16.5.0",
+1 -1
View File
@@ -16,7 +16,7 @@
},
"devDependencies": {
"@types/node": "^20",
"@types/react": "18.3.27",
"@types/react": "^19.2.17",
"typescript": "5.6.2"
}
}
@@ -19,7 +19,7 @@ import { msg } from '@lingui/core/macro';
import { useLingui } from '@lingui/react';
import { Trans } from '@lingui/react/macro';
import { keepPreviousData } from '@tanstack/react-query';
import { commandScore } from 'cmdk/dist/command-score';
import { defaultFilter as commandScore } from 'cmdk';
import {
ArrowLeftIcon,
CheckIcon,
@@ -146,14 +146,17 @@ export const DocumentSigningRadioField = ({ field, onSignField, onUnsignField }:
{isLoading && <DocumentSigningFieldsLoader />}
{!field.inserted && (
<RadioGroup onValueChange={(value) => handleSelectItem(value)} className="z-10 my-0.5 gap-y-1">
<RadioGroup
value={selectedOption}
onValueChange={(value) => handleSelectItem(value)}
className="z-10 my-0.5 gap-y-1"
>
{values?.map((item, index) => (
<div key={index} className="flex items-center">
<RadioGroupItem
className="h-3 w-3 shrink-0"
value={item.value}
id={`option-${field.id}-${item.id}`}
checked={item.checked}
disabled={isReadOnly}
/>
{!item.value.includes('empty-value-') && item.value && (
@@ -167,14 +170,13 @@ export const DocumentSigningRadioField = ({ field, onSignField, onUnsignField }:
)}
{field.inserted && (
<RadioGroup className="my-0.5 gap-y-1">
<RadioGroup value={field.customText ?? ''} className="my-0.5 gap-y-1">
{values?.map((item, index) => (
<div key={index} className="flex items-center">
<RadioGroupItem
className="h-3 w-3"
value={item.value}
id={`option-${field.id}-${item.id}`}
checked={item.value === field.customText}
disabled={isReadOnly}
/>
{!item.value.includes('empty-value-') && item.value && (
@@ -215,7 +215,7 @@ export default function PDFViewer({
type VirtualizedPageListProps = {
scrollParentRef: ScrollTarget;
constraintRef: React.RefObject<HTMLDivElement>;
constraintRef: React.RefObject<HTMLDivElement | null>;
pages: PageMeta[];
numPages: number;
pdf: pdfjsLib.PDFDocumentProxy;
+21
View File
@@ -31,6 +31,26 @@ function initPosthog() {
}
}
/**
* Surfaces hydration recoveries (React 19 discards the server HTML and
* re-renders on the client instead of dying) so we can track how often
* extensions/early clicks interfere with hydration in the wild.
*/
function onRecoverableError(error: unknown, errorInfo: { componentStack?: string }) {
console.error('[hydration] recovered from error', error, errorInfo.componentStack);
if (extractPostHogConfig()) {
void import('posthog-js').then(({ default: posthog }) => {
if (posthog.__loaded) {
posthog.capture('$hydration_recoverable_error', {
message: error instanceof Error ? error.message : String(error),
componentStack: errorInfo.componentStack,
});
}
});
}
}
async function main() {
const locale = detect(fromHtmlTag('lang')) || 'en';
@@ -44,6 +64,7 @@ async function main() {
<HydratedRouter />
</I18nProvider>
</StrictMode>,
{ onRecoverableError },
);
});
+4 -4
View File
@@ -57,9 +57,9 @@
"papaparse": "^5.5.3",
"posthog-js": "^1.297.2",
"posthog-node": "4.18.0",
"react": "^18",
"react": "^19.2.7",
"react-call": "^1.8.1",
"react-dom": "^18",
"react-dom": "^19.2.7",
"react-dropzone": "^14.3.8",
"react-hook-form": "^7.66.1",
"react-hotkeys-hook": "^4.6.2",
@@ -93,8 +93,8 @@
"@types/luxon": "^3.7.1",
"@types/node": "^20",
"@types/papaparse": "^5.5.0",
"@types/react": "18.3.27",
"@types/react-dom": "^18",
"@types/react": "^19.2.17",
"@types/react-dom": "^19.2.3",
"@types/ua-parser-js": "^0.7.39",
"cross-env": "^10.1.0",
"esbuild": "^0.27.0",
+1 -1
View File
@@ -121,7 +121,7 @@ export default defineConfig({
'nodemailer',
/playwright/,
'@playwright/browser-chromium',
'skia-canvas',
'@documenso/skia-canvas',
],
},
},
+1381 -2280
View File
File diff suppressed because it is too large Load Diff
+39 -4
View File
@@ -48,14 +48,17 @@
},
"devDependencies": {
"@biomejs/biome": "2.4.8",
"@types/react": "^19.2.17",
"@types/react-dom": "^19.2.3",
"@commitlint/cli": "^20.1.0",
"@commitlint/config-conventional": "^20.0.0",
"@datadog/pprof": "^5.13.5",
"@documenso/skia-canvas": "^3.0.8-documenso.3",
"@lingui/cli": "^5.6.0",
"@prisma/client": "^6.19.0",
"@trpc/client": "11.8.1",
"@trpc/react-query": "11.8.1",
"@trpc/server": "11.8.1",
"@trpc/client": "11.17.0",
"@trpc/react-query": "11.17.0",
"@trpc/server": "11.17.0",
"@ts-rest/core": "^3.52.1",
"@ts-rest/open-api": "^3.52.1",
"@ts-rest/serverless": "^3.52.1",
@@ -92,13 +95,41 @@
"@lingui/conf": "^5.6.0",
"@lingui/core": "^5.6.0",
"@prisma/extension-read-replicas": "^0.4.1",
"@radix-ui/react-accordion": "^1.2.16",
"@radix-ui/react-alert-dialog": "^1.1.19",
"@radix-ui/react-aspect-ratio": "^1.1.11",
"@radix-ui/react-avatar": "^1.2.2",
"@radix-ui/react-checkbox": "^1.3.7",
"@radix-ui/react-collapsible": "^1.1.16",
"@radix-ui/react-context-menu": "^2.3.3",
"@radix-ui/react-dialog": "^1.1.19",
"@radix-ui/react-dropdown-menu": "^2.1.20",
"@radix-ui/react-hover-card": "^1.1.19",
"@radix-ui/react-label": "^2.1.11",
"@radix-ui/react-menubar": "^1.1.20",
"@radix-ui/react-navigation-menu": "^1.2.18",
"@radix-ui/react-popover": "^1.1.19",
"@radix-ui/react-progress": "^1.1.12",
"@radix-ui/react-radio-group": "^1.4.3",
"@radix-ui/react-scroll-area": "^1.2.14",
"@radix-ui/react-select": "^2.3.3",
"@radix-ui/react-separator": "^1.1.11",
"@radix-ui/react-slider": "^1.4.3",
"@radix-ui/react-slot": "^1.3.0",
"@radix-ui/react-switch": "^1.3.3",
"@radix-ui/react-tabs": "^1.1.17",
"@radix-ui/react-toast": "^1.2.19",
"@radix-ui/react-toggle": "^1.1.14",
"@radix-ui/react-toggle-group": "^1.1.15",
"@radix-ui/react-tooltip": "^1.2.12",
"ai": "^5.0.104",
"cron-parser": "^5.5.0",
"fflate": "^0.8.3",
"luxon": "^3.7.2",
"patch-package": "^8.0.1",
"posthog-node": "4.18.0",
"react": "^18",
"react": "^19.2.7",
"react-dom": "^19.2.7",
"typescript": "5.6.2",
"@marsidev/react-turnstile": "^1.5.0",
"zod": "^3.25.76"
@@ -107,6 +138,10 @@
"lodash": "4.18.1",
"pdfjs-dist": "5.4.296",
"postcss": "^8.5.19",
"react": "$react",
"react-dom": "$react-dom",
"@types/react": "^19.2.17",
"@types/react-dom": "^19.2.3",
"typescript": "5.6.2",
"zod": "$zod",
"fumadocs-mdx": {
+1 -1
View File
@@ -19,7 +19,7 @@
"arctic": "^3.7.0",
"hono": "^4.12.14",
"luxon": "^3.7.2",
"react": "^18",
"react": "^19.2.7",
"ts-pattern": "^5.9.0",
"zod": "^3.25.76"
}
+19 -17
View File
@@ -1,17 +1,19 @@
export { Body } from '@react-email/body';
export { Button } from '@react-email/button';
export { Column } from '@react-email/column';
export { Container } from '@react-email/container';
export { Font } from '@react-email/font';
export { Head } from '@react-email/head';
export { Heading } from '@react-email/heading';
export { Hr } from '@react-email/hr';
export { Html } from '@react-email/html';
export { Img } from '@react-email/img';
export { Link } from '@react-email/link';
export { Preview } from '@react-email/preview';
export { render } from '@react-email/render';
export { Row } from '@react-email/row';
export { Section } from '@react-email/section';
export { Tailwind } from '@react-email/tailwind';
export { Text } from '@react-email/text';
export {
Body,
Button,
Column,
Container,
Font,
Head,
Heading,
Hr,
Html,
Img,
Link,
Preview,
Row,
render,
Section,
Tailwind,
Text,
} from 'react-email';
+2 -20
View File
@@ -19,27 +19,9 @@
"dependencies": {
"@documenso/nodemailer-resend": "5.0.0",
"@documenso/tailwind-config": "*",
"@react-email/body": "0.2.0",
"@react-email/button": "0.2.0",
"@react-email/code-block": "0.2.0",
"@react-email/code-inline": "0.0.5",
"@react-email/column": "0.0.13",
"@react-email/container": "0.0.15",
"@react-email/font": "0.0.9",
"@react-email/head": "0.0.12",
"@react-email/heading": "0.0.15",
"@react-email/hr": "0.0.11",
"@react-email/html": "0.0.11",
"@react-email/img": "0.0.11",
"@react-email/link": "0.0.12",
"@react-email/preview": "0.0.13",
"@react-email/render": "2.0.0",
"@react-email/row": "0.0.12",
"@react-email/section": "0.0.16",
"@react-email/tailwind": "^2.0.1",
"@react-email/text": "0.1.5",
"@react-email/render": "2.1.0",
"nodemailer": "^9.0.0",
"react-email": "^5.0.6",
"react-email": "^6.9.0",
"resend": "^6.5.2"
},
"devDependencies": {
@@ -8,7 +8,7 @@ type SaveRequest<T, R> = {
export const useAutoSave = <T, R = void>(onSave: (data: T) => Promise<R>, options: { delay?: number } = {}) => {
const { delay = 2000 } = options;
const saveTimeoutRef = useRef<NodeJS.Timeout>();
const saveTimeoutRef = useRef<NodeJS.Timeout | undefined>(undefined);
const saveQueueRef = useRef<SaveRequest<T, R>[]>([]);
const isProcessingRef = useRef(false);
@@ -15,7 +15,7 @@ import { useToast } from '@documenso/ui/primitives/use-toast';
import { useLingui } from '@lingui/react/macro';
import { EnvelopeType, Prisma, ReadStatus, SendStatus, SigningStatus } from '@prisma/client';
import type React from 'react';
import { createContext, useCallback, useContext, useMemo, useRef, useState } from 'react';
import { createContext, useCallback, useContext, useMemo, useRef, useState, useSyncExternalStore } from 'react';
import { useSearchParams } from 'react-router';
import type { TDocumentEmailSettings } from '../../types/document-email';
@@ -107,7 +107,39 @@ export const EnvelopeEditorProvider = ({
const [_searchParams, setSearchParams] = useSearchParams();
const [envelope, _setEnvelope] = useState(initialEnvelope);
/**
* The envelope is kept in a ref-backed external store instead of useState so
* that async consumers (debounced autosave callbacks, flushAutosave, resetForms)
* can synchronously read the latest value via `getEnvelope`.
*
* React subscribes to the store through useSyncExternalStore, keeping renders in
* sync without maintaining a separate copy of the state.
*/
const envelopeStoreRef = useRef(initialEnvelope);
const envelopeStoreSubscribersRef = useRef(new Set<() => void>());
const subscribeToEnvelopeStore = useCallback((onStoreChange: () => void) => {
envelopeStoreSubscribersRef.current.add(onStoreChange);
return () => {
envelopeStoreSubscribersRef.current.delete(onStoreChange);
};
}, []);
const getEnvelope = useCallback(() => envelopeStoreRef.current, []);
const setEnvelope = useCallback((action: React.SetStateAction<TEditorEnvelope>) => {
const next = typeof action === 'function' ? action(envelopeStoreRef.current) : action;
envelopeStoreRef.current = next;
for (const onStoreChange of envelopeStoreSubscribersRef.current) {
onStoreChange();
}
}, []);
const envelope = useSyncExternalStore(subscribeToEnvelopeStore, getEnvelope, getEnvelope);
const [autosaveError, setAutosaveError] = useState<boolean>(false);
const isCscMode = IS_INSTANCE_CSC_MODE();
@@ -135,8 +167,6 @@ export const EnvelopeEditorProvider = ({
};
}, [isCscMode, providedEditorConfig]);
const envelopeRef = useRef(initialEnvelope);
const externalFlushCallbacksRef = useRef<Map<string, () => Promise<void>>>(new Map());
const pendingMutationsRef = useRef<Set<Promise<unknown>>>(new Set());
@@ -156,14 +186,6 @@ export const EnvelopeEditorProvider = ({
});
}, []);
const setEnvelope: typeof _setEnvelope = (action) => {
_setEnvelope((prev) => {
const next = typeof action === 'function' ? action(prev) : action;
envelopeRef.current = next;
return next;
});
};
const isEmbedded = editorConfig.embedded !== undefined;
const editorFields = useEditorFields({
@@ -192,16 +214,18 @@ export const EnvelopeEditorProvider = ({
try {
let recipients: TEditorEnvelope['recipients'] = [];
const currentEnvelope = getEnvelope();
if (!isEmbedded) {
const response = await setRecipientsMutation.mutateAsync({
envelopeId: envelope.id,
envelopeType: envelope.type,
envelopeId: currentEnvelope.id,
envelopeType: currentEnvelope.type,
recipients: localRecipients,
});
recipients = response.data;
} else {
recipients = mapLocalRecipientsToRecipients({ envelope, localRecipients });
recipients = mapLocalRecipientsToRecipients({ envelope: currentEnvelope, localRecipients });
}
setEnvelope((prev) => ({
@@ -211,9 +235,7 @@ export const EnvelopeEditorProvider = ({
}));
// Reset the local fields to ensure deleted recipient fields are removed.
editorFields.resetForm(
envelope.fields.filter((field) => recipients.some((recipient) => recipient.id === field.recipientId)),
);
editorFields.resetForm(getEnvelope().fields);
setAutosaveError(false);
} catch (err) {
@@ -248,16 +270,18 @@ export const EnvelopeEditorProvider = ({
try {
let fields: TSetEnvelopeFieldsResponse['data'] = [];
const currentEnvelope = getEnvelope();
if (!isEmbedded) {
const response = await setFieldsMutation.mutateAsync({
envelopeId: envelope.id,
envelopeType: envelope.type,
envelopeId: currentEnvelope.id,
envelopeType: currentEnvelope.type,
fields: localFields,
});
fields = response.data;
} else {
fields = mapLocalFieldsToFields({ envelope, localFields });
fields = mapLocalFieldsToFields({ envelope: currentEnvelope, localFields });
}
setEnvelope((prev) => ({
@@ -309,7 +333,7 @@ export const EnvelopeEditorProvider = ({
try {
const response = !isEmbedded
? await updateEnvelopeMutation.mutateAsync({
envelopeId: envelope.id,
envelopeId: getEnvelope().id,
data,
meta,
})
@@ -467,12 +491,14 @@ export const EnvelopeEditorProvider = ({
};
const resetForms = () => {
const currentEnvelope = getEnvelope();
editorRecipients.resetForm({
recipients: envelopeRef.current.recipients,
documentMeta: envelopeRef.current.documentMeta,
recipients: currentEnvelope.recipients,
documentMeta: currentEnvelope.documentMeta,
});
editorFields.resetForm(envelopeRef.current.fields);
editorFields.resetForm(currentEnvelope.fields);
};
const flushAutosave = async (): Promise<TEditorEnvelope> => {
@@ -488,7 +514,7 @@ export const EnvelopeEditorProvider = ({
await Promise.allSettled(Array.from(pendingMutationsRef.current));
}
return envelopeRef.current;
return getEnvelope();
};
return (
+2 -2
View File
@@ -29,6 +29,7 @@
"@documenso/email": "*",
"@documenso/prisma": "*",
"@documenso/signing": "*",
"@documenso/skia-canvas": "^3.0.8-documenso.3",
"@lingui/core": "^5.6.0",
"@lingui/macro": "^5.6.0",
"@lingui/react": "^5.6.0",
@@ -64,10 +65,9 @@
"postcss-selector-parser": "^7.1.4",
"posthog-js": "^1.297.2",
"posthog-node": "4.18.0",
"react": "^18",
"react": "^19.2.7",
"remeda": "^2.32.0",
"sharp": "0.34.5",
"skia-canvas": "^3.0.8",
"stripe": "^12.18.0",
"ts-pattern": "^5.9.0",
"zod": "^3.25.76"
+1 -1
View File
@@ -1,6 +1,6 @@
import { Canvas, Image, Path2D } from '@documenso/skia-canvas';
import pMap from 'p-map';
import * as pdfjsLib from 'pdfjs-dist/legacy/build/pdf.mjs';
import { Canvas, Image, Path2D } from 'skia-canvas';
// @ts-expect-error napi-rs/canvas satisfies the requirements
globalThis.Path2D = Path2D;
@@ -2,8 +2,9 @@
* !: This is a workaround to fix the memory leak in the skia-canvas library.
* !: Internals are ported from the original `konva/skia-backend.js` file.
*/
import { Canvas, DOMMatrix, Image, Path2D } from '@documenso/skia-canvas';
import { Konva } from 'konva/lib/_CoreInternals';
import { Canvas, DOMMatrix, Image, Path2D } from 'skia-canvas';
// @ts-expect-error skia-canvas satisfies the requirements
global.DOMMatrix = DOMMatrix;
@@ -37,6 +38,6 @@ Konva.Util.createImageElement = () => {
return node as unknown as HTMLImageElement;
};
Konva._renderBackend = 'skia-canvas';
Konva._renderBackend = '@documenso/skia-canvas';
export default Konva;
+1 -1
View File
@@ -1,8 +1,8 @@
import path from 'node:path';
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
import { FontLibrary } from '@documenso/skia-canvas';
import type { Recipient } from '@prisma/client';
import { FieldType } from '@prisma/client';
import { FontLibrary } from 'skia-canvas';
import { match } from 'ts-pattern';
/**
@@ -2,8 +2,8 @@
import '../konva/skia-backend';
import type { FieldWithSignature } from '@documenso/prisma/types/field-with-signature';
import type { Canvas } from '@documenso/skia-canvas';
import Konva from 'konva';
import type { Canvas } from 'skia-canvas';
import { renderField } from '../../universal/field-renderer/render-field';
import { ensureFontLibrary } from './helpers';
@@ -1,14 +1,16 @@
// sort-imports-ignore
import '../konva/skia-backend';
import fs from 'node:fs';
import path from 'node:path';
import type { Canvas } from '@documenso/skia-canvas';
import { Image as SkiaImage } from '@documenso/skia-canvas';
import type { I18n } from '@lingui/core';
import { msg } from '@lingui/core/macro';
import type { DocumentMeta, Envelope, RecipientRole } from '@prisma/client';
import Konva from 'konva';
import 'konva/skia-backend';
import fs from 'node:fs';
import path from 'node:path';
import type { DateTimeFormatOptions } from 'luxon';
import { DateTime } from 'luxon';
import type { Canvas } from 'skia-canvas';
import { Image as SkiaImage } from 'skia-canvas';
import { match, P } from 'ts-pattern';
import { UAParser } from 'ua-parser-js';
@@ -1,14 +1,16 @@
// sort-imports-ignore
import '../konva/skia-backend';
import fs from 'node:fs';
import path from 'node:path';
import type { Canvas } from '@documenso/skia-canvas';
import { Image as SkiaImage } from '@documenso/skia-canvas';
import type { I18n } from '@lingui/core';
import { msg } from '@lingui/core/macro';
import type { Field, RecipientRole, Signature } from '@prisma/client';
import { SigningStatus } from '@prisma/client';
import Konva from 'konva';
import 'konva/skia-backend';
import fs from 'node:fs';
import path from 'node:path';
import { DateTime } from 'luxon';
import type { Canvas } from 'skia-canvas';
import { Image as SkiaImage } from 'skia-canvas';
import { UAParser } from 'ua-parser-js';
import { renderSVG } from 'uqr';
@@ -14,7 +14,7 @@ let SkiaImage: any;
void (async () => {
if (typeof window === 'undefined') {
const mod = await import('skia-canvas');
const mod = await import('@documenso/skia-canvas');
SkiaImage = mod.Image;
}
})();
+3 -3
View File
@@ -12,9 +12,9 @@
"@documenso/prisma": "*",
"@simplewebauthn/server": "^13.2.2",
"@tanstack/react-query": "5.90.10",
"@trpc/client": "11.8.1",
"@trpc/react-query": "11.8.1",
"@trpc/server": "11.8.1",
"@trpc/client": "11.17.0",
"@trpc/react-query": "11.17.0",
"@trpc/server": "11.17.0",
"@ts-rest/core": "^3.52.1",
"formidable": "^3.5.4",
"luxon": "^3.7.2",
+1 -1
View File
@@ -35,7 +35,7 @@ export const SigningCard3D = ({ className, name, signature, signingCelebrationIm
const [trackMouse, setTrackMouse] = useState(false);
const timeoutRef = useRef<number | undefined>();
const timeoutRef = useRef<number | undefined>(undefined);
const cardX = useMotionValue(0);
const cardY = useMotionValue(0);
+7 -34
View File
@@ -18,59 +18,32 @@
"@documenso/tailwind-config": "*",
"@documenso/tsconfig": "*",
"@types/luxon": "^3.7.1",
"@types/react": "18.3.27",
"@types/react-dom": "^18",
"react": "^18",
"@types/react": "^19.2.17",
"@types/react-dom": "^19.2.3",
"react": "^19.2.7",
"typescript": "5.6.2"
},
"dependencies": {
"@documenso/lib": "*",
"@hello-pangea/dnd": "^16.6.0",
"@hello-pangea/dnd": "^18.0.1",
"@hookform/resolvers": "^3",
"@lingui/macro": "^5.6.0",
"@lingui/react": "^5.6.0",
"@radix-ui/react-accordion": "^1.2.12",
"@radix-ui/react-alert-dialog": "^1.1.15",
"@radix-ui/react-aspect-ratio": "^1.1.8",
"@radix-ui/react-avatar": "^1.1.11",
"@radix-ui/react-checkbox": "^1.3.3",
"@radix-ui/react-collapsible": "^1.1.12",
"@radix-ui/react-context-menu": "^2.2.16",
"@radix-ui/react-dialog": "^1.1.15",
"@radix-ui/react-dropdown-menu": "^2.1.16",
"@radix-ui/react-hover-card": "^1.1.15",
"@radix-ui/react-label": "^2.1.8",
"@radix-ui/react-menubar": "^1.1.16",
"@radix-ui/react-navigation-menu": "^1.2.14",
"@radix-ui/react-popover": "^1.1.15",
"@radix-ui/react-progress": "^1.1.8",
"@radix-ui/react-radio-group": "^1.3.8",
"@radix-ui/react-scroll-area": "^1.2.10",
"@radix-ui/react-select": "^2.2.6",
"@radix-ui/react-separator": "^1.1.8",
"@radix-ui/react-slider": "^1.3.6",
"@radix-ui/react-slot": "^1.2.4",
"@radix-ui/react-switch": "^1.2.6",
"@radix-ui/react-tabs": "^1.1.13",
"@radix-ui/react-toast": "^1.2.15",
"@radix-ui/react-toggle": "^1.1.10",
"@radix-ui/react-toggle-group": "^1.1.11",
"@radix-ui/react-tooltip": "^1.2.8",
"@scure/base": "^1.2.6",
"@tanstack/react-table": "^8.21.3",
"class-variance-authority": "^0.7.1",
"clsx": "^1.2.1",
"cmdk": "^0.2.1",
"cmdk": "^1.1.1",
"colord": "^2.9.3",
"framer-motion": "^12.43.0",
"lucide-react": "^0.554.0",
"luxon": "^3.7.2",
"pdfjs-dist": "5.4.296",
"perfect-freehand": "^1.2.2",
"react": "^18",
"react": "^19.2.7",
"react-colorful": "^5.6.1",
"react-day-picker": "^8.10.1",
"react-dom": "^18",
"react-dom": "^19.2.7",
"react-hook-form": "^7.66.1",
"react-rnd": "^10.5.2",
"remeda": "^2.32.0",
+3 -1
View File
@@ -145,7 +145,9 @@ const CommandItem = React.forwardRef<
<CommandPrimitive.Item
ref={ref}
className={cn(
'relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none hover:bg-accent hover:text-accent-foreground aria-selected:bg-accent aria-selected:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50',
// cmdk 1.x always renders data-disabled="true|false", so the variant must
// check the value (bare data-[disabled] matches attribute presence).
'relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none hover:bg-accent hover:text-accent-foreground aria-selected:bg-accent aria-selected:text-accent-foreground data-[disabled=true]:pointer-events-none data-[disabled=true]:opacity-50',
className,
)}
{...props}
@@ -103,15 +103,10 @@ export const FieldContent = ({ field, documentMeta }: FieldIconProps) => {
) {
return (
<div className="flex flex-col gap-y-2 py-0.5">
<RadioGroup className="gap-y-1">
<RadioGroup value={field.customText ?? ''} className="gap-y-1">
{field.fieldMeta.values.map((item, index) => (
<div key={index} className="flex items-center">
<RadioGroupItem
className="pointer-events-none h-3 w-3"
value={item.value}
id={`option-${index}`}
checked={item.value === field.customText}
/>
<RadioGroupItem className="pointer-events-none h-3 w-3" value={item.value} id={`option-${index}`} />
{item.value && (
<Label htmlFor={`option-${index}`} className="ml-1.5 font-normal text-foreground text-xs">
{item.value}
@@ -1,5 +1,6 @@
import type { RecipientRole } from '@prisma/client';
import { BadgeCheck, Copy, Eye, PencilLine, User } from 'lucide-react';
import type { JSX } from 'react';
export const ROLE_ICONS: Record<RecipientRole, JSX.Element> = {
SIGNER: <PencilLine className="h-4 w-4" />,
@@ -13,7 +13,7 @@ import { getSvgPathFromStroke } from './helper';
import { Point } from './point';
import { SignaturePadColorPicker } from './signature-pad-color-picker';
const checkSignatureValidity = (element: RefObject<HTMLCanvasElement>) => {
const checkSignatureValidity = (element: RefObject<HTMLCanvasElement | null>) => {
if (!element.current) {
return false;
}
-73
View File
@@ -1,73 +0,0 @@
#!/usr/bin/env node
import { mkdirSync, readFileSync, writeFileSync } from 'fs';
import { join } from 'path';
import { generateId } from './utils/generate-id';
const JUSTIFICATIONS_DIR = join(process.cwd(), '.agents', 'justifications');
const main = () => {
const args = process.argv.slice(2);
if (args.length === 0) {
console.error('Usage: npx tsx scripts/create-justification.ts "file-slug" [content]');
console.error(' or: npx tsx scripts/create-justification.ts "file-slug" << HEREDOC');
process.exit(1);
}
const slug = args[0];
let content = '';
// Check if content is provided as second argument
if (args.length > 1) {
content = args.slice(1).join(' ');
} else {
// Read from stdin (heredoc)
try {
const stdin = readFileSync(0, 'utf-8');
content = stdin.trim();
} catch (error) {
console.error('Error reading from stdin:', error);
process.exit(1);
}
}
if (!content) {
console.error('Error: No content provided');
process.exit(1);
}
// Generate unique ID
const id = generateId();
const filename = `${id}-${slug}.md`;
const filepath = join(JUSTIFICATIONS_DIR, filename);
// Format title from slug (kebab-case to Title Case)
const title = slug
.split('-')
.map((word) => word.charAt(0).toUpperCase() + word.slice(1))
.join(' ');
// Get current date in ISO format
const date = new Date().toISOString().split('T')[0];
// Create frontmatter
const frontmatter = `---
date: ${date}
title: ${title}
---
`;
// Ensure directory exists
mkdirSync(JUSTIFICATIONS_DIR, { recursive: true });
// Write file with frontmatter
writeFileSync(filepath, frontmatter + content, 'utf-8');
console.log(`Created justification: ${filepath}`);
console.log(`ID: ${id}`);
console.log(`Filename: ${filename}`);
};
main();
-73
View File
@@ -1,73 +0,0 @@
#!/usr/bin/env node
import { mkdirSync, readFileSync, writeFileSync } from 'fs';
import { join } from 'path';
import { generateId } from './utils/generate-id';
const PLANS_DIR = join(process.cwd(), '.agents', 'plans');
const main = () => {
const args = process.argv.slice(2);
if (args.length === 0) {
console.error('Usage: npx tsx scripts/create-plan.ts "file-slug" [content]');
console.error(' or: npx tsx scripts/create-plan.ts "file-slug" << HEREDOC');
process.exit(1);
}
const slug = args[0];
let content = '';
// Check if content is provided as second argument
if (args.length > 1) {
content = args.slice(1).join(' ');
} else {
// Read from stdin (heredoc)
try {
const stdin = readFileSync(0, 'utf-8');
content = stdin.trim();
} catch (error) {
console.error('Error reading from stdin:', error);
process.exit(1);
}
}
if (!content) {
console.error('Error: No content provided');
process.exit(1);
}
// Generate unique ID
const id = generateId();
const filename = `${id}-${slug}.md`;
const filepath = join(PLANS_DIR, filename);
// Format title from slug (kebab-case to Title Case)
const title = slug
.split('-')
.map((word) => word.charAt(0).toUpperCase() + word.slice(1))
.join(' ');
// Get current date in ISO format
const date = new Date().toISOString().split('T')[0];
// Create frontmatter
const frontmatter = `---
date: ${date}
title: ${title}
---
`;
// Ensure directory exists
mkdirSync(PLANS_DIR, { recursive: true });
// Write file with frontmatter
writeFileSync(filepath, frontmatter + content, 'utf-8');
console.log(`Created plan: ${filepath}`);
console.log(`ID: ${id}`);
console.log(`Filename: ${filename}`);
};
main();
-73
View File
@@ -1,73 +0,0 @@
#!/usr/bin/env node
import { mkdirSync, readFileSync, writeFileSync } from 'fs';
import { join } from 'path';
import { generateId } from './utils/generate-id';
const SCRATCHES_DIR = join(process.cwd(), '.agents', 'scratches');
const main = () => {
const args = process.argv.slice(2);
if (args.length === 0) {
console.error('Usage: npx tsx scripts/create-scratch.ts "file-slug" [content]');
console.error(' or: npx tsx scripts/create-scratch.ts "file-slug" << HEREDOC');
process.exit(1);
}
const slug = args[0];
let content = '';
// Check if content is provided as second argument
if (args.length > 1) {
content = args.slice(1).join(' ');
} else {
// Read from stdin (heredoc)
try {
const stdin = readFileSync(0, 'utf-8');
content = stdin.trim();
} catch (error) {
console.error('Error reading from stdin:', error);
process.exit(1);
}
}
if (!content) {
console.error('Error: No content provided');
process.exit(1);
}
// Generate unique ID
const id = generateId();
const filename = `${id}-${slug}.md`;
const filepath = join(SCRATCHES_DIR, filename);
// Format title from slug (kebab-case to Title Case)
const title = slug
.split('-')
.map((word) => word.charAt(0).toUpperCase() + word.slice(1))
.join(' ');
// Get current date in ISO format
const date = new Date().toISOString().split('T')[0];
// Create frontmatter
const frontmatter = `---
date: ${date}
title: ${title}
---
`;
// Ensure directory exists
mkdirSync(SCRATCHES_DIR, { recursive: true });
// Write file with frontmatter
writeFileSync(filepath, frontmatter + content, 'utf-8');
console.log(`Created scratch: ${filepath}`);
console.log(`ID: ${id}`);
console.log(`Filename: ${filename}`);
};
main();
-84
View File
@@ -1,84 +0,0 @@
/**
* Generates a unique identifier using three simple words.
* Falls back to unix timestamp if word generation fails.
*/
export const generateId = (): string => {
const adjectives = [
'happy',
'bright',
'swift',
'calm',
'bold',
'clever',
'gentle',
'quick',
'sharp',
'warm',
'cool',
'fresh',
'solid',
'clear',
'sweet',
'wild',
'quiet',
'loud',
'smooth',
];
const nouns = [
'moon',
'star',
'ocean',
'river',
'forest',
'mountain',
'cloud',
'wave',
'stone',
'flower',
'bird',
'wind',
'light',
'shadow',
'fire',
'earth',
'sky',
'tree',
'leaf',
'rock',
];
const colors = [
'blue',
'red',
'green',
'yellow',
'purple',
'orange',
'pink',
'cyan',
'amber',
'emerald',
'violet',
'indigo',
'coral',
'teal',
'gold',
'silver',
'copper',
'bronze',
'ivory',
'jade',
];
try {
const randomAdjective = adjectives[Math.floor(Math.random() * adjectives.length)];
const randomColor = colors[Math.floor(Math.random() * colors.length)];
const randomNoun = nouns[Math.floor(Math.random() * nouns.length)];
return `${randomAdjective}-${randomColor}-${randomNoun}`;
} catch {
// Fallback to unix timestamp if something goes wrong
return Date.now().toString();
}
};