From 01ad890f5d0109c0df4d3b81029acf88dc770da1 Mon Sep 17 00:00:00 2001 From: Owais Jamil Date: Thu, 19 Mar 2026 20:38:27 -0500 Subject: [PATCH] feat: separate and improve import ui --- docs/at-proto.md | 3 +- docs/roadmap.md | 4 +- src/__tests__/Toolbar.test.tsx | 4 +- src/components/AppLayout/WorkspacePanel.tsx | 5 +- src/components/AtProto/AtProtoAuthSheet.tsx | 230 ++-------------- src/components/AtProto/ImportSheet.tsx | 256 ++++++++++++++++++ src/components/Sidebar/Sidebar.tsx | 43 ++- .../controllers/useWorkspaceViewController.ts | 2 + 8 files changed, 317 insertions(+), 230 deletions(-) create mode 100644 src/components/AtProto/ImportSheet.tsx diff --git a/docs/at-proto.md b/docs/at-proto.md index 82ce3af..493920c 100644 --- a/docs/at-proto.md +++ b/docs/at-proto.md @@ -144,7 +144,8 @@ src/ │ └── useAtProtoController.ts ├── components/ │ ├── AtProto/ -│ │ └── AtProtoAuthSheet.tsx # login, session, and import browser sheet +│ │ ├── AtProtoAuthSheet.tsx # login + session sheet +│ │ └── ImportSheet.tsx # Tangled import browser sheet │ └── AppLayout/LayoutSettingsPanel/ │ └── AtProtoSection.tsx ``` diff --git a/docs/roadmap.md b/docs/roadmap.md index 425d53b..a192f1b 100644 --- a/docs/roadmap.md +++ b/docs/roadmap.md @@ -5,7 +5,7 @@ last_updated: 2026-03-19 ## Tangled string integration -Publish documents as [Tangled strings](https://tangled.sh) (AT Protocol gists) and import strings as documents. Spec in [at-proto.md](./at-proto.md). +Publish documents as [Tangled strings](https://tangled.sh) (AT Protocol gists) and import strings as documents. ### Part 1 — Auth @@ -26,8 +26,6 @@ Publish documents as [Tangled strings](https://tangled.sh) (AT Protocol gists) a - Extensions covered: `py`, `md`, `js`, `ts`, `yaml`, `java`, `sass`, `css`, `csv`, `fs`, `cs` - `i-fluent-document-16-filled` for fallback -Implemented on `2026-03-19` in the Tauri backend, AT Proto controller, and unified auth/import sheet. - ### Part 3 — Push 1. **Tauri commands** - `string_create`, `string_update`, `string_delete` diff --git a/src/__tests__/Toolbar.test.tsx b/src/__tests__/Toolbar.test.tsx index b07026c..8362931 100644 --- a/src/__tests__/Toolbar.test.tsx +++ b/src/__tests__/Toolbar.test.tsx @@ -60,12 +60,12 @@ describe("Toolbar", () => { expect(setEditorOnlyMode).toHaveBeenCalledOnce(); }); - it("opens the Tangled auth entry from the toolbar", () => { + it("opens the AT Protocol auth entry from the toolbar", () => { const onAtProtoAuth = vi.fn(); render(); - fireEvent.click(screen.getByRole("button", { name: "Tangled" })); + fireEvent.click(screen.getByRole("button", { name: "Login" })); expect(onAtProtoAuth).toHaveBeenCalledOnce(); }); }); diff --git a/src/components/AppLayout/WorkspacePanel.tsx b/src/components/AppLayout/WorkspacePanel.tsx index 97ba8c2..74ced32 100644 --- a/src/components/AppLayout/WorkspacePanel.tsx +++ b/src/components/AppLayout/WorkspacePanel.tsx @@ -58,6 +58,7 @@ export type WorkspacePanelProps = { | "isPdfExportDisabled" | "onRefresh" >; + onOpenImportSheet?: () => void; editor: WorkspaceEditorProps; preview: WorkspacePreviewProps; statusBar: StatusBarProps; @@ -156,7 +157,7 @@ function Section({ children, initial, animate, exit, transition, className, styl ); } -export function WorkspacePanel({ toolbar, editor, preview, statusBar, diagnostics }: WorkspacePanelProps) { +export function WorkspacePanel({ toolbar, onOpenImportSheet, editor, preview, statusBar, diagnostics }: WorkspacePanelProps) { const skipAnimation = useSkipAnimation(); const { viewportWidth } = useViewportTier(FALLBACK_VIEWPORT_WIDTH); const { sidebarCollapsed } = useWorkspacePanelSidebarState(); @@ -273,7 +274,7 @@ export function WorkspacePanel({ toolbar, editor, preview, statusBar, diagnostic {...sidebarMotionProps} className="relative flex h-full shrink-0" style={sidebarStyle}> - +
; @@ -137,217 +137,27 @@ function SessionView({ controller }: { controller: Controller }) { ); } -function RecordRow( - { record, isSelected, onSelectTid }: { - record: TangledStringRecord; - isSelected: boolean; - onSelectTid: (tid: string) => void; - }, -) { - const handleClick = useCallback(() => { - onSelectTid(record.tid); - }, [onSelectTid, record.tid]); - - return ( - - ); -} - -function BrowseHandleForm({ controller }: { controller: Controller }) { - const handleChange = useCallback>((event) => { - controller.setImportHandle(event.target.value); - }, [controller]); - - return ( -
- -

- Public strings can be imported without signing in. The browser defaults to your connected handle when available. -

-
- ); -} - -function RecordsPanel({ controller }: { controller: Controller }) { - const handleSelectTid = useCallback((tid: string) => { - controller.handleSelectString(tid); - }, [controller]); - - return ( -
-
- {controller.importState.browseHandle ? `Strings for ${controller.importState.browseHandle}` : "Strings"} -
-
- {controller.importState.records.length === 0 - ? ( -
- {controller.importState.isListing - ? "Loading Tangled strings..." - : "No strings loaded yet. Enter a handle and browse."} -
- ) - : controller.importState.records.map((record) => ( - - ))} -
-
- ); -} - -function ImportDestinationForm({ controller }: { controller: Controller }) { - const handleLocationChange = useCallback>((event) => { - controller.setDestinationLocationId(Number(event.target.value) || null); - }, [controller]); - const handlePathChange = useCallback>((event) => { - controller.setDestinationRelPath(event.target.value); - }, [controller]); - const handleImport = useCallback(() => { - void controller.handleImport(); - }, [controller]); - const importDisabled = controller.importState.isSaving - || !controller.importState.selectedRecord - || !controller.importState.destinationLocationId - || !controller.importState.destinationRelPath.trim() - || !controller.hasLocations; - - return ( -
- - -

- Non-Markdown strings are imported as fenced code blocks so the resulting document stays readable in Writer. -

-
- -
-
- ); -} - -function PreviewPanel({ controller }: { controller: Controller }) { - return ( -
-
- Preview -
-
-        {controller.importState.isFetching
-          ? "Loading string preview..."
-          : controller.importState.previewText || "Select a string to preview the imported document body."}
-      
-
- ); -} - -function SelectedRecordSummary({ controller }: { controller: Controller }) { - const selectedFilename = controller.importState.selectedRecord?.filename ?? "Nothing selected"; - - return ( -
-
{selectedFilename}
-

- {controller.importState.selectedRecord?.description || "Select a string to inspect its contents before import."} -

-
- ); -} - -function ImportView({ controller }: { controller: Controller }) { +export function AtProtoAuthSheet({ controller }: AtProtoAuthSheetProps) { return ( <> - -
-
- - + +
+ {controller.sheetMode === "session" && } + {controller.sheetMode === "login" && }
-
- - - -
-
+ + ); } - -export function AtProtoAuthSheet({ controller }: AtProtoAuthSheetProps) { - return ( - -
- {controller.sheetMode === "session" && } - {controller.sheetMode === "import" && } - {controller.sheetMode === "login" && } -
-
- ); -} diff --git a/src/components/AtProto/ImportSheet.tsx b/src/components/AtProto/ImportSheet.tsx new file mode 100644 index 0000000..fcdb181 --- /dev/null +++ b/src/components/AtProto/ImportSheet.tsx @@ -0,0 +1,256 @@ +import { Button } from "$components/Button"; +import { Sheet } from "$components/Sheet"; +import type { useAtProtoController } from "$hooks/controllers/useAtProtoController"; +import { FileTypeIcon, Tangled } from "$icons"; +import type { ChangeEventHandler } from "react"; +import { useCallback, useMemo } from "react"; + +type Controller = ReturnType; + +type ImportSheetProps = { controller: Controller; isOpen: boolean; onClose: () => void; onBack?: () => void }; + +function ImportSheetTitle() { + return ( +
+ +

Import from Tangled

+
+ ); +} + +function ImportSheetHeader({ onBack }: { onBack?: () => void }) { + return ( +
+
+
+ +

+ Browse any public Tangled handle, preview a string, and save it into one of your locations. +

+
+ {onBack && } +
+
+ ); +} + +function RecordRow( + { record, isSelected, onSelectTid }: { + record: Controller["importState"]["records"][number]; + isSelected: boolean; + onSelectTid: (tid: string) => void; + }, +) { + const handleClick = useCallback(() => { + onSelectTid(record.tid); + }, [onSelectTid, record.tid]); + + return ( + + ); +} + +function BrowseHandleForm({ controller }: { controller: Controller }) { + const handleChange = useCallback>((event) => { + controller.setImportHandle(event.target.value); + }, [controller]); + + return ( +
+ +

+ Public strings can be imported without signing in. The browser defaults to your connected handle when available. +

+ {/* TODO: Add GitHub Gist import alongside Tangled browsing. */} +
+ ); +} + +function RecordsPanel({ controller }: { controller: Controller }) { + const handleSelectTid = useCallback((tid: string) => { + controller.handleSelectString(tid); + }, [controller]); + + return ( +
+
+ {controller.importState.browseHandle ? `Strings for ${controller.importState.browseHandle}` : "Strings"} +
+
+ {controller.importState.records.length === 0 + ? ( +
+ {controller.importState.isListing + ? "Loading Tangled strings..." + : "No strings loaded yet. Enter a handle and browse."} +
+ ) + : controller.importState.records.map((record) => ( + + ))} +
+
+ ); +} + +function ImportDestinationForm({ controller }: { controller: Controller }) { + const handleLocationChange = useCallback>((event) => { + controller.setDestinationLocationId(Number(event.target.value) || null); + }, [controller]); + + const handlePathChange = useCallback>((event) => { + controller.setDestinationRelPath(event.target.value); + }, [controller]); + + const handleImport = useCallback(() => { + void controller.handleImport(); + }, [controller]); + + const importDisabled = useMemo( + () => + controller.importState.isSaving + || !controller.importState.selectedRecord + || !controller.importState.destinationLocationId + || !controller.importState.destinationRelPath.trim() + || !controller.hasLocations, + [ + controller.importState.isSaving, + controller.importState.selectedRecord, + controller.importState.destinationLocationId, + controller.importState.destinationRelPath, + controller.hasLocations, + ], + ); + + return ( +
+ + +

+ Non-Markdown strings are imported as fenced code blocks so the resulting document stays readable in Writer. +

+
+ +
+
+ ); +} + +function PreviewPanel({ controller }: { controller: Controller }) { + return ( +
+
+ Preview +
+
+        {controller.importState.isFetching
+          ? "Loading string preview..."
+          : controller.importState.previewText || "Select a string to preview the imported document body."}
+      
+
+ ); +} + +function SelectedRecordSummary({ controller }: { controller: Controller }) { + const selectedFilename = useMemo(() => controller.importState.selectedRecord?.filename ?? "Nothing selected", [ + controller.importState.selectedRecord, + ]); + + return ( +
+
{selectedFilename}
+

+ {controller.importState.selectedRecord?.description || "Select a string to inspect its contents before import."} +

+
+ ); +} + +function ImportSheetBody({ controller }: { controller: Controller }) { + return ( +
+
+
+ + +
+
+ + + +
+
+
+ ); +} + +export function ImportSheet({ controller, isOpen, onClose, onBack }: ImportSheetProps) { + return ( + +
+ + +
+
+ ); +} diff --git a/src/components/Sidebar/Sidebar.tsx b/src/components/Sidebar/Sidebar.tsx index 9413ce6..33f37ab 100644 --- a/src/components/Sidebar/Sidebar.tsx +++ b/src/components/Sidebar/Sidebar.tsx @@ -1,6 +1,6 @@ import { Button } from "$components/Button"; import { useSidebarActions } from "$hooks/controllers/useSidebarActions"; -import { CollapseIcon, FileAddIcon, FolderAddIcon, RefreshIcon } from "$icons"; +import { CollapseIcon, FileAddIcon, FileTextIcon, FolderAddIcon, FolderIcon, RefreshIcon, Tangled } from "$icons"; import { useSidebarState } from "$state/selectors"; import type { DocMeta } from "$types"; import { formatShortcut } from "$utils/shortcuts"; @@ -21,7 +21,7 @@ import { useSidebarInternalDnD } from "./useSidebarInternalDnD"; const EMPTY_DOCUMENTS: DocMeta[] = []; const EMPTY_DIRECTORIES: string[] = []; -export type SidebarProps = { onNewDocument?: (locationId?: number) => void }; +export type SidebarProps = { onNewDocument?: (locationId?: number) => void; onOpenImportSheet?: () => void }; type SidebarActionsProps = { onAddLocation: () => void; @@ -32,7 +32,7 @@ type SidebarActionsProps = { refreshDisabled: boolean; }; -type CountPillProps = { count: number; label: string; singularLabel: string }; +type CountPillProps = { count: number; kind: "location" | "document" | "directory" }; const HideSidebarButton = ({ onToggleCollapse }: { onToggleCollapse: () => void }) => ( +); + +export function Sidebar({ onNewDocument, onOpenImportSheet }: SidebarProps) { const { handleAddLocation, handleRemoveLocation, @@ -291,11 +310,11 @@ export function Sidebar({ onNewDocument }: SidebarProps) {
- - +
+ + +
+ {onOpenImportSheet && }
({ toolbar: toolbarProps, + onOpenImportSheet: atProto.openImportSheet, editor: editorProps, preview: previewProps, statusBar: statusBarProps, @@ -306,6 +307,7 @@ export function useWorkspaceViewController(): WorkspaceViewController { }), [ toolbarProps, + atProto.openImportSheet, editorProps, previewProps, statusBarProps, -- 2.51.2