Skip to content
Merged
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
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,13 @@ import {
type PathStoreFileTreeOptions,
} from '@pierre/trees/path-store';
import type { ReactNode } from 'react';
import { useCallback, useMemo } from 'react';
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';

import { ExampleCard } from '../_components/ExampleCard';
import { StateLog, useStateLog } from '../_components/StateLog';
import { pathStoreCapabilityMatrix } from './capabilityMatrix';
import { createPresortedPreparedInput } from './createPresortedPreparedInput';
import { PATH_STORE_CUSTOM_ICONS } from './pathStoreDemoIcons';

interface SharedDemoOptions extends Omit<
PathStoreFileTreeOptions,
Expand All @@ -24,6 +25,7 @@ interface PathStorePoweredRenderDemoClientProps {

function HydratedPathStoreExample({
containerHtml,
icons,
description,
footer,
options,
Expand All @@ -32,29 +34,43 @@ function HydratedPathStoreExample({
containerHtml: string;
description: string;
footer?: ReactNode;
options: PathStoreFileTreeOptions;
icons: PathStoreFileTreeOptions['icons'];
options: Omit<PathStoreFileTreeOptions, 'icons'>;
title: string;
}) {
const ref = useCallback(
(node: HTMLDivElement | null) => {
if (node == null) {
return;
}

const fileTree = new PathStoreFileTree(options);
const fileTreeContainer = node.querySelector('file-tree-container');
if (fileTreeContainer instanceof HTMLElement) {
fileTree.hydrate({ fileTreeContainer });
} else {
fileTree.render({ containerWrapper: node });
}

return () => {
fileTree.cleanUp();
};
},
[options]
);
const ref = useRef<HTMLDivElement | null>(null);
const fileTreeRef = useRef<PathStoreFileTree | null>(null);
const latestIconsRef = useRef(icons);
latestIconsRef.current = icons;

useEffect(() => {
const node = ref.current;
if (node == null) {
return;
}

const fileTree = new PathStoreFileTree({
...options,
icons: latestIconsRef.current,
});
fileTreeRef.current = fileTree;
const fileTreeContainer = node.querySelector('file-tree-container');
if (fileTreeContainer instanceof HTMLElement) {
fileTree.hydrate({ fileTreeContainer });
} else {
node.innerHTML = '';
fileTree.render({ containerWrapper: node });
}

return () => {
fileTree.cleanUp();
fileTreeRef.current = null;
};
}, [containerHtml, options]);

useEffect(() => {
fileTreeRef.current?.setIcons(icons);
}, [icons]);

return (
<ExampleCard title={title} description={description} footer={footer}>
Expand All @@ -73,6 +89,9 @@ export function PathStorePoweredRenderDemoClient({
sharedOptions,
}: PathStorePoweredRenderDemoClientProps) {
const { addLog, log } = useStateLog();
const [iconMode, setIconMode] = useState<
'complete' | 'custom' | 'minimal' | 'standard'
>('complete');
const preparedInput = useMemo(
() => createPresortedPreparedInput(sharedOptions.paths),
[sharedOptions.paths]
Expand All @@ -83,7 +102,7 @@ export function PathStorePoweredRenderDemoClient({
},
[addLog]
);
const options = useMemo<PathStoreFileTreeOptions>(
const options = useMemo<Omit<PathStoreFileTreeOptions, 'icons'>>(
() => ({
...sharedOptions,
composition: {
Expand Down Expand Up @@ -113,35 +132,85 @@ export function PathStorePoweredRenderDemoClient({
},
},
},
id: 'pst-phase4',
id: 'pst-phase5-icons',
onSelectionChange: handleSelectionChange,
preparedInput,
}),
[addLog, handleSelectionChange, preparedInput, sharedOptions]
);
const activeIcons =
iconMode === 'custom' ? PATH_STORE_CUSTOM_ICONS : iconMode;

return (
<div className="space-y-6">
<header className="space-y-2">
<p className="text-muted-foreground text-xs font-semibold tracking-wide uppercase">
Path-store lane · provisional
</p>
<h1 className="text-2xl font-bold">Focus + Selection + Header Slot</h1>
<h1 className="text-2xl font-bold">
Focus + Selection + Header Slot + Icon Sets
</h1>
<p className="text-muted-foreground max-w-3xl text-sm leading-6">
Phase 4 keeps the landed focus/navigation model and adds selection:
click and keyboard selection semantics, path-first imperative item
methods, lightweight selection-change observation, and now the first
simple composition surface via a slotted header in the existing
path-store-powered demo.
The path-store lane keeps the landed focus and selection model,
preserves the header slot, and now proves the built-in Minimal,
Standard, and Complete icon sets alongside a fully custom icon
configuration.
</p>
<div className="flex flex-wrap gap-2 pt-2">
<button
type="button"
className="rounded-md border px-3 py-1.5 text-sm font-medium"
aria-pressed={iconMode === 'complete'}
onClick={() => {
setIconMode('complete');
addLog('icons: complete');
}}
>
Show Complete icons
</button>
<button
type="button"
className="rounded-md border px-3 py-1.5 text-sm font-medium"
aria-pressed={iconMode === 'standard'}
onClick={() => {
setIconMode('standard');
addLog('icons: standard');
}}
>
Show Standard icons
</button>
<button
type="button"
className="rounded-md border px-3 py-1.5 text-sm font-medium"
aria-pressed={iconMode === 'minimal'}
onClick={() => {
setIconMode('minimal');
addLog('icons: minimal');
}}
>
Show Minimal icons
</button>
<button
type="button"
className="rounded-md border px-3 py-1.5 text-sm font-medium"
aria-pressed={iconMode === 'custom'}
onClick={() => {
setIconMode('custom');
addLog('icons: custom');
}}
>
Show Custom icons
</button>
</div>
</header>

<HydratedPathStoreExample
containerHtml={containerHtml}
description="Click a row to select it, use Ctrl/Cmd-click and Shift-click for multi-selection, and try the slotted header button above the tree. Directory rows still keep the Phase 2 toggle behavior on plain click, and selection changes are logged below."
description="Click a row to select it, use Ctrl/Cmd-click and Shift-click for multi-selection, try the slotted header button above the tree, then switch between the Complete, Standard, Minimal, and Custom icon modes. Expansion, selection, and focus should stay intact while only the icons change."
footer={<StateLog entries={log} />}
icons={activeIcons}
options={options}
title="Focus + Selection + Header Slot"
title="Focus + Selection + Header Slot + Icon Sets"
/>

<section className="space-y-3 rounded-lg border p-4">
Expand Down
3 changes: 2 additions & 1 deletion apps/docs/app/trees-dev/path-store-powered/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,8 @@ export default function PathStorePoweredPage() {

const payload = preloadPathStoreFileTree({
...sharedOptions,
id: 'pst-phase4',
icons: 'complete',
id: 'pst-phase5-icons',
preparedInput: linuxKernelPreparedInput,
});

Expand Down
21 changes: 21 additions & 0 deletions apps/docs/app/trees-dev/path-store-powered/pathStoreDemoIcons.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import type { FileTreeIcons } from '@pierre/trees';

export const PATH_STORE_CUSTOM_ICONS: FileTreeIcons = {
byFileExtension: {
ts: 'pst-phase5-icon-typescript',
},
byFileName: {
'readme.md': 'pst-phase5-icon-readme',
},
spriteSheet: `<svg data-icon-sprite aria-hidden="true" width="0" height="0">
<symbol id="pst-phase5-icon-readme" viewBox="0 0 16 16">
<path fill="currentColor" d="M3 2.5h7l3 3V13a1 1 0 0 1-1 1H4a1 1 0 0 1-1-1z" />
<path fill="white" d="M10 2.5v3h3" />
<path fill="white" d="M5 8h6v1H5zm0 2h4v1H5z" />
</symbol>
<symbol id="pst-phase5-icon-typescript" viewBox="0 0 16 16">
<rect width="16" height="16" rx="3" fill="currentColor" />
<path fill="white" d="M4 4h8v2H9v6H7V6H4zm8.3 2.5c-.4-.3-.8-.5-1.4-.5-.8 0-1.2.4-1.2 1 0 .7.5 1 1.5 1.3 1.4.5 2.1 1.1 2.1 2.4 0 1.5-1.2 2.4-2.9 2.4-1.2 0-2.2-.4-2.9-1.1l1.1-1.3c.5.4 1.1.7 1.7.7.8 0 1.3-.3 1.3-.9 0-.6-.4-.8-1.5-1.2-1.3-.5-2.1-1.1-2.1-2.5C8.1 5 9.2 4 10.9 4c1 0 1.8.3 2.5.9z" />
</symbol>
</svg>`,
};
Loading
Loading