@humanspeak/svelte-json-view-lite 0.1.5 → 0.2.1
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 +2 -0
- package/dist/EmptyObject.svelte +3 -1
- package/dist/ExpandableObject.svelte +6 -12
- package/dist/JsonPrimitiveValue.svelte +103 -94
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -259,6 +259,7 @@ pnpm build # vite build + svelte-package + publint
|
|
|
259
259
|
|
|
260
260
|
Part of the [Humanspeak](https://humanspeak.com) family of runes-native Svelte 5 packages:
|
|
261
261
|
|
|
262
|
+
<!-- prettier-ignore-start -->
|
|
262
263
|
| Package | Description |
|
|
263
264
|
| --- | --- |
|
|
264
265
|
| [@humanspeak/svelte-markdown](https://markdown.svelte.page) | Runtime markdown renderer for Svelte |
|
|
@@ -271,6 +272,7 @@ Part of the [Humanspeak](https://humanspeak.com) family of runes-native Svelte 5
|
|
|
271
272
|
| [@humanspeak/memory-cache](https://memory.svelte.page) | In-memory cache for TypeScript |
|
|
272
273
|
| **[@humanspeak/svelte-json-view-lite](https://jsonview.svelte.page)** — _this package_ | JSON tree viewer for Svelte 5 |
|
|
273
274
|
| [@humanspeak/svelte-scoped-props](https://scoped.svelte.page) | Scoped class props for Svelte |
|
|
275
|
+
<!-- prettier-ignore-end -->
|
|
274
276
|
|
|
275
277
|
## License
|
|
276
278
|
|
package/dist/EmptyObject.svelte
CHANGED
|
@@ -15,7 +15,9 @@
|
|
|
15
15
|
live in a single punctuation span so the `.punctuation-base + .punctuation-base`
|
|
16
16
|
adjacent-sibling margin rule isn't needed here.
|
|
17
17
|
-->
|
|
18
|
-
|
|
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">
|
|
19
21
|
<!-- prettier-ignore -->
|
|
20
22
|
{#if hasField}<span class={style.label}>{labelText}:</span>{/if}<span class={style.punctuation}
|
|
21
23
|
>{openBracket}{closeBracket}{lastElement ? '' : ','}</span
|
|
@@ -78,8 +78,6 @@
|
|
|
78
78
|
const ariaLabel = $derived(
|
|
79
79
|
expanded ? activeAriaLabels.collapseJson : activeAriaLabels.expandJson
|
|
80
80
|
)
|
|
81
|
-
const childLevel = $derived(level + 1)
|
|
82
|
-
const hasField = $derived(field !== undefined)
|
|
83
81
|
const labelText = $derived(quoteString(field ?? '', style.quotesForFieldNames))
|
|
84
82
|
|
|
85
83
|
// Object keys resolved once and shared by the count and the tuple builder,
|
|
@@ -90,7 +88,6 @@
|
|
|
90
88
|
// Child *count* is cheap — array length, or the key count for objects.
|
|
91
89
|
// Neither touches child *values*, so a collapsed node stays allocation-free.
|
|
92
90
|
const count = $derived(objectKeys ? objectKeys.length : (value as unknown[]).length)
|
|
93
|
-
const lastIndex = $derived(count - 1)
|
|
94
91
|
|
|
95
92
|
// The tuple array is materialized lazily on first open, then kept: a node
|
|
96
93
|
// that is never expanded never allocates its N tuples or reads its N child
|
|
@@ -133,12 +130,9 @@
|
|
|
133
130
|
{#if count === 0}
|
|
134
131
|
<EmptyObject {field} {openBracket} {closeBracket} {lastElement} {style} />
|
|
135
132
|
{:else}
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
aria-expanded={expanded}
|
|
140
|
-
aria-selected={false}
|
|
141
|
-
>
|
|
133
|
+
<!-- Upstream parity: unselected treeitems omit aria-selected entirely. -->
|
|
134
|
+
<!-- svelte-ignore a11y_role_has_required_aria_props -->
|
|
135
|
+
<div class={style.basicChildStyle} role="treeitem" aria-expanded={expanded}>
|
|
142
136
|
<!--
|
|
143
137
|
The entire inline sequence inside a row lives on a single
|
|
144
138
|
prettier-ignored line because Svelte preserves template whitespace
|
|
@@ -150,7 +144,7 @@
|
|
|
150
144
|
it tight anyway for a consistent rule.
|
|
151
145
|
-->
|
|
152
146
|
<!-- prettier-ignore -->
|
|
153
|
-
<span bind:this={expanderButton} use:registerExpander 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
|
|
147
|
+
<span bind:this={expanderButton} use:registerExpander 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(
|
|
154
148
|
{ field: field ?? '', level }
|
|
155
149
|
)}{:else if clickToExpandNode}<!-- svelte-ignore a11y_no_static_element_interactions --><span
|
|
156
150
|
class={style.clickableLabel}
|
|
@@ -163,8 +157,8 @@
|
|
|
163
157
|
field={childField}
|
|
164
158
|
value={childValue}
|
|
165
159
|
{style}
|
|
166
|
-
lastElement={index ===
|
|
167
|
-
level={
|
|
160
|
+
lastElement={index === count - 1}
|
|
161
|
+
level={level + 1}
|
|
168
162
|
{shouldExpandNode}
|
|
169
163
|
{clickToExpandNode}
|
|
170
164
|
{beforeExpandChange}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
|
-
import type { JsonRenderProps } from './types.js'
|
|
2
|
+
import type { JsonRenderProps, SnippetOverrides } from './types.js'
|
|
3
3
|
import {
|
|
4
4
|
isBigInt,
|
|
5
5
|
isBoolean,
|
|
@@ -15,92 +15,99 @@
|
|
|
15
15
|
const { field, value, style, lastElement, level, snippets }: JsonRenderProps<Primitive> =
|
|
16
16
|
$props()
|
|
17
17
|
|
|
18
|
-
const hasField = $derived(field !== undefined)
|
|
19
18
|
const labelText = $derived(quoteString(field ?? '', style.quotesForFieldNames))
|
|
20
19
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
| '
|
|
26
|
-
| '
|
|
27
|
-
| '
|
|
28
|
-
| '
|
|
29
|
-
| '
|
|
30
|
-
| '
|
|
20
|
+
// `text`/`valueStyle` are common to every kind; only the discriminant and
|
|
21
|
+
// its snippet type vary. The intersection keeps `primitive.kind === 'x'`
|
|
22
|
+
// narrowing `primitive.snippet` to the one correct Snippet in the template.
|
|
23
|
+
type PrimitiveRender = { text: string; valueStyle: string } & (
|
|
24
|
+
| { kind: 'null'; snippet: SnippetOverrides['null'] }
|
|
25
|
+
| { kind: 'undefined'; snippet: SnippetOverrides['undefined'] }
|
|
26
|
+
| { kind: 'string'; snippet: SnippetOverrides['string'] }
|
|
27
|
+
| { kind: 'boolean'; snippet: SnippetOverrides['boolean'] }
|
|
28
|
+
| { kind: 'number'; snippet: SnippetOverrides['number'] }
|
|
29
|
+
| { kind: 'bigint'; snippet: SnippetOverrides['bigint'] }
|
|
30
|
+
| { kind: 'date'; snippet: SnippetOverrides['date'] }
|
|
31
|
+
| { kind: 'function'; snippet: SnippetOverrides['function'] }
|
|
32
|
+
| { kind: 'other'; snippet: undefined }
|
|
33
|
+
)
|
|
31
34
|
|
|
32
|
-
//
|
|
33
|
-
//
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
if (isDate(value)) return 'date'
|
|
43
|
-
if (isFunction(value)) return 'function'
|
|
44
|
-
return 'other'
|
|
45
|
-
})
|
|
46
|
-
|
|
47
|
-
type Rendered = { text: string; valueStyle: string }
|
|
48
|
-
const rendered = $derived.by<Rendered>(() => {
|
|
49
|
-
switch (kind) {
|
|
50
|
-
case 'null':
|
|
51
|
-
return { text: 'null', valueStyle: style.nullValue }
|
|
52
|
-
case 'undefined':
|
|
53
|
-
return { text: 'undefined', valueStyle: style.undefinedValue }
|
|
54
|
-
case 'string':
|
|
55
|
-
return {
|
|
56
|
-
text: quoteStringValue(
|
|
57
|
-
value as string,
|
|
58
|
-
!style.noQuotesForStringValues,
|
|
59
|
-
style.stringifyStringValues
|
|
60
|
-
),
|
|
61
|
-
valueStyle: style.stringValue
|
|
62
|
-
}
|
|
63
|
-
case 'boolean':
|
|
64
|
-
return {
|
|
65
|
-
text: (value as boolean) ? 'true' : 'false',
|
|
66
|
-
valueStyle: style.booleanValue
|
|
67
|
-
}
|
|
68
|
-
case 'number':
|
|
69
|
-
return { text: String(value), valueStyle: style.numberValue }
|
|
70
|
-
case 'bigint':
|
|
71
|
-
return {
|
|
72
|
-
text: `${(value as bigint).toString()}n`,
|
|
73
|
-
valueStyle: style.numberValue
|
|
74
|
-
}
|
|
75
|
-
case 'date':
|
|
76
|
-
return { text: (value as Date).toISOString(), valueStyle: style.otherValue }
|
|
77
|
-
case 'function':
|
|
78
|
-
return { text: 'function() { }', valueStyle: style.otherValue }
|
|
79
|
-
default:
|
|
80
|
-
return { text: String(value), valueStyle: style.otherValue }
|
|
35
|
+
// One predicate walk returns everything the row needs, avoiding three
|
|
36
|
+
// separate reactions for the same primitive classification.
|
|
37
|
+
const primitive = $derived.by<PrimitiveRender>(() => {
|
|
38
|
+
if (value === null) {
|
|
39
|
+
return {
|
|
40
|
+
kind: 'null',
|
|
41
|
+
text: 'null',
|
|
42
|
+
valueStyle: style.nullValue,
|
|
43
|
+
snippet: snippets.null
|
|
44
|
+
}
|
|
81
45
|
}
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
46
|
+
if (value === undefined) {
|
|
47
|
+
return {
|
|
48
|
+
kind: 'undefined',
|
|
49
|
+
text: 'undefined',
|
|
50
|
+
valueStyle: style.undefinedValue,
|
|
51
|
+
snippet: snippets.undefined
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
if (isString(value)) {
|
|
55
|
+
return {
|
|
56
|
+
kind: 'string',
|
|
57
|
+
text: quoteStringValue(
|
|
58
|
+
value,
|
|
59
|
+
!style.noQuotesForStringValues,
|
|
60
|
+
style.stringifyStringValues
|
|
61
|
+
),
|
|
62
|
+
valueStyle: style.stringValue,
|
|
63
|
+
snippet: snippets.string
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
if (isBoolean(value)) {
|
|
67
|
+
return {
|
|
68
|
+
kind: 'boolean',
|
|
69
|
+
text: value ? 'true' : 'false',
|
|
70
|
+
valueStyle: style.booleanValue,
|
|
71
|
+
snippet: snippets.boolean
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
if (isNumber(value)) {
|
|
75
|
+
return {
|
|
76
|
+
kind: 'number',
|
|
77
|
+
text: String(value),
|
|
78
|
+
valueStyle: style.numberValue,
|
|
79
|
+
snippet: snippets.number
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
if (isBigInt(value)) {
|
|
83
|
+
return {
|
|
84
|
+
kind: 'bigint',
|
|
85
|
+
text: `${value.toString()}n`,
|
|
86
|
+
valueStyle: style.numberValue,
|
|
87
|
+
snippet: snippets.bigint
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
if (isDate(value)) {
|
|
91
|
+
return {
|
|
92
|
+
kind: 'date',
|
|
93
|
+
text: value.toISOString(),
|
|
94
|
+
valueStyle: style.otherValue,
|
|
95
|
+
snippet: snippets.date
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
if (isFunction(value)) {
|
|
99
|
+
return {
|
|
100
|
+
kind: 'function',
|
|
101
|
+
text: 'function() { }',
|
|
102
|
+
valueStyle: style.otherValue,
|
|
103
|
+
snippet: snippets.function
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
return {
|
|
107
|
+
kind: 'other',
|
|
108
|
+
text: String(value),
|
|
109
|
+
valueStyle: style.otherValue,
|
|
110
|
+
snippet: undefined
|
|
104
111
|
}
|
|
105
112
|
})
|
|
106
113
|
</script>
|
|
@@ -110,41 +117,43 @@
|
|
|
110
117
|
preserves template whitespace between adjacent elements as visible spaces
|
|
111
118
|
under the container's `white-space: pre-wrap`. React/JSX strips that
|
|
112
119
|
whitespace natively; we hand-collapse it. Per-type snippet branches key
|
|
113
|
-
off
|
|
120
|
+
off the merged primitive render model rather than re-running predicates.
|
|
114
121
|
-->
|
|
115
|
-
|
|
122
|
+
<!-- Upstream parity: unselected treeitems omit aria-selected entirely. -->
|
|
123
|
+
<!-- svelte-ignore a11y_role_has_required_aria_props -->
|
|
124
|
+
<div class={style.basicChildStyle} role="treeitem">
|
|
116
125
|
<!-- prettier-ignore -->
|
|
117
|
-
{#if
|
|
126
|
+
{#if field !== undefined}{#if snippets.label}{@render snippets.label({ field: field ?? '', level })}{:else}<span class={style.label}>{labelText}:</span>{/if}{/if}{#if primitive.snippet && primitive.kind === 'null'}{@render primitive.snippet(
|
|
118
127
|
{ value: null, field, level }
|
|
119
|
-
)}{:else if
|
|
128
|
+
)}{:else if primitive.snippet && primitive.kind === 'undefined'}{@render primitive.snippet({
|
|
120
129
|
value: undefined,
|
|
121
130
|
field,
|
|
122
131
|
level
|
|
123
|
-
})}{:else if
|
|
132
|
+
})}{:else if primitive.snippet && primitive.kind === 'string'}{@render primitive.snippet({
|
|
124
133
|
value: value as string,
|
|
125
134
|
field,
|
|
126
135
|
level
|
|
127
|
-
})}{:else if
|
|
136
|
+
})}{:else if primitive.snippet && primitive.kind === 'boolean'}{@render primitive.snippet({
|
|
128
137
|
value: value as boolean,
|
|
129
138
|
field,
|
|
130
139
|
level
|
|
131
|
-
})}{:else if
|
|
140
|
+
})}{:else if primitive.snippet && primitive.kind === 'number'}{@render primitive.snippet({
|
|
132
141
|
value: value as number,
|
|
133
142
|
field,
|
|
134
143
|
level
|
|
135
|
-
})}{:else if
|
|
144
|
+
})}{:else if primitive.snippet && primitive.kind === 'bigint'}{@render primitive.snippet({
|
|
136
145
|
value: value as bigint,
|
|
137
146
|
field,
|
|
138
147
|
level
|
|
139
|
-
})}{:else if
|
|
148
|
+
})}{:else if primitive.snippet && primitive.kind === 'date'}{@render primitive.snippet({
|
|
140
149
|
value: value as Date,
|
|
141
150
|
field,
|
|
142
151
|
level
|
|
143
|
-
})}{:else if
|
|
152
|
+
})}{:else if primitive.snippet && primitive.kind === 'function'}{@render primitive.snippet({
|
|
144
153
|
// trunk-ignore(eslint/@typescript-eslint/no-unsafe-function-type)
|
|
145
154
|
value: value as Function,
|
|
146
155
|
field,
|
|
147
156
|
level
|
|
148
|
-
})}{:else}<span class={
|
|
157
|
+
})}{:else}<span class={primitive.valueStyle}>{primitive.text}</span
|
|
149
158
|
>{/if}{#if !lastElement}<span class={style.punctuation}>,</span>{/if}
|
|
150
159
|
</div>
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@humanspeak/svelte-json-view-lite",
|
|
3
|
-
"version": "0.1
|
|
3
|
+
"version": "0.2.1",
|
|
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",
|