feat(mobile): create notebooks in Android and iOS apps

This commit is contained in:
tianma-if committed 2026-10-05 11:56:11 +08:00
1 parent 66b40b0ee0
commit 8472f19e46
13 files changed
+343 -16

No files matched your search

@@ -7,6 +7,7 @@
objects = {
/* Begin PBXBuildFile section */
A1D1EC7F16E745A68CE9B6D1 /* AiDirectStream.swift in Sources */ = {isa = PBXBuildFile; fileRef = A1D1EC7F16E745A68CE9B6D2 /* AiDirectStream.swift */; };
039594C5BEE4844FBB131C7F /* APIClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6F510BF90B418266F363EBD /* APIClient.swift */; };
0614453E8E9EC13B212B5A2F /* NotebookPickerSheet.swift in Sources */ = {isa = PBXBuildFile; fileRef = E3328D871910187ED827B6CE /* NotebookPickerSheet.swift */; };
06D2661CAB0ADED27F322649 /* Localizable.xcstrings in Resources */ = {isa = PBXBuildFile; fileRef = D87A8CED8498AE654EF8CD1F /* Localizable.xcstrings */; };
@@ -119,6 +120,7 @@
/* End PBXCopyFilesBuildPhase section */
/* Begin PBXFileReference section */
A1D1EC7F16E745A68CE9B6D2 /* AiDirectStream.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AiDirectStream.swift; sourceTree = "<group>"; };
033565CE2E96EC2D23105188 /* ListActionsSheet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ListActionsSheet.swift; sourceTree = "<group>"; };
0531FC6AA81DAF0A55B42362 /* AppDatabase.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDatabase.swift; sourceTree = "<group>"; };
12236E3A2F318CDEB17B80CB /* SyncProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SyncProtocol.swift; sourceTree = "<group>"; };
@@ -450,6 +452,7 @@
isa = PBXGroup;
children = (
D6F510BF90B418266F363EBD /* APIClient.swift */,
A1D1EC7F16E745A68CE9B6D2 /* AiDirectStream.swift */,
);
path = Network;
sourceTree = "<group>";
@@ -653,6 +656,7 @@
buildActionMask = 2147483647;
files = (
039594C5BEE4844FBB131C7F /* APIClient.swift in Sources */,
A1D1EC7F16E745A68CE9B6D1 /* AiDirectStream.swift in Sources */,
564B7A4D6CD3C8140286F881 /* AccountPasswordPanel.swift in Sources */,
F6AACA31885AB69C1CCCCB46 /* AiAssistantSheet.swift in Sources */,
12F0F0D1E937AA88C33222FF /* ApiTokensView.swift in Sources */,
@@ -70,6 +70,12 @@ final class LocalMirrorRepository: @unchecked Sendable {
}
}
func saveNotebook(scope: String, notebook: Notebook) throws {
try dbQueue.write { db in
try upsertNotebook(db, scope: scope, notebook: notebook)
}
}
func listTags(scope: String) throws -> [TagSummary] {
try dbQueue.read { db in
let rows = try Row.fetchAll(
@@ -99,6 +99,21 @@ actor APIClient {
)
}
// MARK: - Notebooks
func createNotebook(name: String, parentId: String?) async throws -> Notebook {
struct Body: Encodable {
var name: String
var parentId: String?
}
let response: NotebookResponse = try await request(
path: "/api/v1/notebooks",
method: "POST",
body: Body(name: name, parentId: parentId)
)
return response.notebook
}
// MARK: - Memos
func createMemo(
@@ -8,6 +8,11 @@ struct NotebookPickerSheet: View {
@State private var query = ""
@State private var collapsedIds: Set<String> = []
@State private var isCreating = false
@State private var isSaving = false
@State private var newName = ""
@State private var newParentId: String?
@State private var createError: String?
private var parentIds: Set<String> {
NotebookHierarchy.parentIdsWithChildren(from: store.notebooks)
@@ -55,6 +60,17 @@ struct NotebookPickerSheet: View {
.foregroundStyle(AppTheme.secondary)
}
Spacer(minLength: 8)
Button {
isCreating.toggle()
createError = nil
} label: {
Image(systemName: "plus")
.font(.system(size: 17, weight: .semibold))
.foregroundStyle(AppTheme.accent)
.frame(width: 36, height: 36)
}
.buttonStyle(.plain)
.accessibilityLabel(env.preferences.t("新建笔记本", en: "New notebook", ja: "新しいノートブック"))
Button {
dismiss()
} label: {
@@ -71,6 +87,51 @@ struct NotebookPickerSheet: View {
Rectangle().fill(AppTheme.border).frame(height: 1)
}
if isCreating {
VStack(alignment: .leading, spacing: 8) {
TextField(env.preferences.t("笔记本名称", en: "Notebook name", ja: "ノートブック名"), text: $newName)
.font(.system(size: 15))
.foregroundStyle(AppTheme.title)
.textInputAutocapitalization(.sentences)
.padding(.horizontal, 12)
.frame(height: 42)
.overlay(RoundedRectangle(cornerRadius: 8).stroke(AppTheme.border))
HStack(spacing: 8) {
parentChoice(env.preferences.t("顶层笔记本", en: "Top level", ja: "最上位"), id: nil)
if let selected = store.selectedNotebookId {
parentChoice(env.preferences.t("位于「\(activeName)」下", en: "Under \(activeName)", ja: "「\(activeName)」の下"), id: selected)
}
}
if let createError {
Text(createError)
.font(.system(size: 12))
.foregroundStyle(.red)
.accessibilityAddTraits(.updatesFrequently)
}
Button {
Task { await createNotebook() }
} label: {
Group {
if isSaving {
ProgressView().tint(.white)
} else {
Text(env.preferences.t("创建笔记本", en: "Create notebook", ja: "ノートブックを作成"))
}
}
.font(.system(size: 14, weight: .bold))
.foregroundStyle(.white)
.frame(maxWidth: .infinity)
.frame(height: 40)
.background(AppTheme.accent)
.clipShape(RoundedRectangle(cornerRadius: 8))
}
.buttonStyle(.plain)
.disabled(isSaving || newName.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty)
}
.padding(.horizontal, 16)
.padding(.vertical, 12)
}
ScrollView {
VStack(alignment: .leading, spacing: 0) {
// Search
@@ -194,9 +255,11 @@ struct NotebookPickerSheet: View {
}
}
.background(AppTheme.card)
.presentationDetents([.medium, .large])
.presentationDetents(isCreating ? [.large] : [.medium, .large])
.presentationDragIndicator(.hidden)
.onAppear {
isCreating = store.startCreatingNotebook
store.startCreatingNotebook = false
// Match Android: collapse branches not on the path to the active notebook.
let parents = parentIds
if let selected = store.selectedNotebookId {
@@ -209,6 +272,48 @@ struct NotebookPickerSheet: View {
}
}
private func parentChoice(_ title: String, id: String?) -> some View {
Button {
newParentId = id
} label: {
Text(title)
.font(.system(size: 12, weight: .semibold))
.foregroundStyle(AppTheme.title)
.lineLimit(1)
.frame(maxWidth: .infinity, minHeight: 36)
.overlay(
RoundedRectangle(cornerRadius: 7)
.stroke(newParentId == id ? AppTheme.accent : AppTheme.border)
)
}
.buttonStyle(.plain)
.accessibilityAddTraits(newParentId == id ? .isSelected : [])
}
private func createNotebook() async {
let name = newName.trimmingCharacters(in: .whitespacesAndNewlines)
guard !isSaving, !name.isEmpty, name.count <= 80 else { return }
isSaving = true
createError = nil
defer { isSaving = false }
do {
let notebook = try await env.session.client.createNotebook(name: name, parentId: newParentId)
if let scope = env.session.dataScope {
try? env.mirror.saveNotebook(scope: scope, notebook: notebook)
}
store.selectedTag = nil
store.selectedNotebookId = notebook.id
store.reload(env: env)
if !store.notebooks.contains(where: { $0.id == notebook.id }) {
store.notebooks.append(notebook)
}
dismiss()
Task { await env.runSyncCycle() }
} catch {
createError = error.localizedDescription
}
}
private func select(_ id: String?) {
store.selectedTag = nil
store.selectedNotebookId = id
@@ -7,6 +7,7 @@ struct NotesListView: View {
@Binding var path: NavigationPath
var onCreateNote: (() -> Void)? = nil
var onCreateFromTemplate: (() -> Void)? = nil
var onCreateNotebook: (() -> Void)? = nil
/// Whole-list settle + Pow jump once when data first becomes available this session.
@State private var listEntranceSettled = false
@@ -61,10 +62,12 @@ struct NotesListView: View {
emptyCard(
title: env.preferences.t("暂无笔记本", en: "No notebooks"),
description: env.preferences.t(
"同步完成后,笔记本会出现在这里。可在桌面/Web 端创建笔记本。",
en: "Notebooks appear after sync. Create them on desktop/web."
"先创建一个笔记本,再开始记录。",
en: "Create a notebook to start taking notes.",
ja: "ノートブックを作成して記録を始めましょう。"
),
showCreate: false
showCreate: false,
showCreateNotebook: true
)
.transition(Motion.softFade)
} else if store.memos.isEmpty && !store.isLoadingList {
@@ -440,7 +443,7 @@ struct NotesListView: View {
}
/// Android `memoListEmptyCard` + `emptyTitle` / `mutedText` + dual create actions.
private func emptyCard(title: String, description: String, showCreate: Bool) -> some View {
private func emptyCard(title: String, description: String, showCreate: Bool, showCreateNotebook: Bool = false) -> some View {
VStack(spacing: 10) {
Text(title)
.font(.system(size: 16, weight: AppTheme.heavy))
@@ -449,6 +452,19 @@ struct NotesListView: View {
.font(.system(size: 13))
.foregroundStyle(AppTheme.secondary)
.multilineTextAlignment(.center)
if showCreateNotebook, let onCreateNotebook {
Button(action: onCreateNotebook) {
Label(env.preferences.t("新建笔记本", en: "New notebook", ja: "新しいノートブック"), systemImage: "plus")
.font(.system(size: 13, weight: .bold))
.foregroundStyle(.white)
.padding(.horizontal, 14)
.frame(height: 38)
.background(AppTheme.accent)
.clipShape(RoundedRectangle(cornerRadius: 8))
}
.buttonStyle(.plain)
.accessibilityIdentifier("emptyCreateNotebook")
}
if showCreate, !store.notebooks.isEmpty {
HStack(spacing: 8) {
if let onCreateNote {
@@ -19,6 +19,7 @@ final class WorkspaceStore {
var selectionMode = false
var selectedMemoIds: Set<String> = []
var showNotebookPicker = false
var startCreatingNotebook = false
var showActions = false
var showTagFilterPicker = false
@@ -57,7 +57,11 @@ struct WorkspaceView: View {
store: store,
path: $path,
onCreateNote: { openCreateNote() },
onCreateFromTemplate: { openCreateFromTemplate() }
onCreateFromTemplate: { openCreateFromTemplate() },
onCreateNotebook: {
store.startCreatingNotebook = true
store.showNotebookPicker = true
}
)
.frame(maxWidth: .infinity, maxHeight: .infinity)
}
+5
View File
@@ -84,6 +84,11 @@ export const listLocalNotebooks = async (scope: string): Promise<{ notebooks: No
};
};
export const upsertLocalNotebook = async (scope: string, notebook: Notebook) => {
const db = await getDatabase();
await upsertNotebook(db, scope, notebook);
};
export const listLocalTags = async (scope: string): Promise<{ tags: TagSummary[] }> => {
const db = await getDatabase();
const rows = await db.getAllAsync<StoredMemoRow>(
+22
View File
@@ -19,6 +19,17 @@ type MobileLocaleContextValue = {
type TranslationPair = { source: string; target: string; pattern?: RegExp; placeholders?: string[] };
const mobileOnlyTranslations = new Map<string, string>([
["新建笔记本", "New notebook"],
["创建笔记本", "Create notebook"],
["笔记本名称", "Notebook name"],
["顶层笔记本", "Top level"],
["位于「{{name}}」下", "Under {{name}}"],
["暂无笔记本", "No notebooks"],
["先创建一个笔记本,再开始记录。", "Create a notebook to start taking notes."],
["创建笔记本失败,请重试", "Could not create the notebook. Try again."],
["连接尚未就绪", "Connection is not ready"],
["笔记本已创建", "Notebook created"],
["本机缓存更新失败,正在重新同步。", "The local cache could not update. Syncing again now."],
["本级暂无笔记", "No notes directly in this notebook"],
["无法保存“父笔记本中显示子笔记本笔记”设置,请稍后重试", "Could not save the \"Show notes from sub-notebooks\" setting. Please try again."],
["子笔记本中还有 {{count}} 条笔记。可以打开子笔记本查看,或在设置中开启“父笔记本中显示子笔记本笔记”。", "Its sub-notebooks still contain {{count}} notes. Open a sub-notebook to see them, or turn on \"Show notes from sub-notebooks\" in Settings."],
@@ -208,6 +219,17 @@ const mobileOnlyTranslations = new Map<string, string>([
]);
const mobileOnlyJapanese = new Map<string, string>([
["新建笔记本", "新しいノートブック"],
["创建笔记本", "ノートブックを作成"],
["笔记本名称", "ノートブック名"],
["顶层笔记本", "最上位"],
["位于「{{name}}」下", "「{{name}}」の下"],
["暂无笔记本", "ノートブックがありません"],
["先创建一个笔记本,再开始记录。", "ノートブックを作成して記録を始めましょう。"],
["创建笔记本失败,请重试", "ノートブックを作成できませんでした。再試行してください。"],
["连接尚未就绪", "接続の準備ができていません"],
["笔记本已创建", "ノートブックを作成しました"],
["本机缓存更新失败,正在重新同步。", "端末のキャッシュを更新できませんでした。再同期しています。"],
["本级暂无笔记", "このノートブック直下にノートはありません"],
["无法保存“父笔记本中显示子笔记本笔记”设置,请稍后重试", "「サブノートブックのノートを表示」の設定を保存できませんでした。しばらくしてから再試行してください。"],
["子笔记本中还有 {{count}} 条笔记。可以打开子笔记本查看,或在设置中开启“父笔记本中显示子笔记本笔记”。", "サブノートブックにはまだ {{count}} 件のノートがあります。サブノートブックを開くか、設定で「サブノートブックのノートを表示」をオンにしてください。"],
+11 -9
View File
@@ -197,16 +197,18 @@ export const NotesView = ({
</View>
<MemoList
emptyActions={memoView === "notebook" && notebooks.length > 0 && !searchActive && !filterActive
? [
{ label: "新建笔记", onPress: onCreate, variant: "primary" as const },
...(onCreateFromTemplate
? [{ label: "从模板新建", onPress: onCreateFromTemplate, variant: "secondary" as const }]
: []),
]
emptyActions={memoView === "notebook" && !searchActive && !filterActive
? notebooks.length === 0
? [{ label: "新建笔记本", onPress: onOpenNotebookPicker, variant: "primary" as const }]
: [
{ label: "新建笔记", onPress: onCreate, variant: "primary" as const },
...(onCreateFromTemplate
? [{ label: "从模板新建", onPress: onCreateFromTemplate, variant: "secondary" as const }]
: []),
]
: undefined}
emptyDescription={hiddenDescendantMemoCount > 0 ? `子笔记本中还有 ${hiddenDescendantMemoCount} 条笔记。可以打开子笔记本查看,或在设置中开启“父笔记本中显示子笔记本笔记”。` : searchActive ? "换个关键词再试" : filterActive ? "试试切换筛选条件,或调整搜索关键词。" : memoView === "trash" ? "删除的笔记会显示在这里。" : "先创建一条笔记,之后可以在这里快速预览、搜索和批量整理。"}
emptyTitle={hiddenDescendantMemoCount > 0 ? "本级暂无笔记" : searchActive ? "没有找到匹配笔记" : filterActive ? "没有符合筛选的笔记" : memoView === "trash" ? "回收站为空" : "暂无笔记"}
emptyDescription={hiddenDescendantMemoCount > 0 ? `子笔记本中还有 ${hiddenDescendantMemoCount} 条笔记。可以打开子笔记本查看,或在设置中开启“父笔记本中显示子笔记本笔记”。` : searchActive ? "换个关键词再试" : filterActive ? "试试切换筛选条件,或调整搜索关键词。" : memoView === "trash" ? "删除的笔记会显示在这里。" : notebooks.length === 0 ? "先创建一个笔记本,再开始记录。" : "先创建一条笔记,之后可以在这里快速预览、搜索和批量整理。"}
emptyTitle={hiddenDescendantMemoCount > 0 ? "本级暂无笔记" : searchActive ? "没有找到匹配笔记" : filterActive ? "没有符合筛选的笔记" : memoView === "trash" ? "回收站为空" : notebooks.length === 0 ? "暂无笔记本" : "暂无笔记"}
error={error}
initialSyncProgress={initialSyncProgress}
isError={isError}
@@ -12,6 +12,7 @@ import {
ChevronDown,
ChevronRight,
Folder,
Plus,
Search,
Tag,
TagPlus,
@@ -89,6 +90,7 @@ export const NotebookPickerModal = ({
includeAllNotes = true,
notebooks,
onClose,
onCreate,
onSelect,
visible,
}: {
@@ -96,12 +98,19 @@ export const NotebookPickerModal = ({
includeAllNotes?: boolean;
notebooks: Notebook[];
onClose: () => void;
onCreate?: (name: string, parentId: string | null) => Promise<string>;
onSelect: (notebookId: string) => void;
visible: boolean;
}) => {
const { translate } = useMobileLocale();
const safeAreaInsets = useSafeAreaInsets();
const [searchText, setSearchText] = useState("");
const [creating, setCreating] = useState(false);
const [newNotebookName, setNewNotebookName] = useState("");
const [newNotebookParentId, setNewNotebookParentId] = useState<string | null>(null);
const [createError, setCreateError] = useState("");
const [isSavingNotebook, setIsSavingNotebook] = useState(false);
const createInFlight = useRef(false);
const [collapsedNotebookIds, setCollapsedNotebookIds] = useState<Set<string>>(() => new Set());
const selectedScroll = useAutoCenterSelectedScrollRow(visible, activeNotebookId);
const notebookOptions = flattenNotebooks(notebooks);
@@ -119,10 +128,33 @@ export const NotebookPickerModal = ({
useEffect(() => {
if (visible) {
setSearchText("");
setCreating(false);
setNewNotebookName("");
setNewNotebookParentId(null);
setCreateError("");
setCollapsedNotebookIds(new Set(Array.from(childNotebookIds).filter((notebookId) => !activeNotebookAncestorIds.has(notebookId))));
}
}, [visible, activeNotebookId, notebooks]);
const submitNewNotebook = async () => {
const name = newNotebookName.trim();
if (!onCreate || createInFlight.current || !name || name.length > 80) {
return;
}
createInFlight.current = true;
setIsSavingNotebook(true);
setCreateError("");
try {
const notebookId = await onCreate(name, newNotebookParentId);
onSelect(notebookId);
} catch (error) {
setCreateError(error instanceof Error ? error.message : "创建笔记本失败,请重试");
} finally {
createInFlight.current = false;
setIsSavingNotebook(false);
}
};
const toggleNotebookCollapsed = (notebookId: string) => {
setCollapsedNotebookIds((current) => {
const next = new Set(current);
@@ -156,6 +188,34 @@ export const NotebookPickerModal = ({
</Pressable>
</View>
{onCreate ? (
<View style={styles.notebookCreateWrap}>
<Pressable accessibilityLabel="新建笔记本" accessibilityRole="button" onPress={() => { setCreating((value) => !value); setCreateError(""); }} style={styles.notebookCreateToggle}>
<Plus color="#059669" size={18} />
<Text style={styles.notebookCreateToggleText}>新建笔记本</Text>
</Pressable>
{creating ? (
<View style={styles.notebookCreateForm}>
<TextInput accessibilityLabel="笔记本名称" autoFocus maxLength={80} onChangeText={setNewNotebookName} placeholder="笔记本名称" placeholderTextColor="#94a3b8" returnKeyType="done" style={styles.notebookCreateInput} value={newNotebookName} />
<View style={styles.notebookCreateParentRow}>
<Pressable accessibilityRole="button" accessibilityState={{ selected: newNotebookParentId === null }} onPress={() => setNewNotebookParentId(null)} style={[styles.notebookCreateParentChoice, newNotebookParentId === null && styles.notebookCreateParentChoiceActive]}>
<Text style={styles.notebookCreateParentText}>顶层笔记本</Text>
</Pressable>
{activeNotebookId !== ALL_NOTES_ID ? (
<Pressable accessibilityRole="button" accessibilityState={{ selected: newNotebookParentId === activeNotebookId }} onPress={() => setNewNotebookParentId(activeNotebookId)} style={[styles.notebookCreateParentChoice, newNotebookParentId === activeNotebookId && styles.notebookCreateParentChoiceActive]}>
<Text numberOfLines={1} style={styles.notebookCreateParentText}>{translate(`位于「${activeNotebookName}」下`)}</Text>
</Pressable>
) : null}
</View>
{createError ? <Text accessibilityRole="alert" style={styles.notebookCreateError}>{createError}</Text> : null}
<Pressable accessibilityLabel="创建笔记本" accessibilityRole="button" accessibilityState={{ disabled: !newNotebookName.trim() || isSavingNotebook }} disabled={!newNotebookName.trim() || isSavingNotebook} onPress={() => void submitNewNotebook()} style={styles.notebookCreateSubmit}>
{isSavingNotebook ? <ActivityIndicator color="#ffffff" size="small" /> : <Text style={styles.notebookCreateSubmitText}>创建笔记本</Text>}
</Pressable>
</View>
) : null}
</View>
) : null}
<ScrollView
contentContainerStyle={styles.notebookPickerContent}
onLayout={selectedScroll.onViewportLayout}
+21 -1
View File
@@ -19,7 +19,7 @@ import {
import { SafeAreaView, useSafeAreaInsets } from "react-native-safe-area-context";
import { Alert, Pressable, Text } from "../components/LocalizedText";
import { ApiRequestError } from "@edgeever/client";
import { DEFAULT_MEMO_TITLE, getNotebookScopeIds, hasDiagramDocumentMarker, markdownToDoc, type MemoDetail } from "@edgeever/shared";
import { DEFAULT_MEMO_TITLE, getNotebookScopeIds, hasDiagramDocumentMarker, markdownToDoc, type MemoDetail, type Notebook } from "@edgeever/shared";
import { MOBILE_UI_METRICS, toggleMobileMemoFilterMode } from "@edgeever/shared/mobile-ui";
import { clearMobileMemoDraft, readMobileMemoDraft, type MobileMemoDraft } from "../lib/mobile-drafts";
import {
@@ -55,6 +55,7 @@ import {
getLocalMemo,
listLocalMemos,
listLocalNotebooks,
upsertLocalNotebook,
listLocalTags,
resolveLocalMemo,
syncMobileLocalMirror,
@@ -1342,6 +1343,25 @@ export const WorkspaceScreen = ({
activeNotebookId={activeNotebookId}
notebooks={notebooks}
onClose={() => setNotebookPickerOpen(false)}
onCreate={async (name, parentId) => {
if (!client) {
throw new Error("连接尚未就绪");
}
const { notebook } = await client.createNotebook({ name, parentId });
queryClient.setQueryData<{ notebooks: Notebook[] }>(["mobile", "notebooks"], (current) => ({
notebooks: [...(current?.notebooks ?? []).filter((item) => item.id !== notebook.id), notebook],
}));
try {
await upsertLocalNotebook(dataScope, notebook);
void queryClient.invalidateQueries({ queryKey: ["mobile", "notebooks"] });
} catch {
Alert.alert("笔记本已创建", "本机缓存更新失败,正在重新同步。", [{ text: "好的" }]);
void syncMobileLocalMirror(client, dataScope)
.then(() => queryClient.invalidateQueries({ queryKey: ["mobile", "notebooks"] }))
.catch(() => undefined);
}
return notebook.id;
}}
onSelect={(notebookId) => {
setSelectedTag(null);
setActiveNotebookId(notebookId);
@@ -1541,6 +1541,73 @@ const baseWorkspaceStyles = StyleSheet.create({
justifyContent: "center",
width: 32,
},
notebookCreateWrap: {
borderBottomColor: "#e2e8f0",
borderBottomWidth: 1,
paddingHorizontal: 16,
},
notebookCreateToggle: {
alignItems: "center",
flexDirection: "row",
gap: 8,
minHeight: 44,
},
notebookCreateToggleText: {
color: "#059669",
fontSize: 14,
fontWeight: "700",
},
notebookCreateForm: {
gap: 8,
paddingBottom: 12,
},
notebookCreateInput: {
borderColor: "#cbd5e1",
borderRadius: 8,
borderWidth: 1,
color: "#0f172a",
fontSize: 15,
minHeight: 42,
paddingHorizontal: 12,
},
notebookCreateParentRow: {
flexDirection: "row",
gap: 8,
},
notebookCreateParentChoice: {
borderColor: "#cbd5e1",
borderRadius: 7,
borderWidth: 1,
flex: 1,
justifyContent: "center",
minHeight: 36,
paddingHorizontal: 8,
},
notebookCreateParentChoiceActive: {
borderColor: "#059669",
backgroundColor: "#ecfdf5",
},
notebookCreateParentText: {
color: "#0f172a",
fontSize: 12,
fontWeight: "600",
},
notebookCreateError: {
color: "#dc2626",
fontSize: 12,
},
notebookCreateSubmit: {
alignItems: "center",
backgroundColor: "#059669",
borderRadius: 8,
justifyContent: "center",
minHeight: 40,
},
notebookCreateSubmitText: {
color: "#ffffff",
fontSize: 14,
fontWeight: "700",
},
notebookPickerSectionHeader: {
alignItems: "center",
flexDirection: "row",