@humanspeak/svelte-json-view-lite 0.2.0 → 0.2.2

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
@@ -266,7 +266,7 @@ 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-match-patch](https://diff.svelte.page) | Diff comparison for Svelte |
269
+ | [@humanspeak/svelte-diff](https://diff.svelte.page) | Diff comparison for Svelte |
270
270
  | [@humanspeak/svelte-purify](https://purify.svelte.page) | HTML sanitisation for Svelte |
271
271
  | [@humanspeak/svelte-virtual-chat](https://virtualchat.svelte.page) | Virtual chat viewport for Svelte 5 |
272
272
  | [@humanspeak/memory-cache](https://memory.svelte.page) | In-memory cache for TypeScript |
@@ -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
@@ -147,7 +144,7 @@
147
144
  it tight anyway for a consistent rule.
148
145
  -->
149
146
  <!-- prettier-ignore -->
150
- <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(
151
148
  { field: field ?? '', level }
152
149
  )}{:else if clickToExpandNode}<!-- svelte-ignore a11y_no_static_element_interactions --><span
153
150
  class={style.clickableLabel}
@@ -160,8 +157,8 @@
160
157
  field={childField}
161
158
  value={childValue}
162
159
  {style}
163
- lastElement={index === lastIndex}
164
- level={childLevel}
160
+ lastElement={index === count - 1}
161
+ level={level + 1}
165
162
  {shouldExpandNode}
166
163
  {clickToExpandNode}
167
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,43 +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
122
  <!-- Upstream parity: unselected treeitems omit aria-selected entirely. -->
116
123
  <!-- svelte-ignore a11y_role_has_required_aria_props -->
117
124
  <div class={style.basicChildStyle} role="treeitem">
118
125
  <!-- prettier-ignore -->
119
- {#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(
120
127
  { value: null, field, level }
121
- )}{:else if activeSnippet && kind === 'undefined'}{@render snippets.undefined?.({
128
+ )}{:else if primitive.snippet && primitive.kind === 'undefined'}{@render primitive.snippet({
122
129
  value: undefined,
123
130
  field,
124
131
  level
125
- })}{:else if activeSnippet && kind === 'string'}{@render snippets.string?.({
132
+ })}{:else if primitive.snippet && primitive.kind === 'string'}{@render primitive.snippet({
126
133
  value: value as string,
127
134
  field,
128
135
  level
129
- })}{:else if activeSnippet && kind === 'boolean'}{@render snippets.boolean?.({
136
+ })}{:else if primitive.snippet && primitive.kind === 'boolean'}{@render primitive.snippet({
130
137
  value: value as boolean,
131
138
  field,
132
139
  level
133
- })}{:else if activeSnippet && kind === 'number'}{@render snippets.number?.({
140
+ })}{:else if primitive.snippet && primitive.kind === 'number'}{@render primitive.snippet({
134
141
  value: value as number,
135
142
  field,
136
143
  level
137
- })}{:else if activeSnippet && kind === 'bigint'}{@render snippets.bigint?.({
144
+ })}{:else if primitive.snippet && primitive.kind === 'bigint'}{@render primitive.snippet({
138
145
  value: value as bigint,
139
146
  field,
140
147
  level
141
- })}{:else if activeSnippet && kind === 'date'}{@render snippets.date?.({
148
+ })}{:else if primitive.snippet && primitive.kind === 'date'}{@render primitive.snippet({
142
149
  value: value as Date,
143
150
  field,
144
151
  level
145
- })}{:else if activeSnippet && kind === 'function'}{@render snippets.function?.({
152
+ })}{:else if primitive.snippet && primitive.kind === 'function'}{@render primitive.snippet({
146
153
  // trunk-ignore(eslint/@typescript-eslint/no-unsafe-function-type)
147
154
  value: value as Function,
148
155
  field,
149
156
  level
150
- })}{:else}<span class={rendered.valueStyle}>{rendered.text}</span
157
+ })}{:else}<span class={primitive.valueStyle}>{primitive.text}</span
151
158
  >{/if}{#if !lastElement}<span class={style.punctuation}>,</span>{/if}
152
159
  </div>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@humanspeak/svelte-json-view-lite",
3
- "version": "0.2.0",
3
+ "version": "0.2.2",
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",