From 23aca46fc6589109abcd85538747ec354566a52e Mon Sep 17 00:00:00 2001 From: "Max G." Date: Sun, 25 May 2025 00:07:34 +0200 Subject: [PATCH 1/6] wip: tree --- package.json | 1 + pnpm-lock.yaml | 8 ++ src/lib/assets/thumbs/trees-dark.png | Bin 0 -> 3144 bytes src/lib/assets/thumbs/trees.png | Bin 0 -> 3114 bytes src/lib/componentRegistry.components.ts | 17 ++- src/lib/componentRegistry.types.ts | 5 +- src/lib/components/trees/tree-01.svelte | 71 +++++++++++ src/lib/components/trees/tree-02.svelte | 73 +++++++++++ src/lib/components/trees/tree-03.svelte | 90 ++++++++++++++ src/lib/components/trees/tree-04.svelte | 90 ++++++++++++++ src/lib/components/trees/tree-05.svelte | 114 ++++++++++++++++++ src/lib/components/ui/tree/index.ts | 8 ++ .../tree-assistive-tree-description.svelte | 68 +++++++++++ .../ui/tree/tree-context-provider.svelte | 15 +++ .../components/ui/tree/tree-context.svelte.ts | 24 ++++ .../components/ui/tree/tree-drag-line.svelte | 42 +++++++ src/lib/components/ui/tree/tree-item.svelte | 78 ++++++++++++ src/lib/components/ui/tree/tree-label.svelte | 49 ++++++++ src/lib/components/ui/tree/tree.svelte | 38 ++++++ src/lib/components/ui/tree/use-tree.svelte.ts | 83 +++++++++++++ vite.config.ts | 5 + 21 files changed, 877 insertions(+), 2 deletions(-) create mode 100644 src/lib/assets/thumbs/trees-dark.png create mode 100644 src/lib/assets/thumbs/trees.png create mode 100644 src/lib/components/trees/tree-01.svelte create mode 100644 src/lib/components/trees/tree-02.svelte create mode 100644 src/lib/components/trees/tree-03.svelte create mode 100644 src/lib/components/trees/tree-04.svelte create mode 100644 src/lib/components/trees/tree-05.svelte create mode 100644 src/lib/components/ui/tree/index.ts create mode 100644 src/lib/components/ui/tree/tree-assistive-tree-description.svelte create mode 100644 src/lib/components/ui/tree/tree-context-provider.svelte create mode 100644 src/lib/components/ui/tree/tree-context.svelte.ts create mode 100644 src/lib/components/ui/tree/tree-drag-line.svelte create mode 100644 src/lib/components/ui/tree/tree-item.svelte create mode 100644 src/lib/components/ui/tree/tree-label.svelte create mode 100644 src/lib/components/ui/tree/tree.svelte create mode 100644 src/lib/components/ui/tree/use-tree.svelte.ts diff --git a/package.json b/package.json index 4752cf10..ea0099a6 100644 --- a/package.json +++ b/package.json @@ -23,6 +23,7 @@ "@fontsource-variable/inter": "^5.2.5", "@fontsource/dm-serif-display": "^5.2.5", "@fontsource/jetbrains-mono": "^5.2.5", + "@headless-tree/core": "^1.0.1", "@internationalized/date": "^3.8.0", "@lucide/svelte": "^0.511.0", "@tanstack/table-core": "^8.21.3", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index a2194edf..98f1a181 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -29,6 +29,9 @@ importers: '@fontsource/jetbrains-mono': specifier: ^5.2.5 version: 5.2.5 + '@headless-tree/core': + specifier: ^1.0.1 + version: 1.0.1 '@internationalized/date': specifier: ^3.8.0 version: 3.8.0 @@ -824,6 +827,9 @@ packages: '@fontsource/jetbrains-mono@5.2.5': resolution: {integrity: sha512-TPZ9b/uq38RMdrlZZkl0RwN8Ju9JxuqMETrw76pUQFbGtE1QbwQaNsLlnUrACNNBNbd0NZRXiJJSkC8ajPgbew==} + '@headless-tree/core@1.0.1': + resolution: {integrity: sha512-HbkveX2B5jltHS+90uTgD1CrgTEexjszff4+PRB8onMnAFZJ/eYPGVHZF5sK2dui9dGmP5OwNYTP6r2YXGCe+w==} + '@humanfs/core@0.19.1': resolution: {integrity: sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==} engines: {node: '>=18.18.0'} @@ -3611,6 +3617,8 @@ snapshots: '@fontsource/jetbrains-mono@5.2.5': {} + '@headless-tree/core@1.0.1': {} + '@humanfs/core@0.19.1': {} '@humanfs/node@0.16.6': diff --git a/src/lib/assets/thumbs/trees-dark.png b/src/lib/assets/thumbs/trees-dark.png new file mode 100644 index 0000000000000000000000000000000000000000..bfeb36d339c2a4f2582d3075f81a83f5818d2cee GIT binary patch literal 3144 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 literal 0 HcmV?d00001 diff --git a/src/lib/componentRegistry.components.ts b/src/lib/componentRegistry.components.ts index 828882ab..cd19cd47 100644 --- a/src/lib/componentRegistry.components.ts +++ b/src/lib/componentRegistry.components.ts @@ -2,7 +2,7 @@ /** * !!!!!!!!!! * This file is auto-generated. Do not edit manually - * Last generated at: 5/24/2025, 7:36:58 PM + * Last generated at: 5/24/2025, 11:03:19 PM * To update, run: pnpm generate:registry --format * @version 0.0.1 * !!!!!!!!!! @@ -730,6 +730,21 @@ export const OUI_DIRECTORIES = { todo: 0, ready: 12 } + }, + TREES: { + directory: 'trees', + name: 'Trees', + components: [ + 'tree-01.svelte', + 'tree-02.svelte', + 'tree-03.svelte', + 'tree-04.svelte', + 'tree-05.svelte' + ], + status: { + todo: 0, + ready: 5 + } } } as const; export type OUIDirectories = typeof OUI_DIRECTORIES; diff --git a/src/lib/componentRegistry.types.ts b/src/lib/componentRegistry.types.ts index ed2a1277..e83c3dec 100644 --- a/src/lib/componentRegistry.types.ts +++ b/src/lib/componentRegistry.types.ts @@ -2,7 +2,7 @@ /** * !!!!!!!!!! * This file is auto-generated. Do not edit manually - * Last generated at: 5/24/2025, 7:36:58 PM + * Last generated at: 5/24/2025, 11:03:19 PM * To update, run: pnpm generate:registry --format * @version 0.0.1 * !!!!!!!!!! @@ -110,6 +110,7 @@ export type OUITabsComponents = OUIComponentHelper<'TABS'>; export type OUITextareasComponents = OUIComponentHelper<'TEXTAREAS'>; export type OUITimelinesComponents = OUIComponentHelper<'TIMELINES'>; export type OUITooltipsComponents = OUIComponentHelper<'TOOLTIPS'>; +export type OUITreesComponents = OUIComponentHelper<'TREES'>; // All Component Types export type OUIComponent = Prettify< @@ -136,6 +137,7 @@ export type OUIComponent = Prettify< | OUITextareasComponents | OUITimelinesComponents | OUITooltipsComponents + | OUITreesComponents >; // Directory To Component @@ -163,4 +165,5 @@ export type OUIDirectoryToComponent = Prettify<{ textareas: OUITextareasComponents; timelines: OUITimelinesComponents; tooltips: OUITooltipsComponents; + trees: OUITreesComponents; }>; diff --git a/src/lib/components/trees/tree-01.svelte b/src/lib/components/trees/tree-01.svelte new file mode 100644 index 00000000..42586492 --- /dev/null +++ b/src/lib/components/trees/tree-01.svelte @@ -0,0 +1,71 @@ + + + + {#each tree.current.getItems() as item (item.getId())} + + + {item.getItemData().name} + + + {/each} + diff --git a/src/lib/components/trees/tree-02.svelte b/src/lib/components/trees/tree-02.svelte new file mode 100644 index 00000000..9904987c --- /dev/null +++ b/src/lib/components/trees/tree-02.svelte @@ -0,0 +1,73 @@ + + +
+
+ + {#each tree.current.getItems() as item (item.getId())} + + + + {/each} + +
+
diff --git a/src/lib/components/trees/tree-03.svelte b/src/lib/components/trees/tree-03.svelte new file mode 100644 index 00000000..0d36e960 --- /dev/null +++ b/src/lib/components/trees/tree-03.svelte @@ -0,0 +1,90 @@ + + +
+
+ + {#each tree.current.getItems() as item (item.getId())} + + + + {#if item.isFolder()} + {#if item.isExpanded()} + + {:else} + + {/if} + {:else} + + {/if} + + {item.getItemName()} + + + + {/each} + +
+
diff --git a/src/lib/components/trees/tree-04.svelte b/src/lib/components/trees/tree-04.svelte new file mode 100644 index 00000000..9c15b940 --- /dev/null +++ b/src/lib/components/trees/tree-04.svelte @@ -0,0 +1,90 @@ + + +
+
+ + {#each tree.current.getItems() as item (item.getId())} + + + + {#if item.isFolder()} + {#if item.isExpanded()} + + {:else} + + {/if} + {:else} + + {/if} + + {item.getItemName()} + + + + {/each} + +
+
diff --git a/src/lib/components/trees/tree-05.svelte b/src/lib/components/trees/tree-05.svelte new file mode 100644 index 00000000..bffa8119 --- /dev/null +++ b/src/lib/components/trees/tree-05.svelte @@ -0,0 +1,114 @@ + + +
+
+ + + {#each tree.current.getItems() as item (item.getId())} + + + + {#if item.isFolder()} + {#if item.isExpanded()} + + {:else} + + {/if} + {/if} + + {item.getItemName()} + + + + {/each} + + +
+
diff --git a/src/lib/components/ui/tree/index.ts b/src/lib/components/ui/tree/index.ts new file mode 100644 index 00000000..c8d174ec --- /dev/null +++ b/src/lib/components/ui/tree/index.ts @@ -0,0 +1,8 @@ +import TreeAssistiveTreeDescription from './tree-assistive-tree-description.svelte'; +import TreeDragLine from './tree-drag-line.svelte'; +import TreeItem from './tree-item.svelte'; +import TreeLabel from './tree-label.svelte'; +import Tree from './tree.svelte'; +import { useTree } from './use-tree.svelte'; + +export { Tree, TreeAssistiveTreeDescription, TreeDragLine, TreeItem, TreeLabel, useTree }; diff --git a/src/lib/components/ui/tree/tree-assistive-tree-description.svelte b/src/lib/components/ui/tree/tree-assistive-tree-description.svelte new file mode 100644 index 00000000..22fa44f3 --- /dev/null +++ b/src/lib/components/ui/tree/tree-assistive-tree-description.svelte @@ -0,0 +1,68 @@ + + + + + {getLabel(state.dnd, state.assistiveDndState ?? AssistiveDndState.None, tree.getHotkeyPresets())} + diff --git a/src/lib/components/ui/tree/tree-context-provider.svelte b/src/lib/components/ui/tree/tree-context-provider.svelte new file mode 100644 index 00000000..bccc7ce0 --- /dev/null +++ b/src/lib/components/ui/tree/tree-context-provider.svelte @@ -0,0 +1,15 @@ + + +{@render children()} diff --git a/src/lib/components/ui/tree/tree-context.svelte.ts b/src/lib/components/ui/tree/tree-context.svelte.ts new file mode 100644 index 00000000..4c882439 --- /dev/null +++ b/src/lib/components/ui/tree/tree-context.svelte.ts @@ -0,0 +1,24 @@ +import type { ItemInstance } from '@headless-tree/core'; + +import { Context } from 'runed'; + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export interface TreeContextValue { + currentItem?: ItemInstance; + indent: number; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + tree?: any; +} + +export const treeContext = new Context('tree:context'); + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export function useTreeContext() { + const context = treeContext.get(); + + if (!context) { + throw new Error('useTree must be used within a Tree'); + } + + return context as TreeContextValue; +} diff --git a/src/lib/components/ui/tree/tree-drag-line.svelte b/src/lib/components/ui/tree/tree-drag-line.svelte new file mode 100644 index 00000000..b398daa1 --- /dev/null +++ b/src/lib/components/ui/tree/tree-drag-line.svelte @@ -0,0 +1,42 @@ + + +
+ {@render children?.()} +
diff --git a/src/lib/components/ui/tree/tree-item.svelte b/src/lib/components/ui/tree/tree-item.svelte new file mode 100644 index 00000000..e8687844 --- /dev/null +++ b/src/lib/components/ui/tree/tree-item.svelte @@ -0,0 +1,78 @@ + + + + + {#if child} + {@render child({ props: mergedProps })} + {:else} + + {/if} + diff --git a/src/lib/components/ui/tree/tree-label.svelte b/src/lib/components/ui/tree/tree-label.svelte new file mode 100644 index 00000000..d4c1a368 --- /dev/null +++ b/src/lib/components/ui/tree/tree-label.svelte @@ -0,0 +1,49 @@ + + + +{#if item} + + {#if item.isFolder()} + + {/if} + {#if children} + {@render children?.()} + {:else} + {typeof item.getItemName === 'function' ? item.getItemName() : null} + {/if} + +{/if} diff --git a/src/lib/components/ui/tree/tree.svelte b/src/lib/components/ui/tree/tree.svelte new file mode 100644 index 00000000..8874b917 --- /dev/null +++ b/src/lib/components/ui/tree/tree.svelte @@ -0,0 +1,38 @@ + + + + +
+ {@render children?.()} +
+
diff --git a/src/lib/components/ui/tree/use-tree.svelte.ts b/src/lib/components/ui/tree/use-tree.svelte.ts new file mode 100644 index 00000000..c5e864cd --- /dev/null +++ b/src/lib/components/ui/tree/use-tree.svelte.ts @@ -0,0 +1,83 @@ +import { + createTree, + type FeatureImplementation, + type TreeConfig, + type TreeInstance, + type TreeState +} from '@headless-tree/core'; +import { createAttachmentKey } from 'svelte/attachments'; +import { createSubscriber } from 'svelte/reactivity'; + +// This is a workaround to allow for behavior in Svelte. + +export const svelteFeatures: FeatureImplementation = { + itemInstance: { + getProps: ({ item, prev }) => { + const { onClick, onDragEnd, onDragLeave, onDragOver, onDragStart, onDrop, ...rest } = + prev?.() ?? {}; + + const attacher = { + [createAttachmentKey()]: (node: HTMLElement) => { + item.registerElement(node); + } + }; + return { + ...rest, + attacher, + onclick: onClick, + ondragend: onDragEnd, + ondragleave: onDragLeave, + ondragover: onDragOver, + ondragstart: onDragStart, + ondrop: onDrop + }; + } + } +}; + +export class ReactiveTree { + #tree: TreeInstance; + #subscribe: () => void; + #currentState: TreeState; + + constructor(config: TreeConfig) { + const configWithFeatures: TreeConfig = { + ...config, + features: [...(config.features ?? []), svelteFeatures] + }; + this.#tree = createTree(configWithFeatures); + + this.#currentState = this.#tree.getState(); + + this.#subscribe = createSubscriber((update) => { + this.#tree.setConfig((prev) => ({ + ...prev, + ...config, + features: [...(config.features ?? []), svelteFeatures], + setState: (newState) => { + this.#currentState = { + ...this.#currentState, + ...newState + }; + config.setState?.(this.#currentState); + // Trigger reactive updates when tree state changes + update(); + }, + state: { + ...this.#currentState, + ...config.state + } + })); + + // Do we need a cleanup? + return () => {}; + }); + } + + get current(): TreeInstance { + this.#subscribe(); + return this.#tree; + } +} + +export const useTree = (config: TreeConfig) => new ReactiveTree(config); diff --git a/vite.config.ts b/vite.config.ts index d0a5b2d1..6a161159 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -13,6 +13,11 @@ export default defineConfig({ compiler: 'svelte' }) ], + + ssr: { + //https://github.com/lukasbach/headless-tree/issues/104 + noExternal: ['@headless-tree/core'] + }, build: { rollupOptions: { onwarn(warning, warn) { From 88b4ec574abc543472906cb70e58b3b52a9e5f97 Mon Sep 17 00:00:00 2001 From: "Max G." Date: Sun, 25 May 2025 19:58:50 +0200 Subject: [PATCH 2/6] progress --- src/lib/componentRegistry.components.ts | 9 +- src/lib/componentRegistry.types.ts | 2 +- src/lib/components/trees/tree-05.svelte | 64 ++-- src/lib/components/trees/tree-06.svelte | 103 ++++++ src/lib/components/trees/tree-07.svelte | 153 +++++++++ src/lib/components/trees/tree-08.svelte | 314 ++++++++++++++++++ .../components/ui/tree/tree-context.svelte.ts | 4 +- src/lib/components/ui/tree/tree-item.svelte | 2 +- src/lib/components/ui/tree/tree-label.svelte | 6 +- src/lib/components/ui/tree/tree.svelte | 8 +- src/lib/components/ui/tree/use-tree.svelte.ts | 128 ++++++- 11 files changed, 737 insertions(+), 56 deletions(-) create mode 100644 src/lib/components/trees/tree-06.svelte create mode 100644 src/lib/components/trees/tree-07.svelte create mode 100644 src/lib/components/trees/tree-08.svelte diff --git a/src/lib/componentRegistry.components.ts b/src/lib/componentRegistry.components.ts index cd19cd47..3be2fa2a 100644 --- a/src/lib/componentRegistry.components.ts +++ b/src/lib/componentRegistry.components.ts @@ -2,7 +2,7 @@ /** * !!!!!!!!!! * This file is auto-generated. Do not edit manually - * Last generated at: 5/24/2025, 11:03:19 PM + * Last generated at: 5/25/2025, 7:58:07 PM * To update, run: pnpm generate:registry --format * @version 0.0.1 * !!!!!!!!!! @@ -739,11 +739,14 @@ export const OUI_DIRECTORIES = { 'tree-02.svelte', 'tree-03.svelte', 'tree-04.svelte', - 'tree-05.svelte' + 'tree-05.svelte', + 'tree-06.svelte', + 'tree-07.svelte', + 'tree-08.svelte' ], status: { todo: 0, - ready: 5 + ready: 8 } } } as const; diff --git a/src/lib/componentRegistry.types.ts b/src/lib/componentRegistry.types.ts index e83c3dec..2a04c091 100644 --- a/src/lib/componentRegistry.types.ts +++ b/src/lib/componentRegistry.types.ts @@ -2,7 +2,7 @@ /** * !!!!!!!!!! * This file is auto-generated. Do not edit manually - * Last generated at: 5/24/2025, 11:03:19 PM + * Last generated at: 5/25/2025, 7:58:07 PM * To update, run: pnpm generate:registry --format * @version 0.0.1 * !!!!!!!!!! diff --git a/src/lib/components/trees/tree-05.svelte b/src/lib/components/trees/tree-05.svelte index bffa8119..b3f803fb 100644 --- a/src/lib/components/trees/tree-05.svelte +++ b/src/lib/components/trees/tree-05.svelte @@ -54,37 +54,39 @@ const indent = 20; - const tree = useTree({ - canReorder: true, - dataLoader: { - getChildren: (itemId) => items[itemId].children ?? [], - getItem: (itemId) => items[itemId] - }, - features: [ - syncDataLoaderFeature, - selectionFeature, - hotkeysCoreFeature, - dragAndDropFeature, - keyboardDragAndDropFeature - ], - getItemName: (item) => item.getItemData().name, - indent, - initialState: { - expandedItems: ['engineering', 'frontend', 'design-system'], - selectedItems: ['components'] - }, - isItemFolder: (item) => (item.getItemData()?.children?.length ?? 0) > 0, - onDrop: createOnDropHandler((parentItem, newChildrenIds) => { - items = { - ...items, - [parentItem.getId()]: { - ...items[parentItem.getId()], - children: newChildrenIds - } - }; - }), - rootItemId: 'company' - }); + const tree = $derived( + useTree({ + canReorder: true, + dataLoader: { + getChildren: (itemId) => items[itemId].children ?? [], + getItem: (itemId) => items[itemId] + }, + features: [ + syncDataLoaderFeature, + selectionFeature, + hotkeysCoreFeature, + dragAndDropFeature, + keyboardDragAndDropFeature + ], + getItemName: (item) => item.getItemData().name, + indent, + initialState: { + expandedItems: ['engineering', 'frontend', 'design-system'], + selectedItems: ['components'] + }, + isItemFolder: (item) => (item.getItemData()?.children?.length ?? 0) > 0, + onDrop: createOnDropHandler((parentItem, newChildrenIds) => { + items = { + ...items, + [parentItem.getId()]: { + ...items[parentItem.getId()], + children: newChildrenIds + } + }; + }), + rootItemId: 'company' + }) + );
diff --git a/src/lib/components/trees/tree-06.svelte b/src/lib/components/trees/tree-06.svelte new file mode 100644 index 00000000..05f4f793 --- /dev/null +++ b/src/lib/components/trees/tree-06.svelte @@ -0,0 +1,103 @@ + + +
+
+ + {#each tree.current.getItems() as item (item.getId())} + + + + {#if item.isFolder()} + {#if item.isExpanded()} + + {:else} + + {/if} + {/if} + + {#if item.isRenaming()} + {@const { attacher, ...rest } = item.getRenameInputProps()} + + {:else} + {item.getItemName()} + {/if} + + + + {/each} + +
+
diff --git a/src/lib/components/trees/tree-07.svelte b/src/lib/components/trees/tree-07.svelte new file mode 100644 index 00000000..e234224e --- /dev/null +++ b/src/lib/components/trees/tree-07.svelte @@ -0,0 +1,153 @@ + + +
+
+ +
+
+
+ +
+ + {#each tree.current.getItems() as item (item.getId())} + + + + {#if item.isFolder()} + {#if item.isExpanded()} + + {:else} + + {/if} + {/if} + + {item.getItemName()} + + + + {/each} + +
+
diff --git a/src/lib/components/trees/tree-08.svelte b/src/lib/components/trees/tree-08.svelte new file mode 100644 index 00000000..7b59c9aa --- /dev/null +++ b/src/lib/components/trees/tree-08.svelte @@ -0,0 +1,314 @@ + + +
+
+ +
+
+ {#if searchValue} + + {/if} +
+ +
+ + {#if searchValue && filteredItems.length === 0} +

No items found for "{searchValue}"

+ {:else} + {#each tree.current.getItems() as item (item.getId())} + {@const isVisible = shouldShowItem(item.getId())} + + + + {#if item.isFolder()} + {#if item.isExpanded()} + + {:else} + + {/if} + {/if} + + {item.getItemName()} + + + + {/each} + {/if} +
+
+ +

+ Tree with filtering ∙ + + API + +

+
diff --git a/src/lib/components/ui/tree/tree-context.svelte.ts b/src/lib/components/ui/tree/tree-context.svelte.ts index 4c882439..8470c697 100644 --- a/src/lib/components/ui/tree/tree-context.svelte.ts +++ b/src/lib/components/ui/tree/tree-context.svelte.ts @@ -1,4 +1,4 @@ -import type { ItemInstance } from '@headless-tree/core'; +import type { ItemInstance, TreeInstance } from '@headless-tree/core'; import { Context } from 'runed'; @@ -7,7 +7,7 @@ export interface TreeContextValue { currentItem?: ItemInstance; indent: number; // eslint-disable-next-line @typescript-eslint/no-explicit-any - tree?: any; + tree?: TreeInstance; } export const treeContext = new Context('tree:context'); diff --git a/src/lib/components/ui/tree/tree-item.svelte b/src/lib/components/ui/tree/tree-item.svelte index e8687844..f9c2d268 100644 --- a/src/lib/components/ui/tree/tree-item.svelte +++ b/src/lib/components/ui/tree/tree-item.svelte @@ -47,7 +47,7 @@ ({ 'aria-expanded': item.isExpanded(), class: cn( - 'z-10 ps-[--tree-padding] outline-hidden select-none not-last:pb-0.5 focus:z-20 data-[disabled]:pointer-events-none data-[disabled]:opacity-50', + 'group/tree-item z-10 ps-[--tree-padding] outline-none select-none not-last:pb-0.5 focus:z-20 data-[disabled]:pointer-events-none data-[disabled]:opacity-50', className ), 'data-drag-target': diff --git a/src/lib/components/ui/tree/tree-label.svelte b/src/lib/components/ui/tree/tree-label.svelte index d4c1a368..46302de6 100644 --- a/src/lib/components/ui/tree/tree-label.svelte +++ b/src/lib/components/ui/tree/tree-label.svelte @@ -32,13 +32,15 @@ {#if item.isFolder()} - + {/if} {#if children} {@render children?.()} diff --git a/src/lib/components/ui/tree/tree.svelte b/src/lib/components/ui/tree/tree.svelte index 8874b917..f55bc629 100644 --- a/src/lib/components/ui/tree/tree.svelte +++ b/src/lib/components/ui/tree/tree.svelte @@ -32,7 +32,13 @@ -
+
{@render children?.()}
diff --git a/src/lib/components/ui/tree/use-tree.svelte.ts b/src/lib/components/ui/tree/use-tree.svelte.ts index c5e864cd..5cce03fd 100644 --- a/src/lib/components/ui/tree/use-tree.svelte.ts +++ b/src/lib/components/ui/tree/use-tree.svelte.ts @@ -1,6 +1,7 @@ import { createTree, type FeatureImplementation, + type ItemInstance, type TreeConfig, type TreeInstance, type TreeState @@ -8,28 +9,66 @@ import { import { createAttachmentKey } from 'svelte/attachments'; import { createSubscriber } from 'svelte/reactivity'; -// This is a workaround to allow for behavior in Svelte. - +// This is a workaround to allow for behavior in Svelte. export const svelteFeatures: FeatureImplementation = { itemInstance: { - getProps: ({ item, prev }) => { - const { onClick, onDragEnd, onDragLeave, onDragOver, onDragStart, onDrop, ...rest } = - prev?.() ?? {}; + getProps: ({ prev }) => { + const props = prev?.() ?? {}; + + return { + ...props, + attacher: { + [createAttachmentKey()]: (node: HTMLElement) => { + props.ref?.(node); + } + }, + onclick: props.onClick ? (e: MouseEvent) => props.onClick(e) : undefined, + ondragend: props.onDragEnd ? (e: DragEvent) => props.onDragEnd(e) : undefined, + ondragleave: props.onDragLeave ? (e: DragEvent) => props.onDragLeave(e) : undefined, + ondragover: props.onDragOver ? (e: DragEvent) => props.onDragOver(e) : undefined, + ondragstart: props.onDragStart ? (e: DragEvent) => props.onDragStart(e) : undefined, + ondrop: props.onDrop ? (e: DragEvent) => props.onDrop(e) : undefined + }; + }, + getRenameInputProps({ prev }) { + const { onBlur, onChange, ...props } = prev?.() ?? {}; - const attacher = { + return { + ...props, [createAttachmentKey()]: (node: HTMLElement) => { - item.registerElement(node); + props.ref?.(node); + }, + onblur: onBlur ? (e: FocusEvent) => onBlur(e) : undefined, + oninput: onChange ? (e: Event) => onChange(e) : undefined + }; + } + }, + treeInstance: { + getContainerProps({ prev }, ...rest) { + const props = prev?.() ?? {}; + return { + ...props, + ...rest, + attacher: { + [createAttachmentKey()]: (node: HTMLElement) => { + props.ref?.(node); + } } }; + }, + getSearchInputElementProps({ prev }, ...rest) { + const { onBlur, onChange, ...props } = prev?.() ?? {}; + const extendedOnChange = (e: Event) => { + onChange?.(e); + }; return { + ...props, ...rest, - attacher, - onclick: onClick, - ondragend: onDragEnd, - ondragleave: onDragLeave, - ondragover: onDragOver, - ondragstart: onDragStart, - ondrop: onDrop + [createAttachmentKey()]: (node: HTMLElement) => { + props.ref?.(node); + }, + onblur: onBlur ? (e: FocusEvent) => onBlur(e) : undefined, + oninput: onChange ? (e: Event) => extendedOnChange(e) : undefined }; } } @@ -39,6 +78,8 @@ export class ReactiveTree { #tree: TreeInstance; #subscribe: () => void; #currentState: TreeState; + #itemProxies = new Map>(); + #treeProxy: null | TreeInstance = null; constructor(config: TreeConfig) { const configWithFeatures: TreeConfig = { @@ -54,12 +95,15 @@ export class ReactiveTree { ...prev, ...config, features: [...(config.features ?? []), svelteFeatures], + setState: (newState) => { this.#currentState = { ...this.#currentState, ...newState }; config.setState?.(this.#currentState); + // Clear item proxies cache when state changes to ensure fresh reactive state + this.#itemProxies.clear(); // Trigger reactive updates when tree state changes update(); }, @@ -72,11 +116,65 @@ export class ReactiveTree { // Do we need a cleanup? return () => {}; }); + + // Create the tree proxy once + this.#treeProxy = this.#createTreeProxy(); + } + + #createItemProxy(item: ItemInstance): ItemInstance { + const proxy = new Proxy(item, { + get: (target, prop) => { + // Make these methods reactive by subscribing when they're called + if (typeof prop === 'string' && (prop.startsWith('is') || prop.startsWith('get'))) { + return (...args: unknown[]) => { + this.#subscribe(); // Subscribe to reactive updates + const method = target[prop as keyof ItemInstance] as (...args: unknown[]) => unknown; + return method?.apply(target, args); + }; + } + + return target[prop as keyof ItemInstance]; + } + }); + + return proxy; + } + + #createTreeProxy(): TreeInstance { + return new Proxy(this.#tree, { + get: (target, prop) => { + // Make getItems return proxied items + if (prop === 'getItems') { + return () => { + this.#subscribe(); // Subscribe to reactive updates + const items = target.getItems(); + return items.map((item) => { + const itemId = item.getId(); + if (!this.#itemProxies.has(itemId)) { + this.#itemProxies.set(itemId, this.#createItemProxy(item)); + } + return this.#itemProxies.get(itemId)!; + }); + }; + } + + // Make other tree methods reactive + if (typeof prop === 'string' && prop.startsWith('get')) { + return (...args: unknown[]) => { + this.#subscribe(); // Subscribe to reactive updates + const method = target[prop as keyof TreeInstance] as (...args: unknown[]) => unknown; + return method?.apply(target, args); + }; + } + + return target[prop as keyof TreeInstance]; + } + }); } get current(): TreeInstance { this.#subscribe(); - return this.#tree; + return this.#treeProxy!; } } From a93b25d450d43c6f587490b49bc6c43d9b06b012 Mon Sep 17 00:00:00 2001 From: "Max G." Date: Sat, 31 May 2025 15:58:42 +0200 Subject: [PATCH 3/6] fix: improve tree component reactivity and event handling --- src/lib/components/ui/tree/use-tree.svelte.ts | 189 +++++++++--------- 1 file changed, 99 insertions(+), 90 deletions(-) diff --git a/src/lib/components/ui/tree/use-tree.svelte.ts b/src/lib/components/ui/tree/use-tree.svelte.ts index 5cce03fd..bd13618d 100644 --- a/src/lib/components/ui/tree/use-tree.svelte.ts +++ b/src/lib/components/ui/tree/use-tree.svelte.ts @@ -1,11 +1,18 @@ +/** + * This is a workaround to allow for behavior in Svelte. + * DO NOT USE THIS IN PRODUCTION. + * If you have a better solution, please submit a PR. Even better, submit a PR to https://github.com/lukasbach/headless-tree + * to give svelte a native way to handle this. + */ + import { createTree, type FeatureImplementation, type ItemInstance, type TreeConfig, - type TreeInstance, - type TreeState + type TreeInstance } from '@headless-tree/core'; +import { tick } from 'svelte'; import { createAttachmentKey } from 'svelte/attachments'; import { createSubscriber } from 'svelte/reactivity'; @@ -14,168 +21,170 @@ export const svelteFeatures: FeatureImplementation = { itemInstance: { getProps: ({ prev }) => { const props = prev?.() ?? {}; - return { ...props, - attacher: { - [createAttachmentKey()]: (node: HTMLElement) => { - props.ref?.(node); - } + [createAttachmentKey()]: (node: HTMLElement) => { + props.ref(node); }, + onblur: props.onBlur ? (e: FocusEvent) => props.onBlur(e) : undefined, onclick: props.onClick ? (e: MouseEvent) => props.onClick(e) : undefined, ondragend: props.onDragEnd ? (e: DragEvent) => props.onDragEnd(e) : undefined, ondragleave: props.onDragLeave ? (e: DragEvent) => props.onDragLeave(e) : undefined, ondragover: props.onDragOver ? (e: DragEvent) => props.onDragOver(e) : undefined, ondragstart: props.onDragStart ? (e: DragEvent) => props.onDragStart(e) : undefined, - ondrop: props.onDrop ? (e: DragEvent) => props.onDrop(e) : undefined + ondrop: props.onDrop ? (e: DragEvent) => props.onDrop(e) : undefined, + onfocus: props.onFocus ? (e: FocusEvent) => props.onFocus(e) : undefined }; }, getRenameInputProps({ prev }) { const { onBlur, onChange, ...props } = prev?.() ?? {}; - return { ...props, [createAttachmentKey()]: (node: HTMLElement) => { props.ref?.(node); }, onblur: onBlur ? (e: FocusEvent) => onBlur(e) : undefined, + // Couldn't get onChange to work, so we're using oninput instead oninput: onChange ? (e: Event) => onChange(e) : undefined }; } }, + treeInstance: { getContainerProps({ prev }, ...rest) { const props = prev?.() ?? {}; return { ...props, ...rest, - attacher: { - [createAttachmentKey()]: (node: HTMLElement) => { - props.ref?.(node); - } + [createAttachmentKey()]: (node: HTMLElement) => { + props.ref(node); } }; }, getSearchInputElementProps({ prev }, ...rest) { const { onBlur, onChange, ...props } = prev?.() ?? {}; - const extendedOnChange = (e: Event) => { - onChange?.(e); - }; return { ...props, ...rest, + [createAttachmentKey()]: (node: HTMLElement) => { props.ref?.(node); }, + onblur: onBlur ? (e: FocusEvent) => onBlur(e) : undefined, - oninput: onChange ? (e: Event) => extendedOnChange(e) : undefined + // Couldn't get onChange to work, so we're using oninput instead + oninput: onChange ? (e: Event) => onChange(e) : undefined }; } } }; +// Create a reactive proxy for ItemInstance +type ReactiveItemInstance = ItemInstance & { + [K in keyof ItemInstance]: ItemInstance[K] extends (...args: never[]) => unknown + ? (...args: Parameters[K]>) => ReturnType[K]> + : ItemInstance[K]; +}; + export class ReactiveTree { - #tree: TreeInstance; + #tree = $state.raw>()!; #subscribe: () => void; - #currentState: TreeState; - #itemProxies = new Map>(); - #treeProxy: null | TreeInstance = null; + #itemProxies = new WeakMap, ReactiveItemInstance>(); constructor(config: TreeConfig) { const configWithFeatures: TreeConfig = { ...config, features: [...(config.features ?? []), svelteFeatures] }; - this.#tree = createTree(configWithFeatures); - this.#currentState = this.#tree.getState(); + this.#tree = createTree(configWithFeatures); this.#subscribe = createSubscriber((update) => { - this.#tree.setConfig((prev) => ({ - ...prev, - ...config, - features: [...(config.features ?? []), svelteFeatures], - - setState: (newState) => { - this.#currentState = { - ...this.#currentState, - ...newState - }; - config.setState?.(this.#currentState); - // Clear item proxies cache when state changes to ensure fresh reactive state - this.#itemProxies.clear(); - // Trigger reactive updates when tree state changes + // Store the original methods + const originalSetState = this.#tree.setState; + const originalApplySubStateUpdate = this.#tree.applySubStateUpdate; + const originalGetItems = this.#tree.getItems; + + this.#tree.setState = (state) => { + originalSetState.call(this.#tree, state); + tick().then(() => { update(); - }, - state: { - ...this.#currentState, - ...config.state - } - })); + }); + }; - // Do we need a cleanup? - return () => {}; + // Override applySubStateUpdate to handle for example selection and drag-and-drop changes + this.#tree.applySubStateUpdate = (( + ...args: Parameters + ) => { + originalApplySubStateUpdate.apply(this.#tree, args); + tick().then(() => { + update(); + }); + }) as typeof originalApplySubStateUpdate; + + this.#tree.getItems = (...args: Parameters) => { + const items = originalGetItems.apply(this.#tree, args); + return items.map((item) => this.#createReactiveItemProxy(item)); + }; + return () => { + this.#tree.setState = originalSetState; + this.#tree.applySubStateUpdate = originalApplySubStateUpdate; + this.#tree.getItems = originalGetItems; + }; }); + } - // Create the tree proxy once - this.#treeProxy = this.#createTreeProxy(); + get current(): TreeInstance { + this.#subscribe(); + return this.#tree; } - #createItemProxy(item: ItemInstance): ItemInstance { + // Create a reactive proxy for an item instance + #createReactiveItemProxy(item: ItemInstance): ReactiveItemInstance { + if (this.#itemProxies.has(item)) { + return this.#itemProxies.get(item)!; + } + const proxy = new Proxy(item, { - get: (target, prop) => { - // Make these methods reactive by subscribing when they're called - if (typeof prop === 'string' && (prop.startsWith('is') || prop.startsWith('get'))) { + get: (target, prop: string | symbol) => { + const value = target[prop as keyof ItemInstance]; + + // If it's a function, check if it should be reactive + // !TODO: this is a hack to make all methods reactive, we should only make the methods that are needed reactive + if (typeof value === 'function' && typeof prop === 'string') { return (...args: unknown[]) => { - this.#subscribe(); // Subscribe to reactive updates - const method = target[prop as keyof ItemInstance] as (...args: unknown[]) => unknown; - return method?.apply(target, args); + // Make the call reactive + this.#subscribe(); + return (value as (...args: unknown[]) => unknown).apply(target, args); }; } - return target[prop as keyof ItemInstance]; + // For non-reactive methods and properties, return as-is + return value; } - }); + }) as ReactiveItemInstance; + // Cache the proxy + this.#itemProxies.set(item, proxy); return proxy; } - #createTreeProxy(): TreeInstance { - return new Proxy(this.#tree, { - get: (target, prop) => { - // Make getItems return proxied items - if (prop === 'getItems') { - return () => { - this.#subscribe(); // Subscribe to reactive updates - const items = target.getItems(); - return items.map((item) => { - const itemId = item.getId(); - if (!this.#itemProxies.has(itemId)) { - this.#itemProxies.set(itemId, this.#createItemProxy(item)); - } - return this.#itemProxies.get(itemId)!; - }); - }; - } - - // Make other tree methods reactive - if (typeof prop === 'string' && prop.startsWith('get')) { - return (...args: unknown[]) => { - this.#subscribe(); // Subscribe to reactive updates - const method = target[prop as keyof TreeInstance] as (...args: unknown[]) => unknown; - return method?.apply(target, args); - }; - } - - return target[prop as keyof TreeInstance]; - } - }); - } - - get current(): TreeInstance { + // Simple reactive helper - call this with any item method to make it reactive + /** + * @param fn - The function to make reactive + * @returns The result of the function + * @example + * ```ts + * const isFolder = tree.reactive(() => item.isFolder()); + * ``` + */ + reactive(fn: () => R): R { this.#subscribe(); - return this.#treeProxy!; + return fn(); } } export const useTree = (config: TreeConfig) => new ReactiveTree(config); + +// Export the reactive item instance type for use in components +export type { ReactiveItemInstance }; From c7ca3879983b11a03f2b7f7b68c39e056cd5170e Mon Sep 17 00:00:00 2001 From: "Max G." Date: Sat, 31 May 2025 16:08:07 +0200 Subject: [PATCH 4/6] refactor: update tree component to use reactive types and improve context handling --- .../components/ui/tree/tree-context.svelte.ts | 9 ++- .../components/ui/tree/tree-drag-line.svelte | 4 +- src/lib/components/ui/tree/tree-item.svelte | 68 +++++++++---------- src/lib/components/ui/tree/tree-label.svelte | 13 ++-- src/lib/components/ui/tree/tree.svelte | 47 ++++++------- 5 files changed, 66 insertions(+), 75 deletions(-) diff --git a/src/lib/components/ui/tree/tree-context.svelte.ts b/src/lib/components/ui/tree/tree-context.svelte.ts index 8470c697..c846f5e0 100644 --- a/src/lib/components/ui/tree/tree-context.svelte.ts +++ b/src/lib/components/ui/tree/tree-context.svelte.ts @@ -1,13 +1,12 @@ -import type { ItemInstance, TreeInstance } from '@headless-tree/core'; - import { Context } from 'runed'; +import type { ReactiveItemInstance, ReactiveTree } from './use-tree.svelte'; + // eslint-disable-next-line @typescript-eslint/no-explicit-any export interface TreeContextValue { - currentItem?: ItemInstance; + currentItem?: ReactiveItemInstance; indent: number; - // eslint-disable-next-line @typescript-eslint/no-explicit-any - tree?: TreeInstance; + tree?: ReactiveTree; } export const treeContext = new Context('tree:context'); diff --git a/src/lib/components/ui/tree/tree-drag-line.svelte b/src/lib/components/ui/tree/tree-drag-line.svelte index b398daa1..b52397d7 100644 --- a/src/lib/components/ui/tree/tree-drag-line.svelte +++ b/src/lib/components/ui/tree/tree-drag-line.svelte @@ -17,14 +17,14 @@ const ctx = useTreeContext(); - if (!ctx.tree || typeof ctx.tree.getDragLineStyle !== 'function') { + if (!ctx.tree || typeof ctx.tree.current.getDragLineStyle !== 'function') { console.warn( 'TreeDragLine: No tree provided via context or tree does not have getDragLineStyle method' ); } const dragLine = $derived.by(() => - Object.entries(ctx.tree?.getDragLineStyle() ?? {}) + Object.entries(ctx.tree?.reactive(() => ctx.tree?.current.getDragLineStyle()) ?? {}) .map(([key, value]) => `${key}: ${value}`) .join('; ') ); diff --git a/src/lib/components/ui/tree/tree-item.svelte b/src/lib/components/ui/tree/tree-item.svelte index f9c2d268..d96cc597 100644 --- a/src/lib/components/ui/tree/tree-item.svelte +++ b/src/lib/components/ui/tree/tree-item.svelte @@ -1,6 +1,5 @@ {#if child} {@render child({ props: mergedProps })} {:else} - {/if} diff --git a/src/lib/components/ui/tree/tree-label.svelte b/src/lib/components/ui/tree/tree-label.svelte index 46302de6..c3712697 100644 --- a/src/lib/components/ui/tree/tree-label.svelte +++ b/src/lib/components/ui/tree/tree-label.svelte @@ -1,12 +1,14 @@ - -
- {@render children?.()} -
-
+
+ {@render children?.()} +
From 13d749f9a0ee843c56a0f40db0c8a35a0e23d873 Mon Sep 17 00:00:00 2001 From: "Max G." Date: Sat, 31 May 2025 16:08:18 +0200 Subject: [PATCH 5/6] refactor: enhance tree components --- src/lib/components/trees/tree-01.svelte | 48 ++++++++++---- src/lib/components/trees/tree-02.svelte | 22 ++++++- src/lib/components/trees/tree-03.svelte | 22 ++++++- src/lib/components/trees/tree-04.svelte | 22 ++++++- src/lib/components/trees/tree-05.svelte | 87 +++++++++++++++---------- src/lib/components/trees/tree-06.svelte | 64 ++++++++++++------ src/lib/components/trees/tree-07.svelte | 22 ++++++- src/lib/components/trees/tree-08.svelte | 13 +++- 8 files changed, 226 insertions(+), 74 deletions(-) diff --git a/src/lib/components/trees/tree-01.svelte b/src/lib/components/trees/tree-01.svelte index 42586492..3fa8ccb2 100644 --- a/src/lib/components/trees/tree-01.svelte +++ b/src/lib/components/trees/tree-01.svelte @@ -56,16 +56,38 @@ }); - - {#each tree.current.getItems() as item (item.getId())} - - - {item.getItemData().name} - - - {/each} - +
+ + {#each tree.current.getItems() as item (item.getId())} + + + {item.getItemData().name} + + + {/each} + +

+ Basic tree with no extra features ∙ + + Headless Tree + + ∙ + + Svelte Integration + +

+
diff --git a/src/lib/components/trees/tree-02.svelte b/src/lib/components/trees/tree-02.svelte index 9904987c..260b5d4b 100644 --- a/src/lib/components/trees/tree-02.svelte +++ b/src/lib/components/trees/tree-02.svelte @@ -59,7 +59,7 @@ {#each tree.current.getItems() as item (item.getId())} @@ -69,5 +69,25 @@ {/each} +

+ Basic tree with vertical lines ∙ + + Headless Tree + + ∙ + + Svelte Integration + +

diff --git a/src/lib/components/trees/tree-03.svelte b/src/lib/components/trees/tree-03.svelte index 0d36e960..41088b77 100644 --- a/src/lib/components/trees/tree-03.svelte +++ b/src/lib/components/trees/tree-03.svelte @@ -62,7 +62,7 @@ {#each tree.current.getItems() as item (item.getId())} @@ -87,4 +87,24 @@ {/each} +

+ Basic tree with icons ∙ + + Headless Tree + + ∙ + + Svelte Integration + +

diff --git a/src/lib/components/trees/tree-04.svelte b/src/lib/components/trees/tree-04.svelte index 9c15b940..9b99edaa 100644 --- a/src/lib/components/trees/tree-04.svelte +++ b/src/lib/components/trees/tree-04.svelte @@ -62,7 +62,7 @@ {#each tree.current.getItems() as item (item.getId())} @@ -87,4 +87,24 @@ {/each} +

+ Basic tree with caret icon on the right ∙ + + Headless Tree + + ∙ + + Svelte Integration + +

diff --git a/src/lib/components/trees/tree-05.svelte b/src/lib/components/trees/tree-05.svelte index b3f803fb..6eb9449a 100644 --- a/src/lib/components/trees/tree-05.svelte +++ b/src/lib/components/trees/tree-05.svelte @@ -54,44 +54,43 @@ const indent = 20; - const tree = $derived( - useTree({ - canReorder: true, - dataLoader: { - getChildren: (itemId) => items[itemId].children ?? [], - getItem: (itemId) => items[itemId] - }, - features: [ - syncDataLoaderFeature, - selectionFeature, - hotkeysCoreFeature, - dragAndDropFeature, - keyboardDragAndDropFeature - ], - getItemName: (item) => item.getItemData().name, - indent, - initialState: { - expandedItems: ['engineering', 'frontend', 'design-system'], - selectedItems: ['components'] - }, - isItemFolder: (item) => (item.getItemData()?.children?.length ?? 0) > 0, - onDrop: createOnDropHandler((parentItem, newChildrenIds) => { - items = { - ...items, - [parentItem.getId()]: { - ...items[parentItem.getId()], - children: newChildrenIds - } - }; - }), - rootItemId: 'company' - }) - ); + const tree = useTree({ + canReorder: true, + dataLoader: { + getChildren: (itemId) => items[itemId].children ?? [], + getItem: (itemId) => items[itemId] + }, + features: [ + syncDataLoaderFeature, + selectionFeature, + hotkeysCoreFeature, + dragAndDropFeature, + keyboardDragAndDropFeature + ], + getItemName: (item) => item.getItemData().name, + indent, + initialState: { + expandedItems: ['engineering', 'frontend', 'design-system'], + selectedItems: ['components'] + }, + isItemFolder: (item) => (item.getItemData()?.children?.length ?? 0) > 0, + onDrop: createOnDropHandler((parentItem, newChildrenIds) => { + items = { + ...items, + [parentItem.getId()]: { + ...items[parentItem.getId()], + children: newChildrenIds + } + }; + }), + + rootItemId: 'company' + });
- + {#each tree.current.getItems() as item (item.getId())} @@ -113,4 +112,24 @@
+

+ Tree with multi-select and drag and drop ∙ + + Headless Tree + + ∙ + + Svelte Integration + +

diff --git a/src/lib/components/trees/tree-06.svelte b/src/lib/components/trees/tree-06.svelte index 05f4f793..a318a4f0 100644 --- a/src/lib/components/trees/tree-06.svelte +++ b/src/lib/components/trees/tree-06.svelte @@ -71,33 +71,55 @@
- + {#each tree.current.getItems() as item (item.getId())} - - - {#if item.isFolder()} - {#if item.isExpanded()} - - {:else} - + {#snippet child({ props })} + + + {#if item.isFolder()} + {#if item.isExpanded()} + + {:else} + + {/if} {/if} - {/if} - {#if item.isRenaming()} - {@const { attacher, ...rest } = item.getRenameInputProps()} - - {:else} - {item.getItemName()} - {/if} - - + {#if tree.reactive(() => item.isRenaming())} + {@const { attacher, ...rest } = item.getRenameInputProps()} + + {:else} + {item.getItemName()} + {/if} + + + {/snippet} {/each}
+

+ Tree with renaming (press F2 to rename) ∙ + + Headless Tree + + ∙ + + Svelte Integration + +

diff --git a/src/lib/components/trees/tree-07.svelte b/src/lib/components/trees/tree-07.svelte index e234224e..e1c49248 100644 --- a/src/lib/components/trees/tree-07.svelte +++ b/src/lib/components/trees/tree-07.svelte @@ -130,7 +130,7 @@
- + {#each tree.current.getItems() as item (item.getId())} @@ -150,4 +150,24 @@ {/each}
+

+ Tree with search highlight ∙ + + Headless Tree + + ∙ + + Svelte Integration + +

diff --git a/src/lib/components/trees/tree-08.svelte b/src/lib/components/trees/tree-08.svelte index 7b59c9aa..c094a6e7 100644 --- a/src/lib/components/trees/tree-08.svelte +++ b/src/lib/components/trees/tree-08.svelte @@ -270,7 +270,7 @@
- + {#if searchValue && filteredItems.length === 0}

No items found for "{searchValue}"

{:else} @@ -308,7 +308,16 @@ target="_blank" rel="noopener noreferrer" > - API + Headless Tree + + ∙ + + Svelte Integration

From 5cf80c248037c36255bce519e43c0b92681e85e3 Mon Sep 17 00:00:00 2001 From: "Max G." Date: Sat, 31 May 2025 16:08:28 +0200 Subject: [PATCH 6/6] chore: add svelte-toolbelt dependency --- package.json | 1 + pnpm-lock.yaml | 16 ++++++++++++++++ 2 files changed, 17 insertions(+) diff --git a/package.json b/package.json index ea0099a6..3e0cc09c 100644 --- a/package.json +++ b/package.json @@ -34,6 +34,7 @@ "runed": "^0.28.0", "svelte-sonner": "^0.3.28", "svelte-tel-input": "^3.6.0", + "svelte-toolbelt": "^0.9.1", "tailwind-merge": "^2.5.4", "tailwind-variants": "^0.2.1", "vaul-svelte": "1.0.0-next.2", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 98f1a181..98e714b0 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -62,6 +62,9 @@ importers: svelte-tel-input: specifier: ^3.6.0 version: 3.6.0(svelte@5.30.2) + svelte-toolbelt: + specifier: ^0.9.1 + version: 0.9.1(svelte@5.30.2) tailwind-merge: specifier: ^2.5.4 version: 2.6.0 @@ -2779,6 +2782,12 @@ packages: peerDependencies: svelte: ^5.0.0 + svelte-toolbelt@0.9.1: + resolution: {integrity: sha512-wBX6MtYw/kpht80j5zLpxJyR9soLizXPIAIWEVd9llAi17SR44ZdG291bldjB7r/K5duC0opDFcuhk2cA1hb8g==} + engines: {node: '>=18', pnpm: '>=8.7.0'} + peerDependencies: + svelte: ^5.30.2 + svelte@5.30.2: resolution: {integrity: sha512-zfGFEwwPeILToOxOqQyFq/vc8euXrX2XyoffkBNgn/k8D1nxbLt5+mNaqQBmZF/vVhBGmkY6VmNK18p9Gf0auQ==} engines: {node: '>=18'} @@ -5613,6 +5622,13 @@ snapshots: style-to-object: 1.0.8 svelte: 5.30.2 + svelte-toolbelt@0.9.1(svelte@5.30.2): + dependencies: + clsx: 2.1.1 + runed: 0.28.0(svelte@5.30.2) + style-to-object: 1.0.8 + svelte: 5.30.2 + svelte@5.30.2: dependencies: '@ampproject/remapping': 2.3.0