@pindoba/astro-tree-view 0.0.0-alpha-20250910225440

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,12 @@
1
+ Copyright 2025 Eduardo Lopes
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
4
+
5
+ The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
6
+
7
+ THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
8
+ INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
9
+ PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE
10
+ FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
11
+ OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
12
+ DEALINGS IN THE SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,136 @@
1
+ # Tree View Component
2
+
3
+ A flexible and accessible tree view component for Astro applications.
4
+
5
+ ## Features
6
+
7
+ - ✅ Collapsible/expandable tree nodes
8
+ - ✅ Selectable items with keyboard navigation
9
+ - ✅ Customizable styling with variants and sizes
10
+ - ✅ Accessible with proper ARIA attributes
11
+ - ✅ TypeScript support
12
+ - ✅ Custom icons support
13
+ - ✅ Event handling for selection
14
+
15
+ ## Installation
16
+
17
+ ```bash
18
+ npm install @pindoba/ui-astro-tree-view @pindoba/styles-tree-view
19
+ ```
20
+
21
+ ## Basic Usage
22
+
23
+ ```astro
24
+ ---
25
+ import { TreeView, TreeViewItem } from "@pindoba/ui-astro-tree-view";
26
+ ---
27
+
28
+ <TreeView>
29
+ <TreeViewItem label="Giant planets" collapsible defaultOpen>
30
+ <TreeViewItem slot="children" label="Gas giants" collapsible>
31
+ <TreeViewItem slot="children" label="Jupiter" />
32
+ <TreeViewItem slot="children" label="Saturn" />
33
+ </TreeViewItem>
34
+ <TreeViewItem slot="children" label="Ice giants" collapsible>
35
+ <TreeViewItem slot="children" label="Uranus" />
36
+ <TreeViewItem slot="children" label="Neptune" />
37
+ </TreeViewItem>
38
+ </TreeViewItem>
39
+ </TreeView>
40
+ ```
41
+
42
+ ## Props
43
+
44
+ ### TreeView
45
+
46
+ | Prop | Type | Default | Description |
47
+ | ------------- | ----------------------------------- | ----------- | ------------------------ |
48
+ | `size` | `"sm" \| "md" \| "lg"` | `"md"` | Size variant of the tree |
49
+ | `variant` | `"default" \| "subtle" \| "ghost"` | `"default"` | Visual variant |
50
+ | `passThrough` | `Record<string, SystemStyleObject>` | - | Custom styles for slots |
51
+
52
+ ### TreeViewItem
53
+
54
+ | Prop | Type | Default | Description |
55
+ | ------------- | ----------------------------------- | ----------- | -------------------------------------------------------- |
56
+ | `label` | `string` | - | The text content for this tree item |
57
+ | `collapsible` | `boolean` | `false` | Whether this item has children and should be collapsible |
58
+ | `defaultOpen` | `boolean` | `false` | Whether the item is expanded by default |
59
+ | `selectable` | `boolean` | `false` | Whether this item is selectable |
60
+ | `selected` | `boolean` | `false` | Whether this item is currently selected |
61
+ | `icon` | `string` | - | Icon HTML to display before the label |
62
+ | `size` | `"sm" \| "md" \| "lg"` | `"md"` | Size variant |
63
+ | `variant` | `"default" \| "subtle" \| "ghost"` | `"default"` | Visual variant |
64
+ | `passThrough` | `Record<string, SystemStyleObject>` | - | Custom styles for slots |
65
+
66
+ ## Examples
67
+
68
+ ### With Selection
69
+
70
+ ```astro
71
+ <TreeView>
72
+ <TreeViewItem label="Documents" collapsible defaultOpen selectable>
73
+ <TreeViewItem slot="children" label="resume.pdf" selectable selected />
74
+ <TreeViewItem slot="children" label="cover-letter.pdf" selectable />
75
+ </TreeViewItem>
76
+ </TreeView>
77
+ ```
78
+
79
+ ### With Icons
80
+
81
+ ```astro
82
+ <TreeView>
83
+ <TreeViewItem label="Folder" collapsible icon="<svg>...</svg>">
84
+ <TreeViewItem slot="children" label="file.txt" icon="<svg>...</svg>" />
85
+ </TreeViewItem>
86
+ </TreeView>
87
+ ```
88
+
89
+ ### Different Sizes and Variants
90
+
91
+ ```astro
92
+ <!-- Small subtle tree -->
93
+ <TreeView size="sm" variant="subtle">
94
+ <TreeViewItem label="Small item" />
95
+ </TreeView>
96
+
97
+ <!-- Large ghost tree -->
98
+ <TreeView size="lg" variant="ghost">
99
+ <TreeViewItem label="Large item" />
100
+ </TreeView>
101
+ ```
102
+
103
+ ## Events
104
+
105
+ ### tree-item-select
106
+
107
+ Fired when a selectable tree item is clicked or activated with keyboard.
108
+
109
+ ```javascript
110
+ document.addEventListener("tree-item-select", (event) => {
111
+ console.log("Selected item:", event.detail.label);
112
+ console.log("Item element:", event.detail.element);
113
+ });
114
+ ```
115
+
116
+ ## Accessibility
117
+
118
+ - Keyboard navigation with Enter and Space for selection
119
+ - Proper focus management with visible focus indicators
120
+ - ARIA attributes for screen readers
121
+ - Semantic HTML with `details`/`summary` for collapsible items
122
+
123
+ ## Styling
124
+
125
+ The component uses CSS-in-JS with Panda CSS. You can customize styles using the `passThrough` prop:
126
+
127
+ ```astro
128
+ <TreeView
129
+ passThrough={{
130
+ root: { backgroundColor: "gray.50" },
131
+ item: { borderRadius: "lg" },
132
+ }}
133
+ >
134
+ <!-- ... -->
135
+ </TreeView>
136
+ ```
package/index.ts ADDED
@@ -0,0 +1,11 @@
1
+ // Do not write code directly here, instead use the `src` folder!
2
+ // Then, use this file to export everything you want your user to access.
3
+
4
+ import TreeView from "./src/TreeView.astro";
5
+ import TreeViewItem from "./src/TreeViewItem.astro";
6
+
7
+ export * from "./src/TreeView.astro";
8
+ export * from "./src/TreeViewItem.astro";
9
+ export * from "./src/tree-utils";
10
+
11
+ export { TreeView, TreeViewItem };
package/package.json ADDED
@@ -0,0 +1,49 @@
1
+ {
2
+ "name": "@pindoba/astro-tree-view",
3
+ "version": "0.0.0-alpha-20250910225440",
4
+ "homepage": "https://github.com/EduardoLopes/pindoba/tree/main/packages/ui/astro/tree-view#README.md",
5
+ "repository": {
6
+ "type": "git",
7
+ "url": "git+https://github.com/EduardoLopes/pindoba.git",
8
+ "directory": "packages/ui/astro/tree-view"
9
+ },
10
+ "sideEffects": false,
11
+ "license": "MIT",
12
+ "type": "module",
13
+ "exports": {
14
+ ".": "./index.ts"
15
+ },
16
+ "files": [
17
+ "src",
18
+ "index.ts"
19
+ ],
20
+ "keywords": [
21
+ "astro-component",
22
+ "tree-view",
23
+ "navigation"
24
+ ],
25
+ "devDependencies": {
26
+ "@astrojs/check": "^0.9.4",
27
+ "astro": "^5.13.7",
28
+ "typescript": "^5.9.2"
29
+ },
30
+ "peerDependencies": {
31
+ "astro": "^4.0.0"
32
+ },
33
+ "dependencies": {
34
+ "@pindoba/panda": "0.0.0-alpha-20250910225440",
35
+ "@pindoba/utils": "0.0.0-alpha-20250910225440",
36
+ "@pindoba/styles-tree-view": "0.0.0-alpha-20250910225440"
37
+ },
38
+ "scripts": {
39
+ "lint": "TIMING=1 eslint . --max-warnings 0",
40
+ "lint:fix": "TIMING=1 eslint . --fix --max-warnings 0",
41
+ "format": "prettier --write .",
42
+ "format:fix": "prettier --write .",
43
+ "link": "pnpm link",
44
+ "unlink": "pnpm unlink",
45
+ "check": "astro check",
46
+ "dev": "astro check --watch",
47
+ "clean": "rm -rf .astro && rm -rf node_modules && rm -rf .turbo"
48
+ }
49
+ }
@@ -0,0 +1,57 @@
1
+ ---
2
+ import { type TreeNode, type TreeState, TreeUtils } from "./tree-utils";
3
+ import {
4
+ type TreeViewStyleProps,
5
+ type TreeViewSlots,
6
+ } from "@pindoba/styles-tree-view";
7
+ import type { SystemStyleObject } from "@pindoba/panda/types";
8
+ import TreeViewItem from "./TreeViewItem.astro";
9
+
10
+ export interface TreeNodeItemProps {
11
+ node: TreeNode;
12
+ state: TreeState;
13
+ size?: TreeViewStyleProps["size"];
14
+ emphasis?: TreeViewStyleProps["emphasis"];
15
+ passThrough?: { [key in TreeViewSlots]?: SystemStyleObject };
16
+ }
17
+
18
+ type Props = TreeNodeItemProps;
19
+
20
+ const { node, state, size, emphasis, passThrough } = Astro.props;
21
+
22
+ const isExpanded = TreeUtils.isExpanded(state, node.id);
23
+ const hasChildren = node.children && node.children.length > 0;
24
+ ---
25
+
26
+ {
27
+ hasChildren ? (
28
+ <TreeViewItem
29
+ label={node.label}
30
+ open={isExpanded}
31
+ size={size}
32
+ emphasis={emphasis}
33
+ passThrough={passThrough}
34
+ data-node-id={node.id}
35
+ >
36
+ {node.children &&
37
+ node.children.map((childNode) => (
38
+ <Astro.self
39
+ slot="children"
40
+ node={childNode}
41
+ state={state}
42
+ size={size}
43
+ emphasis={emphasis}
44
+ passThrough={passThrough}
45
+ />
46
+ ))}
47
+ </TreeViewItem>
48
+ ) : (
49
+ <TreeViewItem
50
+ label={node.label}
51
+ size={size}
52
+ emphasis={emphasis}
53
+ passThrough={passThrough}
54
+ data-node-id={node.id}
55
+ />
56
+ )
57
+ }
@@ -0,0 +1,95 @@
1
+ ---
2
+ import { css, cx } from "@pindoba/panda/css";
3
+ import {
4
+ treeViewStyles,
5
+ type TreeViewSlots,
6
+ type TreeViewStyleProps,
7
+ } from "@pindoba/styles-tree-view";
8
+ import { type TreeNode, type TreeState, TreeUtils } from "./tree-utils";
9
+ import type { HTMLAttributes } from "astro/types";
10
+ import type { SystemStyleObject } from "@pindoba/panda/types";
11
+ import TreeNodeItem from "./TreeNodeItem.astro";
12
+
13
+ export interface TreeViewProps extends HTMLAttributes<"ul"> {
14
+ size?: TreeViewStyleProps["size"];
15
+ emphasis?: TreeViewStyleProps["emphasis"];
16
+ passThrough?: { [key in TreeViewSlots]?: SystemStyleObject };
17
+ // Imperative API props
18
+ data?: TreeNode[];
19
+ onToggle?: (nodeId: string, expanded: boolean) => void;
20
+ }
21
+
22
+ type Props = TreeViewProps;
23
+
24
+ const {
25
+ class: className,
26
+ size = "md",
27
+ emphasis = "default",
28
+ passThrough,
29
+ data,
30
+ onToggle,
31
+ ...props
32
+ } = Astro.props;
33
+
34
+ const classes = treeViewStyles.raw({ size, emphasis });
35
+
36
+ // Initialize state for imperative API
37
+ let treeState: TreeState | undefined;
38
+ if (data) {
39
+ treeState = TreeUtils.getInitialState(data);
40
+ }
41
+ ---
42
+
43
+ <ul
44
+ class={cx(css(classes.root, passThrough?.root), className)}
45
+ data-tree-view-root
46
+ {...props}
47
+ >
48
+ {
49
+ data && treeState ? (
50
+ data.map((node: TreeNode) => (
51
+ <TreeNodeItem
52
+ node={node}
53
+ state={treeState!}
54
+ size={size}
55
+ emphasis={emphasis}
56
+ passThrough={passThrough}
57
+ />
58
+ ))
59
+ ) : (
60
+ <slot />
61
+ )
62
+ }
63
+ </ul>
64
+
65
+ <script
66
+ is:inline
67
+ define:vars={{ onToggleStr: onToggle?.toString(), hasData: !!data }}
68
+ >
69
+ // Add event handling for toggle interactions in imperative mode
70
+ if (hasData && onToggleStr) {
71
+ document.addEventListener("DOMContentLoaded", () => {
72
+ const treeRoot = document.querySelector("[data-tree-view-root]");
73
+
74
+ if (treeRoot) {
75
+ treeRoot.addEventListener("click", (event) => {
76
+ const target = event.target.closest("[data-node-id]");
77
+ if (target) {
78
+ const trigger = event.target.closest("summary");
79
+ if (trigger) {
80
+ const nodeId = target.getAttribute("data-node-id");
81
+ const details = trigger.parentElement;
82
+ const isExpanding = !details.hasAttribute("open");
83
+
84
+ // Dispatch custom event instead of direct function call
85
+ const toggleEvent = new CustomEvent("tree-node-toggle", {
86
+ detail: { nodeId, expanded: isExpanding },
87
+ });
88
+ document.dispatchEvent(toggleEvent);
89
+ }
90
+ }
91
+ });
92
+ }
93
+ });
94
+ }
95
+ </script>
@@ -0,0 +1,130 @@
1
+ ---
2
+ import { css, cx } from "@pindoba/panda/css";
3
+ import {
4
+ treeViewStyles,
5
+ type TreeViewSlots,
6
+ type TreeViewStyleProps,
7
+ } from "@pindoba/styles-tree-view";
8
+ import type { HTMLAttributes } from "astro/types";
9
+ import type { SystemStyleObject } from "@pindoba/panda/types";
10
+
11
+ export interface TreeViewItemProps extends HTMLAttributes<"li"> {
12
+ label?: string;
13
+ open?: boolean;
14
+ size?: TreeViewStyleProps["size"];
15
+ emphasis?: TreeViewStyleProps["emphasis"];
16
+ passThrough?: { [key in TreeViewSlots]?: SystemStyleObject };
17
+ }
18
+
19
+ type Props = TreeViewItemProps;
20
+
21
+ const {
22
+ class: className,
23
+ label,
24
+ open = false,
25
+ size,
26
+ emphasis,
27
+ passThrough,
28
+ ...props
29
+ } = Astro.props;
30
+
31
+ const classes = treeViewStyles.raw({ size, emphasis });
32
+ const hasChildren = Astro.slots.has("children");
33
+ const hasLabel = Astro.slots.has("label");
34
+ const hasLeadingElement = Astro.slots.has("leadingElement");
35
+ const hasTrailingElement = Astro.slots.has("trailingElement");
36
+ const displayLabel = hasLabel ? null : label;
37
+ ---
38
+
39
+ <li class={cx(css(classes.item, passThrough?.item), className)} {...props}>
40
+ {
41
+ hasChildren ? (
42
+ <details open={open}>
43
+ <summary
44
+ class={cx(
45
+ "tree-view-summary",
46
+ css(classes.summary, passThrough?.summary),
47
+ )}
48
+ >
49
+ <span
50
+ class={cx(
51
+ "tree-view-trigger",
52
+ css(classes.trigger, passThrough?.trigger),
53
+ )}
54
+ >
55
+ <svg
56
+ class={cx("tree-view-icon", css(classes.icon, passThrough?.icon))}
57
+ viewBox="0 0 24 24"
58
+ fill="none"
59
+ stroke="currentColor"
60
+ stroke-width="2"
61
+ stroke-linecap="round"
62
+ stroke-linejoin="round"
63
+ >
64
+ <path d="m9 18 6-6-6-6" />
65
+ </svg>
66
+ </span>
67
+
68
+ {hasLeadingElement && (
69
+ <div
70
+ class={cx(
71
+ "tree-view-leading-element",
72
+ css(classes.leadingElement, passThrough?.leadingElement),
73
+ )}
74
+ >
75
+ <slot name="leadingElement" />
76
+ </div>
77
+ )}
78
+
79
+ <span class={css(classes.label, passThrough?.label)}>
80
+ {hasLabel ? <slot name="label" /> : displayLabel}
81
+ </span>
82
+ {hasTrailingElement && (
83
+ <div
84
+ class={cx(
85
+ "tree-view-trailing-element",
86
+ css(classes.trailingElement, passThrough?.trailingElement),
87
+ )}
88
+ >
89
+ <slot name="trailingElement" />
90
+ </div>
91
+ )}
92
+ </summary>
93
+ <ul class={css(classes.children, passThrough?.children)}>
94
+ <slot name="children" />
95
+ </ul>
96
+ </details>
97
+ ) : (
98
+ <div
99
+ class={cx(
100
+ "tree-view-summary",
101
+ css(classes.summary, passThrough?.summary),
102
+ )}
103
+ >
104
+ {hasLeadingElement && (
105
+ <div
106
+ class={cx(
107
+ "tree-view-leading-element",
108
+ css(classes.leadingElement, passThrough?.leadingElement),
109
+ )}
110
+ >
111
+ <slot name="leadingElement" />
112
+ </div>
113
+ )}
114
+ <span class={css(classes.label, passThrough?.label)}>
115
+ {hasLabel ? <slot name="label" /> : displayLabel}
116
+ </span>
117
+ {hasTrailingElement && (
118
+ <div
119
+ class={cx(
120
+ "tree-view-trailing-element",
121
+ css(classes.trailingElement, passThrough?.trailingElement),
122
+ )}
123
+ >
124
+ <slot name="trailingElement" />
125
+ </div>
126
+ )}
127
+ </div>
128
+ )
129
+ }
130
+ </li>
@@ -0,0 +1,54 @@
1
+ // Tree data structure interfaces
2
+ export interface TreeNode {
3
+ id: string;
4
+ label: string;
5
+ expanded?: boolean;
6
+ children?: TreeNode[];
7
+ }
8
+
9
+ export interface TreeState {
10
+ expandedNodes: Set<string>;
11
+ }
12
+
13
+ // Tree manipulation utilities
14
+ export class TreeUtils {
15
+ static expandNode(state: TreeState, nodeId: string): TreeState {
16
+ const newExpandedNodes = new Set(state.expandedNodes);
17
+ newExpandedNodes.add(nodeId);
18
+ return { expandedNodes: newExpandedNodes };
19
+ }
20
+
21
+ static collapseNode(state: TreeState, nodeId: string): TreeState {
22
+ const newExpandedNodes = new Set(state.expandedNodes);
23
+ newExpandedNodes.delete(nodeId);
24
+ return { expandedNodes: newExpandedNodes };
25
+ }
26
+
27
+ static toggleNode(state: TreeState, nodeId: string): TreeState {
28
+ return state.expandedNodes.has(nodeId)
29
+ ? this.collapseNode(state, nodeId)
30
+ : this.expandNode(state, nodeId);
31
+ }
32
+
33
+ static isExpanded(state: TreeState, nodeId: string): boolean {
34
+ return state.expandedNodes.has(nodeId);
35
+ }
36
+
37
+ static getInitialState(data: TreeNode[]): TreeState {
38
+ const expandedNodes = new Set<string>();
39
+
40
+ const collectExpanded = (nodes: TreeNode[]) => {
41
+ nodes.forEach((node) => {
42
+ if (node.expanded) {
43
+ expandedNodes.add(node.id);
44
+ }
45
+ if (node.children) {
46
+ collectExpanded(node.children);
47
+ }
48
+ });
49
+ };
50
+
51
+ collectExpanded(data);
52
+ return { expandedNodes };
53
+ }
54
+ }