feat: refine mobile prototype editing flows
This commit is contained in:
@@ -44,7 +44,7 @@ import { NamedIcon, type IconName } from './NamedIcon'
|
||||
import { SwipeSurface } from './SwipeSurface'
|
||||
import { styles } from './styles'
|
||||
import { colors } from './theme'
|
||||
import { MobileNoteCreatePrompt } from './MobileNoteCreatePrompt'
|
||||
import { MobileEditorBreadcrumb } from './MobileEditorBreadcrumb'
|
||||
import { MobilePropertiesPanel } from './MobilePropertiesPanel'
|
||||
import { MobileVaultManagementCard } from './MobileVaultManagementCard'
|
||||
import { MobileVaultRemotePrompt } from './MobileVaultRemotePrompt'
|
||||
@@ -170,16 +170,11 @@ export function MobileApp() {
|
||||
notes={availableNotes}
|
||||
selectedNoteId={compactNavigation.selectedNoteId}
|
||||
createNoteFailed={createFlow.failed}
|
||||
createNoteTitle={createFlow.title}
|
||||
isCreatePromptOpen={createFlow.isPromptOpen}
|
||||
isCreatingNote={createFlow.isCreating}
|
||||
runtimeLoadFailed={runtimeLoader.failed}
|
||||
onCancelCreateNote={createFlow.cancel}
|
||||
onChangeCreateNoteTitle={createFlow.setTitle}
|
||||
onOpenCreateNote={createFlow.open}
|
||||
onCreateNote={createFlow.create}
|
||||
onGitSyncAction={gitSyncFlow.runPrimaryAction}
|
||||
onRetryRuntimeLoad={runtimeLoader.retry}
|
||||
onSubmitCreateNote={createFlow.submit}
|
||||
onSelectNote={selectNote}
|
||||
/>
|
||||
<EditorPanel
|
||||
@@ -210,16 +205,11 @@ export function MobileApp() {
|
||||
onDeleteNote={deleteFlow.canDelete ? deleteFlow.deleteSelectedNote : undefined}
|
||||
onDraftChange={saveDraft}
|
||||
createNoteFailed={createFlow.failed}
|
||||
createNoteTitle={createFlow.title}
|
||||
isCreatePromptOpen={createFlow.isPromptOpen}
|
||||
isCreatingNote={createFlow.isCreating}
|
||||
runtimeLoadFailed={runtimeLoader.failed}
|
||||
onCancelCreateNote={createFlow.cancel}
|
||||
onChangeCreateNoteTitle={createFlow.setTitle}
|
||||
onOpenCreateNote={createFlow.open}
|
||||
onCreateNote={createFlow.create}
|
||||
onGitSyncAction={gitSyncFlow.runPrimaryAction}
|
||||
onRetryRuntimeLoad={runtimeLoader.retry}
|
||||
onSubmitCreateNote={createFlow.submit}
|
||||
onSelectNote={selectNote}
|
||||
propertiesFailed={propertiesFlow.failed}
|
||||
isSavingProperties={propertiesFlow.isSaving}
|
||||
@@ -254,18 +244,13 @@ function CompactShell({
|
||||
onDeleteNote,
|
||||
onDraftChange,
|
||||
createNoteFailed,
|
||||
createNoteTitle,
|
||||
isCreatePromptOpen,
|
||||
isCreatingNote,
|
||||
runtimeLoadFailed,
|
||||
onCancelCreateNote,
|
||||
onChangeCreateNoteTitle,
|
||||
onOpenCreateNote,
|
||||
onCreateNote,
|
||||
onGitSyncAction,
|
||||
onChangeProperties,
|
||||
onOpenRemoteSetup,
|
||||
onRetryRuntimeLoad,
|
||||
onSubmitCreateNote,
|
||||
onSelectNote,
|
||||
propertiesFailed,
|
||||
isSavingProperties,
|
||||
@@ -278,21 +263,16 @@ function CompactShell({
|
||||
notes: MobileNote[]
|
||||
saveState: MobileEditorSaveState
|
||||
createNoteFailed: boolean
|
||||
createNoteTitle: string
|
||||
isCreatePromptOpen: boolean
|
||||
isCreatingNote: boolean
|
||||
runtimeLoadFailed: boolean
|
||||
onNavigate: (event: CompactNavigationEvent) => void
|
||||
onDeleteNote?: () => void
|
||||
onDraftChange: (draft: MobileEditorDraft) => void
|
||||
onCancelCreateNote: () => void
|
||||
onChangeCreateNoteTitle: (title: string) => void
|
||||
onOpenCreateNote: () => void
|
||||
onCreateNote: () => void
|
||||
onGitSyncAction: () => void
|
||||
onChangeProperties: (patch: MobileNotePropertyPatch) => void
|
||||
onOpenRemoteSetup: () => void
|
||||
onRetryRuntimeLoad: () => void
|
||||
onSubmitCreateNote: () => void
|
||||
onSelectNote: (note: MobileNote) => void
|
||||
propertiesFailed: boolean
|
||||
isSavingProperties: boolean
|
||||
@@ -346,18 +326,13 @@ function CompactShell({
|
||||
notes={notes}
|
||||
selectedNoteId={selectedNoteId}
|
||||
createNoteFailed={createNoteFailed}
|
||||
createNoteTitle={createNoteTitle}
|
||||
isCreatePromptOpen={isCreatePromptOpen}
|
||||
isCreatingNote={isCreatingNote}
|
||||
runtimeLoadFailed={runtimeLoadFailed}
|
||||
onCancelCreateNote={onCancelCreateNote}
|
||||
onChangeCreateNoteTitle={onChangeCreateNoteTitle}
|
||||
onGitSyncAction={onGitSyncAction}
|
||||
onOpenCreateNote={onOpenCreateNote}
|
||||
onCreateNote={onCreateNote}
|
||||
onOpenSidebar={() => onNavigate({ type: 'openSidebar' })}
|
||||
onRetryRuntimeLoad={onRetryRuntimeLoad}
|
||||
onSelectNote={onSelectNote}
|
||||
onSubmitCreateNote={onSubmitCreateNote}
|
||||
/>
|
||||
</SwipeSurface>
|
||||
)
|
||||
@@ -408,35 +383,25 @@ function NoteListPanel({
|
||||
gitSyncPlan,
|
||||
notes,
|
||||
createNoteFailed,
|
||||
createNoteTitle,
|
||||
isCreatePromptOpen,
|
||||
isCreatingNote,
|
||||
runtimeLoadFailed,
|
||||
onCancelCreateNote,
|
||||
onChangeCreateNoteTitle,
|
||||
onGitSyncAction,
|
||||
onOpenCreateNote,
|
||||
onCreateNote,
|
||||
onOpenSidebar,
|
||||
onRetryRuntimeLoad,
|
||||
onSelectNote,
|
||||
onSubmitCreateNote,
|
||||
selectedNoteId,
|
||||
}: {
|
||||
gitSyncPlan: MobileGitSyncPlan
|
||||
notes: MobileNote[]
|
||||
createNoteFailed: boolean
|
||||
createNoteTitle: string
|
||||
isCreatePromptOpen: boolean
|
||||
isCreatingNote: boolean
|
||||
runtimeLoadFailed: boolean
|
||||
onCancelCreateNote: () => void
|
||||
onChangeCreateNoteTitle: (title: string) => void
|
||||
onGitSyncAction: () => void
|
||||
onOpenCreateNote: () => void
|
||||
onCreateNote: () => void
|
||||
onOpenSidebar?: () => void
|
||||
onRetryRuntimeLoad: () => void
|
||||
onSelectNote: (note: MobileNote) => void
|
||||
onSubmitCreateNote: () => void
|
||||
selectedNoteId: string
|
||||
}) {
|
||||
return (
|
||||
@@ -477,20 +442,11 @@ function NoteListPanel({
|
||||
</Pressable>
|
||||
)}
|
||||
/>
|
||||
{isCreatePromptOpen ? (
|
||||
<MobileNoteCreatePrompt
|
||||
failed={createNoteFailed}
|
||||
isCreating={isCreatingNote}
|
||||
onCancel={onCancelCreateNote}
|
||||
onChangeTitle={onChangeCreateNoteTitle}
|
||||
onSubmit={onSubmitCreateNote}
|
||||
title={createNoteTitle}
|
||||
/>
|
||||
) : null}
|
||||
{createNoteFailed ? <Text style={styles.propertyError}>Could not create note.</Text> : null}
|
||||
<Pressable
|
||||
accessibilityLabel="Create note"
|
||||
disabled={isCreatingNote}
|
||||
onPress={onOpenCreateNote}
|
||||
onPress={onCreateNote}
|
||||
style={({ pressed }) => [
|
||||
styles.composeButton,
|
||||
isCreatingNote ? styles.composeButtonDisabled : null,
|
||||
@@ -547,12 +503,12 @@ function EditorPanel({
|
||||
<View style={styles.editor}>
|
||||
<Toolbar>
|
||||
{onBack ? <IconButton icon={<CaretLeft size={25} color={colors.textSoft} />} onPress={onBack} /> : null}
|
||||
<View style={styles.toolbarSpacer} />
|
||||
<MobileEditorBreadcrumb note={note} saveState={saveState ?? idleMobileEditorSaveState} />
|
||||
{onOpenProperties ? <IconButton icon={<Info size={23} color={colors.textSoft} />} onPress={onOpenProperties} /> : null}
|
||||
{onDeleteNote ? <IconButton icon={<Trash size={23} color={colors.textSoft} />} onPress={onDeleteNote} /> : null}
|
||||
<IconButton icon={<DotsThreeVertical size={23} color={colors.textSoft} />} />
|
||||
</Toolbar>
|
||||
<MobileEditorAdapter note={note} saveState={saveState} onDraftChange={onDraftChange} />
|
||||
<MobileEditorAdapter note={note} onDraftChange={onDraftChange} />
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,14 +1,16 @@
|
||||
import { CaretDown, CaretRight } from 'phosphor-react-native'
|
||||
import type { ReactNode } from 'react'
|
||||
import { Pressable, Text, View, type StyleProp, type ViewStyle } from 'react-native'
|
||||
import { useState, type ReactNode } from 'react'
|
||||
import { Pressable, Text, TextInput, View, type StyleProp, type ViewStyle } from 'react-native'
|
||||
import type { MobileNote } from './demoData'
|
||||
import { NamedIcon, type IconName } from './NamedIcon'
|
||||
import {
|
||||
formatMobileNoteTags,
|
||||
isMobileNotePropertySelected,
|
||||
mobileNoteIconOptions,
|
||||
mobileNoteStatusOptions,
|
||||
mobileNoteTagOptions,
|
||||
mobileNoteTypeOptions,
|
||||
parseMobileNoteTags,
|
||||
toggleMobileNoteTag,
|
||||
type MobileNotePropertyPatch,
|
||||
} from './mobileNoteProperties'
|
||||
@@ -29,72 +31,199 @@ export function MobileEditablePropertyPickers({
|
||||
onSelectPicker: (selected: MobilePropertyPickerKey) => void
|
||||
openPicker: MobilePropertyPickerKey | null
|
||||
}) {
|
||||
const today = formatMobilePropertyDate(new Date())
|
||||
|
||||
return (
|
||||
<>
|
||||
<PropertyPickerSection
|
||||
<EditableTextProperty
|
||||
disabled={disabled}
|
||||
isOpen={openPicker === 'type'}
|
||||
key={`type:${note.id}:${note.type}`}
|
||||
label="Type"
|
||||
value={mobilePropertyDisplayValue({ value: note.type })}
|
||||
placeholder="Type"
|
||||
suggestions={mobileNoteTypeOptions}
|
||||
value={note.type}
|
||||
onCommit={(type) => onChangeProperties?.({ type })}
|
||||
onOpen={() => onSelectPicker('type')}
|
||||
>
|
||||
<PropertyChipOptions>
|
||||
{mobileNoteTypeOptions.map((option) => (
|
||||
<PropertyTextChip
|
||||
disabled={disabled}
|
||||
key={option}
|
||||
option={option}
|
||||
value={note.type}
|
||||
onSelect={(type) => onChangeProperties?.({ type })}
|
||||
/>
|
||||
))}
|
||||
</PropertyChipOptions>
|
||||
</PropertyPickerSection>
|
||||
<PropertyPickerSection
|
||||
/>
|
||||
<EditableTextProperty
|
||||
disabled={disabled}
|
||||
isOpen={openPicker === 'status'}
|
||||
key={`status:${note.id}:${note.status ?? ''}`}
|
||||
label="Status"
|
||||
value={mobilePropertyDisplayValue({ value: note.status })}
|
||||
placeholder="Status"
|
||||
suggestions={mobileNoteStatusOptions}
|
||||
value={note.status ?? ''}
|
||||
onCommit={(status) => onChangeProperties?.({ status })}
|
||||
onOpen={() => onSelectPicker('status')}
|
||||
>
|
||||
/>
|
||||
<EditableTextProperty
|
||||
disabled={disabled}
|
||||
isOpen={openPicker === 'date'}
|
||||
key={`date:${note.id}:${note.date}`}
|
||||
label="Date"
|
||||
placeholder="Date"
|
||||
suggestions={[today, '']}
|
||||
value={note.date}
|
||||
onCommit={(date) => onChangeProperties?.({ date })}
|
||||
onOpen={() => onSelectPicker('date')}
|
||||
/>
|
||||
<IconProperty
|
||||
disabled={disabled}
|
||||
isOpen={openPicker === 'icon'}
|
||||
note={note}
|
||||
onChangeProperties={onChangeProperties}
|
||||
onOpen={() => onSelectPicker('icon')}
|
||||
/>
|
||||
<TagsProperty
|
||||
disabled={disabled}
|
||||
isOpen={openPicker === 'tags'}
|
||||
key={`tags:${note.id}:${formatMobileNoteTags(note.tags)}`}
|
||||
note={note}
|
||||
onChangeProperties={onChangeProperties}
|
||||
onOpen={() => onSelectPicker('tags')}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
function EditableTextProperty({
|
||||
disabled,
|
||||
isOpen,
|
||||
label,
|
||||
onCommit,
|
||||
onOpen,
|
||||
placeholder,
|
||||
suggestions,
|
||||
value,
|
||||
}: {
|
||||
disabled: boolean
|
||||
isOpen: boolean
|
||||
label: string
|
||||
onCommit: (value: string) => void
|
||||
onOpen: () => void
|
||||
placeholder: string
|
||||
suggestions: readonly string[]
|
||||
value: string
|
||||
}) {
|
||||
const [draft, setDraft] = useState(value)
|
||||
const commitDraft = () => commitTextValue({ current: value, next: draft, onCommit })
|
||||
|
||||
return (
|
||||
<PropertyPickerSection
|
||||
disabled={disabled}
|
||||
isOpen={isOpen}
|
||||
label={label}
|
||||
value={mobilePropertyDisplayValue({ value })}
|
||||
onOpen={onOpen}
|
||||
>
|
||||
<View style={styles.propertyPickerOptions}>
|
||||
<TextInput
|
||||
autoCapitalize="sentences"
|
||||
editable={!disabled}
|
||||
keyboardType="default"
|
||||
onBlur={commitDraft}
|
||||
onChangeText={setDraft}
|
||||
onSubmitEditing={commitDraft}
|
||||
placeholder={placeholder}
|
||||
placeholderTextColor={colors.mutedText}
|
||||
returnKeyType="done"
|
||||
style={styles.propertyTextInput}
|
||||
value={draft}
|
||||
/>
|
||||
<PropertyChipOptions>
|
||||
{mobileNoteStatusOptions.map((option) => (
|
||||
{suggestions.map((option) => (
|
||||
<PropertyTextChip
|
||||
disabled={disabled}
|
||||
key={option || 'none'}
|
||||
option={option}
|
||||
value={note.status}
|
||||
onSelect={(status) => onChangeProperties?.({ status })}
|
||||
value={value}
|
||||
onSelect={(selected) => {
|
||||
setDraft(selected)
|
||||
onCommit(selected)
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</PropertyChipOptions>
|
||||
</PropertyPickerSection>
|
||||
<PropertyPickerSection
|
||||
disabled={disabled}
|
||||
isOpen={openPicker === 'icon'}
|
||||
label="Icon"
|
||||
value={mobilePropertyDisplayValue({ value: note.icon })}
|
||||
onOpen={() => onSelectPicker('icon')}
|
||||
>
|
||||
<PropertyChipOptions>
|
||||
{mobileNoteIconOptions.map((option) => (
|
||||
<PropertyIconChip
|
||||
disabled={disabled}
|
||||
key={option}
|
||||
option={option}
|
||||
value={note.icon}
|
||||
onSelect={(icon) => onChangeProperties?.({ icon })}
|
||||
/>
|
||||
))}
|
||||
</PropertyChipOptions>
|
||||
</PropertyPickerSection>
|
||||
<PropertyPickerSection
|
||||
disabled={disabled}
|
||||
isOpen={openPicker === 'tags'}
|
||||
label="Tags"
|
||||
value={note.tags.length > 0 ? `${note.tags.length} selected` : 'None'}
|
||||
onOpen={() => onSelectPicker('tags')}
|
||||
>
|
||||
</View>
|
||||
</PropertyPickerSection>
|
||||
)
|
||||
}
|
||||
|
||||
function IconProperty({
|
||||
disabled,
|
||||
isOpen,
|
||||
note,
|
||||
onChangeProperties,
|
||||
onOpen,
|
||||
}: {
|
||||
disabled: boolean
|
||||
isOpen: boolean
|
||||
note: MobileNote
|
||||
onChangeProperties?: (patch: MobileNotePropertyPatch) => void
|
||||
onOpen: () => void
|
||||
}) {
|
||||
return (
|
||||
<PropertyPickerSection
|
||||
disabled={disabled}
|
||||
isOpen={isOpen}
|
||||
label="Icon"
|
||||
value={mobilePropertyDisplayValue({ value: note.icon })}
|
||||
onOpen={onOpen}
|
||||
>
|
||||
<PropertyChipOptions>
|
||||
{mobileNoteIconOptions.map((option) => (
|
||||
<PropertyIconChip
|
||||
disabled={disabled}
|
||||
key={option}
|
||||
option={option}
|
||||
value={note.icon}
|
||||
onSelect={(icon) => onChangeProperties?.({ icon })}
|
||||
/>
|
||||
))}
|
||||
</PropertyChipOptions>
|
||||
</PropertyPickerSection>
|
||||
)
|
||||
}
|
||||
|
||||
function TagsProperty({
|
||||
disabled,
|
||||
isOpen,
|
||||
note,
|
||||
onChangeProperties,
|
||||
onOpen,
|
||||
}: {
|
||||
disabled: boolean
|
||||
isOpen: boolean
|
||||
note: MobileNote
|
||||
onChangeProperties?: (patch: MobileNotePropertyPatch) => void
|
||||
onOpen: () => void
|
||||
}) {
|
||||
const [draft, setDraft] = useState(formatMobileNoteTags(note.tags))
|
||||
const commitDraft = () => onChangeProperties?.({ tags: parseMobileNoteTags(draft) })
|
||||
|
||||
return (
|
||||
<PropertyPickerSection
|
||||
disabled={disabled}
|
||||
isOpen={isOpen}
|
||||
label="Tags"
|
||||
value={note.tags.length > 0 ? formatMobileNoteTags(note.tags) : 'None'}
|
||||
onOpen={onOpen}
|
||||
>
|
||||
<View style={styles.propertyPickerOptions}>
|
||||
<TextInput
|
||||
autoCapitalize="none"
|
||||
editable={!disabled}
|
||||
keyboardType="default"
|
||||
onBlur={commitDraft}
|
||||
onChangeText={setDraft}
|
||||
onSubmitEditing={commitDraft}
|
||||
placeholder="Tags"
|
||||
placeholderTextColor={colors.mutedText}
|
||||
returnKeyType="done"
|
||||
style={styles.propertyTextInput}
|
||||
value={draft}
|
||||
/>
|
||||
<PropertyChipOptions>
|
||||
{mobileNoteTagOptions.map((option) => (
|
||||
<PropertyTextChip
|
||||
@@ -102,12 +231,16 @@ export function MobileEditablePropertyPickers({
|
||||
key={option}
|
||||
option={option}
|
||||
value={note.tags}
|
||||
onSelect={(tag) => onChangeProperties?.({ tags: toggleMobileNoteTag(note.tags, tag) })}
|
||||
onSelect={(tag) => {
|
||||
const tags = toggleMobileNoteTag(note.tags, tag)
|
||||
setDraft(formatMobileNoteTags(tags))
|
||||
onChangeProperties?.({ tags })
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</PropertyChipOptions>
|
||||
</PropertyPickerSection>
|
||||
</>
|
||||
</View>
|
||||
</PropertyPickerSection>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -156,7 +289,7 @@ function PropertyPickerRow({
|
||||
style={({ pressed }) => [styles.propertyRow, disabled ? styles.propertyDisabled : null, pressed ? styles.pressed : null]}
|
||||
>
|
||||
<Text style={styles.propertyLabel}>{label}</Text>
|
||||
<Text style={styles.propertyValue}>{value}</Text>
|
||||
<Text numberOfLines={1} style={styles.propertyValue}>{value}</Text>
|
||||
<Caret size={16} color={colors.textSoft} />
|
||||
</Pressable>
|
||||
)
|
||||
@@ -189,10 +322,8 @@ function PropertyIconChip({
|
||||
|
||||
function PropertyChipOptions({ children }: { children: ReactNode }) {
|
||||
return (
|
||||
<View style={styles.propertyPickerOptions}>
|
||||
<View style={styles.propertyChipRow}>
|
||||
{children}
|
||||
</View>
|
||||
<View style={styles.propertyChipRow}>
|
||||
{children}
|
||||
</View>
|
||||
)
|
||||
}
|
||||
@@ -254,3 +385,26 @@ function SelectablePropertyChip({
|
||||
</Pressable>
|
||||
)
|
||||
}
|
||||
|
||||
function commitTextValue({
|
||||
current,
|
||||
next,
|
||||
onCommit,
|
||||
}: {
|
||||
current: string
|
||||
next: string
|
||||
onCommit: (value: string) => void
|
||||
}) {
|
||||
const normalized = next.trim()
|
||||
if (normalized !== current) {
|
||||
onCommit(normalized)
|
||||
}
|
||||
}
|
||||
|
||||
function formatMobilePropertyDate(date: Date) {
|
||||
return new Intl.DateTimeFormat(undefined, {
|
||||
day: 'numeric',
|
||||
month: 'short',
|
||||
year: 'numeric',
|
||||
}).format(date)
|
||||
}
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
import { useEffect, useMemo, useRef } from 'react'
|
||||
import { KeyboardAvoidingView, Platform, Text, View } from 'react-native'
|
||||
import { KeyboardAvoidingView, Platform, View } from 'react-native'
|
||||
import { RichText, Toolbar, useEditorBridge } from '@10play/tentap-editor'
|
||||
import type { MobileNote } from './mobileNoteProjection'
|
||||
import { createMobileEditorDraft, type MobileEditorDraft } from './mobileEditorDraft'
|
||||
import { idleMobileEditorSaveState, type MobileEditorSaveState } from './mobileEditorSaveState'
|
||||
import {
|
||||
createMobileEditorDocument,
|
||||
createMobileEditorHtml,
|
||||
@@ -13,11 +12,9 @@ import { styles } from './styles'
|
||||
export function MobileEditorAdapter({
|
||||
note,
|
||||
onDraftChange,
|
||||
saveState = idleMobileEditorSaveState,
|
||||
}: {
|
||||
note: MobileNote
|
||||
onDraftChange?: (draft: MobileEditorDraft) => void
|
||||
saveState?: MobileEditorSaveState
|
||||
}) {
|
||||
const document = useMemo(() => createMobileEditorDocument(note), [note])
|
||||
const initialContent = useMemo(() => createMobileEditorHtml(document), [document])
|
||||
@@ -35,15 +32,17 @@ export function MobileEditorAdapter({
|
||||
})
|
||||
},
|
||||
})
|
||||
useEffect(() => {
|
||||
const timer = setTimeout(() => {
|
||||
editor.injectCSS(mobileEditorCss, 'tolaria-mobile-editor')
|
||||
editor.injectJS('document.documentElement.lang = navigator.language || "en"; true;')
|
||||
}, 100)
|
||||
|
||||
return () => clearTimeout(timer)
|
||||
}, [editor, note.id])
|
||||
|
||||
return (
|
||||
<View style={styles.editorAdapterContent}>
|
||||
<View style={styles.breadcrumbRow}>
|
||||
<Text style={styles.breadcrumbText}>{note.type}</Text>
|
||||
<Text style={styles.breadcrumbDivider}>/</Text>
|
||||
<Text style={styles.breadcrumbText}>{note.id}</Text>
|
||||
<Text style={[styles.editorSaveState, saveStateStyle(saveState)]}>{saveState.label}</Text>
|
||||
</View>
|
||||
<View style={styles.tentapEditor}>
|
||||
<RichText key={note.id} editor={editor} />
|
||||
</View>
|
||||
@@ -57,19 +56,33 @@ export function MobileEditorAdapter({
|
||||
)
|
||||
}
|
||||
|
||||
function saveStateStyle(saveState: MobileEditorSaveState) {
|
||||
switch (saveState.state) {
|
||||
case 'blocked':
|
||||
return styles.editorSaveState_blocked
|
||||
case 'failed':
|
||||
return styles.editorSaveState_failed
|
||||
case 'queued':
|
||||
return styles.editorSaveState_queued
|
||||
case 'saved':
|
||||
return styles.editorSaveState_saved
|
||||
case 'saving':
|
||||
return styles.editorSaveState_saving
|
||||
default:
|
||||
return styles.editorSaveState_idle
|
||||
const mobileEditorCss = `
|
||||
html,
|
||||
body,
|
||||
#root,
|
||||
.ProseMirror {
|
||||
color: #292825;
|
||||
font-family: -apple-system, BlinkMacSystemFont, "SF Pro Text", "Segoe UI", sans-serif;
|
||||
font-size: 18px;
|
||||
line-height: 1.55;
|
||||
}
|
||||
}
|
||||
|
||||
.ProseMirror {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.ProseMirror h1 {
|
||||
font-family: inherit;
|
||||
font-size: 42px;
|
||||
font-weight: 760;
|
||||
letter-spacing: 0;
|
||||
line-height: 1.08;
|
||||
margin: 18px 0 28px;
|
||||
}
|
||||
|
||||
.ProseMirror p,
|
||||
.ProseMirror li,
|
||||
.ProseMirror blockquote {
|
||||
font-family: inherit;
|
||||
}
|
||||
`
|
||||
|
||||
38
apps/mobile/src/MobileEditorBreadcrumb.tsx
Normal file
38
apps/mobile/src/MobileEditorBreadcrumb.tsx
Normal file
@@ -0,0 +1,38 @@
|
||||
import { Text, View } from 'react-native'
|
||||
import type { MobileNote } from './demoData'
|
||||
import type { MobileEditorSaveState } from './mobileEditorSaveState'
|
||||
import { styles } from './styles'
|
||||
|
||||
export function MobileEditorBreadcrumb({
|
||||
note,
|
||||
saveState,
|
||||
}: {
|
||||
note: MobileNote
|
||||
saveState: MobileEditorSaveState
|
||||
}) {
|
||||
return (
|
||||
<View style={styles.editorBreadcrumb}>
|
||||
<Text numberOfLines={1} style={styles.editorBreadcrumbText}>{note.type}</Text>
|
||||
<Text style={styles.editorBreadcrumbDivider}>/</Text>
|
||||
<Text numberOfLines={1} style={styles.editorBreadcrumbTitle}>{note.id}</Text>
|
||||
<Text style={[styles.editorSaveState, saveStateStyle(saveState)]}>{saveState.label}</Text>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
function saveStateStyle(saveState: MobileEditorSaveState) {
|
||||
switch (saveState.state) {
|
||||
case 'blocked':
|
||||
return styles.editorSaveState_blocked
|
||||
case 'failed':
|
||||
return styles.editorSaveState_failed
|
||||
case 'queued':
|
||||
return styles.editorSaveState_queued
|
||||
case 'saved':
|
||||
return styles.editorSaveState_saved
|
||||
case 'saving':
|
||||
return styles.editorSaveState_saving
|
||||
default:
|
||||
return styles.editorSaveState_idle
|
||||
}
|
||||
}
|
||||
@@ -1,66 +0,0 @@
|
||||
import { Pressable, Text, TextInput, View } from 'react-native'
|
||||
import { styles } from './styles'
|
||||
|
||||
export function MobileNoteCreatePrompt({
|
||||
failed,
|
||||
isCreating,
|
||||
onCancel,
|
||||
onChangeTitle,
|
||||
onSubmit,
|
||||
title,
|
||||
}: {
|
||||
failed: boolean
|
||||
isCreating: boolean
|
||||
onCancel: () => void
|
||||
onChangeTitle: (title: string) => void
|
||||
onSubmit: () => void
|
||||
title: string
|
||||
}) {
|
||||
return (
|
||||
<View style={styles.createNotePrompt}>
|
||||
<TextInput
|
||||
accessibilityLabel="New note title"
|
||||
autoCapitalize="sentences"
|
||||
editable={!isCreating}
|
||||
onChangeText={onChangeTitle}
|
||||
onSubmitEditing={onSubmit}
|
||||
placeholder="Note title"
|
||||
returnKeyType="done"
|
||||
style={styles.createNoteInput}
|
||||
value={title}
|
||||
/>
|
||||
{failed ? <Text style={styles.createNoteError}>Could not create note</Text> : null}
|
||||
<View style={styles.createNoteActions}>
|
||||
<PromptButton label="Cancel" onPress={onCancel} />
|
||||
<PromptButton label={isCreating ? 'Creating' : 'Create'} onPress={onSubmit} disabled={isCreating} primary />
|
||||
</View>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
function PromptButton({
|
||||
disabled,
|
||||
label,
|
||||
onPress,
|
||||
primary,
|
||||
}: {
|
||||
disabled?: boolean
|
||||
label: string
|
||||
onPress: () => void
|
||||
primary?: boolean
|
||||
}) {
|
||||
return (
|
||||
<Pressable
|
||||
disabled={disabled}
|
||||
onPress={onPress}
|
||||
style={({ pressed }) => [
|
||||
styles.createNoteAction,
|
||||
primary ? styles.createNoteActionPrimary : null,
|
||||
disabled ? styles.createNoteActionDisabled : null,
|
||||
pressed ? styles.pressed : null,
|
||||
]}
|
||||
>
|
||||
<Text style={[styles.createNoteActionText, primary ? styles.createNoteActionTextPrimary : null]}>{label}</Text>
|
||||
</Pressable>
|
||||
)
|
||||
}
|
||||
@@ -21,7 +21,6 @@ export function MobilePropertiesPanel({
|
||||
onChangeProperties?: (patch: MobileNotePropertyPatch) => void
|
||||
onClose?: () => void
|
||||
}) {
|
||||
const today = formatMobilePropertyDate(new Date())
|
||||
const [openPicker, setOpenPicker] = useState<MobilePropertyPickerKey | null>(null)
|
||||
const selectPicker = (selected: MobilePropertyPickerKey) => {
|
||||
setOpenPicker((current) => nextMobilePropertyPicker({ current, selected }))
|
||||
@@ -39,13 +38,6 @@ export function MobilePropertiesPanel({
|
||||
onChangeProperties={onChangeProperties}
|
||||
onSelectPicker={selectPicker}
|
||||
/>
|
||||
<PropertyRow
|
||||
actionLabel="Today"
|
||||
disabled={isSaving}
|
||||
label="Date"
|
||||
value={note.date || 'None'}
|
||||
onPress={() => onChangeProperties?.({ date: today })}
|
||||
/>
|
||||
<PropertyRow label="Words" value={String(note.words)} />
|
||||
<PropertyRow label="Modified" value={note.modified} />
|
||||
<Text style={styles.propertyGroupTitle}>History</Text>
|
||||
@@ -70,44 +62,17 @@ function PanelToolbar({ onClose }: { onClose?: () => void }) {
|
||||
)
|
||||
}
|
||||
|
||||
function formatMobilePropertyDate(date: Date) {
|
||||
return new Intl.DateTimeFormat('en-US', {
|
||||
day: 'numeric',
|
||||
month: 'short',
|
||||
year: 'numeric',
|
||||
}).format(date)
|
||||
}
|
||||
|
||||
function PropertyRow({
|
||||
actionLabel,
|
||||
disabled = false,
|
||||
label,
|
||||
onPress,
|
||||
value,
|
||||
}: {
|
||||
actionLabel?: string
|
||||
disabled?: boolean
|
||||
label: string
|
||||
onPress?: () => void
|
||||
value: string
|
||||
}) {
|
||||
const content = (
|
||||
<>
|
||||
return (
|
||||
<View style={styles.propertyRow}>
|
||||
<Text style={styles.propertyLabel}>{label}</Text>
|
||||
<Text style={styles.propertyValue}>{value}</Text>
|
||||
{actionLabel ? <Text style={styles.propertyAction}>{actionLabel}</Text> : null}
|
||||
</>
|
||||
)
|
||||
|
||||
return onPress ? (
|
||||
<Pressable
|
||||
disabled={disabled}
|
||||
onPress={onPress}
|
||||
style={({ pressed }) => [styles.propertyRow, disabled ? styles.propertyDisabled : null, pressed ? styles.pressed : null]}
|
||||
>
|
||||
{content}
|
||||
</Pressable>
|
||||
) : (
|
||||
<View style={styles.propertyRow}>{content}</View>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -2,7 +2,9 @@ import { describe, expect, it } from 'vitest'
|
||||
import { notes } from './demoData'
|
||||
import {
|
||||
createMobileNoteFrontmatterPatch,
|
||||
formatMobileNoteTags,
|
||||
isMobileNotePropertySelected,
|
||||
parseMobileNoteTags,
|
||||
toggleMobileNoteTag,
|
||||
} from './mobileNoteProperties'
|
||||
|
||||
@@ -41,3 +43,10 @@ describe('toggleMobileNoteTag', () => {
|
||||
expect(toggleMobileNoteTag(['Tolaria MVP', 'mobile'], 'mobile')).toEqual(['Tolaria MVP'])
|
||||
})
|
||||
})
|
||||
|
||||
describe('mobile note tag text helpers', () => {
|
||||
it('formats and parses keyboard-entered tag lists', () => {
|
||||
expect(formatMobileNoteTags(['Tolaria MVP', 'mobile'])).toBe('Tolaria MVP, mobile')
|
||||
expect(parseMobileNoteTags(' Tolaria MVP, mobile, mobile, ')).toEqual(['Tolaria MVP', 'mobile'])
|
||||
})
|
||||
})
|
||||
|
||||
@@ -37,3 +37,14 @@ export function isMobileNotePropertySelected({
|
||||
export function toggleMobileNoteTag(tags: string[], tag: string) {
|
||||
return tags.includes(tag) ? tags.filter((item) => item !== tag) : [...tags, tag]
|
||||
}
|
||||
|
||||
export function formatMobileNoteTags(tags: readonly string[]) {
|
||||
return tags.join(', ')
|
||||
}
|
||||
|
||||
export function parseMobileNoteTags(value: string) {
|
||||
return value
|
||||
.split(',')
|
||||
.map((tag) => tag.trim())
|
||||
.filter((tag, index, tags) => tag.length > 0 && tags.indexOf(tag) === index)
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
export type MobilePropertyPickerKey = 'icon' | 'status' | 'tags' | 'type'
|
||||
export type MobilePropertyPickerKey = 'date' | 'icon' | 'status' | 'tags' | 'type'
|
||||
|
||||
export function nextMobilePropertyPicker({
|
||||
current,
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { breadcrumbStyles } from './styles/breadcrumbStyles'
|
||||
import { commonStyles } from './styles/commonStyles'
|
||||
import { editorSaveStateStyles } from './styles/editorSaveStateStyles'
|
||||
import { editorStyles } from './styles/editorStyles'
|
||||
import { gitSyncStyles } from './styles/gitSyncStyles'
|
||||
import { noteCreateStyles } from './styles/noteCreateStyles'
|
||||
import { noteListStyles } from './styles/noteListStyles'
|
||||
import { propertiesStyles } from './styles/propertiesStyles'
|
||||
import { propertyChipStyles } from './styles/propertyChipStyles'
|
||||
@@ -12,11 +12,11 @@ import { vaultManagementStyles } from './styles/vaultManagementStyles'
|
||||
import { vaultLoadStyles } from './styles/vaultLoadStyles'
|
||||
|
||||
export const styles = {
|
||||
...breadcrumbStyles,
|
||||
...commonStyles,
|
||||
...editorStyles,
|
||||
...editorSaveStateStyles,
|
||||
...gitSyncStyles,
|
||||
...noteCreateStyles,
|
||||
...noteListStyles,
|
||||
...propertiesStyles,
|
||||
...propertyChipStyles,
|
||||
|
||||
28
apps/mobile/src/styles/breadcrumbStyles.ts
Normal file
28
apps/mobile/src/styles/breadcrumbStyles.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
import { StyleSheet } from 'react-native'
|
||||
import { colors, spacing } from '../theme'
|
||||
|
||||
export const breadcrumbStyles = StyleSheet.create({
|
||||
editorBreadcrumb: {
|
||||
minWidth: 0,
|
||||
flex: 1,
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
gap: spacing.sm,
|
||||
},
|
||||
editorBreadcrumbText: {
|
||||
color: colors.mutedText,
|
||||
fontSize: 14,
|
||||
fontWeight: '600',
|
||||
},
|
||||
editorBreadcrumbDivider: {
|
||||
color: colors.border,
|
||||
fontSize: 14,
|
||||
},
|
||||
editorBreadcrumbTitle: {
|
||||
minWidth: 0,
|
||||
flexShrink: 1,
|
||||
color: colors.textSoft,
|
||||
fontSize: 14,
|
||||
fontWeight: '700',
|
||||
},
|
||||
})
|
||||
@@ -12,6 +12,7 @@ export const editorStyles = StyleSheet.create({
|
||||
maxWidth: 760,
|
||||
alignSelf: 'center',
|
||||
paddingHorizontal: spacing.xl,
|
||||
paddingTop: spacing.xl,
|
||||
paddingBottom: 96,
|
||||
},
|
||||
editorAdapterContent: {
|
||||
@@ -22,21 +23,6 @@ export const editorStyles = StyleSheet.create({
|
||||
paddingHorizontal: spacing.xl,
|
||||
paddingBottom: 96,
|
||||
},
|
||||
breadcrumbRow: {
|
||||
marginBottom: spacing.xl,
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
gap: spacing.sm,
|
||||
},
|
||||
breadcrumbText: {
|
||||
color: colors.mutedText,
|
||||
fontSize: 14,
|
||||
fontWeight: '600',
|
||||
},
|
||||
breadcrumbDivider: {
|
||||
color: colors.border,
|
||||
fontSize: 14,
|
||||
},
|
||||
editorTitle: {
|
||||
marginBottom: spacing.xl,
|
||||
color: colors.text,
|
||||
|
||||
@@ -1,64 +0,0 @@
|
||||
import { StyleSheet } from 'react-native'
|
||||
import { colors, spacing } from '../theme'
|
||||
|
||||
export const noteCreateStyles = StyleSheet.create({
|
||||
composeButtonDisabled: {
|
||||
opacity: 0.55,
|
||||
},
|
||||
createNoteAction: {
|
||||
minWidth: 82,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
borderRadius: 16,
|
||||
paddingHorizontal: spacing.md,
|
||||
paddingVertical: spacing.sm,
|
||||
backgroundColor: colors.primarySoft,
|
||||
},
|
||||
createNoteActionDisabled: {
|
||||
opacity: 0.55,
|
||||
},
|
||||
createNoteActionPrimary: {
|
||||
backgroundColor: colors.primary,
|
||||
},
|
||||
createNoteActionText: {
|
||||
color: colors.primary,
|
||||
fontSize: 14,
|
||||
fontWeight: '700',
|
||||
},
|
||||
createNoteActionTextPrimary: {
|
||||
color: colors.canvas,
|
||||
},
|
||||
createNoteActions: {
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'flex-end',
|
||||
gap: spacing.sm,
|
||||
},
|
||||
createNoteError: {
|
||||
color: colors.textSoft,
|
||||
fontSize: 13,
|
||||
fontWeight: '600',
|
||||
},
|
||||
createNoteInput: {
|
||||
minHeight: 44,
|
||||
borderColor: colors.border,
|
||||
borderWidth: StyleSheet.hairlineWidth,
|
||||
borderRadius: 14,
|
||||
paddingHorizontal: spacing.md,
|
||||
color: colors.text,
|
||||
backgroundColor: colors.canvas,
|
||||
fontSize: 16,
|
||||
fontWeight: '600',
|
||||
},
|
||||
createNotePrompt: {
|
||||
position: 'absolute',
|
||||
right: spacing.xl,
|
||||
bottom: spacing.xl + 76,
|
||||
width: 280,
|
||||
gap: spacing.sm,
|
||||
borderColor: colors.border,
|
||||
borderWidth: StyleSheet.hairlineWidth,
|
||||
borderRadius: 18,
|
||||
padding: spacing.md,
|
||||
backgroundColor: colors.canvas,
|
||||
},
|
||||
})
|
||||
@@ -66,4 +66,7 @@ export const noteListStyles = StyleSheet.create({
|
||||
borderRadius: 32,
|
||||
backgroundColor: colors.primary,
|
||||
},
|
||||
composeButtonDisabled: {
|
||||
opacity: 0.55,
|
||||
},
|
||||
})
|
||||
|
||||
@@ -2,12 +2,6 @@ import { StyleSheet } from 'react-native'
|
||||
import { colors, spacing } from '../theme'
|
||||
|
||||
export const propertyChipStyles = StyleSheet.create({
|
||||
propertyAction: {
|
||||
marginLeft: spacing.sm,
|
||||
color: colors.primary,
|
||||
fontSize: 13,
|
||||
fontWeight: '700',
|
||||
},
|
||||
propertyDisabled: {
|
||||
opacity: 0.55,
|
||||
},
|
||||
@@ -17,12 +11,6 @@ export const propertyChipStyles = StyleSheet.create({
|
||||
fontSize: 13,
|
||||
fontWeight: '700',
|
||||
},
|
||||
propertyOptionGroup: {
|
||||
minHeight: 58,
|
||||
paddingVertical: spacing.sm,
|
||||
borderBottomColor: colors.border,
|
||||
borderBottomWidth: StyleSheet.hairlineWidth,
|
||||
},
|
||||
propertyChipRow: {
|
||||
flexDirection: 'row',
|
||||
flexWrap: 'wrap',
|
||||
@@ -34,6 +22,17 @@ export const propertyChipStyles = StyleSheet.create({
|
||||
borderBottomColor: colors.border,
|
||||
borderBottomWidth: StyleSheet.hairlineWidth,
|
||||
},
|
||||
propertyTextInput: {
|
||||
minHeight: 38,
|
||||
marginTop: spacing.sm,
|
||||
borderColor: colors.border,
|
||||
borderRadius: 10,
|
||||
borderWidth: StyleSheet.hairlineWidth,
|
||||
color: colors.text,
|
||||
fontSize: 15,
|
||||
fontWeight: '600',
|
||||
paddingHorizontal: spacing.md,
|
||||
},
|
||||
propertyChip: {
|
||||
minHeight: 30,
|
||||
justifyContent: 'center',
|
||||
|
||||
@@ -10,36 +10,17 @@ export function useMobileNoteCreateFlow({
|
||||
}) {
|
||||
const [failed, setFailed] = useState(false)
|
||||
const [isCreating, setIsCreating] = useState(false)
|
||||
const [isPromptOpen, setIsPromptOpen] = useState(false)
|
||||
const [title, setTitle] = useState('')
|
||||
|
||||
const cancel = useCallback(() => {
|
||||
if (isCreating) {
|
||||
return
|
||||
}
|
||||
|
||||
setFailed(false)
|
||||
setIsPromptOpen(false)
|
||||
setTitle('')
|
||||
}, [isCreating])
|
||||
|
||||
const open = useCallback(() => {
|
||||
setFailed(false)
|
||||
setIsPromptOpen(true)
|
||||
}, [])
|
||||
|
||||
const submit = useCallback(() => {
|
||||
const create = useCallback(() => {
|
||||
if (isCreating) {
|
||||
return
|
||||
}
|
||||
|
||||
setFailed(false)
|
||||
setIsCreating(true)
|
||||
void createNote(title)
|
||||
void createNote('')
|
||||
.then((note) => {
|
||||
if (note) {
|
||||
setIsPromptOpen(false)
|
||||
setTitle('')
|
||||
onCreated(note)
|
||||
}
|
||||
})
|
||||
@@ -49,16 +30,11 @@ export function useMobileNoteCreateFlow({
|
||||
.finally(() => {
|
||||
setIsCreating(false)
|
||||
})
|
||||
}, [createNote, isCreating, onCreated, title])
|
||||
}, [createNote, isCreating, onCreated])
|
||||
|
||||
return {
|
||||
cancel,
|
||||
create,
|
||||
failed,
|
||||
isCreating,
|
||||
isPromptOpen,
|
||||
open,
|
||||
setTitle,
|
||||
submit,
|
||||
title,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -106,6 +106,12 @@ This file is the resumable working log for Tolaria mobile. The strategy and road
|
||||
- Added the optional Expo native-module resolver for `TolariaGit`, so the JavaScript Git transport automatically binds to future development builds that include the native module and keeps the current unavailable-module failure everywhere else.
|
||||
- Created [ADR-0115](./adr/0115-expo-native-module-boundary-for-mobile-git.md) for the mobile Git native module discovery boundary.
|
||||
- Added remote host and auth strategy to the native Git transport request so native Git can select credential callbacks without reparsing remote URLs.
|
||||
- Reworked the prototype compose flow so tapping the pencil immediately creates and opens an app-local `Untitled` note instead of blocking on a title modal.
|
||||
- Moved the editor breadcrumb and save state into the top editor toolbar, matching the desktop/Bear-style bar placement instead of rendering it inside the editor body.
|
||||
- Injected system-font CSS into TenTap so the editor uses iOS-native typography instead of the browser/WebView serif default, while leaving keyboard layout to the OS/simulator input source.
|
||||
- Upgraded the mobile properties prototype so type, status, date, and tags can be entered from the keyboard with quick chips as shortcuts, while icon editing stays chip-based.
|
||||
- Removed the now-unused title prompt component and kept the compose-button disabled state with the note-list styles.
|
||||
- Split new breadcrumb styles out of the shared toolbar styles and removed stale editor/property style entries so the CodeScene pre-commit safeguard stays green.
|
||||
|
||||
## Next Action
|
||||
|
||||
@@ -408,6 +414,11 @@ Continue Phase 4 with editor durability:
|
||||
- `pnpm --filter @tolaria/mobile typecheck` passed after adding remote host/auth strategy to native Git requests.
|
||||
- CodeScene after adding remote host/auth strategy to native Git requests: `apps/mobile/src/mobileNativeGitTransport.ts` and `apps/mobile/src/mobileNativeGitTransport.test.ts` scored `10`.
|
||||
- `pnpm --filter @tolaria/mobile exec expo export --platform ios --output-dir /tmp/tolaria-mobile-export` passed after adding remote host/auth strategy to native Git requests; Metro recovered from a cache deserialize warning by doing a full crawl.
|
||||
- `pnpm --filter @tolaria/mobile test` passed after the direct-create/properties editability update: 46 files / 153 tests.
|
||||
- `pnpm --filter @tolaria/mobile typecheck` passed after the direct-create/properties editability update.
|
||||
- CodeScene after the direct-create/properties editability update: `apps/mobile/src/MobileApp.tsx`, `apps/mobile/src/MobileEditablePropertyPickers.tsx`, `apps/mobile/src/MobileEditorAdapter.tsx`, `apps/mobile/src/MobileEditorBreadcrumb.tsx`, `apps/mobile/src/MobilePropertiesPanel.tsx`, `apps/mobile/src/mobileNoteProperties.ts`, `apps/mobile/src/mobilePropertyPicker.ts`, `apps/mobile/src/styles/breadcrumbStyles.ts`, `apps/mobile/src/styles/commonStyles.ts`, `apps/mobile/src/styles/editorStyles.ts`, `apps/mobile/src/styles/noteListStyles.ts`, `apps/mobile/src/styles/propertyChipStyles.ts`, and `apps/mobile/src/useMobileNoteCreateFlow.ts` scored `10`; `apps/mobile/src/styles.ts` reported no scorable score.
|
||||
- CodeScene pre-commit safeguard passed after splitting the new styles.
|
||||
- `pnpm --filter @tolaria/mobile exec expo export --platform ios --output-dir /tmp/tolaria-mobile-export` passed after the direct-create/properties editability update.
|
||||
|
||||
## Risks / Watch Items
|
||||
|
||||
|
||||
Reference in New Issue
Block a user