@flux-lang/cli-ui 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (177) hide show
  1. package/dist/components/ActionGrid.d.ts +11 -0
  2. package/dist/components/ActionGrid.d.ts.map +1 -0
  3. package/dist/components/ActionGrid.js +9 -0
  4. package/dist/components/ActionGrid.js.map +1 -0
  5. package/dist/components/AppFrame.d.ts +6 -0
  6. package/dist/components/AppFrame.d.ts.map +1 -0
  7. package/dist/components/AppFrame.js +7 -0
  8. package/dist/components/AppFrame.js.map +1 -0
  9. package/dist/components/Button.d.ts +8 -0
  10. package/dist/components/Button.d.ts.map +1 -0
  11. package/dist/components/Button.js +10 -0
  12. package/dist/components/Button.js.map +1 -0
  13. package/dist/components/Card.d.ts +12 -0
  14. package/dist/components/Card.d.ts.map +1 -0
  15. package/dist/components/Card.js +14 -0
  16. package/dist/components/Card.js.map +1 -0
  17. package/dist/components/Clickable.d.ts +8 -0
  18. package/dist/components/Clickable.d.ts.map +1 -0
  19. package/dist/components/Clickable.js +8 -0
  20. package/dist/components/Clickable.js.map +1 -0
  21. package/dist/components/Collapsible.d.ts +10 -0
  22. package/dist/components/Collapsible.d.ts.map +1 -0
  23. package/dist/components/Collapsible.js +8 -0
  24. package/dist/components/Collapsible.js.map +1 -0
  25. package/dist/components/CommandPaletteModal.d.ts +12 -0
  26. package/dist/components/CommandPaletteModal.d.ts.map +1 -0
  27. package/dist/components/CommandPaletteModal.js +41 -0
  28. package/dist/components/CommandPaletteModal.js.map +1 -0
  29. package/dist/components/HelpOverlay.d.ts +8 -0
  30. package/dist/components/HelpOverlay.d.ts.map +1 -0
  31. package/dist/components/HelpOverlay.js +40 -0
  32. package/dist/components/HelpOverlay.js.map +1 -0
  33. package/dist/components/InputLine.d.ts +6 -0
  34. package/dist/components/InputLine.d.ts.map +1 -0
  35. package/dist/components/InputLine.js +10 -0
  36. package/dist/components/InputLine.js.map +1 -0
  37. package/dist/components/NavList.d.ts +10 -0
  38. package/dist/components/NavList.d.ts.map +1 -0
  39. package/dist/components/NavList.js +85 -0
  40. package/dist/components/NavList.js.map +1 -0
  41. package/dist/components/PaneFrame.d.ts +9 -0
  42. package/dist/components/PaneFrame.d.ts.map +1 -0
  43. package/dist/components/PaneFrame.js +7 -0
  44. package/dist/components/PaneFrame.js.map +1 -0
  45. package/dist/components/PromptModal.d.ts +7 -0
  46. package/dist/components/PromptModal.d.ts.map +1 -0
  47. package/dist/components/PromptModal.js +9 -0
  48. package/dist/components/PromptModal.js.map +1 -0
  49. package/dist/components/Spinner.d.ts +2 -0
  50. package/dist/components/Spinner.d.ts.map +1 -0
  51. package/dist/components/Spinner.js +15 -0
  52. package/dist/components/Spinner.js.map +1 -0
  53. package/dist/components/StatusChips.d.ts +8 -0
  54. package/dist/components/StatusChips.d.ts.map +1 -0
  55. package/dist/components/StatusChips.js +10 -0
  56. package/dist/components/StatusChips.js.map +1 -0
  57. package/dist/components/TaskProgressView.d.ts +5 -0
  58. package/dist/components/TaskProgressView.d.ts.map +1 -0
  59. package/dist/components/TaskProgressView.js +14 -0
  60. package/dist/components/TaskProgressView.js.map +1 -0
  61. package/dist/components/ToastHost.d.ts +8 -0
  62. package/dist/components/ToastHost.d.ts.map +1 -0
  63. package/dist/components/ToastHost.js +18 -0
  64. package/dist/components/ToastHost.js.map +1 -0
  65. package/dist/index.d.ts +11 -0
  66. package/dist/index.d.ts.map +1 -0
  67. package/dist/index.js +9 -0
  68. package/dist/index.js.map +1 -0
  69. package/dist/palette/index.d.ts +22 -0
  70. package/dist/palette/index.d.ts.map +1 -0
  71. package/dist/palette/index.js +79 -0
  72. package/dist/palette/index.js.map +1 -0
  73. package/dist/screens/AddWizardScreen.d.ts +5 -0
  74. package/dist/screens/AddWizardScreen.d.ts.map +1 -0
  75. package/dist/screens/AddWizardScreen.js +8 -0
  76. package/dist/screens/AddWizardScreen.js.map +1 -0
  77. package/dist/screens/DashboardScreen.d.ts +25 -0
  78. package/dist/screens/DashboardScreen.d.ts.map +1 -0
  79. package/dist/screens/DashboardScreen.js +15 -0
  80. package/dist/screens/DashboardScreen.js.map +1 -0
  81. package/dist/screens/DocDetailsScreen.d.ts +27 -0
  82. package/dist/screens/DocDetailsScreen.d.ts.map +1 -0
  83. package/dist/screens/DocDetailsScreen.js +13 -0
  84. package/dist/screens/DocDetailsScreen.js.map +1 -0
  85. package/dist/screens/DoctorScreen.d.ts +13 -0
  86. package/dist/screens/DoctorScreen.d.ts.map +1 -0
  87. package/dist/screens/DoctorScreen.js +12 -0
  88. package/dist/screens/DoctorScreen.js.map +1 -0
  89. package/dist/screens/EditPlaceholderScreen.d.ts +6 -0
  90. package/dist/screens/EditPlaceholderScreen.d.ts.map +1 -0
  91. package/dist/screens/EditPlaceholderScreen.js +8 -0
  92. package/dist/screens/EditPlaceholderScreen.js.map +1 -0
  93. package/dist/screens/EditScreen.d.ts +15 -0
  94. package/dist/screens/EditScreen.d.ts.map +1 -0
  95. package/dist/screens/EditScreen.js +18 -0
  96. package/dist/screens/EditScreen.js.map +1 -0
  97. package/dist/screens/ExportScreen.d.ts +15 -0
  98. package/dist/screens/ExportScreen.d.ts.map +1 -0
  99. package/dist/screens/ExportScreen.js +11 -0
  100. package/dist/screens/ExportScreen.js.map +1 -0
  101. package/dist/screens/FormatScreen.d.ts +11 -0
  102. package/dist/screens/FormatScreen.d.ts.map +1 -0
  103. package/dist/screens/FormatScreen.js +11 -0
  104. package/dist/screens/FormatScreen.js.map +1 -0
  105. package/dist/screens/NewWizardScreen.d.ts +20 -0
  106. package/dist/screens/NewWizardScreen.d.ts.map +1 -0
  107. package/dist/screens/NewWizardScreen.js +28 -0
  108. package/dist/screens/NewWizardScreen.js.map +1 -0
  109. package/dist/screens/OpenScreen.d.ts +43 -0
  110. package/dist/screens/OpenScreen.d.ts.map +1 -0
  111. package/dist/screens/OpenScreen.js +37 -0
  112. package/dist/screens/OpenScreen.js.map +1 -0
  113. package/dist/screens/SettingsScreen.d.ts +8 -0
  114. package/dist/screens/SettingsScreen.d.ts.map +1 -0
  115. package/dist/screens/SettingsScreen.js +12 -0
  116. package/dist/screens/SettingsScreen.js.map +1 -0
  117. package/dist/screens/ViewerControlScreen.d.ts +16 -0
  118. package/dist/screens/ViewerControlScreen.d.ts.map +1 -0
  119. package/dist/screens/ViewerControlScreen.js +12 -0
  120. package/dist/screens/ViewerControlScreen.js.map +1 -0
  121. package/dist/state/dashboard-machine.d.ts +11 -0
  122. package/dist/state/dashboard-machine.d.ts.map +1 -0
  123. package/dist/state/dashboard-machine.js +13 -0
  124. package/dist/state/dashboard-machine.js.map +1 -0
  125. package/dist/state/focus.d.ts +6 -0
  126. package/dist/state/focus.d.ts.map +1 -0
  127. package/dist/state/focus.js +29 -0
  128. package/dist/state/focus.js.map +1 -0
  129. package/dist/state/mouse.d.ts +8 -0
  130. package/dist/state/mouse.d.ts.map +1 -0
  131. package/dist/state/mouse.js +89 -0
  132. package/dist/state/mouse.js.map +1 -0
  133. package/dist/state/open-search.d.ts +26 -0
  134. package/dist/state/open-search.d.ts.map +1 -0
  135. package/dist/state/open-search.js +27 -0
  136. package/dist/state/open-search.js.map +1 -0
  137. package/dist/state/progress.d.ts +12 -0
  138. package/dist/state/progress.d.ts.map +1 -0
  139. package/dist/state/progress.js +31 -0
  140. package/dist/state/progress.js.map +1 -0
  141. package/dist/state/toasts.d.ts +12 -0
  142. package/dist/state/toasts.d.ts.map +1 -0
  143. package/dist/state/toasts.js +22 -0
  144. package/dist/state/toasts.js.map +1 -0
  145. package/dist/state/types.d.ts +60 -0
  146. package/dist/state/types.d.ts.map +1 -0
  147. package/dist/state/types.js +2 -0
  148. package/dist/state/types.js.map +1 -0
  149. package/dist/theme/index.d.ts +38 -0
  150. package/dist/theme/index.d.ts.map +1 -0
  151. package/dist/theme/index.js +101 -0
  152. package/dist/theme/index.js.map +1 -0
  153. package/dist/ui/ModalOverlay.d.ts +37 -0
  154. package/dist/ui/ModalOverlay.d.ts.map +1 -0
  155. package/dist/ui/ModalOverlay.js +60 -0
  156. package/dist/ui/ModalOverlay.js.map +1 -0
  157. package/dist/ui/app.d.ts +11 -0
  158. package/dist/ui/app.d.ts.map +1 -0
  159. package/dist/ui/app.js +2409 -0
  160. package/dist/ui/app.js.map +1 -0
  161. package/dist/ui/input.d.ts +4 -0
  162. package/dist/ui/input.d.ts.map +1 -0
  163. package/dist/ui/input.js +11 -0
  164. package/dist/ui/input.js.map +1 -0
  165. package/dist/ui/layout.d.ts +20 -0
  166. package/dist/ui/layout.d.ts.map +1 -0
  167. package/dist/ui/layout.js +37 -0
  168. package/dist/ui/layout.js.map +1 -0
  169. package/dist/ui/useDebouncedValue.d.ts +2 -0
  170. package/dist/ui/useDebouncedValue.d.ts.map +1 -0
  171. package/dist/ui/useDebouncedValue.js +10 -0
  172. package/dist/ui/useDebouncedValue.js.map +1 -0
  173. package/dist/ui/useTerminalDimensions.d.ts +5 -0
  174. package/dist/ui/useTerminalDimensions.d.ts.map +1 -0
  175. package/dist/ui/useTerminalDimensions.js +31 -0
  176. package/dist/ui/useTerminalDimensions.js.map +1 -0
  177. package/package.json +36 -0
package/dist/ui/app.js ADDED
@@ -0,0 +1,2409 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useEffect, useMemo, useRef, useState } from "react";
3
+ import { Box, Text, useApp, useInput } from "ink";
4
+ import path from "node:path";
5
+ import fs from "node:fs/promises";
6
+ import { getRecentsStore, updateRecents, getPinnedDirsStore, addPinnedDir, removePinnedDir, getLastUsedDirStore, setLastUsedDir, indexFiles, walkFilesFromFs, resolveConfig, viewCommand, pdfCommand, checkCommand, formatCommand, addCommand, newCommand, fetchViewerPatch, fetchViewerStatus, requestViewerPdf, } from "@flux-lang/cli-core";
7
+ import { AppFrame } from "../components/AppFrame.js";
8
+ import { Card } from "../components/Card.js";
9
+ import { NavList } from "../components/NavList.js";
10
+ import { CommandPaletteModal } from "../components/CommandPaletteModal.js";
11
+ import { ToastHost } from "../components/ToastHost.js";
12
+ import { HelpOverlay } from "../components/HelpOverlay.js";
13
+ import { Clickable } from "../components/Clickable.js";
14
+ import { NewWizardScreen } from "../screens/NewWizardScreen.js";
15
+ import { OpenScreen } from "../screens/OpenScreen.js";
16
+ import { DocDetailsScreen } from "../screens/DocDetailsScreen.js";
17
+ import { ExportScreen } from "../screens/ExportScreen.js";
18
+ import { DoctorScreen } from "../screens/DoctorScreen.js";
19
+ import { FormatScreen } from "../screens/FormatScreen.js";
20
+ import { EditScreen } from "../screens/EditScreen.js";
21
+ import { SettingsScreen } from "../screens/SettingsScreen.js";
22
+ import { MouseProvider } from "../state/mouse.js";
23
+ import { defaultFocusForRoute, isModalFocus } from "../state/focus.js";
24
+ import { applyOpenSearchInput, shouldEnterOpenSearch, shouldExitOpenSearch } from "../state/open-search.js";
25
+ import { useToasts } from "../state/toasts.js";
26
+ import { useProgress } from "../state/progress.js";
27
+ import { resolveActionRoute, resolveRouteAfterOpen } from "../state/dashboard-machine.js";
28
+ import { buildPaletteItems, filterPaletteItems, groupPaletteItems } from "../palette/index.js";
29
+ import { accent, color, truncateMiddle } from "../theme/index.js";
30
+ import { hasControlChars, sanitizePrintableInput } from "../ui/input.js";
31
+ import { getLayoutMetrics, isTerminalTooSmall, MIN_COLS, MIN_ROWS } from "../ui/layout.js";
32
+ import { useDebouncedValue } from "../ui/useDebouncedValue.js";
33
+ import { useTerminalDimensions } from "../ui/useTerminalDimensions.js";
34
+ import { ModalOverlay, getModalLayout } from "../ui/ModalOverlay.js";
35
+ import { PaneFrame } from "../components/PaneFrame.js";
36
+ const TEMPLATE_OPTIONS = [
37
+ { label: "Demo", value: "demo", hint: "Live slots + assets + annotations" },
38
+ { label: "Article", value: "article", hint: "Narrative article starter" },
39
+ { label: "Spec", value: "spec", hint: "Technical spec layout" },
40
+ { label: "Zine", value: "zine", hint: "Visual zine layout" },
41
+ { label: "Paper", value: "paper", hint: "Academic paper with abstract" },
42
+ { label: "Blank", value: "blank", hint: "Minimal empty layout" },
43
+ ];
44
+ const PAGE_OPTIONS = [
45
+ { label: "Letter", value: "Letter" },
46
+ { label: "A4", value: "A4" },
47
+ ];
48
+ const THEME_OPTIONS = [
49
+ { label: "Screen", value: "screen" },
50
+ { label: "Print", value: "print" },
51
+ { label: "Both", value: "both" },
52
+ ];
53
+ const FONT_OPTIONS = [
54
+ { label: "Tech", value: "tech", hint: "Inter + IBM Plex Sans + JetBrains Mono" },
55
+ { label: "Bookish", value: "bookish", hint: "Iowan Old Style + serif body" },
56
+ ];
57
+ const FALLBACK_OPTIONS = [
58
+ { label: "System fallback", value: "system", hint: "Full stack with safe fallbacks" },
59
+ { label: "Primary only", value: "none", hint: "Use primary fonts only" },
60
+ ];
61
+ const YES_NO_OPTIONS = [
62
+ { label: "Yes", value: true },
63
+ { label: "No", value: false },
64
+ ];
65
+ const CHAPTER_OPTIONS = [
66
+ { label: "1", value: 1 },
67
+ { label: "2", value: 2 },
68
+ { label: "3", value: 3 },
69
+ { label: "4", value: 4 },
70
+ { label: "5", value: 5 },
71
+ { label: "6", value: 6 },
72
+ ];
73
+ const BACKEND_LABEL = "typesetter";
74
+ const FILE_INDEX_CAP = 20000;
75
+ const FILE_INDEX_DEPTH = 7;
76
+ const SEARCH_DEBOUNCE_MS = 200;
77
+ const FILE_SCAN_BATCH_MS = 75;
78
+ export function App(props) {
79
+ const { exit } = useApp();
80
+ const initialRoute = props.mode === "new" ? "new" : "open";
81
+ const initialFocusTarget = props.helpCommand ? "help" : props.version ? "modal" : defaultFocusForRoute(initialRoute);
82
+ const [recents, setRecents] = useState([]);
83
+ const [recentsPath, setRecentsPath] = useState(undefined);
84
+ const [currentDocument, setCurrentDocument] = useState(null);
85
+ const [pinnedDirs, setPinnedDirs] = useState([]);
86
+ const [lastUsedDir, setLastUsedDirState] = useState(null);
87
+ const [navIndex, setNavIndex] = useState(1);
88
+ const [route, setRoute] = useState(initialRoute);
89
+ const [focusTarget, setFocusTarget] = useState(() => initialFocusTarget);
90
+ const focusBeforeOverlay = useRef(defaultFocusForRoute(initialRoute));
91
+ const routeBeforeOverlay = useRef(initialRoute);
92
+ const [pendingAction, setPendingAction] = useState(null);
93
+ const [openQuery, setOpenQuery] = useState("");
94
+ const [openShowAll, setOpenShowAll] = useState(false);
95
+ const [openRoot, setOpenRoot] = useState(props.cwd);
96
+ const [openRootInitialized, setOpenRootInitialized] = useState(false);
97
+ const [openFiles, setOpenFiles] = useState([]);
98
+ const [openIndexing, setOpenIndexing] = useState(false);
99
+ const [openTruncated, setOpenTruncated] = useState(false);
100
+ const [openSelectedIndex, setOpenSelectedIndex] = useState(0);
101
+ const [openFolders, setOpenFolders] = useState([]);
102
+ const [openFolderIndex, setOpenFolderIndex] = useState(0);
103
+ const [openPreview, setOpenPreview] = useState(null);
104
+ const { toasts, push: pushToast } = useToasts();
105
+ const { progress, start: startProgress, stop: stopProgress } = useProgress();
106
+ const [busy, setBusy] = useState(null);
107
+ const [paletteOpen, setPaletteOpen] = useState(false);
108
+ const [paletteQuery, setPaletteQuery] = useState("");
109
+ const [paletteIndex, setPaletteIndex] = useState(0);
110
+ const [helpOpen, setHelpOpen] = useState(Boolean(props.helpCommand));
111
+ const [versionOpen, setVersionOpen] = useState(Boolean(props.version));
112
+ const [wizardStep, setWizardStep] = useState(0);
113
+ const initialTitle = titleFromTemplate("demo");
114
+ const initialName = slugify(initialTitle);
115
+ const [wizardValues, setWizardValues] = useState({
116
+ title: initialTitle,
117
+ name: initialName,
118
+ template: "demo",
119
+ page: "Letter",
120
+ theme: "screen",
121
+ fonts: "tech",
122
+ fontFallback: "system",
123
+ assets: true,
124
+ chaptersEnabled: false,
125
+ chapters: 2,
126
+ live: true,
127
+ });
128
+ const [wizardIndexes, setWizardIndexes] = useState({
129
+ template: 0,
130
+ page: 0,
131
+ theme: 0,
132
+ fonts: 0,
133
+ fontFallback: 0,
134
+ assets: 0,
135
+ chaptersEnabled: 1,
136
+ chapters: 1,
137
+ live: 0,
138
+ });
139
+ const [wizardCreated, setWizardCreated] = useState(null);
140
+ const [wizardPostCreate, setWizardPostCreate] = useState({ openViewer: true, setCurrent: true, selectedIndex: 0 });
141
+ const [wizardLiveTouched, setWizardLiveTouched] = useState(false);
142
+ const [wizardOutDir, setWizardOutDir] = useState(props.cwd);
143
+ const [wizardDefaultsApplied, setWizardDefaultsApplied] = useState(false);
144
+ const [initialRouteHandled, setInitialRouteHandled] = useState(false);
145
+ const [viewerSession, setViewerSession] = useState(null);
146
+ const [viewerStatus, setViewerStatus] = useState(null);
147
+ const [streamOk, setStreamOk] = useState(false);
148
+ const [config, setConfig] = useState(null);
149
+ const [fluxFiles, setFluxFiles] = useState([]);
150
+ const { columns: cols, rows } = useTerminalDimensions();
151
+ const [debugLayout, setDebugLayout] = useState(false);
152
+ const [doctorSummary, setDoctorSummary] = useState("Run Doctor to check this document.");
153
+ const [doctorLogs, setDoctorLogs] = useState([]);
154
+ const [doctorLogsOpen, setDoctorLogsOpen] = useState(false);
155
+ const [formatSummary, setFormatSummary] = useState("Run Format to clean up this document.");
156
+ const [formatLogs, setFormatLogs] = useState([]);
157
+ const [formatLogsOpen, setFormatLogsOpen] = useState(false);
158
+ const [editLogs, setEditLogs] = useState([]);
159
+ const [editLogsOpen, setEditLogsOpen] = useState(false);
160
+ const [exportResultPath, setExportResultPath] = useState(null);
161
+ const [exportActionIndex, setExportActionIndex] = useState(0);
162
+ const [docActionIndex, setDocActionIndex] = useState(0);
163
+ const [wizardNameTouched, setWizardNameTouched] = useState(false);
164
+ const [docPreview, setDocPreview] = useState(null);
165
+ const openScanController = useRef(null);
166
+ const openScanId = useRef(0);
167
+ const openPreviewRequestId = useRef(0);
168
+ const docPreviewRequestId = useRef(0);
169
+ const editLaunchRef = useRef(null);
170
+ const editLaunching = useRef(false);
171
+ const navItems = useMemo(() => ([
172
+ { type: "section", label: "File" },
173
+ { type: "action", id: "open", label: "Open" },
174
+ { type: "action", id: "new", label: "New" },
175
+ { type: "section", label: "Actions" },
176
+ { type: "action", id: "edit", label: "Edit" },
177
+ { type: "action", id: "export", label: "Export PDF" },
178
+ { type: "action", id: "doctor", label: "Doctor" },
179
+ { type: "action", id: "format", label: "Format" },
180
+ ]), []);
181
+ const paletteItems = useMemo(() => buildPaletteItems({
182
+ recents: recents.filter((item) => item.type === "doc"),
183
+ fluxFiles,
184
+ activeDoc: currentDocument,
185
+ }), [recents, fluxFiles, currentDocument]);
186
+ const filteredPalette = useMemo(() => filterPaletteItems(paletteItems, paletteQuery), [paletteItems, paletteQuery]);
187
+ const limitedPalette = useMemo(() => filteredPalette.slice(0, 12), [filteredPalette]);
188
+ const paletteGroups = useMemo(() => groupPaletteItems(limitedPalette), [limitedPalette]);
189
+ const layout = useMemo(() => getLayoutMetrics(cols, rows), [cols, rows]);
190
+ const { innerWidth, navWidth, navContentWidth, paneContentWidth, navListHeight, } = layout;
191
+ const modalLayout = useMemo(() => getModalLayout({ columns: cols, rows }), [cols, rows]);
192
+ const terminalTooSmall = useMemo(() => isTerminalTooSmall(cols, rows), [cols, rows]);
193
+ const openDebouncedQuery = useDebouncedValue(openQuery, SEARCH_DEBOUNCE_MS);
194
+ const isModalOpen = paletteOpen || helpOpen || versionOpen;
195
+ const mouseDisabled = isModalOpen;
196
+ useEffect(() => {
197
+ void refreshRecents();
198
+ void refreshPinnedDirs();
199
+ void refreshLastUsedDir();
200
+ void refreshConfig();
201
+ void loadFluxFiles();
202
+ setWizardDefaultsApplied(false);
203
+ setOpenRootInitialized(false);
204
+ }, [props.cwd]);
205
+ useEffect(() => {
206
+ if (!config || wizardDefaultsApplied)
207
+ return;
208
+ const defaults = buildWizardDefaults(config);
209
+ applyWizardValues(defaults, config);
210
+ setWizardDefaultsApplied(true);
211
+ }, [config, wizardDefaultsApplied]);
212
+ useEffect(() => {
213
+ if (initialRouteHandled)
214
+ return;
215
+ if (!config)
216
+ return;
217
+ if (helpOpen || versionOpen) {
218
+ setInitialRouteHandled(true);
219
+ return;
220
+ }
221
+ const initialArgs = props.initialArgs ?? [];
222
+ if (initialArgs.length === 0) {
223
+ if (props.mode === "new")
224
+ openWizard();
225
+ setInitialRouteHandled(true);
226
+ return;
227
+ }
228
+ void handleInitialRoute(initialArgs).finally(() => setInitialRouteHandled(true));
229
+ }, [config, helpOpen, versionOpen, initialRouteHandled, props.initialArgs, props.mode]);
230
+ useEffect(() => {
231
+ if (!openRootInitialized)
232
+ return;
233
+ void setLastUsedDir(props.cwd, openRoot).then((store) => setLastUsedDirState(store.dir ?? null)).catch(() => null);
234
+ }, [openRoot, openRootInitialized, props.cwd]);
235
+ useEffect(() => {
236
+ if (!openRootInitialized)
237
+ return;
238
+ openScanController.current?.abort();
239
+ const controller = new AbortController();
240
+ openScanController.current = controller;
241
+ const scanId = ++openScanId.current;
242
+ let pendingFiles = [];
243
+ let flushTimer = null;
244
+ const flushPending = () => {
245
+ if (openScanId.current !== scanId)
246
+ return;
247
+ if (pendingFiles.length === 0)
248
+ return;
249
+ const batch = pendingFiles;
250
+ pendingFiles = [];
251
+ setOpenFiles((prev) => {
252
+ if (prev.length >= FILE_INDEX_CAP)
253
+ return prev;
254
+ const remaining = FILE_INDEX_CAP - prev.length;
255
+ const next = batch.slice(0, remaining);
256
+ return next.length === 0 ? prev : [...prev, ...next];
257
+ });
258
+ };
259
+ const scheduleFlush = () => {
260
+ if (flushTimer)
261
+ return;
262
+ flushTimer = setTimeout(() => {
263
+ flushTimer = null;
264
+ flushPending();
265
+ }, FILE_SCAN_BATCH_MS);
266
+ };
267
+ setOpenIndexing(true);
268
+ setOpenTruncated(false);
269
+ setOpenFiles([]);
270
+ const walker = walkFilesFromFs({
271
+ root: openRoot,
272
+ maxDepth: FILE_INDEX_DEPTH,
273
+ signal: controller.signal,
274
+ shouldEnterDir: (dirPath, dirent) => {
275
+ if (dirent.name === "node_modules")
276
+ return false;
277
+ if (dirent.name.startsWith("."))
278
+ return false;
279
+ return true;
280
+ },
281
+ });
282
+ void (async () => {
283
+ for await (const event of indexFiles({
284
+ walker,
285
+ maxFiles: FILE_INDEX_CAP,
286
+ signal: controller.signal,
287
+ })) {
288
+ if (openScanId.current !== scanId)
289
+ return;
290
+ if (event.type === "file") {
291
+ pendingFiles.push(event.path);
292
+ scheduleFlush();
293
+ }
294
+ else if (event.type === "done") {
295
+ if (flushTimer) {
296
+ clearTimeout(flushTimer);
297
+ flushTimer = null;
298
+ }
299
+ flushPending();
300
+ setOpenIndexing(false);
301
+ setOpenTruncated(event.truncated);
302
+ }
303
+ }
304
+ })();
305
+ void loadOpenFolders(openRoot);
306
+ setOpenSelectedIndex(0);
307
+ setOpenFolderIndex(0);
308
+ return () => {
309
+ controller.abort();
310
+ if (flushTimer)
311
+ clearTimeout(flushTimer);
312
+ };
313
+ }, [openRoot, openRootInitialized]);
314
+ const openResults = useMemo(() => {
315
+ const query = openDebouncedQuery.trim().toLowerCase();
316
+ return openFiles.filter((file) => {
317
+ if (!openShowAll && !file.toLowerCase().endsWith(".flux"))
318
+ return false;
319
+ if (!query)
320
+ return true;
321
+ const name = path.basename(file).toLowerCase();
322
+ const full = file.toLowerCase();
323
+ return name.includes(query) || full.includes(query);
324
+ });
325
+ }, [openFiles, openDebouncedQuery, openShowAll]);
326
+ useEffect(() => {
327
+ setOpenSelectedIndex((prev) => Math.max(0, Math.min(prev, Math.max(0, openResults.length - 1))));
328
+ }, [openResults.length]);
329
+ useEffect(() => {
330
+ setOpenFolderIndex((prev) => Math.max(0, Math.min(prev, Math.max(0, openFolders.length - 1))));
331
+ }, [openFolders.length]);
332
+ useEffect(() => {
333
+ const selected = openResults[openSelectedIndex];
334
+ if (!selected) {
335
+ setOpenPreview(null);
336
+ return;
337
+ }
338
+ const requestId = ++openPreviewRequestId.current;
339
+ void buildPreview(selected).then((preview) => {
340
+ if (openPreviewRequestId.current === requestId) {
341
+ setOpenPreview(preview);
342
+ }
343
+ });
344
+ }, [openResults, openSelectedIndex]);
345
+ useEffect(() => {
346
+ if (!currentDocument) {
347
+ setDocPreview(null);
348
+ return;
349
+ }
350
+ const requestId = ++docPreviewRequestId.current;
351
+ void buildPreview(currentDocument).then((preview) => {
352
+ if (docPreviewRequestId.current === requestId) {
353
+ setDocPreview(preview ? {
354
+ title: preview.title,
355
+ filePath: preview.filePath,
356
+ modified: preview.modified,
357
+ size: preview.size,
358
+ } : null);
359
+ }
360
+ });
361
+ }, [currentDocument]);
362
+ useEffect(() => {
363
+ if (route === "doc") {
364
+ setDocActionIndex(0);
365
+ }
366
+ if (route === "export") {
367
+ setExportActionIndex(exportResultPath ? 1 : 0);
368
+ }
369
+ }, [route, exportResultPath]);
370
+ useEffect(() => {
371
+ if (!viewerSession)
372
+ return;
373
+ let alive = true;
374
+ const tick = async () => {
375
+ try {
376
+ const payload = await fetchViewerPatch(viewerSession.url);
377
+ if (!alive)
378
+ return;
379
+ if (payload?.errors) {
380
+ setViewerStatus(null);
381
+ setStreamOk(false);
382
+ return;
383
+ }
384
+ if (typeof payload?.docstep === "number" || typeof payload?.time === "number") {
385
+ setViewerStatus((prev) => ({
386
+ docstep: payload.docstep ?? prev?.docstep ?? 0,
387
+ time: payload.time ?? prev?.time ?? 0,
388
+ running: prev?.running ?? true,
389
+ docstepMs: prev?.docstepMs ?? (config?.docstepMs ?? 1000),
390
+ seed: prev?.seed ?? 0,
391
+ }));
392
+ setStreamOk(true);
393
+ }
394
+ }
395
+ catch {
396
+ setStreamOk(false);
397
+ }
398
+ };
399
+ const timer = setInterval(tick, 1000);
400
+ return () => {
401
+ alive = false;
402
+ clearInterval(timer);
403
+ };
404
+ }, [viewerSession, config]);
405
+ useEffect(() => {
406
+ if (route !== "edit") {
407
+ editLaunchRef.current = null;
408
+ return;
409
+ }
410
+ if (!currentDocument)
411
+ return;
412
+ const activeSession = viewerSession && path.resolve(viewerSession.docPath) === currentDocument
413
+ ? viewerSession
414
+ : null;
415
+ const lastLaunch = editLaunchRef.current;
416
+ if (lastLaunch && lastLaunch.docPath === currentDocument && lastLaunch.url === activeSession?.url) {
417
+ return;
418
+ }
419
+ if (editLaunching.current)
420
+ return;
421
+ editLaunching.current = true;
422
+ void handleEdit(currentDocument).finally(() => {
423
+ editLaunching.current = false;
424
+ });
425
+ }, [route, currentDocument, viewerSession]);
426
+ useEffect(() => {
427
+ return () => {
428
+ if (viewerSession?.close && !props.detach) {
429
+ void viewerSession.close();
430
+ }
431
+ };
432
+ }, [viewerSession, props.detach]);
433
+ useEffect(() => {
434
+ setPaletteIndex((prev) => Math.max(0, Math.min(prev, limitedPalette.length - 1)));
435
+ }, [limitedPalette.length]);
436
+ useEffect(() => {
437
+ if (navIndex >= navItems.length) {
438
+ setNavIndex(Math.max(0, navItems.length - 1));
439
+ }
440
+ if (navItems[navIndex]?.type === "section") {
441
+ moveSelection(1);
442
+ }
443
+ }, [navItems, navIndex]);
444
+ useInput(async (input, key) => {
445
+ if (key.ctrl && input === "c") {
446
+ exit();
447
+ return;
448
+ }
449
+ if (key.escape) {
450
+ return;
451
+ }
452
+ if (versionOpen) {
453
+ if (key.return)
454
+ closeVersion();
455
+ return;
456
+ }
457
+ if (helpOpen) {
458
+ if (key.return)
459
+ closeHelp();
460
+ return;
461
+ }
462
+ if (paletteOpen) {
463
+ if (key.return) {
464
+ const item = limitedPalette[paletteIndex];
465
+ if (item) {
466
+ await handlePaletteSelect(item);
467
+ }
468
+ closePalette();
469
+ return;
470
+ }
471
+ if (key.downArrow) {
472
+ setPaletteIndex((prev) => Math.min(prev + 1, limitedPalette.length - 1));
473
+ return;
474
+ }
475
+ if (key.upArrow) {
476
+ setPaletteIndex((prev) => Math.max(prev - 1, 0));
477
+ return;
478
+ }
479
+ if (key.backspace || key.delete) {
480
+ setPaletteQuery((prev) => prev.slice(0, -1));
481
+ return;
482
+ }
483
+ const paletteInput = sanitizePrintableInput(input);
484
+ if (paletteInput) {
485
+ setPaletteQuery((prev) => prev + paletteInput);
486
+ return;
487
+ }
488
+ return;
489
+ }
490
+ }, { isActive: isModalOpen });
491
+ useInput(async (input, key) => {
492
+ if (key.ctrl && input === "c") {
493
+ exit();
494
+ return;
495
+ }
496
+ if (shouldExitOpenSearch({ focusTarget, key })) {
497
+ setFocusTarget("open.results");
498
+ return;
499
+ }
500
+ if (key.escape) {
501
+ resetToDefault();
502
+ return;
503
+ }
504
+ const hasControl = hasControlChars(input);
505
+ const hasSpecialKey = Boolean(key.return || key.backspace || key.delete || key.upArrow || key.downArrow || key.leftArrow || key.rightArrow || key.tab);
506
+ if (hasControl && !hasSpecialKey) {
507
+ return;
508
+ }
509
+ if (shouldEnterOpenSearch({ route, focusTarget, input, key })) {
510
+ setFocusTarget("open.search");
511
+ return;
512
+ }
513
+ if (key.ctrl && input === "k") {
514
+ openPalette();
515
+ return;
516
+ }
517
+ if (input === "/" && route !== "open") {
518
+ openPalette();
519
+ return;
520
+ }
521
+ if (input === "?" && focusTarget !== "open.search") {
522
+ openHelp();
523
+ return;
524
+ }
525
+ if (input?.toLowerCase() === "q" && focusTarget !== "open.search") {
526
+ exit();
527
+ return;
528
+ }
529
+ if (key.tab) {
530
+ toggleFocus();
531
+ return;
532
+ }
533
+ const printableInput = sanitizePrintableInput(input);
534
+ if (route === "new") {
535
+ await handleWizardInput(printableInput, key);
536
+ return;
537
+ }
538
+ if (focusTarget === "nav") {
539
+ await handleNavInput(input, key);
540
+ return;
541
+ }
542
+ if (route === "open") {
543
+ await handleOpenInput({ input, key });
544
+ return;
545
+ }
546
+ if (route === "doc") {
547
+ await handleDocInput(printableInput, key);
548
+ return;
549
+ }
550
+ if (route === "export") {
551
+ await handleExportInput(printableInput, key);
552
+ return;
553
+ }
554
+ if (route === "doctor") {
555
+ await handleDoctorInput(printableInput, key);
556
+ return;
557
+ }
558
+ if (route === "format") {
559
+ await handleFormatInput(printableInput, key);
560
+ return;
561
+ }
562
+ if (route === "edit") {
563
+ await handleEditInput(printableInput, key);
564
+ return;
565
+ }
566
+ }, { isActive: !isModalOpen && !isModalFocus(focusTarget) });
567
+ const wizardSteps = useMemo(() => {
568
+ const steps = [
569
+ { kind: "select", key: "template", label: "Template", options: TEMPLATE_OPTIONS },
570
+ { kind: "input", key: "title", label: "Title", placeholder: "Flux Document" },
571
+ { kind: "input", key: "name", label: "Name", placeholder: "folder-name" },
572
+ { kind: "select", key: "page", label: "Page size", options: PAGE_OPTIONS },
573
+ { kind: "select", key: "theme", label: "Theme", options: THEME_OPTIONS },
574
+ { kind: "select", key: "fonts", label: "Fonts preset", options: FONT_OPTIONS },
575
+ { kind: "select", key: "fontFallback", label: "Font fallback", options: FALLBACK_OPTIONS },
576
+ { kind: "select", key: "assets", label: "Assets folder", options: YES_NO_OPTIONS },
577
+ { kind: "select", key: "chaptersEnabled", label: "Chapters scaffold", options: YES_NO_OPTIONS },
578
+ ];
579
+ if (wizardValues.chaptersEnabled) {
580
+ steps.push({ kind: "select", key: "chapters", label: "Chapters count", options: CHAPTER_OPTIONS });
581
+ }
582
+ steps.push({ kind: "select", key: "live", label: "Live slots", options: YES_NO_OPTIONS });
583
+ steps.push({ kind: "summary", label: "Summary" });
584
+ return steps;
585
+ }, [wizardValues.chaptersEnabled]);
586
+ useEffect(() => {
587
+ setWizardStep((prev) => Math.max(0, Math.min(prev, wizardSteps.length - 1)));
588
+ }, [wizardSteps.length]);
589
+ const openResultItems = useMemo(() => openResults.map((file) => ({
590
+ id: file,
591
+ label: path.basename(file),
592
+ meta: truncateMiddle(path.dirname(file), Math.max(10, Math.floor(paneContentWidth * 0.6))),
593
+ path: file,
594
+ })), [openResults, paneContentWidth]);
595
+ const openActiveList = openResults.length > 0 ? "results" : "folders";
596
+ const recentDirs = useMemo(() => {
597
+ const dirs = recents
598
+ .filter((item) => item.type === "doc")
599
+ .map((item) => path.dirname(item.path));
600
+ const unique = [];
601
+ for (const dir of dirs) {
602
+ if (!unique.includes(dir))
603
+ unique.push(dir);
604
+ }
605
+ return unique.slice(0, 5);
606
+ }, [recents]);
607
+ const isPinned = pinnedDirs.some((dir) => path.resolve(dir) === path.resolve(openRoot));
608
+ const docPrimaryActions = useMemo(() => ([
609
+ { id: "edit", label: "Edit", icon: "✎", onClick: () => void goToEdit(), active: docActionIndex === 0 },
610
+ { id: "view", label: "View", icon: "◻︎", onClick: () => void handleView(), active: docActionIndex === 1 },
611
+ { id: "export", label: "Export PDF", icon: "⇩", onClick: () => void goToExport(), active: docActionIndex === 2 },
612
+ ]), [docActionIndex]);
613
+ const docSecondaryActions = useMemo(() => ([
614
+ { id: "doctor", label: "Doctor", icon: "✓", onClick: () => void goToDoctor(), active: docActionIndex === 3 },
615
+ { id: "format", label: "Format", icon: "≡", onClick: () => void goToFormat(), active: docActionIndex === 4 },
616
+ ]), [docActionIndex]);
617
+ if (terminalTooSmall) {
618
+ const cardWidth = Math.max(20, Math.min(innerWidth, cols));
619
+ const cardRuleWidth = Math.max(10, cardWidth - 6);
620
+ return (_jsx(MouseProvider, { disabled: true, children: _jsx(AppFrame, { debug: debugLayout, children: _jsx(Box, { flexGrow: 1, alignItems: "center", justifyContent: "center", children: _jsx(Card, { title: "Flux", meta: "", accent: true, ruleWidth: cardRuleWidth, width: cardWidth, debug: debugLayout, children: _jsx(Text, { color: color.muted, children: `Terminal too small (need ≥ ${MIN_COLS}×${MIN_ROWS}). Resize to continue.` }) }) }) }) }));
621
+ }
622
+ const rightPane = (() => {
623
+ if (route === "new") {
624
+ const step = wizardSteps[wizardStep] ?? null;
625
+ return (_jsx(NewWizardScreen, { width: paneContentWidth, step: step, stepIndex: wizardStep, stepsCount: wizardSteps.length, values: wizardValues, selectedIndex: step?.kind === "select" ? wizardIndexes[step.key] ?? 0 : 0, created: wizardCreated, postCreate: wizardPostCreate, outputDir: resolveWizardOutDir() ?? props.cwd, debug: debugLayout }));
626
+ }
627
+ if (route === "open") {
628
+ return (_jsx(OpenScreen, { width: paneContentWidth, query: openQuery, showAll: openShowAll, rootDir: openRoot, results: openResultItems, selectedIndex: openSelectedIndex, folders: openFolders, folderIndex: openFolderIndex, activeList: openActiveList, pinnedDirs: pinnedDirs, recentDirs: recentDirs, isPinned: isPinned, indexing: openIndexing, truncated: openTruncated, preview: openPreview, searchFocused: focusTarget === "open.search", onToggleShowAll: () => setOpenShowAll((prev) => !prev), onOpenSelected: () => {
629
+ setFocusTarget("open.results");
630
+ void openSelectedFile();
631
+ }, onSelectResult: (index) => {
632
+ setFocusTarget("open.results");
633
+ setOpenSelectedIndex(index);
634
+ }, onSelectFolder: (index) => {
635
+ setFocusTarget("open.results");
636
+ setOpenFolderIndex(index);
637
+ const folder = openFolders[index];
638
+ if (folder)
639
+ changeOpenRoot(folder);
640
+ }, onSelectPinned: (dir) => {
641
+ setFocusTarget("open.results");
642
+ changeOpenRoot(dir);
643
+ }, onSelectRecent: (dir) => {
644
+ setFocusTarget("open.results");
645
+ changeOpenRoot(dir);
646
+ }, onTogglePin: () => {
647
+ setFocusTarget("open.results");
648
+ void togglePinForCurrent();
649
+ }, onFocusSearch: () => setFocusTarget("open.search"), onFocusResults: () => setFocusTarget("open.results"), debug: debugLayout }));
650
+ }
651
+ if (route === "doc") {
652
+ return (_jsx(DocDetailsScreen, { width: paneContentWidth, docPath: currentDocument, preview: docPreview, primaryActions: docPrimaryActions, secondaryActions: docSecondaryActions, debug: debugLayout }));
653
+ }
654
+ if (route === "export") {
655
+ return (_jsx(ExportScreen, { width: paneContentWidth, docPath: currentDocument, outputPath: currentDocument ? currentDocument.replace(/\.flux$/i, ".pdf") : null, progress: progress, resultPath: exportResultPath, actionIndex: exportActionIndex, onExport: () => {
656
+ focusPaneForRoute();
657
+ void handleExport();
658
+ }, onOpenFile: () => {
659
+ focusPaneForRoute();
660
+ void handleOpenFileResult();
661
+ }, onReveal: () => {
662
+ focusPaneForRoute();
663
+ void handleRevealResult();
664
+ }, onCopyPath: () => {
665
+ focusPaneForRoute();
666
+ void handleCopyResultPath();
667
+ }, debug: debugLayout }));
668
+ }
669
+ if (route === "doctor") {
670
+ return (_jsx(DoctorScreen, { width: paneContentWidth, docPath: currentDocument, summary: doctorSummary, logs: doctorLogs, logsOpen: doctorLogsOpen, progress: progress, onToggleLogs: () => {
671
+ focusPaneForRoute();
672
+ setDoctorLogsOpen((prev) => !prev);
673
+ }, onRun: () => {
674
+ focusPaneForRoute();
675
+ void handleCheck();
676
+ }, debug: debugLayout }));
677
+ }
678
+ if (route === "format") {
679
+ return (_jsx(FormatScreen, { width: paneContentWidth, docPath: currentDocument, summary: formatSummary, logs: formatLogs, logsOpen: formatLogsOpen, onToggleLogs: () => {
680
+ focusPaneForRoute();
681
+ setFormatLogsOpen((prev) => !prev);
682
+ }, onRun: () => {
683
+ focusPaneForRoute();
684
+ void handleFormat();
685
+ }, debug: debugLayout }));
686
+ }
687
+ if (route === "edit") {
688
+ const viewerUrl = getViewerUrl();
689
+ return (_jsx(EditScreen, { width: paneContentWidth, docPath: currentDocument, title: docPreview?.title ?? null, viewerUrl: viewerUrl, onCopyUrl: () => void handleCopyEditorUrl(), onExport: () => {
690
+ focusPaneForRoute();
691
+ void handleExport();
692
+ }, onDoctor: () => {
693
+ focusPaneForRoute();
694
+ void handleCheck();
695
+ }, onFormat: () => {
696
+ focusPaneForRoute();
697
+ void handleFormat();
698
+ }, logs: editLogs, logsOpen: editLogsOpen, onToggleLogs: () => {
699
+ focusPaneForRoute();
700
+ setEditLogsOpen((prev) => !prev);
701
+ }, debug: debugLayout }));
702
+ }
703
+ if (route === "settings") {
704
+ return (_jsx(SettingsScreen, { width: paneContentWidth, config: config, debugLayout: debugLayout, onToggleDebug: () => setDebugLayout((prev) => !prev), debug: debugLayout }));
705
+ }
706
+ const viewerUrl = getViewerUrl();
707
+ return (_jsx(EditScreen, { width: paneContentWidth, docPath: currentDocument, title: docPreview?.title ?? null, viewerUrl: viewerUrl, onCopyUrl: () => void handleCopyEditorUrl(), onExport: () => {
708
+ focusPaneForRoute();
709
+ void handleExport();
710
+ }, onDoctor: () => {
711
+ focusPaneForRoute();
712
+ void handleCheck();
713
+ }, onFormat: () => {
714
+ focusPaneForRoute();
715
+ void handleFormat();
716
+ }, logs: editLogs, logsOpen: editLogsOpen, onToggleLogs: () => {
717
+ focusPaneForRoute();
718
+ setEditLogsOpen((prev) => !prev);
719
+ }, debug: debugLayout }));
720
+ })();
721
+ return (_jsx(MouseProvider, { disabled: mouseDisabled, children: _jsxs(Box, { position: "relative", width: "100%", height: "100%", children: [_jsxs(AppFrame, { debug: debugLayout, children: [_jsxs(Box, { flexDirection: "row", gap: 2, height: "100%", children: [_jsx(PaneFrame, { focused: focusTarget === "nav", width: navWidth, height: "100%", children: _jsxs(Box, { flexDirection: "column", gap: 1, children: [_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { children: accent("FLUX") }), _jsxs(Box, { flexDirection: "row", gap: 1, children: [_jsx(Text, { color: streamOk ? "green" : color.muted, children: streamOk ? "●" : "○" }), _jsx(Text, { color: color.muted, children: `Flux ${props.version ?? "0.x"} · ${streamOk ? "online" : "offline"} · backend: ${BACKEND_LABEL}` })] })] }), _jsx(Card, { title: "Navigation", meta: "", accent: focusTarget === "nav", ruleWidth: navContentWidth - 2, debug: debugLayout, footer: (_jsx(Text, { color: color.muted, children: "Ctrl+K palette \u00B7 / search \u00B7 Tab focus \u00B7 q quit \u00B7 ? help" })), children: _jsx(NavList, { items: navItems, selectedIndex: navIndex, onSelect: (index) => {
722
+ setNavIndex(index);
723
+ setFocusTarget("nav");
724
+ const item = navItems[index];
725
+ if (item)
726
+ void activateNavItem(item);
727
+ }, width: navContentWidth, maxHeight: navListHeight, debug: debugLayout }) })] }) }), _jsx(PaneFrame, { focused: focusTarget !== "nav", flexGrow: 1, height: "100%", children: _jsx(Clickable, { id: "pane-focus", onClick: () => focusPaneForRoute(), priority: 0, children: _jsx(Box, { flexDirection: "column", gap: 1, children: rightPane }) }) })] }), _jsx(Box, { marginTop: 1, children: _jsx(ToastHost, { toasts: toasts, busy: busy, progress: progress }) })] }), _jsx(ModalOverlay, { isOpen: paletteOpen, title: "Command Palette", subtitle: "Esc to close \u00B7 \u2191\u2193 to navigate \u00B7 Enter to run", width: modalLayout.width, height: modalLayout.height, onRequestClose: closePalette, children: _jsx(CommandPaletteModal, { query: paletteQuery, groups: paletteGroups, selectedId: limitedPalette[paletteIndex]?.id, width: modalLayout.contentWidth, debug: debugLayout }) }), _jsx(ModalOverlay, { isOpen: helpOpen, title: "Help", subtitle: "Esc to close", width: modalLayout.width, height: modalLayout.height, onRequestClose: closeHelp, children: _jsx(HelpOverlay, { width: modalLayout.contentWidth, version: props.version, recentsPath: recentsPath, backend: BACKEND_LABEL, extraLines: props.helpCommand ? getHelpLines(props.helpCommand) : undefined }) }), _jsx(ModalOverlay, { isOpen: versionOpen, title: "Flux CLI", subtitle: "Esc to close", width: modalLayout.width, height: modalLayout.height, onRequestClose: closeVersion, children: _jsx(Box, { flexDirection: "column", gap: 1, children: _jsx(Text, { color: color.muted, children: props.version ?? "version unknown" }) }) })] }) }));
728
+ async function refreshRecents() {
729
+ const store = await getRecentsStore(props.cwd);
730
+ const list = store.entries.map((entry) => ({
731
+ type: "doc",
732
+ label: path.basename(entry.path),
733
+ path: entry.path,
734
+ lastOpened: entry.lastOpened,
735
+ }));
736
+ setRecents(list);
737
+ setRecentsPath(store.storePath);
738
+ }
739
+ async function refreshPinnedDirs() {
740
+ const store = await getPinnedDirsStore(props.cwd);
741
+ setPinnedDirs(store.entries);
742
+ }
743
+ async function refreshLastUsedDir() {
744
+ const store = await getLastUsedDirStore(props.cwd);
745
+ const nextDir = store.dir ?? props.cwd;
746
+ setLastUsedDirState(store.dir);
747
+ setOpenRoot(nextDir);
748
+ setOpenRootInitialized(true);
749
+ }
750
+ async function refreshConfig() {
751
+ const resolved = await resolveConfig({ cwd: props.cwd, env: process.env });
752
+ setConfig(resolved.config);
753
+ }
754
+ async function loadFluxFiles() {
755
+ try {
756
+ const entries = await fs.readdir(props.cwd, { withFileTypes: true, encoding: "utf8" });
757
+ const files = entries
758
+ .filter((entry) => entry.isFile() && entry.name.endsWith(".flux"))
759
+ .map((entry) => path.join(props.cwd, entry.name));
760
+ setFluxFiles(files.slice(0, 20));
761
+ }
762
+ catch {
763
+ setFluxFiles([]);
764
+ }
765
+ }
766
+ function moveSelection(delta) {
767
+ let next = navIndex + delta;
768
+ while (next >= 0 && next < navItems.length) {
769
+ const item = navItems[next];
770
+ if (item && item.type !== "section") {
771
+ setNavIndex(next);
772
+ return;
773
+ }
774
+ next += delta;
775
+ }
776
+ }
777
+ function setRouteWithFocus(nextRoute) {
778
+ setRoute(nextRoute);
779
+ setFocusTarget(defaultFocusForRoute(nextRoute));
780
+ }
781
+ function focusPaneForRoute(nextRoute = route) {
782
+ setFocusTarget(defaultFocusForRoute(nextRoute));
783
+ }
784
+ function toggleFocus() {
785
+ setFocusTarget((prev) => (prev === "nav" ? defaultFocusForRoute(route) : "nav"));
786
+ }
787
+ function restoreFocusAfterOverlay() {
788
+ if (routeBeforeOverlay.current !== route) {
789
+ setFocusTarget(defaultFocusForRoute(route));
790
+ return;
791
+ }
792
+ setFocusTarget(focusBeforeOverlay.current ?? defaultFocusForRoute(route));
793
+ }
794
+ function openPalette() {
795
+ focusBeforeOverlay.current = focusTarget;
796
+ routeBeforeOverlay.current = route;
797
+ setPaletteOpen(true);
798
+ setPaletteQuery("");
799
+ setPaletteIndex(0);
800
+ setFocusTarget("palette");
801
+ }
802
+ function closePalette() {
803
+ setPaletteOpen(false);
804
+ setPaletteQuery("");
805
+ setPaletteIndex(0);
806
+ restoreFocusAfterOverlay();
807
+ }
808
+ function openHelp() {
809
+ focusBeforeOverlay.current = focusTarget;
810
+ routeBeforeOverlay.current = route;
811
+ setHelpOpen(true);
812
+ setFocusTarget("help");
813
+ }
814
+ function closeHelp() {
815
+ setHelpOpen(false);
816
+ restoreFocusAfterOverlay();
817
+ }
818
+ function closeVersion() {
819
+ setVersionOpen(false);
820
+ restoreFocusAfterOverlay();
821
+ }
822
+ async function activateNavItem(item) {
823
+ if (item.type !== "action")
824
+ return;
825
+ switch (item.id) {
826
+ case "open":
827
+ setPendingAction(null);
828
+ setRouteWithFocus("open");
829
+ return;
830
+ case "new":
831
+ openWizard();
832
+ return;
833
+ case "edit":
834
+ await requireDocAndRoute("edit");
835
+ return;
836
+ case "export":
837
+ await requireDocAndRoute("export");
838
+ return;
839
+ case "doctor":
840
+ await requireDocAndRoute("doctor");
841
+ return;
842
+ case "format":
843
+ await requireDocAndRoute("format");
844
+ return;
845
+ default:
846
+ return;
847
+ }
848
+ }
849
+ function resetToDefault() {
850
+ setPendingAction(null);
851
+ setRouteWithFocus("open");
852
+ selectNavAction("open");
853
+ setFocusTarget("open.results");
854
+ }
855
+ async function requireDocAndRoute(action) {
856
+ const resolved = resolveActionRoute(currentDocument, action);
857
+ setPendingAction(resolved.pendingAction);
858
+ if (resolved.route === "open") {
859
+ setRouteWithFocus("open");
860
+ selectNavAction("open");
861
+ setFocusTarget("open.results");
862
+ showToast("Select a document to continue.", "info");
863
+ return;
864
+ }
865
+ selectNavAction(action);
866
+ setRouteWithFocus(resolved.route);
867
+ }
868
+ function goToEdit() {
869
+ void requireDocAndRoute("edit");
870
+ }
871
+ function goToExport() {
872
+ void requireDocAndRoute("export");
873
+ }
874
+ function goToDoctor() {
875
+ void requireDocAndRoute("doctor");
876
+ }
877
+ function goToFormat() {
878
+ void requireDocAndRoute("format");
879
+ }
880
+ async function selectCurrentDoc(docPath) {
881
+ const resolved = path.resolve(docPath);
882
+ setCurrentDocument(resolved);
883
+ await updateRecents(props.cwd, resolved);
884
+ void refreshRecents();
885
+ const dir = path.dirname(resolved);
886
+ setOpenRoot(dir);
887
+ setLastUsedDirState(dir);
888
+ void setLastUsedDir(props.cwd, dir);
889
+ }
890
+ function changeOpenRoot(nextDir) {
891
+ setOpenRoot(path.resolve(nextDir));
892
+ }
893
+ async function togglePinForCurrent() {
894
+ const resolved = path.resolve(openRoot);
895
+ try {
896
+ if (pinnedDirs.some((dir) => path.resolve(dir) === resolved)) {
897
+ const store = await removePinnedDir(props.cwd, resolved);
898
+ setPinnedDirs(store.entries);
899
+ showToast("Unpinned directory", "success");
900
+ }
901
+ else {
902
+ const store = await addPinnedDir(props.cwd, resolved);
903
+ setPinnedDirs(store.entries);
904
+ showToast("Pinned directory", "success");
905
+ }
906
+ }
907
+ catch (error) {
908
+ showToast(error.message ?? "Pin update failed", "error");
909
+ }
910
+ }
911
+ async function openSelectedFile() {
912
+ const target = openResults[openSelectedIndex];
913
+ if (!target) {
914
+ showToast("Select a file first.", "error");
915
+ return;
916
+ }
917
+ await selectCurrentDoc(target);
918
+ const resolved = resolveRouteAfterOpen(pendingAction);
919
+ setPendingAction(resolved.pendingAction);
920
+ if (resolved.route !== "doc") {
921
+ selectNavAction(resolved.route);
922
+ setRouteWithFocus(resolved.route);
923
+ return;
924
+ }
925
+ setRouteWithFocus("doc");
926
+ }
927
+ async function handleNavInput(_input, key) {
928
+ if (key.downArrow) {
929
+ moveSelection(1);
930
+ return;
931
+ }
932
+ if (key.upArrow) {
933
+ moveSelection(-1);
934
+ return;
935
+ }
936
+ if (key.return) {
937
+ const item = navItems[navIndex];
938
+ if (item)
939
+ await activateNavItem(item);
940
+ }
941
+ }
942
+ async function handleOpenInput({ input, key }) {
943
+ if (key.ctrl && input === "f") {
944
+ setOpenShowAll((prev) => !prev);
945
+ return;
946
+ }
947
+ if (key.downArrow) {
948
+ if (openActiveList === "results") {
949
+ setOpenSelectedIndex((prev) => Math.min(prev + 1, Math.max(0, openResults.length - 1)));
950
+ }
951
+ else {
952
+ setOpenFolderIndex((prev) => Math.min(prev + 1, Math.max(0, openFolders.length - 1)));
953
+ }
954
+ return;
955
+ }
956
+ if (key.upArrow) {
957
+ if (openActiveList === "results") {
958
+ setOpenSelectedIndex((prev) => Math.max(0, prev - 1));
959
+ }
960
+ else {
961
+ setOpenFolderIndex((prev) => Math.max(0, prev - 1));
962
+ }
963
+ return;
964
+ }
965
+ if (key.return) {
966
+ if (openActiveList === "results") {
967
+ await openSelectedFile();
968
+ }
969
+ else {
970
+ const folder = openFolders[openFolderIndex];
971
+ if (folder)
972
+ changeOpenRoot(folder);
973
+ }
974
+ return;
975
+ }
976
+ if (focusTarget === "open.search") {
977
+ const nextQuery = applyOpenSearchInput({ focusTarget, query: openQuery, input, key });
978
+ if (nextQuery !== openQuery)
979
+ setOpenQuery(nextQuery);
980
+ return;
981
+ }
982
+ if (key.backspace || key.delete) {
983
+ const parent = path.dirname(openRoot);
984
+ if (parent && parent !== openRoot)
985
+ changeOpenRoot(parent);
986
+ }
987
+ }
988
+ async function handleDocInput(_input, key) {
989
+ const actionIds = ["edit", "view", "export", "doctor", "format"];
990
+ if (key.downArrow || key.rightArrow) {
991
+ setDocActionIndex((prev) => Math.min(prev + 1, actionIds.length - 1));
992
+ return;
993
+ }
994
+ if (key.upArrow || key.leftArrow) {
995
+ setDocActionIndex((prev) => Math.max(prev - 1, 0));
996
+ return;
997
+ }
998
+ if (key.return) {
999
+ const action = actionIds[docActionIndex];
1000
+ if (action === "edit")
1001
+ goToEdit();
1002
+ if (action === "view")
1003
+ await handleView();
1004
+ if (action === "export")
1005
+ goToExport();
1006
+ if (action === "doctor")
1007
+ goToDoctor();
1008
+ if (action === "format")
1009
+ goToFormat();
1010
+ }
1011
+ }
1012
+ async function handleExportInput(_input, key) {
1013
+ const maxIndex = exportResultPath ? 3 : 0;
1014
+ if (key.downArrow || key.rightArrow) {
1015
+ setExportActionIndex((prev) => Math.min(prev + 1, maxIndex));
1016
+ return;
1017
+ }
1018
+ if (key.upArrow || key.leftArrow) {
1019
+ setExportActionIndex((prev) => Math.max(prev - 1, 0));
1020
+ return;
1021
+ }
1022
+ if (key.return) {
1023
+ if (exportActionIndex === 0) {
1024
+ await handleExport();
1025
+ }
1026
+ else if (exportActionIndex === 1) {
1027
+ await handleOpenFileResult();
1028
+ }
1029
+ else if (exportActionIndex === 2) {
1030
+ await handleRevealResult();
1031
+ }
1032
+ else if (exportActionIndex === 3) {
1033
+ await handleCopyResultPath();
1034
+ }
1035
+ }
1036
+ }
1037
+ async function handleDoctorInput(input, key) {
1038
+ if (input?.toLowerCase() === "l") {
1039
+ setDoctorLogsOpen((prev) => !prev);
1040
+ return;
1041
+ }
1042
+ if (key.return) {
1043
+ await handleCheck();
1044
+ }
1045
+ }
1046
+ async function handleFormatInput(input, key) {
1047
+ if (input?.toLowerCase() === "l") {
1048
+ setFormatLogsOpen((prev) => !prev);
1049
+ return;
1050
+ }
1051
+ if (key.return) {
1052
+ await handleFormat();
1053
+ }
1054
+ }
1055
+ async function handleEditInput(input, key) {
1056
+ if (input?.toLowerCase() === "l" || key.return) {
1057
+ setEditLogsOpen((prev) => !prev);
1058
+ }
1059
+ }
1060
+ async function handleWizardInput(input, key) {
1061
+ if (wizardCreated) {
1062
+ if (key.upArrow || key.downArrow) {
1063
+ setWizardPostCreate((prev) => ({ ...prev, selectedIndex: prev.selectedIndex === 0 ? 1 : 0 }));
1064
+ return;
1065
+ }
1066
+ if (input === " ") {
1067
+ setWizardPostCreate((prev) => {
1068
+ if (prev.selectedIndex === 0) {
1069
+ return { ...prev, openViewer: !prev.openViewer };
1070
+ }
1071
+ return { ...prev, setCurrent: !prev.setCurrent };
1072
+ });
1073
+ return;
1074
+ }
1075
+ if (key.return) {
1076
+ const created = wizardCreated;
1077
+ setWizardCreated(null);
1078
+ const shouldSetCurrent = wizardPostCreate.setCurrent || wizardPostCreate.openViewer;
1079
+ if (shouldSetCurrent) {
1080
+ await selectCurrentDoc(created.docPath);
1081
+ }
1082
+ if (wizardPostCreate.openViewer) {
1083
+ await handleView(created.docPath);
1084
+ }
1085
+ setRouteWithFocus(shouldSetCurrent ? "doc" : "open");
1086
+ return;
1087
+ }
1088
+ if (key.escape) {
1089
+ resetToDefault();
1090
+ }
1091
+ return;
1092
+ }
1093
+ const step = wizardSteps[wizardStep];
1094
+ if (!step)
1095
+ return;
1096
+ if (step.kind === "summary") {
1097
+ if (key.return) {
1098
+ await submitWizard();
1099
+ return;
1100
+ }
1101
+ if (key.backspace || key.leftArrow) {
1102
+ setWizardStep((prev) => Math.max(0, prev - 1));
1103
+ }
1104
+ return;
1105
+ }
1106
+ if (step.kind === "select") {
1107
+ if (key.backspace || key.leftArrow) {
1108
+ setWizardStep((prev) => Math.max(0, prev - 1));
1109
+ return;
1110
+ }
1111
+ if (key.return) {
1112
+ await advanceWizard();
1113
+ return;
1114
+ }
1115
+ if (key.downArrow) {
1116
+ updateWizardChoice(1);
1117
+ return;
1118
+ }
1119
+ if (key.upArrow) {
1120
+ updateWizardChoice(-1);
1121
+ return;
1122
+ }
1123
+ return;
1124
+ }
1125
+ if (step.kind === "input") {
1126
+ const currentValue = String(wizardValues[step.key] ?? "");
1127
+ if (key.backspace || key.delete) {
1128
+ if (currentValue.length > 0) {
1129
+ updateWizardInput(step.key, currentValue.slice(0, -1));
1130
+ }
1131
+ else {
1132
+ setWizardStep((prev) => Math.max(0, prev - 1));
1133
+ }
1134
+ return;
1135
+ }
1136
+ if (key.return) {
1137
+ await advanceWizard();
1138
+ return;
1139
+ }
1140
+ if (input) {
1141
+ updateWizardInput(step.key, currentValue + input);
1142
+ }
1143
+ }
1144
+ }
1145
+ async function startViewerSession(docPath, overrides, context = "viewer") {
1146
+ await selectCurrentDoc(docPath);
1147
+ setBusy(context === "editor" ? "Starting editor..." : "Starting viewer...");
1148
+ const result = await viewCommand({
1149
+ cwd: props.cwd,
1150
+ docPath,
1151
+ docstepMs: overrides?.docstepMs ?? config?.docstepMs ?? 1000,
1152
+ seed: overrides?.seed ?? 0,
1153
+ allowNet: overrides?.allowNet ?? [],
1154
+ port: overrides?.port,
1155
+ advanceTime: overrides?.advanceTime ?? (config?.advanceTime ?? true),
1156
+ editorDist: overrides?.editorDist,
1157
+ });
1158
+ setBusy(null);
1159
+ if (!result.ok || !result.data) {
1160
+ const fallback = context === "editor" ? "Editor failed" : "Viewer failed";
1161
+ showToast(result.error?.message ?? fallback, "error");
1162
+ return null;
1163
+ }
1164
+ setViewerSession(result.data.session);
1165
+ try {
1166
+ const status = await fetchViewerStatus(result.data.session.url);
1167
+ setViewerStatus({
1168
+ docstep: status.docstep ?? 0,
1169
+ time: status.time ?? 0,
1170
+ running: status.running ?? true,
1171
+ docstepMs: status.docstepMs ?? (config?.docstepMs ?? 1000),
1172
+ seed: status.seed ?? 0,
1173
+ });
1174
+ }
1175
+ catch {
1176
+ setViewerStatus({
1177
+ docstep: 0,
1178
+ time: 0,
1179
+ running: true,
1180
+ docstepMs: config?.docstepMs ?? 1000,
1181
+ seed: 0,
1182
+ });
1183
+ }
1184
+ return result.data.session;
1185
+ }
1186
+ async function handleView(docPath, overrides) {
1187
+ const target = docPath ?? currentDocument;
1188
+ if (!target) {
1189
+ showToast("Select a document first.", "error");
1190
+ return;
1191
+ }
1192
+ const session = await startViewerSession(target, overrides);
1193
+ if (!session)
1194
+ return;
1195
+ openBrowser(session.url);
1196
+ showToast(session.attached ? "Attached to viewer" : "Viewer running", "success");
1197
+ }
1198
+ function appendEditLog(message) {
1199
+ setEditLogs((prev) => [...prev, message]);
1200
+ }
1201
+ async function handleEdit(docPath) {
1202
+ const target = docPath ?? currentDocument;
1203
+ if (!target) {
1204
+ showToast("Select a document first.", "error");
1205
+ return;
1206
+ }
1207
+ if (editLaunchRef.current?.docPath !== target) {
1208
+ setEditLogs([]);
1209
+ }
1210
+ appendEditLog("Starting editor...");
1211
+ const session = await startViewerSession(target, undefined, "editor");
1212
+ if (!session) {
1213
+ appendEditLog("Editor failed to start.");
1214
+ return;
1215
+ }
1216
+ const editorUrl = `${session.url}/edit`;
1217
+ openBrowser(editorUrl);
1218
+ editLaunchRef.current = { docPath: target, url: session.url };
1219
+ appendEditLog(session.attached ? `Attached to editor at ${editorUrl}` : `Editor running at ${editorUrl}`);
1220
+ showToast(session.attached ? "Attached to editor" : "Editor running", "success");
1221
+ }
1222
+ function getViewerUrl() {
1223
+ if (!viewerSession || !currentDocument)
1224
+ return null;
1225
+ const matches = path.resolve(viewerSession.docPath) === currentDocument;
1226
+ if (!matches)
1227
+ return null;
1228
+ return viewerSession.url;
1229
+ }
1230
+ async function handleExport() {
1231
+ if (!currentDocument) {
1232
+ showToast("Select a document first.", "error");
1233
+ return;
1234
+ }
1235
+ const defaultOut = currentDocument.replace(/\.flux$/i, ".pdf");
1236
+ setBusy("Exporting PDF...");
1237
+ startProgress("Export PDF");
1238
+ try {
1239
+ const result = viewerSession
1240
+ ? await requestViewerPdf(viewerSession.url)
1241
+ : null;
1242
+ if (result) {
1243
+ await fs.writeFile(defaultOut, result);
1244
+ }
1245
+ else {
1246
+ await pdfCommand({ file: currentDocument, outPath: defaultOut });
1247
+ }
1248
+ setExportResultPath(defaultOut);
1249
+ setExportActionIndex(1);
1250
+ showToast(`Exported ${path.basename(defaultOut)}`, "success");
1251
+ }
1252
+ catch (error) {
1253
+ showToast(`Export failed: ${error.message}`, "error");
1254
+ }
1255
+ finally {
1256
+ stopProgress();
1257
+ setBusy(null);
1258
+ }
1259
+ }
1260
+ async function handleCheck(docPath) {
1261
+ const target = docPath ?? currentDocument;
1262
+ if (!target) {
1263
+ showToast("Select a document first.", "error");
1264
+ return;
1265
+ }
1266
+ setBusy("Checking...");
1267
+ startProgress("Doctor check");
1268
+ const result = await checkCommand({ files: [target] });
1269
+ setBusy(null);
1270
+ stopProgress();
1271
+ if (!result.ok || !result.data) {
1272
+ showToast("Check failed", "error");
1273
+ setDoctorSummary("Doctor failed to run.");
1274
+ return;
1275
+ }
1276
+ const failures = result.data.results.filter((r) => !r.ok);
1277
+ if (failures.length) {
1278
+ showToast(`Doctor found ${failures.length} issue${failures.length === 1 ? "" : "s"}`, "error");
1279
+ setDoctorSummary(`Found ${failures.length} issue${failures.length === 1 ? "" : "s"}.`);
1280
+ setDoctorLogs(failures.flatMap((r) => r.errors ?? []));
1281
+ setDoctorLogsOpen(false);
1282
+ }
1283
+ else {
1284
+ showToast("All checks passed", "success");
1285
+ setDoctorSummary("All checks passed.");
1286
+ setDoctorLogs([]);
1287
+ }
1288
+ }
1289
+ async function handleFormat(docPath) {
1290
+ const target = docPath ?? currentDocument;
1291
+ if (!target) {
1292
+ showToast("Select a document first.", "error");
1293
+ return;
1294
+ }
1295
+ setBusy("Formatting...");
1296
+ setFormatSummary("Formatting document...");
1297
+ const result = await formatCommand({ file: target });
1298
+ setBusy(null);
1299
+ if (!result.ok) {
1300
+ showToast(result.error?.message ?? "Format failed", "error");
1301
+ setFormatSummary("Format failed.");
1302
+ setFormatLogs([result.error?.message ?? "Format failed."]);
1303
+ return;
1304
+ }
1305
+ showToast("Formatted document", "success");
1306
+ setFormatSummary("Format complete.");
1307
+ setFormatLogs([]);
1308
+ }
1309
+ async function handleOpenFileResult() {
1310
+ if (!exportResultPath)
1311
+ return;
1312
+ openFile(exportResultPath);
1313
+ showToast("Opened file", "success");
1314
+ }
1315
+ async function handleRevealResult() {
1316
+ if (!exportResultPath)
1317
+ return;
1318
+ revealInFinder(exportResultPath);
1319
+ showToast("Revealed in folder", "success");
1320
+ }
1321
+ async function handleCopyResultPath() {
1322
+ if (!exportResultPath)
1323
+ return;
1324
+ const ok = await copyToClipboard(exportResultPath);
1325
+ showToast(ok ? "Copied path" : "Copy failed", ok ? "success" : "error");
1326
+ }
1327
+ async function handleCopyEditorUrl() {
1328
+ const viewerUrl = getViewerUrl();
1329
+ if (!viewerUrl) {
1330
+ showToast("Editor URL not available yet.", "error");
1331
+ return;
1332
+ }
1333
+ const ok = await copyToClipboard(`${viewerUrl}/edit`);
1334
+ showToast(ok ? "Copied editor URL" : "Copy failed", ok ? "success" : "error");
1335
+ }
1336
+ async function loadOpenFolders(dir) {
1337
+ try {
1338
+ const entries = await fs.readdir(dir, { withFileTypes: true, encoding: "utf8" });
1339
+ const folders = entries
1340
+ .filter((entry) => entry.isDirectory())
1341
+ .filter((entry) => entry.name !== "node_modules" && !entry.name.startsWith("."))
1342
+ .map((entry) => path.join(dir, entry.name))
1343
+ .sort((a, b) => a.localeCompare(b));
1344
+ setOpenFolders(folders);
1345
+ }
1346
+ catch {
1347
+ setOpenFolders([]);
1348
+ }
1349
+ }
1350
+ async function buildPreview(filePath) {
1351
+ try {
1352
+ const stats = await fs.stat(filePath);
1353
+ const size = formatBytes(stats.size);
1354
+ const modified = new Date(stats.mtimeMs).toLocaleString();
1355
+ let title = null;
1356
+ let status = null;
1357
+ if (stats.size <= 256 * 1024) {
1358
+ const text = await fs.readFile(filePath, "utf8");
1359
+ const sample = text.slice(0, 8000);
1360
+ const titleMatch = /title\\s*=\\s*\"([^\"]+)\"/.exec(sample);
1361
+ title = titleMatch ? titleMatch[1] : null;
1362
+ if (/document\\s*\\{/.test(sample)) {
1363
+ status = titleMatch ? "valid" : "warnings";
1364
+ }
1365
+ else {
1366
+ status = "errors";
1367
+ }
1368
+ }
1369
+ return {
1370
+ title,
1371
+ filePath,
1372
+ modified,
1373
+ size,
1374
+ status,
1375
+ };
1376
+ }
1377
+ catch {
1378
+ return {
1379
+ title: null,
1380
+ filePath,
1381
+ modified: undefined,
1382
+ size: undefined,
1383
+ status: "errors",
1384
+ };
1385
+ }
1386
+ }
1387
+ function updateWizardInput(key, nextValue) {
1388
+ if (key === "name") {
1389
+ setWizardNameTouched(true);
1390
+ }
1391
+ setWizardValues((prev) => {
1392
+ if (key === "title") {
1393
+ const nextTitle = nextValue;
1394
+ const nextName = wizardNameTouched ? prev.name : slugify(nextTitle);
1395
+ return { ...prev, title: nextTitle, name: nextName };
1396
+ }
1397
+ const nextName = slugify(nextValue);
1398
+ return { ...prev, name: nextName };
1399
+ });
1400
+ }
1401
+ function titleFromTemplate(template) {
1402
+ const map = {
1403
+ demo: "Flux Demo",
1404
+ article: "Flux Article",
1405
+ spec: "Flux Spec",
1406
+ zine: "Flux Zine",
1407
+ paper: "Flux Paper",
1408
+ blank: "Flux Document",
1409
+ };
1410
+ return map[template] ?? "Flux Document";
1411
+ }
1412
+ function slugify(value) {
1413
+ return value
1414
+ .trim()
1415
+ .toLowerCase()
1416
+ .replace(/[^a-z0-9]+/g, "-")
1417
+ .replace(/^-+|-+$/g, "")
1418
+ .slice(0, 60) || "flux-document";
1419
+ }
1420
+ function formatBytes(size) {
1421
+ if (!Number.isFinite(size))
1422
+ return "";
1423
+ if (size < 1024)
1424
+ return `${size} B`;
1425
+ const units = ["KB", "MB", "GB"];
1426
+ let value = size / 1024;
1427
+ let idx = 0;
1428
+ while (value >= 1024 && idx < units.length - 1) {
1429
+ value /= 1024;
1430
+ idx += 1;
1431
+ }
1432
+ return `${value.toFixed(value < 10 ? 1 : 0)} ${units[idx]}`;
1433
+ }
1434
+ async function handlePaletteSelect(item) {
1435
+ if (item.kind === "template") {
1436
+ await runTemplate(item.payload.template);
1437
+ return;
1438
+ }
1439
+ if (item.kind === "doc" || item.kind === "file") {
1440
+ await selectCurrentDoc(item.payload.path);
1441
+ setRouteWithFocus("doc");
1442
+ return;
1443
+ }
1444
+ if (item.kind === "action") {
1445
+ if (item.payload.action === "open") {
1446
+ setPendingAction(null);
1447
+ setRouteWithFocus("open");
1448
+ return;
1449
+ }
1450
+ if (item.payload.action === "new") {
1451
+ openWizard();
1452
+ return;
1453
+ }
1454
+ if (item.payload.action === "edit")
1455
+ await requireDocAndRoute("edit");
1456
+ if (item.payload.action === "export")
1457
+ await requireDocAndRoute("export");
1458
+ if (item.payload.action === "doctor")
1459
+ await requireDocAndRoute("doctor");
1460
+ if (item.payload.action === "format")
1461
+ await requireDocAndRoute("format");
1462
+ }
1463
+ }
1464
+ async function runAdd(kind, docPath, options) {
1465
+ const target = docPath ?? currentDocument;
1466
+ if (!target) {
1467
+ showToast("Select a document first.", "error");
1468
+ return;
1469
+ }
1470
+ setBusy(`Adding ${kind}...`);
1471
+ const result = await addCommand({
1472
+ cwd: props.cwd,
1473
+ file: target,
1474
+ kind: kind,
1475
+ text: options?.text,
1476
+ heading: options?.heading,
1477
+ label: options?.label,
1478
+ noHeading: options?.noHeading,
1479
+ noCheck: options?.noCheck,
1480
+ });
1481
+ setBusy(null);
1482
+ if (!result.ok) {
1483
+ showToast(result.error?.message ?? "Add failed", "error");
1484
+ return;
1485
+ }
1486
+ showToast(`Added ${kind}`, "success");
1487
+ }
1488
+ async function runTemplate(template) {
1489
+ const title = titleFromTemplate(template);
1490
+ const name = slugify(title);
1491
+ const outRoot = resolveWizardOutDir() ?? props.cwd;
1492
+ const outDir = path.join(outRoot, name);
1493
+ setBusy(`Creating ${template}...`);
1494
+ const result = await newCommand({
1495
+ cwd: props.cwd,
1496
+ template: template,
1497
+ out: outDir,
1498
+ title,
1499
+ slug: name,
1500
+ page: (config?.defaultPageSize ?? "Letter"),
1501
+ theme: (config?.defaultTheme ?? "screen"),
1502
+ fonts: (config?.defaultFonts ?? "tech"),
1503
+ fontFallback: "system",
1504
+ assets: true,
1505
+ chapters: 0,
1506
+ live: template === "demo",
1507
+ });
1508
+ setBusy(null);
1509
+ if (!result.ok || !result.data) {
1510
+ showToast(result.error?.message ?? "New failed", "error");
1511
+ return;
1512
+ }
1513
+ await selectCurrentDoc(result.data.docPath);
1514
+ showToast("Document created", "success");
1515
+ if (template === "blank") {
1516
+ setRouteWithFocus("edit");
1517
+ }
1518
+ else {
1519
+ setRouteWithFocus("doc");
1520
+ }
1521
+ }
1522
+ function buildWizardDefaults(cfg) {
1523
+ const template = "demo";
1524
+ const title = titleFromTemplate(template);
1525
+ const name = slugify(title);
1526
+ return {
1527
+ title,
1528
+ name,
1529
+ template,
1530
+ page: (cfg?.defaultPageSize ?? "Letter"),
1531
+ theme: (cfg?.defaultTheme ?? "screen"),
1532
+ fonts: (cfg?.defaultFonts ?? "tech"),
1533
+ fontFallback: "system",
1534
+ assets: true,
1535
+ chaptersEnabled: false,
1536
+ chapters: 2,
1537
+ live: template === "demo",
1538
+ };
1539
+ }
1540
+ function resolveWizardOutDir(outDir) {
1541
+ if (outDir)
1542
+ return outDir;
1543
+ if (wizardOutDir)
1544
+ return wizardOutDir;
1545
+ return props.cwd;
1546
+ }
1547
+ function indexForValue(options, value) {
1548
+ const idx = options.findIndex((opt) => opt.value === value);
1549
+ return idx >= 0 ? idx : 0;
1550
+ }
1551
+ function applyWizardValues(nextValues, cfg, outDir) {
1552
+ setWizardValues(nextValues);
1553
+ setWizardIndexes({
1554
+ template: indexForValue(TEMPLATE_OPTIONS, nextValues.template),
1555
+ page: indexForValue(PAGE_OPTIONS, nextValues.page),
1556
+ theme: indexForValue(THEME_OPTIONS, nextValues.theme),
1557
+ fonts: indexForValue(FONT_OPTIONS, nextValues.fonts),
1558
+ fontFallback: indexForValue(FALLBACK_OPTIONS, nextValues.fontFallback),
1559
+ assets: indexForValue(YES_NO_OPTIONS, nextValues.assets),
1560
+ chaptersEnabled: indexForValue(YES_NO_OPTIONS, nextValues.chaptersEnabled),
1561
+ chapters: indexForValue(CHAPTER_OPTIONS, nextValues.chapters),
1562
+ live: indexForValue(YES_NO_OPTIONS, nextValues.live),
1563
+ });
1564
+ const nextOut = outDir ?? props.cwd;
1565
+ setWizardOutDir(nextOut);
1566
+ }
1567
+ function updateWizardChoice(direction) {
1568
+ const step = wizardSteps[wizardStep];
1569
+ if (!step || step.kind !== "select")
1570
+ return;
1571
+ const max = step.options.length - 1;
1572
+ const currentIndex = wizardIndexes[step.key] ?? 0;
1573
+ const nextIndex = Math.max(0, Math.min(max, currentIndex + direction));
1574
+ const nextValue = step.options[nextIndex]?.value;
1575
+ setWizardIndexes((prev) => ({ ...prev, [step.key]: nextIndex }));
1576
+ setWizardValues((prev) => {
1577
+ const next = { ...prev, [step.key]: nextValue };
1578
+ if (step.key === "template" && !wizardLiveTouched) {
1579
+ const nextLive = nextValue === "demo";
1580
+ next.live = nextLive;
1581
+ setWizardIndexes((indexes) => ({
1582
+ ...indexes,
1583
+ live: indexForValue(YES_NO_OPTIONS, nextLive),
1584
+ }));
1585
+ }
1586
+ if (step.key === "template") {
1587
+ const prevTitle = prev.title;
1588
+ const defaultTitle = titleFromTemplate(prev.template);
1589
+ if (prevTitle === defaultTitle) {
1590
+ const nextTitle = titleFromTemplate(nextValue);
1591
+ next.title = nextTitle;
1592
+ if (!wizardNameTouched) {
1593
+ next.name = slugify(nextTitle);
1594
+ }
1595
+ }
1596
+ }
1597
+ if (step.key === "chaptersEnabled" && nextValue === false) {
1598
+ next.chaptersEnabled = false;
1599
+ }
1600
+ if (step.key === "live") {
1601
+ setWizardLiveTouched(true);
1602
+ }
1603
+ return next;
1604
+ });
1605
+ }
1606
+ async function advanceWizard() {
1607
+ const step = wizardSteps[wizardStep];
1608
+ if (!step)
1609
+ return;
1610
+ if (step.kind === "summary") {
1611
+ await submitWizard();
1612
+ return;
1613
+ }
1614
+ if (wizardStep < wizardSteps.length - 1) {
1615
+ setWizardStep((prev) => prev + 1);
1616
+ }
1617
+ }
1618
+ async function submitWizard(valuesOverride, outDirOverride) {
1619
+ const values = valuesOverride ?? wizardValues;
1620
+ const outputRoot = resolveWizardOutDir(outDirOverride) ?? props.cwd;
1621
+ const outputDir = path.join(outputRoot, values.name);
1622
+ setBusy("Creating document...");
1623
+ const result = await newCommand({
1624
+ cwd: props.cwd,
1625
+ template: values.template,
1626
+ out: outputDir,
1627
+ page: values.page,
1628
+ theme: values.theme,
1629
+ fonts: values.fonts,
1630
+ fontFallback: values.fontFallback,
1631
+ assets: values.assets,
1632
+ chapters: values.chaptersEnabled ? values.chapters : 0,
1633
+ live: values.live,
1634
+ title: values.title,
1635
+ slug: values.name,
1636
+ });
1637
+ setBusy(null);
1638
+ if (!result.ok || !result.data) {
1639
+ showToast(result.error?.message ?? "New failed", "error");
1640
+ return;
1641
+ }
1642
+ if (values.template === "blank") {
1643
+ await selectCurrentDoc(result.data.docPath);
1644
+ setWizardCreated(null);
1645
+ setRouteWithFocus("edit");
1646
+ showToast("Document created", "success");
1647
+ return;
1648
+ }
1649
+ setWizardCreated(result.data);
1650
+ setWizardPostCreate({ openViewer: true, setCurrent: true, selectedIndex: 0 });
1651
+ showToast("Document created", "success");
1652
+ }
1653
+ function openWizard(reset = true) {
1654
+ setRouteWithFocus("new");
1655
+ if (reset) {
1656
+ const defaults = buildWizardDefaults(config);
1657
+ applyWizardValues(defaults, config);
1658
+ setWizardStep(0);
1659
+ setWizardCreated(null);
1660
+ setWizardPostCreate({ openViewer: true, setCurrent: true, selectedIndex: 0 });
1661
+ setWizardLiveTouched(false);
1662
+ setWizardNameTouched(false);
1663
+ }
1664
+ }
1665
+ function selectNavAction(id) {
1666
+ const idx = navItems.findIndex((entry) => entry.type === "action" && entry.id === id);
1667
+ if (idx >= 0)
1668
+ setNavIndex(idx);
1669
+ }
1670
+ async function handleInitialRoute(initialArgs) {
1671
+ const [command, ...rest] = initialArgs;
1672
+ if (!command)
1673
+ return;
1674
+ switch (command) {
1675
+ case "new": {
1676
+ const parsed = parseNewArgsForUi(rest);
1677
+ if (parsed.unknownTemplate) {
1678
+ showToast(`Unknown template '${parsed.unknownTemplate}'.`, "error");
1679
+ }
1680
+ const defaults = buildWizardDefaults(config);
1681
+ const next = { ...defaults };
1682
+ if (parsed.template) {
1683
+ next.template = parsed.template;
1684
+ next.live = parsed.live ?? (parsed.template === "demo");
1685
+ }
1686
+ if (parsed.page)
1687
+ next.page = parsed.page;
1688
+ if (parsed.theme)
1689
+ next.theme = parsed.theme;
1690
+ if (parsed.fonts)
1691
+ next.fonts = parsed.fonts;
1692
+ if (parsed.fontFallback)
1693
+ next.fontFallback = parsed.fontFallback;
1694
+ if (parsed.assets !== undefined)
1695
+ next.assets = parsed.assets;
1696
+ if (parsed.chapters !== undefined) {
1697
+ next.chaptersEnabled = parsed.chapters > 0;
1698
+ next.chapters = parsed.chapters > 0 ? Math.max(1, parsed.chapters) : next.chapters;
1699
+ }
1700
+ if (parsed.live !== undefined) {
1701
+ next.live = parsed.live;
1702
+ }
1703
+ let outOverride = parsed.out;
1704
+ if (parsed.out && parsed.out.endsWith(".flux")) {
1705
+ const resolved = path.resolve(props.cwd, parsed.out);
1706
+ next.name = slugify(path.basename(resolved, ".flux"));
1707
+ outOverride = path.dirname(resolved);
1708
+ }
1709
+ if (!wizardNameTouched) {
1710
+ next.name = slugify(next.title);
1711
+ }
1712
+ applyWizardValues(next, config, outOverride);
1713
+ openWizard(true);
1714
+ if (parsed.provided.live) {
1715
+ setWizardLiveTouched(true);
1716
+ }
1717
+ const autoRun = Boolean(parsed.template
1718
+ && parsed.page
1719
+ && parsed.theme
1720
+ && parsed.fonts
1721
+ && parsed.assets !== undefined
1722
+ && parsed.chapters !== undefined
1723
+ && parsed.live !== undefined);
1724
+ if (autoRun) {
1725
+ await submitWizard(next, outOverride);
1726
+ }
1727
+ return;
1728
+ }
1729
+ case "view": {
1730
+ const parsed = parseViewArgsForUi(rest);
1731
+ const target = parsed.file ?? currentDocument ?? null;
1732
+ if (!target) {
1733
+ showToast("flux view: missing <file>", "error");
1734
+ return;
1735
+ }
1736
+ await handleView(target, {
1737
+ docstepMs: parsed.docstepMs,
1738
+ seed: parsed.seed,
1739
+ allowNet: parsed.allowNet,
1740
+ port: parsed.port,
1741
+ advanceTime: parsed.advanceTime,
1742
+ editorDist: parsed.editorDist,
1743
+ });
1744
+ setRouteWithFocus("doc");
1745
+ return;
1746
+ }
1747
+ case "check": {
1748
+ const target = firstFileArg(rest) ?? currentDocument ?? null;
1749
+ if (!target) {
1750
+ showToast("flux check: missing <file>", "error");
1751
+ return;
1752
+ }
1753
+ await selectCurrentDoc(target);
1754
+ setRouteWithFocus("doctor");
1755
+ await handleCheck(target);
1756
+ return;
1757
+ }
1758
+ case "fmt": {
1759
+ const target = firstFileArg(rest) ?? currentDocument ?? null;
1760
+ if (!target) {
1761
+ showToast("flux fmt: missing <file>", "error");
1762
+ return;
1763
+ }
1764
+ await selectCurrentDoc(target);
1765
+ setRouteWithFocus("format");
1766
+ await handleFormat(target);
1767
+ return;
1768
+ }
1769
+ case "add": {
1770
+ const parsed = parseAddArgsForUi(rest);
1771
+ if (parsed.kind) {
1772
+ await runAdd(parsed.kind, parsed.file, parsed);
1773
+ if (parsed.file)
1774
+ await selectCurrentDoc(parsed.file);
1775
+ setRouteWithFocus("edit");
1776
+ }
1777
+ else {
1778
+ showToast("flux add: missing <kind> (use --no-ui for prompts)", "error");
1779
+ }
1780
+ return;
1781
+ }
1782
+ case "pdf": {
1783
+ const parsed = parsePdfArgsForUi(rest);
1784
+ if (!parsed.file || !parsed.outPath) {
1785
+ showToast("flux pdf: missing <file> or --out", "error");
1786
+ return;
1787
+ }
1788
+ setBusy("Exporting PDF...");
1789
+ const result = await pdfCommand({ file: parsed.file, outPath: parsed.outPath, seed: parsed.seed, docstep: parsed.docstep });
1790
+ setBusy(null);
1791
+ if (!result.ok) {
1792
+ showToast(result.error?.message ?? "Export failed", "error");
1793
+ return;
1794
+ }
1795
+ showToast(`Wrote ${parsed.outPath}`, "success");
1796
+ setExportResultPath(parsed.outPath);
1797
+ setExportActionIndex(1);
1798
+ await selectCurrentDoc(parsed.file);
1799
+ setRouteWithFocus("export");
1800
+ return;
1801
+ }
1802
+ case "config": {
1803
+ setRouteWithFocus("settings");
1804
+ return;
1805
+ }
1806
+ case "parse":
1807
+ case "render":
1808
+ case "tick":
1809
+ case "step": {
1810
+ showToast("Use --no-ui for JSON output.", "error");
1811
+ return;
1812
+ }
1813
+ default:
1814
+ return;
1815
+ }
1816
+ }
1817
+ function showToast(message, kind = "info") {
1818
+ pushToast(message, kind);
1819
+ }
1820
+ }
1821
+ function openBrowser(url) {
1822
+ const command = process.platform === "darwin"
1823
+ ? "open"
1824
+ : process.platform === "win32"
1825
+ ? "cmd"
1826
+ : "xdg-open";
1827
+ const args = process.platform === "win32" ? ["/c", "start", url.replace(/&/g, "^&")] : [url];
1828
+ // eslint-disable-next-line @typescript-eslint/no-floating-promises
1829
+ import("node:child_process").then(({ spawn }) => {
1830
+ spawn(command, args, { stdio: "ignore", detached: true });
1831
+ });
1832
+ }
1833
+ function openFile(target) {
1834
+ const resolved = path.resolve(target);
1835
+ const command = process.platform === "darwin"
1836
+ ? "open"
1837
+ : process.platform === "win32"
1838
+ ? "cmd"
1839
+ : "xdg-open";
1840
+ const args = process.platform === "win32"
1841
+ ? ["/c", "start", "", resolved.replace(/&/g, "^&")]
1842
+ : [resolved];
1843
+ // eslint-disable-next-line @typescript-eslint/no-floating-promises
1844
+ import("node:child_process").then(({ spawn }) => {
1845
+ spawn(command, args, { stdio: "ignore", detached: true });
1846
+ });
1847
+ }
1848
+ function revealInFinder(target) {
1849
+ const resolved = path.resolve(target);
1850
+ const command = process.platform === "darwin"
1851
+ ? "open"
1852
+ : process.platform === "win32"
1853
+ ? "explorer"
1854
+ : "xdg-open";
1855
+ const args = process.platform === "darwin"
1856
+ ? ["-R", resolved]
1857
+ : process.platform === "win32"
1858
+ ? ["/select,", resolved]
1859
+ : [path.dirname(resolved)];
1860
+ // eslint-disable-next-line @typescript-eslint/no-floating-promises
1861
+ import("node:child_process").then(({ spawn }) => {
1862
+ spawn(command, args, { stdio: "ignore", detached: true });
1863
+ });
1864
+ }
1865
+ async function copyToClipboard(value) {
1866
+ const text = value ?? "";
1867
+ try {
1868
+ const clipboardy = await import("clipboardy").catch(() => null);
1869
+ if (clipboardy?.default?.write) {
1870
+ await clipboardy.default.write(text);
1871
+ return true;
1872
+ }
1873
+ if (process.platform === "darwin") {
1874
+ const { spawn } = await import("node:child_process");
1875
+ const proc = spawn("pbcopy");
1876
+ proc.stdin?.write(text);
1877
+ proc.stdin?.end();
1878
+ return true;
1879
+ }
1880
+ if (process.platform === "win32") {
1881
+ const { spawn } = await import("node:child_process");
1882
+ const proc = spawn("clip");
1883
+ proc.stdin?.write(text);
1884
+ proc.stdin?.end();
1885
+ return true;
1886
+ }
1887
+ const { spawn } = await import("node:child_process");
1888
+ const proc = spawn("xclip", ["-selection", "clipboard"]);
1889
+ proc.stdin?.write(text);
1890
+ proc.stdin?.end();
1891
+ return true;
1892
+ }
1893
+ catch {
1894
+ return false;
1895
+ }
1896
+ }
1897
+ function parseNewArgsForUi(args) {
1898
+ const provided = {
1899
+ page: false,
1900
+ theme: false,
1901
+ fonts: false,
1902
+ fontFallback: false,
1903
+ assets: false,
1904
+ chapters: false,
1905
+ live: false,
1906
+ };
1907
+ let template;
1908
+ let unknownTemplate;
1909
+ let out;
1910
+ let page;
1911
+ let theme;
1912
+ let fonts;
1913
+ let fontFallback;
1914
+ let assets;
1915
+ let chapters;
1916
+ let live;
1917
+ for (let i = 0; i < args.length; i += 1) {
1918
+ const arg = args[i];
1919
+ if (!arg.startsWith("-") && !template && !unknownTemplate) {
1920
+ if (isTemplateName(arg)) {
1921
+ template = arg;
1922
+ }
1923
+ else {
1924
+ unknownTemplate = arg;
1925
+ }
1926
+ continue;
1927
+ }
1928
+ if (arg === "--out") {
1929
+ out = args[i + 1];
1930
+ i += 1;
1931
+ continue;
1932
+ }
1933
+ if (arg.startsWith("--out=")) {
1934
+ out = arg.slice("--out=".length);
1935
+ continue;
1936
+ }
1937
+ if (arg === "--page") {
1938
+ page = normalizePage(args[i + 1]);
1939
+ provided.page = true;
1940
+ i += 1;
1941
+ continue;
1942
+ }
1943
+ if (arg.startsWith("--page=")) {
1944
+ page = normalizePage(arg.slice("--page=".length));
1945
+ provided.page = true;
1946
+ continue;
1947
+ }
1948
+ if (arg === "--theme") {
1949
+ theme = normalizeTheme(args[i + 1]);
1950
+ provided.theme = true;
1951
+ i += 1;
1952
+ continue;
1953
+ }
1954
+ if (arg.startsWith("--theme=")) {
1955
+ theme = normalizeTheme(arg.slice("--theme=".length));
1956
+ provided.theme = true;
1957
+ continue;
1958
+ }
1959
+ if (arg === "--fonts") {
1960
+ fonts = normalizeFonts(args[i + 1]);
1961
+ provided.fonts = true;
1962
+ i += 1;
1963
+ continue;
1964
+ }
1965
+ if (arg.startsWith("--fonts=")) {
1966
+ fonts = normalizeFonts(arg.slice("--fonts=".length));
1967
+ provided.fonts = true;
1968
+ continue;
1969
+ }
1970
+ if (arg === "--fallback" || arg === "--font-fallback") {
1971
+ fontFallback = normalizeFallback(args[i + 1]);
1972
+ provided.fontFallback = true;
1973
+ i += 1;
1974
+ continue;
1975
+ }
1976
+ if (arg.startsWith("--fallback=")) {
1977
+ fontFallback = normalizeFallback(arg.slice("--fallback=".length));
1978
+ provided.fontFallback = true;
1979
+ continue;
1980
+ }
1981
+ if (arg.startsWith("--font-fallback=")) {
1982
+ fontFallback = normalizeFallback(arg.slice("--font-fallback=".length));
1983
+ provided.fontFallback = true;
1984
+ continue;
1985
+ }
1986
+ if (arg === "--assets") {
1987
+ assets = parseYesNoArg(args[i + 1]);
1988
+ provided.assets = true;
1989
+ i += 1;
1990
+ continue;
1991
+ }
1992
+ if (arg.startsWith("--assets=")) {
1993
+ assets = parseYesNoArg(arg.slice("--assets=".length));
1994
+ provided.assets = true;
1995
+ continue;
1996
+ }
1997
+ if (arg === "--chapters") {
1998
+ chapters = parseNumberArg(args[i + 1]);
1999
+ provided.chapters = true;
2000
+ i += 1;
2001
+ continue;
2002
+ }
2003
+ if (arg.startsWith("--chapters=")) {
2004
+ chapters = parseNumberArg(arg.slice("--chapters=".length));
2005
+ provided.chapters = true;
2006
+ continue;
2007
+ }
2008
+ if (arg === "--live") {
2009
+ live = parseYesNoArg(args[i + 1]);
2010
+ provided.live = true;
2011
+ i += 1;
2012
+ continue;
2013
+ }
2014
+ if (arg.startsWith("--live=")) {
2015
+ live = parseYesNoArg(arg.slice("--live=".length));
2016
+ provided.live = true;
2017
+ continue;
2018
+ }
2019
+ }
2020
+ return {
2021
+ template,
2022
+ unknownTemplate,
2023
+ out,
2024
+ page,
2025
+ theme,
2026
+ fonts,
2027
+ fontFallback,
2028
+ assets,
2029
+ chapters: Number.isFinite(chapters ?? NaN) ? chapters : undefined,
2030
+ live,
2031
+ provided,
2032
+ };
2033
+ }
2034
+ function parseViewArgsForUi(args) {
2035
+ let file;
2036
+ let port;
2037
+ let docstepMs;
2038
+ let seed;
2039
+ let advanceTime;
2040
+ let editorDist;
2041
+ const allowNet = [];
2042
+ for (let i = 0; i < args.length; i += 1) {
2043
+ const arg = args[i];
2044
+ if (arg === "--port") {
2045
+ port = parseNumberArg(args[i + 1]);
2046
+ i += 1;
2047
+ continue;
2048
+ }
2049
+ if (arg.startsWith("--port=")) {
2050
+ port = parseNumberArg(arg.slice("--port=".length));
2051
+ continue;
2052
+ }
2053
+ if (arg === "--docstep-ms") {
2054
+ docstepMs = parseNumberArg(args[i + 1]);
2055
+ i += 1;
2056
+ continue;
2057
+ }
2058
+ if (arg.startsWith("--docstep-ms=")) {
2059
+ docstepMs = parseNumberArg(arg.slice("--docstep-ms=".length));
2060
+ continue;
2061
+ }
2062
+ if (arg === "--seed") {
2063
+ seed = parseNumberArg(args[i + 1]);
2064
+ i += 1;
2065
+ continue;
2066
+ }
2067
+ if (arg.startsWith("--seed=")) {
2068
+ seed = parseNumberArg(arg.slice("--seed=".length));
2069
+ continue;
2070
+ }
2071
+ if (arg === "--allow-net") {
2072
+ const raw = args[i + 1] ?? "";
2073
+ allowNet.push(...raw.split(",").map((item) => item.trim()).filter(Boolean));
2074
+ i += 1;
2075
+ continue;
2076
+ }
2077
+ if (arg.startsWith("--allow-net=")) {
2078
+ const raw = arg.slice("--allow-net=".length);
2079
+ allowNet.push(...raw.split(",").map((item) => item.trim()).filter(Boolean));
2080
+ continue;
2081
+ }
2082
+ if (arg === "--editor-dist") {
2083
+ editorDist = args[i + 1];
2084
+ i += 1;
2085
+ continue;
2086
+ }
2087
+ if (arg.startsWith("--editor-dist=")) {
2088
+ editorDist = arg.slice("--editor-dist=".length);
2089
+ continue;
2090
+ }
2091
+ if (arg === "--no-time") {
2092
+ advanceTime = false;
2093
+ continue;
2094
+ }
2095
+ if (arg === "--tty") {
2096
+ continue;
2097
+ }
2098
+ if (!arg.startsWith("-")) {
2099
+ file = arg;
2100
+ }
2101
+ }
2102
+ return { file, port, docstepMs, seed, allowNet, advanceTime, editorDist };
2103
+ }
2104
+ function parseAddArgsForUi(args) {
2105
+ let kind;
2106
+ let file;
2107
+ let text;
2108
+ let heading;
2109
+ let label;
2110
+ let noHeading = false;
2111
+ let noCheck = false;
2112
+ for (let i = 0; i < args.length; i += 1) {
2113
+ const arg = args[i];
2114
+ if (!arg.startsWith("-") && !kind) {
2115
+ kind = arg;
2116
+ continue;
2117
+ }
2118
+ if (arg === "--text") {
2119
+ text = args[i + 1];
2120
+ i += 1;
2121
+ continue;
2122
+ }
2123
+ if (arg.startsWith("--text=")) {
2124
+ text = arg.slice("--text=".length);
2125
+ continue;
2126
+ }
2127
+ if (arg === "--heading") {
2128
+ heading = args[i + 1];
2129
+ i += 1;
2130
+ continue;
2131
+ }
2132
+ if (arg.startsWith("--heading=")) {
2133
+ heading = arg.slice("--heading=".length);
2134
+ continue;
2135
+ }
2136
+ if (arg === "--label") {
2137
+ label = args[i + 1];
2138
+ i += 1;
2139
+ continue;
2140
+ }
2141
+ if (arg.startsWith("--label=")) {
2142
+ label = arg.slice("--label=".length);
2143
+ continue;
2144
+ }
2145
+ if (arg === "--no-heading") {
2146
+ noHeading = true;
2147
+ continue;
2148
+ }
2149
+ if (arg === "--no-check") {
2150
+ noCheck = true;
2151
+ continue;
2152
+ }
2153
+ if (!arg.startsWith("-")) {
2154
+ file = arg;
2155
+ }
2156
+ }
2157
+ return { kind, file, text, heading, label, noHeading, noCheck };
2158
+ }
2159
+ function parsePdfArgsForUi(args) {
2160
+ let outPath;
2161
+ let seed;
2162
+ let docstep;
2163
+ let file;
2164
+ for (let i = 0; i < args.length; i += 1) {
2165
+ const arg = args[i];
2166
+ if (arg === "--out") {
2167
+ outPath = args[i + 1];
2168
+ i += 1;
2169
+ continue;
2170
+ }
2171
+ if (arg.startsWith("--out=")) {
2172
+ outPath = arg.slice("--out=".length);
2173
+ continue;
2174
+ }
2175
+ if (arg === "--seed") {
2176
+ seed = parseNumberArg(args[i + 1]);
2177
+ i += 1;
2178
+ continue;
2179
+ }
2180
+ if (arg.startsWith("--seed=")) {
2181
+ seed = parseNumberArg(arg.slice("--seed=".length));
2182
+ continue;
2183
+ }
2184
+ if (arg === "--docstep") {
2185
+ docstep = parseNumberArg(args[i + 1]);
2186
+ i += 1;
2187
+ continue;
2188
+ }
2189
+ if (arg.startsWith("--docstep=")) {
2190
+ docstep = parseNumberArg(arg.slice("--docstep=".length));
2191
+ continue;
2192
+ }
2193
+ if (!arg.startsWith("-")) {
2194
+ file = arg;
2195
+ }
2196
+ }
2197
+ return { file, outPath, seed, docstep };
2198
+ }
2199
+ function firstFileArg(args) {
2200
+ return args.find((arg) => !arg.startsWith("-"));
2201
+ }
2202
+ function normalizePage(raw) {
2203
+ return raw === "A4" ? "A4" : raw === "Letter" ? "Letter" : undefined;
2204
+ }
2205
+ function normalizeTheme(raw) {
2206
+ if (raw === "screen" || raw === "print" || raw === "both")
2207
+ return raw;
2208
+ return undefined;
2209
+ }
2210
+ function normalizeFonts(raw) {
2211
+ if (raw === "tech" || raw === "bookish")
2212
+ return raw;
2213
+ return undefined;
2214
+ }
2215
+ function normalizeFallback(raw) {
2216
+ if (!raw)
2217
+ return undefined;
2218
+ if (raw === "none" || raw === "off" || raw === "false" || raw === "0")
2219
+ return "none";
2220
+ if (raw === "system")
2221
+ return "system";
2222
+ return undefined;
2223
+ }
2224
+ function parseYesNoArg(raw) {
2225
+ if (!raw)
2226
+ return true;
2227
+ return !(raw === "no" || raw === "false" || raw === "0");
2228
+ }
2229
+ function parseNumberArg(raw) {
2230
+ if (!raw)
2231
+ return undefined;
2232
+ const value = Number(raw);
2233
+ return Number.isFinite(value) ? value : undefined;
2234
+ }
2235
+ function isTemplateName(raw) {
2236
+ return raw === "demo" || raw === "article" || raw === "spec" || raw === "zine" || raw === "paper" || raw === "blank";
2237
+ }
2238
+ function getHelpLines(command) {
2239
+ const topic = command ?? "";
2240
+ if (topic === "parse") {
2241
+ return [
2242
+ "Usage:",
2243
+ " flux parse [options] <files...>",
2244
+ "",
2245
+ "Description:",
2246
+ " Parse Flux source files and print their IR as JSON.",
2247
+ "",
2248
+ "Options:",
2249
+ " --ndjson Emit one JSON object per line: { \"file\", \"doc\" }.",
2250
+ " --pretty Pretty-print JSON (2-space indent). (default for a single file)",
2251
+ " --compact Compact JSON (no whitespace).",
2252
+ " -h, --help Show this message.",
2253
+ "",
2254
+ ];
2255
+ }
2256
+ if (topic === "check") {
2257
+ return [
2258
+ "Usage:",
2259
+ " flux check [options] <files...>",
2260
+ "",
2261
+ "Description:",
2262
+ " Parse Flux files and run basic static checks (grid references,",
2263
+ " neighbors.* usage, and runtime shape).",
2264
+ "",
2265
+ "Options:",
2266
+ " --json Emit NDJSON diagnostics to stdout.",
2267
+ " -h, --help Show this message.",
2268
+ "",
2269
+ ];
2270
+ }
2271
+ if (topic === "render") {
2272
+ return [
2273
+ "Usage:",
2274
+ " flux render [options] <file>",
2275
+ "",
2276
+ "Description:",
2277
+ " Render a Flux document to canonical Render IR JSON.",
2278
+ "",
2279
+ "Options:",
2280
+ " --format ir Output format. (required; currently only 'ir')",
2281
+ " --seed N Deterministic RNG seed (default: 0).",
2282
+ " --time T Render time in seconds (default: 0).",
2283
+ " --docstep D Render at docstep D (default: 0).",
2284
+ " -h, --help Show this message.",
2285
+ "",
2286
+ ];
2287
+ }
2288
+ if (topic === "fmt") {
2289
+ return [
2290
+ "Usage:",
2291
+ " flux fmt <file>",
2292
+ "",
2293
+ "Description:",
2294
+ " Apply a minimal formatter to a Flux document.",
2295
+ "",
2296
+ "Options:",
2297
+ " -h, --help Show this message.",
2298
+ "",
2299
+ ];
2300
+ }
2301
+ if (topic === "tick") {
2302
+ return [
2303
+ "Usage:",
2304
+ " flux tick [options] <file>",
2305
+ "",
2306
+ "Description:",
2307
+ " Advance time by a number of seconds and render the updated IR.",
2308
+ "",
2309
+ "Options:",
2310
+ " --seconds S Seconds to advance time by.",
2311
+ " --seed N Deterministic RNG seed (default: 0).",
2312
+ " -h, --help Show this message.",
2313
+ "",
2314
+ ];
2315
+ }
2316
+ if (topic === "step") {
2317
+ return [
2318
+ "Usage:",
2319
+ " flux step [options] <file>",
2320
+ "",
2321
+ "Description:",
2322
+ " Advance docsteps and render the updated IR.",
2323
+ "",
2324
+ "Options:",
2325
+ " --n N Docsteps to advance by (default: 1).",
2326
+ " --seed N Deterministic RNG seed (default: 0).",
2327
+ " -h, --help Show this message.",
2328
+ "",
2329
+ ];
2330
+ }
2331
+ if (topic === "view") {
2332
+ return [
2333
+ "Usage:",
2334
+ " flux view [options] <file>",
2335
+ "",
2336
+ "Description:",
2337
+ " Open a local web viewer for a Flux document.",
2338
+ "",
2339
+ "Options:",
2340
+ " --port <n> Port for the local server (default: auto).",
2341
+ " --docstep-ms <n> Docstep interval in milliseconds.",
2342
+ " --seed <n> Seed for deterministic rendering.",
2343
+ " --allow-net <orig> Allow remote assets for origin (repeatable or comma-separated).",
2344
+ " --no-time Disable automatic time advancement.",
2345
+ " --tty Use the legacy TTY grid viewer.",
2346
+ " -h, --help Show this message.",
2347
+ "",
2348
+ ];
2349
+ }
2350
+ if (topic === "pdf") {
2351
+ return [
2352
+ "Usage:",
2353
+ " flux pdf [options] <file> --out <file.pdf>",
2354
+ "",
2355
+ "Description:",
2356
+ " Render a Flux document snapshot to PDF.",
2357
+ "",
2358
+ "Options:",
2359
+ " --out <file> Output PDF path. (required)",
2360
+ " --seed <n> Seed for deterministic rendering.",
2361
+ " --docstep <n> Docstep to render.",
2362
+ " -h, --help Show this message.",
2363
+ "",
2364
+ ];
2365
+ }
2366
+ if (topic === "config") {
2367
+ return [
2368
+ "Usage:",
2369
+ " flux config",
2370
+ " flux config set <key> <value> [--init]",
2371
+ "",
2372
+ "Options:",
2373
+ " --json Emit JSON.",
2374
+ " --init Create flux.config.json if missing.",
2375
+ "",
2376
+ ];
2377
+ }
2378
+ if (topic === "new") {
2379
+ return [
2380
+ "Usage:",
2381
+ " flux new (launch wizard)",
2382
+ " flux new <template> --out <dir> --page Letter|A4 --theme print|screen|both --fonts tech|bookish",
2383
+ " --fallback system|none --assets yes|no --chapters N --live yes|no",
2384
+ "",
2385
+ "Templates:",
2386
+ " demo, article, spec, zine, paper, blank",
2387
+ "",
2388
+ ];
2389
+ }
2390
+ if (topic === "add") {
2391
+ return [
2392
+ "Usage:",
2393
+ " flux add <kind> [options] <file>",
2394
+ "",
2395
+ "Kinds:",
2396
+ " title, page, section, figure, callout, table, slot, inline-slot, bibliography-stub",
2397
+ "",
2398
+ "Options:",
2399
+ " --text <value> Text value for title/callout.",
2400
+ " --heading <value> Heading text for sections.",
2401
+ " --label <value> Optional label for figure/callout.",
2402
+ " --no-heading Omit section heading.",
2403
+ " --no-check Skip check after editing.",
2404
+ "",
2405
+ ];
2406
+ }
2407
+ return [];
2408
+ }
2409
+ //# sourceMappingURL=app.js.map