@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 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
 
@@ -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
- <div class={style.basicChildStyle} role="treeitem" aria-selected={false}>
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
- <div
137
- class={style.basicChildStyle}
138
- role="treeitem"
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 hasField}{#if snippets.label}{@render snippets.label(
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 === lastIndex}
167
- level={childLevel}
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
- type ValueKind =
22
- | 'null'
23
- | 'undefined'
24
- | 'string'
25
- | 'boolean'
26
- | 'number'
27
- | 'bigint'
28
- | 'date'
29
- | 'function'
30
- | 'other'
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
- // A single predicate walk picks the discriminant; `rendered`, the
33
- // snippet lookup, and the template all key off `kind` so the 8-way
34
- // type ladder is evaluated once per render instead of three times.
35
- const kind = $derived.by<ValueKind>(() => {
36
- if (value === null) return 'null'
37
- if (value === undefined) return 'undefined'
38
- if (isString(value)) return 'string'
39
- if (isBoolean(value)) return 'boolean'
40
- if (isNumber(value)) return 'number'
41
- if (isBigInt(value)) return 'bigint'
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
- const activeSnippet = $derived.by(() => {
85
- switch (kind) {
86
- case 'null':
87
- return snippets.null
88
- case 'undefined':
89
- return snippets.undefined
90
- case 'string':
91
- return snippets.string
92
- case 'boolean':
93
- return snippets.boolean
94
- case 'number':
95
- return snippets.number
96
- case 'bigint':
97
- return snippets.bigint
98
- case 'date':
99
- return snippets.date
100
- case 'function':
101
- return snippets.function
102
- default:
103
- return undefined
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 `kind` rather than re-running predicates.
120
+ off the merged primitive render model rather than re-running predicates.
114
121
  -->
115
- <div class={style.basicChildStyle} role="treeitem" aria-selected={false}>
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 hasField}{#if snippets.label}{@render snippets.label({ field: field ?? '', level })}{:else}<span class={style.label}>{labelText}:</span>{/if}{/if}{#if activeSnippet && kind === 'null'}{@render snippets.null?.(
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 activeSnippet && kind === 'undefined'}{@render snippets.undefined?.({
128
+ )}{:else if primitive.snippet && primitive.kind === 'undefined'}{@render primitive.snippet({
120
129
  value: undefined,
121
130
  field,
122
131
  level
123
- })}{:else if activeSnippet && kind === 'string'}{@render snippets.string?.({
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 activeSnippet && kind === 'boolean'}{@render snippets.boolean?.({
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 activeSnippet && kind === 'number'}{@render snippets.number?.({
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 activeSnippet && kind === 'bigint'}{@render snippets.bigint?.({
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 activeSnippet && kind === 'date'}{@render snippets.date?.({
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 activeSnippet && kind === 'function'}{@render snippets.function?.({
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={rendered.valueStyle}>{rendered.text}</span
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.5",
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",