mirror of
https://github.com/stablyai/orca.git
synced 2026-10-15 16:02:49 +00:00
Evaluate localized strings dynamically in static UI structures
- Use ES6 getters with `translate()` in static object maps (such as SSH connection status, usage ranges, and feature tour copy) to evaluate strings on demand when the language changes. - Add the `useTranslation()` hook to `LinearIssueMarkdownToolbar` so it subscribes directly to translation updates. - Extract markdown slash command primitives to a separate file to keep catalog configuration clean.
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
import React, { useCallback } from 'react'
|
||||
import type { Editor } from '@tiptap/react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import {
|
||||
Bold,
|
||||
Code,
|
||||
@@ -94,6 +95,8 @@ export function LinearIssueMarkdownToolbar({
|
||||
editor: Editor | null
|
||||
disabled: boolean
|
||||
}): React.JSX.Element {
|
||||
// Why: this toolbar can outlive editor recreation, so subscribe directly to language changes.
|
||||
useTranslation()
|
||||
const runCommand = useCallback(
|
||||
(command: (editor: Editor) => void) => {
|
||||
if (!editor || disabled) {
|
||||
|
||||
@@ -100,21 +100,50 @@ type NewWorkspaceComposerCardProps = {
|
||||
sparseControlsEnabled?: boolean
|
||||
}
|
||||
|
||||
const SSH_STATUS_LABEL_FALLBACKS: Partial<Record<SshConnectionStatus, string>> = {
|
||||
disconnected: 'SSH not connected',
|
||||
connecting: 'Connecting SSH...',
|
||||
'auth-failed': 'SSH authentication failed',
|
||||
'deploying-relay': 'Preparing SSH connection...',
|
||||
connected: 'Connected',
|
||||
reconnecting: 'Reconnecting SSH...',
|
||||
'reconnection-failed': 'SSH reconnection failed'
|
||||
const SSH_STATUS_LABELS: Partial<Record<SshConnectionStatus, string>> = {
|
||||
get disconnected() {
|
||||
return translate(
|
||||
'auto.components.NewWorkspaceComposerCard.sshNotConnected',
|
||||
'SSH not connected'
|
||||
)
|
||||
},
|
||||
get connecting() {
|
||||
return translate('auto.components.NewWorkspaceComposerCard.connectingSsh', 'Connecting SSH...')
|
||||
},
|
||||
get 'auth-failed'() {
|
||||
return translate(
|
||||
'auto.components.NewWorkspaceComposerCard.sshAuthenticationFailed',
|
||||
'SSH authentication failed'
|
||||
)
|
||||
},
|
||||
get 'deploying-relay'() {
|
||||
return translate(
|
||||
'auto.components.NewWorkspaceComposerCard.preparingSshConnection',
|
||||
'Preparing SSH connection...'
|
||||
)
|
||||
},
|
||||
get connected() {
|
||||
return translate('auto.components.NewWorkspaceComposerCard.connected', 'Connected')
|
||||
},
|
||||
get reconnecting() {
|
||||
return translate(
|
||||
'auto.components.NewWorkspaceComposerCard.reconnectingSsh',
|
||||
'Reconnecting SSH...'
|
||||
)
|
||||
},
|
||||
get 'reconnection-failed'() {
|
||||
return translate(
|
||||
'auto.components.NewWorkspaceComposerCard.sshReconnectionFailed',
|
||||
'SSH reconnection failed'
|
||||
)
|
||||
},
|
||||
get error() {
|
||||
return translate('auto.components.NewWorkspaceComposerCard.a239038146', 'SSH connection error')
|
||||
}
|
||||
}
|
||||
|
||||
function getSshStatusLabel(status: SshConnectionStatus): string {
|
||||
if (status === 'error') {
|
||||
return translate('auto.components.NewWorkspaceComposerCard.a239038146', 'SSH connection error')
|
||||
}
|
||||
return SSH_STATUS_LABEL_FALLBACKS[status] ?? status
|
||||
return SSH_STATUS_LABELS[status] ?? status
|
||||
}
|
||||
|
||||
function SetupCommandPreview({
|
||||
@@ -308,7 +337,7 @@ export default function NewWorkspaceComposerCard({
|
||||
}, [eligibleRepos, repoId])
|
||||
const sshStatusLabel = selectedRepoSshStatus
|
||||
? getSshStatusLabel(selectedRepoSshStatus)
|
||||
: 'Not connected'
|
||||
: translate('auto.components.NewWorkspaceComposerCard.notConnected', 'Not connected')
|
||||
const connectButtonLabel =
|
||||
selectedRepoSshStatus === 'disconnected' || selectedRepoSshStatus === null
|
||||
? 'Connect'
|
||||
|
||||
@@ -1,6 +1,3 @@
|
||||
import type React from 'react'
|
||||
import type { Editor } from '@tiptap/react'
|
||||
import { TextSelection } from '@tiptap/pm/state'
|
||||
import type {} from '@tiptap/extension-mathematics'
|
||||
import {
|
||||
ChevronRight,
|
||||
@@ -17,119 +14,22 @@ import {
|
||||
} from 'lucide-react'
|
||||
import { translate } from '@/i18n/i18n'
|
||||
|
||||
export type SlashMenuState = {
|
||||
query: string
|
||||
from: number
|
||||
to: number
|
||||
left: number
|
||||
top: number
|
||||
}
|
||||
import {
|
||||
icon,
|
||||
insertCodeBlock,
|
||||
insertTextWithSelection,
|
||||
insertToggle,
|
||||
textIcon,
|
||||
type SlashCommand
|
||||
} from './rich-markdown-slash-command-primitives'
|
||||
|
||||
export type SlashCommandId =
|
||||
| 'text'
|
||||
| 'toggle-text'
|
||||
| 'heading-1'
|
||||
| 'toggle-h1'
|
||||
| 'heading-2'
|
||||
| 'heading-3'
|
||||
| 'task-list'
|
||||
| 'bullet-list'
|
||||
| 'ordered-list'
|
||||
| 'blockquote'
|
||||
| 'code-block'
|
||||
| 'divider'
|
||||
| 'image'
|
||||
| 'table'
|
||||
| 'mermaid'
|
||||
| 'inline-math'
|
||||
| 'math-block'
|
||||
| 'emoji'
|
||||
|
||||
export type SlashCommandIcon =
|
||||
| { kind: 'component'; component: React.ComponentType<{ className?: string }> }
|
||||
| { kind: 'text'; value: string }
|
||||
|
||||
export type SlashCommandGroup = 'Headings' | 'Basic blocks' | 'Advanced' | 'Media' | 'Others'
|
||||
|
||||
export type SlashCommand = {
|
||||
id: SlashCommandId
|
||||
label: string
|
||||
aliases: string[]
|
||||
icon: SlashCommandIcon
|
||||
group: SlashCommandGroup
|
||||
description: string
|
||||
run: (editor: Editor) => void
|
||||
}
|
||||
|
||||
function icon(component: React.ComponentType<{ className?: string }>): SlashCommandIcon {
|
||||
return { kind: 'component', component }
|
||||
}
|
||||
|
||||
function textIcon(value: string): SlashCommandIcon {
|
||||
return { kind: 'text', value }
|
||||
}
|
||||
|
||||
function insertTextWithSelection(
|
||||
editor: Editor,
|
||||
text: string,
|
||||
selectionStartOffset?: number,
|
||||
selectionEndOffset = selectionStartOffset
|
||||
): void {
|
||||
editor.commands.command(({ state, dispatch }) => {
|
||||
const from = state.selection.from
|
||||
const tr = state.tr.insertText(text, from, state.selection.to)
|
||||
|
||||
if (selectionStartOffset !== undefined) {
|
||||
const selectionFrom = from + selectionStartOffset
|
||||
const selectionTo = from + (selectionEndOffset ?? selectionStartOffset)
|
||||
tr.setSelection(TextSelection.create(tr.doc, selectionFrom, selectionTo))
|
||||
}
|
||||
|
||||
dispatch?.(tr.scrollIntoView())
|
||||
return true
|
||||
})
|
||||
}
|
||||
|
||||
function insertCodeBlock(editor: Editor, language: string, text: string): void {
|
||||
editor.commands.command(({ state, dispatch }) => {
|
||||
const codeBlockType = state.schema.nodes.codeBlock
|
||||
if (!codeBlockType) {
|
||||
return false
|
||||
}
|
||||
const node = codeBlockType.create({ language }, text ? state.schema.text(text) : undefined)
|
||||
const tr = state.tr.replaceSelectionWith(node).scrollIntoView()
|
||||
const cursor = tr.selection.from + 1
|
||||
tr.setSelection(TextSelection.create(tr.doc, cursor, cursor))
|
||||
dispatch?.(tr)
|
||||
return true
|
||||
})
|
||||
}
|
||||
|
||||
function insertToggle(editor: Editor, variant?: 'heading-1'): void {
|
||||
const insertAt = editor.state.selection.from
|
||||
|
||||
editor
|
||||
.chain()
|
||||
.focus()
|
||||
.insertContentAt(insertAt, {
|
||||
type: 'details',
|
||||
attrs: {
|
||||
open: true,
|
||||
...(variant ? { variant } : {})
|
||||
},
|
||||
content: [
|
||||
{
|
||||
type: 'detailsSummary'
|
||||
},
|
||||
{
|
||||
type: 'detailsContent',
|
||||
content: [{ type: 'paragraph' }]
|
||||
}
|
||||
]
|
||||
})
|
||||
.setTextSelection(insertAt + 1)
|
||||
.run()
|
||||
}
|
||||
export type {
|
||||
SlashCommand,
|
||||
SlashCommandGroup,
|
||||
SlashCommandIcon,
|
||||
SlashCommandId,
|
||||
SlashMenuState
|
||||
} from './rich-markdown-slash-command-primitives'
|
||||
|
||||
export const slashCommands: SlashCommand[] = [
|
||||
{
|
||||
|
||||
@@ -0,0 +1,117 @@
|
||||
import type React from 'react'
|
||||
import type { Editor } from '@tiptap/react'
|
||||
import { TextSelection } from '@tiptap/pm/state'
|
||||
|
||||
export type SlashMenuState = {
|
||||
query: string
|
||||
from: number
|
||||
to: number
|
||||
left: number
|
||||
top: number
|
||||
}
|
||||
|
||||
export type SlashCommandId =
|
||||
| 'text'
|
||||
| 'toggle-text'
|
||||
| 'heading-1'
|
||||
| 'toggle-h1'
|
||||
| 'heading-2'
|
||||
| 'heading-3'
|
||||
| 'task-list'
|
||||
| 'bullet-list'
|
||||
| 'ordered-list'
|
||||
| 'blockquote'
|
||||
| 'code-block'
|
||||
| 'divider'
|
||||
| 'image'
|
||||
| 'table'
|
||||
| 'mermaid'
|
||||
| 'inline-math'
|
||||
| 'math-block'
|
||||
| 'emoji'
|
||||
|
||||
export type SlashCommandIcon =
|
||||
| { kind: 'component'; component: React.ComponentType<{ className?: string }> }
|
||||
| { kind: 'text'; value: string }
|
||||
|
||||
export type SlashCommandGroup = 'Headings' | 'Basic blocks' | 'Advanced' | 'Media' | 'Others'
|
||||
|
||||
export type SlashCommand = {
|
||||
id: SlashCommandId
|
||||
label: string
|
||||
aliases: string[]
|
||||
icon: SlashCommandIcon
|
||||
group: SlashCommandGroup
|
||||
description: string
|
||||
run: (editor: Editor) => void
|
||||
}
|
||||
|
||||
export function icon(component: React.ComponentType<{ className?: string }>): SlashCommandIcon {
|
||||
return { kind: 'component', component }
|
||||
}
|
||||
|
||||
export function textIcon(value: string): SlashCommandIcon {
|
||||
return { kind: 'text', value }
|
||||
}
|
||||
|
||||
export function insertTextWithSelection(
|
||||
editor: Editor,
|
||||
text: string,
|
||||
selectionStartOffset?: number,
|
||||
selectionEndOffset = selectionStartOffset
|
||||
): void {
|
||||
editor.commands.command(({ state, dispatch }) => {
|
||||
const from = state.selection.from
|
||||
const tr = state.tr.insertText(text, from, state.selection.to)
|
||||
|
||||
if (selectionStartOffset !== undefined) {
|
||||
const selectionFrom = from + selectionStartOffset
|
||||
const selectionTo = from + (selectionEndOffset ?? selectionStartOffset)
|
||||
tr.setSelection(TextSelection.create(tr.doc, selectionFrom, selectionTo))
|
||||
}
|
||||
|
||||
dispatch?.(tr.scrollIntoView())
|
||||
return true
|
||||
})
|
||||
}
|
||||
|
||||
export function insertCodeBlock(editor: Editor, language: string, text: string): void {
|
||||
editor.commands.command(({ state, dispatch }) => {
|
||||
const codeBlockType = state.schema.nodes.codeBlock
|
||||
if (!codeBlockType) {
|
||||
return false
|
||||
}
|
||||
const node = codeBlockType.create({ language }, text ? state.schema.text(text) : undefined)
|
||||
const tr = state.tr.replaceSelectionWith(node).scrollIntoView()
|
||||
const cursor = tr.selection.from + 1
|
||||
tr.setSelection(TextSelection.create(tr.doc, cursor, cursor))
|
||||
dispatch?.(tr)
|
||||
return true
|
||||
})
|
||||
}
|
||||
|
||||
export function insertToggle(editor: Editor, variant?: 'heading-1'): void {
|
||||
const insertAt = editor.state.selection.from
|
||||
|
||||
editor
|
||||
.chain()
|
||||
.focus()
|
||||
.insertContentAt(insertAt, {
|
||||
type: 'details',
|
||||
attrs: {
|
||||
open: true,
|
||||
...(variant ? { variant } : {})
|
||||
},
|
||||
content: [
|
||||
{
|
||||
type: 'detailsSummary'
|
||||
},
|
||||
{
|
||||
type: 'detailsContent',
|
||||
content: [{ type: 'paragraph' }]
|
||||
}
|
||||
]
|
||||
})
|
||||
.setTextSelection(insertAt + 1)
|
||||
.run()
|
||||
}
|
||||
@@ -1,9 +1,10 @@
|
||||
import { useEffect, useRef } from 'react'
|
||||
import { useRef } from 'react'
|
||||
import type { ComponentType, JSX, ReactNode } from 'react'
|
||||
import { Files, GitBranch, ListChecks, MessageSquare, Search } from 'lucide-react'
|
||||
import { useShortcutLabel } from '@/hooks/useShortcutLabel'
|
||||
import { ReviewPRViewVisualStyles } from './review-animated-visual-pr-view-styles'
|
||||
import { CheckTinyIcon, ChevDownIcon, CursorIcon } from './review-animated-visual-shared'
|
||||
import { useReviewPrViewAnimation } from './review-pr-view-animation'
|
||||
import { translate } from '@/i18n/i18n'
|
||||
|
||||
type SidebarTabId = 'explorer' | 'search' | 'source-control' | 'checks'
|
||||
@@ -121,20 +122,6 @@ function CommentCard(props: { index: number; path: string; children: ReactNode }
|
||||
)
|
||||
}
|
||||
|
||||
function moveCursor(
|
||||
root: HTMLElement,
|
||||
cursor: HTMLElement,
|
||||
anchor: HTMLElement,
|
||||
ox = 0,
|
||||
oy = 0
|
||||
): void {
|
||||
const rootRect = root.getBoundingClientRect()
|
||||
const anchorRect = anchor.getBoundingClientRect()
|
||||
cursor.style.transform = `translate(${anchorRect.left - rootRect.left + ox}px, ${
|
||||
anchorRect.top - rootRect.top + oy
|
||||
}px)`
|
||||
}
|
||||
|
||||
// Why: the Review PR visual follows the approved HTML mock beat-for-beat. The
|
||||
// real app keeps Explorer / Checks in one right-sidebar surface, so the
|
||||
// animation selects Checks before the PR status content appears.
|
||||
@@ -142,205 +129,7 @@ export function ReviewPRViewAnimatedVisual(props: { reducedMotion: boolean }): J
|
||||
const { reducedMotion } = props
|
||||
const rootRef = useRef<HTMLDivElement | null>(null)
|
||||
|
||||
useEffect(() => {
|
||||
const root = rootRef.current
|
||||
if (!root) {
|
||||
return
|
||||
}
|
||||
|
||||
const sidebarPeek = root.querySelector<HTMLDivElement>('[data-checks-sidebar-peek]')
|
||||
const prCard = root.querySelector<HTMLDivElement>('[data-pr-view-card]')
|
||||
const cursor = root.querySelector<HTMLDivElement>('[data-cursor]')
|
||||
const explorerTab = root.querySelector<HTMLSpanElement>('[data-explorer-tab]')
|
||||
const checksTab = root.querySelector<HTMLSpanElement>('[data-checks-tab]')
|
||||
const checksTooltip = root.querySelector<HTMLSpanElement>('[data-checks-tooltip]')
|
||||
const checksBlock = root.querySelector<HTMLDivElement>('[data-checks-block]')
|
||||
const commentsBlock = root.querySelector<HTMLDivElement>('[data-comments-block]')
|
||||
const comments = Array.from(root.querySelectorAll<HTMLDivElement>('[data-comment-card]'))
|
||||
const commentsCount = root.querySelector<HTMLSpanElement>('[data-comments-count]')
|
||||
const checkSummary = root.querySelector<HTMLDivElement>('[data-check-summary]')
|
||||
const checkSummaryLabel = root.querySelector<HTMLSpanElement>('[data-check-summary-label]')
|
||||
const checkSummaryMeta = root.querySelector<HTMLSpanElement>('[data-check-summary-meta]')
|
||||
const verifyRow = root.querySelector<HTMLDivElement>('[data-check-row="verify"]')
|
||||
const verifyState = root.querySelector<HTMLSpanElement>('[data-check-verify-state]')
|
||||
const mergeBtn = root.querySelector<HTMLButtonElement>('[data-merge-btn]')
|
||||
if (
|
||||
!sidebarPeek ||
|
||||
!prCard ||
|
||||
!cursor ||
|
||||
!explorerTab ||
|
||||
!checksTab ||
|
||||
!checksTooltip ||
|
||||
!checksBlock ||
|
||||
!commentsBlock ||
|
||||
!commentsCount ||
|
||||
!checkSummary ||
|
||||
!checkSummaryLabel ||
|
||||
!checkSummaryMeta ||
|
||||
!verifyRow ||
|
||||
!verifyState ||
|
||||
!mergeBtn
|
||||
) {
|
||||
return
|
||||
}
|
||||
|
||||
const rootEl: HTMLDivElement = root
|
||||
const sidebarPeekEl: HTMLDivElement = sidebarPeek
|
||||
const prCardEl: HTMLDivElement = prCard
|
||||
const cursorEl: HTMLDivElement = cursor
|
||||
const explorerTabEl: HTMLSpanElement = explorerTab
|
||||
const checksTabEl: HTMLSpanElement = checksTab
|
||||
const checksTooltipEl: HTMLSpanElement = checksTooltip
|
||||
const checksBlockEl: HTMLDivElement = checksBlock
|
||||
const commentsBlockEl: HTMLDivElement = commentsBlock
|
||||
const commentsCountEl: HTMLSpanElement = commentsCount
|
||||
const checkSummaryEl: HTMLDivElement = checkSummary
|
||||
const checkSummaryLabelEl: HTMLSpanElement = checkSummaryLabel
|
||||
const checkSummaryMetaEl: HTMLSpanElement = checkSummaryMeta
|
||||
const verifyRowEl: HTMLDivElement = verifyRow
|
||||
const verifyStateEl: HTMLSpanElement = verifyState
|
||||
const mergeBtnEl: HTMLButtonElement = mergeBtn
|
||||
|
||||
let cancelled = false
|
||||
const timers: number[] = []
|
||||
const wait = (ms: number): Promise<void> =>
|
||||
new Promise((resolve) => {
|
||||
const id = window.setTimeout(() => resolve(), ms)
|
||||
timers.push(id)
|
||||
})
|
||||
|
||||
function resetState(): void {
|
||||
sidebarPeekEl.classList.add('is-visible')
|
||||
sidebarPeekEl.classList.remove('is-hiding')
|
||||
prCardEl.classList.remove('is-visible')
|
||||
explorerTabEl.classList.add('is-active')
|
||||
checksTabEl.classList.remove('is-active', 'is-hovered')
|
||||
checksTooltipEl.classList.remove('is-visible')
|
||||
cursorEl.classList.remove('is-visible', 'is-clicking')
|
||||
cursorEl.style.transition = 'none'
|
||||
cursorEl.style.transform = 'translate(-30px, 220px)'
|
||||
void cursorEl.offsetWidth
|
||||
cursorEl.style.transition = ''
|
||||
checksBlockEl.classList.remove('is-visible')
|
||||
commentsBlockEl.classList.remove('is-visible')
|
||||
comments.forEach((el) => el.classList.remove('is-visible'))
|
||||
commentsCountEl.textContent = '0'
|
||||
checkSummaryEl.classList.remove('is-done')
|
||||
checkSummaryLabelEl.textContent = '1 pending'
|
||||
checkSummaryMetaEl.textContent = 'verify'
|
||||
verifyRowEl.classList.remove('is-done')
|
||||
verifyStateEl.textContent = 'Running'
|
||||
mergeBtnEl.classList.remove('is-ready')
|
||||
}
|
||||
|
||||
function showFinalState(): void {
|
||||
resetState()
|
||||
sidebarPeekEl.classList.add('is-hiding')
|
||||
prCardEl.classList.add('is-visible')
|
||||
checksBlockEl.classList.add('is-visible')
|
||||
commentsBlockEl.classList.add('is-visible')
|
||||
comments.forEach((el) => el.classList.add('is-visible'))
|
||||
commentsCountEl.textContent = String(comments.length)
|
||||
checkSummaryEl.classList.add('is-done')
|
||||
checkSummaryLabelEl.textContent = 'Checks passed'
|
||||
checkSummaryMetaEl.textContent = '3 checks'
|
||||
verifyRowEl.classList.add('is-done')
|
||||
verifyStateEl.textContent = 'Passed'
|
||||
mergeBtnEl.classList.add('is-ready')
|
||||
cursorEl.classList.remove('is-visible')
|
||||
}
|
||||
|
||||
if (reducedMotion) {
|
||||
showFinalState()
|
||||
return
|
||||
}
|
||||
|
||||
async function loop(): Promise<void> {
|
||||
while (!cancelled) {
|
||||
resetState()
|
||||
await wait(420)
|
||||
if (cancelled) {
|
||||
return
|
||||
}
|
||||
|
||||
cursorEl.classList.add('is-visible')
|
||||
moveCursor(rootEl, cursorEl, checksTabEl, 5, 6)
|
||||
checksTabEl.classList.add('is-hovered')
|
||||
await wait(260)
|
||||
if (cancelled) {
|
||||
return
|
||||
}
|
||||
checksTooltipEl.classList.add('is-visible')
|
||||
await wait(1300)
|
||||
if (cancelled) {
|
||||
return
|
||||
}
|
||||
|
||||
cursorEl.classList.add('is-clicking')
|
||||
await wait(220)
|
||||
if (cancelled) {
|
||||
return
|
||||
}
|
||||
cursorEl.classList.remove('is-clicking')
|
||||
checksTooltipEl.classList.remove('is-visible')
|
||||
checksTabEl.classList.remove('is-hovered')
|
||||
explorerTabEl.classList.remove('is-active')
|
||||
checksTabEl.classList.add('is-active')
|
||||
await wait(420)
|
||||
if (cancelled) {
|
||||
return
|
||||
}
|
||||
|
||||
sidebarPeekEl.classList.add('is-hiding')
|
||||
prCardEl.classList.add('is-visible')
|
||||
cursorEl.classList.remove('is-visible')
|
||||
await wait(560)
|
||||
if (cancelled) {
|
||||
return
|
||||
}
|
||||
|
||||
checksBlockEl.classList.add('is-visible')
|
||||
await wait(1050)
|
||||
if (cancelled) {
|
||||
return
|
||||
}
|
||||
|
||||
verifyRowEl.classList.add('is-done')
|
||||
verifyStateEl.textContent = 'Passed'
|
||||
checkSummaryEl.classList.add('is-done')
|
||||
checkSummaryLabelEl.textContent = 'Checks passed'
|
||||
checkSummaryMetaEl.textContent = '3 checks'
|
||||
mergeBtnEl.classList.add('is-ready')
|
||||
|
||||
await wait(560)
|
||||
if (cancelled) {
|
||||
return
|
||||
}
|
||||
commentsBlockEl.classList.add('is-visible')
|
||||
await wait(260)
|
||||
if (cancelled) {
|
||||
return
|
||||
}
|
||||
|
||||
for (let i = 0; i < comments.length; i++) {
|
||||
comments[i]?.classList.add('is-visible')
|
||||
commentsCountEl.textContent = String(i + 1)
|
||||
await wait(520)
|
||||
if (cancelled) {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
await wait(2900)
|
||||
}
|
||||
}
|
||||
|
||||
void loop()
|
||||
return () => {
|
||||
cancelled = true
|
||||
timers.forEach((timer) => window.clearTimeout(timer))
|
||||
}
|
||||
}, [reducedMotion])
|
||||
useReviewPrViewAnimation(rootRef, reducedMotion)
|
||||
|
||||
return (
|
||||
<div ref={rootRef} className="ravpr-stage" data-page="pr-view">
|
||||
|
||||
@@ -17,8 +17,12 @@ export const FEATURE_TOUR_PREVIEW_COPY: readonly FeatureTourPreviewFrameCopy[] =
|
||||
'Isolated workspaces'
|
||||
)
|
||||
},
|
||||
caption:
|
||||
'Ship several things at once. Each workspace keeps its branch, terminal, and agent activity together.'
|
||||
get caption() {
|
||||
return translate(
|
||||
'auto.components.feature.wall.FeatureTourPreview.47f16ecf34',
|
||||
'Ship several things at once. Each workspace keeps its branch, terminal, and agent activity together.'
|
||||
)
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
@@ -28,7 +32,12 @@ export const FEATURE_TOUR_PREVIEW_COPY: readonly FeatureTourPreviewFrameCopy[] =
|
||||
'Agent orchestration'
|
||||
)
|
||||
},
|
||||
caption: 'Hand off a goal and walk away. A coordinator agent fans out and ships parallel PRs.'
|
||||
get caption() {
|
||||
return translate(
|
||||
'auto.components.feature.wall.FeatureTourPreview.70aa182266',
|
||||
'Hand off a goal and walk away. A coordinator agent fans out and ships parallel PRs.'
|
||||
)
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
@@ -38,8 +47,12 @@ export const FEATURE_TOUR_PREVIEW_COPY: readonly FeatureTourPreviewFrameCopy[] =
|
||||
'GitHub & Linear tasks'
|
||||
)
|
||||
},
|
||||
caption:
|
||||
'Skip the tab-switching. Pick from your GitHub or Linear backlog and start a workspace in one click.'
|
||||
get caption() {
|
||||
return translate(
|
||||
'auto.components.feature.wall.FeatureTourPreview.f10c14dd9d',
|
||||
'Skip the tab-switching. Pick from your GitHub or Linear backlog and start a workspace in one click.'
|
||||
)
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
@@ -49,8 +62,12 @@ export const FEATURE_TOUR_PREVIEW_COPY: readonly FeatureTourPreviewFrameCopy[] =
|
||||
'Splittable terminal'
|
||||
)
|
||||
},
|
||||
caption:
|
||||
'Open any workspace to return to its terminal, then split panes for tests, logs, and agents.'
|
||||
get caption() {
|
||||
return translate(
|
||||
'auto.components.feature.wall.FeatureTourPreview.5d6ee181b6',
|
||||
'Open any workspace to return to its terminal, then split panes for tests, logs, and agents.'
|
||||
)
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
|
||||
@@ -0,0 +1,261 @@
|
||||
import { useEffect } from 'react'
|
||||
import type { RefObject } from 'react'
|
||||
import { translate } from '@/i18n/i18n'
|
||||
|
||||
function getReviewPrAnimatedStatusCopy(): {
|
||||
pendingLabel: string
|
||||
verifyLabel: string
|
||||
runningLabel: string
|
||||
checksPassedLabel: string
|
||||
checksCountLabel: string
|
||||
passedLabel: string
|
||||
} {
|
||||
return {
|
||||
pendingLabel: translate(
|
||||
'auto.components.feature.wall.ReviewPRViewAnimatedVisual.9a097cae12',
|
||||
'1 pending'
|
||||
),
|
||||
verifyLabel: translate(
|
||||
'auto.components.feature.wall.ReviewPRViewAnimatedVisual.d340c052fb',
|
||||
'verify'
|
||||
),
|
||||
runningLabel: translate(
|
||||
'auto.components.feature.wall.ReviewPRViewAnimatedVisual.8ed213397c',
|
||||
'Running'
|
||||
),
|
||||
checksPassedLabel: translate(
|
||||
'auto.components.feature.wall.ReviewPRViewAnimatedVisual.a6c8b9e32f',
|
||||
'Checks passed'
|
||||
),
|
||||
checksCountLabel: translate(
|
||||
'auto.components.feature.wall.ReviewPRViewAnimatedVisual.f4d5e1a7b2',
|
||||
'3 checks'
|
||||
),
|
||||
passedLabel: translate(
|
||||
'auto.components.feature.wall.ReviewPRViewAnimatedVisual.ca36f7b27c',
|
||||
'Passed'
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
function moveCursor(
|
||||
root: HTMLElement,
|
||||
cursor: HTMLElement,
|
||||
anchor: HTMLElement,
|
||||
ox = 0,
|
||||
oy = 0
|
||||
): void {
|
||||
const rootRect = root.getBoundingClientRect()
|
||||
const anchorRect = anchor.getBoundingClientRect()
|
||||
cursor.style.transform = `translate(${anchorRect.left - rootRect.left + ox}px, ${
|
||||
anchorRect.top - rootRect.top + oy
|
||||
}px)`
|
||||
}
|
||||
|
||||
export function useReviewPrViewAnimation(
|
||||
rootRef: RefObject<HTMLDivElement | null>,
|
||||
reducedMotion: boolean
|
||||
): void {
|
||||
useEffect(() => {
|
||||
const root = rootRef.current
|
||||
if (!root) {
|
||||
return
|
||||
}
|
||||
|
||||
const sidebarPeek = root.querySelector<HTMLDivElement>('[data-checks-sidebar-peek]')
|
||||
const prCard = root.querySelector<HTMLDivElement>('[data-pr-view-card]')
|
||||
const cursor = root.querySelector<HTMLDivElement>('[data-cursor]')
|
||||
const explorerTab = root.querySelector<HTMLSpanElement>('[data-explorer-tab]')
|
||||
const checksTab = root.querySelector<HTMLSpanElement>('[data-checks-tab]')
|
||||
const checksTooltip = root.querySelector<HTMLSpanElement>('[data-checks-tooltip]')
|
||||
const checksBlock = root.querySelector<HTMLDivElement>('[data-checks-block]')
|
||||
const commentsBlock = root.querySelector<HTMLDivElement>('[data-comments-block]')
|
||||
const comments = Array.from(root.querySelectorAll<HTMLDivElement>('[data-comment-card]'))
|
||||
const commentsCount = root.querySelector<HTMLSpanElement>('[data-comments-count]')
|
||||
const checkSummary = root.querySelector<HTMLDivElement>('[data-check-summary]')
|
||||
const checkSummaryLabel = root.querySelector<HTMLSpanElement>('[data-check-summary-label]')
|
||||
const checkSummaryMeta = root.querySelector<HTMLSpanElement>('[data-check-summary-meta]')
|
||||
const verifyRow = root.querySelector<HTMLDivElement>('[data-check-row="verify"]')
|
||||
const verifyState = root.querySelector<HTMLSpanElement>('[data-check-verify-state]')
|
||||
const mergeBtn = root.querySelector<HTMLButtonElement>('[data-merge-btn]')
|
||||
if (
|
||||
!sidebarPeek ||
|
||||
!prCard ||
|
||||
!cursor ||
|
||||
!explorerTab ||
|
||||
!checksTab ||
|
||||
!checksTooltip ||
|
||||
!checksBlock ||
|
||||
!commentsBlock ||
|
||||
!commentsCount ||
|
||||
!checkSummary ||
|
||||
!checkSummaryLabel ||
|
||||
!checkSummaryMeta ||
|
||||
!verifyRow ||
|
||||
!verifyState ||
|
||||
!mergeBtn
|
||||
) {
|
||||
return
|
||||
}
|
||||
|
||||
const rootEl: HTMLDivElement = root
|
||||
const sidebarPeekEl: HTMLDivElement = sidebarPeek
|
||||
const prCardEl: HTMLDivElement = prCard
|
||||
const cursorEl: HTMLDivElement = cursor
|
||||
const explorerTabEl: HTMLSpanElement = explorerTab
|
||||
const checksTabEl: HTMLSpanElement = checksTab
|
||||
const checksTooltipEl: HTMLSpanElement = checksTooltip
|
||||
const checksBlockEl: HTMLDivElement = checksBlock
|
||||
const commentsBlockEl: HTMLDivElement = commentsBlock
|
||||
const commentsCountEl: HTMLSpanElement = commentsCount
|
||||
const checkSummaryEl: HTMLDivElement = checkSummary
|
||||
const checkSummaryLabelEl: HTMLSpanElement = checkSummaryLabel
|
||||
const checkSummaryMetaEl: HTMLSpanElement = checkSummaryMeta
|
||||
const verifyRowEl: HTMLDivElement = verifyRow
|
||||
const verifyStateEl: HTMLSpanElement = verifyState
|
||||
const mergeBtnEl: HTMLButtonElement = mergeBtn
|
||||
|
||||
let cancelled = false
|
||||
const timers: number[] = []
|
||||
const wait = (ms: number): Promise<void> =>
|
||||
new Promise((resolve) => {
|
||||
const id = window.setTimeout(() => resolve(), ms)
|
||||
timers.push(id)
|
||||
})
|
||||
|
||||
function resetState(): void {
|
||||
sidebarPeekEl.classList.add('is-visible')
|
||||
sidebarPeekEl.classList.remove('is-hiding')
|
||||
prCardEl.classList.remove('is-visible')
|
||||
explorerTabEl.classList.add('is-active')
|
||||
checksTabEl.classList.remove('is-active', 'is-hovered')
|
||||
checksTooltipEl.classList.remove('is-visible')
|
||||
cursorEl.classList.remove('is-visible', 'is-clicking')
|
||||
cursorEl.style.transition = 'none'
|
||||
cursorEl.style.transform = 'translate(-30px, 220px)'
|
||||
void cursorEl.offsetWidth
|
||||
cursorEl.style.transition = ''
|
||||
checksBlockEl.classList.remove('is-visible')
|
||||
commentsBlockEl.classList.remove('is-visible')
|
||||
comments.forEach((el) => el.classList.remove('is-visible'))
|
||||
commentsCountEl.textContent = '0'
|
||||
checkSummaryEl.classList.remove('is-done')
|
||||
const copy = getReviewPrAnimatedStatusCopy()
|
||||
checkSummaryLabelEl.textContent = copy.pendingLabel
|
||||
checkSummaryMetaEl.textContent = copy.verifyLabel
|
||||
verifyRowEl.classList.remove('is-done')
|
||||
verifyStateEl.textContent = copy.runningLabel
|
||||
mergeBtnEl.classList.remove('is-ready')
|
||||
}
|
||||
|
||||
function showFinalState(): void {
|
||||
resetState()
|
||||
sidebarPeekEl.classList.add('is-hiding')
|
||||
prCardEl.classList.add('is-visible')
|
||||
checksBlockEl.classList.add('is-visible')
|
||||
commentsBlockEl.classList.add('is-visible')
|
||||
comments.forEach((el) => el.classList.add('is-visible'))
|
||||
commentsCountEl.textContent = String(comments.length)
|
||||
checkSummaryEl.classList.add('is-done')
|
||||
const copy = getReviewPrAnimatedStatusCopy()
|
||||
checkSummaryLabelEl.textContent = copy.checksPassedLabel
|
||||
checkSummaryMetaEl.textContent = copy.checksCountLabel
|
||||
verifyRowEl.classList.add('is-done')
|
||||
verifyStateEl.textContent = copy.passedLabel
|
||||
mergeBtnEl.classList.add('is-ready')
|
||||
cursorEl.classList.remove('is-visible')
|
||||
}
|
||||
|
||||
if (reducedMotion) {
|
||||
showFinalState()
|
||||
return
|
||||
}
|
||||
|
||||
async function loop(): Promise<void> {
|
||||
while (!cancelled) {
|
||||
resetState()
|
||||
await wait(420)
|
||||
if (cancelled) {
|
||||
return
|
||||
}
|
||||
|
||||
cursorEl.classList.add('is-visible')
|
||||
moveCursor(rootEl, cursorEl, checksTabEl, 5, 6)
|
||||
checksTabEl.classList.add('is-hovered')
|
||||
await wait(260)
|
||||
if (cancelled) {
|
||||
return
|
||||
}
|
||||
checksTooltipEl.classList.add('is-visible')
|
||||
await wait(1300)
|
||||
if (cancelled) {
|
||||
return
|
||||
}
|
||||
|
||||
cursorEl.classList.add('is-clicking')
|
||||
await wait(220)
|
||||
if (cancelled) {
|
||||
return
|
||||
}
|
||||
cursorEl.classList.remove('is-clicking')
|
||||
checksTooltipEl.classList.remove('is-visible')
|
||||
checksTabEl.classList.remove('is-hovered')
|
||||
explorerTabEl.classList.remove('is-active')
|
||||
checksTabEl.classList.add('is-active')
|
||||
await wait(420)
|
||||
if (cancelled) {
|
||||
return
|
||||
}
|
||||
|
||||
sidebarPeekEl.classList.add('is-hiding')
|
||||
prCardEl.classList.add('is-visible')
|
||||
cursorEl.classList.remove('is-visible')
|
||||
await wait(560)
|
||||
if (cancelled) {
|
||||
return
|
||||
}
|
||||
|
||||
checksBlockEl.classList.add('is-visible')
|
||||
await wait(1050)
|
||||
if (cancelled) {
|
||||
return
|
||||
}
|
||||
|
||||
verifyRowEl.classList.add('is-done')
|
||||
const copy = getReviewPrAnimatedStatusCopy()
|
||||
verifyStateEl.textContent = copy.passedLabel
|
||||
checkSummaryEl.classList.add('is-done')
|
||||
checkSummaryLabelEl.textContent = copy.checksPassedLabel
|
||||
checkSummaryMetaEl.textContent = copy.checksCountLabel
|
||||
mergeBtnEl.classList.add('is-ready')
|
||||
|
||||
await wait(560)
|
||||
if (cancelled) {
|
||||
return
|
||||
}
|
||||
commentsBlockEl.classList.add('is-visible')
|
||||
await wait(260)
|
||||
if (cancelled) {
|
||||
return
|
||||
}
|
||||
|
||||
for (let i = 0; i < comments.length; i++) {
|
||||
comments[i]?.classList.add('is-visible')
|
||||
commentsCountEl.textContent = String(i + 1)
|
||||
await wait(520)
|
||||
if (cancelled) {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
await wait(2900)
|
||||
}
|
||||
}
|
||||
|
||||
void loop()
|
||||
return () => {
|
||||
cancelled = true
|
||||
timers.forEach((timer) => window.clearTimeout(timer))
|
||||
}
|
||||
}, [reducedMotion, rootRef])
|
||||
}
|
||||
@@ -54,7 +54,7 @@ const stepCopy = {
|
||||
get subtitle() {
|
||||
return translate(
|
||||
'auto.components.onboarding.OnboardingFlow.ff92d15436',
|
||||
'Orca will notify you know when agents are done or need help.'
|
||||
'Orca will notify you when agents are done or need help.'
|
||||
)
|
||||
}
|
||||
},
|
||||
|
||||
@@ -142,19 +142,7 @@ export function SourceControlAiActionRecipeDefaults({
|
||||
<SearchableSetting
|
||||
title={ACTION_RECIPES_SEARCH_ENTRY.title}
|
||||
description={ACTION_RECIPES_SEARCH_ENTRY.description}
|
||||
keywords={[
|
||||
'agent',
|
||||
'arguments',
|
||||
'args',
|
||||
'cli',
|
||||
'command',
|
||||
'model',
|
||||
'template',
|
||||
'fix',
|
||||
'checks',
|
||||
'commit',
|
||||
'pull request'
|
||||
]}
|
||||
keywords={ACTION_RECIPES_SEARCH_ENTRY.keywords}
|
||||
className="space-y-3 px-1 py-2"
|
||||
>
|
||||
<div className="space-y-0.5">
|
||||
|
||||
@@ -24,16 +24,36 @@ type SshDisconnectedDialogProps = {
|
||||
}
|
||||
|
||||
const STATUS_MESSAGES: Partial<Record<SshConnectionStatus, string>> = {
|
||||
disconnected: 'This remote repository is not connected.',
|
||||
reconnecting: 'Reconnecting to the remote host...',
|
||||
'reconnection-failed': 'Reconnection to the remote host failed.',
|
||||
get disconnected() {
|
||||
return translate(
|
||||
'auto.components.sidebar.SshDisconnectedDialog.disconnected',
|
||||
'This remote repository is not connected.'
|
||||
)
|
||||
},
|
||||
get reconnecting() {
|
||||
return translate(
|
||||
'auto.components.sidebar.SshDisconnectedDialog.reconnecting',
|
||||
'Reconnecting to the remote host...'
|
||||
)
|
||||
},
|
||||
get 'reconnection-failed'() {
|
||||
return translate(
|
||||
'auto.components.sidebar.SshDisconnectedDialog.reconnectionFailed',
|
||||
'Reconnection to the remote host failed.'
|
||||
)
|
||||
},
|
||||
get error() {
|
||||
return translate(
|
||||
'auto.components.sidebar.SshDisconnectedDialog.376bed88e5',
|
||||
'The connection to the remote host encountered an error.'
|
||||
)
|
||||
},
|
||||
'auth-failed': 'Authentication to the remote host failed.'
|
||||
get 'auth-failed'() {
|
||||
return translate(
|
||||
'auto.components.sidebar.SshDisconnectedDialog.authFailed',
|
||||
'Authentication to the remote host failed.'
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
function isReconnectable(status: SshConnectionStatus): boolean {
|
||||
@@ -78,9 +98,21 @@ export function SshDisconnectedDialog({
|
||||
status === 'connecting' ||
|
||||
status === 'deploying-relay' ||
|
||||
status === 'reconnecting'
|
||||
const reconnectingMessage =
|
||||
STATUS_MESSAGES.reconnecting ??
|
||||
translate(
|
||||
'auto.components.sidebar.SshDisconnectedDialog.reconnecting',
|
||||
'Reconnecting to the remote host...'
|
||||
)
|
||||
const disconnectedMessage =
|
||||
STATUS_MESSAGES.disconnected ??
|
||||
translate(
|
||||
'auto.components.sidebar.SshDisconnectedDialog.disconnected',
|
||||
'This remote repository is not connected.'
|
||||
)
|
||||
const message = isConnecting
|
||||
? 'Reconnecting to the remote host...'
|
||||
: (STATUS_MESSAGES[status] ?? 'This remote repository is not connected.')
|
||||
? reconnectingMessage
|
||||
: (STATUS_MESSAGES[status] ?? disconnectedMessage)
|
||||
const showReconnect = isReconnectable(status)
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
@@ -45,10 +45,18 @@ const SCOPE_OPTIONS: { value: CodexUsageScope; label: string }[] = [
|
||||
}
|
||||
]
|
||||
const RANGE_LABELS: Record<CodexUsageRange, string> = {
|
||||
'7d': 'Last 7 days',
|
||||
'30d': 'Last 30 days',
|
||||
'90d': 'Last 90 days',
|
||||
all: 'All time'
|
||||
get '7d'() {
|
||||
return translate('auto.components.stats.CodexUsagePane.rangeLast7Days', 'Last 7 days')
|
||||
},
|
||||
get '30d'() {
|
||||
return translate('auto.components.stats.CodexUsagePane.rangeLast30Days', 'Last 30 days')
|
||||
},
|
||||
get '90d'() {
|
||||
return translate('auto.components.stats.CodexUsagePane.rangeLast90Days', 'Last 90 days')
|
||||
},
|
||||
get all() {
|
||||
return translate('auto.components.stats.CodexUsagePane.rangeAllTime', 'All time')
|
||||
}
|
||||
}
|
||||
|
||||
export function CodexUsagePane(): React.JSX.Element {
|
||||
|
||||
@@ -50,10 +50,18 @@ const SCOPE_OPTIONS: { value: OpenCodeUsageScope; label: string }[] = [
|
||||
}
|
||||
]
|
||||
const RANGE_LABELS: Record<OpenCodeUsageRange, string> = {
|
||||
'7d': 'Last 7 days',
|
||||
'30d': 'Last 30 days',
|
||||
'90d': 'Last 90 days',
|
||||
all: 'All time'
|
||||
get '7d'() {
|
||||
return translate('auto.components.stats.OpenCodeUsagePane.rangeLast7Days', 'Last 7 days')
|
||||
},
|
||||
get '30d'() {
|
||||
return translate('auto.components.stats.OpenCodeUsagePane.rangeLast30Days', 'Last 30 days')
|
||||
},
|
||||
get '90d'() {
|
||||
return translate('auto.components.stats.OpenCodeUsagePane.rangeLast90Days', 'Last 90 days')
|
||||
},
|
||||
get all() {
|
||||
return translate('auto.components.stats.OpenCodeUsagePane.rangeAllTime', 'All time')
|
||||
}
|
||||
}
|
||||
|
||||
export function OpenCodeUsagePane(): React.JSX.Element {
|
||||
|
||||
Reference in New Issue
Block a user