chore: pnpm format
📊 Benchmark the Runtime / benchmark (pull_request) Successful in 1m2s
🚀 Lint & Test & Deploy / quality (pull_request) Successful in 2m30s
🚀 Lint & Test & Deploy / test-unit (pull_request) Successful in 32s
🚀 Lint & Test & Deploy / test-e2e (pull_request) Failing after 33s
🚀 Lint & Test & Deploy / deploy (pull_request) Has been skipped
📊 Benchmark the Runtime / benchmark (pull_request) Successful in 1m2s
🚀 Lint & Test & Deploy / quality (pull_request) Successful in 2m30s
🚀 Lint & Test & Deploy / test-unit (pull_request) Successful in 32s
🚀 Lint & Test & Deploy / test-e2e (pull_request) Failing after 33s
🚀 Lint & Test & Deploy / deploy (pull_request) Has been skipped
This commit is contained in:
@@ -1,13 +1,23 @@
|
|||||||
|
import type { Graph } from '@nodarium/types';
|
||||||
import { describe, expect, it } from 'vitest';
|
import { describe, expect, it } from 'vitest';
|
||||||
import { expandGroups } from './runtime-executor';
|
import { expandGroups } from './runtime-executor';
|
||||||
import type { Graph } from '@nodarium/types';
|
|
||||||
|
|
||||||
// Helpers to build minimal serialized nodes/edges
|
// Helpers to build minimal serialized nodes/edges
|
||||||
function node(id: number, type: string, props?: Record<string, number>) {
|
function node(id: number, type: string, props?: Record<string, number>) {
|
||||||
return { id, type: type as Graph['nodes'][0]['type'], position: [0, 0] as [number, number], ...(props ? { props } : {}) };
|
return {
|
||||||
|
id,
|
||||||
|
type: type as Graph['nodes'][0]['type'],
|
||||||
|
position: [0, 0] as [number, number],
|
||||||
|
...(props ? { props } : {})
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function edge(from: number, fromSocket: number, to: number, toSocket: string): [number, number, number, string] {
|
function edge(
|
||||||
|
from: number,
|
||||||
|
fromSocket: number,
|
||||||
|
to: number,
|
||||||
|
toSocket: string
|
||||||
|
): [number, number, number, string] {
|
||||||
return [from, fromSocket, to, toSocket];
|
return [from, fromSocket, to, toSocket];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -41,8 +51,8 @@ describe('expandGroups', () => {
|
|||||||
node(3, 'test/node/input')
|
node(3, 'test/node/input')
|
||||||
],
|
],
|
||||||
edges: [
|
edges: [
|
||||||
edge(1, 0, groupNodeId, 'input_0'), // A → group
|
edge(1, 0, groupNodeId, 'input_0'), // A → group
|
||||||
edge(groupNodeId, 0, 3, 'value') // group → C
|
edge(groupNodeId, 0, 3, 'value') // group → C
|
||||||
],
|
],
|
||||||
groups: [{
|
groups: [{
|
||||||
id: groupId,
|
id: groupId,
|
||||||
@@ -52,8 +62,8 @@ describe('expandGroups', () => {
|
|||||||
node(7, '__internal/group/output')
|
node(7, '__internal/group/output')
|
||||||
],
|
],
|
||||||
edges: [
|
edges: [
|
||||||
edge(6, 0, 2, 'input'), // inputBoundary → B
|
edge(6, 0, 2, 'input'), // inputBoundary → B
|
||||||
edge(2, 0, 7, 'Out') // B → outputBoundary
|
edge(2, 0, 7, 'Out') // B → outputBoundary
|
||||||
],
|
],
|
||||||
inputs: { input_0: { type: 'float' } },
|
inputs: { input_0: { type: 'float' } },
|
||||||
outputs: [{ type: 'float', label: 'Output 0' }]
|
outputs: [{ type: 'float', label: 'Output 0' }]
|
||||||
@@ -69,7 +79,7 @@ describe('expandGroups', () => {
|
|||||||
expect(ids).toContain(3); // C
|
expect(ids).toContain(3); // C
|
||||||
expect(result.nodes.length).toBe(3); // A, B(remapped), C
|
expect(result.nodes.length).toBe(3); // A, B(remapped), C
|
||||||
|
|
||||||
expect(result.edges).toContainEqual(edge(1, 0, remappedB, 'input')); // A → B
|
expect(result.edges).toContainEqual(edge(1, 0, remappedB, 'input')); // A → B
|
||||||
expect(result.edges).toContainEqual(edge(remappedB, 0, 3, 'value')); // B → C
|
expect(result.edges).toContainEqual(edge(remappedB, 0, 3, 'value')); // B → C
|
||||||
expect(result.edges.length).toBe(2);
|
expect(result.edges.length).toBe(2);
|
||||||
});
|
});
|
||||||
@@ -96,14 +106,14 @@ describe('expandGroups', () => {
|
|||||||
id: groupId,
|
id: groupId,
|
||||||
nodes: [
|
nodes: [
|
||||||
node(3, '__internal/group/input'),
|
node(3, '__internal/group/input'),
|
||||||
node(1, 'test/node/output'), // B
|
node(1, 'test/node/output'), // B
|
||||||
node(2, 'test/node/output'), // D
|
node(2, 'test/node/output'), // D
|
||||||
node(4, '__internal/group/output')
|
node(4, '__internal/group/output')
|
||||||
],
|
],
|
||||||
edges: [
|
edges: [
|
||||||
edge(3, 0, 1, 'input'), // inputBoundary → B
|
edge(3, 0, 1, 'input'), // inputBoundary → B
|
||||||
edge(1, 0, 2, 'input'), // B → D (internal)
|
edge(1, 0, 2, 'input'), // B → D (internal)
|
||||||
edge(2, 0, 4, 'Out') // D → outputBoundary
|
edge(2, 0, 4, 'Out') // D → outputBoundary
|
||||||
],
|
],
|
||||||
inputs: { input_0: { type: 'float' } },
|
inputs: { input_0: { type: 'float' } },
|
||||||
outputs: [{ type: 'float' }]
|
outputs: [{ type: 'float' }]
|
||||||
@@ -116,9 +126,9 @@ describe('expandGroups', () => {
|
|||||||
expect(result.nodes.map(n => n.id)).toContain(remappedB);
|
expect(result.nodes.map(n => n.id)).toContain(remappedB);
|
||||||
expect(result.nodes.map(n => n.id)).toContain(remappedD);
|
expect(result.nodes.map(n => n.id)).toContain(remappedD);
|
||||||
|
|
||||||
expect(result.edges).toContainEqual(edge(0, 0, remappedB, 'input')); // A → B
|
expect(result.edges).toContainEqual(edge(0, 0, remappedB, 'input')); // A → B
|
||||||
expect(result.edges).toContainEqual(edge(remappedB, 0, remappedD, 'input')); // B → D (internal)
|
expect(result.edges).toContainEqual(edge(remappedB, 0, remappedD, 'input')); // B → D (internal)
|
||||||
expect(result.edges).toContainEqual(edge(remappedD, 0, 9, 'value')); // D → C
|
expect(result.edges).toContainEqual(edge(remappedD, 0, 9, 'value')); // D → C
|
||||||
expect(result.edges.length).toBe(3);
|
expect(result.edges.length).toBe(3);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
+72
-60
@@ -47,6 +47,7 @@ User Interaction
|
|||||||
```
|
```
|
||||||
|
|
||||||
**Event flow:**
|
**Event flow:**
|
||||||
|
|
||||||
1. User edits graph → GraphManager mutates state
|
1. User edits graph → GraphManager mutates state
|
||||||
2. GraphManager emits `save` → ProjectManager persists to IndexDB
|
2. GraphManager emits `save` → ProjectManager persists to IndexDB
|
||||||
3. GraphManager emits `result` → Runtime executes graph → Viewer updates
|
3. GraphManager emits `result` → Runtime executes graph → Viewer updates
|
||||||
@@ -55,26 +56,26 @@ User Interaction
|
|||||||
|
|
||||||
## Critical Files
|
## Critical Files
|
||||||
|
|
||||||
| File | Role |
|
| File | Role |
|
||||||
|------|------|
|
| ------------------------------------------------------ | --------------------------------------------------------------------- |
|
||||||
| `app/src/routes/+page.svelte` | Wires all systems; creates GraphManager, runtime, registry |
|
| `app/src/routes/+page.svelte` | Wires all systems; creates GraphManager, runtime, registry |
|
||||||
| `app/src/lib/graph-interface/graph-manager.svelte.ts` | Central graph logic: createNode, createEdge, serialize, load, history |
|
| `app/src/lib/graph-interface/graph-manager.svelte.ts` | Central graph logic: createNode, createEdge, serialize, load, history |
|
||||||
| `app/src/lib/graph-interface/graph-state.svelte.ts` | UI state: camera, selection, mouse, clipboard, groupSelectedNodes |
|
| `app/src/lib/graph-interface/graph-state.svelte.ts` | UI state: camera, selection, mouse, clipboard, groupSelectedNodes |
|
||||||
| `app/src/lib/graph-interface/graph/Graph.svelte` | Canvas renderer |
|
| `app/src/lib/graph-interface/graph/Graph.svelte` | Canvas renderer |
|
||||||
| `app/src/lib/graph-interface/node/Node.svelte` | 3D mesh node (Three.js shader) |
|
| `app/src/lib/graph-interface/node/Node.svelte` | 3D mesh node (Three.js shader) |
|
||||||
| `app/src/lib/graph-interface/node/NodeHTML.svelte` | HTML overlay: labels + parameters |
|
| `app/src/lib/graph-interface/node/NodeHTML.svelte` | HTML overlay: labels + parameters |
|
||||||
| `app/src/lib/graph-interface/node/NodeHeader.svelte` | Node title bar |
|
| `app/src/lib/graph-interface/node/NodeHeader.svelte` | Node title bar |
|
||||||
| `app/src/lib/graph-interface/keymaps.ts` | Keyboard shortcuts |
|
| `app/src/lib/graph-interface/keymaps.ts` | Keyboard shortcuts |
|
||||||
| `app/src/lib/graph-interface/helpers/nodeHelpers.ts` | Node height calculations |
|
| `app/src/lib/graph-interface/helpers/nodeHelpers.ts` | Node height calculations |
|
||||||
| `app/src/lib/graph-interface/graph/colors.svelte.ts` | Socket type → color mapping |
|
| `app/src/lib/graph-interface/graph/colors.svelte.ts` | Socket type → color mapping |
|
||||||
| `app/src/lib/runtime/runtime-executor.ts` | Executes nodes in DAG order; expandGroups() |
|
| `app/src/lib/runtime/runtime-executor.ts` | Executes nodes in DAG order; expandGroups() |
|
||||||
| `app/src/lib/node-registry/index.ts` | RemoteNodeRegistry entry |
|
| `app/src/lib/node-registry/index.ts` | RemoteNodeRegistry entry |
|
||||||
| `app/src/lib/node-registry/groupNode.ts` | Built-in group node definition |
|
| `app/src/lib/node-registry/groupNode.ts` | Built-in group node definition |
|
||||||
| `app/src/lib/node-registry/debugNode.ts` | Built-in debug node |
|
| `app/src/lib/node-registry/debugNode.ts` | Built-in debug node |
|
||||||
| `app/src/lib/sidebar/panels/ActiveNodeSettings.svelte` | Per-node settings panel |
|
| `app/src/lib/sidebar/panels/ActiveNodeSettings.svelte` | Per-node settings panel |
|
||||||
| `packages/types/src/types.ts` | Graph, NodeInstance, NodeDefinition, Edge, GroupDefinition |
|
| `packages/types/src/types.ts` | Graph, NodeInstance, NodeDefinition, Edge, GroupDefinition |
|
||||||
| `packages/types/src/inputs.ts` | NodeInput union types (float, vec3, geometry, path, …) |
|
| `packages/types/src/inputs.ts` | NodeInput union types (float, vec3, geometry, path, …) |
|
||||||
| `packages/utils/src/wasm.ts` | createWasmWrapper() — wraps WASM bytes into a NodeDefinition |
|
| `packages/utils/src/wasm.ts` | createWasmWrapper() — wraps WASM bytes into a NodeDefinition |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -83,54 +84,56 @@ User Interaction
|
|||||||
```typescript
|
```typescript
|
||||||
// packages/types/src/types.ts
|
// packages/types/src/types.ts
|
||||||
|
|
||||||
type NodeId = `${string}/${string}/${string}` // e.g. "max/plantarium/stem"
|
type NodeId = `${string}/${string}/${string}`; // e.g. "max/plantarium/stem"
|
||||||
|
|
||||||
type NodeInstance = {
|
type NodeInstance = {
|
||||||
id: number
|
id: number;
|
||||||
type: NodeId
|
type: NodeId;
|
||||||
position: [number, number]
|
position: [number, number];
|
||||||
props?: Record<string, number | number[]> // current parameter values
|
props?: Record<string, number | number[]>; // current parameter values
|
||||||
meta?: { title?: string; lastModified?: string }
|
meta?: { title?: string; lastModified?: string };
|
||||||
state: NodeRuntimeState // runtime-only, NOT serialized
|
state: NodeRuntimeState; // runtime-only, NOT serialized
|
||||||
}
|
};
|
||||||
|
|
||||||
type NodeRuntimeState = {
|
type NodeRuntimeState = {
|
||||||
type?: NodeDefinition // resolved definition
|
type?: NodeDefinition; // resolved definition
|
||||||
parents?: NodeInstance[]
|
parents?: NodeInstance[];
|
||||||
children?: NodeInstance[]
|
children?: NodeInstance[];
|
||||||
x?: number; y?: number // interpolated position
|
x?: number;
|
||||||
mesh?: Mesh // Three.js mesh reference
|
y?: number; // interpolated position
|
||||||
ref?: HTMLElement
|
mesh?: Mesh; // Three.js mesh reference
|
||||||
}
|
ref?: HTMLElement;
|
||||||
|
};
|
||||||
|
|
||||||
type NodeDefinition = {
|
type NodeDefinition = {
|
||||||
id: NodeId
|
id: NodeId;
|
||||||
inputs?: Record<string, NodeInput>
|
inputs?: Record<string, NodeInput>;
|
||||||
outputs?: string[] // output type names
|
outputs?: string[]; // output type names
|
||||||
meta?: { title?: string; description?: string }
|
meta?: { title?: string; description?: string };
|
||||||
execute(input: Int32Array): Int32Array // WASM function
|
execute(input: Int32Array): Int32Array; // WASM function
|
||||||
}
|
};
|
||||||
|
|
||||||
// Edge: [fromNode, outputIndex, toNode, inputSocketName]
|
// Edge: [fromNode, outputIndex, toNode, inputSocketName]
|
||||||
type Edge = [NodeInstance, number, NodeInstance, string]
|
type Edge = [NodeInstance, number, NodeInstance, string];
|
||||||
|
|
||||||
type Graph = {
|
type Graph = {
|
||||||
nodes: NodeInstance[]
|
nodes: NodeInstance[];
|
||||||
edges: [number, number, number, string][] // serialized (IDs, not refs)
|
edges: [number, number, number, string][]; // serialized (IDs, not refs)
|
||||||
settings: Record<string, unknown>
|
settings: Record<string, unknown>;
|
||||||
groups: GroupDefinition[]
|
groups: GroupDefinition[];
|
||||||
}
|
};
|
||||||
|
|
||||||
type GroupDefinition = {
|
type GroupDefinition = {
|
||||||
id: number
|
id: number;
|
||||||
nodes: NodeInstance[]
|
nodes: NodeInstance[];
|
||||||
edges: Edge[]
|
edges: Edge[];
|
||||||
inputs?: Record<string, NodeInput>
|
inputs?: Record<string, NodeInput>;
|
||||||
outputs?: string[]
|
outputs?: string[];
|
||||||
}
|
};
|
||||||
```
|
```
|
||||||
|
|
||||||
### NodeInput socket types
|
### NodeInput socket types
|
||||||
|
|
||||||
`float` | `integer` | `boolean` | `select` | `seed` | `vec3` | `geometry` | `path` | `shape` | `color` | `*` (wildcard)
|
`float` | `integer` | `boolean` | `select` | `seed` | `vec3` | `geometry` | `path` | `shape` | `color` | `*` (wildcard)
|
||||||
|
|
||||||
Each input can have: `value` (default), `label`, `hidden`, `external`, `setting` (link to graph setting), `accepts` (extra compatible types).
|
Each input can have: `value` (default), `label`, `hidden`, `external`, `setting` (link to graph setting), `accepts` (extra compatible types).
|
||||||
@@ -140,34 +143,43 @@ Each input can have: `value` (default), `label`, `hidden`, `external`, `setting`
|
|||||||
## Patterns & Conventions
|
## Patterns & Conventions
|
||||||
|
|
||||||
### Svelte 5 reactivity
|
### Svelte 5 reactivity
|
||||||
|
|
||||||
The codebase uses Svelte 5 runes throughout — `$state`, `$derived`, `$effect`. Collections use `SvelteMap<K,V>` and `SvelteSet<T>` (from `svelte/reactivity`) instead of plain Map/Set so that mutations trigger reactive updates.
|
The codebase uses Svelte 5 runes throughout — `$state`, `$derived`, `$effect`. Collections use `SvelteMap<K,V>` and `SvelteSet<T>` (from `svelte/reactivity`) instead of plain Map/Set so that mutations trigger reactive updates.
|
||||||
|
|
||||||
### Context API
|
### Context API
|
||||||
|
|
||||||
`GraphManager` and `GraphState` are provided via Svelte context (`setContext` / `getContext`) inside `GraphInterface`. All child components (Node, Edge, etc.) consume them via context rather than props.
|
`GraphManager` and `GraphState` are provided via Svelte context (`setContext` / `getContext`) inside `GraphInterface`. All child components (Node, Edge, etc.) consume them via context rather than props.
|
||||||
|
|
||||||
### Edge representation
|
### Edge representation
|
||||||
|
|
||||||
In memory, edges are `[NodeInstance, outputIndex, NodeInstance, inputSocketName]` — direct object references for fast traversal. On serialization (`Graph.edges`), they become `[nodeId, outputIndex, nodeId, inputSocketName]` (plain IDs).
|
In memory, edges are `[NodeInstance, outputIndex, NodeInstance, inputSocketName]` — direct object references for fast traversal. On serialization (`Graph.edges`), they become `[nodeId, outputIndex, nodeId, inputSocketName]` (plain IDs).
|
||||||
|
|
||||||
### Socket compatibility
|
### Socket compatibility
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
areSocketsCompatible(outputType: string, inputType: string | string[]): boolean
|
areSocketsCompatible(outputType: string, inputType: string | string[]): boolean
|
||||||
// '*' wildcard matches any type; 'geometry' accepts ['geometry', 'instances']
|
// '*' wildcard matches any type; 'geometry' accepts ['geometry', 'instances']
|
||||||
```
|
```
|
||||||
|
|
||||||
### WASM execution interface
|
### WASM execution interface
|
||||||
|
|
||||||
Every node exposes a single function: `execute(input: Int32Array): Int32Array`.
|
Every node exposes a single function: `execute(input: Int32Array): Int32Array`.
|
||||||
Data encoding (Plantarium):
|
Data encoding (Plantarium):
|
||||||
|
|
||||||
- `[0, stemDepth, ...x,y,z,thickness]` — path
|
- `[0, stemDepth, ...x,y,z,thickness]` — path
|
||||||
- `[1, vertexCount, faceCount, ...faces, ...vertices, ...normals]` — geometry
|
- `[1, vertexCount, faceCount, ...faces, ...vertices, ...normals]` — geometry
|
||||||
- `[2, vertexCount, faceCount, instanceCount, stemDepth, ...]` — instances
|
- `[2, vertexCount, faceCount, instanceCount, stemDepth, ...]` — instances
|
||||||
|
|
||||||
### Event emitter
|
### Event emitter
|
||||||
|
|
||||||
`GraphManager extends EventEmitter<{ save, result, settings }>`. Subscribe with `manager.on('result', cb)`. Used to decouple the editor UI from runtime execution and persistence.
|
`GraphManager extends EventEmitter<{ save, result, settings }>`. Subscribe with `manager.on('result', cb)`. Used to decouple the editor UI from runtime execution and persistence.
|
||||||
|
|
||||||
### History
|
### History
|
||||||
|
|
||||||
Every mutation goes through `HistoryManager`. Call `this.history.save(this.serialize())` before mutations; undo/redo replays jsondiffpatch deltas.
|
Every mutation goes through `HistoryManager`. Call `this.history.save(this.serialize())` before mutations; undo/redo replays jsondiffpatch deltas.
|
||||||
|
|
||||||
### Internal node IDs
|
### Internal node IDs
|
||||||
|
|
||||||
Built-in nodes use the `__internal/` namespace: `__internal/group/instance`, `__internal/node/debug`. Virtual boundary nodes use `__virtual/`: `__virtual/group/input`, `__virtual/group/output`.
|
Built-in nodes use the `__internal/` namespace: `__internal/group/instance`, `__internal/node/debug`. Virtual boundary nodes use `__virtual/`: `__virtual/group/input`, `__virtual/group/output`.
|
||||||
|
|
||||||
---
|
---
|
||||||
@@ -178,13 +190,13 @@ Group selected nodes with **Ctrl+G**. A `GroupDefinition` is stored in `Graph.gr
|
|||||||
|
|
||||||
**Known gaps as of 2026-05-03:**
|
**Known gaps as of 2026-05-03:**
|
||||||
|
|
||||||
| Issue | Location |
|
| Issue | Location |
|
||||||
|-------|----------|
|
| ----------------------------------------------------------------------------------------- | ------------------------------------------------------------------ |
|
||||||
| `createGroupNode()` called but not defined | `graph-state.svelte.ts:334` → missing in `graph-manager.svelte.ts` |
|
| `createGroupNode()` called but not defined | `graph-state.svelte.ts:334` → missing in `graph-manager.svelte.ts` |
|
||||||
| Runtime expects `group.graph.nodes/edges`; schema has flat `nodes/edges` | `runtime-executor.ts` vs `types.ts` |
|
| Runtime expects `group.graph.nodes/edges`; schema has flat `nodes/edges` | `runtime-executor.ts` vs `types.ts` |
|
||||||
| Runtime expects `group.inputs` as array; schema defines it as `Record<string, NodeInput>` | Same mismatch |
|
| Runtime expects `group.inputs` as array; schema defines it as `Record<string, NodeInput>` | Same mismatch |
|
||||||
| `enterGroupNode()` is a stub — no group navigation | `graph-state.svelte.ts` |
|
| `enterGroupNode()` is a stub — no group navigation | `graph-state.svelte.ts` |
|
||||||
| `serialize()` writes parent-graph edges into group instead of group-internal edges | `graph-manager.svelte.ts` |
|
| `serialize()` writes parent-graph edges into group instead of group-internal edges | `graph-manager.svelte.ts` |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
<script module>
|
<script module lang="ts">
|
||||||
import { SvelteMap } from 'svelte/reactivity';
|
import { SvelteMap } from 'svelte/reactivity';
|
||||||
const cache = new SvelteMap<string, Record<string, boolean>>();
|
const cache = new SvelteMap<string, Record<string, boolean>>();
|
||||||
|
|
||||||
@@ -60,7 +60,7 @@
|
|||||||
}
|
}
|
||||||
return [] as [string, unknown][];
|
return [] as [string, unknown][];
|
||||||
});
|
});
|
||||||
const showKeys = $derived(!isArr || typeof items[0]?.[1] === "object")
|
const showKeys = $derived(!isArr || typeof items[0]?.[1] === 'object');
|
||||||
|
|
||||||
function toggle(next: boolean) {
|
function toggle(next: boolean) {
|
||||||
open = next;
|
open = next;
|
||||||
@@ -92,8 +92,10 @@
|
|||||||
<button
|
<button
|
||||||
class="text-text hover:bg-layer-3 cursor-pointer"
|
class="text-text hover:bg-layer-3 cursor-pointer"
|
||||||
title="Copy value"
|
title="Copy value"
|
||||||
onclick={() => navigator.clipboard.writeText(JSON.stringify({[key]: value}, null, 2))}
|
onclick={() => navigator.clipboard.writeText(JSON.stringify({ [key]: value }, null, 2))}
|
||||||
>{key}</button><span class="text-text/40">: </span>
|
>
|
||||||
|
{key}
|
||||||
|
</button><span class="text-text/40">: </span>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
{#if isExpandable}
|
{#if isExpandable}
|
||||||
@@ -111,7 +113,7 @@
|
|||||||
<div>
|
<div>
|
||||||
<JsonViewer
|
<JsonViewer
|
||||||
value={v}
|
value={v}
|
||||||
key={showKeys ? k : undefined }
|
key={showKeys ? k : undefined}
|
||||||
depth={depth + 1}
|
depth={depth + 1}
|
||||||
path={path ? `${path}/${k}` : k}
|
path={path ? `${path}/${k}` : k}
|
||||||
/>{#if i < items.length - 1}<span class="text-text/20">,</span>{/if}
|
/>{#if i < items.length - 1}<span class="text-text/20">,</span>{/if}
|
||||||
|
|||||||
@@ -10,9 +10,7 @@
|
|||||||
let { options = [], value = $bindable(0), id = '' }: Props = $props();
|
let { options = [], value = $bindable(0), id = '' }: Props = $props();
|
||||||
|
|
||||||
const normalized = $derived(
|
const normalized = $derived(
|
||||||
options.map((opt, i) =>
|
options.map((opt, i) => typeof opt === 'string' ? { value: i, label: opt } : opt)
|
||||||
typeof opt === 'string' ? { value: i, label: opt } : opt
|
|
||||||
)
|
|
||||||
);
|
);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user