Skip to main content

💤 z-idx

z-index DAG builder converting declarative stacking rules into stable numeric layers for shared UI libraries.

Contents

  1. Getting Started
    1. Installation
    2. Example
    3. Purpose
  2. Rationale
    1. Core Concepts
    2. Type Inference
    3. API Surface
  1. Pair Catalogue
    1. Pair Basics
    2. Pair Recursion
    3. Pair Inference
  2. Topology Atlas
    1. Three Nodes
    2. Four Nodes
    3. Five Nodes
  1. Extensions
    1. Extension Stability
    2. Extension Density
    3. Extension Packing
  2. Appendix
    1. Design Notes
    2. Contributing
    3. License

Getting Started

Installation

npm i z-idx

Example

const base = index((z) => z(['overlay1', 'overlay2'], 'Github'), { step: 1 })
const next = base((z) => z('overlay2', ['modal1', 'modal2']))

if (base.Github !== next.Github) throw Error()

render(<MenuPlayground api={next} />)

Purpose

z-idx turns declarative partial-order z-relations into numeric stacking ranks that stay stable when extended. It accepts linear chains, parent-to-children trees, and nest, lifting all key names into TypeScript inference so downstream packages share identical numbers even after override phases.

Rationale

Core Concepts

A z-idx build receives a helper z. Passing multiple strings like z('a','b','c') emits ordered pairs a<b<c with uniform stride. Passing a parent and an array such as z('a',['b','c','d']) links the parent below each child while placing siblings at the same rank. Nested arrays or previously returned Edge can be embedded, enabling tree-shaped DAGs without losing ordering. Ranks start at START (1<<10) with stride STEP (1<<10) so later inserts can bisect gaps without moving seeded nodes. A topological pass (Kahn) rejects cycles; a second pass computes lower and upper bounds per node.

Type Inference

z-idx returns an object that is both callable and map-like. Every key encountered during build is captured in the return type, allowing editors to suggest properties (base.a, base.b) and to hint previous keys during extension (base((z)=>[z('b','x','c')])). Edges preserve their embedded key set even when nested inside arrays, so deeply composed trees still surface full autocomplete.

API Surface

(build: (z: ZFun) => P): ZApi<Keys<P>>

ZFun supports two shapes. Linear form: z(lower, mid, upper, ...) creates consecutive relations. Tree form: z(parent, childrenArray) where childrenArray may contain strings, nested arrays, or Edge; siblings in an array share the same rank. Returned ZApi is callable for extension and exposes numeric ranks keyed by name.

Pair Catalogue

Pair Basics

Deterministic stride across linear chains: z('a','b','c','d') yields ascending ranks with constant gap. Parent-array flattening: z('a',['b','c','d']) keeps a below each child and places siblings at the same rank. Mixed declarations co-exist: a chain plus subtree (z('a','b','c'), z('b',['d','e'])) still keeps a uniform step. Deeply nested arrays collapse into one level above the parent while preserving sibling rank equality.

Pair Recursion

Edge can be reused as children. Building z('a',[z('b','c','d'),'e']) links a below the chain b<c<d and also below the sibling e. Multiple subtrees under one parent share the parent as lower bound and maintain their own internal ordering. Mixed top-level chains with sibling arrays maintain uniform stride while branching at each fork point. Inference spans linear and array inputs ensuring the returned shape exposes every node.

Pair Inference

Nested subtrees recurse without losing step: z('a',[z('b',[z('c','d'),'e']), z('f',['g'])]) yields monotone ranks where a sits below both b and f, with b below c<d and e, while f sits below g. Deeply wrapped sibling arrays such as z('a',[['b','c'],['d','e'],'f']) flatten into ordered groups above a where sub-arrays introduce ordering between groups. Extensions that combine chain and tree forms leave seeds untouched while placing new nodes between them; new keys remain greater than their lower bounds and below preserved uppers. Further composition (z('a',['b','c']), z('b',[z('d',['e',z('f','g')]),'h'])) keeps all eight keys ordered and accessible on the returned API.

Topology Atlas

Three Nodes

All six non-isomorphic DAG shapes for three vertices are expressible. Straight chain a<b<c stays sorted. Single-parent sibling array a<[b,c] places both children at the same rank above the parent. Dual roots into one sink (a->c, b->c) converge cleanly. Fully dense triangle (a->b, a->c, b->c) respects transitivity. Nested pair arrays z('a',[z('b','c')]) flatten to the same order. Late-arriving ancestor pairs still yield the canonical topological sequence.

flowchart TB
g[" "]
a ~~~ g
g ~~~ b
g ~~~ c
style g fill:none,stroke:none,color:none
z(['a', 'b', 'c'])
flowchart TB
g[" "]
a ~~~ g
g ~~~ b
g ~~~ c
style g fill:none,stroke:none,color:none
a --> b
z('a', 'b')
flowchart TB
g[" "]
a ~~~ g
g ~~~ b
g ~~~ c
style g fill:none,stroke:none,color:none
a --> b
a --> c
z('a', ['b', 'c'])
flowchart TB
g[" "]
a ~~~ g
g ~~~ b
g ~~~ c
style g fill:none,stroke:none,color:none
b ---> a
c ---> a
z(['b', 'c'], 'a')
flowchart TB
g[" "]
a ~~~ g
g ~~~ b
g ~~~ c
style g fill:none,stroke:none,color:none
a --> b
b --> c
z('a', 'b', 'c')
flowchart TB
g[" "]
a ~~~ g
g ~~~ b
g ~~~ c
style g fill:none,stroke:none,color:none
a --> b
b --> c
a --> c
z('a', 'b', 'c'), z('a', 'c')

Four Nodes

The catalogue of four-vertex DAGs (31 shapes) maps onto builds combining chains, fans, diamonds, ladders, and merged roots. Examples include diamond a->b, a->c, b->d, c->d, balanced forks a->[b,c,d], reversed sibling order a->[d,c,b], cross-braced ladders a->b, a->c, b->c, c->d, parallel roots with tails, and dual roots merging before a sink. Deterministic spacing holds across all enumerated isomorphism classes, with chains producing uniform stride and fan siblings sharing equal rank.

6 edges

flowchart LR
subgraph x[" "]
direction TB
a
b
end
subgraph y[" "]
direction TB
c
d
end
a ~~~ b
c ~~~ d
style x fill:none,stroke:none
style y fill:none,stroke:none
a --> b
a --> c
b --> d
a --> d
b --> c
c --> d
z(['a', 'b'], ['c', 'd']), z('a', 'b'), z('c', 'd')

5 edges

flowchart LR
subgraph x[" "]
direction TB
a
b
end
subgraph y[" "]
direction TB
c
d
end
a ~~~ b
c ~~~ d
style x fill:none,stroke:none
style y fill:none,stroke:none
b --> d
a --> b
a --> d
a --> c
b --> c
z(['a', 'b'], ['c', 'd']), z('a', 'b')
flowchart LR
subgraph x[" "]
direction TB
a
b
end
subgraph y[" "]
direction TB
c
d
end
a ~~~ b
c ~~~ d
style x fill:none,stroke:none
style y fill:none,stroke:none
a --> c
a --> d
c --> d
b --> d
b --> c
z(['a', 'b'], ['c', 'd']), z('c', 'd')
flowchart LR
subgraph x[" "]
direction TB
a
b
end
subgraph y[" "]
direction TB
c
d
end
a ~~~ b
c ~~~ d
style x fill:none,stroke:none
style y fill:none,stroke:none
a --> c
a --> b
c --> d
a --> d
b --> c
z('a', ['b', 'c', 'd']), z('b', 'c', 'd')
flowchart LR
subgraph x[" "]
direction TB
a
c
end
subgraph y[" "]
direction TB
b
d
end
a ~~~ b
c ~~~ d
style x fill:none,stroke:none
style y fill:none,stroke:none
a --> b
a --> d
a --> c
b --> d
c --> d
z('a', ['b', 'c'], 'd'), z('a', 'd')
flowchart LR
subgraph x[" "]
direction TB
a
b
end
subgraph y[" "]
direction TB
c
d
end
a ~~~ b
c ~~~ d
style x fill:none,stroke:none
style y fill:none,stroke:none
a --> c
a --> b
b --> c
c --> d
b --> d
z('a', ['b', 'c'], 'd'), z('b', 'c')
flowchart LR
subgraph x[" "]
direction TB
a
b
end
subgraph y[" "]
direction TB
c
d
end
a ~~~ b
c ~~~ d
style x fill:none,stroke:none
style y fill:none,stroke:none
a --> b
a --> d
b --> c
b --> d
c --> d
z('a', 'b', ['c', 'd']), z(['a', 'c'], 'd')

4 edges

flowchart LR
subgraph x[" "]
direction TB
a
b
end
subgraph y[" "]
direction TB
c
d
end
a ~~~ b
c ~~~ d
style x fill:none,stroke:none
style y fill:none,stroke:none
a --> c
b --> d
a --> d
b --> c
z(['a', 'b'], ['c', 'd'])
flowchart LR
subgraph x[" "]
direction TB
a
b
end
subgraph y[" "]
direction TB
c
d
end
a ~~~ b
c ~~~ d
style x fill:none,stroke:none
style y fill:none,stroke:none
a --> b
a --> c
a --> d
b --> c
z('a', ['b', 'c', 'd']), z('b', 'c')
flowchart LR
subgraph x[" "]
direction TB
a
b
end
subgraph y[" "]
direction TB
c
d
end
a ~~~ b
c ~~~ d
style x fill:none,stroke:none
style y fill:none,stroke:none
a --> b
a --> c
b --> c
b --> d
z('a', ['b', 'c']), z('b', ['c', 'd'])
flowchart LR
subgraph x[" "]
direction TB
a
c
end
subgraph y[" "]
direction TB
b
d
end
a ~~~ b
c ~~~ d
style x fill:none,stroke:none
style y fill:none,stroke:none
a --> b
a --> c
b --> c
c --> d
z('a', 'b', 'c', 'd'), z('a', 'c')
flowchart LR
subgraph x[" "]
direction TB
a
b
end
subgraph y[" "]
direction TB
c
d
end
a ~~~ b
c ~~~ d
style x fill:none,stroke:none
style y fill:none,stroke:none
a --> b
a --> d
b --> c
c --> d
z('a', 'b', 'c', 'd'), z('a', 'd')
flowchart LR
subgraph x[" "]
direction TB
a
b
end
subgraph y[" "]
direction TB
c
d
end
a ~~~ b
c ~~~ d
style x fill:none,stroke:none
style y fill:none,stroke:none
a --> c
a --> d
b --> c
c --> d
z(['a', 'b'], 'c', 'd'), z('a', 'd')
flowchart LR
subgraph x[" "]
direction TB
a
b
end
subgraph y[" "]
direction TB
c
d
end
style x fill:none,stroke:none
style y fill:none,stroke:none
a --> b
a --> c
b --> d
c --> d
z('a', ['b', 'c'], 'd')
flowchart LR
subgraph x[" "]
direction TB
a
b
end
subgraph y[" "]
direction TB
c
d
end
a ~~~ b
style x fill:none,stroke:none
style y fill:none,stroke:none
a --> c
a --> d
b --> d
c --> d
z('a', ['c', 'd']), z(['b', 'c'], 'd')
flowchart LR
subgraph x[" "]
direction TB
a
b
end
subgraph y[" "]
direction TB
c
d
end
a ~~~ b
c ~~~ d
style x fill:none,stroke:none
style y fill:none,stroke:none
a --> b
b --> c
b --> d
c --> d
z('a', 'b', 'c', 'd'), z('b', 'd')

3 edges

flowchart LR
subgraph x[" "]
direction TB
a
c
end
subgraph y[" "]
direction TB
b
d
end
a ~~~ b
c ~~~ d
style x fill:none,stroke:none
style y fill:none,stroke:none
a --> b
a --> c
a --> d
z('a', ['b', 'c', 'd'])
flowchart LR
subgraph x[" "]
direction TB
a
b
end
subgraph y[" "]
direction TB
c
d
end
a ~~~ b
c ~~~ d
style x fill:none,stroke:none
style y fill:none,stroke:none
a --> b
a --> c
b --> c
z('a', ['b', 'c']), z('b', 'c')
flowchart LR
subgraph x[" "]
direction TB
a
b
end
subgraph y[" "]
direction TB
c
d
end
a ~~~ b
c ~~~ d
style x fill:none,stroke:none
style y fill:none,stroke:none
a --> c
a --> d
b --> c
z(['a', 'b'], 'c'), z('a', 'd')
flowchart LR
subgraph x[" "]
direction TB
a
b
end
subgraph y[" "]
direction TB
c
d
end
a ~~~ b
c ~~~ d
style x fill:none,stroke:none
style y fill:none,stroke:none
a --> b
b --> c
b --> d
z('a', 'b', ['c', 'd'])
flowchart LR
subgraph x[" "]
direction TB
a
b
end
subgraph y[" "]
direction TB
c
d
end
a ~~~ b
c ~~~ d
style x fill:none,stroke:none
style y fill:none,stroke:none
a --> b
b --> c
c --> d
z('a', 'b', 'c', 'd')
flowchart LR
subgraph x[" "]
direction TB
a
c
end
subgraph y[" "]
direction TB
b
d
end
a ~~~ b
c ~~~ d
style x fill:none,stroke:none
style y fill:none,stroke:none
a --> c
b --> c
c --> d
z(['a', 'b'], 'c', 'd')
flowchart LR
subgraph x[" "]
direction TB
a
b
end
subgraph y[" "]
direction TB
c
d
end
a ~~~ b
c ~~~ d
style x fill:none,stroke:none
style y fill:none,stroke:none
a --> d
b --> c
c --> d
z('a', 'd'), z('b', 'c', 'd')
flowchart LR
subgraph x[" "]
direction TB
a
c
end
subgraph y[" "]
direction TB
b
d
end
a ~~~ b
c ~~~ d
style x fill:none,stroke:none
style y fill:none,stroke:none
a --> d
b --> d
c --> d
z(['a', 'b', 'c'], 'd')

2 edges

flowchart LR
subgraph x[" "]
direction TB
a
b
end
subgraph y[" "]
direction TB
c
d
end
a ~~~ b
c ~~~ d
style x fill:none,stroke:none
style y fill:none,stroke:none
a --> b
b --> c
z('a', 'b', 'c')
flowchart LR
subgraph x[" "]
direction TB
a
c
end
subgraph y[" "]
direction TB
b
d
end
a ~~~ b
c ~~~ d
style x fill:none,stroke:none
style y fill:none,stroke:none
a --> b
a --> c
z('a', ['b', 'c'])
flowchart LR
subgraph x[" "]
direction TB
a
b
end
subgraph y[" "]
direction TB
c
d
end
a ~~~ b
c ~~~ d
style x fill:none,stroke:none
style y fill:none,stroke:none
a --> c
b --> c
z(['a', 'b'], 'c')
flowchart LR
subgraph x[" "]
direction TB
a
c
end
subgraph y[" "]
direction TB
b
d
end
a ~~~ b
c ~~~ d
style x fill:none,stroke:none
style y fill:none,stroke:none
a --> b
c --> d
z('a', 'b'), z('c', 'd')

1 edges

flowchart LR
subgraph x[" "]
direction TB
a
c
end
subgraph y[" "]
direction TB
b
d
end
a ~~~ b
c ~~~ d
style x fill:none,stroke:none
style y fill:none,stroke:none
a --> b
z('a', 'b')

0 edges

flowchart LR
subgraph x[" "]
direction TB
a
c
end
subgraph y[" "]
direction TB
b
d
end
a ~~~ b
c ~~~ d
style x fill:none,stroke:none
style y fill:none,stroke:none
z(['a', 'b', 'c', 'd'])

Five Nodes

Five-vertex coverage extends chains, wide fans, multi-source funnels, diamonds with tails, interleaved ladders, balanced two-level trees, partial fans with extended child, mid-node splits, and diamonds with head. Each construction confirms sorted order, equal stride between consecutive nodes in the topological sequence, and stable root-to-leaf monotonicity even as edges multiply. The tests validate that every key participates in the final ordering and that no hidden permutations violate declared constraints.

chainwide fanfunneldiamond tail
flowchart LR
g[" "]
g ~~~ a
g ~~~ b
g ~~~ c
g ~~~ d
g ~~~ e
style g fill:none,stroke:none,color:none
a --> b
b --> c
c --> d
d --> e
z('a', 'b', 'c', 'd', 'e')
flowchart LR
g[" "]
g ~~~ a
g ~~~ b
g ~~~ c
g ~~~ d
g ~~~ e
style g fill:none,stroke:none,color:none
a --> b
a --> c
a --> d
a --> e
z('a', ['b', 'c', 'd', 'e'])
flowchart LR
g[" "]
g ~~~ a
g ~~~ b
g ~~~ c
g ~~~ d
g ~~~ e
style g fill:none,stroke:none,color:none
a --> e
b --> e
c --> e
d --> e
z(['a', 'b', 'c', 'd'], 'e')
flowchart LR
g[" "]
g ~~~ a
g ~~~ b
g ~~~ c
g ~~~ d
g ~~~ e
style g fill:none,stroke:none,color:none
a --> b
a --> c
b --> d
c --> d
d --> e
z('a', ['b', 'c'], 'd', 'e')
interleaved laddersbalanced two-levelfan with deep childdiamond head
flowchart LR
g[" "]
g ~~~ a
g ~~~ b
g ~~~ c
g ~~~ d
g ~~~ e
style g fill:none,stroke:none,color:none
a --> b
b --> e
a --> c
c --> d
d --> e
z('a', 'b', 'e'), z('a', 'c', 'd', 'e')
flowchart LR
g[" "]
g ~~~ a
g ~~~ b
g ~~~ c
g ~~~ d
g ~~~ e
style g fill:none,stroke:none,color:none
a --> b
a --> c
b --> d
c --> e
z('a', 'b', 'd'), z('a', 'c', 'e')
flowchart LR
g[" "]
g ~~~ a
g ~~~ b
g ~~~ c
g ~~~ d
g ~~~ e
style g fill:none,stroke:none,color:none
a --> b
a --> c
a --> d
d --> e
z('a', ['b', 'c', 'd']), z('d', 'e')
flowchart LR
g[" "]
g ~~~ a
g ~~~ b
g ~~~ c
g ~~~ d
g ~~~ e
style g fill:none,stroke:none,color:none
a --> b
b --> c
b --> d
c --> e
d --> e
z('a', 'b', ['c', 'd'], 'e')

Extensions

Extension Stability

First builds are reproducible: identical inputs yield identical ranks. Extending with additional relations preserves all seeded values while inserting newcomers at midpoints between valid bounds (d = mid(a+1, b-1)). Multiple extensions chained in different regions keep earlier inserts fixed, showing that rank assignment uses seeds as immutable fences. When nested tree shorthand seeds wide gaps, subsequent inserts between siblings respect original positions and narrow gaps symmetrically.

Extension Density

Iterative midpoint insertions demonstrate gap shrinkage. Starting from a<b, successive overrides insert c, then d, then e, then f, each halving or quartering the remaining interval. Earlier inserts remain unchanged (c stays at the first midpoint) while new points land strictly inside ever smaller windows. Inserts can target both sides of a midpoint or only the right side; ordering remains intact and seeds never drift.

Extension Packing

Extensions also work outside the initial segment: placing nodes above the highest seed uses STEP-sized spacing, while placing nodes below the lowest seed uses dyadic interval subdivision. Dense clusters near a large gap center keep seeds stable. Mixing outer inserts with inner midpoints leaves fences intact while layering additional nodes between existing seeds. Packed sibling gaps across multiple regions show that each local interval can be subdivided independently across successive overrides, with later splits narrower than the prior interval. Cycles throw an exception immediately, ensuring the DAG assumption holds even in extension calls.

Appendix

Design Notes

Implementation relies on Kahn topological sorting over all declared pairs plus seeded extras, followed by forward and backward constraint propagation through parent-child edges to derive lower and upper fences. A constant STEP of 1024 defines initial gaps; the dyadic function (1 << (31 - clz32(n))) subdivides intervals during insertion cascades by selecting the largest power-of-two that fits within the available span. Fence propagation walks the DAG forward for lower bounds and backward for upper bounds, keeping determinism across runs.

Contributing

Contributions should mirror the existing Vitest suites: pair basics, recursion, inference, topology (three through five nodes), extension stability, density, and packing. Each scenario should validate ordering, stride constancy, and seed preservation without relying on external fixtures.

License

MIT