Skip to content

Commit 72e52a4

Browse files
add tailwind example, refactor site (#22)
* cursor tailwind demo * update tailwind demo * refactor examples into separate pages, utils into lib * fix add no-sroll, keep-focus to links * move TailwindNodeDemo into +page * refactor * add DataSelector for setting example data * refactor * fix cypress tests
1 parent ecaa70b commit 72e52a4

File tree

19 files changed

+733
-154
lines changed

19 files changed

+733
-154
lines changed

packages/site/src/app.css

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -149,10 +149,22 @@ a:hover {
149149
color: var(--tree-view-base0F);
150150
}
151151

152-
.btn {
152+
.btn-sm {
153153
background-color: var(--tree-view-base01);
154154
color: var(--tree-view-base0A);
155+
@apply rounded px-1 py-0.5 text-sm tracking-wide transition-colors duration-100;
156+
157+
&:hover {
158+
background-color: var(--tree-view-base02);
159+
}
160+
&:focus {
161+
@apply shadow outline-none;
162+
}
163+
}
155164

165+
.btn {
166+
background-color: var(--tree-view-base01);
167+
color: var(--tree-view-base0A);
156168
@apply rounded px-4 py-2 text-base tracking-wide transition-colors duration-100;
157169
}
158170
.btn:hover {
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
<script lang="ts">
2+
import { DATA, state, setExampleData } from '$lib/store'
3+
</script>
4+
5+
<div class="mt-2 flex items-center gap-2">
6+
<label for="data-selector" class="control-label">Example Data:</label>
7+
<div>
8+
<select
9+
id="data-selector"
10+
value={$state.selectedData}
11+
oninput={e => setExampleData(e.currentTarget.value)}
12+
class="control-select"
13+
>
14+
{#each Object.keys(DATA) as opt}
15+
<option value={opt}>{opt}</option>
16+
{/each}
17+
</select>
18+
</div>
19+
</div>
20+
21+
<style lang="postcss">
22+
@reference "#app.css";
23+
24+
.control-label {
25+
@apply block text-sm font-medium text-gray-700 dark:text-gray-300;
26+
}
27+
28+
.control-select {
29+
@apply w-full rounded-md border border-gray-300 bg-white px-3 py-0.5 text-gray-900 focus:border-blue-500 focus:ring-2 focus:ring-blue-500 dark:border-gray-600 dark:bg-gray-700 dark:text-gray-100 dark:focus:border-blue-400 dark:focus:ring-blue-400;
30+
}
31+
</style>

packages/site/src/components/PropsForm.svelte

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
<script lang="ts">
2-
import { state, update } from '../utils/store'
2+
import { state, update } from '$lib/store'
33
</script>
44

55
<fieldset class="container-sm flex flex-col border-2 p-2 text-sm">
@@ -42,7 +42,7 @@
4242
<div
4343
class="text-0B rounded-sm px-1 py-0.5 text-xs tracking-wide transition-colors duration-100"
4444
>
45-
See Example 2
45+
See Diff and Tailwind
4646
</div>
4747
</div>
4848
</div>
Lines changed: 297 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,297 @@
1+
<script lang="ts">
2+
import type { NodeProps } from 'svelte-tree-view'
3+
4+
let {
5+
node,
6+
TreeViewNode,
7+
getTreeContext,
8+
handleLogNode,
9+
handleCopyNodeToClipboard,
10+
handleToggleCollapse
11+
}: NodeProps = $props()
12+
13+
const {
14+
propsStore: { props: propsObj }
15+
} = getTreeContext()
16+
17+
let hasChildren = $derived(node && node.children.length > 0)
18+
let descend = $derived(!node.collapsed && hasChildren)
19+
20+
// Function to create truncated preview of objects and arrays
21+
function createTruncatedPreview(value: any, type: string): string {
22+
if (type === 'object' && value && typeof value === 'object' && !Array.isArray(value)) {
23+
const keys = Object.keys(value)
24+
if (keys.length === 0) return '{}'
25+
26+
const preview = keys
27+
.slice(0, 3)
28+
.map(key => {
29+
const val = value[key]
30+
if (val === null) return `${key}: null`
31+
if (typeof val === 'object') return `${key}: {…}`
32+
if (typeof val === 'string') {
33+
return `${key}: "${val.length > 10 ? val.substring(0, 10) + '…' : val}"`
34+
}
35+
return `${key}: ${val}`
36+
})
37+
.join(', ')
38+
39+
const suffix = keys.length > 3 ? `, …` : ''
40+
return `{ ${preview}${suffix} }`
41+
}
42+
43+
if (type === 'array' && Array.isArray(value)) {
44+
if (value.length === 0) return '[]'
45+
46+
const preview = value
47+
.slice(0, 3)
48+
.map(item => {
49+
if (item === null) return 'null'
50+
if (typeof item === 'object') return '{…}'
51+
if (typeof item === 'string') {
52+
return `"${item.length > 10 ? item.substring(0, 10) + '…' : item}"`
53+
}
54+
return String(item)
55+
})
56+
.join(', ')
57+
58+
const suffix = value.length > 3 ? ', …' : ''
59+
return `[ ${preview}${suffix} ]`
60+
}
61+
62+
// For other types, use the default formatter or string representation
63+
return $propsObj.valueFormatter?.(value, node) ?? String(value)
64+
}
65+
66+
// Get the appropriate value to display
67+
function getDisplayValue(): string {
68+
if (hasChildren && node.collapsed) {
69+
// Show truncated preview when collapsed
70+
return createTruncatedPreview(node.value, node.type)
71+
} else {
72+
// Show full value when expanded or for leaf nodes
73+
return $propsObj.valueFormatter?.(node.value, node) ?? String(node.value)
74+
}
75+
}
76+
77+
// Get type-specific styling classes
78+
function getTypeClasses() {
79+
switch (node.type) {
80+
case 'string':
81+
return 'text-green-600 dark:text-green-400'
82+
case 'number':
83+
case 'bigint':
84+
return 'text-blue-600 dark:text-blue-400'
85+
case 'boolean':
86+
return 'text-purple-600 dark:text-purple-400'
87+
case 'null':
88+
case 'undefined':
89+
return 'text-gray-500 dark:text-gray-400 italic'
90+
case 'object':
91+
case 'array':
92+
case 'map':
93+
case 'set':
94+
return 'text-orange-600 dark:text-orange-400'
95+
case 'function':
96+
return 'text-indigo-600 dark:text-indigo-400'
97+
case 'date':
98+
return 'text-teal-600 dark:text-teal-400'
99+
default:
100+
return 'text-gray-700 dark:text-gray-300'
101+
}
102+
}
103+
</script>
104+
105+
<div class="tree-node-container" data-tree-id={node.id}>
106+
<div
107+
class="tree-node-card group"
108+
class:collapsed={node.collapsed && hasChildren}
109+
class:has-children={hasChildren}
110+
>
111+
<!-- Arrow button for expandable nodes -->
112+
{#if hasChildren}
113+
<button
114+
class="arrow-button"
115+
class:collapsed={node.collapsed}
116+
onclick={handleToggleCollapse}
117+
aria-label={node.collapsed ? 'Expand' : 'Collapse'}
118+
>
119+
<svg class="arrow-icon" viewBox="0 0 20 20" fill="currentColor">
120+
<path
121+
fill-rule="evenodd"
122+
d="M7.293 14.707a1 1 0 010-1.414L10.586 10 7.293 6.707a1 1 0 011.414-1.414l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414 0z"
123+
clip-rule="evenodd"
124+
/>
125+
</svg>
126+
</button>
127+
{:else}
128+
<div class="arrow-placeholder"></div>
129+
{/if}
130+
131+
<!-- Node content -->
132+
<button
133+
class="node-content"
134+
class:clickable={hasChildren}
135+
onclick={hasChildren ? handleToggleCollapse : undefined}
136+
>
137+
<!-- Key section -->
138+
<div class="node-key-section">
139+
<span class="node-key">{node.key}:</span>
140+
</div>
141+
142+
<!-- Value section -->
143+
<div class="node-value-section">
144+
<div
145+
class={`node-value truncate ${getTypeClasses()}`}
146+
class:clickable={hasChildren}
147+
data-type={node.type}
148+
>
149+
{getDisplayValue()}
150+
</div>
151+
</div>
152+
</button>
153+
154+
<!-- Action buttons -->
155+
<div class="action-buttons">
156+
{#if $propsObj.showLogButton}
157+
<button
158+
class="action-button log-button"
159+
onclick={handleLogNode}
160+
aria-label="Log to console"
161+
>
162+
<svg class="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
163+
<path
164+
stroke-linecap="round"
165+
stroke-linejoin="round"
166+
stroke-width="2"
167+
d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"
168+
/>
169+
</svg>
170+
</button>
171+
{/if}
172+
{#if $propsObj.showCopyButton}
173+
<button
174+
class="action-button copy-button"
175+
onclick={handleCopyNodeToClipboard}
176+
aria-label="Copy to clipboard"
177+
>
178+
<svg class="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
179+
<path
180+
stroke-linecap="round"
181+
stroke-linejoin="round"
182+
stroke-width="2"
183+
d="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z"
184+
/>
185+
</svg>
186+
</button>
187+
{/if}
188+
</div>
189+
</div>
190+
191+
{#if descend}
192+
<div class="children-container">
193+
{#each node.children as child}
194+
<TreeViewNode id={child.id} />
195+
{/each}
196+
</div>
197+
{/if}
198+
</div>
199+
200+
<style lang="postcss">
201+
@reference "#app.css";
202+
203+
.tree-node-container {
204+
@apply w-full;
205+
}
206+
207+
.tree-node-card {
208+
@apply flex items-center gap-2 rounded-lg border border-gray-200 bg-white p-2 transition-all duration-200 ease-in-out hover:bg-gray-100 dark:border-gray-700 dark:bg-gray-800 dark:hover:bg-gray-700;
209+
}
210+
211+
.tree-node-card.has-children {
212+
@apply cursor-pointer;
213+
}
214+
215+
.tree-node-card.has-children:hover {
216+
@apply border-gray-300 shadow-md dark:border-gray-600;
217+
}
218+
219+
.arrow-button {
220+
@apply flex h-6 w-6 flex-shrink-0 items-center justify-center rounded-md text-gray-500 transition-all duration-200 hover:bg-gray-100 hover:text-gray-700 dark:text-gray-400 dark:hover:bg-gray-600 dark:hover:text-gray-200;
221+
}
222+
223+
.arrow-button.collapsed .arrow-icon {
224+
@apply rotate-0 transform;
225+
}
226+
227+
.arrow-icon {
228+
@apply h-4 w-4 transition-transform duration-200;
229+
}
230+
231+
.arrow-placeholder {
232+
@apply h-6 w-6 flex-shrink-0;
233+
}
234+
235+
.node-content {
236+
@apply flex min-w-0 flex-1 gap-2;
237+
}
238+
239+
.node-content.clickable {
240+
@apply cursor-pointer;
241+
}
242+
243+
.node-key-section {
244+
@apply flex-shrink-0;
245+
}
246+
247+
.node-key {
248+
@apply font-medium text-gray-700 dark:text-gray-300;
249+
}
250+
251+
.node-value-section {
252+
@apply min-w-0 flex-1;
253+
}
254+
255+
.node-value {
256+
@apply flex break-words;
257+
}
258+
259+
.node-value.clickable {
260+
@apply cursor-pointer;
261+
}
262+
263+
.action-buttons {
264+
@apply flex flex-shrink-0 items-center gap-1 opacity-0 transition-opacity duration-200 group-hover:opacity-100;
265+
}
266+
267+
.action-button {
268+
@apply rounded-md p-1.5 text-gray-500 transition-all duration-200 hover:bg-gray-100 hover:text-gray-700 dark:text-gray-400 dark:hover:bg-gray-600 dark:hover:text-gray-200;
269+
}
270+
271+
.children-container {
272+
@apply ml-6 mt-2 space-y-1;
273+
}
274+
275+
/* Responsive design */
276+
@media (max-width: 640px) {
277+
.tree-node-card {
278+
@apply p-1.5;
279+
}
280+
281+
.arrow-button {
282+
@apply h-5 w-5;
283+
}
284+
285+
.arrow-icon {
286+
@apply h-3.5 w-3.5;
287+
}
288+
289+
.arrow-placeholder {
290+
@apply h-5 w-5;
291+
}
292+
293+
.action-buttons {
294+
@apply opacity-100;
295+
}
296+
}
297+
</style>

0 commit comments

Comments
 (0)