diff --git a/apps/mobile/src/MobileEditablePropertyPickers.tsx b/apps/mobile/src/MobileEditablePropertyPickers.tsx
new file mode 100644
index 00000000..ac6beb63
--- /dev/null
+++ b/apps/mobile/src/MobileEditablePropertyPickers.tsx
@@ -0,0 +1,256 @@
+import { CaretDown, CaretRight } from 'phosphor-react-native'
+import type { ReactNode } from 'react'
+import { Pressable, Text, View, type StyleProp, type ViewStyle } from 'react-native'
+import type { MobileNote } from './demoData'
+import { NamedIcon, type IconName } from './NamedIcon'
+import {
+ isMobileNotePropertySelected,
+ mobileNoteIconOptions,
+ mobileNoteStatusOptions,
+ mobileNoteTagOptions,
+ mobileNoteTypeOptions,
+ toggleMobileNoteTag,
+ type MobileNotePropertyPatch,
+} from './mobileNoteProperties'
+import { mobilePropertyDisplayValue, type MobilePropertyPickerKey } from './mobilePropertyPicker'
+import { styles } from './styles'
+import { colors } from './theme'
+
+export function MobileEditablePropertyPickers({
+ disabled,
+ note,
+ onChangeProperties,
+ onSelectPicker,
+ openPicker,
+}: {
+ disabled: boolean
+ note: MobileNote
+ onChangeProperties?: (patch: MobileNotePropertyPatch) => void
+ onSelectPicker: (selected: MobilePropertyPickerKey) => void
+ openPicker: MobilePropertyPickerKey | null
+}) {
+ return (
+ <>
+ onSelectPicker('type')}
+ >
+
+ {mobileNoteTypeOptions.map((option) => (
+ onChangeProperties?.({ type })}
+ />
+ ))}
+
+
+ onSelectPicker('status')}
+ >
+
+ {mobileNoteStatusOptions.map((option) => (
+ onChangeProperties?.({ status })}
+ />
+ ))}
+
+
+ onSelectPicker('icon')}
+ >
+
+ {mobileNoteIconOptions.map((option) => (
+ onChangeProperties?.({ icon })}
+ />
+ ))}
+
+
+ 0 ? `${note.tags.length} selected` : 'None'}
+ onOpen={() => onSelectPicker('tags')}
+ >
+
+ {mobileNoteTagOptions.map((option) => (
+ onChangeProperties?.({ tags: toggleMobileNoteTag(note.tags, tag) })}
+ />
+ ))}
+
+
+ >
+ )
+}
+
+function PropertyPickerSection({
+ children,
+ disabled,
+ isOpen,
+ label,
+ onOpen,
+ value,
+}: {
+ children: ReactNode
+ disabled: boolean
+ isOpen: boolean
+ label: string
+ onOpen: () => void
+ value: string
+}) {
+ return (
+ <>
+
+ {isOpen ? children : null}
+ >
+ )
+}
+
+function PropertyPickerRow({
+ disabled,
+ isOpen,
+ label,
+ onPress,
+ value,
+}: {
+ disabled: boolean
+ isOpen: boolean
+ label: string
+ onPress: () => void
+ value: string
+}) {
+ const Caret = isOpen ? CaretDown : CaretRight
+
+ return (
+ [styles.propertyRow, disabled ? styles.propertyDisabled : null, pressed ? styles.pressed : null]}
+ >
+ {label}
+ {value}
+
+
+ )
+}
+
+function PropertyIconChip({
+ disabled,
+ onSelect,
+ option,
+ value,
+}: {
+ disabled: boolean
+ onSelect: (option: string) => void
+ option: string
+ value: string | undefined
+}) {
+ const isSelected = isMobileNotePropertySelected({ current: value, option })
+
+ return (
+ onSelect(option)}
+ style={styles.propertyIconChip}
+ >
+
+
+ )
+}
+
+function PropertyChipOptions({ children }: { children: ReactNode }) {
+ return (
+
+
+ {children}
+
+
+ )
+}
+
+function PropertyTextChip({
+ disabled,
+ onSelect,
+ option,
+ value,
+}: {
+ disabled: boolean
+ onSelect: (option: string) => void
+ option: string
+ value: readonly string[] | string | undefined
+}) {
+ const isSelected = Array.isArray(value)
+ ? value.includes(option)
+ : isMobileNotePropertySelected({ current: typeof value === 'string' ? value : undefined, option })
+
+ return (
+ onSelect(option)}
+ style={styles.propertyChip}
+ >
+
+ {option || 'None'}
+
+
+ )
+}
+
+function SelectablePropertyChip({
+ children,
+ disabled,
+ isSelected,
+ onPress,
+ style,
+}: {
+ children: ReactNode
+ disabled: boolean
+ isSelected: boolean
+ onPress: () => void
+ style: StyleProp
+}) {
+ return (
+ [
+ style,
+ isSelected ? styles.propertyChipSelected : null,
+ disabled ? styles.propertyDisabled : null,
+ pressed ? styles.pressed : null,
+ ]}
+ >
+ {children}
+
+ )
+}
diff --git a/apps/mobile/src/MobilePropertiesPanel.tsx b/apps/mobile/src/MobilePropertiesPanel.tsx
index b7ecc6e5..a509edac 100644
--- a/apps/mobile/src/MobilePropertiesPanel.tsx
+++ b/apps/mobile/src/MobilePropertiesPanel.tsx
@@ -1,17 +1,10 @@
import { CaretLeft } from 'phosphor-react-native'
-import type { ReactNode } from 'react'
-import { Pressable, ScrollView, Text, View, type StyleProp, type ViewStyle } from 'react-native'
+import { useState } from 'react'
+import { Pressable, ScrollView, Text, View } from 'react-native'
import type { MobileNote } from './demoData'
-import { NamedIcon, type IconName } from './NamedIcon'
-import {
- isMobileNotePropertySelected,
- mobileNoteIconOptions,
- mobileNoteStatusOptions,
- mobileNoteTagOptions,
- mobileNoteTypeOptions,
- toggleMobileNoteTag,
- type MobileNotePropertyPatch,
-} from './mobileNoteProperties'
+import { MobileEditablePropertyPickers } from './MobileEditablePropertyPickers'
+import type { MobileNotePropertyPatch } from './mobileNoteProperties'
+import { nextMobilePropertyPicker, type MobilePropertyPickerKey } from './mobilePropertyPicker'
import { styles } from './styles'
import { colors } from './theme'
@@ -29,32 +22,22 @@ export function MobilePropertiesPanel({
onClose?: () => void
}) {
const today = formatMobilePropertyDate(new Date())
+ const [openPicker, setOpenPicker] = useState(null)
+ const selectPicker = (selected: MobilePropertyPickerKey) => {
+ setOpenPicker((current) => nextMobilePropertyPicker({ current, selected }))
+ }
return (
{failed ? Could not save property. : null}
- onChangeProperties?.({ type })}
- />
- onChangeProperties?.({ status })}
- />
- onChangeProperties?.({ icon })}
+ note={note}
+ openPicker={openPicker}
+ onChangeProperties={onChangeProperties}
+ onSelectPicker={selectPicker}
/>
- Tags
- onChangeProperties?.({ tags: toggleMobileNoteTag(note.tags, tag) })}
- />
History
eb373865c - Updated 1 note
5e853fdfe - Updated 1 note
@@ -135,126 +111,3 @@ function PropertyRow({
{content}
)
}
-
-function PropertyOptionGroup({
- disabled,
- label,
- onSelect,
- options,
- value,
-}: {
- disabled: boolean
- label: string
- onSelect: (option: string) => void
- options: readonly string[]
- value: string | undefined
-}) {
- return (
-
- {label}
-
-
- )
-}
-
-function PropertyIconOptionGroup({
- disabled,
- label,
- onSelect,
- options,
- value,
-}: {
- disabled: boolean
- label: string
- onSelect: (option: string) => void
- options: readonly string[]
- value: string | undefined
-}) {
- return (
-
- {label}
-
- {options.map((option) => {
- const isSelected = isMobileNotePropertySelected({ current: value, option })
-
- return (
- onSelect(option)}
- style={styles.propertyIconChip}
- >
-
-
- )
- })}
-
-
- )
-}
-
-function PropertyOptionChips({
- disabled,
- onSelect,
- options,
- value,
-}: {
- disabled: boolean
- onSelect: (option: string) => void
- options: readonly string[]
- value: readonly string[] | string | undefined
-}) {
- return (
-
- {options.map((option) => {
- const isSelected = Array.isArray(value)
- ? value.includes(option)
- : isMobileNotePropertySelected({ current: typeof value === 'string' ? value : undefined, option })
-
- return (
- onSelect(option)}
- style={styles.propertyChip}
- >
-
- {option || 'None'}
-
-
- )
- })}
-
- )
-}
-
-function SelectablePropertyChip({
- children,
- disabled,
- isSelected,
- onPress,
- style,
-}: {
- children: ReactNode
- disabled: boolean
- isSelected: boolean
- onPress: () => void
- style: StyleProp
-}) {
- return (
- [
- style,
- isSelected ? styles.propertyChipSelected : null,
- disabled ? styles.propertyDisabled : null,
- pressed ? styles.pressed : null,
- ]}
- >
- {children}
-
- )
-}
diff --git a/apps/mobile/src/mobilePropertyPicker.test.ts b/apps/mobile/src/mobilePropertyPicker.test.ts
new file mode 100644
index 00000000..5afd6d32
--- /dev/null
+++ b/apps/mobile/src/mobilePropertyPicker.test.ts
@@ -0,0 +1,21 @@
+import { describe, expect, it } from 'vitest'
+import { mobilePropertyDisplayValue, nextMobilePropertyPicker } from './mobilePropertyPicker'
+
+describe('nextMobilePropertyPicker', () => {
+ it('opens a selected picker and closes it when selected again', () => {
+ expect(nextMobilePropertyPicker({ current: null, selected: 'type' })).toBe('type')
+ expect(nextMobilePropertyPicker({ current: 'type', selected: 'type' })).toBeNull()
+ })
+
+ it('switches between editable property pickers', () => {
+ expect(nextMobilePropertyPicker({ current: 'type', selected: 'status' })).toBe('status')
+ })
+})
+
+describe('mobilePropertyDisplayValue', () => {
+ it('normalizes missing property values for compact property rows', () => {
+ expect(mobilePropertyDisplayValue({ value: undefined })).toBe('None')
+ expect(mobilePropertyDisplayValue({ value: '' })).toBe('None')
+ expect(mobilePropertyDisplayValue({ value: 'Essay' })).toBe('Essay')
+ })
+})
diff --git a/apps/mobile/src/mobilePropertyPicker.ts b/apps/mobile/src/mobilePropertyPicker.ts
new file mode 100644
index 00000000..5a047457
--- /dev/null
+++ b/apps/mobile/src/mobilePropertyPicker.ts
@@ -0,0 +1,21 @@
+export type MobilePropertyPickerKey = 'icon' | 'status' | 'tags' | 'type'
+
+export function nextMobilePropertyPicker({
+ current,
+ selected,
+}: {
+ current: MobilePropertyPickerKey | null
+ selected: MobilePropertyPickerKey
+}) {
+ return current === selected ? null : selected
+}
+
+export function mobilePropertyDisplayValue({
+ fallback = 'None',
+ value,
+}: {
+ fallback?: string
+ value: string | undefined
+}) {
+ return value?.trim() || fallback
+}
diff --git a/apps/mobile/src/styles/propertyChipStyles.ts b/apps/mobile/src/styles/propertyChipStyles.ts
index 317bd3c9..a3da1cea 100644
--- a/apps/mobile/src/styles/propertyChipStyles.ts
+++ b/apps/mobile/src/styles/propertyChipStyles.ts
@@ -29,6 +29,11 @@ export const propertyChipStyles = StyleSheet.create({
gap: spacing.sm,
marginTop: spacing.sm,
},
+ propertyPickerOptions: {
+ paddingBottom: spacing.md,
+ borderBottomColor: colors.border,
+ borderBottomWidth: StyleSheet.hairlineWidth,
+ },
propertyChip: {
minHeight: 30,
justifyContent: 'center',
diff --git a/docs/MOBILE_PROGRESS.md b/docs/MOBILE_PROGRESS.md
index ac889a29..d5c67016 100644
--- a/docs/MOBILE_PROGRESS.md
+++ b/docs/MOBILE_PROGRESS.md
@@ -85,14 +85,15 @@ This file is the resumable working log for Tolaria mobile. The strategy and road
- Created [ADR-0112](./adr/0112-expo-development-client-for-mobile-native-qa.md) for the mobile development-client QA path.
- Added safe TenTap image serialization for persisted relative/remote image sources while continuing to block transient image sources such as `blob:` URLs.
- Added simple TenTap table serialization for rectangular tables, including entity decoding and escaped pipe characters, while continuing to block malformed table shapes.
+- Reworked the mobile properties panel into expandable Type/Status/Icon/Tags picker rows that show current values first and reveal chip choices on demand.
## Next Action
Continue Phase 4 with editor durability:
-1. Replace the first property chips with richer desktop-compatible pickers once the mobile metadata schema is finalized.
-2. Continue TenTap Markdown serialization coverage for any editor output observed in simulator QA.
-3. Retry the iOS development-client build after installing an iOS 26.2 simulator runtime in Xcode.
+1. Continue TenTap Markdown serialization coverage for any editor output observed in simulator QA.
+2. Retry the iOS development-client build after installing an iOS 26.2 simulator runtime in Xcode.
+3. Start the Git sync/auth vertical slice for app-local vaults.
## Verification Log
@@ -312,6 +313,10 @@ Continue Phase 4 with editor durability:
- `pnpm --filter @tolaria/mobile typecheck` passed after simple table serialization.
- CodeScene after simple table serialization: `apps/mobile/src/mobileEditorHtmlMarkdown.ts`, `apps/mobile/src/mobileEditorTableMarkdown.ts`, `apps/mobile/src/mobileEditorDraft.test.ts`, and `apps/mobile/src/mobileEditorDraftSave.test.ts` scored `10`.
- `pnpm --filter @tolaria/mobile exec expo export --platform ios --output-dir /tmp/tolaria-mobile-export` passed after simple table serialization.
+- `pnpm --filter @tolaria/mobile test -- src/mobilePropertyPicker.test.ts src/mobileNoteProperties.test.ts` passed after expandable mobile property pickers: 28 files / 93 tests.
+- `pnpm --filter @tolaria/mobile typecheck` passed after expandable mobile property pickers.
+- CodeScene after expandable mobile property pickers: `apps/mobile/src/MobilePropertiesPanel.tsx`, `apps/mobile/src/MobileEditablePropertyPickers.tsx`, `apps/mobile/src/mobilePropertyPicker.ts`, `apps/mobile/src/mobilePropertyPicker.test.ts`, and `apps/mobile/src/styles/propertyChipStyles.ts` scored `10`.
+- `pnpm --filter @tolaria/mobile exec expo export --platform ios --output-dir /tmp/tolaria-mobile-export` passed after expandable mobile property pickers.
## Risks / Watch Items