Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 11 additions & 1 deletion apps/docs/app/(trees)/_docs/DocsPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import * as chooseYourIntegrationConstants from '../docs/Guides/ChooseYourIntegr
import * as customizeIconsConstants from '../docs/Guides/CustomizeIcons/constants';
import * as getStartedWithReactConstants from '../docs/Guides/GetStartedWithReact/constants';
import * as getStartedWithVanillaConstants from '../docs/Guides/GetStartedWithVanilla/constants';
import * as getStartedWithVueConstants from '../docs/Guides/GetStartedWithVue/constants';
import * as handleLargeTreesEfficientlyConstants from '../docs/Guides/HandleLargeTreesEfficiently/constants';
import * as navigateSelectionFocusAndSearchConstants from '../docs/Guides/NavigateSelectionFocusAndSearch/constants';
import * as renameDragAndTriggerItemActionsConstants from '../docs/Guides/RenameDragAndTriggerItemActions/constants';
Expand All @@ -24,6 +25,7 @@ import * as reactApiConstants from '../docs/Reference/ReactAPI/constants';
import * as ssrApiConstants from '../docs/Reference/SSRAPI/constants';
import * as stylingAndThemingConstants from '../docs/Reference/StylingAndTheming/constants';
import * as vanillaApiConstants from '../docs/Reference/VanillaAPI/constants';
import * as vueApiConstants from '../docs/Reference/VueAPI/constants';
import { DocsLayout } from '@/components/docs/DocsLayout';
import { HeadingAnchors } from '@/components/docs/HeadingAnchors';
import { ProseWrapper } from '@/components/docs/ProseWrapper';
Expand All @@ -45,6 +47,10 @@ const GUIDE_SECTIONS: readonly DocsSection[] = [
filePath: '(trees)/docs/Guides/GetStartedWithReact/content.mdx',
constants: getStartedWithReactConstants,
},
{
filePath: '(trees)/docs/Guides/GetStartedWithVue/content.mdx',
constants: getStartedWithVueConstants,
},
{
filePath: '(trees)/docs/Guides/GetStartedWithVanilla/content.mdx',
constants: getStartedWithVanillaConstants,
Expand Down Expand Up @@ -89,6 +95,10 @@ const REFERENCE_SECTIONS: readonly DocsSection[] = [
filePath: '(trees)/docs/Reference/ReactAPI/content.mdx',
constants: reactApiConstants,
},
{
filePath: '(trees)/docs/Reference/VueAPI/content.mdx',
constants: vueApiConstants,
},
{
filePath: '(trees)/docs/Reference/VanillaAPI/content.mdx',
constants: vanillaApiConstants,
Expand All @@ -106,7 +116,7 @@ const REFERENCE_SECTIONS: readonly DocsSection[] = [

const treesDocsTitle = 'Trees, from Pierre';
const treesDocsDescription =
'Guide-first documentation for @pierre/trees, covering React, vanilla, prepared input, styling, icons, Git status, large trees, and SSR hydration.';
'Guide-first documentation for @pierre/trees, covering React, Vue, vanilla, prepared input, styling, icons, Git status, large trees, and SSR hydration.';

// Next.js replaces (does not deep-merge) nested metadata objects like
// `openGraph` and `twitter` from parent segments. Re-declare `images` here
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,20 @@ export function ProjectTree({ paths }: { paths: readonly string[] }) {
}`
);

export const CHOOSE_INTEGRATION_VUE_EXAMPLE = docsCodeSnippet(
'project-tree.vue',
`<script setup lang="ts">
import { FileTree, useFileTree } from '@pierre/trees/vue';

const props = defineProps<{ paths: readonly string[] }>();
const { model } = useFileTree({ paths: props.paths, search: true });
</script>

<template>
<FileTree :model="model" class="h-96 rounded-lg border" />
</template>`
);

export const CHOOSE_INTEGRATION_VANILLA_EXAMPLE = docsCodeSnippet(
'mount-tree.ts',
`import { FileTree } from '@pierre/trees';
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
`@pierre/trees` exposes one path-first model and two primary runtime entries: a
thin React layer in [`@pierre/trees/react`](#get-started-with-react) and the
vanilla class in [`@pierre/trees`](#get-started-with-vanilla).
`@pierre/trees` exposes one path-first model and three primary runtime entries:
a thin React layer in [`@pierre/trees/react`](#get-started-with-react), a Vue 3
layer in [`@pierre/trees/vue`](#get-started-with-vue), and the vanilla class in
[`@pierre/trees`](#get-started-with-vanilla).

### Choose your integration

Expand All @@ -14,25 +15,29 @@ and the target you rename or move later.
For the shared vocabulary behind that rule, jump to
[Shared concepts](#shared-concepts).

#### 2. React vs Vanilla JS
#### 2. React, Vue, or Vanilla JS

Pick the React entry point when the surrounding UI already lives in React. Jump
to the [getting started with React](#get-started-with-react) guide for more
info.

<DocsCodeExample {...CHOOSE_INTEGRATION_REACT_EXAMPLE} />

Pick the vanilla class when your app is not React-based, or when another
Pick the Vue entry point when the surrounding UI already lives in Vue 3. Jump to
the [getting started with Vue](#get-started-with-vue) guide for more.

<DocsCodeExample {...CHOOSE_INTEGRATION_VUE_EXAMPLE} />

Pick the vanilla class when your app is not React- or Vue-based, or when another
framework will own lifecycle around an imperative model. Jump to the
[getting started with vanilla JS](#get-started-with-vanilla) for more.

<DocsCodeExample {...CHOOSE_INTEGRATION_VANILLA_EXAMPLE} />

#### 3. Understand tree-shape

Both runtimes consume the same tree data. Small examples can start with raw
All runtimes consume the same tree data. Small examples can start with raw
`paths`, but real application trees should move to prepared input before the
client pays that shaping work on every load. Regardless of your runtime being
React or vanilla JS, be sure to read
client pays that shaping work on every load. Regardless of your runtime, read
[Shape tree data for fast rendering](#shape-tree-data-for-fast-rendering) after
your runtime quickstart.
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
### Get started with vanilla

Use the vanilla runtime when your app is not React-based, or when another
framework will own lifecycle around an imperative tree model.
Use the vanilla runtime when your app is not React- or Vue-based, or when
another framework will own lifecycle around an imperative tree model.
`new FileTree(...)` creates the model, and `render(...)` or `hydrate(...)`
attaches it to the DOM.

Expand Down Expand Up @@ -72,5 +72,5 @@ the same:
- mount or unmount around the instance
- keep all tree reads and writes on the model itself

That keeps React as the only first-class wrapper surface without changing how
the underlying model works.
That keeps framework wrappers thin without changing how the underlying model
works.
75 changes: 75 additions & 0 deletions apps/docs/app/(trees)/docs/Guides/GetStartedWithVue/constants.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import { docsCodeSnippet } from '@/lib/docsCodeSnippet';

export const VUE_QUICKSTART_INSTALL = docsCodeSnippet(
'install.sh',
`bun add @pierre/trees
# npm: npm install @pierre/trees
# pnpm: pnpm add @pierre/trees`
);

export const VUE_QUICKSTART_PROJECT_TREE = docsCodeSnippet(
'project-tree.vue',
`<script setup lang="ts">
import { FileTree, useFileTree } from '@pierre/trees/vue';
import type { FileTreePreparedInput } from '@pierre/trees';

const props = defineProps<{
preparedInput: FileTreePreparedInput;
}>();

const { model } = useFileTree({
preparedInput: props.preparedInput,
search: true,
initialExpandedPaths: ['src', 'src/components'],
});
</script>

<template>
<FileTree
:model="model"
class="rounded-lg border"
style="height: 320px"
/>
</template>`
);

export const VUE_QUICKSTART_SEARCHABLE_TREE = docsCodeSnippet(
'searchable-tree.vue',
`<script setup lang="ts">
import {
FileTree,
useFileTree,
useFileTreeSearch,
useFileTreeSelection,
} from '@pierre/trees/vue';

const props = defineProps<{
paths: readonly string[];
}>();

const { model } = useFileTree({
paths: props.paths,
fileTreeSearchMode: 'hide-non-matches',
search: true,
});
const selectedPaths = useFileTreeSelection(model);
const search = useFileTreeSearch(model);
const searchValue = search.value;

function handleSearchInput(event: Event) {
search.setValue((event.target as HTMLInputElement).value);
}
</script>

<template>
<div class="space-y-3">
<input
:value="searchValue"
placeholder="Search files"
@input="handleSearchInput"
/>
<p>{{ selectedPaths.length }} item(s) selected.</p>
<FileTree :model="model" class="rounded-lg border" />
</div>
</template>`
);
77 changes: 77 additions & 0 deletions apps/docs/app/(trees)/docs/Guides/GetStartedWithVue/content.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
### Get started with Vue

Use the Vue entry point when your surrounding UI already lives in Vue 3. The
composable creates one stable tree model, and the component mounts that model
into the host element.

#### Install `@pierre/trees`

Use the package root for the vanilla runtime, and the `/vue` entry point for the
Vue wrapper.

<DocsCodeExample {...VUE_QUICKSTART_INSTALL} />

#### Create the model with `useFileTree(...)`

`useFileTree(...)` from `@pierre/trees/vue` creates the model exactly once for
the component lifetime. Later option changes are not a controlled update path.
If the tree data or runtime behavior changes after mount, update the model
through methods such as `resetPaths(...)`, `setComposition(...)`,
`setGitStatus(...)`, or `setIcons(...)`.

For small trees, you can pass raw `paths`. For scalable trees, prefer
`preparedInput` produced on the server or another non-UI boundary.

<DocsCodeExample {...VUE_QUICKSTART_PROJECT_TREE} />

If you are still deciding how to shape that input, read
[Shape tree data for fast rendering](#shape-tree-data-for-fast-rendering) after
this quickstart.

#### Render with `<FileTree :model="model" />`

`<FileTree />` is a thin Vue wrapper over the model. It mounts the tree into the
host element, forwards normal host attributes such as `class` and `style`, and
hydrates existing server output when you pass `preloadedData` later.

Keep the mental model simple: the model owns the tree state, and the Vue
component renders it.

#### Read and update tree state through the model

Vue code reads snapshots from the model through composables, and writes back
through model methods.

<DocsCodeExample {...VUE_QUICKSTART_SEARCHABLE_TREE} />

Use `useFileTreeSelector(model, selector, equality?)` when sibling UI needs a
custom derived ref without updating on unrelated tree changes. For the shared
interaction vocabulary, read
[Navigate selection, focus, and search](#navigate-selection-focus-and-search)
and [Vue API](#vue-api).

#### Use simple `paths` input only when the tree is small

Raw `paths` is the low-ceremony path for demos, tests, and very small static
trees. It is not the scalable default. Once the tree becomes expensive to shape
or sort on the client, move that work out of the UI.

#### Move to prepared input before the tree gets expensive

The recommended scale path is:

1. Load canonical paths on the server or another non-UI boundary.
2. Prepare the tree input once.
3. Pass `preparedInput` into `useFileTree(...)`.

If the server already knows the final order,
`preparePresortedFileTreeInput(...)` is the highest-performance prepared-input
variant because the client can skip both shaping and extra sorting work.

#### Add SSR later when first paint matters

Hydration layers on top of the same model-first Vue story. The client still
calls `useFileTree(...)`, and the Vue wrapper still renders the same model. The
only difference is that the tree starts from preloaded server output.

When you need that flow, continue with [SSR](#ssr) and [SSR API](#ssr-api).
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ scale-oriented public path.
<DocsCodeExample {...LARGE_TREES_LOAD_WORKSPACE_TREE} />

If you only have an unsorted path list, use `prepareFileTreeInput(...)` instead.
Either way, pass the prepared result into React, vanilla, or SSR hydration.
Either way, pass the prepared result into React, Vue, vanilla, or SSR hydration.

#### Keep rendering work small

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -69,8 +69,8 @@ The DOM host is not the source of truth. The model is.

#### What to read next

- For runtime-specific lookup, jump to [React API](#react-api) or
[Vanilla API](#vanilla-api).
- For runtime-specific lookup, jump to [React API](#react-api),
[Vue API](#vue-api), or [Vanilla API](#vanilla-api).
- For shared search-mode semantics, read
[Shared concepts](#shared-concepts-search-mode-semantics).
- For row-level editing workflows that build on this same focus model, continue
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -83,4 +83,5 @@ separation clear.

If you need the shared vocabulary behind those events, read
[Shared concepts](#shared-concepts-mutation-vocabulary). For runtime lookup,
jump to [React API](#react-api) or [Vanilla API](#vanilla-api).
jump to [React API](#react-api), [Vue API](#vue-api), or
[Vanilla API](#vanilla-api).
26 changes: 26 additions & 0 deletions apps/docs/app/(trees)/docs/Guides/SSR/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,32 @@ export function ProjectTreeClient({
}`
);

export const SSR_GUIDE_VUE_HYDRATION = docsCodeSnippet(
'project-tree-client.vue',
`<script setup lang="ts">
import { FileTree, useFileTree } from '@pierre/trees/vue';
import type { FileTreePreparedInput } from '@pierre/trees';
import type { FileTreeSsrPayload } from '@pierre/trees/ssr';

const props = defineProps<{
preparedInput: FileTreePreparedInput;
preloadedData: FileTreeSsrPayload;
}>();

const { model } = useFileTree({
preparedInput: props.preparedInput,
id: props.preloadedData.id,
initialExpandedPaths: ['src'],
search: true,
initialVisibleRowCount: 11,
});
</script>

<template>
<FileTree :model="model" :preloaded-data="props.preloadedData" />
</template>`
);

export const SSR_GUIDE_VANILLA_HYDRATION = docsCodeSnippet(
'vanilla-hydrate.ts',
`import { FileTree } from '@pierre/trees';
Expand Down
22 changes: 16 additions & 6 deletions apps/docs/app/(trees)/docs/Guides/SSR/content.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@

Use SSR when you want the tree to arrive from the server with a fast first
paint, then become interactive on the client. This is not a third primary
runtime. It is a preload-and-hydrate layer over the same React or vanilla model.
runtime. It is a preload-and-hydrate layer over the same React, Vue, or vanilla
model.

#### Start with the server step

Expand Down Expand Up @@ -36,6 +37,15 @@ the wrapper receives the opaque handoff object as `preloadedData`.

The model stays primary. `preloadedData` only activates hydration.

#### Vue flow

Vue still creates the model with `useFileTree(...)`. The difference is that the
component receives the opaque handoff object as `preloadedData`.

<DocsCodeExample {...SSR_GUIDE_VUE_HYDRATION} />

The model stays primary. `preloadedData` only activates hydration.

#### Vanilla flow

Vanilla uses the same preload step, but the client hydrates the server-rendered
Expand All @@ -49,9 +59,9 @@ hydrating.
#### Keep the payload opaque

In docs and application code, refer to the preload result as an SSR payload or
handoff object. React consumes it as `preloadedData`. Vanilla hydrates existing
server markup. Both flows reuse the same server work without teaching payload
internals as the main story.
handoff object. React and Vue consume it as `preloadedData`. Vanilla hydrates
existing server markup. Every flow reuses the same server work without teaching
payload internals as the main story.

#### Pair SSR with prepared input for large trees

Expand All @@ -61,8 +71,8 @@ client hydrate that same result.

#### Advanced note: declarative shadow DOM

The preloaded path uses declarative shadow DOM under the hood. In React, the
packaged wrapper already handles the host ownership details it needs for
The preloaded path uses declarative shadow DOM under the hood. In React and Vue,
the packaged wrappers already handle the host ownership details they need for
hydration. Use the runtime behavior instead of inventing custom DOM diffing or
raw payload plumbing.

Expand Down
Loading