Call recorder: skeleton loading state + port front components to twenty-ui (#22473)

## What this does

Three changes to the call-recorder app's recording/transcript front
component:

### 1. Fix loading flicker with a skeleton loader (`4dc3a167`)

The widget's loading state rendered an empty `<video controls>` element
at 16:9, which was torn down and replaced once the recording query
resolved — so every open flashed a video player, even for calendar
events with no video at all.

- New `RecordingSkeletonLoader`: three transcript-shaped pulsing rows
(avatar circle + speaker bar + text bars) held at the same 240px
min-height as the empty state, so nothing jumps when content arrives.
Visuals follow the twenty-front skeleton standard
(react-loading-skeleton theming: `background.tertiary` base,
`background.transparent.lighter` highlight sweep, 4px radius), rebuilt
with emotion since the sandboxed bundle can't import the host's global
skeleton stylesheet.
- `RecordingVideoPlayer` now only renders with a real URL (`src:
string`).

### 2. Port front components to twenty-ui (`cea0d6ee`)

`twenty-ui@1.0.0-alpha.1` is now published and consumable by apps
(CSS-injection build support + the renderer's style bridge), so the
"remove once twenty-ui can be imported safely" duplications are
resolved:

- Deleted `recording-theme-css-variables.ts` → all components use
`themeCssVariables` from `twenty-ui/theme-constants` (only non-1:1
rename: `accent.primary` → `accent.accent9`).
- Deleted `TranscriptSpeakerAvatar` / `TranscriptSpeakerChip` → replaced
with twenty-ui `Avatar` (`size="md"`, `type="rounded"`) and `Chip`
(`ChipVariant.Transparent`, `isBold`, non-clickable) in
`TranscriptEntryListItem`.
- Added `import 'twenty-ui/style.css'` to the front-component entry,
plus a root `css.d.ts` declaration. The import is load-bearing: the SDK
inlines the CSS into the component bundle and the renderer's style
bridge injects it into the host document — the host's own twenty-ui CSS
can't be relied on, since scoped class hashes are content-derived and
drift between builds.
- Bumped `twenty-sdk` / `twenty-client-sdk` to `^2.18.0` (needed for the
CSS-injection build support) and added `twenty-ui@^1.0.0-alpha.1`.

### 3. Fix manifest build + typecheck after the port (`baa6eb2f`)

- `yarn twenty dev --once` failed at "Building manifest..." — the
manifest build loads front-component modules with `twenty-ui`
proxy-mocked, so the named `themeCssVariables` import resolves to
`undefined` and any static `${themeCssVariables.x}` interpolation throws
at module scope. All static interpolations are now lazy (`${() =>
themeCssVariables.x}`), deferring the access to render time in the
browser.
- Added `css.d.ts` to `tsconfig.spec.json`'s `include` — its
`["src/**"]` include replaced the parent config's default file set, so
the `*.css` module declaration was never loaded and `yarn typecheck`
failed with TS2882.

## Notes for review / testing

- Worth exercising manually: the skeleton during load, speaker
chips/avatars rendering via twenty-ui, and hovering a long (overflowing)
speaker name — Chip's tooltip uses a react-dom portal, which is the one
path not previously exercised inside the front-component sandbox.
- App version intentionally left at `1.0.4`.

https://claude.ai/code/session_01WuHQFWiRocn82ejxsnmz28

---
_Generated by [Claude
Code](https://claude.ai/code/session_01WuHQFWiRocn82ejxsnmz28)_

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/22473?utm_source=github"
target="_blank" rel="noopener noreferrer"
data-no-image-dialog="true"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img
alt="Review in cubic"
src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a>
<!-- End of auto-generated description by cubic. -->
This commit is contained in:
nitin
2026-07-03 23:10:28 +05:30
committed by GitHub
parent 9be2b21e51
commit badaae17cd
23 changed files with 702 additions and 392 deletions
+1
View File
@@ -0,0 +1 @@
declare module '*.css';
@@ -34,8 +34,9 @@
"oxlint": "^0.16.0",
"react": "^19.0.0",
"react-dom": "^19.0.0",
"twenty-client-sdk": "^2.16.0",
"twenty-sdk": "^2.16.0",
"twenty-client-sdk": "^2.18.0",
"twenty-sdk": "^2.18.0",
"twenty-ui": "^1.0.0-alpha.1",
"typescript": "^5.9.3",
"vite-tsconfig-paths": "^4.2.1",
"vitest": "^4.1.9"
@@ -1,5 +1,7 @@
import { defineFrontComponent } from 'twenty-sdk/define';
import 'twenty-ui/style.css';
import { CALENDAR_EVENT_RECORDING_FRONT_COMPONENT_UNIVERSAL_IDENTIFIER } from 'src/constants/calendar-event-recording-front-component-universal-identifier';
import { CalendarEventRecording } from 'src/front-components/components/CalendarEventRecording';
@@ -1,20 +1,20 @@
import styled from '@emotion/styled';
import { isUndefined } from '@sniptt/guards';
import { useSelectedRecordIds } from 'twenty-sdk/front-component';
import { themeCssVariables } from 'twenty-ui/theme-constants';
import { CalendarEventRecordingContent } from 'src/front-components/components/CalendarEventRecordingContent';
import { recordingThemeCssVariables } from 'src/front-components/constants/recording-theme-css-variables';
const StyledCenteredState = styled.div`
align-items: center;
box-sizing: border-box;
color: ${recordingThemeCssVariables.font.colorTertiary};
color: ${() => themeCssVariables.font.color.tertiary};
display: flex;
font-family: ${recordingThemeCssVariables.font.family};
font-size: ${recordingThemeCssVariables.font.sizeSm};
font-family: ${() => themeCssVariables.font.family};
font-size: ${() => themeCssVariables.font.size.sm};
height: 100%;
justify-content: center;
padding: ${recordingThemeCssVariables.spacing[4]};
padding: ${() => themeCssVariables.spacing[4]};
`;
export const CalendarEventRecording = () => {
@@ -1,29 +1,30 @@
import styled from '@emotion/styled';
import { isUndefined } from '@sniptt/guards';
import { themeCssVariables } from 'twenty-ui/theme-constants';
import { RecordingSkeletonLoader } from 'src/front-components/components/RecordingSkeletonLoader';
import { RecordingTranscript } from 'src/front-components/components/RecordingTranscript';
import { RecordingVideoPlayer } from 'src/front-components/components/RecordingVideoPlayer';
import { TranscriptErrorBox } from 'src/front-components/components/TranscriptErrorBox';
import { recordingThemeCssVariables } from 'src/front-components/constants/recording-theme-css-variables';
import { type CalendarEventRecordingParticipant } from 'src/front-components/types/calendar-event-recording-participant.type';
const StyledCenteredState = styled.div`
align-items: center;
box-sizing: border-box;
color: ${recordingThemeCssVariables.font.colorTertiary};
color: ${() => themeCssVariables.font.color.tertiary};
display: flex;
font-family: ${recordingThemeCssVariables.font.family};
font-size: ${recordingThemeCssVariables.font.sizeSm};
font-family: ${() => themeCssVariables.font.family};
font-size: ${() => themeCssVariables.font.size.sm};
justify-content: center;
min-height: 240px;
padding: ${recordingThemeCssVariables.spacing[4]};
padding: ${() => themeCssVariables.spacing[4]};
`;
const StyledRecordingContainer = styled.div<{
$hasVideo?: boolean;
}>`
display: grid;
gap: ${recordingThemeCssVariables.spacing[2]};
gap: ${() => themeCssVariables.spacing[2]};
grid-template-rows: ${({ $hasVideo }) =>
$hasVideo ? 'auto minmax(0, 1fr)' : 'minmax(0, 1fr)'};
min-height: 0;
@@ -37,6 +38,7 @@ type CalendarEventRecordingBodyProps = {
currentTimeSeconds: number;
calendarEventParticipants: CalendarEventRecordingParticipant[];
onVideoTimeUpdate: (videoCurrentTimeSeconds: number) => void;
onVideoRetry: () => void;
};
export const CalendarEventRecordingBody = ({
@@ -47,6 +49,7 @@ export const CalendarEventRecordingBody = ({
currentTimeSeconds,
calendarEventParticipants,
onVideoTimeUpdate,
onVideoRetry,
}: CalendarEventRecordingBodyProps) => {
const hasVideo = !isUndefined(videoFileUrl);
@@ -60,14 +63,7 @@ export const CalendarEventRecordingBody = ({
}
if (isCalendarEventRecordingQueryLoading) {
return (
<StyledRecordingContainer $hasVideo={false}>
<RecordingVideoPlayer
src={undefined}
onTimeUpdate={onVideoTimeUpdate}
/>
</StyledRecordingContainer>
);
return <RecordingSkeletonLoader />;
}
if (isUndefined(transcript) && !hasVideo) {
@@ -84,6 +80,7 @@ export const CalendarEventRecordingBody = ({
<RecordingVideoPlayer
src={videoFileUrl}
onTimeUpdate={onVideoTimeUpdate}
onRetry={onVideoRetry}
/>
)}
<RecordingTranscript
@@ -1,21 +1,21 @@
import styled from '@emotion/styled';
import { useCallback, useState } from 'react';
import { useState } from 'react';
import { themeCssVariables } from 'twenty-ui/theme-constants';
import { CalendarEventRecordingBody } from 'src/front-components/components/CalendarEventRecordingBody';
import { recordingThemeCssVariables } from 'src/front-components/constants/recording-theme-css-variables';
import { useCalendarEventParticipants } from 'src/front-components/hooks/use-calendar-event-participants';
import { useCalendarEventRecording } from 'src/front-components/hooks/use-calendar-event-recording';
const TRANSCRIPT_TIME_UPDATE_INTERVAL_SECONDS = 0.25;
const StyledRecordingShell = styled.div`
background: ${recordingThemeCssVariables.background.primary};
background: ${() => themeCssVariables.background.primary};
border: 1px solid transparent;
border-bottom: 1px solid transparent;
border-radius: ${recordingThemeCssVariables.border.radiusMd};
border-radius: ${() => themeCssVariables.border.radius.md};
box-sizing: border-box;
font-family: ${recordingThemeCssVariables.font.family};
padding: ${recordingThemeCssVariables.spacing[4]};
font-family: ${() => themeCssVariables.font.family};
padding: ${() => themeCssVariables.spacing[4]};
position: relative;
width: 100%;
`;
@@ -24,31 +24,31 @@ const StyledRecordingHeader = styled.div`
align-items: center;
box-sizing: border-box;
display: flex;
height: ${recordingThemeCssVariables.spacing[6]};
height: ${() => themeCssVariables.spacing[6]};
`;
const StyledRecordingTitle = styled.h2`
color: ${recordingThemeCssVariables.font.colorPrimary};
color: ${() => themeCssVariables.font.color.primary};
flex: 1;
font-size: ${recordingThemeCssVariables.font.sizeMd};
font-weight: ${recordingThemeCssVariables.font.weightMedium};
font-size: ${() => themeCssVariables.font.size.md};
font-weight: ${() => themeCssVariables.font.weight.medium};
margin: 0;
overflow: hidden;
padding-inline: ${recordingThemeCssVariables.spacing[1]};
padding-inline: ${() => themeCssVariables.spacing[1]};
user-select: none;
`;
const StyledRecordingBody = styled.div`
box-sizing: border-box;
margin-top: ${recordingThemeCssVariables.spacing[2]};
margin-top: ${() => themeCssVariables.spacing[2]};
`;
const StyledRecordingContentFrame = styled.div`
background-color: ${recordingThemeCssVariables.background.secondary};
border: 1px solid ${recordingThemeCssVariables.border.colorMedium};
border-radius: ${recordingThemeCssVariables.border.radiusMd};
background-color: ${() => themeCssVariables.background.secondary};
border: 1px solid ${() => themeCssVariables.border.color.medium};
border-radius: ${() => themeCssVariables.border.radius.md};
box-sizing: border-box;
padding: ${recordingThemeCssVariables.spacing[2]};
padding: ${() => themeCssVariables.spacing[2]};
`;
type CalendarEventRecordingContentProps = {
@@ -59,31 +59,35 @@ export const CalendarEventRecordingContent = ({
calendarEventId,
}: CalendarEventRecordingContentProps) => {
const [currentTimeSeconds, setCurrentTimeSeconds] = useState(0);
const updateCurrentTimeSeconds = useCallback(
(videoCurrentTimeSeconds: number) => {
const nextCurrentTimeSeconds =
Math.floor(
videoCurrentTimeSeconds / TRANSCRIPT_TIME_UPDATE_INTERVAL_SECONDS,
) * TRANSCRIPT_TIME_UPDATE_INTERVAL_SECONDS;
setCurrentTimeSeconds((previousCurrentTimeSeconds) =>
previousCurrentTimeSeconds === nextCurrentTimeSeconds
? previousCurrentTimeSeconds
: nextCurrentTimeSeconds,
);
},
[],
);
const updateCurrentTimeSeconds = (videoCurrentTimeSeconds: number) => {
const nextCurrentTimeSeconds =
Math.floor(
videoCurrentTimeSeconds / TRANSCRIPT_TIME_UPDATE_INTERVAL_SECONDS,
) * TRANSCRIPT_TIME_UPDATE_INTERVAL_SECONDS;
setCurrentTimeSeconds((previousCurrentTimeSeconds) =>
previousCurrentTimeSeconds === nextCurrentTimeSeconds
? previousCurrentTimeSeconds
: nextCurrentTimeSeconds,
);
};
const {
transcript,
videoFile,
isCalendarEventRecordingQueryLoading,
errorMessage,
refetchCalendarEventRecording,
} = useCalendarEventRecording(calendarEventId);
const { calendarEventParticipants } =
useCalendarEventParticipants(calendarEventId);
const handleVideoRetry = () => {
setCurrentTimeSeconds(0);
refetchCalendarEventRecording();
};
const videoFileUrl = videoFile?.url ?? undefined;
return (
@@ -103,6 +107,7 @@ export const CalendarEventRecordingContent = ({
currentTimeSeconds={currentTimeSeconds}
calendarEventParticipants={calendarEventParticipants}
onVideoTimeUpdate={updateCurrentTimeSeconds}
onVideoRetry={handleVideoRetry}
/>
</StyledRecordingContentFrame>
</StyledRecordingBody>
@@ -1,20 +1,20 @@
import styled from '@emotion/styled';
import { isUndefined } from '@sniptt/guards';
import { useSelectedRecordIds } from 'twenty-sdk/front-component';
import { themeCssVariables } from 'twenty-ui/theme-constants';
import { CalendarEventSummaryContent } from 'src/front-components/components/CalendarEventSummaryContent';
import { recordingThemeCssVariables } from 'src/front-components/constants/recording-theme-css-variables';
const StyledCenteredState = styled.div`
align-items: center;
box-sizing: border-box;
color: ${recordingThemeCssVariables.font.colorTertiary};
color: ${() => themeCssVariables.font.color.tertiary};
display: flex;
font-family: ${recordingThemeCssVariables.font.family};
font-size: ${recordingThemeCssVariables.font.sizeSm};
font-family: ${() => themeCssVariables.font.family};
font-size: ${() => themeCssVariables.font.size.sm};
height: 100%;
justify-content: center;
padding: ${recordingThemeCssVariables.spacing[4]};
padding: ${() => themeCssVariables.spacing[4]};
`;
export const CalendarEventSummary = () => {
@@ -1,20 +1,20 @@
import styled from '@emotion/styled';
import { isUndefined } from '@sniptt/guards';
import { themeCssVariables } from 'twenty-ui/theme-constants';
import { SummaryMarkdown } from 'src/front-components/components/SummaryMarkdown';
import { TranscriptErrorBox } from 'src/front-components/components/TranscriptErrorBox';
import { recordingThemeCssVariables } from 'src/front-components/constants/recording-theme-css-variables';
const StyledCenteredState = styled.div`
align-items: center;
box-sizing: border-box;
color: ${recordingThemeCssVariables.font.colorTertiary};
color: ${() => themeCssVariables.font.color.tertiary};
display: flex;
font-family: ${recordingThemeCssVariables.font.family};
font-size: ${recordingThemeCssVariables.font.sizeSm};
font-family: ${() => themeCssVariables.font.family};
font-size: ${() => themeCssVariables.font.size.sm};
justify-content: center;
min-height: 240px;
padding: ${recordingThemeCssVariables.spacing[4]};
padding: ${() => themeCssVariables.spacing[4]};
`;
type CalendarEventSummaryBodyProps = {
@@ -1,16 +1,16 @@
import styled from '@emotion/styled';
import { themeCssVariables } from 'twenty-ui/theme-constants';
import { CalendarEventSummaryBody } from 'src/front-components/components/CalendarEventSummaryBody';
import { recordingThemeCssVariables } from 'src/front-components/constants/recording-theme-css-variables';
import { useCalendarEventSummary } from 'src/front-components/hooks/use-calendar-event-summary';
const StyledSummaryShell = styled.div`
background: ${recordingThemeCssVariables.background.primary};
background: ${() => themeCssVariables.background.primary};
border: 1px solid transparent;
border-radius: ${recordingThemeCssVariables.border.radiusMd};
border-radius: ${() => themeCssVariables.border.radius.md};
box-sizing: border-box;
font-family: ${recordingThemeCssVariables.font.family};
padding: ${recordingThemeCssVariables.spacing[4]};
font-family: ${() => themeCssVariables.font.family};
padding: ${() => themeCssVariables.spacing[4]};
position: relative;
width: 100%;
`;
@@ -19,31 +19,31 @@ const StyledSummaryHeader = styled.div`
align-items: center;
box-sizing: border-box;
display: flex;
height: ${recordingThemeCssVariables.spacing[6]};
height: ${() => themeCssVariables.spacing[6]};
`;
const StyledSummaryTitle = styled.h2`
color: ${recordingThemeCssVariables.font.colorPrimary};
color: ${() => themeCssVariables.font.color.primary};
flex: 1;
font-size: ${recordingThemeCssVariables.font.sizeMd};
font-weight: ${recordingThemeCssVariables.font.weightMedium};
font-size: ${() => themeCssVariables.font.size.md};
font-weight: ${() => themeCssVariables.font.weight.medium};
margin: 0;
overflow: hidden;
padding-inline: ${recordingThemeCssVariables.spacing[1]};
padding-inline: ${() => themeCssVariables.spacing[1]};
user-select: none;
`;
const StyledSummaryBody = styled.div`
box-sizing: border-box;
margin-top: ${recordingThemeCssVariables.spacing[2]};
margin-top: ${() => themeCssVariables.spacing[2]};
`;
const StyledSummaryContentFrame = styled.div`
background-color: ${recordingThemeCssVariables.background.secondary};
border: 1px solid ${recordingThemeCssVariables.border.colorMedium};
border-radius: ${recordingThemeCssVariables.border.radiusMd};
background-color: ${() => themeCssVariables.background.secondary};
border: 1px solid ${() => themeCssVariables.border.color.medium};
border-radius: ${() => themeCssVariables.border.radius.md};
box-sizing: border-box;
padding: ${recordingThemeCssVariables.spacing[3]};
padding: ${() => themeCssVariables.spacing[3]};
`;
type CalendarEventSummaryContentProps = {
@@ -0,0 +1,98 @@
import { keyframes } from '@emotion/react';
import styled from '@emotion/styled';
import { themeCssVariables } from 'twenty-ui/theme-constants';
const SKELETON_ENTRY_COUNT = 3;
const SKELETON_BAR_HEIGHT = '16px';
const SKELETON_BORDER_RADIUS = '4px';
const skeletonHighlightSweep = keyframes`
100% {
transform: translateX(100%);
}
`;
const StyledSkeletonBar = styled.div`
background-color: ${() => themeCssVariables.background.tertiary};
border-radius: ${SKELETON_BORDER_RADIUS};
height: ${SKELETON_BAR_HEIGHT};
overflow: hidden;
position: relative;
&::after {
animation: ${skeletonHighlightSweep} 1.5s ease-in-out infinite;
background-image: linear-gradient(
90deg,
transparent,
${() => themeCssVariables.background.transparent.lighter},
transparent
);
content: '';
inset: 0;
position: absolute;
transform: translateX(-100%);
}
`;
const StyledSkeletonAvatar = styled(StyledSkeletonBar)`
border-radius: 50px;
flex-shrink: 0;
width: ${SKELETON_BAR_HEIGHT};
`;
const StyledSkeletonSpeakerBar = styled(StyledSkeletonBar)`
width: 96px;
`;
const StyledSkeletonTextBar = styled(StyledSkeletonBar)<{ $width: string }>`
width: ${({ $width }) => $width};
`;
const StyledSkeletonEntry = styled.div`
box-sizing: border-box;
display: flex;
flex-direction: column;
gap: ${() => themeCssVariables.spacing[2]};
padding: ${() => themeCssVariables.spacing[2]};
width: 100%;
`;
const StyledSkeletonEntryHeader = styled.div`
align-items: center;
display: flex;
gap: ${() => themeCssVariables.spacing[2]};
min-height: ${() => themeCssVariables.spacing[6]};
`;
const StyledSkeletonContainer = styled.div`
box-sizing: border-box;
display: flex;
flex-direction: column;
gap: ${() => themeCssVariables.spacing[2]};
min-height: 240px;
width: 100%;
`;
export const RecordingSkeletonLoader = () => {
const skeletonEntries = Array.from(
{ length: SKELETON_ENTRY_COUNT },
(_, skeletonEntryIndex) => skeletonEntryIndex,
);
return (
<StyledSkeletonContainer aria-hidden="true">
{skeletonEntries.map((skeletonEntryIndex) => (
<StyledSkeletonEntry key={skeletonEntryIndex}>
<StyledSkeletonEntryHeader>
<StyledSkeletonAvatar />
<StyledSkeletonSpeakerBar />
</StyledSkeletonEntryHeader>
<StyledSkeletonTextBar $width="100%" />
<StyledSkeletonTextBar
$width={skeletonEntryIndex % 2 === 0 ? '75%' : '55%'}
/>
</StyledSkeletonEntry>
))}
</StyledSkeletonContainer>
);
};
@@ -1,10 +1,10 @@
import styled from '@emotion/styled';
import { isUndefined } from '@sniptt/guards';
import { useMemo } from 'react';
import { themeCssVariables } from 'twenty-ui/theme-constants';
import { TranscriptEntryList } from 'src/front-components/components/TranscriptEntryList';
import { TranscriptErrorBox } from 'src/front-components/components/TranscriptErrorBox';
import { recordingThemeCssVariables } from 'src/front-components/constants/recording-theme-css-variables';
import { type CalendarEventRecordingParticipant } from 'src/front-components/types/calendar-event-recording-participant.type';
import { parseTranscriptEntries } from 'src/front-components/utils/parse-transcript-entries.util';
import { parseTranscriptMarker } from 'src/logic-functions/domain/parse-transcript-marker.util';
@@ -12,10 +12,10 @@ import { isNonEmptyString } from 'src/logic-functions/utils/is-non-empty-string.
const StyledTranscriptCenteredState = styled.div`
align-items: center;
color: ${recordingThemeCssVariables.font.colorTertiary};
color: ${() => themeCssVariables.font.color.tertiary};
display: flex;
flex: 1;
font-size: ${recordingThemeCssVariables.font.sizeSm};
font-size: ${() => themeCssVariables.font.size.sm};
justify-content: center;
`;
@@ -1,52 +1,170 @@
import styled from '@emotion/styled';
import { memo, type SyntheticEvent } from 'react';
import { recordingThemeCssVariables } from 'src/front-components/constants/recording-theme-css-variables';
import { useState, type SyntheticEvent } from 'react';
import { CircularProgressBar } from 'twenty-ui/feedback';
import { themeCssVariables } from 'twenty-ui/theme-constants';
const DEFAULT_VIDEO_ASPECT_RATIO = '16 / 9';
// Forces the browser to decode the first frame under preload="metadata"
// (Safari otherwise paints nothing until play). Fragments stay client-side,
// so the signed file URL is unaffected.
const FIRST_FRAME_SEEK_FRAGMENT = '#t=0.001';
type RecordingVideoLoadState = 'awaiting-first-frame' | 'ready' | 'errored';
const StyledVideoViewport = styled.div`
aspect-ratio: ${DEFAULT_VIDEO_ASPECT_RATIO};
background: ${recordingThemeCssVariables.background.primary};
border-radius: ${recordingThemeCssVariables.border.radiusSm};
background: ${() => themeCssVariables.background.primary};
border-radius: ${() => themeCssVariables.border.radius.sm};
overflow: hidden;
position: relative;
width: 100%;
`;
const StyledVideo = styled.video`
accent-color: ${recordingThemeCssVariables.accent.primary};
background: ${recordingThemeCssVariables.background.primary};
const StyledVideo = styled.video<{ $isFirstFrameReady: boolean }>`
accent-color: ${() => themeCssVariables.accent.accent9};
background: ${() => themeCssVariables.background.primary};
color-scheme: light dark;
display: block;
height: 100%;
object-fit: contain;
opacity: ${({ $isFirstFrameReady }) => ($isFirstFrameReady ? 1 : 0)};
transition: opacity 200ms ease;
width: 100%;
@media (prefers-reduced-motion: reduce) {
transition: none;
}
`;
const StyledBufferingOverlay = styled.div<{ $isVisible: boolean }>`
align-items: center;
color: ${() => themeCssVariables.font.color.tertiary};
display: flex;
inset: 0;
justify-content: center;
opacity: ${({ $isVisible }) => ($isVisible ? 1 : 0)};
pointer-events: none;
position: absolute;
transition: opacity 150ms ease
${({ $isVisible }) => ($isVisible ? '300ms' : '0ms')};
@media (prefers-reduced-motion: reduce) {
transition-duration: 0ms;
}
`;
const StyledPlaybackErrorState = styled.div`
align-items: center;
box-sizing: border-box;
display: flex;
flex-direction: column;
gap: ${() => themeCssVariables.spacing[2]};
height: 100%;
justify-content: center;
padding: ${() => themeCssVariables.spacing[4]};
`;
const StyledPlaybackErrorTitle = styled.span`
color: ${() => themeCssVariables.font.color.primary};
font-size: ${() => themeCssVariables.font.size.sm};
font-weight: ${() => themeCssVariables.font.weight.medium};
`;
const StyledPlaybackErrorDescription = styled.span`
color: ${() => themeCssVariables.font.color.secondary};
font-size: ${() => themeCssVariables.font.size.xs};
`;
const StyledRetryButton = styled.button`
background: ${() => themeCssVariables.background.secondary};
border: 1px solid ${() => themeCssVariables.border.color.medium};
border-radius: ${() => themeCssVariables.border.radius.sm};
color: ${() => themeCssVariables.font.color.secondary};
cursor: pointer;
font-family: ${() => themeCssVariables.font.family};
font-size: ${() => themeCssVariables.font.size.sm};
font-weight: ${() => themeCssVariables.font.weight.medium};
margin-top: ${() => themeCssVariables.spacing[1]};
padding: ${() => themeCssVariables.spacing[1]}
${() => themeCssVariables.spacing[3]};
&:hover {
background: ${() => themeCssVariables.background.tertiary};
}
`;
type RecordingVideoPlayerProps = {
src: string | undefined;
src: string;
onTimeUpdate: (currentTimeSeconds: number) => void;
onRetry: () => void;
};
const RecordingVideoPlayerComponent = ({
export const RecordingVideoPlayer = ({
src,
onTimeUpdate,
onRetry,
}: RecordingVideoPlayerProps) => {
const [loadState, setLoadState] = useState<RecordingVideoLoadState>(
'awaiting-first-frame',
);
const [isPlaybackStalled, setIsPlaybackStalled] = useState(false);
const markFirstFrameReady = () => {
setLoadState((previousLoadState) =>
previousLoadState === 'awaiting-first-frame'
? 'ready'
: previousLoadState,
);
setIsPlaybackStalled(false);
};
const handleTimeUpdate = (event: SyntheticEvent<HTMLVideoElement>) => {
markFirstFrameReady();
onTimeUpdate(event.currentTarget.currentTime);
};
const handleWaiting = () => setIsPlaybackStalled(true);
const handlePause = () => setIsPlaybackStalled(false);
const handleError = () => setLoadState('errored');
if (loadState === 'errored') {
return (
<StyledVideoViewport>
<StyledPlaybackErrorState>
<StyledPlaybackErrorTitle>Playback failed</StyledPlaybackErrorTitle>
<StyledPlaybackErrorDescription>
The recording could not be loaded.
</StyledPlaybackErrorDescription>
<StyledRetryButton onClick={onRetry}>Retry</StyledRetryButton>
</StyledPlaybackErrorState>
</StyledVideoViewport>
);
}
return (
<StyledVideoViewport>
<StyledVideo
$isFirstFrameReady={loadState === 'ready'}
controls
playsInline
preload="metadata"
src={src}
src={`${src}${FIRST_FRAME_SEEK_FRAGMENT}`}
onCanPlay={markFirstFrameReady}
onError={handleError}
onLoadedData={markFirstFrameReady}
onPause={handlePause}
onSeeked={markFirstFrameReady}
onTimeUpdate={handleTimeUpdate}
onWaiting={handleWaiting}
/>
<StyledBufferingOverlay
$isVisible={loadState === 'awaiting-first-frame' || isPlaybackStalled}
>
<CircularProgressBar barWidth={3} size={24} />
</StyledBufferingOverlay>
</StyledVideoViewport>
);
};
export const RecordingVideoPlayer = memo(RecordingVideoPlayerComponent);
@@ -1,12 +1,12 @@
import styled from '@emotion/styled';
import { Fragment } from 'react';
import { themeCssVariables } from 'twenty-ui/theme-constants';
import { recordingThemeCssVariables } from 'src/front-components/constants/recording-theme-css-variables';
import { type SummaryInlineSegment } from 'src/front-components/types/summary-inline-segment.type';
const StyledBold = styled.strong`
color: ${recordingThemeCssVariables.font.colorPrimary};
font-weight: ${recordingThemeCssVariables.font.weightMedium};
color: ${() => themeCssVariables.font.color.primary};
font-weight: ${() => themeCssVariables.font.weight.medium};
`;
type SummaryInlineSegmentsProps = {
@@ -1,31 +1,31 @@
import styled from '@emotion/styled';
import { useMemo } from 'react';
import { themeCssVariables } from 'twenty-ui/theme-constants';
import { SummaryInlineSegments } from 'src/front-components/components/SummaryInlineSegments';
import { recordingThemeCssVariables } from 'src/front-components/constants/recording-theme-css-variables';
import { parseSummaryMarkdownBlocks } from 'src/front-components/utils/parse-summary-markdown-blocks.util';
const TOP_LEVEL_HEADING_MAX = 2;
const StyledSummary = styled.div`
color: ${recordingThemeCssVariables.font.colorSecondary};
color: ${() => themeCssVariables.font.color.secondary};
display: flex;
flex-direction: column;
font-family: ${recordingThemeCssVariables.font.family};
font-size: ${recordingThemeCssVariables.font.sizeSm};
gap: ${recordingThemeCssVariables.spacing[2]};
font-family: ${() => themeCssVariables.font.family};
font-size: ${() => themeCssVariables.font.size.sm};
gap: ${() => themeCssVariables.spacing[2]};
line-height: 1.5;
`;
const StyledHeading = styled.h3<{ $isTopLevel: boolean }>`
color: ${recordingThemeCssVariables.font.colorPrimary};
color: ${() => themeCssVariables.font.color.primary};
font-size: ${({ $isTopLevel }) =>
$isTopLevel
? recordingThemeCssVariables.font.sizeMd
: recordingThemeCssVariables.font.sizeSm};
font-weight: ${recordingThemeCssVariables.font.weightMedium};
? themeCssVariables.font.size.md
: themeCssVariables.font.size.sm};
font-weight: ${() => themeCssVariables.font.weight.medium};
margin: 0;
margin-top: ${recordingThemeCssVariables.spacing[1]};
margin-top: ${() => themeCssVariables.spacing[1]};
`;
const StyledParagraph = styled.p`
@@ -35,9 +35,9 @@ const StyledParagraph = styled.p`
const StyledList = styled.ul`
display: flex;
flex-direction: column;
gap: ${recordingThemeCssVariables.spacing[1]};
gap: ${() => themeCssVariables.spacing[1]};
margin: 0;
padding-left: ${recordingThemeCssVariables.spacing[3]};
padding-left: ${() => themeCssVariables.spacing[3]};
`;
type SummaryMarkdownProps = {
@@ -1,8 +1,8 @@
import styled from '@emotion/styled';
import { useMemo } from 'react';
import { themeCssVariables } from 'twenty-ui/theme-constants';
import { TranscriptEntryListItem } from 'src/front-components/components/TranscriptEntryListItem';
import { recordingThemeCssVariables } from 'src/front-components/constants/recording-theme-css-variables';
import { type CalendarEventRecordingParticipant } from 'src/front-components/types/calendar-event-recording-participant.type';
import { type TranscriptEntry } from 'src/front-components/types/transcript-entry.type';
import { buildCalendarEventParticipantBySpeakerName } from 'src/front-components/utils/build-calendar-event-participant-by-speaker-name.util';
@@ -13,7 +13,7 @@ const StyledTranscriptContainer = styled.div`
display: flex;
flex: 1;
flex-direction: column;
gap: ${recordingThemeCssVariables.spacing[2]};
gap: ${() => themeCssVariables.spacing[2]};
min-height: 0;
`;
@@ -1,8 +1,8 @@
import styled from '@emotion/styled';
import { isUndefined } from '@sniptt/guards';
import { Avatar, Chip, ChipVariant } from 'twenty-ui/data-display';
import { themeCssVariables } from 'twenty-ui/theme-constants';
import { TranscriptSpeakerChip } from 'src/front-components/components/TranscriptSpeakerChip';
import { recordingThemeCssVariables } from 'src/front-components/constants/recording-theme-css-variables';
import { type CalendarEventRecordingParticipant } from 'src/front-components/types/calendar-event-recording-participant.type';
import {
type TranscriptEntry,
@@ -14,15 +14,15 @@ const StyledEntry = styled.div<{ $isActive: boolean }>`
align-items: flex-start;
background: ${({ $isActive }) =>
$isActive
? recordingThemeCssVariables.background.transparentBlue
? themeCssVariables.background.transparent.blue
: 'transparent'};
border-radius: ${recordingThemeCssVariables.border.radiusSm};
border-radius: ${() => themeCssVariables.border.radius.sm};
box-sizing: border-box;
display: flex;
flex-direction: column;
gap: ${recordingThemeCssVariables.spacing[2]};
gap: ${() => themeCssVariables.spacing[2]};
justify-content: center;
padding: ${recordingThemeCssVariables.spacing[2]};
padding: ${() => themeCssVariables.spacing[2]};
width: 100%;
`;
@@ -30,21 +30,21 @@ const StyledEntryHeader = styled.div`
align-items: center;
align-self: stretch;
display: flex;
gap: ${recordingThemeCssVariables.spacing[2]};
min-height: ${recordingThemeCssVariables.spacing[6]};
gap: ${() => themeCssVariables.spacing[2]};
min-height: ${() => themeCssVariables.spacing[6]};
min-width: 0;
`;
const StyledTimestamp = styled.span`
color: ${recordingThemeCssVariables.font.colorTertiary};
font-size: ${recordingThemeCssVariables.font.sizeXs};
color: ${() => themeCssVariables.font.color.tertiary};
font-size: ${() => themeCssVariables.font.size.xs};
line-height: 1.4;
`;
const StyledEntryText = styled.p`
align-self: stretch;
color: ${recordingThemeCssVariables.font.colorSecondary};
font-size: ${recordingThemeCssVariables.font.sizeSm};
color: ${() => themeCssVariables.font.color.secondary};
font-size: ${() => themeCssVariables.font.size.sm};
line-height: 1.4;
margin: 0;
`;
@@ -52,8 +52,8 @@ const StyledEntryText = styled.p`
const StyledWord = styled.span<{ $isSpoken: boolean }>`
color: ${({ $isSpoken }) =>
$isSpoken
? recordingThemeCssVariables.font.colorPrimary
: recordingThemeCssVariables.font.colorSecondary};
? themeCssVariables.font.color.primary
: themeCssVariables.font.color.secondary};
line-height: 1.4;
transition: color 0.15s ease;
`;
@@ -77,11 +77,22 @@ export const TranscriptEntryListItem = ({
return (
<StyledEntry $isActive={isActive}>
<StyledEntryHeader>
<TranscriptSpeakerChip
speakerName={speakerDisplayName}
avatarUrl={calendarEventParticipant?.avatarUrl}
placeholderColorSeed={
calendarEventParticipant?.placeholderColorSeed ?? speakerDisplayName
<Chip
clickable={false}
isBold
label={speakerDisplayName}
variant={ChipVariant.Transparent}
leftComponent={
<Avatar
avatarUrl={calendarEventParticipant?.avatarUrl}
placeholder={speakerDisplayName}
placeholderColorSeed={
calendarEventParticipant?.placeholderColorSeed ??
speakerDisplayName
}
size="md"
type="rounded"
/>
}
/>
{!isUndefined(entry.startSeconds) && (
@@ -1,33 +1,32 @@
import styled from '@emotion/styled';
import { recordingThemeCssVariables } from 'src/front-components/constants/recording-theme-css-variables';
import { themeCssVariables } from 'twenty-ui/theme-constants';
const StyledStateContainer = styled.div`
box-sizing: border-box;
font-family: ${recordingThemeCssVariables.font.family};
font-family: ${() => themeCssVariables.font.family};
height: 100%;
padding: ${recordingThemeCssVariables.spacing[4]};
padding: ${() => themeCssVariables.spacing[4]};
`;
const StyledErrorBox = styled.div`
background: ${recordingThemeCssVariables.background.transparentDanger};
border: 1px solid ${recordingThemeCssVariables.border.colorDanger};
border-radius: ${recordingThemeCssVariables.border.radiusMd};
background: ${() => themeCssVariables.background.transparent.danger};
border: 1px solid ${() => themeCssVariables.border.color.danger};
border-radius: ${() => themeCssVariables.border.radius.md};
display: flex;
flex-direction: column;
gap: ${recordingThemeCssVariables.spacing[1]};
padding: ${recordingThemeCssVariables.spacing[3]};
gap: ${() => themeCssVariables.spacing[1]};
padding: ${() => themeCssVariables.spacing[3]};
`;
const StyledErrorTitle = styled.span`
color: ${recordingThemeCssVariables.font.colorDanger};
font-size: ${recordingThemeCssVariables.font.sizeSm};
font-weight: ${recordingThemeCssVariables.font.weightMedium};
color: ${() => themeCssVariables.font.color.danger};
font-size: ${() => themeCssVariables.font.size.sm};
font-weight: ${() => themeCssVariables.font.weight.medium};
`;
const StyledErrorDescription = styled.span`
color: ${recordingThemeCssVariables.font.colorSecondary};
font-size: ${recordingThemeCssVariables.font.sizeSm};
color: ${() => themeCssVariables.font.color.secondary};
font-size: ${() => themeCssVariables.font.size.sm};
`;
type TranscriptErrorBoxProps = {
@@ -1,141 +0,0 @@
// Duplicates minimal twenty-ui Avatar logic for this app.
// Remove once twenty-ui can be imported safely in front components.
import styled from '@emotion/styled';
import { useState } from 'react';
import { recordingThemeCssVariables } from 'src/front-components/constants/recording-theme-css-variables';
import { isNonEmptyString } from 'src/logic-functions/utils/is-non-empty-string.util';
const AVATAR_COLOR_NAMES = [
'red',
'ruby',
'crimson',
'tomato',
'orange',
'amber',
'yellow',
'lime',
'grass',
'green',
'jade',
'mint',
'turquoise',
'cyan',
'sky',
'blue',
'iris',
'violet',
'purple',
'plum',
'pink',
'bronze',
'gold',
'brown',
'gray',
] as const;
const StyledAvatar = styled.div<{
$backgroundColor: string;
$color: string;
}>`
align-items: center;
background: ${({ $backgroundColor }) => $backgroundColor};
border-radius: 50px;
box-sizing: border-box;
color: ${({ $color }) => $color};
display: flex;
flex-shrink: 0;
font-size: ${recordingThemeCssVariables.font.sizeXs};
font-weight: ${recordingThemeCssVariables.font.weightMedium};
height: 16px;
justify-content: center;
line-height: 15px;
overflow: hidden;
width: 16px;
`;
const StyledAvatarImage = styled.img`
height: 100%;
object-fit: cover;
width: 100%;
`;
type TranscriptSpeakerAvatarProps = {
speakerName: string;
avatarUrl: string | undefined;
placeholderColorSeed: string;
};
const getSpeakerInitial = (speakerName: string) =>
speakerName.trim().charAt(0).toUpperCase() || '-';
export const TranscriptSpeakerAvatar = ({
speakerName,
avatarUrl,
placeholderColorSeed,
}: TranscriptSpeakerAvatarProps) => {
const [erroredAvatarUrl, setErroredAvatarUrl] = useState<string | undefined>(
undefined,
);
const shouldShowAvatarImage =
isNonEmptyString(avatarUrl) && erroredAvatarUrl !== avatarUrl;
const handleAvatarImageError = () => {
if (isNonEmptyString(avatarUrl)) {
setErroredAvatarUrl(avatarUrl);
}
};
const avatarPlaceholderColor = getAvatarPlaceholderColor({
placeholderColorSeed,
variant: 12,
});
const avatarPlaceholderBackgroundColor = getAvatarPlaceholderColor({
placeholderColorSeed,
variant: 4,
});
return (
<StyledAvatar
aria-hidden="true"
$backgroundColor={avatarPlaceholderBackgroundColor}
$color={avatarPlaceholderColor}
>
{shouldShowAvatarImage ? (
<StyledAvatarImage
src={avatarUrl}
alt=""
onError={handleAvatarImageError}
/>
) : (
getSpeakerInitial(speakerName)
)}
</StyledAvatar>
);
};
const getAvatarPlaceholderColor = ({
placeholderColorSeed,
variant,
}: {
placeholderColorSeed: string;
variant: 4 | 12;
}): string => {
const avatarColorName =
AVATAR_COLOR_NAMES[
Math.abs(hashString(placeholderColorSeed)) % AVATAR_COLOR_NAMES.length
];
return `var(--t-color-${avatarColorName}${variant})`;
};
const hashString = (value: string): number => {
let hash = 0;
for (let valueIndex = 0; valueIndex < value.length; valueIndex++) {
hash = value.charCodeAt(valueIndex) + ((hash << 5) - hash);
}
return hash;
};
@@ -1,51 +0,0 @@
// Duplicates minimal twenty-ui Chip logic for this app.
// Remove once twenty-ui can be imported safely in front components.
import styled from '@emotion/styled';
import { TranscriptSpeakerAvatar } from 'src/front-components/components/TranscriptSpeakerAvatar';
import { recordingThemeCssVariables } from 'src/front-components/constants/recording-theme-css-variables';
const StyledSpeakerChip = styled.span`
align-items: center;
border-radius: ${recordingThemeCssVariables.border.radiusSm};
color: ${recordingThemeCssVariables.font.colorPrimary};
display: inline-flex;
font-size: ${recordingThemeCssVariables.font.sizeSm};
font-weight: ${recordingThemeCssVariables.font.weightMedium};
gap: ${recordingThemeCssVariables.spacing[1]};
line-height: 1.4;
max-width: 100%;
min-width: 0;
text-decoration: none;
white-space: nowrap;
`;
const StyledSpeakerName = styled.span`
flex-shrink: 1;
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
`;
type TranscriptSpeakerChipProps = {
speakerName: string;
avatarUrl: string | undefined;
placeholderColorSeed: string;
};
export const TranscriptSpeakerChip = ({
speakerName,
avatarUrl,
placeholderColorSeed,
}: TranscriptSpeakerChipProps) => {
return (
<StyledSpeakerChip>
<TranscriptSpeakerAvatar
speakerName={speakerName}
avatarUrl={avatarUrl}
placeholderColorSeed={placeholderColorSeed}
/>
<StyledSpeakerName>{speakerName}</StyledSpeakerName>
</StyledSpeakerChip>
);
};
@@ -1,40 +0,0 @@
// Avoid the SDK UI entrypoint until its bundle is safe for the browser runtime.
export const recordingThemeCssVariables = {
accent: {
primary: 'var(--t-accent-accent9)',
},
background: {
primary: 'var(--t-background-primary)',
secondary: 'var(--t-background-secondary)',
transparentBlue: 'var(--t-background-transparent-blue)',
transparentDanger: 'var(--t-background-transparent-danger)',
},
border: {
colorDanger: 'var(--t-border-color-danger)',
colorLight: 'var(--t-border-color-light)',
colorMedium: 'var(--t-border-color-medium)',
radiusMd: 'var(--t-border-radius-md)',
radiusSm: 'var(--t-border-radius-sm)',
},
boxShadow: {
light: 'var(--t-box-shadow-light)',
},
font: {
colorDanger: 'var(--t-font-color-danger)',
colorPrimary: 'var(--t-font-color-primary)',
colorSecondary: 'var(--t-font-color-secondary)',
colorTertiary: 'var(--t-font-color-tertiary)',
family: 'var(--t-font-family)',
sizeMd: 'var(--t-font-size-md)',
sizeSm: 'var(--t-font-size-sm)',
sizeXs: 'var(--t-font-size-xs)',
weightMedium: 'var(--t-font-weight-medium)',
},
spacing: {
1: 'var(--t-spacing-1)',
2: 'var(--t-spacing-2)',
3: 'var(--t-spacing-3)',
4: 'var(--t-spacing-4)',
6: 'var(--t-spacing-6)',
},
} as const;
@@ -9,6 +9,10 @@ type CalendarEventRecordingState = {
errorMessage: string | undefined;
};
type CalendarEventRecordingResult = CalendarEventRecordingState & {
refetchCalendarEventRecording: () => void;
};
type CalendarEventRecordingVideoFile = {
fileId: string;
label: string | null;
@@ -31,13 +35,18 @@ const CALENDAR_EVENT_RECORDING_ERROR_MESSAGE = 'Please try again later.';
export const useCalendarEventRecording = (
calendarEventId: string | undefined,
): CalendarEventRecordingState => {
): CalendarEventRecordingResult => {
const [state, setState] = useState<CalendarEventRecordingState>({
transcript: undefined,
videoFile: undefined,
isCalendarEventRecordingQueryLoading: !isUndefined(calendarEventId),
errorMessage: undefined,
});
const [refetchToken, setRefetchToken] = useState(0);
const refetchCalendarEventRecording = () => {
setRefetchToken((previousRefetchToken) => previousRefetchToken + 1);
};
useEffect(() => {
if (isUndefined(calendarEventId)) {
@@ -123,9 +132,9 @@ export const useCalendarEventRecording = (
return () => {
cancelled = true;
};
}, [calendarEventId]);
}, [calendarEventId, refetchToken]);
return state;
return { ...state, refetchCalendarEventRecording };
};
const hasTranscript = (
@@ -4,6 +4,6 @@
"composite": true,
"types": ["vitest/globals", "node"]
},
"include": ["src/**/*.ts", "src/**/*.tsx"],
"include": ["css.d.ts", "src/**/*.ts", "src/**/*.tsx"],
"exclude": ["node_modules", "dist"]
}
@@ -81,7 +81,7 @@ __metadata:
languageName: node
linkType: hard
"@babel/runtime@npm:^7.12.5, @babel/runtime@npm:^7.18.3":
"@babel/runtime@npm:^7.12.5, @babel/runtime@npm:^7.18.3, @babel/runtime@npm:^7.29.2":
version: 7.29.7
resolution: "@babel/runtime@npm:7.29.7"
checksum: 10c0/ca11572f7146b21e0bde6a9ed4bb6a89eafbee5f0944c7eb54d0d8a2dac962c33638a1d611e14faa71dfbb92b4b5f9236232208568a6b7d5c6f3f39ddb91771e
@@ -124,6 +124,51 @@ __metadata:
languageName: node
linkType: hard
"@base-ui/react@npm:^1.5.0":
version: 1.6.0
resolution: "@base-ui/react@npm:1.6.0"
dependencies:
"@babel/runtime": "npm:^7.29.2"
"@base-ui/utils": "npm:0.3.1"
"@floating-ui/react-dom": "npm:^2.1.8"
"@floating-ui/utils": "npm:^0.2.11"
use-sync-external-store: "npm:^1.6.0"
peerDependencies:
"@date-fns/tz": ^1.2.0
"@types/react": ^17 || ^18 || ^19
date-fns: ^4.0.0
react: ^17 || ^18 || ^19
react-dom: ^17 || ^18 || ^19
peerDependenciesMeta:
"@date-fns/tz":
optional: true
"@types/react":
optional: true
date-fns:
optional: true
checksum: 10c0/5e5994dc782710eac900d68cc0691aea70a47fabc6ef8d2ea943103d9aa96c4b44f9fd76ba65d29fa2667a5fbd7ea7a1cc02fe111c4bf639d4e3e65a1c281118
languageName: node
linkType: hard
"@base-ui/utils@npm:0.3.1":
version: 0.3.1
resolution: "@base-ui/utils@npm:0.3.1"
dependencies:
"@babel/runtime": "npm:^7.29.2"
"@floating-ui/utils": "npm:^0.2.11"
reselect: "npm:^5.2.0"
use-sync-external-store: "npm:^1.6.0"
peerDependencies:
"@types/react": ^17 || ^18 || ^19
react: ^17 || ^18 || ^19
react-dom: ^17 || ^18 || ^19
peerDependenciesMeta:
"@types/react":
optional: true
checksum: 10c0/4de858c3e2c8a4486a549d51c5e887bba6921e5a7f8db54439ba558feaf1656c3592a873928aee8a1711ee9210be3a65ca43b26713e97f649695ed2fa8c3af2c
languageName: node
linkType: hard
"@emnapi/core@npm:1.10.0":
version: 1.10.0
resolution: "@emnapi/core@npm:1.10.0"
@@ -489,6 +534,44 @@ __metadata:
languageName: node
linkType: hard
"@floating-ui/core@npm:^1.7.5":
version: 1.7.5
resolution: "@floating-ui/core@npm:1.7.5"
dependencies:
"@floating-ui/utils": "npm:^0.2.11"
checksum: 10c0/f9c52205e198b231d63a387b09c659aab08c46a1899e0b0bbe147b8b4f048b546f15ba17cb5d2a471da9534f1883d979425e13e5c4ceee67be63e4b0abd4db5d
languageName: node
linkType: hard
"@floating-ui/dom@npm:^1.7.6":
version: 1.7.6
resolution: "@floating-ui/dom@npm:1.7.6"
dependencies:
"@floating-ui/core": "npm:^1.7.5"
"@floating-ui/utils": "npm:^0.2.11"
checksum: 10c0/5c098e0d7b58c9bc769f276cca1766994c2c9c70c92d091a61bba8b3e9be53c011e0a79a8457fc2fb2f3d91697a26eb52e0a4962ef936dc963b45f58613c212f
languageName: node
linkType: hard
"@floating-ui/react-dom@npm:^2.1.8":
version: 2.1.8
resolution: "@floating-ui/react-dom@npm:2.1.8"
dependencies:
"@floating-ui/dom": "npm:^1.7.6"
peerDependencies:
react: ">=16.8.0"
react-dom: ">=16.8.0"
checksum: 10c0/26260ca4bb23b57c73b824062505abf977a008ce6e0463bdacca74f7e49853c4cd1d2bbf1a77c6caa17fa37dfffda2c6c4cd07a8737ebd7474aaff7818401d75
languageName: node
linkType: hard
"@floating-ui/utils@npm:^0.2.11":
version: 0.2.11
resolution: "@floating-ui/utils@npm:0.2.11"
checksum: 10c0/f4bcea1559bdbb721ecc8e8ead423ac58d6a5b6e70b602cf0810ba6ad4ed1c77211b207faa88b278a9042f0c743133de08a203ed6741c1b6443423332884d5b3
languageName: node
linkType: hard
"@genql/runtime@npm:^2.10.0":
version: 2.10.0
resolution: "@genql/runtime@npm:2.10.0"
@@ -1020,6 +1103,28 @@ __metadata:
languageName: node
linkType: hard
"@monaco-editor/loader@npm:^1.5.0":
version: 1.7.0
resolution: "@monaco-editor/loader@npm:1.7.0"
dependencies:
state-local: "npm:^1.0.6"
checksum: 10c0/1fd3579cb09b669493cf553dfc0723a93483140a44353ce9a739827edfa7b2c6541b254024d15c48176ece749ef666b6d16cce6cf0e4396c7fa1c5a48012d08a
languageName: node
linkType: hard
"@monaco-editor/react@npm:^4.7.0":
version: 4.7.0
resolution: "@monaco-editor/react@npm:4.7.0"
dependencies:
"@monaco-editor/loader": "npm:^1.5.0"
peerDependencies:
monaco-editor: ">= 0.25.0 < 1"
react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
checksum: 10c0/a4e91c6eda1a71f5fd23871bd801de435490ccbc43934d23cba65cefe2be7e9d02c9a57c4ef80fc9fe99e4d4deea51e5db19cb4e0ecf3f51818dda0eb9cdbf12
languageName: node
linkType: hard
"@napi-rs/wasm-runtime@npm:^1.1.4":
version: 1.1.5
resolution: "@napi-rs/wasm-runtime@npm:1.1.5"
@@ -1095,6 +1200,20 @@ __metadata:
languageName: node
linkType: hard
"@radix-ui/colors@npm:^3.0.0":
version: 3.0.0
resolution: "@radix-ui/colors@npm:3.0.0"
checksum: 10c0/612c6bb4efe987f2d49aaca2f785687d08150da59a248328bc2b4e187ece4ef731c1dc5c6a9430fffcac4af9e65cf2f89ceb9806f8809940c9eb238b79d2cf2f
languageName: node
linkType: hard
"@remix-run/router@npm:1.23.3":
version: 1.23.3
resolution: "@remix-run/router@npm:1.23.3"
checksum: 10c0/73622465dd9e9a2c5a7ced7d1151ea6e9195a8a979c99b4f70a67093eeff7f339daf03a7c6304709a9c27deb2081a8fff6737663aacb33529c1a12a0a0827a17
languageName: node
linkType: hard
"@rolldown/binding-android-arm64@npm:1.0.3":
version: 1.0.3
resolution: "@rolldown/binding-android-arm64@npm:1.0.3"
@@ -1225,6 +1344,24 @@ __metadata:
languageName: node
linkType: hard
"@tabler/icons-react@npm:^3.31.0":
version: 3.44.0
resolution: "@tabler/icons-react@npm:3.44.0"
dependencies:
"@tabler/icons": "npm:3.44.0"
peerDependencies:
react: ">= 16"
checksum: 10c0/9a4bb6f1121d001049be8bb5757160ce4d5f2430671479ee94763f88ef43a69ab8d89b835d6c6451de22f3071793b779fefeed43db49d7993de1ea65c50145c9
languageName: node
linkType: hard
"@tabler/icons@npm:3.44.0":
version: 3.44.0
resolution: "@tabler/icons@npm:3.44.0"
checksum: 10c0/0d5c1f9d6e68aa04c4a661e96035190e0884e0bc0022d31922efad3cfba210a25b2f013c0b6c0ab6aeb3d5a402a9b85a168966bd11a30ba6087f614f8521ccf3
languageName: node
linkType: hard
"@twentyhq/call-recorder@workspace:.":
version: 0.0.0-use.local
resolution: "@twentyhq/call-recorder@workspace:."
@@ -1239,8 +1376,9 @@ __metadata:
react: "npm:^19.0.0"
react-dom: "npm:^19.0.0"
sharp: "npm:^0.34.5"
twenty-client-sdk: "npm:^2.16.0"
twenty-sdk: "npm:^2.16.0"
twenty-client-sdk: "npm:^2.18.0"
twenty-sdk: "npm:^2.18.0"
twenty-ui: "npm:^1.0.0-alpha.1"
typescript: "npm:^5.9.3"
vite-tsconfig-paths: "npm:^4.2.1"
vitest: "npm:^4.1.9"
@@ -1691,6 +1829,13 @@ __metadata:
languageName: node
linkType: hard
"clsx@npm:^2.1.1":
version: 2.1.1
resolution: "clsx@npm:2.1.1"
checksum: 10c0/c4c8eb865f8c82baab07e71bfa8897c73454881c4f99d6bc81585aecd7c441746c1399d08363dc096c550cceaf97bd4ce1e8854e1771e9998d9f94c4fe075839
languageName: node
linkType: hard
"code-excerpt@npm:^4.0.0":
version: 4.0.0
resolution: "code-excerpt@npm:4.0.0"
@@ -1750,6 +1895,13 @@ __metadata:
languageName: node
linkType: hard
"css-mediaquery@npm:^0.1.2":
version: 0.1.2
resolution: "css-mediaquery@npm:0.1.2"
checksum: 10c0/b7825a78f52ce8a8198e004fcad0d7be1d3c9a0463ecd05ba31a0f2c94fb81468ad6f4d7bf715a6ca775696e7a17500c2a339b5216a6d0f789cbf78f9454d048
languageName: node
linkType: hard
"csstype@npm:^3.0.2, csstype@npm:^3.2.2":
version: 3.2.3
resolution: "csstype@npm:3.2.3"
@@ -2239,6 +2391,13 @@ __metadata:
languageName: node
linkType: hard
"hyphenate-style-name@npm:^1.0.0":
version: 1.1.0
resolution: "hyphenate-style-name@npm:1.1.0"
checksum: 10c0/bfe88deac2414a41a0d08811e277c8c098f23993d6a1eb17f14a0f11b54c4d42865a63d3cfe1914668eefb9a188e2de58f38b55a179a238fd1fef606893e194f
languageName: node
linkType: hard
"iconv-lite@npm:^0.7.2":
version: 0.7.2
resolution: "iconv-lite@npm:0.7.2"
@@ -2384,7 +2543,7 @@ __metadata:
languageName: node
linkType: hard
"js-tokens@npm:^4.0.0":
"js-tokens@npm:^3.0.0 || ^4.0.0, js-tokens@npm:^4.0.0":
version: 4.0.0
resolution: "js-tokens@npm:4.0.0"
checksum: 10c0/e248708d377aa058eacf2037b07ded847790e6de892bbad3dac0abba2e759cb9f121b00099a65195616badcb6eca8d14d975cb3e89eb1cfda644756402c8aeed
@@ -2548,6 +2707,17 @@ __metadata:
languageName: node
linkType: hard
"loose-envify@npm:^1.4.0":
version: 1.4.0
resolution: "loose-envify@npm:1.4.0"
dependencies:
js-tokens: "npm:^3.0.0 || ^4.0.0"
bin:
loose-envify: cli.js
checksum: 10c0/655d110220983c1a4b9c0c679a2e8016d4b67f6e9c7b5435ff5979ecdb20d0813f4dec0a08674fcbdd4846a3f07edbb50a36811fd37930b94aaa0d9daceb017e
languageName: node
linkType: hard
"magic-string@npm:^0.30.21":
version: 0.30.21
resolution: "magic-string@npm:0.30.21"
@@ -2557,6 +2727,15 @@ __metadata:
languageName: node
linkType: hard
"matchmediaquery@npm:^0.3.0":
version: 0.3.1
resolution: "matchmediaquery@npm:0.3.1"
dependencies:
css-mediaquery: "npm:^0.1.2"
checksum: 10c0/19a254e0a1eb639fcc6aa7428a6e2d36cb5646a165579897e771580653e470b43d7f4a224c522700dbf1d8f19fb7c63a9719e0a6017727ade967155a2ae89fea
languageName: node
linkType: hard
"math-intrinsics@npm:^1.1.0":
version: 1.1.0
resolution: "math-intrinsics@npm:1.1.0"
@@ -2671,6 +2850,13 @@ __metadata:
languageName: node
linkType: hard
"object-assign@npm:^4.1.1":
version: 4.1.1
resolution: "object-assign@npm:4.1.1"
checksum: 10c0/1f4df9945120325d041ccf7b86f31e8bcc14e73d29171e37a7903050e96b81323784ec59f93f102ec635bcf6fa8034ba3ea0a8c7e69fa202b87ae3b6cec5a414
languageName: node
linkType: hard
"obug@npm:^2.1.1":
version: 2.1.3
resolution: "obug@npm:2.1.3"
@@ -2820,6 +3006,17 @@ __metadata:
languageName: node
linkType: hard
"prop-types@npm:^15.6.1":
version: 15.8.1
resolution: "prop-types@npm:15.8.1"
dependencies:
loose-envify: "npm:^1.4.0"
object-assign: "npm:^4.1.1"
react-is: "npm:^16.13.1"
checksum: 10c0/59ece7ca2fb9838031d73a48d4becb9a7cc1ed10e610517c7d8f19a1e02fa47f7c27d557d8a5702bec3cfeccddc853579832b43f449e54635803f277b1c78077
languageName: node
linkType: hard
"proxy-from-env@npm:^2.1.0":
version: 2.1.0
resolution: "proxy-from-env@npm:2.1.0"
@@ -2838,7 +3035,7 @@ __metadata:
languageName: node
linkType: hard
"react-is@npm:^16.7.0":
"react-is@npm:^16.13.1, react-is@npm:^16.7.0":
version: 16.13.1
resolution: "react-is@npm:16.13.1"
checksum: 10c0/33977da7a5f1a287936a0c85639fec6ca74f4f15ef1e59a6bc20338fc73dc69555381e211f7a3529b8150a1f71e4225525b41b60b52965bda53ce7d47377ada1
@@ -2856,6 +3053,44 @@ __metadata:
languageName: node
linkType: hard
"react-responsive@npm:^9.0.2":
version: 9.0.2
resolution: "react-responsive@npm:9.0.2"
dependencies:
hyphenate-style-name: "npm:^1.0.0"
matchmediaquery: "npm:^0.3.0"
prop-types: "npm:^15.6.1"
shallow-equal: "npm:^1.2.1"
peerDependencies:
react: ">=16.8.0"
checksum: 10c0/c58b30d8b8ff993edbc73f7de59a2d86d8abd6380b06a9b41667c3bdd5401e5e710fa1d0fdac21368759ab9b958e8b479a111329a06e9b0241ee08f500b37a20
languageName: node
linkType: hard
"react-router-dom@npm:^6.4.4":
version: 6.30.4
resolution: "react-router-dom@npm:6.30.4"
dependencies:
"@remix-run/router": "npm:1.23.3"
react-router: "npm:6.30.4"
peerDependencies:
react: ">=16.8"
react-dom: ">=16.8"
checksum: 10c0/1b25ab26a288da852f7b58eec5c14b3f37919e6773a557cd846d3a05d7a7d890c8d49bda93c0a7f497f240df1df8b0ad50635f990aafb55f2fc545ad8269a822
languageName: node
linkType: hard
"react-router@npm:6.30.4":
version: 6.30.4
resolution: "react-router@npm:6.30.4"
dependencies:
"@remix-run/router": "npm:1.23.3"
peerDependencies:
react: ">=16.8"
checksum: 10c0/fb6de7d1002bcab9ea12c4072d93792eca494f1e4f30cfec334ccb6523756d23ce3e093c7c1241399296cebed94789a3fb89f96ee76004e0e746458a8f6bab33
languageName: node
linkType: hard
"react@npm:^19.0.0, react@npm:^19.2.0":
version: 19.2.7
resolution: "react@npm:19.2.7"
@@ -2870,6 +3105,13 @@ __metadata:
languageName: node
linkType: hard
"reselect@npm:^5.2.0":
version: 5.2.0
resolution: "reselect@npm:5.2.0"
checksum: 10c0/d0a8497e404b25a769fe792eacae88b9b576c30870450cd243d76ec9427d91a59c4a2cc00d824d283448b444c4c698a8f961d771c8ae39c759313afb31950e2e
languageName: node
linkType: hard
"resolve-from@npm:^4.0.0":
version: 4.0.0
resolution: "resolve-from@npm:4.0.0"
@@ -3021,6 +3263,13 @@ __metadata:
languageName: node
linkType: hard
"shallow-equal@npm:^1.2.1":
version: 1.2.1
resolution: "shallow-equal@npm:1.2.1"
checksum: 10c0/51e03abadd97c9ebe590547d92db9148446962a3f23a3a0fb1ba2fccab80af881eef0ff1f8ccefd3f066c0bc5a4c8ca53706194813b95c8835fa66448a843a26
languageName: node
linkType: hard
"sharp@npm:^0.34.5":
version: 0.34.5
resolution: "sharp@npm:0.34.5"
@@ -3166,6 +3415,13 @@ __metadata:
languageName: node
linkType: hard
"state-local@npm:^1.0.6":
version: 1.0.7
resolution: "state-local@npm:1.0.7"
checksum: 10c0/8dc7daeac71844452fafb514a6d6b6f40d7e2b33df398309ea1c7b3948d6110c57f112b7196500a10c54fdde40291488c52c875575670fb5c819602deca48bd9
languageName: node
linkType: hard
"std-env@npm:^4.0.0-rc.1":
version: 4.1.0
resolution: "std-env@npm:4.1.0"
@@ -3332,22 +3588,22 @@ __metadata:
languageName: node
linkType: hard
"twenty-client-sdk@npm:2.16.0, twenty-client-sdk@npm:^2.16.0":
version: 2.16.0
resolution: "twenty-client-sdk@npm:2.16.0"
"twenty-client-sdk@npm:2.18.0, twenty-client-sdk@npm:^2.18.0":
version: 2.18.0
resolution: "twenty-client-sdk@npm:2.18.0"
dependencies:
"@genql/runtime": "npm:^2.10.0"
esbuild: "npm:^0.28.1"
graphql: "npm:^16.8.1"
lodash: "npm:^4.17.21"
prettier: "npm:^3.8.3"
checksum: 10c0/f990220bec103a04ca50dc8bb38c295e94a4e51b27a3188f4a0ae515c204b76bf87f5591739eb705b97e7aa5a9aa6a215d0d4da05297e50a0392801ea64a2234
checksum: 10c0/3b7d6ca4e7b59c04cd2348af0f2ba8de153e70d565b4a38c2744b44998dfcc6c2bdb84ba6315cec208953d8c6fa7573ab33c17355f971f279299d470c57be06e
languageName: node
linkType: hard
"twenty-sdk@npm:^2.16.0":
version: 2.16.0
resolution: "twenty-sdk@npm:2.16.0"
"twenty-sdk@npm:^2.18.0":
version: 2.18.0
resolution: "twenty-sdk@npm:2.18.0"
dependencies:
"@sniptt/guards": "npm:^0.2.0"
axios: "npm:^1.16.0"
@@ -3366,12 +3622,41 @@ __metadata:
semver: "npm:7.6.3"
sharp: "npm:^0.34.5"
tinyglobby: "npm:^0.2.15"
twenty-client-sdk: "npm:2.16.0"
twenty-client-sdk: "npm:2.18.0"
typescript: "npm:^5.9.3"
uuid: "npm:^13.0.2"
bin:
twenty: dist/cli.cjs
checksum: 10c0/528f275f8bda022061a52c3563af514d2257f291fc5a232be90ca6011c50ff18a250984b6c68fc8e4c690644c92e1ccb0b89c511b8a19d832455c8860f2751a8
checksum: 10c0/7e6d300894c427029e4124047614b55988671c914332613acb38f830cee11ecc480ff2a586b6153756aab1b7fa7f4520cf072c9db1428cd9ce9b9f7a62dfcd64
languageName: node
linkType: hard
"twenty-ui@npm:^1.0.0-alpha.1":
version: 1.0.0-alpha.1
resolution: "twenty-ui@npm:1.0.0-alpha.1"
dependencies:
"@base-ui/react": "npm:^1.5.0"
"@monaco-editor/react": "npm:^4.7.0"
"@radix-ui/colors": "npm:^3.0.0"
"@sniptt/guards": "npm:^0.2.0"
"@tabler/icons-react": "npm:^3.31.0"
clsx: "npm:^2.1.1"
react-responsive: "npm:^9.0.2"
react-router-dom: "npm:^6.4.4"
type-fest: "npm:^4.10.1"
zod: "npm:^4.1.11"
peerDependencies:
monaco-editor: ">= 0.25.0 < 1"
react: ^19.0.0
react-dom: ^19.0.0
checksum: 10c0/213582e582949d6ffe90a7a8ab89a1d4e9dc724d34ed1b80bd7d50e834a8cb8539723556d41b39dc658f76f176bd8490ae0da8a3d6a713e79e6573b732084179
languageName: node
linkType: hard
"type-fest@npm:^4.10.1":
version: 4.41.0
resolution: "type-fest@npm:4.41.0"
checksum: 10c0/f5ca697797ed5e88d33ac8f1fec21921839871f808dc59345c9cf67345bfb958ce41bd821165dbf3ae591cedec2bf6fe8882098dfdd8dc54320b859711a2c1e4
languageName: node
linkType: hard
@@ -3432,6 +3717,15 @@ __metadata:
languageName: node
linkType: hard
"use-sync-external-store@npm:^1.6.0":
version: 1.6.0
resolution: "use-sync-external-store@npm:1.6.0"
peerDependencies:
react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
checksum: 10c0/35e1179f872a53227bdf8a827f7911da4c37c0f4091c29b76b1e32473d1670ebe7bcd880b808b7549ba9a5605c233350f800ffab963ee4a4ee346ee983b6019b
languageName: node
linkType: hard
"utility-types@npm:^3.10.0":
version: 3.11.0
resolution: "utility-types@npm:3.11.0"
@@ -3725,3 +4019,10 @@ __metadata:
checksum: 10c0/71cc2f2bbb537300c3f569e25693d37b3bc91f225cefce251a71c30bc6bb3e7f8e9420ca0eb57f2ac9e492b085b8dfa075fd1e8195c40b83c951dd59c6e4fbf8
languageName: node
linkType: hard
"zod@npm:^4.1.11":
version: 4.4.3
resolution: "zod@npm:4.4.3"
checksum: 10c0/7ea31b558e88f9faf44f31dd185e2e1cbf51fed3081787fb96cc2534749b50c0acfc6da7f0922a7353ed092dd358c7d50c28ea96c94d04af64191bd33152eca3
languageName: node
linkType: hard