diff --git a/apps/web/src/app/(private)/projects/[projectId]/versions/[commitUuid]/_components/Sidebar/index.tsx b/apps/web/src/app/(private)/projects/[projectId]/versions/[commitUuid]/_components/Sidebar/index.tsx
new file mode 100644
index 000000000..4e087536c
--- /dev/null
+++ b/apps/web/src/app/(private)/projects/[projectId]/versions/[commitUuid]/_components/Sidebar/index.tsx
@@ -0,0 +1,39 @@
+import { Suspense } from 'react'
+
+import { DocumentSidebar, FilesTree } from '@latitude-data/web-ui'
+
+// FIXME: Mock data
+const documents = [
+ { path: 'Documents/Intro', doumentUuid: '1' },
+ {
+ path: 'Documents/Sumaries/Product/Prompts/Coms Summaries',
+ doumentUuid: '2',
+ },
+ {
+ path: 'Documents/Sumaries/Product/Prompts/TheBRo',
+ doumentUuid: '2bro',
+ },
+ {
+ path: 'Documents/Sumaries/file2',
+ doumentUuid: '33',
+ },
+ {
+ path: 'Documents/Sumaries/Product/file3',
+ doumentUuid: '43',
+ },
+ { path: 'Zonboaring/doc5', doumentUuid: '5' },
+ { path: 'Onboarding/doc3', doumentUuid: '3' },
+ { path: 'P_Bording/Nested/doc4', doumentUuid: '4' },
+ { path: 'b_doc_6', doumentUuid: '6' },
+ { path: 'a_doc_7', doumentUuid: '7' },
+]
+
+export default async function Sidebar() {
+ return (
+ Loading...}>
+
+
+
+
+ )
+}
diff --git a/apps/web/src/app/(private)/projects/[projectId]/versions/[commitUuid]/layout.tsx b/apps/web/src/app/(private)/projects/[projectId]/versions/[commitUuid]/layout.tsx
index 7db261a82..6125c3fd9 100644
--- a/apps/web/src/app/(private)/projects/[projectId]/versions/[commitUuid]/layout.tsx
+++ b/apps/web/src/app/(private)/projects/[projectId]/versions/[commitUuid]/layout.tsx
@@ -64,13 +64,7 @@ export default async function CommitLayout({
},
]}
>
-
-
- {/* TODO: commented out until fixed toTree methods to new path schema */}
- {/* */}
-
- {children}
-
+ {children}
diff --git a/apps/web/src/app/(private)/projects/[projectId]/versions/[commitUuid]/page.tsx b/apps/web/src/app/(private)/projects/[projectId]/versions/[commitUuid]/page.tsx
index 4fa07c7a1..1bf8e566a 100644
--- a/apps/web/src/app/(private)/projects/[projectId]/versions/[commitUuid]/page.tsx
+++ b/apps/web/src/app/(private)/projects/[projectId]/versions/[commitUuid]/page.tsx
@@ -1,5 +1,14 @@
+import { DocumentDetailWrapper } from '@latitude-data/web-ui'
+
+import Sidebar from './_components/Sidebar'
+
export const dynamic = 'force-dynamic'
export default async function CommitRoot() {
- return
Commits home
+ return (
+
+
+ Main content. Remove Tailwind Styles from here
+
+ )
}
diff --git a/packages/web-ui/src/ds/atoms/Icons/index.tsx b/packages/web-ui/src/ds/atoms/Icons/index.tsx
index a49f14f55..f7cc30831 100644
--- a/packages/web-ui/src/ds/atoms/Icons/index.tsx
+++ b/packages/web-ui/src/ds/atoms/Icons/index.tsx
@@ -1,4 +1,12 @@
-import { Copy, type LucideIcon } from 'lucide-react'
+import {
+ ChevronDown,
+ ChevronRight,
+ Copy,
+ File,
+ FolderClosed,
+ FolderOpen,
+ type LucideIcon,
+} from 'lucide-react'
import { LatitudeLogo, LatitudeLogoMonochrome } from './custom-icons'
@@ -7,5 +15,10 @@ export type Icon = LucideIcon
export const Icons = {
logo: LatitudeLogo,
logoMonochrome: LatitudeLogoMonochrome,
+ chevronDown: ChevronDown,
+ chevronRight: ChevronRight,
+ folderClose: FolderClosed,
+ file: File,
+ folderOpen: FolderOpen,
clipboard: Copy,
}
diff --git a/packages/web-ui/src/index.ts b/packages/web-ui/src/index.ts
index d11516f1e..12a6f18da 100644
--- a/packages/web-ui/src/index.ts
+++ b/packages/web-ui/src/index.ts
@@ -1,5 +1,8 @@
export * from './ds/tokens'
export * from './ds/atoms'
export * from './ds/molecules'
-export * from './sections'
export * from './providers'
+
+export { default as DocumentSidebar } from './sections/Document/Sidebar'
+export { default as DocumentDetailWrapper } from './sections/Document/DetailWrapper'
+export * from './sections/Document/Sidebar/Files'
diff --git a/packages/web-ui/src/layouts/AppLayout/index.tsx b/packages/web-ui/src/layouts/AppLayout/index.tsx
index 456ae9712..ba2dc1fae 100644
--- a/packages/web-ui/src/layouts/AppLayout/index.tsx
+++ b/packages/web-ui/src/layouts/AppLayout/index.tsx
@@ -20,7 +20,7 @@ export default function AppLayout({
navigationLinks={navigationLinks}
currentUser={currentUser}
/>
- {children}
+ {children}
)
}
diff --git a/packages/web-ui/src/sections/Document/DetailWrapper/index.tsx b/packages/web-ui/src/sections/Document/DetailWrapper/index.tsx
new file mode 100644
index 000000000..266b3cd6d
--- /dev/null
+++ b/packages/web-ui/src/sections/Document/DetailWrapper/index.tsx
@@ -0,0 +1,9 @@
+import { ReactNode } from 'react'
+
+export default async function DocumentDetailWrapper({
+ children,
+}: {
+ children: ReactNode
+}) {
+ return {children}
+}
diff --git a/packages/web-ui/src/sections/DocumentEditor/index.tsx b/packages/web-ui/src/sections/Document/Editor/index.tsx
similarity index 100%
rename from packages/web-ui/src/sections/DocumentEditor/index.tsx
rename to packages/web-ui/src/sections/Document/Editor/index.tsx
diff --git a/packages/web-ui/src/sections/Document/Sidebar/Files/index.tsx b/packages/web-ui/src/sections/Document/Sidebar/Files/index.tsx
new file mode 100644
index 000000000..65ff444b3
--- /dev/null
+++ b/packages/web-ui/src/sections/Document/Sidebar/Files/index.tsx
@@ -0,0 +1,209 @@
+'use client'
+
+import { ReactNode, useState } from 'react'
+
+import { Icons } from '$ui/ds/atoms/Icons'
+import Text from '$ui/ds/atoms/Text'
+import { ReactStateDispatch } from '$ui/lib/commonTypes'
+import { cn } from '$ui/lib/utils'
+
+import { Node, SidebarDocument, useTree } from './useTree'
+
+const ICON_CLASS = 'w-6 h-6 text-muted-foreground'
+
+type IndentType = { isLast: boolean }
+function IndentationBar({
+ indentation,
+ open,
+}: {
+ open: boolean
+ indentation: IndentType[]
+}) {
+ return indentation.map((indent, index) => {
+ const anyNextIndentIsNotLast = !!indentation
+ .slice(index)
+ .find((i) => !i.isLast)
+ const showBorder = anyNextIndentIsNotLast ? false : indent.isLast
+ return (
+
+ {index > 0 ? (
+
+ {!open && showBorder ? (
+
+ ) : (
+
+ )}
+
+ ) : null}
+
+ )
+ })
+}
+
+function NodeHeaderWrapper({
+ open,
+ node,
+ children,
+ indentation,
+}: {
+ open: boolean
+ children: ReactNode
+ node: Node
+ indentation: IndentType[]
+}) {
+ return (
+
+
+ {children}
+
+ )
+}
+
+function FolderHeader({
+ node,
+ open,
+ onClick,
+ indentation,
+}: {
+ isLast: boolean
+ node: Node
+ open: boolean
+ onClick: ReactStateDispatch
+ indentation: IndentType[]
+}) {
+ const FolderIcon = open ? Icons.folderOpen : Icons.folderClose
+ const ChevronIcon = open ? Icons.chevronDown : Icons.chevronRight
+ return (
+
+ onClick(!open)}
+ className='flex flex-row items-center gap-x-1'
+ >
+
+
+
+
+
{node.name}
+
+
+ )
+}
+
+function FileHeader({
+ open,
+ node,
+ indentation,
+}: {
+ open: boolean
+ node: Node
+ indentation: IndentType[]
+}) {
+ return (
+
+
+
+
+ {node.name}
+
+
+
+ )
+}
+
+function NodeHeader({
+ isLast,
+ node,
+ open,
+ onClick,
+ indentation,
+}: {
+ isLast: boolean
+ node: Node
+ open: boolean
+ onClick: ReactStateDispatch
+ indentation: IndentType[]
+}) {
+ if (node.isRoot) return null
+ if (node.isFile) {
+ return
+ }
+
+ return (
+
+ )
+}
+
+function FileNode({
+ isLast = false,
+ node,
+ indentation = [],
+}: {
+ node: Node
+ isLast?: boolean
+ indentation?: IndentType[]
+}) {
+ const [open, setOpen] = useState(node.containsSelected)
+ const lastIdx = node.children.length - 1
+ return (
+
+
+
+ {node.isFile ? null : (
+
+ {node.children.map((node, idx) => (
+ -
+
+
+ ))}
+
+ )}
+
+ )
+}
+
+export function FilesTree({
+ documents,
+ currentDocumentUuid,
+}: {
+ documents: SidebarDocument[]
+ currentDocumentUuid: string | undefined
+}) {
+ const rootNode = useTree({ documents, currentDocumentUuid })
+
+ return
+}
diff --git a/packages/web-ui/src/sections/Document/Sidebar/Files/useTree/index.test.ts b/packages/web-ui/src/sections/Document/Sidebar/Files/useTree/index.test.ts
new file mode 100644
index 000000000..2474899b1
--- /dev/null
+++ b/packages/web-ui/src/sections/Document/Sidebar/Files/useTree/index.test.ts
@@ -0,0 +1,162 @@
+import { renderHook } from '@testing-library/react'
+import { describe, expect, it } from 'vitest'
+
+import { Node, useTree } from './index'
+
+const FAKE_RANDOM_ID = 'RANDOM_ID'
+
+function fakeRandomId({ uuid }: { uuid?: string } = {}) {
+ if (uuid) return uuid
+
+ return FAKE_RANDOM_ID
+}
+
+function nodeToJson(node: Node): object {
+ return {
+ name: node.name,
+ id: node.id,
+ depth: node.depth,
+ containsSelected: node.containsSelected,
+ selected: node.selected,
+ children: node.children.map(nodeToJson),
+ }
+}
+
+let list = [
+ { path: 'a_thing/doc1', doumentUuid: '1' },
+ { path: 'a_thing/doc2', doumentUuid: '2' },
+ { path: 'a_thing/other-things/doc3', doumentUuid: '3' },
+ { path: 'z_thing/doc5', doumentUuid: '5' },
+ { path: 'b_thing/doc4', doumentUuid: '4' },
+ { path: 'b_doc_6', doumentUuid: '6' },
+ { path: 'a_doc_7', doumentUuid: '7' },
+]
+
+describe('useTree', () => {
+ it('return all node attributes', async () => {
+ const { result } = renderHook(() =>
+ useTree({
+ documents: list,
+ currentDocumentUuid: '4',
+ generateNodeId: fakeRandomId,
+ }),
+ )
+ const root = result.current
+ expect(root.id).toBe(FAKE_RANDOM_ID)
+ expect(root.doc).toBeUndefined()
+ expect(root.name).toBe('root')
+ expect(root.isFile).toBeFalsy()
+ expect(root.children).toHaveLength(5)
+ })
+
+ it('should return a tree with children', async () => {
+ const { result } = renderHook(() =>
+ useTree({
+ documents: list,
+ currentDocumentUuid: '4',
+ generateNodeId: fakeRandomId,
+ }),
+ )
+ expect(nodeToJson(result.current)).toEqual({
+ id: FAKE_RANDOM_ID,
+ name: 'root',
+ depth: 0,
+ selected: false,
+ containsSelected: true,
+ children: [
+ {
+ id: FAKE_RANDOM_ID,
+ name: 'a_thing',
+ depth: 1,
+ selected: false,
+ containsSelected: false,
+ children: [
+ {
+ name: 'other-things',
+ id: FAKE_RANDOM_ID,
+ depth: 2,
+ selected: false,
+ containsSelected: false,
+ children: [
+ {
+ id: '3',
+ selected: false,
+ containsSelected: false,
+ depth: 3,
+ name: 'doc3',
+ children: [],
+ },
+ ],
+ },
+ {
+ id: '1',
+ selected: false,
+ containsSelected: false,
+ depth: 2,
+ name: 'doc1',
+ children: [],
+ },
+ {
+ id: '2',
+ selected: false,
+ containsSelected: false,
+ depth: 2,
+ name: 'doc2',
+ children: [],
+ },
+ ],
+ },
+ {
+ name: 'b_thing',
+ id: FAKE_RANDOM_ID,
+ depth: 1,
+ selected: false,
+ containsSelected: true,
+ children: [
+ {
+ id: '4',
+ selected: true,
+ containsSelected: false,
+ depth: 2,
+ name: 'doc4',
+ children: [],
+ },
+ ],
+ },
+ {
+ id: FAKE_RANDOM_ID,
+ name: 'z_thing',
+ depth: 1,
+ selected: false,
+ containsSelected: false,
+ children: [
+ {
+ id: '5',
+ selected: false,
+ containsSelected: false,
+ depth: 2,
+ name: 'doc5',
+ children: [],
+ },
+ ],
+ },
+ {
+ id: '7',
+ selected: false,
+ containsSelected: false,
+ depth: 1,
+ name: 'a_doc_7',
+ children: [],
+ },
+ {
+ id: '6',
+ selected: false,
+ containsSelected: false,
+ depth: 1,
+ name: 'b_doc_6',
+ children: [],
+ },
+ ],
+ })
+ })
+})
diff --git a/packages/web-ui/src/sections/DocumentsSidebar/useTree/index.ts b/packages/web-ui/src/sections/Document/Sidebar/Files/useTree/index.ts
similarity index 73%
rename from packages/web-ui/src/sections/DocumentsSidebar/useTree/index.ts
rename to packages/web-ui/src/sections/Document/Sidebar/Files/useTree/index.ts
index 10d797d27..99d062f73 100644
--- a/packages/web-ui/src/sections/DocumentsSidebar/useTree/index.ts
+++ b/packages/web-ui/src/sections/Document/Sidebar/Files/useTree/index.ts
@@ -9,17 +9,24 @@ export class Node {
public id: string
public name: string
public isRoot: boolean = false
+ public isFile: boolean = false
+ public depth: number = 0
+ public containsSelected: boolean = false
+ public selected: boolean = false
public children: Node[] = []
public doc?: SidebarDocument
+ public parent?: Node
constructor({
id,
doc,
children = [],
+ selected = false,
isRoot = false,
name = '',
}: {
id: string
+ selected: boolean
doc?: SidebarDocument
children?: Node[]
isRoot?: boolean
@@ -27,10 +34,20 @@ export class Node {
}) {
this.id = id
this.name = isRoot ? 'root' : name
+ this.selected = selected
this.isRoot = isRoot
+ this.isFile = !!doc
this.children = children
this.doc = doc
}
+
+ recursiveSelectParents() {
+ this.containsSelected = true
+
+ if (this.parent) {
+ this.parent.recursiveSelectParents()
+ }
+ }
}
function sortByPathDepth(a: SidebarDocument, b: SidebarDocument) {
@@ -64,11 +81,13 @@ function findChildrenIndex(node: Node, children: Node[]) {
function buildTree({
root,
+ currentDocumentUuid,
nodeMap,
documents,
generateNodeId,
}: {
root: Node
+ currentDocumentUuid?: string
nodeMap: Map
documents: SidebarDocument[]
generateNodeId: typeof defaultGenerateNodeUuid
@@ -84,9 +103,11 @@ function buildTree({
if (!nodeMap.has(path)) {
const file = isFile ? doc : undefined
const uuid = isFile ? doc.doumentUuid : undefined
+ const selected = isFile && uuid === currentDocumentUuid
const node = new Node({
id: generateNodeId({ uuid }),
doc: file,
+ selected: selected,
name: segment,
})
nodeMap.set(path, node)
@@ -97,12 +118,21 @@ function buildTree({
// We pre-sorted documents by path depth, so we know
// that the parent node exists
const parent = nodeMap.get(parentPath)!
+
+ node.depth = parent.depth + 1
+
const index = findChildrenIndex(node, parent.children)
if (index === -1) {
parent.children.push(node)
} else {
parent.children.splice(index, 0, node)
}
+
+ node.parent = parent
+
+ if (selected) {
+ node.parent.recursiveSelectParents()
+ }
}
})
})
@@ -112,18 +142,31 @@ function buildTree({
export function useTree({
documents,
+ currentDocumentUuid,
generateNodeId = defaultGenerateNodeUuid,
}: {
documents: SidebarDocument[]
+ currentDocumentUuid: string | undefined
generateNodeId?: typeof defaultGenerateNodeUuid
}) {
return useMemo(() => {
- const root = new Node({ id: generateNodeId(), children: [], isRoot: true })
+ const root = new Node({
+ id: generateNodeId(),
+ children: [],
+ isRoot: true,
+ selected: false,
+ })
const nodeMap = new Map()
nodeMap.set('', root)
const sorted = documents.slice().sort(sortByPathDepth)
- const tree = buildTree({ root, nodeMap, documents: sorted, generateNodeId })
+ const tree = buildTree({
+ root,
+ currentDocumentUuid,
+ nodeMap,
+ documents: sorted,
+ generateNodeId,
+ })
return tree
- }, [documents])
+ }, [documents, currentDocumentUuid])
}
diff --git a/packages/web-ui/src/sections/Document/Sidebar/index.tsx b/packages/web-ui/src/sections/Document/Sidebar/index.tsx
new file mode 100644
index 000000000..c84cfd782
--- /dev/null
+++ b/packages/web-ui/src/sections/Document/Sidebar/index.tsx
@@ -0,0 +1,12 @@
+'use client'
+
+import { ReactNode } from 'react'
+
+export default function DocumentSidebar({ children }: { children: ReactNode }) {
+ return (
+
+ )
+}
diff --git a/packages/web-ui/src/sections/DocumentsSidebar/index.tsx b/packages/web-ui/src/sections/DocumentsSidebar/index.tsx
deleted file mode 100644
index 1f24d1c45..000000000
--- a/packages/web-ui/src/sections/DocumentsSidebar/index.tsx
+++ /dev/null
@@ -1,29 +0,0 @@
-'use client'
-
-import {
- Node,
- SidebarDocument,
- useTree,
-} from '$ui/sections/DocumentsSidebar/useTree'
-
-function TreeNode({ node, level = 0 }: { node: Node; level?: number }) {
- return (
-
-
- {node.children.map((node, idx) => (
-
- ))}
-
-
- )
-}
-
-export default function DocumentTree({
- documents,
-}: {
- documents: SidebarDocument[]
-}) {
- const rootNode = useTree({ documents })
-
- return
-}
diff --git a/packages/web-ui/src/sections/DocumentsSidebar/useTree/index.test.ts b/packages/web-ui/src/sections/DocumentsSidebar/useTree/index.test.ts
deleted file mode 100644
index d495ee236..000000000
--- a/packages/web-ui/src/sections/DocumentsSidebar/useTree/index.test.ts
+++ /dev/null
@@ -1,80 +0,0 @@
-import { renderHook } from '@testing-library/react'
-import { describe, expect, it } from 'vitest'
-
-import { Node, useTree } from './index'
-
-const FAKE_RANDOM_ID = 'RANDOM_ID'
-
-function fakeRandomId({ uuid }: { uuid?: string } = {}) {
- if (uuid) return uuid
-
- return FAKE_RANDOM_ID
-}
-
-function nodeToJson(node: Node): object {
- return {
- name: node.name,
- id: node.id,
- children: node.children.map(nodeToJson),
- }
-}
-
-let list = [
- { path: 'a_thing/doc1', doumentUuid: '1' },
- { path: 'a_thing/doc2', doumentUuid: '2' },
- { path: 'a_thing/other-things/doc3', doumentUuid: '3' },
- { path: 'z_thing/doc5', doumentUuid: '5' },
- { path: 'b_thing/doc4', doumentUuid: '4' },
- { path: 'b_doc_6', doumentUuid: '6' },
- { path: 'a_doc_7', doumentUuid: '7' },
-]
-
-describe('useTree', () => {
- it('return all node attributes', async () => {
- const { result } = renderHook(() =>
- useTree({ documents: list, generateNodeId: fakeRandomId }),
- )
- const root = result.current
- expect(root.id).toBe(FAKE_RANDOM_ID)
- expect(root.doc).toBeUndefined()
- expect(root.name).toBe('root')
- expect(root.children).toHaveLength(5)
- })
-
- it('should return a tree with children', async () => {
- const { result } = renderHook(() =>
- useTree({ documents: list, generateNodeId: fakeRandomId }),
- )
- expect(nodeToJson(result.current)).toEqual({
- id: FAKE_RANDOM_ID,
- name: 'root',
- children: [
- {
- id: FAKE_RANDOM_ID,
- name: 'a_thing',
- children: [
- {
- name: 'other-things',
- id: FAKE_RANDOM_ID,
- children: [{ id: '3', name: 'doc3', children: [] }],
- },
- { id: '1', name: 'doc1', children: [] },
- { id: '2', name: 'doc2', children: [] },
- ],
- },
- {
- name: 'b_thing',
- id: FAKE_RANDOM_ID,
- children: [{ id: '4', name: 'doc4', children: [] }],
- },
- {
- id: FAKE_RANDOM_ID,
- name: 'z_thing',
- children: [{ id: '5', name: 'doc5', children: [] }],
- },
- { id: '7', name: 'a_doc_7', children: [] },
- { id: '6', name: 'b_doc_6', children: [] },
- ],
- })
- })
-})
diff --git a/packages/web-ui/src/sections/index.ts b/packages/web-ui/src/sections/index.ts
deleted file mode 100644
index b44fd4646..000000000
--- a/packages/web-ui/src/sections/index.ts
+++ /dev/null
@@ -1,2 +0,0 @@
-export * from './DocumentEditor'
-export * from './DocumentsSidebar'
diff --git a/packages/web-ui/styles.css b/packages/web-ui/styles.css
index 42594c931..bc24c17eb 100644
--- a/packages/web-ui/styles.css
+++ b/packages/web-ui/styles.css
@@ -22,7 +22,7 @@
--muted: #F3F4F6;
--muted-foreground: #66727F;
- --accent: #D7EBFE;
+ --accent: #EFF7FF;
--accent-foreground: #0356B0;
--destructive: #DC2828;