diff --git a/packages/components/src/components/Tree/Tree.css b/packages/components/src/components/Tree/Tree.css index cfcd23c87..b324d56a7 100644 --- a/packages/components/src/components/Tree/Tree.css +++ b/packages/components/src/components/Tree/Tree.css @@ -49,6 +49,13 @@ } } +.kbq-TreeItem:has([data-slot='list-item-addon']) { + [data-slot='chevron'], + [data-slot='checkbox'] { + margin-inline-end: var(--kbq-size-s); + } +} + .kbq-TreeLoader { display: flex; align-items: center; diff --git a/packages/components/src/components/Tree/Tree.mdx b/packages/components/src/components/Tree/Tree.mdx index 36f8b357d..37936779e 100644 --- a/packages/components/src/components/Tree/Tree.mdx +++ b/packages/components/src/components/Tree/Tree.mdx @@ -37,6 +37,8 @@ Use these components to build hierarchical navigation, selection, and lazy loadi - `Tree` — Root container for hierarchical items. - `Tree.Item` — Defines a node in the tree (leaf or branch). - `Tree.ItemContent` — Customizes row content (text, icons, slots). +- `Tree.ItemContentText` — Displays text and an optional caption in an item. +- `Tree.ItemContentAddon` — Displays an icon, badge, or other secondary content in an item. - `Tree.LoadMoreItem` — Triggers and displays async loading state for additional items. ## Content @@ -53,12 +55,23 @@ Use `selectionBehavior="toggle"` to render checkbox selection controls. -## Slots +## Item content -Use `Tree.ItemContent` to customize item layout. -For example, you can add icons, typography, badges, and slot-specific props for chevron and checkbox controls. +The `Tree.ItemContent` can be composed with helper components: - +- `Tree.ItemContentText` for text and captions. +- `Tree.ItemContentAddon` for icons, badges, or other secondary content. + +The `Tree.Item` also has layout props, such as `align`, which sets vertical alignment. +Use `align="start"` for captions or multi-line content. + + + +## Item actions + +Add an extra action to an item, revealed on hover or focus. + + ## Empty state @@ -92,7 +105,3 @@ Tree supports progressive loading with `Tree.LoadMoreItem`. Combine it with `useAsyncList` to fetch nested data on demand. - -## Other examples - - diff --git a/packages/components/src/components/Tree/Tree.stories.tsx b/packages/components/src/components/Tree/Tree.stories.tsx index 03aa1109e..e08bc83df 100644 --- a/packages/components/src/components/Tree/Tree.stories.tsx +++ b/packages/components/src/components/Tree/Tree.stories.tsx @@ -1,11 +1,16 @@ import { useState } from 'react'; -import { IconEllipsisVertical16, IconFolder16 } from '@koobiq/react-icons'; +import { + IconCircle16, + IconEllipsisVertical16, + IconFolder16, +} from '@koobiq/react-icons'; import { Collection } from '@koobiq/react-primitives'; import type { Meta, StoryObj } from '@storybook/react'; -import { FlexBox, useAsyncList } from '../../index'; +import { useAsyncList } from '../../index'; import type { Selection } from '../../index'; +import { Badge } from '../Badge'; import { IconButton } from '../IconButton'; import { spacing } from '../layout'; import { Menu } from '../Menu'; @@ -22,10 +27,12 @@ const meta = { subcomponents: { 'Tree.Item': Tree.Item, 'Tree.ItemContent': Tree.ItemContent, + 'Tree.ItemContentText': Tree.ItemContentText, + 'Tree.ItemContentAddon': Tree.ItemContentAddon, 'Tree.LoadMoreItem': Tree.LoadMoreItem, }, argTypes: {}, - tags: ['status:new', 'date:2026-03-02'], + tags: ['status:updated', 'date:2026-07-03'], } satisfies Meta; export default meta; @@ -222,7 +229,95 @@ export const Content: Story = { }, }; -export const Slots: Story = { +export const ItemContent: Story = { + name: 'Item content', + parameters: { + layout: 'padded', + }, + render: function Render() { + const longText = + 'Lorem ipsum dolor sit amet, consectetur adipisicing elit. A at cupiditate dolor itaque molestias quasi quisquam quo. Deleniti ducimus fugit nulla repudiandae tenetur. Aliquid autem corporis culpa debitis exercitationem inventore labore nihil officia recusandae veniam. Beatae, doloribus, suscipit. Aut beatae consectetur consequuntur cum hic, obcaecati quia sunt temporibus unde vel!'; + + return ( + + + + + + + app + + + + + index.html + + + + + + + + + + + .gitignore + + + Badge + + + + + + + + + + README.md + + + + + + + + + + + {longText} + + + + + + + + + + {longText} + + + Badge + + + + + ); + }, +}; + +export const ItemActions: Story = { + name: 'Item actions', parameters: { layout: 'padded', }, @@ -240,22 +335,28 @@ export const Slots: Story = { {({ isHovered, isFocusVisibleWithin }) => ( <> - {type === 'directory' && } - {title} + {type === 'directory' && ( + + + + )} + {title} {(isHovered || isFocusVisibleWithin || isMenuOpen) && ( ( - - - + + + + + )} > Edit @@ -547,107 +648,3 @@ export const AsyncLoading: Story = { ); }, }; - -export const Examples: Story = { - parameters: { - layout: 'padded', - }, - render: (args) => ( - - - app - - Http - - index.html - - - - Providers - - EventServiceProvider.js - - - - - config - - app.js - - - database.js - - - - - - Lorem ipsum dolor sit amet, consectetur adipisicing elit. Accusamus - asperiores delectus doloremque fugiat illo laudantium nesciunt - omnis. Aliquam, earum, velit? - - - - - - Lorem ipsum dolor sit amet, consectetur adipisicing elit. - Accusamus asperiores delectus doloremque fugiat illo laudantium - nesciunt omnis. Aliquam, earum, velit? - - - - - - .env - - - .gitignore - - - README.md - - - - - - Lorem ipsum dolor sit amet, consectetur adipisicing elit. - Accusamus asperiores delectus doloremque fugiat illo laudantium - nesciunt omnis. Aliquam, earum, velit? - - - Lorem ipsum dolor sit amet, consectetur adipisicing elit. - Accusamus asperiores delectus doloremque fugiat illo laudantium - nesciunt omnis. Aliquam, earum, velit? - - - - - - ), -}; diff --git a/packages/components/src/components/Tree/Tree.tsx b/packages/components/src/components/Tree/Tree.tsx index 4bb946ec0..d9ea06162 100644 --- a/packages/components/src/components/Tree/Tree.tsx +++ b/packages/components/src/components/Tree/Tree.tsx @@ -1,21 +1,17 @@ 'use client'; import { clsx } from '@koobiq/react-core'; -import type { TreeProps as AriaTreeProps } from '@koobiq/react-primitives'; import { Tree as AriaTree, composeRenderProps } from '@koobiq/react-primitives'; import './Tree.css'; import { utilClasses } from '../../styles/utility'; +import { ListItemAddon, ListItemText } from '../List/components'; import { TreeItem, TreeItemContent, TreeLoadMoreItem } from './components'; +import type { TreeProps } from './types'; const { list } = utilClasses; -export type TreeProps = AriaTreeProps & { - /** Whether the tree has outer padding. */ - isPadded?: boolean; -}; - /** * A tree provides users with a way to navigate nested hierarchical * information, with support for keyboard navigation and selection. @@ -41,6 +37,8 @@ TreeComponent.displayName = 'Tree'; type CompoundedComponent = typeof TreeComponent & { Item: typeof TreeItem; ItemContent: typeof TreeItemContent; + ItemContentText: typeof ListItemText; + ItemContentAddon: typeof ListItemAddon; LoadMoreItem: typeof TreeLoadMoreItem; }; @@ -52,4 +50,6 @@ export const Tree = TreeComponent as CompoundedComponent; TreeComponent.Item = TreeItem; TreeComponent.ItemContent = TreeItemContent; +TreeComponent.ItemContentText = ListItemText; +TreeComponent.ItemContentAddon = ListItemAddon; TreeComponent.LoadMoreItem = TreeLoadMoreItem; diff --git a/packages/components/src/components/Tree/components/TreeItem/TreeItem.tsx b/packages/components/src/components/Tree/components/TreeItem/TreeItem.tsx index a3aab7bea..09ea27518 100644 --- a/packages/components/src/components/Tree/components/TreeItem/TreeItem.tsx +++ b/packages/components/src/components/Tree/components/TreeItem/TreeItem.tsx @@ -17,10 +17,12 @@ export function TreeItem({ children, className, textValue, + align = 'center', ...props }: TreeItemProps) { return ( diff --git a/packages/components/src/components/Tree/components/TreeItem/types.ts b/packages/components/src/components/Tree/components/TreeItem/types.ts index 132ebcc8e..5b512e8b3 100644 --- a/packages/components/src/components/Tree/components/TreeItem/types.ts +++ b/packages/components/src/components/Tree/components/TreeItem/types.ts @@ -1,4 +1,15 @@ import type { DataAttributeProps } from '@koobiq/react-core'; import type { TreeItemProps as AriaTreeItemProps } from '@koobiq/react-primitives'; -export type TreeItemProps = Partial & DataAttributeProps; +export const treeItemPropAlign = ['start', 'center'] as const; + +export type TreeItemPropAlign = (typeof treeItemPropAlign)[number]; + +export type TreeItemProps = Partial & + DataAttributeProps & { + /** + * Vertical alignment of the item content. + * @default 'center' + */ + align?: TreeItemPropAlign; + }; diff --git a/packages/components/src/components/Tree/components/TreeItemContent/TreeItemContent.tsx b/packages/components/src/components/Tree/components/TreeItemContent/TreeItemContent.tsx index 88c5ccd73..2e4c45286 100644 --- a/packages/components/src/components/Tree/components/TreeItemContent/TreeItemContent.tsx +++ b/packages/components/src/components/Tree/components/TreeItemContent/TreeItemContent.tsx @@ -39,7 +39,11 @@ export function TreeItemContent(props: TreeItemContentProps) { /> {selectionBehavior === 'toggle' && selectionMode === 'multiple' && ( - + )} {typeof children === 'function' ? children(renderProps) : children} diff --git a/packages/components/src/components/Tree/index.ts b/packages/components/src/components/Tree/index.ts index 304332966..814853ee5 100644 --- a/packages/components/src/components/Tree/index.ts +++ b/packages/components/src/components/Tree/index.ts @@ -1,2 +1,3 @@ export * from './Tree'; export * from './components'; +export * from './types'; diff --git a/packages/components/src/components/Tree/types.ts b/packages/components/src/components/Tree/types.ts new file mode 100644 index 000000000..2d65b59c6 --- /dev/null +++ b/packages/components/src/components/Tree/types.ts @@ -0,0 +1,11 @@ +import type { TreeProps as AriaTreeProps } from '@koobiq/react-primitives'; + +import type { ListItemAddonProps, ListItemTextProps } from '../List/components'; + +export type TreeItemContentText = ListItemTextProps; +export type TreeItemContentAddon = ListItemAddonProps; + +export type TreeProps = AriaTreeProps & { + /** Whether the tree has outer padding. */ + isPadded?: boolean; +}; diff --git a/tools/public_api_guard/components/Tree.api.md b/tools/public_api_guard/components/Tree.api.md index 25b078905..b68abca23 100644 --- a/tools/public_api_guard/components/Tree.api.md +++ b/tools/public_api_guard/components/Tree.api.md @@ -10,10 +10,13 @@ import type { ComponentPropsWithRef } from 'react'; import type { CSSProperties } from 'react'; import type { DataAttributeProps } from '@koobiq/react-core'; import type { ElementType } from 'react'; +import type { ExtendableComponentPropsWithRef } from '@koobiq/react-core'; import type { ExtendableProps } from '@koobiq/react-core'; +import { ForwardRefExoticComponent } from 'react'; import { JSX } from 'react/jsx-runtime'; import { PolyForwardComponent } from '@koobiq/react-core'; import type { ReactNode } from 'react'; +import { RefAttributes } from 'react'; import type { TreeItemContentProps as TreeItemContentProps_2 } from '@koobiq/react-primitives'; import type { TreeItemProps as TreeItemProps_2 } from '@koobiq/react-primitives'; import { TreeLoadMoreItemProps as TreeLoadMoreItemProps_2 } from '@koobiq/react-primitives'; @@ -35,6 +38,14 @@ export namespace TreeComponent { Item: typeof TreeItem; var // (undocumented) ItemContent: typeof TreeItemContent; + var // Warning: (ae-forgotten-export) The symbol "ListItemTextProps" needs to be exported by the entry point index.d.ts + // + // (undocumented) + ItemContentText: ForwardRefExoticComponent & RefAttributes>; + var // Warning: (ae-forgotten-export) The symbol "ListItemAddonProps" needs to be exported by the entry point index.d.ts + // + // (undocumented) + ItemContentAddon: ForwardRefExoticComponent & RefAttributes>; var // (undocumented) LoadMoreItem: typeof TreeLoadMoreItem; } @@ -45,6 +56,9 @@ export function TreeItem(input: TreeItemProps): JSX.Element; // @public (undocumented) export function TreeItemContent(props: TreeItemContentProps): JSX.Element; +// @public (undocumented) +export type TreeItemContentAddon = ListItemAddonProps; + // @public (undocumented) export type TreeItemContentProps = TreeItemContentProps_2 & { slotProps?: TreeItemContentPropSlotProps; @@ -57,7 +71,18 @@ export type TreeItemContentPropSlotProps = { }; // @public (undocumented) -export type TreeItemProps = Partial & DataAttributeProps; +export type TreeItemContentText = ListItemTextProps; + +// @public (undocumented) +export type TreeItemPropAlign = (typeof treeItemPropAlign)[number]; + +// @public (undocumented) +export const treeItemPropAlign: readonly ["start", "center"]; + +// @public (undocumented) +export type TreeItemProps = Partial & DataAttributeProps & { + align?: TreeItemPropAlign; +}; // @public (undocumented) export function TreeLoadMoreItem(props: TreeLoadMoreItemProps): JSX.Element;