@humanspeak/svelte-json-view-lite 0.2.1 → 0.2.3
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/README.md +3 -3
- package/dist/DataRender.svelte +9 -4
- package/dist/ExpandableObject.svelte +40 -34
- package/dist/JsonView.svelte +17 -2
- package/dist/types.d.ts +4 -7
- package/package.json +21 -21
- package/dist/EmptyObject.svelte +0 -25
- package/dist/EmptyObject.svelte.d.ts +0 -4
package/README.md
CHANGED
|
@@ -266,12 +266,12 @@ Part of the [Humanspeak](https://humanspeak.com) family of runes-native Svelte 5
|
|
|
266
266
|
| [@humanspeak/svelte-virtual-list](https://virtuallist.svelte.page) | Virtual scrolling for Svelte |
|
|
267
267
|
| [@humanspeak/svelte-motion](https://motion.svelte.page) | Framer Motion for Svelte 5 |
|
|
268
268
|
| [@humanspeak/svelte-headless-table](https://table.svelte.page) | Headless data tables for Svelte |
|
|
269
|
-
| [@humanspeak/svelte-diff
|
|
270
|
-
| [@humanspeak/svelte-purify](https://purify.svelte.page) | HTML sanitisation for Svelte |
|
|
269
|
+
| [@humanspeak/svelte-diff](https://diff.svelte.page) | Diff comparison for Svelte |
|
|
271
270
|
| [@humanspeak/svelte-virtual-chat](https://virtualchat.svelte.page) | Virtual chat viewport for Svelte 5 |
|
|
271
|
+
| [@humanspeak/svelte-purify](https://purify.svelte.page) | HTML sanitisation for Svelte |
|
|
272
272
|
| [@humanspeak/memory-cache](https://memory.svelte.page) | In-memory cache for TypeScript |
|
|
273
|
-
| **[@humanspeak/svelte-json-view-lite](https://jsonview.svelte.page)** — _this package_ | JSON tree viewer for Svelte 5 |
|
|
274
273
|
| [@humanspeak/svelte-scoped-props](https://scoped.svelte.page) | Scoped class props for Svelte |
|
|
274
|
+
| **[@humanspeak/svelte-json-view-lite](https://jsonview.svelte.page)** — _this package_ | JSON tree viewer for Svelte 5 |
|
|
275
275
|
<!-- prettier-ignore-end -->
|
|
276
276
|
|
|
277
277
|
## License
|
package/dist/DataRender.svelte
CHANGED
|
@@ -13,8 +13,13 @@
|
|
|
13
13
|
const value = $derived(props.value)
|
|
14
14
|
|
|
15
15
|
// Array check first — arrays are also objects, so order matters.
|
|
16
|
-
const
|
|
17
|
-
|
|
16
|
+
const kind = $derived(
|
|
17
|
+
isArray(value)
|
|
18
|
+
? 'array'
|
|
19
|
+
: isObject(value) && !isDate(value) && !isFunction(value)
|
|
20
|
+
? 'object'
|
|
21
|
+
: 'primitive'
|
|
22
|
+
)
|
|
18
23
|
|
|
19
24
|
// The children tuple array is NOT built here: a collapsed node only needs
|
|
20
25
|
// its child *count*, and materializing `Array<[key, value]>` for every
|
|
@@ -23,7 +28,7 @@
|
|
|
23
28
|
// lazily, gated on its own `expanded` state. See issue #21.
|
|
24
29
|
</script>
|
|
25
30
|
|
|
26
|
-
{#if
|
|
31
|
+
{#if kind === 'array'}
|
|
27
32
|
<ExpandableObject
|
|
28
33
|
{...props}
|
|
29
34
|
value={value as unknown[]}
|
|
@@ -31,7 +36,7 @@
|
|
|
31
36
|
openBracket="["
|
|
32
37
|
closeBracket="]"
|
|
33
38
|
/>
|
|
34
|
-
{:else if
|
|
39
|
+
{:else if kind === 'object'}
|
|
35
40
|
<ExpandableObject
|
|
36
41
|
{...props}
|
|
37
42
|
value={value as object}
|
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import { untrack } from 'svelte'
|
|
3
3
|
import DataRender from './DataRender.svelte'
|
|
4
|
-
import
|
|
5
|
-
import type { AriaLabels, ExpandableRenderProps } from './types.js'
|
|
4
|
+
import type { AriaLabels, ExpandableRenderProps, ExpansionStrategy } from './types.js'
|
|
6
5
|
import { quoteString } from './utils/quoteString.js'
|
|
7
6
|
|
|
8
7
|
const {
|
|
@@ -21,9 +20,8 @@
|
|
|
21
20
|
snippets
|
|
22
21
|
}: ExpandableRenderProps = $props()
|
|
23
22
|
|
|
24
|
-
//
|
|
25
|
-
//
|
|
26
|
-
// field mutations from ancestor re-renders don't collapse the node.
|
|
23
|
+
// Capture initial expansion once; only a new strategy or a user toggle
|
|
24
|
+
// changes it. Data/theme updates preserve the user's manual override.
|
|
27
25
|
// svelte-ignore state_referenced_locally
|
|
28
26
|
let expanded = $state(shouldExpandNode(level, value, field))
|
|
29
27
|
|
|
@@ -33,38 +31,39 @@
|
|
|
33
31
|
// svelte-ignore state_referenced_locally
|
|
34
32
|
let hasMaterialized = $state(expanded)
|
|
35
33
|
|
|
36
|
-
let
|
|
34
|
+
let appliedStrategy = untrack(() => shouldExpandNode)
|
|
37
35
|
|
|
38
|
-
//
|
|
39
|
-
// click, keyboard) routes through here, so the materialization high-water
|
|
40
|
-
// mark can't be forgotten when a new expansion path is added.
|
|
36
|
+
// Preserve the child cache through both strategy changes and user toggles.
|
|
41
37
|
function applyExpanded(next: boolean) {
|
|
42
38
|
expanded = next
|
|
43
39
|
if (next) hasMaterialized = true
|
|
44
40
|
}
|
|
45
41
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
}
|
|
52
|
-
// Track only the callback identity: untrack level/value/field so an
|
|
53
|
-
// ancestor re-render that mutates them can't rerun this effect and
|
|
54
|
-
// overwrite the user's expansion state.
|
|
55
|
-
applyExpanded(untrack(() => fn(level, value, field)))
|
|
56
|
-
})
|
|
42
|
+
const updateExpansion = (fn: ExpansionStrategy) => {
|
|
43
|
+
if (appliedStrategy === fn) return
|
|
44
|
+
appliedStrategy = fn
|
|
45
|
+
applyExpanded(fn(level, value, field))
|
|
46
|
+
}
|
|
57
47
|
|
|
58
48
|
// SSR-stable id for aria-controls linkage.
|
|
59
49
|
const contentsId = $props.id()
|
|
60
50
|
|
|
61
51
|
let expanderButton = $state<HTMLSpanElement | null>(null)
|
|
62
52
|
|
|
63
|
-
//
|
|
64
|
-
//
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
53
|
+
// The container survives empty/nonempty transitions, so it keeps receiving
|
|
54
|
+
// strategy updates even when there is no expander button. Its action owns
|
|
55
|
+
// both cleanup paths; no node subscribes to shouldExpandNode reactively.
|
|
56
|
+
function registerContainer(_node: HTMLDivElement) {
|
|
57
|
+
outerRef.expansionListeners.add(updateExpansion)
|
|
58
|
+
$effect(() => {
|
|
59
|
+
const button = expanderButton
|
|
60
|
+
if (button) return untrack(() => outerRef.navigation.register(button))
|
|
61
|
+
})
|
|
62
|
+
return {
|
|
63
|
+
destroy() {
|
|
64
|
+
outerRef.expansionListeners.delete(updateExpansion)
|
|
65
|
+
}
|
|
66
|
+
}
|
|
68
67
|
}
|
|
69
68
|
|
|
70
69
|
const activeAriaLabels = $derived<AriaLabels>(
|
|
@@ -127,12 +126,19 @@
|
|
|
127
126
|
}
|
|
128
127
|
</script>
|
|
129
128
|
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
{
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
129
|
+
<!-- svelte-ignore a11y_role_has_required_aria_props -->
|
|
130
|
+
<div
|
|
131
|
+
class={style.basicChildStyle}
|
|
132
|
+
role="treeitem"
|
|
133
|
+
aria-expanded={count === 0 ? undefined : expanded}
|
|
134
|
+
use:registerContainer
|
|
135
|
+
>
|
|
136
|
+
{#if count === 0}
|
|
137
|
+
<!-- prettier-ignore -->
|
|
138
|
+
{#if field !== undefined}<span class={style.label}>{labelText}:</span>{/if}<span
|
|
139
|
+
class={style.punctuation}>{openBracket}{closeBracket}{lastElement ? '' : ','}</span
|
|
140
|
+
>
|
|
141
|
+
{:else}
|
|
136
142
|
<!--
|
|
137
143
|
The entire inline sequence inside a row lives on a single
|
|
138
144
|
prettier-ignored line because Svelte preserves template whitespace
|
|
@@ -144,7 +150,7 @@
|
|
|
144
150
|
it tight anyway for a consistent rule.
|
|
145
151
|
-->
|
|
146
152
|
<!-- prettier-ignore -->
|
|
147
|
-
<span bind:this={expanderButton}
|
|
153
|
+
<span bind:this={expanderButton} class={expanderIconStyle} role="button" aria-label={ariaLabel} aria-expanded={expanded} aria-controls={expanded ? contentsId : undefined} tabindex={level === 0 ? 0 : -1} onclick={onClick} onkeydown={onKeyDown}></span>{#if field !== undefined}{#if snippets.label}{@render snippets.label(
|
|
148
154
|
{ field: field ?? '', level }
|
|
149
155
|
)}{:else if clickToExpandNode}<!-- svelte-ignore a11y_no_static_element_interactions --><span
|
|
150
156
|
class={style.clickableLabel}
|
|
@@ -172,5 +178,5 @@
|
|
|
172
178
|
></span>{/if}<span class={style.punctuation}
|
|
173
179
|
>{closeBracket}{lastElement ? '' : ','}</span
|
|
174
180
|
>
|
|
175
|
-
|
|
176
|
-
|
|
181
|
+
{/if}
|
|
182
|
+
</div>
|
package/dist/JsonView.svelte
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
|
+
import { untrack } from 'svelte'
|
|
2
3
|
import DataRender from './DataRender.svelte'
|
|
3
4
|
import { defaultStyles } from './index.js'
|
|
4
|
-
import type { OuterRef, Props, StyleProps } from './types.js'
|
|
5
|
+
import type { ExpansionListener, OuterRef, Props, StyleProps } from './types.js'
|
|
5
6
|
import { isObject } from './utils/dataTypeDetection.js'
|
|
6
7
|
import { createExpanderNavigation } from './utils/expanderNavigation.js'
|
|
7
8
|
import { allExpanded } from './utils/expandStrategies.js'
|
|
@@ -26,6 +27,19 @@
|
|
|
26
27
|
...rest
|
|
27
28
|
}: Props = $props()
|
|
28
29
|
|
|
30
|
+
// One strategy subscription per tree. Untracked dispatch lets each node read
|
|
31
|
+
// its current props without turning data/theme changes into expansion resets.
|
|
32
|
+
const expansionListeners = new Set<ExpansionListener>()
|
|
33
|
+
let previousStrategy = untrack(() => shouldExpandNode)
|
|
34
|
+
$effect(() => {
|
|
35
|
+
const fn = shouldExpandNode
|
|
36
|
+
if (previousStrategy === fn) return
|
|
37
|
+
previousStrategy = fn
|
|
38
|
+
untrack(() => {
|
|
39
|
+
for (const update of expansionListeners) update(fn)
|
|
40
|
+
})
|
|
41
|
+
})
|
|
42
|
+
|
|
29
43
|
let outerElement = $state<HTMLDivElement | null>(null)
|
|
30
44
|
const navigation = createExpanderNavigation()
|
|
31
45
|
|
|
@@ -55,7 +69,8 @@
|
|
|
55
69
|
get current() {
|
|
56
70
|
return outerElement
|
|
57
71
|
},
|
|
58
|
-
navigation
|
|
72
|
+
navigation,
|
|
73
|
+
expansionListeners
|
|
59
74
|
}
|
|
60
75
|
|
|
61
76
|
const snippets = $derived({
|
package/dist/types.d.ts
CHANGED
|
@@ -105,6 +105,9 @@ export interface Props extends Omit<HTMLAttributes<HTMLDivElement>, 'data' | 'st
|
|
|
105
105
|
function?: Snippet<[FunctionSnippetProps]>;
|
|
106
106
|
label?: Snippet<[LabelSnippetProps]>;
|
|
107
107
|
}
|
|
108
|
+
/** Internal expansion callbacks shared by one viewer's controller. */
|
|
109
|
+
export type ExpansionStrategy = NonNullable<Props['shouldExpandNode']>;
|
|
110
|
+
export type ExpansionListener = (_strategy: ExpansionStrategy) => void;
|
|
108
111
|
/**
|
|
109
112
|
* Tree-local controller for roving tabindex across expandable nodes.
|
|
110
113
|
*
|
|
@@ -162,6 +165,7 @@ export interface ExpanderNavigation {
|
|
|
162
165
|
export interface OuterRef {
|
|
163
166
|
readonly current: HTMLDivElement | null;
|
|
164
167
|
readonly navigation: ExpanderNavigation;
|
|
168
|
+
readonly expansionListeners: Set<ExpansionListener>;
|
|
165
169
|
}
|
|
166
170
|
/** Internal shared props threaded through every renderer. Not exported. */
|
|
167
171
|
export interface CommonRenderProps {
|
|
@@ -187,10 +191,3 @@ export interface ExpandableRenderProps extends CommonRenderProps {
|
|
|
187
191
|
openBracket: string;
|
|
188
192
|
closeBracket: string;
|
|
189
193
|
}
|
|
190
|
-
export interface EmptyRenderProps {
|
|
191
|
-
field?: string;
|
|
192
|
-
openBracket: string;
|
|
193
|
-
closeBracket: string;
|
|
194
|
-
lastElement: boolean;
|
|
195
|
-
style: StyleProps;
|
|
196
|
-
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@humanspeak/svelte-json-view-lite",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.3",
|
|
4
4
|
"description": "Fast, tiny JSON tree viewer for Svelte 5 — port of react-json-view-lite with runes, SSR, snippet overrides, and zero runtime dependencies",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"svelte",
|
|
@@ -57,37 +57,37 @@
|
|
|
57
57
|
"devDependencies": {
|
|
58
58
|
"@eslint/compat": "^2.1.0",
|
|
59
59
|
"@eslint/js": "^10.0.1",
|
|
60
|
-
"@playwright/test": "^1.
|
|
60
|
+
"@playwright/test": "^1.62.1",
|
|
61
61
|
"@sveltejs/adapter-auto": "^7.0.1",
|
|
62
|
-
"@sveltejs/kit": "^2.
|
|
62
|
+
"@sveltejs/kit": "^2.70.3",
|
|
63
63
|
"@sveltejs/package": "^2.5.8",
|
|
64
|
-
"@sveltejs/vite-plugin-svelte": "^7.
|
|
65
|
-
"@testing-library/jest-dom": "^
|
|
64
|
+
"@sveltejs/vite-plugin-svelte": "^7.3.0",
|
|
65
|
+
"@testing-library/jest-dom": "^7.0.1",
|
|
66
66
|
"@testing-library/svelte": "^5.4.2",
|
|
67
|
-
"@testing-library/user-event": "^14.6.
|
|
68
|
-
"@types/node": "^26.
|
|
69
|
-
"@typescript-eslint/eslint-plugin": "^8.
|
|
70
|
-
"@typescript-eslint/parser": "^8.
|
|
71
|
-
"@vitest/coverage-v8": "^4.1.
|
|
72
|
-
"eslint": "^10.
|
|
67
|
+
"@testing-library/user-event": "^14.6.5",
|
|
68
|
+
"@types/node": "^26.2.0",
|
|
69
|
+
"@typescript-eslint/eslint-plugin": "^8.67.0",
|
|
70
|
+
"@typescript-eslint/parser": "^8.67.0",
|
|
71
|
+
"@vitest/coverage-v8": "^4.1.11",
|
|
72
|
+
"eslint": "^10.8.1",
|
|
73
73
|
"eslint-config-prettier": "^10.1.8",
|
|
74
74
|
"eslint-plugin-import": "^2.32.0",
|
|
75
|
-
"eslint-plugin-svelte": "^3.
|
|
75
|
+
"eslint-plugin-svelte": "^3.23.0",
|
|
76
76
|
"eslint-plugin-unused-imports": "^4.4.1",
|
|
77
|
-
"globals": "^17.
|
|
77
|
+
"globals": "^17.11.0",
|
|
78
78
|
"husky": "^9.1.7",
|
|
79
|
-
"jsdom": "^
|
|
79
|
+
"jsdom": "^30.0.1",
|
|
80
80
|
"mprocs": "^0.9.6",
|
|
81
|
-
"prettier": "^3.9.
|
|
81
|
+
"prettier": "^3.9.6",
|
|
82
82
|
"prettier-plugin-organize-imports": "^4.3.0",
|
|
83
83
|
"prettier-plugin-svelte": "^4.1.1",
|
|
84
|
-
"publint": "^0.3.
|
|
85
|
-
"svelte": "^5.56.
|
|
86
|
-
"svelte-check": "^4.7.
|
|
84
|
+
"publint": "^0.3.24",
|
|
85
|
+
"svelte": "^5.56.10",
|
|
86
|
+
"svelte-check": "^4.7.6",
|
|
87
87
|
"typescript": "^6.0.3",
|
|
88
|
-
"typescript-eslint": "^8.
|
|
89
|
-
"vite": "^8.
|
|
90
|
-
"vitest": "^4.1.
|
|
88
|
+
"typescript-eslint": "^8.67.0",
|
|
89
|
+
"vite": "^8.2.2",
|
|
90
|
+
"vitest": "^4.1.11"
|
|
91
91
|
},
|
|
92
92
|
"peerDependencies": {
|
|
93
93
|
"svelte": "^5.0.0"
|
package/dist/EmptyObject.svelte
DELETED
|
@@ -1,25 +0,0 @@
|
|
|
1
|
-
<script lang="ts">
|
|
2
|
-
import type { EmptyRenderProps } from './types.js'
|
|
3
|
-
import { quoteString } from './utils/quoteString.js'
|
|
4
|
-
|
|
5
|
-
const { field, openBracket, closeBracket, lastElement, style }: EmptyRenderProps = $props()
|
|
6
|
-
const hasField = $derived(field !== undefined)
|
|
7
|
-
const labelText = $derived(quoteString(field ?? '', style.quotesForFieldNames))
|
|
8
|
-
</script>
|
|
9
|
-
|
|
10
|
-
<!--
|
|
11
|
-
Label + brackets collapsed onto one prettier-ignored line because Svelte
|
|
12
|
-
preserves template whitespace between adjacent elements as visible spaces
|
|
13
|
-
under the container's `white-space: pre-wrap`. React/JSX strips that
|
|
14
|
-
whitespace natively; we hand-collapse it. Brackets and trailing comma also
|
|
15
|
-
live in a single punctuation span so the `.punctuation-base + .punctuation-base`
|
|
16
|
-
adjacent-sibling margin rule isn't needed here.
|
|
17
|
-
-->
|
|
18
|
-
<!-- Upstream parity: unselected treeitems omit aria-selected entirely. -->
|
|
19
|
-
<!-- svelte-ignore a11y_role_has_required_aria_props -->
|
|
20
|
-
<div class={style.basicChildStyle} role="treeitem">
|
|
21
|
-
<!-- prettier-ignore -->
|
|
22
|
-
{#if hasField}<span class={style.label}>{labelText}:</span>{/if}<span class={style.punctuation}
|
|
23
|
-
>{openBracket}{closeBracket}{lastElement ? '' : ','}</span
|
|
24
|
-
>
|
|
25
|
-
</div>
|