chore: merge main, resolve biome formatting conflicts

Merge origin/main into feat/external-2fa-codes. Resolve formatting
conflicts caused by biome rollout; preserve both feature streams:
PR's external 2FA token + signing-session 2FA proof additions plus
main's RateLimit/RecipientExpired/signingReminders/date-auto-insert.

In complete-document-with-token.ts, drop the duplicate early
field-fetching block introduced when main moved that logic later
with date auto-insert support; keep the EXTERNAL_TWO_FACTOR_AUTH
check using derivedRecipientActionAuth.
This commit is contained in:
ephraimduncan
2026-05-12 11:46:11 +00:00
parent 9194884fbe
commit 138d663c25
1959 changed files with 93488 additions and 47038 deletions
+6 -12
View File
@@ -1,10 +1,8 @@
import { env } from '@documenso/lib/utils/env';
export const APP_DOCUMENT_UPLOAD_SIZE_LIMIT =
Number(env('NEXT_PUBLIC_DOCUMENT_SIZE_UPLOAD_LIMIT')) || 50;
export const APP_DOCUMENT_UPLOAD_SIZE_LIMIT = Number(env('NEXT_PUBLIC_DOCUMENT_SIZE_UPLOAD_LIMIT')) || 50;
export const NEXT_PUBLIC_WEBAPP_URL = () =>
env('NEXT_PUBLIC_WEBAPP_URL') ?? 'http://localhost:3000';
export const NEXT_PUBLIC_WEBAPP_URL = () => env('NEXT_PUBLIC_WEBAPP_URL') ?? 'http://localhost:3000';
export const NEXT_PUBLIC_SIGNING_CONTACT_INFO = () =>
env('NEXT_PUBLIC_SIGNING_CONTACT_INFO') ?? NEXT_PUBLIC_WEBAPP_URL();
@@ -22,11 +20,9 @@ export const API_V2_URL = '/api/v2';
export const SUPPORT_EMAIL = env('NEXT_PUBLIC_SUPPORT_EMAIL') ?? 'support@documenso.com';
export const USE_INTERNAL_URL_BROWSERLESS = () =>
env('NEXT_PUBLIC_USE_INTERNAL_URL_BROWSERLESS') === 'true';
export const USE_INTERNAL_URL_BROWSERLESS = () => env('NEXT_PUBLIC_USE_INTERNAL_URL_BROWSERLESS') === 'true';
export const IS_AI_FEATURES_CONFIGURED = () =>
!!env('GOOGLE_VERTEX_PROJECT_ID') && !!env('GOOGLE_VERTEX_API_KEY');
export const IS_AI_FEATURES_CONFIGURED = () => !!env('GOOGLE_VERTEX_PROJECT_ID') && !!env('GOOGLE_VERTEX_API_KEY');
/**
* Temporary flag to toggle between Playwright-based and Konva-based PDF generation
@@ -34,8 +30,6 @@ export const IS_AI_FEATURES_CONFIGURED = () =>
*
* @deprecated This is a temporary flag and will be removed once Konva-based generation is stable.
*/
export const NEXT_PRIVATE_USE_PLAYWRIGHT_PDF = () =>
env('NEXT_PRIVATE_USE_PLAYWRIGHT_PDF') === 'true';
export const NEXT_PRIVATE_USE_PLAYWRIGHT_PDF = () => env('NEXT_PRIVATE_USE_PLAYWRIGHT_PDF') === 'true';
export const NEXT_PRIVATE_SIGNING_TIMESTAMP_AUTHORITY = () =>
env('NEXT_PRIVATE_SIGNING_TIMESTAMP_AUTHORITY');
export const NEXT_PRIVATE_SIGNING_TIMESTAMP_AUTHORITY = () => env('NEXT_PRIVATE_SIGNING_TIMESTAMP_AUTHORITY');
+72 -3
View File
@@ -1,8 +1,23 @@
import { z } from 'zod';
import { env } from '../utils/env';
import { NEXT_PUBLIC_WEBAPP_URL } from './app';
export const SALT_ROUNDS = 12;
export const URL_PATTERN = /https?:\/\/|www\./i;
/**
* Shared name schema that disallows URLs to prevent phishing via email rendering.
*/
export const ZNameSchema = z
.string()
.trim()
.min(3, { message: 'Please enter a valid name.' })
.refine((value) => !URL_PATTERN.test(value), {
message: 'Name cannot contain URLs.',
});
export const IDENTITY_PROVIDER_NAME: Record<string, string> = {
DOCUMENSO: 'Documenso',
GOOGLE: 'Google',
@@ -19,9 +34,7 @@ export const IS_MICROSOFT_SSO_ENABLED = Boolean(
);
export const IS_OIDC_SSO_ENABLED = Boolean(
env('NEXT_PRIVATE_OIDC_WELL_KNOWN') &&
env('NEXT_PRIVATE_OIDC_CLIENT_ID') &&
env('NEXT_PRIVATE_OIDC_CLIENT_SECRET'),
env('NEXT_PRIVATE_OIDC_WELL_KNOWN') && env('NEXT_PRIVATE_OIDC_CLIENT_ID') && env('NEXT_PRIVATE_OIDC_CLIENT_SECRET'),
);
export const OIDC_PROVIDER_LABEL = env('NEXT_PRIVATE_OIDC_PROVIDER_LABEL');
@@ -69,3 +82,59 @@ export const getCookieDomain = () => {
return url.hostname;
};
/**
* Get allowed signup domains from env var.
* Returns empty array if not set (meaning all domains allowed).
*/
export const getAllowedSignupDomains = (): string[] => {
const domains = env('NEXT_PRIVATE_ALLOWED_SIGNUP_DOMAINS');
if (!domains) {
return [];
}
return domains
.split(',')
.map((d) => d.trim().toLowerCase())
.filter(Boolean);
};
/**
* Check if email domain is allowed for signup.
* Returns true if no domain restriction is configured.
*/
export const isEmailDomainAllowedForSignup = (email: string): boolean => {
const allowedDomains = getAllowedSignupDomains();
if (allowedDomains.length === 0) {
return true;
}
const emailDomain = email.toLowerCase().split('@').pop();
if (!emailDomain) {
return false;
}
return allowedDomains.includes(emailDomain);
};
/**
* Check if signup is enabled for the given provider.
* The master switch takes precedence over the per-provider flags.
*/
export const isSignupEnabledForProvider = (provider: 'email' | 'google' | 'microsoft' | 'oidc'): boolean => {
if (env('NEXT_PUBLIC_DISABLE_SIGNUP') === 'true') {
return false;
}
const flagMap = {
email: 'NEXT_PUBLIC_DISABLE_EMAIL_PASSWORD_SIGNUP',
google: 'NEXT_PUBLIC_DISABLE_GOOGLE_SIGNUP',
microsoft: 'NEXT_PUBLIC_DISABLE_MICROSOFT_SIGNUP',
oidc: 'NEXT_PUBLIC_DISABLE_OIDC_SIGNUP',
} as const;
return env(flagMap[provider]) !== 'true';
};
+13 -3
View File
@@ -1,3 +1,4 @@
import { msg } from '@lingui/core/macro';
import { SubscriptionStatus } from '@prisma/client';
export enum STRIPE_PLAN_TYPE {
@@ -12,7 +13,16 @@ export enum STRIPE_PLAN_TYPE {
export const FREE_TIER_DOCUMENT_QUOTA = 5;
export const SUBSCRIPTION_STATUS_MAP = {
[SubscriptionStatus.ACTIVE]: 'Active',
[SubscriptionStatus.INACTIVE]: 'Inactive',
[SubscriptionStatus.PAST_DUE]: 'Past Due',
[SubscriptionStatus.ACTIVE]: msg({
message: 'Active',
context: 'Subscription status',
}),
[SubscriptionStatus.INACTIVE]: msg({
message: 'Inactive',
context: 'Subscription status',
}),
[SubscriptionStatus.PAST_DUE]: msg({
message: 'Past Due',
context: 'Subscription status',
}),
};
+11
View File
@@ -0,0 +1,11 @@
/**
* Maximum length (in characters) of the user-supplied custom CSS for branding.
* Bound enforced at the TRPC request boundary on both the organisation and
* team settings update routes. The sanitiser is run after this check; this
* limit is purely a request-size guard.
*
* 256 KB — generous enough for hand-written branding CSS and the occasional
* compiled-from-Tailwind-or-similar paste, while still keeping a request
* cap so a malicious or runaway payload can't exhaust PostCSS/server memory.
*/
export const BRANDING_CSS_MAX_LENGTH = 256 * 1024;
+18
View File
@@ -8,12 +8,15 @@ export const VALID_DATE_FORMAT_VALUES = [
DEFAULT_DOCUMENT_DATE_FORMAT,
'yyyy-MM-dd',
'dd/MM/yyyy',
'dd-MM-yyyy',
'MM/dd/yyyy',
'yy-MM-dd',
'MMMM dd, yyyy',
'EEEE, MMMM dd, yyyy',
'dd/MM/yyyy hh:mm a',
'dd/MM/yyyy HH:mm',
'dd-MM-yyyy hh:mm a',
'dd-MM-yyyy HH:mm',
'MM/dd/yyyy hh:mm a',
'MM/dd/yyyy HH:mm',
'dd.MM.yyyy',
@@ -52,6 +55,16 @@ export const DATE_FORMATS = [
label: 'DD/MM/YYYY HH:mm AM/PM',
value: 'dd/MM/yyyy hh:mm a',
},
{
key: 'DDMMYYYY_DASH_TIME',
label: 'DD-MM-YYYY HH:mm',
value: 'dd-MM-yyyy HH:mm',
},
{
key: 'DDMMYYYY_DASH_TIME_12H',
label: 'DD-MM-YYYY HH:mm AM/PM',
value: 'dd-MM-yyyy hh:mm a',
},
{
key: 'MMDDYYYY_TIME',
label: 'MM/DD/YYYY HH:mm',
@@ -117,6 +130,11 @@ export const DATE_FORMATS = [
label: 'DD/MM/YYYY',
value: 'dd/MM/yyyy',
},
{
key: 'DDMMYYYY_DASH',
label: 'DD-MM-YYYY',
value: 'dd-MM-yyyy',
},
{
key: 'MMDDYYYY',
label: 'MM/DD/YYYY',
@@ -19,4 +19,7 @@ export const DOCUMENT_AUDIT_LOG_EMAIL_FORMAT = {
[DOCUMENT_EMAIL_TYPE.DOCUMENT_COMPLETED]: {
description: 'Document completed',
},
[DOCUMENT_EMAIL_TYPE.REMINDER]: {
description: 'Signing Reminder',
},
} satisfies Record<keyof typeof DOCUMENT_EMAIL_TYPE, unknown>;
@@ -1,8 +1,7 @@
import { DocumentVisibility } from '@documenso/lib/types/document-visibility';
import type { MessageDescriptor } from '@lingui/core';
import { msg } from '@lingui/core/macro';
import { DocumentVisibility } from '@documenso/lib/types/document-visibility';
import type { TDocumentVisibility } from '../types/document-visibility';
type DocumentVisibilityTypeData = {
+9 -4
View File
@@ -1,14 +1,19 @@
import type { MessageDescriptor } from '@lingui/core';
import { msg } from '@lingui/core/macro';
import { DocumentDistributionMethod, DocumentStatus } from '@prisma/client';
/**
* Workaround for E2E tests to not import `msg`.
*/
import { DocumentSignatureType } from '@documenso/lib/utils/teams';
import type { MessageDescriptor } from '@lingui/core';
import { msg } from '@lingui/core/macro';
import { DocumentDistributionMethod, DocumentStatus } from '@prisma/client';
export { DocumentSignatureType };
/**
* Maximum count returned per status bucket in document stats. The server clamps
* each count to this value; the UI should display "10,000+" when it sees it.
*/
export const STATS_COUNT_CAP = 10_000;
export const DOCUMENT_STATUS: {
[status in DocumentStatus]: { description: MessageDescriptor };
} = {
-2
View File
@@ -8,8 +8,6 @@ export const DOCUMENSO_INTERNAL_EMAIL = {
address: FROM_ADDRESS,
};
export const SERVICE_USER_EMAIL = 'serviceaccount@documenso.com';
export const EMAIL_VERIFICATION_STATE = {
NOT_FOUND: 'NOT_FOUND',
VERIFIED: 'VERIFIED',
@@ -0,0 +1,61 @@
import type { DurationLikeObject } from 'luxon';
import { Duration } from 'luxon';
import { z } from 'zod';
export const ZEnvelopeExpirationDurationPeriod = z.object({
unit: z.enum(['day', 'week', 'month', 'year']),
amount: z.number().int().min(1),
});
export const ZEnvelopeExpirationDisabledPeriod = z.object({
disabled: z.literal(true),
});
export const ZEnvelopeExpirationPeriod = z.union([
ZEnvelopeExpirationDurationPeriod,
ZEnvelopeExpirationDisabledPeriod,
]);
export type TEnvelopeExpirationPeriod = z.infer<typeof ZEnvelopeExpirationPeriod>;
export type TEnvelopeExpirationDurationPeriod = z.infer<typeof ZEnvelopeExpirationDurationPeriod>;
const UNIT_TO_LUXON_KEY: Record<TEnvelopeExpirationDurationPeriod['unit'], keyof DurationLikeObject> = {
day: 'days',
week: 'weeks',
month: 'months',
year: 'years',
};
export const DEFAULT_ENVELOPE_EXPIRATION_PERIOD: TEnvelopeExpirationDurationPeriod = {
unit: 'month',
amount: 3,
};
export const getEnvelopeExpirationDuration = (period: TEnvelopeExpirationDurationPeriod): Duration => {
return Duration.fromObject({ [UNIT_TO_LUXON_KEY[period.unit]]: period.amount });
};
/**
* Resolve the concrete expiresAt timestamp from a raw expiration period (from JSON column).
*
* - `null` means use the default period (3 months).
* - `{ disabled: true }` means never expires (returns null).
* - `{ unit, amount }` means compute the timestamp from now + duration.
*/
export const resolveExpiresAt = (rawPeriod: unknown): Date | null => {
if (rawPeriod === null || rawPeriod === undefined) {
const duration = getEnvelopeExpirationDuration(DEFAULT_ENVELOPE_EXPIRATION_PERIOD);
return new Date(Date.now() + duration.toMillis());
}
const parsed = ZEnvelopeExpirationPeriod.parse(rawPeriod);
if ('disabled' in parsed) {
return null;
}
const duration = getEnvelopeExpirationDuration(parsed);
return new Date(Date.now() + duration.toMillis());
};
+104
View File
@@ -0,0 +1,104 @@
import type { DurationLikeObject } from 'luxon';
import { Duration } from 'luxon';
import { z } from 'zod';
export const ZEnvelopeReminderDurationPeriod = z.object({
unit: z.enum(['day', 'week', 'month']),
amount: z.number().int().min(1),
});
export const ZEnvelopeReminderDisabledPeriod = z.object({
disabled: z.literal(true),
});
export const ZEnvelopeReminderPeriod = z.union([ZEnvelopeReminderDurationPeriod, ZEnvelopeReminderDisabledPeriod]);
export type TEnvelopeReminderPeriod = z.infer<typeof ZEnvelopeReminderPeriod>;
export type TEnvelopeReminderDurationPeriod = z.infer<typeof ZEnvelopeReminderDurationPeriod>;
export const ZEnvelopeReminderSettings = z.object({
sendAfter: ZEnvelopeReminderPeriod,
repeatEvery: ZEnvelopeReminderPeriod,
});
export type TEnvelopeReminderSettings = z.infer<typeof ZEnvelopeReminderSettings>;
export const DEFAULT_ENVELOPE_REMINDER_SETTINGS: TEnvelopeReminderSettings = {
sendAfter: { unit: 'day', amount: 5 },
repeatEvery: { unit: 'day', amount: 2 },
};
/**
* Hard upper bound on the window in which automated reminders may be sent,
* measured from the moment the signing request was first sent to the
* recipient. Prevents runaway reminder chains for recipients with no
* expiration set who never sign.
*/
export const MAX_REMINDER_WINDOW_DAYS = 30;
const UNIT_TO_LUXON_KEY: Record<TEnvelopeReminderDurationPeriod['unit'], keyof DurationLikeObject> = {
day: 'days',
week: 'weeks',
month: 'months',
};
export const getEnvelopeReminderDuration = (period: TEnvelopeReminderDurationPeriod): Duration => {
return Duration.fromObject({ [UNIT_TO_LUXON_KEY[period.unit]]: period.amount });
};
/**
* Resolve the next reminder timestamp from the config and the last reminder sent time.
*
* - `null` config means reminders are disabled (inherit = no override, resolved as disabled).
* - `{ sendAfter: { disabled: true }, ... }` means never send the first reminder.
* - `{ repeatEvery: { disabled: true }, ... }` means don't repeat after the first reminder.
*
* A hard cap of `MAX_REMINDER_WINDOW_DAYS` days from `sentAt` is enforced —
* any computed reminder beyond that point returns null so reminders stop.
*
* `sentAt` is when the signing request was sent to this specific recipient.
*
* Returns the next Date the reminder should be sent, or null if no reminder should be sent.
*/
export const resolveNextReminderAt = (options: {
config: TEnvelopeReminderSettings | null;
sentAt: Date;
lastReminderSentAt: Date | null;
}): Date | null => {
const { config, sentAt, lastReminderSentAt } = options;
if (!config) {
return null;
}
const maxReminderAt = new Date(sentAt.getTime() + Duration.fromObject({ days: MAX_REMINDER_WINDOW_DAYS }).toMillis());
let candidate: Date;
// If we haven't sent the first reminder yet, use sendAfter.
if (!lastReminderSentAt) {
if ('disabled' in config.sendAfter) {
return null;
}
const delay = getEnvelopeReminderDuration(config.sendAfter);
candidate = new Date(sentAt.getTime() + delay.toMillis());
} else {
// For subsequent reminders, use repeatEvery.
if ('disabled' in config.repeatEvery) {
return null;
}
const interval = getEnvelopeReminderDuration(config.repeatEvery);
candidate = new Date(lastReminderSentAt.getTime() + interval.toMillis());
}
// Stop if the candidate is past the hard cap measured from sentAt.
if (candidate.getTime() > maxReminderAt.getTime()) {
return null;
}
return candidate;
};
+16 -35
View File
@@ -1,22 +1,9 @@
import { z } from 'zod';
import type { MessageDescriptor } from '@lingui/core';
import { msg } from '@lingui/core/macro';
export const SUPPORTED_LANGUAGE_CODES = [
'de',
'en',
'fr',
'es',
'it',
'nl',
'pl',
'pt-BR',
'ja',
'ko',
'zh',
] as const;
import { SUPPORTED_LANGUAGE_CODES, type SupportedLanguageCodes } from './locales';
export const ZSupportedLanguageCodeSchema = z.enum(SUPPORTED_LANGUAGE_CODES).catch('en');
export type SupportedLanguageCodes = (typeof SUPPORTED_LANGUAGE_CODES)[number];
export * from './locales';
export type I18nLocaleData = {
/**
@@ -30,61 +17,55 @@ export type I18nLocaleData = {
locales: string[];
};
export const APP_I18N_OPTIONS = {
supportedLangs: SUPPORTED_LANGUAGE_CODES,
sourceLang: 'en',
defaultLocale: 'en-US',
} as const;
type SupportedLanguage = {
full: string;
short: string;
full: MessageDescriptor;
};
export const SUPPORTED_LANGUAGES: Record<string, SupportedLanguage> = {
de: {
full: 'German',
short: 'de',
full: msg`German`,
},
en: {
full: 'English',
short: 'en',
full: msg`English`,
},
fr: {
full: 'French',
short: 'fr',
full: msg`French`,
},
es: {
full: 'Spanish',
short: 'es',
full: msg`Spanish`,
},
it: {
full: 'Italian',
short: 'it',
full: msg`Italian`,
},
nl: {
short: 'nl',
full: 'Dutch',
full: msg`Dutch`,
},
pl: {
short: 'pl',
full: 'Polish',
full: msg`Polish`,
},
'pt-BR': {
short: 'pt-BR',
full: 'Portuguese (Brazil)',
full: msg`Portuguese (Brazil)`,
},
ja: {
short: 'ja',
full: 'Japanese',
full: msg`Japanese`,
},
ko: {
short: 'ko',
full: 'Korean',
full: msg`Korean`,
},
zh: {
short: 'zh',
full: 'Chinese',
full: msg`Chinese`,
},
} satisfies Record<SupportedLanguageCodes, SupportedLanguage>;
+13
View File
@@ -0,0 +1,13 @@
import { z } from 'zod';
export const SUPPORTED_LANGUAGE_CODES = ['de', 'en', 'fr', 'es', 'it', 'nl', 'pl', 'pt-BR', 'ja', 'ko', 'zh'] as const;
export type SupportedLanguageCodes = (typeof SUPPORTED_LANGUAGE_CODES)[number];
export const APP_I18N_OPTIONS = {
supportedLangs: SUPPORTED_LANGUAGE_CODES,
sourceLang: 'en',
defaultLocale: 'en-US',
} as const;
export const ZSupportedLanguageCodeSchema = z.enum(SUPPORTED_LANGUAGE_CODES).catch('en');
@@ -6,19 +6,13 @@ import type { MessageDescriptor } from '@lingui/core';
import { msg } from '@lingui/core/macro';
import type { OrganisationMemberRole } from '@prisma/client';
export const ORGANISATION_MEMBER_ROLE_MAP: Record<
keyof typeof OrganisationMemberRole,
MessageDescriptor
> = {
export const ORGANISATION_MEMBER_ROLE_MAP: Record<keyof typeof OrganisationMemberRole, MessageDescriptor> = {
ADMIN: msg`Admin`,
MANAGER: msg`Manager`,
MEMBER: msg`Member`,
};
export const EXTENDED_ORGANISATION_MEMBER_ROLE_MAP: Record<
keyof typeof OrganisationMemberRole,
MessageDescriptor
> = {
export const EXTENDED_ORGANISATION_MEMBER_ROLE_MAP: Record<keyof typeof OrganisationMemberRole, MessageDescriptor> = {
ADMIN: msg`Organisation Admin`,
MANAGER: msg`Organisation Manager`,
MEMBER: msg`Organisation Member`,
+2 -2
View File
@@ -1,7 +1,7 @@
import { OrganisationGroupType, OrganisationMemberRole } from '@prisma/client';
export const ORGANISATION_URL_ROOT_REGEX = new RegExp('^/t/[^/]+/?$');
export const ORGANISATION_URL_REGEX = new RegExp('^/t/[^/]+');
export const ORGANISATION_URL_ROOT_REGEX = /^\/t\/[^/]+\/?$/;
export const ORGANISATION_URL_REGEX = /^\/t\/[^/]+/;
export const ORGANISATION_INTERNAL_GROUPS: {
organisationRole: OrganisationMemberRole;
+22
View File
@@ -0,0 +1,22 @@
import type { MessageDescriptor } from '@lingui/core';
import { msg } from '@lingui/core/macro';
// This is separate from the pdf-viewer.ts constant file due to parsing issues during testing.
export const PDF_VIEWER_ERROR_MESSAGES = {
editor: {
title: msg`Configuration Error`,
description: msg`There was an issue rendering some fields, please review the fields and try again.`,
},
preview: {
title: msg`Configuration Error`,
description: msg`Something went wrong while rendering the document, some fields may be missing or corrupted.`,
},
signing: {
title: msg`Configuration Error`,
description: msg`Something went wrong while rendering the document, some fields may be missing or corrupted.`,
},
default: {
title: msg`Configuration Error`,
description: msg`Something went wrong while rendering the document, please try again or contact our support.`,
},
} satisfies Record<string, { title: MessageDescriptor; description: MessageDescriptor }>;
+16 -1
View File
@@ -1,2 +1,17 @@
export const PDF_VIEWER_CONTAINER_SELECTOR = '.react-pdf__Document';
// Keep these two constants in sync.
export const PDF_VIEWER_PAGE_SELECTOR = '.react-pdf__Page';
export const PDF_VIEWER_PAGE_CLASSNAME = 'react-pdf__Page z-0';
export const PDF_VIEWER_CONTENT_SELECTOR = '[data-pdf-content]';
export const getPdfPagesCount = () => {
const pageCountAttr = document.querySelector(PDF_VIEWER_CONTENT_SELECTOR)?.getAttribute('data-page-count');
const totalPages = Number(pageCountAttr);
if (!Number.isInteger(totalPages) || totalPages < 1) {
return 0;
}
return totalPages;
};
+5 -6
View File
@@ -8,9 +8,8 @@ export const TEAM_MEMBER_ROLE_MAP: Record<keyof typeof TeamMemberRole, MessageDe
MEMBER: msg`Member`,
};
export const EXTENDED_TEAM_MEMBER_ROLE_MAP: Record<keyof typeof TeamMemberRole, MessageDescriptor> =
{
ADMIN: msg`Team Admin`,
MANAGER: msg`Team Manager`,
MEMBER: msg`Team Member`,
};
export const EXTENDED_TEAM_MEMBER_ROLE_MAP: Record<keyof typeof TeamMemberRole, MessageDescriptor> = {
ADMIN: msg`Team Admin`,
MANAGER: msg`Team Manager`,
MEMBER: msg`Team Member`,
};
+3 -7
View File
@@ -1,7 +1,7 @@
import { DocumentVisibility, OrganisationGroupType, TeamMemberRole } from '@prisma/client';
export const TEAM_URL_ROOT_REGEX = new RegExp('^/t/[^/]+/?$');
export const TEAM_URL_REGEX = new RegExp('^/t/[^/]+');
export const TEAM_URL_ROOT_REGEX = /^\/t\/[^/]+\/?$/;
export const TEAM_URL_REGEX = /^\/t\/[^/]+/;
export const LOWEST_TEAM_ROLE = TeamMemberRole.MEMBER;
@@ -34,11 +34,7 @@ export const TEAM_MEMBER_ROLE_PERMISSIONS_MAP = {
} satisfies Record<string, TeamMemberRole[]>;
export const TEAM_DOCUMENT_VISIBILITY_MAP = {
[TeamMemberRole.ADMIN]: [
DocumentVisibility.ADMIN,
DocumentVisibility.MANAGER_AND_ABOVE,
DocumentVisibility.EVERYONE,
],
[TeamMemberRole.ADMIN]: [DocumentVisibility.ADMIN, DocumentVisibility.MANAGER_AND_ABOVE, DocumentVisibility.EVERYONE],
[TeamMemberRole.MANAGER]: [DocumentVisibility.MANAGER_AND_ABOVE, DocumentVisibility.EVERYONE],
[TeamMemberRole.MEMBER]: [DocumentVisibility.EVERYONE],
} satisfies Record<TeamMemberRole, DocumentVisibility[]>;
+58
View File
@@ -0,0 +1,58 @@
import type { TCssVarsSchema } from '../types/css-vars';
/**
* !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
*
* KEEP THIS FILE IN SYNC WITH `packages/ui/styles/theme.css`.
*
* These are the light-mode default values for the CSS custom properties
* defined under `:root` in the theme stylesheet, exposed here as hex strings
* so they can be used as defaults for colour-picker UI components and other
* places that don't render through CSS variables.
*
* If you change a value in `theme.css`, update it here too. There is NO
* automated check linking the two files; they have drifted historically
* and will drift again unless you update both.
*
* Computed via `colord({ h, s, l }).toHex()` — see the inline HSL comments
* for the source-of-truth values from `theme.css`.
*
* !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
*/
export const DEFAULT_BRAND_COLORS = {
background: '#ffffff', // 0 0% 100%
foreground: '#0f172a', // 222.2 47.4% 11.2%
muted: '#f1f5f9', // 210 40% 96.1%
mutedForeground: '#64748b', // 215.4 16.3% 46.9%
popover: '#ffffff', // 0 0% 100%
popoverForeground: '#0f172a', // 222.2 47.4% 11.2%
card: '#ffffff', // 0 0% 100%
cardBorder: '#e2e8f0', // 214.3 31.8% 91.4%
cardForeground: '#0f172a', // 222.2 47.4% 11.2%
fieldCard: '#e2f8d3', // 95 74% 90%
fieldCardBorder: '#a2e771', // 95.08 71.08% 67.45%
fieldCardForeground: '#0f172a', // 222.2 47.4% 11.2%
widget: '#f7f7f7', // 0 0% 97%
widgetForeground: '#f2f2f2', // 0 0% 95%
border: '#e2e8f0', // 214.3 31.8% 91.4%
input: '#e2e8f0', // 214.3 31.8% 91.4%
primary: '#a2e771', // 95.08 71.08% 67.45%
primaryForeground: '#162c07', // 95.08 71.08% 10%
secondary: '#f1f5f9', // 210 40% 96.1%
secondaryForeground: '#0f172a', // 222.2 47.4% 11.2%
accent: '#f1f5f9', // 210 40% 96.1%
accentForeground: '#0f172a', // 222.2 47.4% 11.2%
destructive: '#ff0000', // 0 100% 50%
destructiveForeground: '#f8fafc', // 210 40% 98%
ring: '#a2e771', // 95.08 71.08% 67.45%
warning: '#e1cb05', // 54 96% 45%
envelopeEditorBackground: '#f8fafc', //210 40% 98.04%
// `cardBorderTint` is intentionally excluded from the colour-picker UI:
// unlike the rest of these tokens it is consumed via `rgb(var(--token))`
// (not `hsl(...)`) and stored as raw RGB triplets in `theme.css`. It does
// not flow through `toNativeCssVars` and is not user-customisable from the
// branding form. `radius` is a length, not a colour, so it lives in
// `DEFAULT_BRAND_RADIUS` below.
} as const satisfies Record<keyof Omit<TCssVarsSchema, 'radius' | 'cardBorderTint'>, string>;
export const DEFAULT_BRAND_RADIUS = '0.5rem';