Files
orca/mobile/src/components/mobile-markdown-styles.ts
Jinjing d1ae177242 feat(mobile): floating answer card, queued messages above it (#41611)
* feat(mobile): floating answer card, queued messages above it

Port of stablyai/orca-private#724 (e1a794e6), plus:
- keep orca #40820's filled Steer pill on the lit head row
- let the open queue list shrink and scroll under a height limit so
  the Show less row stays whole at 320pt on a short screen

* test(mobile): pin the short-screen queue shrink at narrow width and on the box

* fix(mobile): scroll a failed queued message into view and keep the lit Steer pill's press

- A failure that opens the queue scrolls its row into view, so on a short
  screen the reason the list opened is visible above Show less.
- The pressed opacity comes after the lit fill on the Steer pill.
- Test the lit pill (orca #40820) and the failure scroll.

* fix(mobile): place a failed queued row only from its fresh height and let stale requests lapse

- The failure scroll waits for the failed row to lay out again, then settles
  on the known heights after 300ms, so a stale height or an unresolved
  request never moves the list later.
- A deferred show expires after 1s and on a queue change; the inline
  editor's reveal scrolls at once again.

* refactor(mobile): keep the failure-scroll request as the queue list's own state

React Doctor reads a hook-returned setter called during render as a prop
callback; the list now owns the request with useState and hands it to the hook.

* fix(mobile): address review threads on the queued list

- Fold again for a later prompt: an open choice made in one placement no
  longer comes back in the next.
- Scroll a message the person queues into a folded list into view through
  the same deferred show as a failure.
- Lift from the row only in the plain presentation; a settings row keeps
  its grip gesture.

* fix(mobile): let a choice made in the render a prompt arrives win over the placement reset

* fix(mobile): count an open inline editor's height when placing a later queued row

* test(mobile): note why the inline-editor test stub is cast
2026-11-12 11:11:28 -08:00

296 lines
7.6 KiB
TypeScript

import { StyleSheet, type TextStyle } from 'react-native'
import { colors, radii, spacing, typography } from '../theme/mobile-theme'
import { themedStyleSheet, themedStyles } from '../theme/themed-styles'
/** Lifts the 13pt base to 14pt body text for documents (file preview, PR bodies); headings scale with it. */
export const MOBILE_MARKDOWN_DOCUMENT_SCALE = 14 / 13
export const styles = themedStyleSheet(() => ({
root: {
gap: spacing.md
},
paragraph: {
fontSize: 13,
lineHeight: 21,
color: colors.textPrimary
},
heading: {
fontSize: 14,
lineHeight: 21,
fontWeight: '700',
color: colors.textPrimary
},
headingLarge: {
fontSize: 16,
lineHeight: 23
},
headingTitle: {
fontSize: 18,
lineHeight: 25
},
bold: {
fontWeight: '700',
color: colors.textPrimary
},
italic: {
fontStyle: 'italic'
},
strike: {
textDecorationLine: 'line-through'
},
link: {
color: colors.accentBlue,
textDecorationLine: 'underline'
},
inlineCode: {
fontFamily: typography.monoFamily,
fontSize: 12,
color: colors.textPrimary,
backgroundColor: colors.bgRaised,
borderWidth: StyleSheet.hairlineWidth,
borderColor: colors.borderSubtle,
borderRadius: radii.mark,
paddingHorizontal: 4
},
// GitHub's inline tags: raised and lowered small text, a key cap, an insertion.
sup: { fontSize: 10, position: 'relative', top: -5 },
sub: { fontSize: 10, position: 'relative', top: 3 },
kbd: { borderWidth: 1, borderColor: colors.borderSubtle },
ins: { textDecorationLine: 'underline' },
inlineCodeLink: {
color: colors.accentBlue,
textDecorationLine: 'underline'
},
callout: {
borderLeftWidth: 2,
borderTopRightRadius: radii.surface,
borderBottomRightRadius: radii.surface,
backgroundColor: colors.bgPanel,
paddingHorizontal: spacing.md,
paddingVertical: spacing.sm,
gap: spacing.sm
},
calloutTitle: { flexDirection: 'row', alignItems: 'center', gap: spacing.xs },
calloutTitleText: { fontSize: 13, fontWeight: '600' },
quote: {
borderLeftWidth: 2,
borderLeftColor: colors.borderSubtle,
borderTopRightRadius: radii.surface,
borderBottomRightRadius: radii.surface,
backgroundColor: colors.bgPanel,
paddingHorizontal: spacing.md,
paddingVertical: spacing.sm,
gap: spacing.sm
},
quoteText: {
fontSize: 13,
lineHeight: 21,
color: colors.textSecondary
},
codeBlock: {
backgroundColor: colors.bgRaised,
borderWidth: 1,
borderColor: colors.borderSubtle,
borderRadius: radii.surface,
overflow: 'hidden'
},
codeHeader: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
gap: spacing.sm,
minHeight: 32,
paddingLeft: spacing.md,
paddingRight: spacing.xs,
borderBottomWidth: StyleSheet.hairlineWidth,
borderBottomColor: colors.borderSubtle
},
codeLanguage: {
flexShrink: 1,
fontSize: typography.metaSize,
color: colors.textSecondary,
textTransform: 'uppercase'
},
codeBody: {
flexDirection: 'row',
paddingVertical: spacing.sm
},
codeLineNumbers: {
paddingHorizontal: spacing.sm,
color: colors.textMuted,
textAlign: 'right'
},
codeLines: {
paddingRight: spacing.md
},
codeLinesWrapped: {
flex: 1,
paddingLeft: spacing.md
},
codeActions: {
flexDirection: 'row',
alignItems: 'center',
gap: spacing.xs
},
codeWrapButton: {
width: 24,
height: 24,
alignItems: 'center',
justifyContent: 'center',
borderRadius: radii.control
},
codeWrapButtonOn: {
backgroundColor: colors.translucentFill
},
codeText: {
fontFamily: typography.monoFamily,
fontSize: 12,
lineHeight: 17,
color: colors.textPrimary
},
imageParagraph: {
gap: spacing.sm
},
details: {
borderWidth: StyleSheet.hairlineWidth,
borderColor: colors.borderSubtle,
borderRadius: radii.surface,
overflow: 'hidden'
},
detailsSummary: {
flexDirection: 'row',
alignItems: 'center',
gap: spacing.xs,
paddingHorizontal: spacing.sm,
paddingVertical: spacing.xs,
backgroundColor: colors.bgRaised
},
detailsSummaryText: { fontWeight: '600', flexShrink: 1 },
detailsBody: { padding: spacing.sm, gap: spacing.md },
imageFrame: {
borderWidth: 1,
borderColor: colors.borderSubtle,
borderRadius: radii.surface,
backgroundColor: colors.bgRaised,
overflow: 'hidden',
padding: spacing.sm
},
imageCaption: {
paddingHorizontal: spacing.sm,
paddingVertical: spacing.xs,
fontSize: 11,
color: colors.textSecondary
},
tableScroll: {
minWidth: '100%'
},
table: {
flexGrow: 1,
borderTopWidth: 1,
borderLeftWidth: 1,
borderColor: colors.borderSubtle,
borderRadius: radii.surface,
overflow: 'hidden',
backgroundColor: colors.bgPanel
},
tableRow: {
flexDirection: 'row'
},
tableCell: {
borderRightWidth: 1,
borderBottomWidth: 1,
borderColor: colors.borderSubtle,
paddingHorizontal: spacing.sm,
paddingVertical: spacing.xs,
fontSize: 12,
lineHeight: 17,
color: colors.textPrimary,
// Every row grows its cells from the same widths, so a short table fills the message aligned.
flexGrow: 1,
flexShrink: 0
},
tableHeader: {
fontWeight: '700',
backgroundColor: colors.bgRaised
},
list: {
gap: spacing.sm
},
listItem: {
flexDirection: 'row',
alignItems: 'flex-start',
gap: spacing.sm
},
listMarker: {
// A floor, not a width: "10." would wrap inside a fixed 22.
minWidth: 22,
fontSize: 13,
lineHeight: 21,
color: colors.textSecondary,
fontFamily: typography.monoFamily
},
listItemBody: {
flex: 1,
minWidth: 0,
gap: spacing.xs
},
taskMarker: { minWidth: 22, justifyContent: 'center' },
listText: {
fontSize: 13,
lineHeight: 21,
color: colors.textPrimary
},
footnoteReference: {
color: colors.accentBlue
},
rule: {
marginVertical: spacing.xs,
height: StyleSheet.hairlineWidth,
backgroundColor: colors.borderSubtle
}
}))
/** A style's size and line height under the reader's zoom; null at 1x, where the sheet holds. */
export function scaledMarkdownText(
style: { fontSize: number; lineHeight: number },
textScale: number
): { fontSize: number; lineHeight: number } | null {
return textScale !== 1
? { fontSize: style.fontSize * textScale, lineHeight: style.lineHeight * textScale }
: null
}
export type MarkdownTone = {
text: TextStyle | null
secondary: TextStyle | null
link: TextStyle | null
inlineCode: TextStyle | null
}
/** Colours and weight over the styles above, for one surface. The default tone overrides nothing. */
export const MARKDOWN_TONES: Record<
'default' | 'userMessage' | 'strong' | 'medium' | 'muted',
MarkdownTone
> = {
default: { text: null, secondary: null, link: null, inlineCode: null },
// A card's title: the question an agent asks.
strong: { text: { fontWeight: '600' }, secondary: null, link: null, inlineCode: null },
// The answer card's question: body size, lighter than its options' bold labels.
medium: { text: { fontWeight: '500' }, secondary: null, link: null, inlineCode: null },
// Settled text beside a receipt's muted answer.
muted: themedStyles(() => ({
text: { color: colors.textMuted },
secondary: null,
link: null,
inlineCode: null
})),
// Desktop's user bubble only strengthens the text; links and code keep their chat colors.
userMessage: themedStyles(() => ({
text: { color: colors.chatForegroundStrong },
secondary: null,
link: null,
inlineCode: null
}))
}