@humanspeak/svelte-json-view-lite 0.1.0
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 +24 -0
- package/README.md +187 -0
- package/dist/DataRender.svelte +51 -0
- package/dist/DataRender.svelte.d.ts +4 -0
- package/dist/EmptyObject.svelte +23 -0
- package/dist/EmptyObject.svelte.d.ts +4 -0
- package/dist/ExpandableObject.svelte +157 -0
- package/dist/ExpandableObject.svelte.d.ts +4 -0
- package/dist/JsonPrimitiveValue.svelte +99 -0
- package/dist/JsonPrimitiveValue.svelte.d.ts +4 -0
- package/dist/JsonView.svelte +106 -0
- package/dist/JsonView.svelte.d.ts +4 -0
- package/dist/index.d.ts +14 -0
- package/dist/index.js +38 -0
- package/dist/styles.module.css +184 -0
- package/dist/types.d.ts +145 -0
- package/dist/types.js +1 -0
- package/dist/utils/dataTypeDetection.d.ts +10 -0
- package/dist/utils/dataTypeDetection.js +31 -0
- package/dist/utils/expandStrategies.d.ts +4 -0
- package/dist/utils/expandStrategies.js +4 -0
- package/dist/utils/quoteString.d.ts +13 -0
- package/dist/utils/quoteString.js +20 -0
- package/package.json +127 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024-2026 Humanspeak, Inc.
|
|
4
|
+
|
|
5
|
+
Copyright (c) 2024 AnyRoad (original react-json-view-lite, https://github.com/AnyRoad/react-json-view-lite)
|
|
6
|
+
|
|
7
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
|
8
|
+
a copy of this software and associated documentation files (the
|
|
9
|
+
"Software"), to deal in the Software without restriction, including
|
|
10
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
|
11
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
|
12
|
+
permit persons to whom the Software is furnished to do so, subject to
|
|
13
|
+
the following conditions:
|
|
14
|
+
|
|
15
|
+
The above copyright notice and this permission notice shall be
|
|
16
|
+
included in all copies or substantial portions of the Software.
|
|
17
|
+
|
|
18
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
|
19
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
|
20
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
|
21
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
|
22
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
|
23
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
|
24
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
# @humanspeak/svelte-json-view-lite
|
|
2
|
+
|
|
3
|
+
Fast, tiny JSON tree viewer for Svelte 5 — a port of
|
|
4
|
+
[react-json-view-lite](https://github.com/AnyRoad/react-json-view-lite)
|
|
5
|
+
(MIT © 2024 AnyRoad) with runes, SSR, per-type snippet overrides, and zero
|
|
6
|
+
runtime dependencies.
|
|
7
|
+
|
|
8
|
+
## Features
|
|
9
|
+
|
|
10
|
+
- Svelte 5 runes throughout (`$props`, `$state`, `$derived`, `$effect`).
|
|
11
|
+
- Drop-in API parity with `react-json-view-lite`: same prop names, themes,
|
|
12
|
+
and strategy helpers.
|
|
13
|
+
- Per-type `Snippet` overrides for custom value rendering (strings, numbers,
|
|
14
|
+
dates, etc.) — new in the Svelte port.
|
|
15
|
+
- SSR-safe: uses `$props.id()` for stable `aria-controls` linkage across
|
|
16
|
+
server/client.
|
|
17
|
+
- Keyboard accessible: roving tabindex + `ArrowUp`/`ArrowDown`/`ArrowLeft`/
|
|
18
|
+
`ArrowRight` navigation, full WAI-ARIA treeview semantics.
|
|
19
|
+
- Ships built-in light and dark themes via CSS Modules.
|
|
20
|
+
|
|
21
|
+
## Installation
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
npm i -S @humanspeak/svelte-json-view-lite
|
|
25
|
+
# or
|
|
26
|
+
pnpm add @humanspeak/svelte-json-view-lite
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## Basic usage
|
|
30
|
+
|
|
31
|
+
```svelte
|
|
32
|
+
<script lang="ts">
|
|
33
|
+
import { JsonView, defaultStyles } from '@humanspeak/svelte-json-view-lite'
|
|
34
|
+
|
|
35
|
+
const data = {
|
|
36
|
+
name: 'Ada Lovelace',
|
|
37
|
+
tags: ['admin', 'beta'],
|
|
38
|
+
active: true,
|
|
39
|
+
joined: new Date('2024-01-15')
|
|
40
|
+
}
|
|
41
|
+
</script>
|
|
42
|
+
|
|
43
|
+
<JsonView {data} style={defaultStyles} />
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
## Props
|
|
47
|
+
|
|
48
|
+
| Prop | Type | Default | Description |
|
|
49
|
+
| ----------------------- | ---------------------------------------- | --------------- | ---------------------------------------------------------------------------------- |
|
|
50
|
+
| `data` | `object \| unknown[]` | — | The JSON-shaped value to render. |
|
|
51
|
+
| `style` | `Partial<StyleProps>` | `defaultStyles` | Classname-map that themes every slot. |
|
|
52
|
+
| `shouldExpandNode` | `(level, value, field?) => boolean` | `allExpanded` | Initial-expand strategy per node. |
|
|
53
|
+
| `clickToExpandNode` | `boolean` | `false` | When true, clicking the field label also toggles the node. |
|
|
54
|
+
| `beforeExpandChange` | `(event: NodeExpandingEvent) => boolean` | — | Return `false` to veto an expand/collapse transition. |
|
|
55
|
+
| `compactTopLevel` | `boolean` | `false` | Spread root-object entries instead of nesting them under a single root expander. |
|
|
56
|
+
| `string`, `number`, ... | `Snippet<[{ value, field?, level }]>` | — | Optional per-type renderer overrides. See [Snippet overrides](#snippet-overrides). |
|
|
57
|
+
|
|
58
|
+
Any additional HTML attributes (`aria-*`, `data-*`, `id`, `class`, etc.) are
|
|
59
|
+
forwarded onto the root `<div role="tree">`.
|
|
60
|
+
|
|
61
|
+
## Themes
|
|
62
|
+
|
|
63
|
+
Two themes ship out of the box:
|
|
64
|
+
|
|
65
|
+
```svelte
|
|
66
|
+
<script lang="ts">
|
|
67
|
+
import { JsonView, defaultStyles, darkStyles } from '@humanspeak/svelte-json-view-lite'
|
|
68
|
+
</script>
|
|
69
|
+
|
|
70
|
+
<JsonView data={json} style={darkStyles} />
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
Override individual slots by spreading:
|
|
74
|
+
|
|
75
|
+
```svelte
|
|
76
|
+
<JsonView
|
|
77
|
+
{data}
|
|
78
|
+
style={{
|
|
79
|
+
...defaultStyles,
|
|
80
|
+
stringValue: 'my-custom-string-class',
|
|
81
|
+
punctuation: 'my-custom-punctuation-class'
|
|
82
|
+
}}
|
|
83
|
+
/>
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
## Snippet overrides
|
|
87
|
+
|
|
88
|
+
Unlike the React lib, `svelte-json-view-lite` exposes typed `Snippet`
|
|
89
|
+
overrides for every primitive type plus the field label. Each snippet
|
|
90
|
+
receives `{ value, field?, level }`; omit the snippet to fall through to
|
|
91
|
+
the default rendering.
|
|
92
|
+
|
|
93
|
+
```svelte
|
|
94
|
+
<script lang="ts">
|
|
95
|
+
import {
|
|
96
|
+
JsonView,
|
|
97
|
+
type DateSnippetProps,
|
|
98
|
+
type StringSnippetProps
|
|
99
|
+
} from '@humanspeak/svelte-json-view-lite'
|
|
100
|
+
|
|
101
|
+
const data = {
|
|
102
|
+
joined: new Date('2024-01-15'),
|
|
103
|
+
docs: 'https://example.com/docs'
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function relative(d: Date): string {
|
|
107
|
+
const days = Math.round((d.getTime() - Date.now()) / 86_400_000)
|
|
108
|
+
return new Intl.RelativeTimeFormat('en').format(days, 'day')
|
|
109
|
+
}
|
|
110
|
+
</script>
|
|
111
|
+
|
|
112
|
+
<JsonView {data}>
|
|
113
|
+
{#snippet date({ value }: DateSnippetProps)}
|
|
114
|
+
<span title={value.toISOString()}>{relative(value)}</span>
|
|
115
|
+
{/snippet}
|
|
116
|
+
{#snippet string({ value }: StringSnippetProps)}
|
|
117
|
+
{#if /^https?:/.test(value)}
|
|
118
|
+
<a href={value}>{value}</a>
|
|
119
|
+
{:else}
|
|
120
|
+
"{value}"
|
|
121
|
+
{/if}
|
|
122
|
+
{/snippet}
|
|
123
|
+
</JsonView>
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
Available snippets: `string`, `number`, `boolean`, `null`, `undefined`,
|
|
127
|
+
`bigint`, `date`, `function`, `label`. Their typed prop interfaces
|
|
128
|
+
(`StringSnippetProps`, `LabelSnippetProps`, etc.) are all exported from
|
|
129
|
+
the package root.
|
|
130
|
+
|
|
131
|
+
## Expand strategies
|
|
132
|
+
|
|
133
|
+
```svelte
|
|
134
|
+
<script lang="ts">
|
|
135
|
+
import { JsonView, collapseAllNested, allExpanded } from '@humanspeak/svelte-json-view-lite'
|
|
136
|
+
</script>
|
|
137
|
+
|
|
138
|
+
<!-- expand only the root, collapse every child -->
|
|
139
|
+
<JsonView {data} shouldExpandNode={collapseAllNested} />
|
|
140
|
+
|
|
141
|
+
<!-- expand everything (default) -->
|
|
142
|
+
<JsonView {data} shouldExpandNode={allExpanded} />
|
|
143
|
+
|
|
144
|
+
<!-- custom: expand up to depth 2 -->
|
|
145
|
+
<JsonView {data} shouldExpandNode={(level) => level < 2} />
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
## Migrating from `react-json-view-lite`
|
|
149
|
+
|
|
150
|
+
The Svelte API preserves the React prop names and theme-object shape. Only
|
|
151
|
+
two differences exist:
|
|
152
|
+
|
|
153
|
+
| React | Svelte |
|
|
154
|
+
| --------------------------- | ---------------------------------------------------------- |
|
|
155
|
+
| `style.ariaLables` (typoed) | `style.ariaLabels` (fixed; typo still honored with a warn) |
|
|
156
|
+
| — (not supported) | Per-type `Snippet` overrides |
|
|
157
|
+
|
|
158
|
+
Everything else — `style`, `clickToExpandNode`, `compactTopLevel`,
|
|
159
|
+
`beforeExpandChange`, `shouldExpandNode`, `defaultStyles`, `darkStyles`,
|
|
160
|
+
`allExpanded`, `collapseAllNested` — works identically.
|
|
161
|
+
|
|
162
|
+
## Accessibility
|
|
163
|
+
|
|
164
|
+
- Root element has `role="tree"` with `aria-label="JSON view"` (overridable).
|
|
165
|
+
- Every expandable node is a `role="treeitem"` with live `aria-expanded`
|
|
166
|
+
and, when open, `aria-controls` pointing at its child `<ul role="group">`.
|
|
167
|
+
- Keyboard support follows the [WAI-ARIA 1.2 Treeview pattern]:
|
|
168
|
+
- `ArrowRight` expands, `ArrowLeft` collapses.
|
|
169
|
+
- `ArrowDown` / `ArrowUp` move focus between expanders (wrapping).
|
|
170
|
+
- Roving `tabindex` keeps only one expander in the tab order.
|
|
171
|
+
|
|
172
|
+
[WAI-ARIA 1.2 Treeview pattern]: https://www.w3.org/WAI/ARIA/apg/patterns/treeview/
|
|
173
|
+
|
|
174
|
+
## Development
|
|
175
|
+
|
|
176
|
+
```bash
|
|
177
|
+
pnpm install
|
|
178
|
+
pnpm dev # launch SvelteKit playground on :8233
|
|
179
|
+
pnpm check # svelte-check
|
|
180
|
+
pnpm test # vitest + coverage
|
|
181
|
+
pnpm build # vite build + svelte-package + publint
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
## License
|
|
185
|
+
|
|
186
|
+
MIT. Copyright © 2024-2026 Humanspeak, Inc. Original
|
|
187
|
+
`react-json-view-lite` © 2024 AnyRoad.
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import ExpandableObject from './ExpandableObject.svelte'
|
|
3
|
+
import JsonPrimitiveValue from './JsonPrimitiveValue.svelte'
|
|
4
|
+
import type { JsonRenderProps } from './types.js'
|
|
5
|
+
import { isArray, isDate, isFunction, isObject } from './utils/dataTypeDetection.js'
|
|
6
|
+
|
|
7
|
+
// Declared as constants because Svelte markup parses `"{"` as an
|
|
8
|
+
// expression opener, and eslint's no-useless-mustaches objects to
|
|
9
|
+
// `{'{'}` inline.
|
|
10
|
+
const OBJECT_OPEN = '{'
|
|
11
|
+
const OBJECT_CLOSE = '}'
|
|
12
|
+
|
|
13
|
+
const props: JsonRenderProps<unknown> = $props()
|
|
14
|
+
const value = $derived(props.value)
|
|
15
|
+
|
|
16
|
+
// Array check first — arrays are also objects, so order matters.
|
|
17
|
+
const isArr = $derived(isArray(value))
|
|
18
|
+
const isObj = $derived(isObject(value) && !isDate(value) && !isFunction(value))
|
|
19
|
+
</script>
|
|
20
|
+
|
|
21
|
+
{#if isArr}
|
|
22
|
+
<ExpandableObject
|
|
23
|
+
{...props}
|
|
24
|
+
value={value as unknown[]}
|
|
25
|
+
data={(value as unknown[]).map((el) => [undefined, el])}
|
|
26
|
+
openBracket="["
|
|
27
|
+
closeBracket="]"
|
|
28
|
+
/>
|
|
29
|
+
{:else if isObj}
|
|
30
|
+
<ExpandableObject
|
|
31
|
+
{...props}
|
|
32
|
+
value={value as object}
|
|
33
|
+
data={Object.keys(value as object).map((k) => [k, (value as Record<string, unknown>)[k]])}
|
|
34
|
+
openBracket={OBJECT_OPEN}
|
|
35
|
+
closeBracket={OBJECT_CLOSE}
|
|
36
|
+
/>
|
|
37
|
+
{:else}
|
|
38
|
+
<JsonPrimitiveValue
|
|
39
|
+
{...props}
|
|
40
|
+
value={value as
|
|
41
|
+
| string
|
|
42
|
+
| number
|
|
43
|
+
| boolean
|
|
44
|
+
| bigint
|
|
45
|
+
| Date
|
|
46
|
+
// trunk-ignore(eslint/@typescript-eslint/no-unsafe-function-type)
|
|
47
|
+
| Function
|
|
48
|
+
| null
|
|
49
|
+
| undefined}
|
|
50
|
+
/>
|
|
51
|
+
{/if}
|
|
@@ -0,0 +1,23 @@
|
|
|
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
|
+
<div class={style.basicChildStyle} role="treeitem" aria-selected={false}>
|
|
19
|
+
<!-- prettier-ignore -->
|
|
20
|
+
{#if hasField}<span class={style.label}>{labelText}:</span>{/if}<span class={style.punctuation}
|
|
21
|
+
>{openBracket}{closeBracket}{lastElement ? '' : ','}</span
|
|
22
|
+
>
|
|
23
|
+
</div>
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import DataRender from './DataRender.svelte'
|
|
3
|
+
import EmptyObject from './EmptyObject.svelte'
|
|
4
|
+
import type { AriaLabels, ExpandableRenderProps } from './types.js'
|
|
5
|
+
import { quoteString } from './utils/quoteString.js'
|
|
6
|
+
|
|
7
|
+
const {
|
|
8
|
+
field,
|
|
9
|
+
value,
|
|
10
|
+
data,
|
|
11
|
+
lastElement,
|
|
12
|
+
openBracket,
|
|
13
|
+
closeBracket,
|
|
14
|
+
level,
|
|
15
|
+
style,
|
|
16
|
+
shouldExpandNode,
|
|
17
|
+
clickToExpandNode,
|
|
18
|
+
outerRef,
|
|
19
|
+
beforeExpandChange,
|
|
20
|
+
snippets
|
|
21
|
+
}: ExpandableRenderProps = $props()
|
|
22
|
+
|
|
23
|
+
// Lazy init — runs once on mount (react useState(() => ...) equivalent).
|
|
24
|
+
// svelte-ignore state_referenced_locally
|
|
25
|
+
let expanded = $state(shouldExpandNode(level, value, field))
|
|
26
|
+
|
|
27
|
+
// React's useRef<boolean>: plain mutable local.
|
|
28
|
+
let shouldExpandNodeCalled = false
|
|
29
|
+
|
|
30
|
+
// Match React useEffect(fn, [shouldExpandNode]) — fire only when the
|
|
31
|
+
// callback identity changes. We intentionally avoid reading level/value/
|
|
32
|
+
// field so an ancestor re-render doesn't trigger a respectful collapse.
|
|
33
|
+
$effect(() => {
|
|
34
|
+
const fn = shouldExpandNode
|
|
35
|
+
if (!shouldExpandNodeCalled) {
|
|
36
|
+
shouldExpandNodeCalled = true
|
|
37
|
+
return
|
|
38
|
+
}
|
|
39
|
+
expanded = fn(level, value, field)
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
// SSR-stable unique id for aria-controls linkage (React.useId equivalent).
|
|
43
|
+
const contentsId = $props.id()
|
|
44
|
+
|
|
45
|
+
// bind:this target for focus management.
|
|
46
|
+
let expanderButton = $state<HTMLSpanElement | null>(null)
|
|
47
|
+
|
|
48
|
+
const activeAriaLabels = $derived<AriaLabels>(
|
|
49
|
+
style.ariaLabels ??
|
|
50
|
+
style.ariaLables ?? {
|
|
51
|
+
collapseJson: 'collapse JSON',
|
|
52
|
+
expandJson: 'expand JSON'
|
|
53
|
+
}
|
|
54
|
+
)
|
|
55
|
+
const expanderIconStyle = $derived(expanded ? style.collapseIcon : style.expandIcon)
|
|
56
|
+
const ariaLabel = $derived(
|
|
57
|
+
expanded ? activeAriaLabels.collapseJson : activeAriaLabels.expandJson
|
|
58
|
+
)
|
|
59
|
+
const childLevel = $derived(level + 1)
|
|
60
|
+
const lastIndex = $derived(data.length - 1)
|
|
61
|
+
const hasField = $derived(field !== undefined)
|
|
62
|
+
const labelText = $derived(quoteString(field ?? '', style.quotesForFieldNames))
|
|
63
|
+
|
|
64
|
+
function setExpandWithCallback(newExpandValue: boolean) {
|
|
65
|
+
if (expanded === newExpandValue) return
|
|
66
|
+
if (beforeExpandChange && !beforeExpandChange({ level, value, field, newExpandValue })) {
|
|
67
|
+
return
|
|
68
|
+
}
|
|
69
|
+
expanded = newExpandValue
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function onKeyDown(e: KeyboardEvent) {
|
|
73
|
+
if (e.key === 'ArrowRight' || e.key === 'ArrowLeft') {
|
|
74
|
+
e.preventDefault()
|
|
75
|
+
setExpandWithCallback(e.key === 'ArrowRight')
|
|
76
|
+
return
|
|
77
|
+
}
|
|
78
|
+
if (e.key !== 'ArrowUp' && e.key !== 'ArrowDown') return
|
|
79
|
+
e.preventDefault()
|
|
80
|
+
const direction = e.key === 'ArrowUp' ? -1 : 1
|
|
81
|
+
const outer = outerRef.current
|
|
82
|
+
if (!outer) return
|
|
83
|
+
const buttons = outer.querySelectorAll<HTMLElement>('[role=button]')
|
|
84
|
+
let currentIndex = -1
|
|
85
|
+
for (let i = 0; i < buttons.length; i++) {
|
|
86
|
+
if (buttons[i].tabIndex === 0) {
|
|
87
|
+
currentIndex = i
|
|
88
|
+
break
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
if (currentIndex < 0) return
|
|
92
|
+
const nextIndex = (currentIndex + direction + buttons.length) % buttons.length
|
|
93
|
+
buttons[currentIndex].tabIndex = -1
|
|
94
|
+
buttons[nextIndex].tabIndex = 0
|
|
95
|
+
buttons[nextIndex].focus()
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function onClick() {
|
|
99
|
+
setExpandWithCallback(!expanded)
|
|
100
|
+
if (!expanderButton) return
|
|
101
|
+
const prev = outerRef.current?.querySelector<HTMLElement>('[role=button][tabindex="0"]')
|
|
102
|
+
if (prev) prev.tabIndex = -1
|
|
103
|
+
expanderButton.tabIndex = 0
|
|
104
|
+
expanderButton.focus()
|
|
105
|
+
}
|
|
106
|
+
</script>
|
|
107
|
+
|
|
108
|
+
{#if data.length === 0}
|
|
109
|
+
<EmptyObject {field} {openBracket} {closeBracket} {lastElement} {style} />
|
|
110
|
+
{:else}
|
|
111
|
+
<div
|
|
112
|
+
class={style.basicChildStyle}
|
|
113
|
+
role="treeitem"
|
|
114
|
+
aria-expanded={expanded}
|
|
115
|
+
aria-selected={false}
|
|
116
|
+
>
|
|
117
|
+
<!--
|
|
118
|
+
The entire inline sequence inside a row lives on a single
|
|
119
|
+
prettier-ignored line because Svelte preserves template whitespace
|
|
120
|
+
between adjacent elements (expander→label→openBracket→children→
|
|
121
|
+
closeBracket) as visible spaces under the container's
|
|
122
|
+
`white-space: pre-wrap`. React/JSX strips that whitespace natively;
|
|
123
|
+
we have to hand-collapse it. The `<ul>` of children is block-level
|
|
124
|
+
so the whitespace around it is not layout-significant, but we keep
|
|
125
|
+
it tight anyway for a consistent rule.
|
|
126
|
+
-->
|
|
127
|
+
<!-- prettier-ignore -->
|
|
128
|
+
<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 hasField}{#if snippets.label}{@render snippets.label(
|
|
129
|
+
{ field: field ?? '', level }
|
|
130
|
+
)}{:else if clickToExpandNode}<!-- svelte-ignore a11y_no_static_element_interactions --><span
|
|
131
|
+
class={style.clickableLabel}
|
|
132
|
+
onclick={onClick}
|
|
133
|
+
onkeydown={onKeyDown}>{labelText}:</span
|
|
134
|
+
>{:else}<span class={style.label}>{labelText}:</span>{/if}{/if}<span
|
|
135
|
+
class={style.punctuation}>{openBracket}</span
|
|
136
|
+
>{#if expanded}<ul id={contentsId} role="group" class={style.childFieldsContainer}>
|
|
137
|
+
{#each data as [childField, childValue], index (childField ?? index)}<DataRender
|
|
138
|
+
field={childField}
|
|
139
|
+
value={childValue}
|
|
140
|
+
{style}
|
|
141
|
+
lastElement={index === lastIndex}
|
|
142
|
+
level={childLevel}
|
|
143
|
+
{shouldExpandNode}
|
|
144
|
+
{clickToExpandNode}
|
|
145
|
+
{beforeExpandChange}
|
|
146
|
+
{outerRef}
|
|
147
|
+
{snippets}
|
|
148
|
+
/>{/each}
|
|
149
|
+
</ul>{:else}<!-- svelte-ignore a11y_no_static_element_interactions --><span
|
|
150
|
+
class={style.collapsedContent}
|
|
151
|
+
onclick={onClick}
|
|
152
|
+
onkeydown={onKeyDown}
|
|
153
|
+
></span>{/if}<span class={style.punctuation}
|
|
154
|
+
>{closeBracket}{lastElement ? '' : ','}</span
|
|
155
|
+
>
|
|
156
|
+
</div>
|
|
157
|
+
{/if}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { JsonRenderProps } from './types.js'
|
|
3
|
+
import {
|
|
4
|
+
isBigInt,
|
|
5
|
+
isBoolean,
|
|
6
|
+
isDate,
|
|
7
|
+
isFunction,
|
|
8
|
+
isNumber,
|
|
9
|
+
isString
|
|
10
|
+
} from './utils/dataTypeDetection.js'
|
|
11
|
+
import { quoteString, quoteStringValue } from './utils/quoteString.js'
|
|
12
|
+
|
|
13
|
+
// trunk-ignore(eslint/@typescript-eslint/no-unsafe-function-type)
|
|
14
|
+
type Primitive = string | number | boolean | bigint | Date | Function | null | undefined
|
|
15
|
+
const { field, value, style, lastElement, level, snippets }: JsonRenderProps<Primitive> =
|
|
16
|
+
$props()
|
|
17
|
+
|
|
18
|
+
const hasField = $derived(field !== undefined)
|
|
19
|
+
const labelText = $derived(quoteString(field ?? '', style.quotesForFieldNames))
|
|
20
|
+
|
|
21
|
+
type Rendered = { text: string; valueStyle: string }
|
|
22
|
+
const rendered = $derived.by<Rendered>(() => {
|
|
23
|
+
if (value === null) return { text: 'null', valueStyle: style.nullValue }
|
|
24
|
+
if (value === undefined) return { text: 'undefined', valueStyle: style.undefinedValue }
|
|
25
|
+
if (isString(value))
|
|
26
|
+
return {
|
|
27
|
+
text: quoteStringValue(
|
|
28
|
+
value,
|
|
29
|
+
!style.noQuotesForStringValues,
|
|
30
|
+
style.stringifyStringValues
|
|
31
|
+
),
|
|
32
|
+
valueStyle: style.stringValue
|
|
33
|
+
}
|
|
34
|
+
if (isBoolean(value))
|
|
35
|
+
return { text: value ? 'true' : 'false', valueStyle: style.booleanValue }
|
|
36
|
+
if (isNumber(value)) return { text: value.toString(), valueStyle: style.numberValue }
|
|
37
|
+
if (isBigInt(value)) return { text: `${value.toString()}n`, valueStyle: style.numberValue }
|
|
38
|
+
if (isDate(value)) return { text: value.toISOString(), valueStyle: style.otherValue }
|
|
39
|
+
if (isFunction(value)) return { text: 'function() { }', valueStyle: style.otherValue }
|
|
40
|
+
return { text: String(value), valueStyle: style.otherValue }
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
const snippetForValue = $derived.by(() => {
|
|
44
|
+
if (value === null) return snippets.null
|
|
45
|
+
if (value === undefined) return snippets.undefined
|
|
46
|
+
if (isString(value)) return snippets.string
|
|
47
|
+
if (isBoolean(value)) return snippets.boolean
|
|
48
|
+
if (isNumber(value)) return snippets.number
|
|
49
|
+
if (isBigInt(value)) return snippets.bigint
|
|
50
|
+
if (isDate(value)) return snippets.date
|
|
51
|
+
if (isFunction(value)) return snippets.function
|
|
52
|
+
return undefined
|
|
53
|
+
})
|
|
54
|
+
</script>
|
|
55
|
+
|
|
56
|
+
<!--
|
|
57
|
+
The entire row body lives on a single prettier-ignored line because Svelte
|
|
58
|
+
preserves template whitespace between adjacent elements (e.g. between the
|
|
59
|
+
label span and the value span, or between the value and the trailing
|
|
60
|
+
comma). Those whitespace text nodes render as visible spaces under the
|
|
61
|
+
container's `white-space: pre-wrap`. React/JSX strips that whitespace
|
|
62
|
+
natively; we have to hand-collapse it. Every margin we need is provided
|
|
63
|
+
by the CSS module (e.g. `.label { margin-right: 5px; }`).
|
|
64
|
+
-->
|
|
65
|
+
<div class={style.basicChildStyle} role="treeitem" aria-selected={false}>
|
|
66
|
+
<!-- prettier-ignore -->
|
|
67
|
+
{#if hasField}{#if snippets.label}{@render snippets.label({ field: field ?? '', level })}{:else}<span class={style.label}>{labelText}:</span>{/if}{/if}{#if snippetForValue && value === null}{@render snippets.null?.(
|
|
68
|
+
{ value: null, field, level }
|
|
69
|
+
)}{:else if snippetForValue && value === undefined}{@render snippets.undefined?.({
|
|
70
|
+
value: undefined,
|
|
71
|
+
field,
|
|
72
|
+
level
|
|
73
|
+
})}{:else if snippetForValue && isString(value)}{@render snippets.string?.({
|
|
74
|
+
value,
|
|
75
|
+
field,
|
|
76
|
+
level
|
|
77
|
+
})}{:else if snippetForValue && isBoolean(value)}{@render snippets.boolean?.({
|
|
78
|
+
value,
|
|
79
|
+
field,
|
|
80
|
+
level
|
|
81
|
+
})}{:else if snippetForValue && isNumber(value)}{@render snippets.number?.({
|
|
82
|
+
value,
|
|
83
|
+
field,
|
|
84
|
+
level
|
|
85
|
+
})}{:else if snippetForValue && isBigInt(value)}{@render snippets.bigint?.({
|
|
86
|
+
value,
|
|
87
|
+
field,
|
|
88
|
+
level
|
|
89
|
+
})}{:else if snippetForValue && isDate(value)}{@render snippets.date?.({
|
|
90
|
+
value,
|
|
91
|
+
field,
|
|
92
|
+
level
|
|
93
|
+
})}{:else if snippetForValue && isFunction(value)}{@render snippets.function?.({
|
|
94
|
+
value,
|
|
95
|
+
field,
|
|
96
|
+
level
|
|
97
|
+
})}{:else}<span class={rendered.valueStyle}>{rendered.text}</span
|
|
98
|
+
>{/if}{#if !lastElement}<span class={style.punctuation}>,</span>{/if}
|
|
99
|
+
</div>
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { JsonRenderProps } from './types.js';
|
|
2
|
+
declare const JsonPrimitiveValue: import("svelte").Component<JsonRenderProps<string | number | bigint | boolean | Function | Date | null | undefined>, {}, "">;
|
|
3
|
+
type JsonPrimitiveValue = ReturnType<typeof JsonPrimitiveValue>;
|
|
4
|
+
export default JsonPrimitiveValue;
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import DataRender from './DataRender.svelte'
|
|
3
|
+
import { defaultStyles } from './index.js'
|
|
4
|
+
import type { OuterRef, Props, StyleProps } from './types.js'
|
|
5
|
+
import { isObject } from './utils/dataTypeDetection.js'
|
|
6
|
+
import { allExpanded } from './utils/expandStrategies.js'
|
|
7
|
+
|
|
8
|
+
const {
|
|
9
|
+
data,
|
|
10
|
+
style = {},
|
|
11
|
+
shouldExpandNode = allExpanded,
|
|
12
|
+
clickToExpandNode = false,
|
|
13
|
+
beforeExpandChange,
|
|
14
|
+
compactTopLevel = false,
|
|
15
|
+
string,
|
|
16
|
+
number,
|
|
17
|
+
boolean,
|
|
18
|
+
null: nullSnippet,
|
|
19
|
+
undefined: undefinedSnippet,
|
|
20
|
+
bigint,
|
|
21
|
+
date,
|
|
22
|
+
function: functionSnippet,
|
|
23
|
+
label,
|
|
24
|
+
'aria-label': ariaLabel = 'JSON view',
|
|
25
|
+
...rest
|
|
26
|
+
}: Props = $props()
|
|
27
|
+
|
|
28
|
+
let outerElement = $state<HTMLDivElement | null>(null)
|
|
29
|
+
|
|
30
|
+
// Merge user theme onto defaults. Also emit a deprecation warning when the
|
|
31
|
+
// legacy `ariaLables` key (typo in react-json-view-lite) is supplied
|
|
32
|
+
// without the corrected `ariaLabels`; the typoed value is still honored
|
|
33
|
+
// during 1.x so React migrators have a soft landing.
|
|
34
|
+
const mergedStyle = $derived.by<StyleProps>(() => {
|
|
35
|
+
const merged: StyleProps = { ...defaultStyles, ...style }
|
|
36
|
+
// Accept the misspelled ariaLables (from react-json-view-lite) only
|
|
37
|
+
// when the user has not provided an ariaLabels value — users who
|
|
38
|
+
// supply both should have the corrected key win.
|
|
39
|
+
if (style?.ariaLables && !style.ariaLabels) {
|
|
40
|
+
if (typeof console !== 'undefined') {
|
|
41
|
+
console.warn(
|
|
42
|
+
'[svelte-json-view-lite] StyleProps.ariaLables is deprecated (typo preserved from react-json-view-lite); use ariaLabels.'
|
|
43
|
+
)
|
|
44
|
+
}
|
|
45
|
+
merged.ariaLabels = style.ariaLables
|
|
46
|
+
}
|
|
47
|
+
return merged
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
// Getter wrapper so children always see the current bind:this target
|
|
51
|
+
// rather than a frozen snapshot from the first render.
|
|
52
|
+
const outerRef: OuterRef = {
|
|
53
|
+
get current() {
|
|
54
|
+
return outerElement
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const snippets = $derived({
|
|
59
|
+
string,
|
|
60
|
+
number,
|
|
61
|
+
boolean,
|
|
62
|
+
null: nullSnippet,
|
|
63
|
+
undefined: undefinedSnippet,
|
|
64
|
+
bigint,
|
|
65
|
+
date,
|
|
66
|
+
function: functionSnippet,
|
|
67
|
+
label
|
|
68
|
+
})
|
|
69
|
+
</script>
|
|
70
|
+
|
|
71
|
+
<div
|
|
72
|
+
bind:this={outerElement}
|
|
73
|
+
role="tree"
|
|
74
|
+
aria-label={ariaLabel}
|
|
75
|
+
{...rest}
|
|
76
|
+
class={mergedStyle.container}
|
|
77
|
+
>
|
|
78
|
+
{#if compactTopLevel && isObject(data)}
|
|
79
|
+
{#each Object.entries(data) as [key, value] (key)}
|
|
80
|
+
<DataRender
|
|
81
|
+
field={key}
|
|
82
|
+
{value}
|
|
83
|
+
style={mergedStyle}
|
|
84
|
+
lastElement={true}
|
|
85
|
+
level={1}
|
|
86
|
+
{shouldExpandNode}
|
|
87
|
+
{clickToExpandNode}
|
|
88
|
+
{beforeExpandChange}
|
|
89
|
+
{outerRef}
|
|
90
|
+
{snippets}
|
|
91
|
+
/>
|
|
92
|
+
{/each}
|
|
93
|
+
{:else}
|
|
94
|
+
<DataRender
|
|
95
|
+
value={data}
|
|
96
|
+
style={mergedStyle}
|
|
97
|
+
lastElement={true}
|
|
98
|
+
level={0}
|
|
99
|
+
{shouldExpandNode}
|
|
100
|
+
{clickToExpandNode}
|
|
101
|
+
{beforeExpandChange}
|
|
102
|
+
{outerRef}
|
|
103
|
+
{snippets}
|
|
104
|
+
/>
|
|
105
|
+
{/if}
|
|
106
|
+
</div>
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import JsonView from './JsonView.svelte';
|
|
2
|
+
import type { AriaLabels, StyleProps } from './types.js';
|
|
3
|
+
export default JsonView;
|
|
4
|
+
export type { AriaLabels, BigIntSnippetProps, BooleanSnippetProps, DateSnippetProps, FunctionSnippetProps, LabelSnippetProps, NodeExpandingEvent, NullSnippetProps, NumberSnippetProps, Props, SnippetOverrides, StringSnippetProps, StyleProps, UndefinedSnippetProps } from './types.js';
|
|
5
|
+
export { allExpanded, collapseAllNested } from './utils/expandStrategies.js';
|
|
6
|
+
export { JsonView };
|
|
7
|
+
/**
|
|
8
|
+
* Correctly-spelled default aria labels. Introduced in the Svelte port;
|
|
9
|
+
* prefer this over the typoed `StyleProps.ariaLables` shipped by
|
|
10
|
+
* `react-json-view-lite`.
|
|
11
|
+
*/
|
|
12
|
+
export declare const defaultAriaLabels: AriaLabels;
|
|
13
|
+
export declare const defaultStyles: StyleProps;
|
|
14
|
+
export declare const darkStyles: StyleProps;
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import JsonView from './JsonView.svelte';
|
|
2
|
+
import styles from './styles.module.css';
|
|
3
|
+
export default JsonView;
|
|
4
|
+
export { allExpanded, collapseAllNested } from './utils/expandStrategies.js';
|
|
5
|
+
export { JsonView };
|
|
6
|
+
const baseAriaLabels = {
|
|
7
|
+
collapseJson: 'collapse JSON',
|
|
8
|
+
expandJson: 'expand JSON'
|
|
9
|
+
};
|
|
10
|
+
/**
|
|
11
|
+
* Correctly-spelled default aria labels. Introduced in the Svelte port;
|
|
12
|
+
* prefer this over the typoed `StyleProps.ariaLables` shipped by
|
|
13
|
+
* `react-json-view-lite`.
|
|
14
|
+
*/
|
|
15
|
+
export const defaultAriaLabels = baseAriaLabels;
|
|
16
|
+
const buildStyles = (variant) => ({
|
|
17
|
+
container: styles[`container-${variant}`],
|
|
18
|
+
basicChildStyle: styles['basic-element-style'],
|
|
19
|
+
childFieldsContainer: styles['child-fields-container'],
|
|
20
|
+
label: styles[`label-${variant}`],
|
|
21
|
+
clickableLabel: styles[`clickable-label-${variant}`],
|
|
22
|
+
nullValue: styles[`value-null-${variant}`],
|
|
23
|
+
undefinedValue: styles[`value-undefined-${variant}`],
|
|
24
|
+
stringValue: styles[`value-string-${variant}`],
|
|
25
|
+
booleanValue: styles[`value-boolean-${variant}`],
|
|
26
|
+
numberValue: styles[`value-number-${variant}`],
|
|
27
|
+
otherValue: styles[`value-other-${variant}`],
|
|
28
|
+
punctuation: styles[`punctuation-${variant}`],
|
|
29
|
+
collapseIcon: styles[`collapse-icon-${variant}`],
|
|
30
|
+
expandIcon: styles[`expand-icon-${variant}`],
|
|
31
|
+
collapsedContent: styles[`collapsed-content-${variant}`],
|
|
32
|
+
noQuotesForStringValues: false,
|
|
33
|
+
quotesForFieldNames: false,
|
|
34
|
+
ariaLabels: baseAriaLabels,
|
|
35
|
+
stringifyStringValues: false
|
|
36
|
+
});
|
|
37
|
+
export const defaultStyles = buildStyles('light');
|
|
38
|
+
export const darkStyles = buildStyles('dark');
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
/* base styles */
|
|
2
|
+
|
|
3
|
+
.container-base {
|
|
4
|
+
line-height: 1.2;
|
|
5
|
+
white-space: pre-wrap;
|
|
6
|
+
white-space: -moz-pre-wrap;
|
|
7
|
+
white-space: -pre-wrap;
|
|
8
|
+
white-space: -o-pre-wrap;
|
|
9
|
+
word-wrap: break-word;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
.punctuation-base {
|
|
13
|
+
margin-right: 5px;
|
|
14
|
+
font-weight: bold;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
.punctuation-base + .punctuation-base {
|
|
18
|
+
margin-left: -5px;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
.pointer {
|
|
22
|
+
cursor: pointer;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
.expander-base {
|
|
26
|
+
composes: pointer;
|
|
27
|
+
font-size: 1.2em;
|
|
28
|
+
margin-right: 5px;
|
|
29
|
+
user-select: none;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
.expand-icon::after {
|
|
33
|
+
content: '▸';
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
.collapse-icon::after {
|
|
37
|
+
content: '▾';
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
.collapsed-content-base {
|
|
41
|
+
composes: pointer;
|
|
42
|
+
margin-right: 5px;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
.collapsed-content-base::after {
|
|
46
|
+
content: '...';
|
|
47
|
+
font-size: 0.8em;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
.container-light {
|
|
51
|
+
composes: container-base;
|
|
52
|
+
background: #eee;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
.basic-element-style {
|
|
56
|
+
margin: 0;
|
|
57
|
+
padding: 0 10px;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
.child-fields-container {
|
|
61
|
+
margin: 0;
|
|
62
|
+
padding: 0;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/* default light style */
|
|
66
|
+
.label-light {
|
|
67
|
+
font-weight: 600;
|
|
68
|
+
margin-right: 5px;
|
|
69
|
+
color: #000000;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
.clickable-label-light {
|
|
73
|
+
composes: label-light;
|
|
74
|
+
composes: pointer;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
.punctuation-light {
|
|
78
|
+
composes: punctuation-base;
|
|
79
|
+
color: #000000;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
.value-null-light {
|
|
83
|
+
color: #df113a;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
.value-undefined-light {
|
|
87
|
+
color: #df113a;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
.value-string-light {
|
|
91
|
+
color: rgb(42, 63, 60);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
.value-number-light {
|
|
95
|
+
color: #0b75f5;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
.value-boolean-light {
|
|
99
|
+
color: rgb(70, 144, 56);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
.value-other-light {
|
|
103
|
+
color: #43413d;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
.collapse-icon-light {
|
|
107
|
+
composes: expander-base;
|
|
108
|
+
composes: collapse-icon;
|
|
109
|
+
color: #000000;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
.expand-icon-light {
|
|
113
|
+
composes: expander-base;
|
|
114
|
+
composes: expand-icon;
|
|
115
|
+
color: #000000;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
.collapsed-content-light {
|
|
119
|
+
composes: collapsed-content-base;
|
|
120
|
+
color: #000000;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/* default dark style */
|
|
124
|
+
.container-dark {
|
|
125
|
+
background: rgb(0, 43, 54);
|
|
126
|
+
composes: container-base;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
.expand-icon-dark {
|
|
130
|
+
composes: expander-base;
|
|
131
|
+
composes: expand-icon;
|
|
132
|
+
color: rgb(253, 246, 227);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
.collapse-icon-dark {
|
|
136
|
+
composes: expander-base;
|
|
137
|
+
composes: collapse-icon;
|
|
138
|
+
color: rgb(253, 246, 227);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
.collapsed-content-dark {
|
|
142
|
+
composes: collapsed-content-base;
|
|
143
|
+
color: rgb(253, 246, 227);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
.label-dark {
|
|
147
|
+
font-weight: bolder;
|
|
148
|
+
margin-right: 5px;
|
|
149
|
+
color: rgb(253, 246, 227);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
.clickable-label-dark {
|
|
153
|
+
composes: label-dark;
|
|
154
|
+
composes: pointer;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
.punctuation-dark {
|
|
158
|
+
composes: punctuation-base;
|
|
159
|
+
color: rgb(253, 246, 227);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
.value-null-dark {
|
|
163
|
+
color: rgb(129, 181, 172);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
.value-undefined-dark {
|
|
167
|
+
color: rgb(129, 181, 172);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
.value-string-dark {
|
|
171
|
+
color: rgb(203, 75, 22);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
.value-number-dark {
|
|
175
|
+
color: rgb(211, 54, 130);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
.value-boolean-dark {
|
|
179
|
+
color: rgb(174, 129, 255);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
.value-other-dark {
|
|
183
|
+
color: rgb(38, 139, 210);
|
|
184
|
+
}
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import type { Snippet } from 'svelte';
|
|
2
|
+
import type { HTMLAttributes } from 'svelte/elements';
|
|
3
|
+
/**
|
|
4
|
+
* Event payload passed to `beforeExpandChange`. Return `false` to veto the
|
|
5
|
+
* expand/collapse transition.
|
|
6
|
+
*/
|
|
7
|
+
export interface NodeExpandingEvent {
|
|
8
|
+
level: number;
|
|
9
|
+
value: unknown;
|
|
10
|
+
field?: string;
|
|
11
|
+
newExpandValue: boolean;
|
|
12
|
+
}
|
|
13
|
+
/** Screen-reader labels applied to expander buttons. */
|
|
14
|
+
export interface AriaLabels {
|
|
15
|
+
collapseJson: string;
|
|
16
|
+
expandJson: string;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Classname-map used to theme every slot in the viewer. The `defaultStyles`
|
|
20
|
+
* and `darkStyles` exports provide ready-made instances; consumers can spread
|
|
21
|
+
* and override individual keys to customize the theme.
|
|
22
|
+
*/
|
|
23
|
+
export interface StyleProps {
|
|
24
|
+
container: string;
|
|
25
|
+
basicChildStyle: string;
|
|
26
|
+
label: string;
|
|
27
|
+
clickableLabel: string;
|
|
28
|
+
nullValue: string;
|
|
29
|
+
undefinedValue: string;
|
|
30
|
+
numberValue: string;
|
|
31
|
+
stringValue: string;
|
|
32
|
+
booleanValue: string;
|
|
33
|
+
otherValue: string;
|
|
34
|
+
punctuation: string;
|
|
35
|
+
expandIcon: string;
|
|
36
|
+
collapseIcon: string;
|
|
37
|
+
collapsedContent: string;
|
|
38
|
+
childFieldsContainer: string;
|
|
39
|
+
noQuotesForStringValues?: boolean;
|
|
40
|
+
quotesForFieldNames?: boolean;
|
|
41
|
+
/**
|
|
42
|
+
* Correctly-spelled aria labels. New in the Svelte port; `ariaLables`
|
|
43
|
+
* (sic) continues to be accepted at runtime for one minor version with a
|
|
44
|
+
* deprecation warning.
|
|
45
|
+
*/
|
|
46
|
+
ariaLabels: AriaLabels;
|
|
47
|
+
/**
|
|
48
|
+
* @deprecated Use `ariaLabels` (correctly spelled). The typoed key is
|
|
49
|
+
* accepted at runtime for parity with `react-json-view-lite`; it will be
|
|
50
|
+
* removed in 2.0.
|
|
51
|
+
*/
|
|
52
|
+
ariaLables?: AriaLabels;
|
|
53
|
+
stringifyStringValues: boolean;
|
|
54
|
+
}
|
|
55
|
+
/** Data passed to every per-type snippet override. */
|
|
56
|
+
export interface ValueSnippetProps<T> {
|
|
57
|
+
value: T;
|
|
58
|
+
field?: string;
|
|
59
|
+
level: number;
|
|
60
|
+
}
|
|
61
|
+
export type StringSnippetProps = ValueSnippetProps<string>;
|
|
62
|
+
export type NumberSnippetProps = ValueSnippetProps<number>;
|
|
63
|
+
export type BooleanSnippetProps = ValueSnippetProps<boolean>;
|
|
64
|
+
export type NullSnippetProps = ValueSnippetProps<null>;
|
|
65
|
+
export type UndefinedSnippetProps = ValueSnippetProps<undefined>;
|
|
66
|
+
export type BigIntSnippetProps = ValueSnippetProps<bigint>;
|
|
67
|
+
export type DateSnippetProps = ValueSnippetProps<Date>;
|
|
68
|
+
export type FunctionSnippetProps = ValueSnippetProps<Function>;
|
|
69
|
+
/** Data passed to the `label` snippet override for field names. */
|
|
70
|
+
export interface LabelSnippetProps {
|
|
71
|
+
field: string;
|
|
72
|
+
level: number;
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Bag of optional per-type snippets that replace default value rendering.
|
|
76
|
+
* Each snippet receives the typed value, its field name (if any), and the
|
|
77
|
+
* depth of the node.
|
|
78
|
+
*/
|
|
79
|
+
export interface SnippetOverrides {
|
|
80
|
+
string?: Snippet<[StringSnippetProps]>;
|
|
81
|
+
number?: Snippet<[NumberSnippetProps]>;
|
|
82
|
+
boolean?: Snippet<[BooleanSnippetProps]>;
|
|
83
|
+
null?: Snippet<[NullSnippetProps]>;
|
|
84
|
+
undefined?: Snippet<[UndefinedSnippetProps]>;
|
|
85
|
+
bigint?: Snippet<[BigIntSnippetProps]>;
|
|
86
|
+
date?: Snippet<[DateSnippetProps]>;
|
|
87
|
+
function?: Snippet<[FunctionSnippetProps]>;
|
|
88
|
+
label?: Snippet<[LabelSnippetProps]>;
|
|
89
|
+
}
|
|
90
|
+
/** Public props accepted by `<JsonView>`. */
|
|
91
|
+
export interface Props extends Omit<HTMLAttributes<HTMLDivElement>, 'data' | 'style'> {
|
|
92
|
+
data: object | unknown[];
|
|
93
|
+
style?: Partial<StyleProps>;
|
|
94
|
+
shouldExpandNode?: (_level: number, _value: unknown, _field?: string) => boolean;
|
|
95
|
+
clickToExpandNode?: boolean;
|
|
96
|
+
beforeExpandChange?: (_event: NodeExpandingEvent) => boolean;
|
|
97
|
+
compactTopLevel?: boolean;
|
|
98
|
+
string?: Snippet<[StringSnippetProps]>;
|
|
99
|
+
number?: Snippet<[NumberSnippetProps]>;
|
|
100
|
+
boolean?: Snippet<[BooleanSnippetProps]>;
|
|
101
|
+
null?: Snippet<[NullSnippetProps]>;
|
|
102
|
+
undefined?: Snippet<[UndefinedSnippetProps]>;
|
|
103
|
+
bigint?: Snippet<[BigIntSnippetProps]>;
|
|
104
|
+
date?: Snippet<[DateSnippetProps]>;
|
|
105
|
+
function?: Snippet<[FunctionSnippetProps]>;
|
|
106
|
+
label?: Snippet<[LabelSnippetProps]>;
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* Reference wrapper passed from the root down to every expandable node so
|
|
110
|
+
* that cross-sibling keyboard navigation can query `[role=button]` elements
|
|
111
|
+
* scoped to the tree. Using a getter ensures the child always reads the
|
|
112
|
+
* current `bind:this` target rather than a frozen snapshot.
|
|
113
|
+
*/
|
|
114
|
+
export interface OuterRef {
|
|
115
|
+
readonly current: HTMLDivElement | null;
|
|
116
|
+
}
|
|
117
|
+
/** Internal shared props threaded through every renderer. Not exported. */
|
|
118
|
+
export interface CommonRenderProps {
|
|
119
|
+
lastElement: boolean;
|
|
120
|
+
level: number;
|
|
121
|
+
style: StyleProps;
|
|
122
|
+
shouldExpandNode: (_level: number, _value: unknown, _field?: string) => boolean;
|
|
123
|
+
clickToExpandNode: boolean;
|
|
124
|
+
outerRef: OuterRef;
|
|
125
|
+
beforeExpandChange?: (_event: NodeExpandingEvent) => boolean;
|
|
126
|
+
snippets: SnippetOverrides;
|
|
127
|
+
}
|
|
128
|
+
export interface JsonRenderProps<T> extends CommonRenderProps {
|
|
129
|
+
field?: string;
|
|
130
|
+
value: T;
|
|
131
|
+
}
|
|
132
|
+
export interface ExpandableRenderProps extends CommonRenderProps {
|
|
133
|
+
field?: string;
|
|
134
|
+
value: object | unknown[];
|
|
135
|
+
data: Array<[string | undefined, unknown]>;
|
|
136
|
+
openBracket: string;
|
|
137
|
+
closeBracket: string;
|
|
138
|
+
}
|
|
139
|
+
export interface EmptyRenderProps {
|
|
140
|
+
field?: string;
|
|
141
|
+
openBracket: string;
|
|
142
|
+
closeBracket: string;
|
|
143
|
+
lastElement: boolean;
|
|
144
|
+
style: StyleProps;
|
|
145
|
+
}
|
package/dist/types.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export declare const isBoolean: (data: unknown) => data is boolean;
|
|
2
|
+
export declare const isNumber: (data: unknown) => data is number;
|
|
3
|
+
export declare const isBigInt: (data: unknown) => data is bigint;
|
|
4
|
+
export declare const isDate: (data: unknown) => data is Date;
|
|
5
|
+
export declare const isString: (data: unknown) => data is string;
|
|
6
|
+
export declare const isArray: (data: unknown) => data is unknown[];
|
|
7
|
+
export declare const isObject: (data: unknown) => data is object;
|
|
8
|
+
export declare const isNull: (data: unknown) => data is null;
|
|
9
|
+
export declare const isUndefined: (data: unknown) => data is undefined;
|
|
10
|
+
export declare const isFunction: (data: unknown) => data is Function;
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
export const isBoolean = (data) => {
|
|
2
|
+
return typeof data === 'boolean' || data instanceof Boolean;
|
|
3
|
+
};
|
|
4
|
+
export const isNumber = (data) => {
|
|
5
|
+
return typeof data === 'number' || data instanceof Number;
|
|
6
|
+
};
|
|
7
|
+
export const isBigInt = (data) => {
|
|
8
|
+
return typeof data === 'bigint' || data instanceof BigInt;
|
|
9
|
+
};
|
|
10
|
+
export const isDate = (data) => {
|
|
11
|
+
return !!data && data instanceof Date;
|
|
12
|
+
};
|
|
13
|
+
export const isString = (data) => {
|
|
14
|
+
return typeof data === 'string' || data instanceof String;
|
|
15
|
+
};
|
|
16
|
+
export const isArray = (data) => {
|
|
17
|
+
return Array.isArray(data);
|
|
18
|
+
};
|
|
19
|
+
export const isObject = (data) => {
|
|
20
|
+
return typeof data === 'object' && data !== null;
|
|
21
|
+
};
|
|
22
|
+
export const isNull = (data) => {
|
|
23
|
+
return data === null;
|
|
24
|
+
};
|
|
25
|
+
export const isUndefined = (data) => {
|
|
26
|
+
return data === undefined;
|
|
27
|
+
};
|
|
28
|
+
// trunk-ignore(eslint/@typescript-eslint/no-unsafe-function-type)
|
|
29
|
+
export const isFunction = (data) => {
|
|
30
|
+
return !!data && data instanceof Object && typeof data === 'function';
|
|
31
|
+
};
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Formats a field label. Empty labels always appear quoted so the colon is
|
|
3
|
+
* still rendered; non-empty labels are quoted only when the theme sets
|
|
4
|
+
* `quotesForFieldNames`. Matches `react-json-view-lite` exactly.
|
|
5
|
+
*/
|
|
6
|
+
export declare function quoteString(value: string, quoted?: boolean): string;
|
|
7
|
+
/**
|
|
8
|
+
* Formats a string value for display. When `stringify` is true, `JSON.stringify`
|
|
9
|
+
* is used to escape special characters; otherwise `quoted` simply toggles
|
|
10
|
+
* surrounding quotes. Empty strings intentionally render without quotes to
|
|
11
|
+
* avoid stacking quotes on inline CSS snippets.
|
|
12
|
+
*/
|
|
13
|
+
export declare function quoteStringValue(value: string, quoted: boolean, stringify: boolean): string;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Formats a field label. Empty labels always appear quoted so the colon is
|
|
3
|
+
* still rendered; non-empty labels are quoted only when the theme sets
|
|
4
|
+
* `quotesForFieldNames`. Matches `react-json-view-lite` exactly.
|
|
5
|
+
*/
|
|
6
|
+
export function quoteString(value, quoted = false) {
|
|
7
|
+
return !value || quoted ? `"${value}"` : value;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Formats a string value for display. When `stringify` is true, `JSON.stringify`
|
|
11
|
+
* is used to escape special characters; otherwise `quoted` simply toggles
|
|
12
|
+
* surrounding quotes. Empty strings intentionally render without quotes to
|
|
13
|
+
* avoid stacking quotes on inline CSS snippets.
|
|
14
|
+
*/
|
|
15
|
+
export function quoteStringValue(value, quoted, stringify) {
|
|
16
|
+
if (stringify) {
|
|
17
|
+
return JSON.stringify(value);
|
|
18
|
+
}
|
|
19
|
+
return quoted ? `"${value}"` : value;
|
|
20
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@humanspeak/svelte-json-view-lite",
|
|
3
|
+
"version": "0.1.0",
|
|
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
|
+
"keywords": [
|
|
6
|
+
"svelte",
|
|
7
|
+
"svelte5",
|
|
8
|
+
"sveltekit",
|
|
9
|
+
"json",
|
|
10
|
+
"json-view",
|
|
11
|
+
"json-tree",
|
|
12
|
+
"viewer",
|
|
13
|
+
"tree",
|
|
14
|
+
"typescript",
|
|
15
|
+
"runes",
|
|
16
|
+
"lite",
|
|
17
|
+
"renderer",
|
|
18
|
+
"component"
|
|
19
|
+
],
|
|
20
|
+
"homepage": "https://github.com/humanspeak/svelte-json-view-lite",
|
|
21
|
+
"bugs": {
|
|
22
|
+
"url": "https://github.com/humanspeak/svelte-json-view-lite/issues"
|
|
23
|
+
},
|
|
24
|
+
"repository": {
|
|
25
|
+
"type": "git",
|
|
26
|
+
"url": "git+https://github.com/humanspeak/svelte-json-view-lite.git"
|
|
27
|
+
},
|
|
28
|
+
"funding": {
|
|
29
|
+
"type": "github",
|
|
30
|
+
"url": "https://github.com/sponsors/humanspeak"
|
|
31
|
+
},
|
|
32
|
+
"license": "MIT",
|
|
33
|
+
"author": "Humanspeak, Inc.",
|
|
34
|
+
"sideEffects": [
|
|
35
|
+
"**/*.css"
|
|
36
|
+
],
|
|
37
|
+
"type": "module",
|
|
38
|
+
"exports": {
|
|
39
|
+
".": {
|
|
40
|
+
"types": "./dist/index.d.ts",
|
|
41
|
+
"svelte": "./dist/index.js"
|
|
42
|
+
}
|
|
43
|
+
},
|
|
44
|
+
"svelte": "./dist/index.js",
|
|
45
|
+
"types": "./dist/index.d.ts",
|
|
46
|
+
"files": [
|
|
47
|
+
"dist",
|
|
48
|
+
"!dist/**/*.test.*",
|
|
49
|
+
"!dist/**/*.spec.*",
|
|
50
|
+
"!dist/test/**/*"
|
|
51
|
+
],
|
|
52
|
+
"overrides": {
|
|
53
|
+
"@sveltejs/kit": {
|
|
54
|
+
"cookie": "^0.7.0"
|
|
55
|
+
}
|
|
56
|
+
},
|
|
57
|
+
"devDependencies": {
|
|
58
|
+
"@eslint/compat": "^2.0.5",
|
|
59
|
+
"@eslint/js": "^10.0.1",
|
|
60
|
+
"@playwright/test": "^1.59.1",
|
|
61
|
+
"@sveltejs/adapter-auto": "^7.0.1",
|
|
62
|
+
"@sveltejs/kit": "^2.57.1",
|
|
63
|
+
"@sveltejs/package": "^2.5.7",
|
|
64
|
+
"@sveltejs/vite-plugin-svelte": "^7.0.0",
|
|
65
|
+
"@testing-library/jest-dom": "^6.9.1",
|
|
66
|
+
"@testing-library/svelte": "^5.3.1",
|
|
67
|
+
"@testing-library/user-event": "^14.6.1",
|
|
68
|
+
"@types/node": "^25.6.0",
|
|
69
|
+
"@typescript-eslint/eslint-plugin": "^8.58.2",
|
|
70
|
+
"@typescript-eslint/parser": "^8.58.2",
|
|
71
|
+
"@vitest/coverage-v8": "^4.1.4",
|
|
72
|
+
"eslint": "^10.2.1",
|
|
73
|
+
"eslint-config-prettier": "^10.1.8",
|
|
74
|
+
"eslint-plugin-import": "^2.32.0",
|
|
75
|
+
"eslint-plugin-svelte": "^3.17.0",
|
|
76
|
+
"eslint-plugin-unused-imports": "^4.4.1",
|
|
77
|
+
"globals": "^17.5.0",
|
|
78
|
+
"husky": "^9.1.7",
|
|
79
|
+
"jsdom": "^29.0.2",
|
|
80
|
+
"mprocs": "^0.9.2",
|
|
81
|
+
"prettier": "^3.8.3",
|
|
82
|
+
"prettier-plugin-organize-imports": "^4.3.0",
|
|
83
|
+
"prettier-plugin-svelte": "^3.5.1",
|
|
84
|
+
"publint": "^0.3.18",
|
|
85
|
+
"svelte": "^5.55.4",
|
|
86
|
+
"svelte-check": "^4.4.6",
|
|
87
|
+
"typescript": "^6.0.3",
|
|
88
|
+
"typescript-eslint": "^8.58.2",
|
|
89
|
+
"vite": "^8.0.8",
|
|
90
|
+
"vitest": "^4.1.4"
|
|
91
|
+
},
|
|
92
|
+
"peerDependencies": {
|
|
93
|
+
"svelte": "^5.0.0"
|
|
94
|
+
},
|
|
95
|
+
"volta": {
|
|
96
|
+
"node": "24.15.0"
|
|
97
|
+
},
|
|
98
|
+
"publishConfig": {
|
|
99
|
+
"access": "public"
|
|
100
|
+
},
|
|
101
|
+
"tags": [
|
|
102
|
+
"svelte",
|
|
103
|
+
"json",
|
|
104
|
+
"viewer"
|
|
105
|
+
],
|
|
106
|
+
"scripts": {
|
|
107
|
+
"build": "vite build && npm run package",
|
|
108
|
+
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
|
|
109
|
+
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
|
|
110
|
+
"dev": "vite dev",
|
|
111
|
+
"dev:all": "mprocs",
|
|
112
|
+
"dev:pkg": "svelte-kit sync && svelte-package --watch",
|
|
113
|
+
"format": "prettier --write .",
|
|
114
|
+
"lint": "prettier --check . && eslint .",
|
|
115
|
+
"lint:fix": "npm run format && eslint . --fix",
|
|
116
|
+
"package": "svelte-kit sync && svelte-package && publint",
|
|
117
|
+
"preview": "vite preview",
|
|
118
|
+
"test": "vitest run --coverage",
|
|
119
|
+
"test:all": "npm run test && npm run test:e2e",
|
|
120
|
+
"test:e2e": "playwright test",
|
|
121
|
+
"test:e2e:debug": "playwright test --debug",
|
|
122
|
+
"test:e2e:report": "playwright show-report",
|
|
123
|
+
"test:e2e:ui": "playwright test --ui",
|
|
124
|
+
"test:only": "vitest run",
|
|
125
|
+
"test:watch": "vitest"
|
|
126
|
+
}
|
|
127
|
+
}
|