diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml
deleted file mode 100644
index 6f8a16f9d..000000000
--- a/.github/workflows/test.yaml
+++ /dev/null
@@ -1,101 +0,0 @@
-name: test
-
-# On every pull request, but only on push to master
-on:
- push:
- branches:
- - master
- pull_request:
-
-permissions:
- contents: read
-
-jobs:
- test-node:
- runs-on: ubuntu-latest
-
- # Keep env simple. You don't need --openssl-legacy-provider for Node 20.
- env:
- NODE_ENV: test
-
- steps:
- - uses: actions/checkout@v4
-
- - uses: actions/setup-node@v4
- with:
- node-version: '20' # ✅ match what Vite/Vitest expect
-
- # ✅ Enable Corepack so the `packageManager` field is honored
- - name: Enable Corepack / Yarn 4
- run: |
- corepack enable
- corepack prepare yarn@4.10.3 --activate
- yarn -v
-
- # ✅ Cache Yarn 4's compressed archives
- - name: Cache Yarn 4 artifacts
- id: yarn-cache
- uses: actions/cache@v4
- with:
- path: |
- .yarn/cache
- .pnp.* # if you use Plug’n’Play
- .yarn/install-state.gz
- key: yarn-cache-${{ runner.os }}-${{ hashFiles('**/yarn.lock') }}
- restore-keys: |
- yarn-cache-${{ runner.os }}-
-
- - name: Print versions
- run: |
- node -v
- yarn -v
- node -p "process.versions"
-
- - name: Install deps
- # Always run to materialize node_modules; the cache only restores tarballs.
- run: yarn --frozen-lockfile
-
- # Sanity-check that Vite sees the root config (this MUST be true)
- - name: Verify vite.config.ts presence
- run: |
- test -f vite.config.ts || (echo "vite.config.ts missing at repo root" && exit 1)
- node -e "console.log('vite.config.ts exists, good')"
-
- - name: Build packages
- run: yarn build
-
- - name: Run node tests
- run: yarn test-node
-
- - name: Cache Playwright browsers
- id: playwright-cache
- uses: actions/cache@v4
- with:
- path: ~/.cache/ms-playwright
- key: pw-${{ runner.os }}-${{ hashFiles('**/yarn.lock') }}
- restore-keys: pw-${{ runner.os }}-
-
- - name: Install Playwright Chromium (only)
- if: steps.playwright-cache.outputs.cache-hit != 'true'
- run: npx playwright install --with-deps chromium
-
- - name: Run headless browser tests
- run: yarn test-headless --reporter=verbose
-
- - name: Cache website Yarn artifacts
- id: website-yarn-cache
- uses: actions/cache@v4
- with:
- path: |
- website/.yarn/cache
- website/.pnp.*
- website/.yarn/install-state.gz
- key: website-yarn-cache-${{ runner.os }}-${{ hashFiles('website/yarn.lock') }}
- restore-keys: |
- website-yarn-cache-${{ runner.os }}-
-
- - name: Build website
- run: |
- cd website
- yarn
- yarn build
diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
new file mode 100644
index 000000000..549e1e622
--- /dev/null
+++ b/.github/workflows/test.yml
@@ -0,0 +1,242 @@
+name: test
+
+on:
+ push:
+ branches:
+ - master
+ pull_request:
+
+permissions:
+ contents: read
+
+jobs:
+ lint:
+ runs-on: ubuntu-22.04
+
+ steps:
+ - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
+
+ - name: Setup Node.js
+ uses: actions/setup-node@v4
+ with:
+ node-version: '20'
+
+ - name: Enable Corepack / Yarn 4
+ run: |
+ corepack enable
+ corepack prepare yarn@4.10.3 --activate
+
+ - name: Cache Yarn 4 artifacts
+ uses: actions/cache@v4
+ with:
+ path: |
+ .yarn/cache
+ .pnp.*
+ .yarn/install-state.gz
+ key: yarn-cache-${{ runner.os }}-${{ hashFiles('**/yarn.lock') }}
+ restore-keys: |
+ yarn-cache-${{ runner.os }}-
+
+ - name: Install dependencies
+ run: yarn
+
+ - name: Lint
+ run: yarn lint
+
+ test-node:
+ runs-on: ubuntu-22.04
+ permissions:
+ contents: read
+ actions: read
+ pull-requests: write
+ checks: write
+
+ steps:
+ - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
+
+ - name: Setup Node.js
+ uses: actions/setup-node@v4
+ with:
+ node-version: '20'
+
+ - name: Enable Corepack / Yarn 4
+ run: |
+ corepack enable
+ corepack prepare yarn@4.10.3 --activate
+
+ - name: Cache Yarn 4 artifacts
+ uses: actions/cache@v4
+ with:
+ path: |
+ .yarn/cache
+ .pnp.*
+ .yarn/install-state.gz
+ key: yarn-cache-${{ runner.os }}-${{ hashFiles('**/yarn.lock') }}
+ restore-keys: |
+ yarn-cache-${{ runner.os }}-
+
+ - name: Install dependencies
+ run: yarn
+
+ - name: Build packages
+ run: yarn build
+
+ - name: Run node tests with coverage
+ run: yarn test-ci 2>&1 | tee /tmp/coverage.txt; exit ${PIPESTATUS[0]}
+
+ - name: Save coverage summary
+ if: always()
+ run: grep -m1 "^All files" /tmp/coverage.txt > /tmp/coverage-summary.txt || true
+
+ - name: Upload coverage baseline
+ if: github.ref == 'refs/heads/master'
+ uses: actions/upload-artifact@v4
+ with:
+ name: coverage-baseline
+ path: /tmp/coverage-summary.txt
+ retention-days: 90
+
+ - name: Find latest master run with baseline
+ if: github.event_name == 'pull_request'
+ id: find-baseline
+ uses: actions/github-script@v7
+ with:
+ result-encoding: string
+ script: |
+ const runs = await github.rest.actions.listWorkflowRuns({
+ owner: context.repo.owner,
+ repo: context.repo.repo,
+ workflow_id: 'test.yml',
+ branch: 'master',
+ status: 'success',
+ per_page: 5,
+ });
+ for (const run of runs.data.workflow_runs) {
+ const {data} = await github.rest.actions.listWorkflowRunArtifacts({
+ owner: context.repo.owner,
+ repo: context.repo.repo,
+ run_id: run.id,
+ });
+ if (data.artifacts.find(a => a.name === 'coverage-baseline')) {
+ return String(run.id);
+ }
+ }
+ return '';
+
+ - name: Download coverage baseline
+ if: github.event_name == 'pull_request' && steps.find-baseline.outputs.result != ''
+ uses: actions/download-artifact@v4
+ continue-on-error: true
+ with:
+ name: coverage-baseline
+ path: /tmp/baseline
+ run-id: ${{ steps.find-baseline.outputs.result }}
+ github-token: ${{ secrets.GITHUB_TOKEN }}
+
+ - name: Post coverage summary to PR
+ if: github.event_name == 'pull_request'
+ uses: actions/github-script@v7
+ with:
+ script: |
+ const fs = require('fs');
+ const raw = fs.readFileSync('/tmp/coverage.txt', 'utf8');
+ const lines = raw.split('\n');
+
+ const parseAllFiles = text => {
+ const line = text.split('\n').find(l => l.match(/^All files\s*\|/));
+ if (!line) return null;
+ const [, stmts, branch, funcs, ln] = line.split('|').map(s => s.trim());
+ return {stmts: parseFloat(stmts), branch: parseFloat(branch), funcs: parseFloat(funcs), ln: parseFloat(ln)};
+ };
+
+ const current = parseAllFiles(raw);
+ let baseline = null;
+ try { baseline = parseAllFiles(fs.readFileSync('/tmp/baseline/coverage-summary.txt', 'utf8')); } catch {}
+
+ const fmt = (val, baseVal) => {
+ const str = `**${val.toFixed(2)}%**`;
+ if (baseline === null || isNaN(baseVal)) return str;
+ const d = (val - baseVal).toFixed(2);
+ const sign = d >= 0 ? '+' : '';
+ const color = d > 0 ? '▲' : d < 0 ? '▼' : '●';
+ return `${str} ${color}${sign}${d}%`;
+ };
+
+ let coverageLine = '_Coverage data not found_';
+ if (current) {
+ const b = baseline || {};
+ coverageLine = [
+ `Stmts: ${fmt(current.stmts, b.stmts)}`,
+ `Branch: ${fmt(current.branch, b.branch)}`,
+ `Funcs: ${fmt(current.funcs, b.funcs)}`,
+ `Lines: ${fmt(current.ln, b.ln)}`,
+ ].join(' | ');
+ }
+
+ const testLine = lines.find(l => l.includes('Test Files')) || '';
+ const body = [
+ '## Coverage',
+ coverageLine,
+ testLine.trim() ? `> ${testLine.trim()}` : '',
+ ].filter(Boolean).join('\n');
+
+ const {data: comments} = await github.rest.issues.listComments({
+ owner: context.repo.owner,
+ repo: context.repo.repo,
+ issue_number: context.issue.number,
+ });
+ const existing = comments.find(c => c.body.startsWith('## Coverage'));
+ if (existing) {
+ await github.rest.issues.updateComment({
+ owner: context.repo.owner,
+ repo: context.repo.repo,
+ comment_id: existing.id,
+ body,
+ });
+ } else {
+ await github.rest.issues.createComment({
+ owner: context.repo.owner,
+ repo: context.repo.repo,
+ issue_number: context.issue.number,
+ body,
+ });
+ }
+
+ test-headless:
+ runs-on: ubuntu-22.04
+
+ steps:
+ - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
+
+ - name: Setup Node.js
+ uses: actions/setup-node@v4
+ with:
+ node-version: '20'
+
+ - name: Enable Corepack / Yarn 4
+ run: |
+ corepack enable
+ corepack prepare yarn@4.10.3 --activate
+
+ - name: Cache Yarn 4 artifacts
+ uses: actions/cache@v4
+ with:
+ path: |
+ .yarn/cache
+ .pnp.*
+ .yarn/install-state.gz
+ key: yarn-cache-${{ runner.os }}-${{ hashFiles('**/yarn.lock') }}
+ restore-keys: |
+ yarn-cache-${{ runner.os }}-
+
+ - name: Install dependencies
+ run: yarn
+
+ - name: Build packages
+ run: yarn build
+
+ - name: Install Playwright browsers
+ run: npx playwright install chromium --with-deps
+
+ - name: Run headless browser tests
+ run: yarn test-headless
diff --git a/dev/README.md b/dev/README.md
new file mode 100644
index 000000000..e8d88f4de
--- /dev/null
+++ b/dev/README.md
@@ -0,0 +1,10 @@
+# dev/
+
+Work-in-progress modules and examples. Not published to npm or indexed by the docs site.
+
+| Directory | Type | Status |
+|---|---|---|
+| `timeline-layers/` | module + examples | in progress — see #517, #379 |
+| `arrow-layers/` | examples | arrow-layers WIP examples |
+
+Design proposals and RFCs belong in [GitHub Issues/Discussions](https://github.com/visgl/deck.gl-community/issues).
diff --git a/examples/arrow-layers-wip/linestring/README.md b/dev/arrow-layers/linestring/README.md
similarity index 100%
rename from examples/arrow-layers-wip/linestring/README.md
rename to dev/arrow-layers/linestring/README.md
diff --git a/examples/arrow-layers-wip/linestring/app.tsx b/dev/arrow-layers/linestring/app.tsx
similarity index 100%
rename from examples/arrow-layers-wip/linestring/app.tsx
rename to dev/arrow-layers/linestring/app.tsx
diff --git a/examples/arrow-layers-wip/linestring/index.html b/dev/arrow-layers/linestring/index.html
similarity index 100%
rename from examples/arrow-layers-wip/linestring/index.html
rename to dev/arrow-layers/linestring/index.html
diff --git a/examples/arrow-layers-wip/linestring/package.json b/dev/arrow-layers/linestring/package.json
similarity index 100%
rename from examples/arrow-layers-wip/linestring/package.json
rename to dev/arrow-layers/linestring/package.json
diff --git a/examples/arrow-layers-wip/multilinestring/app.tsx b/dev/arrow-layers/multilinestring/app.tsx
similarity index 100%
rename from examples/arrow-layers-wip/multilinestring/app.tsx
rename to dev/arrow-layers/multilinestring/app.tsx
diff --git a/examples/arrow-layers-wip/multilinestring/index.html b/dev/arrow-layers/multilinestring/index.html
similarity index 100%
rename from examples/arrow-layers-wip/multilinestring/index.html
rename to dev/arrow-layers/multilinestring/index.html
diff --git a/examples/arrow-layers-wip/multilinestring/package.json b/dev/arrow-layers/multilinestring/package.json
similarity index 100%
rename from examples/arrow-layers-wip/multilinestring/package.json
rename to dev/arrow-layers/multilinestring/package.json
diff --git a/examples/arrow-layers-wip/multipoint/app.tsx b/dev/arrow-layers/multipoint/app.tsx
similarity index 100%
rename from examples/arrow-layers-wip/multipoint/app.tsx
rename to dev/arrow-layers/multipoint/app.tsx
diff --git a/examples/arrow-layers-wip/multipoint/index.html b/dev/arrow-layers/multipoint/index.html
similarity index 100%
rename from examples/arrow-layers-wip/multipoint/index.html
rename to dev/arrow-layers/multipoint/index.html
diff --git a/examples/arrow-layers-wip/multipoint/package.json b/dev/arrow-layers/multipoint/package.json
similarity index 100%
rename from examples/arrow-layers-wip/multipoint/package.json
rename to dev/arrow-layers/multipoint/package.json
diff --git a/examples/arrow-layers-wip/multipolygon/README.md b/dev/arrow-layers/multipolygon/README.md
similarity index 100%
rename from examples/arrow-layers-wip/multipolygon/README.md
rename to dev/arrow-layers/multipolygon/README.md
diff --git a/examples/arrow-layers-wip/multipolygon/app.tsx b/dev/arrow-layers/multipolygon/app.tsx
similarity index 100%
rename from examples/arrow-layers-wip/multipolygon/app.tsx
rename to dev/arrow-layers/multipolygon/app.tsx
diff --git a/examples/arrow-layers-wip/multipolygon/generate_data.py b/dev/arrow-layers/multipolygon/generate_data.py
similarity index 100%
rename from examples/arrow-layers-wip/multipolygon/generate_data.py
rename to dev/arrow-layers/multipolygon/generate_data.py
diff --git a/examples/arrow-layers-wip/multipolygon/index.html b/dev/arrow-layers/multipolygon/index.html
similarity index 100%
rename from examples/arrow-layers-wip/multipolygon/index.html
rename to dev/arrow-layers/multipolygon/index.html
diff --git a/examples/arrow-layers-wip/multipolygon/package.json b/dev/arrow-layers/multipolygon/package.json
similarity index 100%
rename from examples/arrow-layers-wip/multipolygon/package.json
rename to dev/arrow-layers/multipolygon/package.json
diff --git a/examples/arrow-layers-wip/multipolygon/poetry.lock.disable-dependabot b/dev/arrow-layers/multipolygon/poetry.lock.disable-dependabot
similarity index 100%
rename from examples/arrow-layers-wip/multipolygon/poetry.lock.disable-dependabot
rename to dev/arrow-layers/multipolygon/poetry.lock.disable-dependabot
diff --git a/examples/arrow-layers-wip/multipolygon/pyproject.toml b/dev/arrow-layers/multipolygon/pyproject.toml
similarity index 100%
rename from examples/arrow-layers-wip/multipolygon/pyproject.toml
rename to dev/arrow-layers/multipolygon/pyproject.toml
diff --git a/examples/arrow-layers-wip/point/README.md b/dev/arrow-layers/point/README.md
similarity index 100%
rename from examples/arrow-layers-wip/point/README.md
rename to dev/arrow-layers/point/README.md
diff --git a/examples/arrow-layers-wip/point/app.tsx b/dev/arrow-layers/point/app.tsx
similarity index 100%
rename from examples/arrow-layers-wip/point/app.tsx
rename to dev/arrow-layers/point/app.tsx
diff --git a/examples/arrow-layers-wip/point/generate_data.py b/dev/arrow-layers/point/generate_data.py
similarity index 100%
rename from examples/arrow-layers-wip/point/generate_data.py
rename to dev/arrow-layers/point/generate_data.py
diff --git a/examples/arrow-layers-wip/point/index.html b/dev/arrow-layers/point/index.html
similarity index 100%
rename from examples/arrow-layers-wip/point/index.html
rename to dev/arrow-layers/point/index.html
diff --git a/examples/arrow-layers-wip/point/package.json b/dev/arrow-layers/point/package.json
similarity index 100%
rename from examples/arrow-layers-wip/point/package.json
rename to dev/arrow-layers/point/package.json
diff --git a/examples/arrow-layers-wip/point/poetry.lock.disable-dependabot b/dev/arrow-layers/point/poetry.lock.disable-dependabot
similarity index 100%
rename from examples/arrow-layers-wip/point/poetry.lock.disable-dependabot
rename to dev/arrow-layers/point/poetry.lock.disable-dependabot
diff --git a/examples/arrow-layers-wip/point/pyproject.toml b/dev/arrow-layers/point/pyproject.toml
similarity index 100%
rename from examples/arrow-layers-wip/point/pyproject.toml
rename to dev/arrow-layers/point/pyproject.toml
diff --git a/examples/arrow-layers-wip/polygon/README.md b/dev/arrow-layers/polygon/README.md
similarity index 100%
rename from examples/arrow-layers-wip/polygon/README.md
rename to dev/arrow-layers/polygon/README.md
diff --git a/examples/arrow-layers-wip/polygon/app.tsx b/dev/arrow-layers/polygon/app.tsx
similarity index 100%
rename from examples/arrow-layers-wip/polygon/app.tsx
rename to dev/arrow-layers/polygon/app.tsx
diff --git a/examples/arrow-layers-wip/polygon/generate_data.py b/dev/arrow-layers/polygon/generate_data.py
similarity index 100%
rename from examples/arrow-layers-wip/polygon/generate_data.py
rename to dev/arrow-layers/polygon/generate_data.py
diff --git a/examples/arrow-layers-wip/polygon/index.html b/dev/arrow-layers/polygon/index.html
similarity index 100%
rename from examples/arrow-layers-wip/polygon/index.html
rename to dev/arrow-layers/polygon/index.html
diff --git a/examples/arrow-layers-wip/polygon/package.json b/dev/arrow-layers/polygon/package.json
similarity index 100%
rename from examples/arrow-layers-wip/polygon/package.json
rename to dev/arrow-layers/polygon/package.json
diff --git a/examples/arrow-layers-wip/polygon/poetry.lock.disable-dependabot b/dev/arrow-layers/polygon/poetry.lock.disable-dependabot
similarity index 100%
rename from examples/arrow-layers-wip/polygon/poetry.lock.disable-dependabot
rename to dev/arrow-layers/polygon/poetry.lock.disable-dependabot
diff --git a/examples/arrow-layers-wip/polygon/pyproject.toml b/dev/arrow-layers/polygon/pyproject.toml
similarity index 100%
rename from examples/arrow-layers-wip/polygon/pyproject.toml
rename to dev/arrow-layers/polygon/pyproject.toml
diff --git a/examples/arrow-layers-wip/text/app.tsx b/dev/arrow-layers/text/app.tsx
similarity index 100%
rename from examples/arrow-layers-wip/text/app.tsx
rename to dev/arrow-layers/text/app.tsx
diff --git a/examples/arrow-layers-wip/text/generate_data.py b/dev/arrow-layers/text/generate_data.py
similarity index 100%
rename from examples/arrow-layers-wip/text/generate_data.py
rename to dev/arrow-layers/text/generate_data.py
diff --git a/examples/arrow-layers-wip/text/index.html b/dev/arrow-layers/text/index.html
similarity index 100%
rename from examples/arrow-layers-wip/text/index.html
rename to dev/arrow-layers/text/index.html
diff --git a/examples/arrow-layers-wip/text/package.json b/dev/arrow-layers/text/package.json
similarity index 100%
rename from examples/arrow-layers-wip/text/package.json
rename to dev/arrow-layers/text/package.json
diff --git a/examples/arrow-layers-wip/text/poetry.lock.disable-dependabot b/dev/arrow-layers/text/poetry.lock.disable-dependabot
similarity index 100%
rename from examples/arrow-layers-wip/text/poetry.lock.disable-dependabot
rename to dev/arrow-layers/text/poetry.lock.disable-dependabot
diff --git a/examples/arrow-layers-wip/text/pyproject.toml b/dev/arrow-layers/text/pyproject.toml
similarity index 100%
rename from examples/arrow-layers-wip/text/pyproject.toml
rename to dev/arrow-layers/text/pyproject.toml
diff --git a/examples/arrow-layers-wip/trips/README.md b/dev/arrow-layers/trips/README.md
similarity index 100%
rename from examples/arrow-layers-wip/trips/README.md
rename to dev/arrow-layers/trips/README.md
diff --git a/examples/arrow-layers-wip/trips/app.tsx b/dev/arrow-layers/trips/app.tsx
similarity index 100%
rename from examples/arrow-layers-wip/trips/app.tsx
rename to dev/arrow-layers/trips/app.tsx
diff --git a/examples/arrow-layers-wip/trips/generate_data.py b/dev/arrow-layers/trips/generate_data.py
similarity index 100%
rename from examples/arrow-layers-wip/trips/generate_data.py
rename to dev/arrow-layers/trips/generate_data.py
diff --git a/examples/arrow-layers-wip/trips/index.html b/dev/arrow-layers/trips/index.html
similarity index 100%
rename from examples/arrow-layers-wip/trips/index.html
rename to dev/arrow-layers/trips/index.html
diff --git a/examples/arrow-layers-wip/trips/package.json b/dev/arrow-layers/trips/package.json
similarity index 100%
rename from examples/arrow-layers-wip/trips/package.json
rename to dev/arrow-layers/trips/package.json
diff --git a/examples/arrow-layers-wip/trips/poetry.lock.disable-dependabot b/dev/arrow-layers/trips/poetry.lock.disable-dependabot
similarity index 100%
rename from examples/arrow-layers-wip/trips/poetry.lock.disable-dependabot
rename to dev/arrow-layers/trips/poetry.lock.disable-dependabot
diff --git a/examples/arrow-layers-wip/trips/pyproject.toml b/dev/arrow-layers/trips/pyproject.toml
similarity index 100%
rename from examples/arrow-layers-wip/trips/pyproject.toml
rename to dev/arrow-layers/trips/pyproject.toml
diff --git a/modules/timeline-layers/LICENSE b/dev/timeline-layers/LICENSE
similarity index 100%
rename from modules/timeline-layers/LICENSE
rename to dev/timeline-layers/LICENSE
diff --git a/modules/timeline-layers/README.md b/dev/timeline-layers/README.md
similarity index 100%
rename from modules/timeline-layers/README.md
rename to dev/timeline-layers/README.md
diff --git a/examples/timeline-layers/horizon-graph-layer/app.tsx b/dev/timeline-layers/examples/horizon-graph-layer/app.tsx
similarity index 100%
rename from examples/timeline-layers/horizon-graph-layer/app.tsx
rename to dev/timeline-layers/examples/horizon-graph-layer/app.tsx
diff --git a/examples/timeline-layers/horizon-graph-layer/index.html b/dev/timeline-layers/examples/horizon-graph-layer/index.html
similarity index 100%
rename from examples/timeline-layers/horizon-graph-layer/index.html
rename to dev/timeline-layers/examples/horizon-graph-layer/index.html
diff --git a/examples/timeline-layers/horizon-graph-layer/index.tsx b/dev/timeline-layers/examples/horizon-graph-layer/index.tsx
similarity index 100%
rename from examples/timeline-layers/horizon-graph-layer/index.tsx
rename to dev/timeline-layers/examples/horizon-graph-layer/index.tsx
diff --git a/examples/timeline-layers/horizon-graph-layer/package.json b/dev/timeline-layers/examples/horizon-graph-layer/package.json
similarity index 100%
rename from examples/timeline-layers/horizon-graph-layer/package.json
rename to dev/timeline-layers/examples/horizon-graph-layer/package.json
diff --git a/examples/timeline-layers/horizon-graph-layer/tsconfig.json b/dev/timeline-layers/examples/horizon-graph-layer/tsconfig.json
similarity index 100%
rename from examples/timeline-layers/horizon-graph-layer/tsconfig.json
rename to dev/timeline-layers/examples/horizon-graph-layer/tsconfig.json
diff --git a/dev/timeline-layers/examples/timeline-layer/README.md b/dev/timeline-layers/examples/timeline-layer/README.md
new file mode 100644
index 000000000..d303681e4
--- /dev/null
+++ b/dev/timeline-layers/examples/timeline-layer/README.md
@@ -0,0 +1,52 @@
+# Timeline Layer Example
+
+An interactive GPU-accelerated timeline built with `@deck.gl-community/timeline-layers` — a deck.gl `CompositeLayer` that replaces DOM-driven timelines and handles hundreds of thousands of datapoints without lag.
+
+### Prototype
+
+https://github.com/user-attachments/assets/95006924-913f-4025-a0ef-6d74b336ca67
+
+### End Goal
+
+
+
+## Features
+
+- Multi-track timeline with clip visualization
+- Collision detection for overlapping clips (automatic subtrack assignment)
+- Interactive playhead scrubber with drag support
+- Zoom (mouse wheel) and pan (click + drag when zoomed)
+- Clip and track selection with hover highlighting
+- Multiple time formatting options
+
+## Usage
+
+```bash
+# Against installed packages
+yarn start
+
+# Against local source (dev)
+yarn start-local
+```
+
+## API
+
+```ts
+import {TimelineLayer} from '@deck.gl-community/timeline-layers';
+
+new TimelineLayer({
+ data: tracks, // TimelineTrack[]
+ timelineStart: 0, // start of full range (ms)
+ timelineEnd: 60_000, // end of full range (ms)
+ currentTimeMs: 5_000, // current playhead position
+ onClipClick: ({clip, track}) => console.log('clicked', clip.label),
+ onViewportChange: (startMs, endMs) => setViewport({startMs, endMs}),
+ onZoomChange: (zoom) => setZoom(zoom)
+});
+```
+
+See [`TimelineLayerProps`](../../src/layers/timeline-layer/timeline-types.ts) for the full API reference.
+
+## Context
+
+Originally prototyped in PR [#379](https://github.com/visgl/deck.gl-community/pull/379), refined in PR [#517](https://github.com/visgl/deck.gl-community/pull/517). Lives in `dev/timeline-layers` until promoted to `modules/` — tracked in [#38](https://github.com/visgl/deck.gl-community/issues/38).
diff --git a/dev/timeline-layers/examples/timeline-layer/app.tsx b/dev/timeline-layers/examples/timeline-layer/app.tsx
new file mode 100644
index 000000000..d3765d9c6
--- /dev/null
+++ b/dev/timeline-layers/examples/timeline-layer/app.tsx
@@ -0,0 +1,174 @@
+// deck.gl-community
+// SPDX-License-Identifier: MIT
+// Copyright (c) vis.gl contributors
+
+import React, {ReactElement, useMemo, useEffect} from 'react';
+import DeckGL from '@deck.gl/react';
+import {OrthographicView, type OrthographicViewState} from '@deck.gl/core';
+import {TimelineLayer} from '@deck.gl-community/timeline-layers';
+import {useTimelineControls, TimelineControls} from './demo-controls';
+import {
+ useTimelineInteractionState,
+ useTimelineRefs,
+ useGlobalMouseUpCleanup,
+ useWheelZoom,
+ useTimelineCallbacks,
+ useContainerHandlers,
+ useDeckGLHandlers,
+ useCursorGetter,
+ useTimelineResize
+} from './timeline-hooks';
+
+const ORTHOGRAPHIC_VIEW = new OrthographicView();
+
+const CONTROLLER_CONFIG = {
+ scrollZoom: false,
+ doubleClickZoom: false,
+ touchZoom: false,
+ dragPan: false,
+ dragRotate: false,
+ keyboard: false
+} as const;
+
+export default function App(): ReactElement {
+ const {state, controls, trackCount, clipsPerTrack, labelFormatterType} = useTimelineControls();
+ const {state: interactionState, setState} = useTimelineInteractionState();
+ const {timelineLayerRef, containerRef} = useTimelineRefs();
+
+ const initialViewState = useMemo((): OrthographicViewState => {
+ const w = (typeof window !== 'undefined' ? window.innerWidth : 1280) - 320;
+ const h = typeof window !== 'undefined' ? window.innerHeight : 720;
+ return {target: [w / 2, h / 2, 0], zoom: 0};
+ }, []);
+
+ useGlobalMouseUpCleanup(
+ setState.setIsDraggingScrubber,
+ setState.setIsPanning,
+ setState.setPanStartViewport
+ );
+ useWheelZoom(containerRef, timelineLayerRef, state.zoomLevel);
+ useTimelineResize(controls.setTimelineWidth);
+
+ const timelineCallbacks = useTimelineCallbacks(controls);
+
+ const containerHandlers = useContainerHandlers(
+ interactionState,
+ setState,
+ timelineLayerRef,
+ controls,
+ state
+ );
+
+ const deckGLHandlers = useDeckGLHandlers(setState, timelineLayerRef, controls, state);
+ const getCursor = useCursorGetter(interactionState, state.zoomLevel);
+
+ const viewport = useMemo(
+ () => ({startMs: state.viewportStartMs, endMs: state.viewportEndMs}),
+ [state.viewportStartMs, state.viewportEndMs]
+ );
+
+ const selectionStyle = useMemo(
+ () => ({
+ selectedClipColor: [255, 200, 0, 255] as [number, number, number, number],
+ hoveredClipColor: [200, 200, 200, 255] as [number, number, number, number],
+ selectedTrackColor: [80, 80, 80, 255] as [number, number, number, number],
+ hoveredTrackColor: [70, 70, 70, 255] as [number, number, number, number],
+ selectedLineWidth: state.selectedLineWidth,
+ hoveredLineWidth: state.hoveredLineWidth
+ }),
+ [state.selectedLineWidth, state.hoveredLineWidth]
+ );
+
+ const timelineLayer = useMemo(
+ () =>
+ new TimelineLayer({
+ id: 'timeline',
+ data: state.tracks,
+ timelineStart: state.timelineStart,
+ timelineEnd: state.timelineEnd,
+ x: state.timelineX,
+ y: state.timelineY,
+ width: state.timelineWidth,
+ trackHeight: state.trackHeight,
+ trackSpacing: state.trackSpacing,
+ currentTimeMs: state.currentTimeMs,
+ viewport,
+ timeFormatter: state.labelFormatter,
+ selectedClipId: state.selectedClip?.id,
+ hoveredClipId: state.hoveredClip?.id,
+ selectedTrackId: state.selectedTrack?.id,
+ hoveredTrackId: state.hoveredTrack?.id,
+ showTrackLabels: true,
+ showClipLabels: true,
+ showScrubber: true,
+ showAxis: true,
+ showSubtrackSeparators: state.showSubtrackSeparators,
+ selectionStyle,
+ onClipClick: timelineCallbacks.handleClipClick,
+ onClipHover: timelineCallbacks.handleClipHover,
+ onTrackClick: timelineCallbacks.handleTrackClick,
+ onTrackHover: timelineCallbacks.handleTrackHover,
+ onScrubberDrag: timelineCallbacks.handleScrubberDrag,
+ onTimelineClick: timelineCallbacks.handleTimelineClick,
+ onViewportChange: timelineCallbacks.handleViewportChange,
+ onZoomChange: timelineCallbacks.handleZoomChange
+ }),
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ [
+ state.tracks,
+ state.timelineStart,
+ state.timelineEnd,
+ state.timelineX,
+ state.timelineY,
+ state.timelineWidth,
+ state.trackHeight,
+ state.trackSpacing,
+ state.currentTimeMs,
+ viewport,
+ state.labelFormatter,
+ state.selectedClip?.id,
+ state.hoveredClip?.id,
+ state.selectedTrack?.id,
+ state.hoveredTrack?.id,
+ state.showSubtrackSeparators,
+ selectionStyle,
+ timelineCallbacks
+ ]
+ );
+
+ useEffect(() => {
+ timelineLayerRef.current = timelineLayer;
+ }, [timelineLayer, timelineLayerRef]);
+
+ return (
+
+ );
+}
diff --git a/dev/timeline-layers/examples/timeline-layer/demo-controls.tsx b/dev/timeline-layers/examples/timeline-layer/demo-controls.tsx
new file mode 100644
index 000000000..212907cb8
--- /dev/null
+++ b/dev/timeline-layers/examples/timeline-layer/demo-controls.tsx
@@ -0,0 +1,520 @@
+// deck.gl-community
+// SPDX-License-Identifier: MIT
+// Copyright (c) vis.gl contributors
+
+import React, {ReactElement, useMemo, useState} from 'react';
+import {
+ timeAxisFormatters,
+ type TimelineTrack,
+ type TimelineClip
+} from '@deck.gl-community/timeline-layers';
+import {generateRandomTracks} from './demo-utils';
+
+const PANEL_WIDTH = 320;
+const INITIAL_CANVAS_WIDTH = (typeof window !== 'undefined' ? window.innerWidth : 1280) - PANEL_WIDTH;
+
+export type TimelineControlsState = {
+ tracks: TimelineTrack[];
+ timelineStart: number;
+ timelineEnd: number;
+ currentTimeMs: number;
+ zoomLevel: number;
+ trackHeight: number;
+ trackSpacing: number;
+ timelineX: number;
+ timelineY: number;
+ timelineWidth: number;
+ viewportStartMs?: number;
+ viewportEndMs?: number;
+ labelFormatter: (timeMs: number) => string;
+ selectedClip: TimelineClip | null;
+ hoveredClip: TimelineClip | null;
+ selectedTrack: TimelineTrack | null;
+ hoveredTrack: TimelineTrack | null;
+ selectedLineWidth: number;
+ hoveredLineWidth: number;
+ showSubtrackSeparators: boolean;
+};
+
+export function useTimelineControls() {
+ const [trackCount, setTrackCount] = useState(4);
+ const [clipsPerTrack, setClipsPerTrack] = useState(3);
+ const [timelineStart] = useState(0);
+ const [timelineEnd, setTimelineEnd] = useState(60000);
+ const [currentTimeMs, setCurrentTimeMs] = useState(0);
+ const [zoomLevel, setZoomLevel] = useState(1);
+
+ const [trackHeight, setTrackHeight] = useState(40);
+ const [trackSpacing, setTrackSpacing] = useState(8);
+ const [timelineX] = useState(150);
+ const [timelineY] = useState(80);
+ const [timelineWidth, setTimelineWidth] = useState(INITIAL_CANVAS_WIDTH - 200);
+
+ const [viewportStartMs, setViewportStartMs] = useState(undefined);
+ const [viewportEndMs, setViewportEndMs] = useState(undefined);
+
+ const [selectedClip, setSelectedClip] = useState(null);
+ const [hoveredClip, setHoveredClip] = useState(null);
+ const [selectedTrack, setSelectedTrack] = useState(null);
+ const [hoveredTrack, setHoveredTrack] = useState(null);
+
+ const [labelFormatterType, setLabelFormatterType] =
+ useState('seconds');
+ const labelFormatter = timeAxisFormatters[labelFormatterType];
+
+ const [selectedLineWidth, setSelectedLineWidth] = useState(3);
+ const [hoveredLineWidth, setHoveredLineWidth] = useState(2);
+ const [showSubtrackSeparators, setShowSubtrackSeparators] = useState(true);
+
+ const tracks = useMemo(
+ () => generateRandomTracks(trackCount, clipsPerTrack, timelineStart, timelineEnd),
+ [trackCount, clipsPerTrack, timelineStart, timelineEnd]
+ );
+
+ const state: TimelineControlsState = useMemo(
+ () => ({
+ tracks,
+ timelineStart,
+ timelineEnd,
+ currentTimeMs,
+ zoomLevel,
+ trackHeight,
+ trackSpacing,
+ timelineX,
+ timelineY,
+ timelineWidth,
+ viewportStartMs,
+ viewportEndMs,
+ labelFormatter,
+ selectedClip,
+ hoveredClip,
+ selectedTrack,
+ hoveredTrack,
+ selectedLineWidth,
+ hoveredLineWidth,
+ showSubtrackSeparators
+ }),
+ [
+ tracks,
+ timelineStart,
+ timelineEnd,
+ currentTimeMs,
+ zoomLevel,
+ trackHeight,
+ trackSpacing,
+ timelineX,
+ timelineY,
+ timelineWidth,
+ viewportStartMs,
+ viewportEndMs,
+ labelFormatter,
+ selectedClip,
+ hoveredClip,
+ selectedTrack,
+ hoveredTrack,
+ selectedLineWidth,
+ hoveredLineWidth,
+ showSubtrackSeparators
+ ]
+ );
+
+ const controls = useMemo(
+ () => ({
+ setTrackCount,
+ setClipsPerTrack,
+ setTimelineEnd,
+ setCurrentTimeMs,
+ setZoomLevel,
+ setTrackHeight,
+ setTrackSpacing,
+ setTimelineWidth,
+ setViewportStartMs,
+ setViewportEndMs,
+ setLabelFormatterType,
+ setSelectedClip,
+ setHoveredClip,
+ setSelectedTrack,
+ setHoveredTrack,
+ setSelectedLineWidth,
+ setHoveredLineWidth,
+ setShowSubtrackSeparators
+ }),
+ []
+ );
+
+ return {state, controls, trackCount, clipsPerTrack, labelFormatterType};
+}
+
+export function TimelineControls({
+ state,
+ controls,
+ trackCount,
+ clipsPerTrack,
+ labelFormatterType
+}: {
+ state: TimelineControlsState;
+ controls: ReturnType['controls'];
+ trackCount: number;
+ clipsPerTrack: number;
+ labelFormatterType: string;
+}): ReactElement {
+ return (
+
+
+
Timeline Controls
+
+
+
+
+
+
+ controls.setTimelineEnd(state.timelineStart + v)}
+ />
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {state.selectedClip && (
+
controls.setSelectedClip(null)} />
+ )}
+
+ {state.selectedTrack && (
+ controls.setSelectedTrack(null)} />
+ )}
+
+
+ );
+}
+
+function Section({title, children}: {title: string; children: React.ReactNode}): ReactElement {
+ return (
+
+
{title}
+ {children}
+
+ );
+}
+
+function RangeField({
+ label,
+ min,
+ max,
+ step = 1,
+ value,
+ onChange
+}: {
+ label: string;
+ min: number;
+ max: number;
+ step?: number;
+ value: number;
+ onChange: (v: number) => void;
+}): ReactElement {
+ return (
+
+
+ onChange(Number(e.target.value))}
+ style={{width: '100%'}}
+ />
+
+ );
+}
+
+function SelectField({
+ label,
+ value,
+ options,
+ onChange
+}: {
+ label: string;
+ value: number;
+ options: number[];
+ onChange: (v: number) => void;
+}): ReactElement {
+ return (
+
+
+
+
+ );
+}
+
+function AdvancedSettings({
+ state,
+ controls
+}: {
+ state: TimelineControlsState;
+ controls: ReturnType['controls'];
+}): ReactElement {
+ const [isExpanded, setIsExpanded] = useState(false);
+
+ return (
+
+
setIsExpanded(!isExpanded)}
+ style={{
+ cursor: 'pointer',
+ padding: '10px',
+ background: '#f5f5f5',
+ border: '1px solid #ddd',
+ borderRadius: '4px',
+ display: 'flex',
+ justifyContent: 'space-between',
+ alignItems: 'center',
+ marginBottom: isExpanded ? '10px' : '0'
+ }}
+ >
+
Advanced Settings
+ {isExpanded ? '▼' : '▶'}
+
+
+ {isExpanded && (
+
+
+
+
+
+ )}
+
+ );
+}
+
+function ClipDetails({
+ clip,
+ formatter,
+ onClear
+}: {
+ clip: TimelineClip;
+ formatter: (timeMs: number) => string;
+ onClear: () => void;
+}): ReactElement {
+ return (
+
+
+ Selected Clip
+
+
+
+ Label: {clip.label}
+
+
+ Start: {formatter(clip.startMs)}
+
+
+ Duration: {formatter(clip.endMs - clip.startMs)}
+
+
+
+
+ );
+}
+
+function TrackDetails({
+ track,
+ onClear
+}: {
+ track: TimelineTrack;
+ onClear: () => void;
+}): ReactElement {
+ return (
+
+
+ Selected Track
+
+
+
+ Name: {track.name}
+
+
+ Clips: {track.clips.length}
+
+
+
+
+ );
+}
diff --git a/dev/timeline-layers/examples/timeline-layer/demo-utils.ts b/dev/timeline-layers/examples/timeline-layer/demo-utils.ts
new file mode 100644
index 000000000..012dcd143
--- /dev/null
+++ b/dev/timeline-layers/examples/timeline-layer/demo-utils.ts
@@ -0,0 +1,64 @@
+// deck.gl-community
+// SPDX-License-Identifier: MIT
+// Copyright (c) vis.gl contributors
+
+import type {TimelineClip, TimelineTrack} from '@deck.gl-community/timeline-layers';
+
+/**
+ * Generate random tracks with clips for demo purposes.
+ */
+export function generateRandomTracks(
+ trackCount: number,
+ clipsPerTrack: number,
+ timelineStart: number,
+ timelineEnd: number
+): TimelineTrack[] {
+ const colors: [number, number, number, number][] = [
+ [80, 120, 160, 220],
+ [120, 160, 180, 220],
+ [100, 140, 120, 220],
+ [140, 120, 140, 220],
+ [160, 140, 100, 220],
+ [120, 100, 140, 220],
+ [100, 120, 120, 220],
+ [140, 120, 100, 220]
+ ];
+
+ const tracks: TimelineTrack[] = [];
+
+ for (let trackIdx = 0; trackIdx < trackCount; trackIdx++) {
+ const clips: TimelineClip[] = [];
+
+ for (let clipIdx = 0; clipIdx < clipsPerTrack; clipIdx++) {
+ const duration = Math.random() * (timelineEnd - timelineStart) * 0.1;
+ let startMs: number;
+
+ // 30% chance of an overlapping clip (tests collision detection)
+ if (clipIdx > 0 && Math.random() < 0.3) {
+ const prevClip = clips[clips.length - 1];
+ const overlapPoint =
+ prevClip.startMs + (prevClip.endMs - prevClip.startMs) * (0.3 + Math.random() * 0.4);
+ startMs = overlapPoint;
+ } else {
+ startMs = Math.random() * (timelineEnd - timelineStart - duration) + timelineStart;
+ }
+
+ clips.push({
+ id: `track-${trackIdx}-clip-${clipIdx}`,
+ startMs,
+ endMs: startMs + duration,
+ label: `Clip ${clipIdx + 1}`,
+ color: colors[clipIdx % colors.length]
+ });
+ }
+
+ tracks.push({
+ id: trackIdx,
+ name: `Track ${trackIdx + 1}`,
+ visible: true,
+ clips
+ });
+ }
+
+ return tracks;
+}
diff --git a/dev/timeline-layers/examples/timeline-layer/index.html b/dev/timeline-layers/examples/timeline-layer/index.html
new file mode 100644
index 000000000..550c87aba
--- /dev/null
+++ b/dev/timeline-layers/examples/timeline-layer/index.html
@@ -0,0 +1,17 @@
+
+
+
+
+ Timeline Layer Example
+
+
+
+
+
+
diff --git a/dev/timeline-layers/examples/timeline-layer/index.tsx b/dev/timeline-layers/examples/timeline-layer/index.tsx
new file mode 100644
index 000000000..b884876b3
--- /dev/null
+++ b/dev/timeline-layers/examples/timeline-layer/index.tsx
@@ -0,0 +1,10 @@
+// deck.gl-community
+// SPDX-License-Identifier: MIT
+// Copyright (c) vis.gl contributors
+
+import React from 'react';
+import {createRoot} from 'react-dom/client';
+import App from './app';
+
+const root = createRoot(document.body.appendChild(document.createElement('div')));
+root.render();
diff --git a/dev/timeline-layers/examples/timeline-layer/package.json b/dev/timeline-layers/examples/timeline-layer/package.json
new file mode 100644
index 000000000..0307a6900
--- /dev/null
+++ b/dev/timeline-layers/examples/timeline-layer/package.json
@@ -0,0 +1,19 @@
+{
+ "license": "MIT",
+ "scripts": {
+ "start": "vite --open",
+ "start-local": "vite --config ../../vite.config.local.mjs"
+ },
+ "dependencies": {
+ "@deck.gl-community/timeline-layers": "workspace:*",
+ "@deck.gl/core": "~9.2.1",
+ "@deck.gl/layers": "~9.2.1",
+ "@deck.gl/react": "~9.2.1",
+ "@luma.gl/core": "~9.2.0",
+ "react": "^18.3.1",
+ "react-dom": "^18.3.1"
+ },
+ "devDependencies": {
+ "vite": "7.1.1"
+ }
+}
diff --git a/dev/timeline-layers/examples/timeline-layer/timeline-hooks.ts b/dev/timeline-layers/examples/timeline-layer/timeline-hooks.ts
new file mode 100644
index 000000000..82e866dc2
--- /dev/null
+++ b/dev/timeline-layers/examples/timeline-layer/timeline-hooks.ts
@@ -0,0 +1,467 @@
+// deck.gl-community
+// SPDX-License-Identifier: MIT
+// Copyright (c) vis.gl contributors
+
+import {useState, useRef, useEffect, useCallback, useMemo} from 'react';
+import type {TimelineLayer, TimelineClipInfo, TimelineTrackInfo} from '@deck.gl-community/timeline-layers';
+import {positionToTime} from '@deck.gl-community/timeline-layers';
+import type {TimelineControlsState} from './demo-controls';
+
+const DRAG_THRESHOLD = 5;
+
+interface TimelineInteractionState {
+ isDraggingScrubber: boolean;
+ isPanning: boolean;
+ panStartX: number;
+ panStartViewport: {start: number; end: number} | null;
+ mouseDownPos: {x: number; y: number} | null;
+ hoveredObjectId: string | null;
+}
+
+interface TimelineRefs {
+ timelineLayerRef: React.RefObject;
+ containerRef: React.RefObject;
+}
+
+interface TimelineControls {
+ setSelectedClip: (clip: unknown) => void;
+ setHoveredClip: (clip: unknown) => void;
+ setSelectedTrack: (track: unknown) => void;
+ setHoveredTrack: (track: unknown) => void;
+ setCurrentTimeMs: (timeMs: number) => void;
+ setViewportStartMs: (startMs: number | undefined) => void;
+ setViewportEndMs: (endMs: number | undefined) => void;
+ setZoomLevel: (zoomLevel: number) => void;
+}
+
+/** Hook for managing timeline interaction state */
+export function useTimelineInteractionState(): {
+ state: TimelineInteractionState;
+ setState: {
+ setIsDraggingScrubber: (value: boolean) => void;
+ setIsPanning: (value: boolean) => void;
+ setPanStartX: (value: number) => void;
+ setPanStartViewport: (value: {start: number; end: number} | null) => void;
+ setMouseDownPos: (value: {x: number; y: number} | null) => void;
+ setHoveredObjectId: (value: string | null) => void;
+ };
+} {
+ const [isDraggingScrubber, setIsDraggingScrubber] = useState(false);
+ const [isPanning, setIsPanning] = useState(false);
+ const [panStartX, setPanStartX] = useState(0);
+ const [panStartViewport, setPanStartViewport] = useState<{start: number; end: number} | null>(
+ null
+ );
+ const [mouseDownPos, setMouseDownPos] = useState<{x: number; y: number} | null>(null);
+ const [hoveredObjectId, setHoveredObjectId] = useState(null);
+
+ const state = useMemo(
+ () => ({isDraggingScrubber, isPanning, panStartX, panStartViewport, mouseDownPos, hoveredObjectId}),
+ [isDraggingScrubber, isPanning, panStartX, panStartViewport, mouseDownPos, hoveredObjectId]
+ );
+
+ const setState = useMemo(
+ () => ({
+ setIsDraggingScrubber,
+ setIsPanning,
+ setPanStartX,
+ setPanStartViewport,
+ setMouseDownPos,
+ setHoveredObjectId
+ }),
+ []
+ );
+
+ return {state, setState};
+}
+
+/** Hook for creating timeline refs */
+export function useTimelineRefs(): TimelineRefs {
+ const timelineLayerRef = useRef(null);
+ const containerRef = useRef(null);
+ return useMemo(() => ({timelineLayerRef, containerRef}), []);
+}
+
+/** Hook for global mouse-up cleanup */
+export function useGlobalMouseUpCleanup(
+ setIsDraggingScrubber: (value: boolean) => void,
+ setIsPanning: (value: boolean) => void,
+ setPanStartViewport: (value: {start: number; end: number} | null) => void
+): void {
+ useEffect(() => {
+ const handleGlobalMouseUp = () => {
+ setIsDraggingScrubber(false);
+ setIsPanning(false);
+ setPanStartViewport(null);
+ };
+ document.addEventListener('mouseup', handleGlobalMouseUp);
+ return () => document.removeEventListener('mouseup', handleGlobalMouseUp);
+ }, [setIsDraggingScrubber, setIsPanning, setPanStartViewport]);
+}
+
+/** Hook for mouse-wheel zoom */
+export function useWheelZoom(
+ containerRef: React.RefObject,
+ timelineLayerRef: React.RefObject,
+ zoomLevel: number
+): void {
+ useEffect(() => {
+ const container = containerRef.current;
+ if (!container) return undefined;
+
+ const handleWheel = (e: WheelEvent) => {
+ if (!timelineLayerRef.current) return;
+ e.preventDefault();
+ const rect = container.getBoundingClientRect();
+ const mouseX = e.clientX - rect.left;
+ const zoomFactor = e.deltaY > 0 ? 0.8 : 1.2;
+ timelineLayerRef.current.zoomToPoint(zoomFactor, mouseX, zoomLevel);
+ };
+
+ container.addEventListener('wheel', handleWheel, {passive: false});
+ return () => container.removeEventListener('wheel', handleWheel);
+ }, [containerRef, timelineLayerRef, zoomLevel]);
+}
+
+/** Hook for timeline layer event callbacks */
+export function useTimelineCallbacks(controls: TimelineControls): {
+ handleClipClick: (info: TimelineClipInfo) => void;
+ handleClipHover: (info: TimelineClipInfo | null) => void;
+ handleTrackClick: (info: TimelineTrackInfo) => void;
+ handleTrackHover: (info: TimelineTrackInfo | null) => void;
+ handleScrubberDrag: (timeMs: number) => void;
+ handleTimelineClick: (timeMs: number) => void;
+ handleViewportChange: (startMs: number, endMs: number) => void;
+ handleZoomChange: (zoomLevel: number) => void;
+} {
+ const {
+ setSelectedClip,
+ setHoveredClip,
+ setSelectedTrack,
+ setHoveredTrack,
+ setCurrentTimeMs,
+ setViewportStartMs,
+ setViewportEndMs,
+ setZoomLevel
+ } = controls;
+
+ const handleClipClick = useCallback((info: TimelineClipInfo) => setSelectedClip(info.clip), [setSelectedClip]);
+ const handleClipHover = useCallback((info: TimelineClipInfo | null) => setHoveredClip(info?.clip ?? null), [setHoveredClip]);
+ const handleTrackClick = useCallback((info: TimelineTrackInfo) => setSelectedTrack(info.track), [setSelectedTrack]);
+ const handleTrackHover = useCallback((info: TimelineTrackInfo | null) => setHoveredTrack(info?.track ?? null), [setHoveredTrack]);
+ const handleScrubberDrag = useCallback((timeMs: number) => setCurrentTimeMs(timeMs), [setCurrentTimeMs]);
+ const handleTimelineClick = useCallback((timeMs: number) => setCurrentTimeMs(timeMs), [setCurrentTimeMs]);
+
+ const handleViewportChange = useCallback(
+ (startMs: number, endMs: number) => {
+ setViewportStartMs(startMs);
+ setViewportEndMs(endMs);
+ },
+ [setViewportStartMs, setViewportEndMs]
+ );
+
+ const handleZoomChange = useCallback((zoomLevel: number) => setZoomLevel(zoomLevel), [setZoomLevel]);
+
+ return useMemo(
+ () => ({
+ handleClipClick,
+ handleClipHover,
+ handleTrackClick,
+ handleTrackHover,
+ handleScrubberDrag,
+ handleTimelineClick,
+ handleViewportChange,
+ handleZoomChange
+ }),
+ [
+ handleClipClick,
+ handleClipHover,
+ handleTrackClick,
+ handleTrackHover,
+ handleScrubberDrag,
+ handleTimelineClick,
+ handleViewportChange,
+ handleZoomChange
+ ]
+ );
+}
+
+/** Hook for container mouse event handlers (scrubber drag + pan) */
+export function useContainerHandlers(
+ interactionState: TimelineInteractionState,
+ setState: ReturnType['setState'],
+ _timelineLayerRef: React.RefObject,
+ controls: TimelineControls,
+ state: TimelineControlsState
+): {
+ handleContainerMouseDown: (e: React.MouseEvent) => void;
+ handleContainerMouseMove: (e: React.MouseEvent) => void;
+ handleContainerMouseUp: () => void;
+} {
+ const {setCurrentTimeMs, setViewportStartMs, setViewportEndMs} = controls;
+ const {setIsDraggingScrubber, setMouseDownPos, setIsPanning, setPanStartX, setPanStartViewport} =
+ setState;
+
+ const handleContainerMouseDown = useCallback(
+ (e: React.MouseEvent) => {
+ if ((e.target as HTMLElement).tagName !== 'CANVAS') return;
+
+ if (interactionState.hoveredObjectId === 'scrubber-handle') {
+ setIsDraggingScrubber(true);
+ return;
+ }
+
+ if (state.zoomLevel > 1.0) {
+ const rect = e.currentTarget.getBoundingClientRect();
+ setMouseDownPos({x: e.clientX - rect.left, y: e.clientY - rect.top});
+ }
+ },
+ [interactionState.hoveredObjectId, state.zoomLevel, setIsDraggingScrubber, setMouseDownPos]
+ );
+
+ const handleContainerMouseMove = useCallback(
+ (e: React.MouseEvent) => {
+ const rect = e.currentTarget.getBoundingClientRect();
+ const mouseX = e.clientX - rect.left;
+ const mouseY = e.clientY - rect.top;
+
+ // Scrubber dragging
+ if (interactionState.isDraggingScrubber) {
+ const effectiveStartMs = state.viewportStartMs ?? state.timelineStart;
+ const effectiveEndMs = state.viewportEndMs ?? state.timelineEnd;
+ const timeMs = positionToTime(
+ mouseX,
+ state.timelineX,
+ state.timelineWidth,
+ effectiveStartMs,
+ effectiveEndMs
+ );
+ setCurrentTimeMs(timeMs);
+ return;
+ }
+
+ // Pan start detection
+ if (
+ interactionState.mouseDownPos &&
+ !interactionState.isPanning &&
+ !interactionState.isDraggingScrubber
+ ) {
+ const dx = mouseX - interactionState.mouseDownPos.x;
+ const dy = mouseY - interactionState.mouseDownPos.y;
+ if (Math.sqrt(dx * dx + dy * dy) > DRAG_THRESHOLD && state.zoomLevel > 1.0) {
+ setIsPanning(true);
+ setPanStartX(interactionState.mouseDownPos.x);
+ setPanStartViewport({
+ start: state.viewportStartMs ?? state.timelineStart,
+ end: state.viewportEndMs ?? state.timelineEnd
+ });
+ setMouseDownPos(null);
+ }
+ }
+
+ // Pan update
+ if (interactionState.isPanning && interactionState.panStartViewport) {
+ e.preventDefault();
+ const deltaX = mouseX - interactionState.panStartX;
+ const currentRange = interactionState.panStartViewport.end - interactionState.panStartViewport.start;
+ const timeDelta = -(deltaX / state.timelineWidth) * currentRange;
+
+ let newStart = interactionState.panStartViewport.start + timeDelta;
+ let newEnd = interactionState.panStartViewport.end + timeDelta;
+
+ if (newStart < state.timelineStart) {
+ newStart = state.timelineStart;
+ newEnd = state.timelineStart + currentRange;
+ } else if (newEnd > state.timelineEnd) {
+ newEnd = state.timelineEnd;
+ newStart = state.timelineEnd - currentRange;
+ }
+
+ setViewportStartMs(newStart);
+ setViewportEndMs(newEnd);
+ }
+ },
+ [
+ interactionState,
+ state.zoomLevel,
+ state.timelineStart,
+ state.timelineEnd,
+ state.timelineWidth,
+ state.timelineX,
+ state.viewportStartMs,
+ state.viewportEndMs,
+ setCurrentTimeMs,
+ setIsPanning,
+ setPanStartX,
+ setPanStartViewport,
+ setMouseDownPos,
+ setViewportStartMs,
+ setViewportEndMs
+ ]
+ );
+
+ const handleContainerMouseUp = useCallback(() => {
+ setMouseDownPos(null);
+ if (interactionState.isDraggingScrubber) setIsDraggingScrubber(false);
+ if (interactionState.isPanning) {
+ setIsPanning(false);
+ setPanStartViewport(null);
+ }
+ }, [
+ interactionState.isDraggingScrubber,
+ interactionState.isPanning,
+ setMouseDownPos,
+ setIsDraggingScrubber,
+ setIsPanning,
+ setPanStartViewport
+ ]);
+
+ return useMemo(
+ () => ({handleContainerMouseDown, handleContainerMouseMove, handleContainerMouseUp}),
+ [handleContainerMouseDown, handleContainerMouseMove, handleContainerMouseUp]
+ );
+}
+
+/** Hook for DeckGL hover/click handlers */
+export function useDeckGLHandlers(
+ setState: ReturnType['setState'],
+ _timelineLayerRef: React.RefObject,
+ controls: TimelineControls,
+ state: TimelineControlsState
+): {
+ handleDeckGLHover: (info: any) => void;
+ handleDeckGLClick: (info: any) => void;
+} {
+ const {setCurrentTimeMs, setSelectedClip, setSelectedTrack, setHoveredClip, setHoveredTrack} =
+ controls;
+ const {setHoveredObjectId, setIsDraggingScrubber, setMouseDownPos} = setState;
+
+ const handleDeckGLHover = useCallback(
+ (info: any) => {
+ if (!info.object) {
+ setHoveredObjectId(null);
+ setHoveredClip(null);
+ setHoveredTrack(null);
+ return;
+ }
+
+ const objectId = String(info.object.id);
+ setHoveredObjectId(objectId);
+
+ if (objectId.startsWith('track-') && objectId.includes('-clip-')) {
+ if (info.object.clip) setHoveredClip(info.object.clip);
+ setHoveredTrack(null);
+ } else if (objectId.startsWith('track-bg-')) {
+ setHoveredClip(null);
+ if (info.object.track) setHoveredTrack(info.object.track);
+ } else {
+ setHoveredClip(null);
+ setHoveredTrack(null);
+ }
+ },
+ [setHoveredObjectId, setHoveredClip, setHoveredTrack]
+ );
+
+ const handleDeckGLClick = useCallback(
+ (info: any) => {
+ if (!info.object) {
+ if (info.coordinate) {
+ const effectiveStartMs = state.viewportStartMs ?? state.timelineStart;
+ const effectiveEndMs = state.viewportEndMs ?? state.timelineEnd;
+ const timeMs = positionToTime(
+ info.coordinate[0] || 0,
+ state.timelineX,
+ state.timelineWidth,
+ effectiveStartMs,
+ effectiveEndMs
+ );
+ setCurrentTimeMs(timeMs);
+ }
+ setSelectedClip(null);
+ setSelectedTrack(null);
+ return;
+ }
+
+ const objectId = String(info.object.id);
+
+ if (objectId.startsWith('track-') && objectId.includes('-clip-')) {
+ if (info.object.clip) setSelectedClip(info.object.clip);
+ setSelectedTrack(null);
+ } else if (objectId === 'scrubber-handle') {
+ setIsDraggingScrubber(true);
+ setMouseDownPos(null);
+ setSelectedClip(null);
+ setSelectedTrack(null);
+ } else if (objectId.startsWith('track-bg-')) {
+ if (info.coordinate) {
+ const effectiveStartMs = state.viewportStartMs ?? state.timelineStart;
+ const effectiveEndMs = state.viewportEndMs ?? state.timelineEnd;
+ const timeMs = positionToTime(
+ info.coordinate[0] || 0,
+ state.timelineX,
+ state.timelineWidth,
+ effectiveStartMs,
+ effectiveEndMs
+ );
+ setCurrentTimeMs(timeMs);
+ }
+ setSelectedClip(null);
+ if (info.object.track) setSelectedTrack(info.object.track);
+ } else {
+ setSelectedClip(null);
+ setSelectedTrack(null);
+ }
+ },
+ [
+ state.timelineX,
+ state.timelineWidth,
+ state.viewportStartMs,
+ state.viewportEndMs,
+ state.timelineStart,
+ state.timelineEnd,
+ setCurrentTimeMs,
+ setSelectedClip,
+ setSelectedTrack,
+ setIsDraggingScrubber,
+ setMouseDownPos
+ ]
+ );
+
+ return useMemo(
+ () => ({handleDeckGLHover, handleDeckGLClick}),
+ [handleDeckGLHover, handleDeckGLClick]
+ );
+}
+
+/** Hook for cursor style */
+export function useCursorGetter(
+ interactionState: TimelineInteractionState,
+ zoomLevel: number
+): ({isHovering}: {isHovering: boolean}) => string {
+ return useCallback(
+ ({isHovering}: {isHovering: boolean}) => {
+ if (interactionState.isDraggingScrubber || interactionState.isPanning) return 'grabbing';
+ if (interactionState.hoveredObjectId === 'scrubber-handle') return 'grab';
+ if (isHovering) return 'pointer';
+ if (zoomLevel > 1.0) return 'grab';
+ return 'default';
+ },
+ [
+ interactionState.isDraggingScrubber,
+ interactionState.isPanning,
+ interactionState.hoveredObjectId,
+ zoomLevel
+ ]
+ );
+}
+
+/** Hook to update timeline width on window resize */
+export function useTimelineResize(setTimelineWidth: (width: number) => void): void {
+ useEffect(() => {
+ const handleResize = () => {
+ setTimelineWidth(window.innerWidth - 320 - 200);
+ };
+ window.addEventListener('resize', handleResize);
+ return () => window.removeEventListener('resize', handleResize);
+ }, [setTimelineWidth]);
+}
diff --git a/dev/timeline-layers/examples/timeline-layer/tsconfig.json b/dev/timeline-layers/examples/timeline-layer/tsconfig.json
new file mode 100644
index 000000000..3fd35f2e6
--- /dev/null
+++ b/dev/timeline-layers/examples/timeline-layer/tsconfig.json
@@ -0,0 +1,4 @@
+{
+ "extends": "../../../../tsconfig.json",
+ "include": ["./*.tsx", "./*.ts"]
+}
diff --git a/modules/timeline-layers/package.json b/dev/timeline-layers/package.json
similarity index 100%
rename from modules/timeline-layers/package.json
rename to dev/timeline-layers/package.json
diff --git a/dev/timeline-layers/src/index.ts b/dev/timeline-layers/src/index.ts
new file mode 100644
index 000000000..31f419259
--- /dev/null
+++ b/dev/timeline-layers/src/index.ts
@@ -0,0 +1,55 @@
+// deck.gl-community
+// SPDX-License-Identifier: MIT
+// Copyright (c) vis.gl contributors
+
+// HORIZON GRAPH LAYERS
+export type {HorizonGraphLayerProps} from './layers/horizon-graph-layer/horizon-graph-layer';
+export {HorizonGraphLayer} from './layers/horizon-graph-layer/horizon-graph-layer';
+export type {MultiHorizonGraphLayerProps} from './layers/horizon-graph-layer/multi-horizon-graph-layer';
+export {MultiHorizonGraphLayer} from './layers/horizon-graph-layer/multi-horizon-graph-layer';
+
+// AXIS LAYERS
+export type {TimeAxisLayerProps} from './layers/time-axis-layer';
+export {TimeAxisLayer} from './layers/time-axis-layer';
+export type {VerticalGridLayerProps} from './layers/vertical-grid-layer';
+export {VerticalGridLayer} from './layers/vertical-grid-layer';
+export {formatTimeMs, formatTimeRangeMs} from './utils/format-utils';
+
+// TIMELINE LAYER
+export type {TimelineLayerProps} from './layers/timeline-layer/timeline-layer';
+export {TimelineLayer} from './layers/timeline-layer/timeline-layer';
+
+export type {
+ TimelineClip,
+ ClipWithSubtrack,
+ TimelineTrack,
+ TrackWithSubtracks,
+ TrackPosition,
+ TrackBackgroundData,
+ TrackLabelData,
+ ClipPolygonData,
+ ClipLabelData,
+ SeparatorLineData,
+ AxisLineData,
+ AxisLabelData,
+ ScrubberLineData,
+ ScrubberHandleData,
+ ScrubberLabelData,
+ TimelineTick,
+ TimelineViewport,
+ TimelineClipInfo,
+ TimelineTrackInfo,
+ TimelineScrubberInfo,
+ TimelineViewportInfo,
+ TimeAxisLabelFormatter
+} from './layers/timeline-layer/timeline-types';
+
+export type {SelectionStyle, TimelineLayout} from './layers/timeline-layer/timeline-layout';
+export {DEFAULT_TIMELINE_LAYOUT} from './layers/timeline-layer/timeline-layout';
+
+export {
+ positionToTime,
+ timeToPosition,
+ timeAxisFormatters,
+ type GenerateTicksOptions
+} from './layers/timeline-layer/timeline-utils';
diff --git a/modules/timeline-layers/src/layers/horizon-graph-layer/horizon-graph-layer-uniforms.ts b/dev/timeline-layers/src/layers/horizon-graph-layer/horizon-graph-layer-uniforms.ts
similarity index 100%
rename from modules/timeline-layers/src/layers/horizon-graph-layer/horizon-graph-layer-uniforms.ts
rename to dev/timeline-layers/src/layers/horizon-graph-layer/horizon-graph-layer-uniforms.ts
diff --git a/modules/timeline-layers/src/layers/horizon-graph-layer/horizon-graph-layer.fs.ts b/dev/timeline-layers/src/layers/horizon-graph-layer/horizon-graph-layer.fs.ts
similarity index 100%
rename from modules/timeline-layers/src/layers/horizon-graph-layer/horizon-graph-layer.fs.ts
rename to dev/timeline-layers/src/layers/horizon-graph-layer/horizon-graph-layer.fs.ts
diff --git a/modules/timeline-layers/src/layers/horizon-graph-layer/horizon-graph-layer.ts b/dev/timeline-layers/src/layers/horizon-graph-layer/horizon-graph-layer.ts
similarity index 100%
rename from modules/timeline-layers/src/layers/horizon-graph-layer/horizon-graph-layer.ts
rename to dev/timeline-layers/src/layers/horizon-graph-layer/horizon-graph-layer.ts
diff --git a/modules/timeline-layers/src/layers/horizon-graph-layer/horizon-graph-layer.vs.ts b/dev/timeline-layers/src/layers/horizon-graph-layer/horizon-graph-layer.vs.ts
similarity index 100%
rename from modules/timeline-layers/src/layers/horizon-graph-layer/horizon-graph-layer.vs.ts
rename to dev/timeline-layers/src/layers/horizon-graph-layer/horizon-graph-layer.vs.ts
diff --git a/modules/timeline-layers/src/layers/horizon-graph-layer/multi-horizon-graph-layer.ts b/dev/timeline-layers/src/layers/horizon-graph-layer/multi-horizon-graph-layer.ts
similarity index 100%
rename from modules/timeline-layers/src/layers/horizon-graph-layer/multi-horizon-graph-layer.ts
rename to dev/timeline-layers/src/layers/horizon-graph-layer/multi-horizon-graph-layer.ts
diff --git a/modules/timeline-layers/src/layers/time-axis-layer.ts b/dev/timeline-layers/src/layers/time-axis-layer.ts
similarity index 100%
rename from modules/timeline-layers/src/layers/time-axis-layer.ts
rename to dev/timeline-layers/src/layers/time-axis-layer.ts
diff --git a/dev/timeline-layers/src/layers/timeline-layer/timeline-collision.ts b/dev/timeline-layers/src/layers/timeline-layer/timeline-collision.ts
new file mode 100644
index 000000000..2062d139e
--- /dev/null
+++ b/dev/timeline-layers/src/layers/timeline-layer/timeline-collision.ts
@@ -0,0 +1,51 @@
+// deck.gl-community
+// SPDX-License-Identifier: MIT
+// Copyright (c) vis.gl contributors
+
+import type {TimelineClip, ClipWithSubtrack} from './timeline-types';
+
+export type {ClipWithSubtrack};
+/**
+ * Detects overlapping clips and assigns them to subtracks.
+ * Uses a greedy algorithm to minimize the number of subtracks needed.
+ */
+export function assignClipsToSubtracks(clips: TimelineClip[]): ClipWithSubtrack[] {
+ if (clips.length === 0) return [];
+
+ // Sort clips by start time
+ const sortedClips = [...clips].sort((a, b) => a.startMs - b.startMs);
+
+ // Track the end time of the last clip in each subtrack
+ const subtrackEndTimes: number[] = [];
+ const result: ClipWithSubtrack[] = [];
+
+ for (const clip of sortedClips) {
+ let assignedSubtrack = -1;
+
+ for (let i = 0; i < subtrackEndTimes.length; i++) {
+ if (clip.startMs >= subtrackEndTimes[i]) {
+ assignedSubtrack = i;
+ subtrackEndTimes[i] = clip.endMs;
+ break;
+ }
+ }
+
+ if (assignedSubtrack === -1) {
+ assignedSubtrack = subtrackEndTimes.length;
+ subtrackEndTimes.push(clip.endMs);
+ }
+
+ result.push({...clip, subtrackIndex: assignedSubtrack});
+ }
+
+ return result;
+}
+
+/**
+ * Calculate the number of subtracks needed for a set of clips.
+ */
+export function calculateSubtrackCount(clips: TimelineClip[]): number {
+ if (clips.length === 0) return 1;
+ const clipsWithSubtracks = assignClipsToSubtracks(clips);
+ return Math.max(...clipsWithSubtracks.map((c) => c.subtrackIndex)) + 1;
+}
diff --git a/dev/timeline-layers/src/layers/timeline-layer/timeline-layer.ts b/dev/timeline-layers/src/layers/timeline-layer/timeline-layer.ts
new file mode 100644
index 000000000..8348e93c1
--- /dev/null
+++ b/dev/timeline-layers/src/layers/timeline-layer/timeline-layer.ts
@@ -0,0 +1,868 @@
+// deck.gl-community
+// SPDX-License-Identifier: MIT
+// Copyright (c) vis.gl contributors
+
+import {CompositeLayer, COORDINATE_SYSTEM, type PickingInfo, type Layer} from '@deck.gl/core';
+import {SolidPolygonLayer, LineLayer, TextLayer} from '@deck.gl/layers';
+import type {CompositeLayerProps} from '@deck.gl/core';
+import type {LineLayerProps, SolidPolygonLayerProps, TextLayerProps} from '@deck.gl/layers';
+
+import type {
+ TimelineClipInfo,
+ TimelineTrackInfo,
+ TimelineTrack,
+ TrackWithSubtracks,
+ TrackPosition,
+ TrackBackgroundData,
+ TrackLabelData,
+ ClipPolygonData,
+ ClipLabelData,
+ ClipWithSubtrack,
+ SeparatorLineData,
+ AxisLineData,
+ AxisLabelData,
+ ScrubberLineData,
+ ScrubberHandleData,
+ ScrubberLabelData,
+ TimeAxisLabelFormatter
+} from './timeline-types';
+
+import type {SelectionStyle} from './timeline-layout';
+
+import {
+ timeAxisFormatters,
+ generateTimelineTicks,
+ timeToPosition,
+ positionToTime
+} from './timeline-utils';
+import {assignClipsToSubtracks, calculateSubtrackCount} from './timeline-collision';
+
+function lightenColor(
+ color: [number, number, number, number],
+ amount: number = 30
+): [number, number, number, number] {
+ return [
+ Math.min(255, color[0] + amount),
+ Math.min(255, color[1] + amount),
+ Math.min(255, color[2] + amount),
+ color[3]
+ ];
+}
+
+const defaultProps = {
+ x: 150,
+ y: 100,
+ width: 800,
+ trackHeight: 40,
+ trackSpacing: 10,
+ currentTimeMs: 0,
+ showScrubber: true,
+ showClipLabels: true,
+ showTrackLabels: true,
+ showAxis: true,
+ showSubtrackSeparators: true,
+ timeFormatter: timeAxisFormatters.seconds,
+ selectionStyle: {
+ selectedClipColor: [255, 200, 0, 255] as [number, number, number, number],
+ hoveredClipColor: [200, 200, 200, 255] as [number, number, number, number],
+ selectedTrackColor: [80, 80, 80, 255] as [number, number, number, number],
+ hoveredTrackColor: [70, 70, 70, 255] as [number, number, number, number],
+ selectedLineWidth: 3,
+ hoveredLineWidth: 2
+ }
+};
+
+export type TimelineLayerProps = CompositeLayerProps & {
+ /** Array of timeline tracks, each containing clips */
+ data: TimelineTrack[];
+ /** Start of the full timeline range in milliseconds */
+ timelineStart: number;
+ /** End of the full timeline range in milliseconds */
+ timelineEnd: number;
+
+ /** X offset of the timeline in canvas coordinates */
+ x?: number;
+ /** Y offset of the timeline in canvas coordinates */
+ y?: number;
+ /** Width of the timeline in canvas coordinates */
+ width?: number;
+ /** Height of each track row in canvas coordinates */
+ trackHeight?: number;
+ /** Spacing between tracks in canvas coordinates */
+ trackSpacing?: number;
+
+ /** Current playhead time in milliseconds */
+ currentTimeMs?: number;
+ /** Optional zoomed viewport range */
+ viewport?: {startMs?: number; endMs?: number};
+ /** Formatter for time axis labels */
+ timeFormatter?: TimeAxisLabelFormatter;
+
+ /** ID of the currently selected clip */
+ selectedClipId?: string | number | null;
+ /** ID of the currently hovered clip */
+ hoveredClipId?: string | number | null;
+ /** ID of the currently selected track */
+ selectedTrackId?: string | number | null;
+ /** ID of the currently hovered track */
+ hoveredTrackId?: string | number | null;
+ /** Colors and line widths for selected/hovered states */
+ selectionStyle?: SelectionStyle;
+
+ /** Whether to show the playhead scrubber */
+ showScrubber?: boolean;
+ /** Whether to show labels on clips */
+ showClipLabels?: boolean;
+ /** Whether to show labels on tracks */
+ showTrackLabels?: boolean;
+ /** Whether to show the time axis */
+ showAxis?: boolean;
+ /** Whether to show separators between collision subtracks */
+ showSubtrackSeparators?: boolean;
+
+ /** Override props for the clip polygon sub-layer */
+ clipProps?: Partial>;
+ /** Override props for the track background sub-layer */
+ trackProps?: Partial>;
+ /** Override props for the track label sub-layer */
+ trackLabelProps?: Partial>;
+ /** Override props for the clip label sub-layer */
+ clipLabelProps?: Partial>;
+ /** Override props for the axis line sub-layer */
+ axisLineProps?: Partial>;
+ /** Override props for the axis label sub-layer */
+ axisLabelProps?: Partial>;
+ /** Override props for the scrubber line sub-layer */
+ scrubberLineProps?: Partial>;
+
+ /** Callback when a clip is clicked */
+ onClipClick?: (info: TimelineClipInfo, event: PickingInfo) => void;
+ /** Callback when a clip is hovered */
+ onClipHover?: (info: TimelineClipInfo | null, event: PickingInfo) => void;
+ /** Callback when a track is clicked */
+ onTrackClick?: (info: TimelineTrackInfo, event: PickingInfo) => void;
+ /** Callback when a track is hovered */
+ onTrackHover?: (info: TimelineTrackInfo | null, event: PickingInfo) => void;
+ /** Callback when the scrubber handle is hovered */
+ onScrubberHover?: (isHovering: boolean, event: PickingInfo) => void;
+ /** Callback when a scrubber drag begins */
+ onScrubberDragStart?: (event: PickingInfo) => void;
+ /** Callback when the scrubber is dragged to a new time */
+ onScrubberDrag?: (timeMs: number, event: PickingInfo) => void;
+ /** Callback when the timeline background is clicked */
+ onTimelineClick?: (timeMs: number, event: PickingInfo) => void;
+
+ /** Callback when the current time changes */
+ onCurrentTimeChange?: (timeMs: number) => void;
+ /** Callback when the viewport (zoom/pan) changes */
+ onViewportChange?: (startMs: number, endMs: number) => void;
+ /** Callback when the zoom level changes */
+ onZoomChange?: (zoomLevel: number) => void;
+};
+
+export class TimelineLayer extends CompositeLayer {
+ static layerName = 'TimelineLayer';
+ static defaultProps = defaultProps;
+
+ /** Convert a canvas X coordinate to a time in milliseconds */
+ getTimeFromPosition(x: number): number {
+ const {timelineStart, timelineEnd, viewport, x: timelineX = 150, width = 800} = this.props;
+ const effectiveStartMs = viewport?.startMs ?? timelineStart;
+ const effectiveEndMs = viewport?.endMs ?? timelineEnd;
+ return positionToTime(x, timelineX, width, effectiveStartMs, effectiveEndMs);
+ }
+
+ /** Zoom the timeline viewport around a canvas X coordinate */
+ zoomToPoint(zoomFactor: number, mouseX: number, currentZoomLevel: number): void {
+ const {
+ timelineStart,
+ timelineEnd,
+ viewport,
+ x: timelineX = 150,
+ width = 800,
+ onViewportChange,
+ onZoomChange
+ } = this.props;
+
+ const newZoomLevel = Math.max(1.0, Math.min(100, currentZoomLevel * zoomFactor));
+ const mouseRatio = Math.max(0, Math.min(1, (mouseX - timelineX) / width));
+ const currentStartMs = viewport?.startMs ?? timelineStart;
+ const currentEndMs = viewport?.endMs ?? timelineEnd;
+ const mouseTimeMs = currentStartMs + mouseRatio * (currentEndMs - currentStartMs);
+
+ const fullTimeRange = timelineEnd - timelineStart;
+ const newViewportRange = fullTimeRange / newZoomLevel;
+
+ let newStartMs = mouseTimeMs - mouseRatio * newViewportRange;
+ let newEndMs = newStartMs + newViewportRange;
+
+ if (newStartMs < timelineStart) {
+ newStartMs = timelineStart;
+ newEndMs = timelineStart + newViewportRange;
+ } else if (newEndMs > timelineEnd) {
+ newEndMs = timelineEnd;
+ newStartMs = timelineEnd - newViewportRange;
+ }
+
+ if (newZoomLevel > 1.0) {
+ onViewportChange?.(newStartMs, newEndMs);
+ } else {
+ onViewportChange?.(timelineStart, timelineEnd);
+ }
+
+ onZoomChange?.(newZoomLevel);
+ }
+
+ // ===== LAYOUT CALCULATION =====
+
+ private _calculateTrackPositions(
+ tracksWithSubtracks: TrackWithSubtracks[],
+ y: number,
+ trackHeight: number,
+ trackSpacing: number
+ ): {trackPositions: TrackPosition[]; totalTimelineHeight: number} {
+ let currentY = y;
+ const trackPositions: TrackPosition[] = [];
+ const subtrackSpacing = 2;
+
+ for (const {subtrackCount} of tracksWithSubtracks) {
+ const trackTotalHeight = subtrackCount * trackHeight + (subtrackCount - 1) * subtrackSpacing;
+ trackPositions.push({y: currentY, height: trackTotalHeight, subtrackCount});
+ currentY += trackTotalHeight + trackSpacing;
+ }
+
+ const totalTimelineHeight = currentY - y - trackSpacing;
+ return {trackPositions, totalTimelineHeight};
+ }
+
+ // ===== DATA GENERATION =====
+
+ private _generateTrackBackgrounds(
+ tracksWithSubtracks: TrackWithSubtracks[],
+ trackPositions: TrackPosition[]
+ ): TrackBackgroundData[] {
+ const {
+ x = 150,
+ width = 800,
+ selectedTrackId,
+ hoveredTrackId,
+ selectionStyle = defaultProps.selectionStyle
+ } = this.props;
+
+ return tracksWithSubtracks.map(({track, trackIndex}, i) => {
+ const {y: trackY, height} = trackPositions[i];
+ const isSelected = selectedTrackId === track.id;
+ const isHovered = hoveredTrackId === track.id;
+
+ let color: [number, number, number, number] = [60, 60, 60, 255];
+ if (isSelected) {
+ color = selectionStyle.selectedTrackColor!;
+ } else if (isHovered) {
+ color = lightenColor([60, 60, 60, 255], 20);
+ }
+
+ return {
+ id: `track-bg-${track.id}`,
+ track,
+ trackIndex,
+ polygon: [
+ [x, trackY],
+ [x + width, trackY],
+ [x + width, trackY + height],
+ [x, trackY + height]
+ ],
+ color
+ };
+ });
+ }
+
+ private _generateTrackLabels(
+ tracksWithSubtracks: TrackWithSubtracks[],
+ trackPositions: TrackPosition[]
+ ): TrackLabelData[] {
+ const {x = 150, showTrackLabels = true} = this.props;
+ if (!showTrackLabels) return [];
+
+ return tracksWithSubtracks.map(({track}, i) => {
+ const label = track.name || `Track ${track.id}`;
+ const {y: trackY, height} = trackPositions[i];
+ return {text: label, position: [x - 10, trackY + height / 2, 0]};
+ });
+ }
+
+ private _buildClipPolygon(
+ clip: ClipWithSubtrack,
+ opts: {
+ track: TimelineTrack;
+ trackIndex: number;
+ clipIndex: number;
+ subtrackHeight: number;
+ baseTrackY: number;
+ x: number;
+ width: number;
+ effectiveStartMs: number;
+ effectiveEndMs: number;
+ selectedClipId: string | number | null | undefined;
+ hoveredClipId: string | number | null | undefined;
+ selectionStyle: SelectionStyle;
+ }
+ ): ClipPolygonData | null {
+ const {id: clipId, startMs, endMs, subtrackIndex = 0} = clip;
+ const {
+ track,
+ trackIndex,
+ clipIndex,
+ subtrackHeight,
+ baseTrackY,
+ x,
+ width,
+ effectiveStartMs,
+ effectiveEndMs,
+ selectedClipId,
+ hoveredClipId,
+ selectionStyle
+ } = opts;
+
+ if (endMs <= effectiveStartMs || startMs >= effectiveEndMs) return null;
+
+ const clipPadding = 2;
+ const subtrackSpacing = 2;
+ const clipTrackY = baseTrackY + subtrackIndex * (subtrackHeight + subtrackSpacing);
+ const clipStartRatio = (startMs - effectiveStartMs) / (effectiveEndMs - effectiveStartMs);
+ const clipEndRatio = (endMs - effectiveStartMs) / (effectiveEndMs - effectiveStartMs);
+ const clipStartX = x + Math.max(0, clipStartRatio) * width;
+ const clipEndX = x + Math.min(1, clipEndRatio) * width;
+
+ const baseColor = clip.color || ([80, 120, 160, 220] as [number, number, number, number]);
+ const isSelected = selectedClipId !== null && String(selectedClipId) === String(clipId);
+ const isHovered = hoveredClipId !== null && String(hoveredClipId) === String(clipId);
+
+ let color = baseColor;
+ if (isSelected) {
+ color = selectionStyle.selectedClipColor!;
+ } else if (isHovered) {
+ color = lightenColor(baseColor, 40);
+ }
+
+ return {
+ id: clipId,
+ clip,
+ track,
+ clipIndex,
+ trackIndex,
+ subtrackIndex,
+ polygon: [
+ [clipStartX, clipTrackY + clipPadding],
+ [clipEndX, clipTrackY + clipPadding],
+ [clipEndX, clipTrackY + subtrackHeight - clipPadding],
+ [clipStartX, clipTrackY + subtrackHeight - clipPadding]
+ ],
+ color,
+ label: clip.label || '',
+ labelPosition: [clipStartX + (clipEndX - clipStartX) / 2, clipTrackY + subtrackHeight / 2, 0]
+ };
+ }
+
+ private _generateClipPolygons(
+ tracksWithSubtracks: TrackWithSubtracks[],
+ trackPositions: TrackPosition[],
+ effectiveStartMs: number,
+ effectiveEndMs: number
+ ): ClipPolygonData[] {
+ const {
+ x = 150,
+ width = 800,
+ selectedClipId,
+ hoveredClipId,
+ selectionStyle = defaultProps.selectionStyle
+ } = this.props;
+
+ const subtrackSpacing = 2;
+ const clipPolygons: ClipPolygonData[] = [];
+
+ for (let i = 0; i < tracksWithSubtracks.length; i++) {
+ const {track, trackIndex, clips, subtrackCount} = tracksWithSubtracks[i];
+ const {y: baseTrackY, height: trackTotalHeight} = trackPositions[i];
+ const subtrackHeight =
+ (trackTotalHeight - (subtrackCount - 1) * subtrackSpacing) / subtrackCount;
+
+ for (let clipIndex = 0; clipIndex < clips.length; clipIndex++) {
+ const polygon = this._buildClipPolygon(clips[clipIndex], {
+ track,
+ trackIndex,
+ clipIndex,
+ subtrackHeight,
+ baseTrackY,
+ x,
+ width,
+ effectiveStartMs,
+ effectiveEndMs,
+ selectedClipId,
+ hoveredClipId,
+ selectionStyle
+ });
+ if (polygon) {
+ clipPolygons.push(polygon);
+ }
+ }
+ }
+
+ return clipPolygons;
+ }
+
+ private _generateSubtrackSeparators(
+ tracksWithSubtracks: TrackWithSubtracks[],
+ trackPositions: TrackPosition[]
+ ): SeparatorLineData[] {
+ const {x = 150, width = 800, showSubtrackSeparators = true} = this.props;
+ if (!showSubtrackSeparators) return [];
+
+ const subtrackSpacing = 2;
+ const separatorLines: SeparatorLineData[] = [];
+
+ for (let i = 0; i < tracksWithSubtracks.length; i++) {
+ const {subtrackCount} = tracksWithSubtracks[i];
+ if (subtrackCount <= 1) {
+ // No separators needed for single-subtrack rows
+ } else {
+ const {y: baseTrackY, height: trackTotalHeight} = trackPositions[i];
+ const subtrackHeight =
+ (trackTotalHeight - (subtrackCount - 1) * subtrackSpacing) / subtrackCount;
+
+ for (let j = 1; j < subtrackCount; j++) {
+ const separatorY =
+ baseTrackY + j * (subtrackHeight + subtrackSpacing) - subtrackSpacing / 2;
+ separatorLines.push({
+ sourcePosition: [x, separatorY],
+ targetPosition: [x + width, separatorY]
+ });
+ }
+ }
+ }
+
+ return separatorLines;
+ }
+
+ private _generateAxis(
+ totalTimelineHeight: number,
+ effectiveStartMs: number,
+ effectiveEndMs: number
+ ): {axisLines: AxisLineData[]; axisLabels: AxisLabelData[]} {
+ const {
+ x = 150,
+ y = 100,
+ width = 800,
+ showAxis = true,
+ timeFormatter = timeAxisFormatters.seconds
+ } = this.props;
+
+ const axisLines: AxisLineData[] = [];
+ const axisLabels: AxisLabelData[] = [];
+
+ if (!showAxis) return {axisLines, axisLabels};
+
+ const axisHeight = 30;
+ const tickCount = Math.max(4, Math.min(10, Math.floor(width / 80)));
+
+ const timelineTicks = generateTimelineTicks({
+ startMs: effectiveStartMs,
+ endMs: effectiveEndMs,
+ timelineX: x,
+ timelineWidth: width,
+ tickCount,
+ formatter: timeFormatter
+ });
+
+ const axisY = y + totalTimelineHeight + axisHeight;
+
+ axisLines.push({sourcePosition: [x, axisY], targetPosition: [x + width, axisY]});
+
+ for (const tick of timelineTicks) {
+ axisLines.push({
+ sourcePosition: [tick.position, axisY - 5],
+ targetPosition: [tick.position, axisY + 5]
+ });
+ axisLabels.push({text: tick.label, position: [tick.position, axisY + 15, 0]});
+ }
+
+ return {axisLines, axisLabels};
+ }
+
+ private _generateScrubber(
+ totalTimelineHeight: number,
+ effectiveStartMs: number,
+ effectiveEndMs: number
+ ): {
+ scrubberLine: ScrubberLineData[];
+ scrubberHandle: ScrubberHandleData[];
+ scrubberLabel: ScrubberLabelData[];
+ } {
+ const {
+ x = 150,
+ y = 100,
+ width = 800,
+ showScrubber = true,
+ currentTimeMs = 0,
+ timeFormatter = timeAxisFormatters.seconds
+ } = this.props;
+
+ if (!showScrubber) return {scrubberLine: [], scrubberHandle: [], scrubberLabel: []};
+
+ const scrubberPosition = timeToPosition(
+ currentTimeMs,
+ x,
+ width,
+ effectiveStartMs,
+ effectiveEndMs
+ );
+
+ const scrubberLine: ScrubberLineData[] = [
+ {
+ sourcePosition: [scrubberPosition, y - 30],
+ targetPosition: [scrubberPosition, y + totalTimelineHeight + 30]
+ }
+ ];
+
+ const scrubberHandle: ScrubberHandleData[] = [
+ {
+ id: 'scrubber-handle',
+ polygon: [
+ [scrubberPosition - 8, y - 35],
+ [scrubberPosition + 8, y - 35],
+ [scrubberPosition + 8, y - 20],
+ [scrubberPosition - 8, y - 20]
+ ],
+ color: [255, 100, 100, 255]
+ }
+ ];
+
+ const scrubberLabel: ScrubberLabelData[] = [
+ {text: timeFormatter(currentTimeMs), position: [scrubberPosition, y - 40, 0]}
+ ];
+
+ return {scrubberLine, scrubberHandle, scrubberLabel};
+ }
+
+ // ===== LAYER CREATION =====
+
+ private _createTrackLayers(
+ trackBackgrounds: TrackBackgroundData[],
+ trackLabels: TrackLabelData[]
+ ): Layer[] {
+ const {trackProps, showTrackLabels = true, onTrackClick, onTrackHover} = this.props;
+ const layers: Layer[] = [];
+
+ layers.push(
+ new SolidPolygonLayer(
+ this.getSubLayerProps({
+ ...trackProps,
+ id: 'tracks',
+ data: trackBackgrounds,
+ getPolygon: (d: TrackBackgroundData) => d.polygon,
+ getFillColor: (d: TrackBackgroundData) => d.color,
+ stroked: true,
+ getLineColor: [100, 100, 100, 255],
+ getLineWidth: 1,
+ pickable: Boolean(onTrackClick) || Boolean(onTrackHover),
+ onClick: (info: PickingInfo) => {
+ if (info.object && onTrackClick) {
+ const obj = info.object as TrackBackgroundData;
+ onTrackClick({track: obj.track, index: obj.trackIndex}, info);
+ }
+ },
+ onHover: (info: PickingInfo) => {
+ if (onTrackHover) {
+ const obj = info.object as TrackBackgroundData | undefined;
+ onTrackHover(obj ? {track: obj.track, index: obj.trackIndex} : null, info);
+ }
+ }
+ })
+ )
+ );
+
+ if (showTrackLabels) {
+ layers.push(
+ new TextLayer({
+ id: `${this.props.id}-track-labels`,
+ data: trackLabels,
+ getText: (d: TrackLabelData) => d.text,
+ getPosition: (d: TrackLabelData) => d.position,
+ getSize: 12,
+ getColor: [60, 60, 60, 255],
+ getTextAnchor: 'end',
+ getAlignmentBaseline: 'center',
+ fontFamily: 'Arial, sans-serif',
+ fontWeight: 'bold',
+ coordinateSystem: COORDINATE_SYSTEM.CARTESIAN
+ })
+ );
+ }
+
+ return layers;
+ }
+
+ private _createClipLayers(
+ clipPolygons: ClipPolygonData[],
+ clipLabelsData: ClipLabelData[]
+ ): Layer[] {
+ const {
+ clipProps,
+ showClipLabels = true,
+ selectedClipId,
+ selectionStyle = defaultProps.selectionStyle,
+ onClipClick,
+ onClipHover
+ } = this.props;
+ const layers: Layer[] = [];
+
+ layers.push(
+ new SolidPolygonLayer(
+ this.getSubLayerProps({
+ ...clipProps,
+ id: 'clips',
+ data: clipPolygons,
+ getPolygon: (d: ClipPolygonData) => d.polygon,
+ getFillColor: (d: ClipPolygonData) => d.color,
+ stroked: true,
+ getLineColor: [255, 255, 255, 200],
+ getLineWidth: (d: ClipPolygonData) => {
+ const isSelected = selectedClipId !== null && String(selectedClipId) === String(d.id);
+ return isSelected ? selectionStyle.selectedLineWidth || 3 : 2;
+ },
+ pickable: Boolean(onClipClick) || Boolean(onClipHover),
+ autoHighlight: true,
+ onClick: (info: PickingInfo) => {
+ if (info.object && onClipClick) {
+ const obj = info.object as ClipPolygonData;
+ onClipClick(
+ {
+ clip: obj.clip,
+ track: obj.track,
+ clipIndex: obj.clipIndex,
+ trackIndex: obj.trackIndex,
+ subtrackIndex: obj.subtrackIndex
+ },
+ info
+ );
+ }
+ },
+ onHover: (info: PickingInfo) => {
+ if (onClipHover) {
+ const obj = info.object as ClipPolygonData | undefined;
+ onClipHover(
+ obj
+ ? {
+ clip: obj.clip,
+ track: obj.track,
+ clipIndex: obj.clipIndex,
+ trackIndex: obj.trackIndex,
+ subtrackIndex: obj.subtrackIndex
+ }
+ : null,
+ info
+ );
+ }
+ }
+ })
+ )
+ );
+
+ if (showClipLabels) {
+ layers.push(
+ new TextLayer({
+ id: `${this.props.id}-clip-labels`,
+ data: clipLabelsData,
+ getText: (d: ClipLabelData) => d.text,
+ getPosition: (d: ClipLabelData) => d.position,
+ getSize: 10,
+ getColor: [255, 255, 255, 255],
+ getTextAnchor: 'middle',
+ getAlignmentBaseline: 'center',
+ fontFamily: 'Arial, sans-serif',
+ coordinateSystem: COORDINATE_SYSTEM.CARTESIAN
+ })
+ );
+ }
+
+ return layers;
+ }
+
+ private _createSubtrackSeparatorLayer(separators: SeparatorLineData[]): Layer | null {
+ const {showSubtrackSeparators = true} = this.props;
+ if (!showSubtrackSeparators) return null;
+
+ return new LineLayer({
+ id: `${this.props.id}-subtrack-separators`,
+ data: separators,
+ getSourcePosition: (d: SeparatorLineData) => d.sourcePosition,
+ getTargetPosition: (d: SeparatorLineData) => d.targetPosition,
+ getColor: [180, 180, 180, 128],
+ getWidth: 1,
+ coordinateSystem: COORDINATE_SYSTEM.CARTESIAN
+ });
+ }
+
+ private _createAxisLayers(axisLines: AxisLineData[], axisLabels: AxisLabelData[]): Layer[] {
+ const {axisLineProps, axisLabelProps, showAxis = true} = this.props;
+ if (!showAxis) return [];
+
+ return [
+ new LineLayer(
+ this.getSubLayerProps({
+ ...axisLineProps,
+ id: 'axis-lines',
+ data: axisLines,
+ getSourcePosition: (d: AxisLineData) => d.sourcePosition,
+ getTargetPosition: (d: AxisLineData) => d.targetPosition,
+ getColor: [150, 150, 150, 255],
+ getWidth: 2
+ })
+ ),
+ new TextLayer(
+ this.getSubLayerProps({
+ ...axisLabelProps,
+ id: 'axis-labels',
+ data: axisLabels,
+ getText: (d: AxisLabelData) => d.text,
+ getPosition: (d: AxisLabelData) => d.position,
+ getSize: 11,
+ getColor: [150, 150, 150, 255],
+ getTextAnchor: 'middle',
+ getAlignmentBaseline: 'top'
+ })
+ )
+ ];
+ }
+
+ private _createScrubberLayers(
+ scrubberLine: ScrubberLineData[],
+ scrubberHandle: ScrubberHandleData[],
+ scrubberLabel: ScrubberLabelData[]
+ ): Layer[] {
+ const {
+ scrubberLineProps,
+ showScrubber = true,
+ onScrubberDragStart,
+ onScrubberHover
+ } = this.props;
+ if (!showScrubber) return [];
+
+ return [
+ new LineLayer(
+ this.getSubLayerProps({
+ ...scrubberLineProps,
+ id: 'scrubber-line',
+ data: scrubberLine,
+ getSourcePosition: (d: ScrubberLineData) => d.sourcePosition,
+ getTargetPosition: (d: ScrubberLineData) => d.targetPosition,
+ getColor: [255, 100, 100, 255],
+ getWidth: 2
+ })
+ ),
+ new SolidPolygonLayer(
+ this.getSubLayerProps({
+ id: 'scrubber-handle',
+ data: scrubberHandle,
+ getPolygon: (d: ScrubberHandleData) => d.polygon,
+ getFillColor: (d: ScrubberHandleData) => d.color,
+ stroked: true,
+ getLineColor: [255, 255, 255, 255],
+ getLineWidth: 2,
+ pickable: true,
+ onClick: (info: PickingInfo) => {
+ if (onScrubberDragStart && info.object) {
+ onScrubberDragStart(info);
+ }
+ },
+ onHover: (info: PickingInfo) => {
+ if (onScrubberHover) {
+ onScrubberHover(Boolean(info.object), info);
+ }
+ }
+ })
+ ),
+ new TextLayer(
+ this.getSubLayerProps({
+ id: 'scrubber-label',
+ data: scrubberLabel,
+ getText: (d: ScrubberLabelData) => d.text,
+ getPosition: (d: ScrubberLabelData) => d.position,
+ getSize: 11,
+ getColor: [255, 100, 100, 255],
+ getTextAnchor: 'middle',
+ getAlignmentBaseline: 'bottom'
+ })
+ )
+ ];
+ }
+
+ // ===== MAIN RENDER =====
+
+ renderLayers(): Layer[] {
+ const {
+ data: tracks,
+ timelineStart,
+ timelineEnd,
+ viewport,
+ y = 100,
+ trackHeight = 40,
+ trackSpacing = 10
+ } = this.props;
+
+ const effectiveStartMs = viewport?.startMs ?? timelineStart;
+ const effectiveEndMs = viewport?.endMs ?? timelineEnd;
+
+ const visibleTracks = tracks.filter((track) => track.visible !== false);
+
+ const tracksWithSubtracks: TrackWithSubtracks[] = visibleTracks.map((track, trackIndex) => {
+ const clips = track.clips || [];
+ const clipsWithSubtracks = assignClipsToSubtracks(clips);
+ const subtrackCount = Math.max(1, calculateSubtrackCount(clips));
+ return {track, trackIndex, clips: clipsWithSubtracks, subtrackCount};
+ });
+
+ const {trackPositions, totalTimelineHeight} = this._calculateTrackPositions(
+ tracksWithSubtracks,
+ y,
+ trackHeight,
+ trackSpacing
+ );
+
+ const trackBackgrounds = this._generateTrackBackgrounds(tracksWithSubtracks, trackPositions);
+ const trackLabels = this._generateTrackLabels(tracksWithSubtracks, trackPositions);
+ const clipPolygons = this._generateClipPolygons(
+ tracksWithSubtracks,
+ trackPositions,
+ effectiveStartMs,
+ effectiveEndMs
+ );
+ const clipLabelsData = clipPolygons.map((clip) => ({
+ text: clip.label,
+ position: clip.labelPosition
+ }));
+ const subtrackSeparators = this._generateSubtrackSeparators(
+ tracksWithSubtracks,
+ trackPositions
+ );
+ const {axisLines, axisLabels} = this._generateAxis(
+ totalTimelineHeight,
+ effectiveStartMs,
+ effectiveEndMs
+ );
+ const {scrubberLine, scrubberHandle, scrubberLabel} = this._generateScrubber(
+ totalTimelineHeight,
+ effectiveStartMs,
+ effectiveEndMs
+ );
+
+ const layers = [
+ ...this._createTrackLayers(trackBackgrounds, trackLabels),
+ ...this._createClipLayers(clipPolygons, clipLabelsData),
+ this._createSubtrackSeparatorLayer(subtrackSeparators),
+ ...this._createAxisLayers(axisLines, axisLabels),
+ ...this._createScrubberLayers(scrubberLine, scrubberHandle, scrubberLabel)
+ ].filter((layer): layer is Layer => layer !== null);
+
+ return layers;
+ }
+}
diff --git a/dev/timeline-layers/src/layers/timeline-layer/timeline-layout.ts b/dev/timeline-layers/src/layers/timeline-layer/timeline-layout.ts
new file mode 100644
index 000000000..554ac513f
--- /dev/null
+++ b/dev/timeline-layers/src/layers/timeline-layer/timeline-layout.ts
@@ -0,0 +1,80 @@
+// deck.gl-community
+// SPDX-License-Identifier: MIT
+// Copyright (c) vis.gl contributors
+
+// ===== STYLE TYPES =====
+
+export type SelectionStyle = {
+ selectedClipColor?: [number, number, number, number];
+ hoveredClipColor?: [number, number, number, number];
+ selectedTrackColor?: [number, number, number, number];
+ hoveredTrackColor?: [number, number, number, number];
+ selectedLineWidth?: number;
+ hoveredLineWidth?: number;
+};
+
+export type TimelineLayout = {
+ x?: number;
+ y?: number;
+ width?: number;
+ trackHeight?: number;
+ trackSpacing?: number;
+ showTopAxis?: boolean;
+ showBottomAxis?: boolean;
+ axisHeight?: number;
+ axisTickHeight?: number;
+ axisLabelSize?: number;
+ showVerticalGrid?: boolean;
+ showTrackLabels?: boolean;
+ trackLabelWidth?: number;
+ scrubberWidth?: number;
+ scrubberHandleSize?: number;
+ scrubberLabelSize?: number;
+ clipPadding?: number;
+ clipLabelSize?: number;
+ showClipLabels?: boolean;
+ clipLabelsMinZoom?: number;
+ maxClipLabels?: number;
+ backgroundColor?: [number, number, number, number];
+ trackBackgroundColor?: [number, number, number, number];
+ trackBorderColor?: [number, number, number, number];
+ scrubberColor?: [number, number, number, number];
+ axisColor?: [number, number, number, number];
+ gridColor?: [number, number, number, number];
+ selectedClipColor?: [number, number, number, number];
+ hoveredClipColor?: [number, number, number, number];
+};
+
+// ===== CONSTANTS =====
+
+export const DEFAULT_TIMELINE_LAYOUT: Required = {
+ x: 50,
+ y: 100,
+ width: 800,
+ trackHeight: 60,
+ trackSpacing: 10,
+ showTopAxis: true,
+ showBottomAxis: false,
+ axisHeight: 30,
+ axisTickHeight: 10,
+ axisLabelSize: 11,
+ showVerticalGrid: true,
+ showTrackLabels: true,
+ trackLabelWidth: 100,
+ scrubberWidth: 3,
+ scrubberHandleSize: 16,
+ scrubberLabelSize: 10,
+ clipPadding: 5,
+ clipLabelSize: 10,
+ showClipLabels: true,
+ clipLabelsMinZoom: 1.5,
+ maxClipLabels: 1000,
+ backgroundColor: [255, 255, 255, 0],
+ trackBackgroundColor: [240, 240, 240, 100],
+ trackBorderColor: [200, 200, 200, 255],
+ scrubberColor: [255, 69, 0, 255],
+ axisColor: [50, 50, 50, 255],
+ gridColor: [200, 200, 200, 128],
+ selectedClipColor: [255, 255, 0, 220],
+ hoveredClipColor: [255, 255, 255, 255]
+};
diff --git a/dev/timeline-layers/src/layers/timeline-layer/timeline-types.ts b/dev/timeline-layers/src/layers/timeline-layer/timeline-types.ts
new file mode 100644
index 000000000..161d91da2
--- /dev/null
+++ b/dev/timeline-layers/src/layers/timeline-layer/timeline-types.ts
@@ -0,0 +1,146 @@
+// deck.gl-community
+// SPDX-License-Identifier: MIT
+// Copyright (c) vis.gl contributors
+
+// ===== CORE DATA TYPES =====
+
+export type TimelineClip = {
+ id: string | number;
+ startMs: number;
+ endMs: number;
+ color?: [number, number, number, number];
+ label?: string;
+ subtrackIndex?: number;
+ [key: string]: unknown;
+};
+
+export type TimelineTrack = {
+ id: string | number;
+ clips: TimelineClip[];
+ visible?: boolean;
+ name?: string;
+ [key: string]: unknown;
+};
+
+// ===== INTERNAL DATA TYPES =====
+
+/** A clip that has been assigned to a specific subtrack by the collision-detection algorithm */
+export type ClipWithSubtrack = TimelineClip & {
+ subtrackIndex: number;
+};
+
+export type TrackWithSubtracks = {
+ track: TimelineTrack;
+ trackIndex: number;
+ clips: ClipWithSubtrack[];
+ subtrackCount: number;
+};
+
+export type TrackPosition = {
+ y: number;
+ height: number;
+ subtrackCount: number;
+};
+
+export type TrackBackgroundData = {
+ id: string;
+ track: TimelineTrack;
+ trackIndex: number;
+ polygon: [number, number][];
+ color: [number, number, number, number];
+};
+
+export type TrackLabelData = {
+ text: string;
+ position: [number, number, number];
+};
+
+export type ClipPolygonData = {
+ id: string | number;
+ clip: TimelineClip;
+ track: TimelineTrack;
+ clipIndex: number;
+ trackIndex: number;
+ subtrackIndex: number;
+ polygon: [number, number][];
+ color: [number, number, number, number];
+ label: string;
+ labelPosition: [number, number, number];
+};
+
+export type ClipLabelData = {
+ text: string;
+ position: [number, number, number];
+};
+
+export type SeparatorLineData = {
+ sourcePosition: [number, number];
+ targetPosition: [number, number];
+};
+
+export type AxisLineData = {
+ sourcePosition: [number, number];
+ targetPosition: [number, number];
+};
+
+export type AxisLabelData = {
+ text: string;
+ position: [number, number, number];
+};
+
+export type ScrubberLineData = {
+ sourcePosition: [number, number];
+ targetPosition: [number, number];
+};
+
+export type ScrubberHandleData = {
+ id: string;
+ polygon: [number, number][];
+ color: [number, number, number, number];
+};
+
+export type ScrubberLabelData = {
+ text: string;
+ position: [number, number, number];
+};
+
+export type TimelineTick = {
+ position: number;
+ timeMs: number;
+ label: string;
+};
+
+// ===== CALLBACK INFO TYPES =====
+
+export type TimelineViewport = {
+ startMs?: number;
+ endMs?: number;
+};
+
+export type TimelineClipInfo = {
+ clip: TimelineClip;
+ track: TimelineTrack;
+ clipIndex: number;
+ trackIndex: number;
+ subtrackIndex: number;
+};
+
+export type TimelineTrackInfo = {
+ track: TimelineTrack;
+ index: number;
+};
+
+export type TimelineScrubberInfo = {
+ timeMs: number;
+ isDragging: boolean;
+};
+
+export type TimelineViewportInfo = {
+ startMs: number;
+ endMs: number;
+ zoomLevel: number;
+};
+
+// ===== FORMATTER TYPE =====
+
+export type TimeAxisLabelFormatter = (timeMs: number) => string;
diff --git a/dev/timeline-layers/src/layers/timeline-layer/timeline-utils.ts b/dev/timeline-layers/src/layers/timeline-layer/timeline-utils.ts
new file mode 100644
index 000000000..2b1becc09
--- /dev/null
+++ b/dev/timeline-layers/src/layers/timeline-layer/timeline-utils.ts
@@ -0,0 +1,85 @@
+// deck.gl-community
+// SPDX-License-Identifier: MIT
+// Copyright (c) vis.gl contributors
+
+import type {TimeAxisLabelFormatter, TimelineTick} from './timeline-types';
+
+// ===== TIME FORMATTERS =====
+
+export const timeAxisFormatters = {
+ seconds: (timeMs: number): string => `${(timeMs / 1000).toFixed(1)}s`,
+ timestamp: (timeMs: number): string => new Date(timeMs).toLocaleTimeString(),
+ minutesSeconds: (timeMs: number): string => {
+ const totalSeconds = Math.floor(timeMs / 1000);
+ const minutes = Math.floor(totalSeconds / 60);
+ const seconds = totalSeconds % 60;
+ return `${minutes}:${seconds.toString().padStart(2, '0')}`;
+ },
+ hoursMinutesSeconds: (timeMs: number): string => {
+ const totalSeconds = Math.floor(timeMs / 1000);
+ const hours = Math.floor(totalSeconds / 3600);
+ const minutes = Math.floor((totalSeconds % 3600) / 60);
+ const seconds = totalSeconds % 60;
+ return `${hours}:${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}`;
+ }
+};
+
+// ===== TIMELINE HELPERS =====
+
+export type GenerateTicksOptions = {
+ startMs: number;
+ endMs: number;
+ timelineX: number;
+ timelineWidth: number;
+ tickCount: number;
+ formatter: TimeAxisLabelFormatter;
+};
+
+/**
+ * Generate timeline axis ticks
+ */
+export function generateTimelineTicks(opts: GenerateTicksOptions): TimelineTick[] {
+ const {startMs, endMs, timelineX, timelineWidth, tickCount, formatter} = opts;
+ const ticks: TimelineTick[] = [];
+ const timeRange = endMs - startMs;
+ const step = timeRange / (tickCount - 1);
+
+ for (let i = 0; i < tickCount; i++) {
+ const timeMs = startMs + i * step;
+ const position = timelineX + (i / (tickCount - 1)) * timelineWidth;
+ const label = formatter(timeMs);
+ ticks.push({position, timeMs, label});
+ }
+
+ return ticks;
+}
+
+/**
+ * Convert canvas/pixel position to time
+ */
+export function positionToTime(
+ x: number,
+ timelineX: number,
+ timelineWidth: number,
+ startMs: number,
+ endMs: number
+): number {
+ const ratio = (x - timelineX) / timelineWidth;
+ const clampedRatio = Math.max(0, Math.min(1, ratio));
+ return startMs + clampedRatio * (endMs - startMs);
+}
+
+/**
+ * Convert time to canvas/pixel position
+ */
+export function timeToPosition(
+ timeMs: number,
+ timelineX: number,
+ timelineWidth: number,
+ startMs: number,
+ endMs: number
+): number {
+ const timeRatio = (timeMs - startMs) / (endMs - startMs);
+ const clampedRatio = Math.max(0, Math.min(1, timeRatio));
+ return timelineX + clampedRatio * timelineWidth;
+}
diff --git a/modules/timeline-layers/src/layers/vertical-grid-layer.ts b/dev/timeline-layers/src/layers/vertical-grid-layer.ts
similarity index 100%
rename from modules/timeline-layers/src/layers/vertical-grid-layer.ts
rename to dev/timeline-layers/src/layers/vertical-grid-layer.ts
diff --git a/modules/timeline-layers/src/utils/format-utils.ts b/dev/timeline-layers/src/utils/format-utils.ts
similarity index 100%
rename from modules/timeline-layers/src/utils/format-utils.ts
rename to dev/timeline-layers/src/utils/format-utils.ts
diff --git a/modules/timeline-layers/src/utils/tick-utils.ts b/dev/timeline-layers/src/utils/tick-utils.ts
similarity index 100%
rename from modules/timeline-layers/src/utils/tick-utils.ts
rename to dev/timeline-layers/src/utils/tick-utils.ts
diff --git a/dev/timeline-layers/test/index.spec.ts b/dev/timeline-layers/test/index.spec.ts
new file mode 100644
index 000000000..712c4c1ce
--- /dev/null
+++ b/dev/timeline-layers/test/index.spec.ts
@@ -0,0 +1,50 @@
+import {describe, it, expect} from 'vitest';
+import {
+ HorizonGraphLayer,
+ MultiHorizonGraphLayer,
+ TimeAxisLayer,
+ VerticalGridLayer,
+ TimelineLayer,
+ formatTimeMs,
+ timeAxisFormatters,
+ positionToTime,
+ timeToPosition
+} from '../src';
+
+describe('@deck.gl-community/timeline-layers', () => {
+ it('exports HorizonGraph layers', () => {
+ expect(HorizonGraphLayer).toBeDefined();
+ expect(MultiHorizonGraphLayer).toBeDefined();
+ });
+
+ it('exports timeline utilities', () => {
+ expect(TimeAxisLayer).toBeDefined();
+ expect(VerticalGridLayer).toBeDefined();
+ expect(formatTimeMs).toBeTypeOf('function');
+ });
+
+ it('exports TimelineLayer', () => {
+ expect(TimelineLayer).toBeDefined();
+ });
+
+ it('exports time axis formatters', () => {
+ expect(timeAxisFormatters.seconds).toBeTypeOf('function');
+ expect(timeAxisFormatters.seconds(5000)).toBe('5.0s');
+ expect(timeAxisFormatters.minutesSeconds(90000)).toBe('1:30');
+ });
+
+ it('exports position/time conversion utilities', () => {
+ expect(positionToTime).toBeTypeOf('function');
+ expect(timeToPosition).toBeTypeOf('function');
+
+ // positionToTime: x=0 maps to startMs, x=width maps to endMs
+ expect(positionToTime(0, 0, 100, 0, 1000)).toBe(0);
+ expect(positionToTime(100, 0, 100, 0, 1000)).toBe(1000);
+ expect(positionToTime(50, 0, 100, 0, 1000)).toBe(500);
+
+ // timeToPosition: startMs maps to x=0, endMs maps to x=width
+ expect(timeToPosition(0, 0, 100, 0, 1000)).toBe(0);
+ expect(timeToPosition(1000, 0, 100, 0, 1000)).toBe(100);
+ expect(timeToPosition(500, 0, 100, 0, 1000)).toBe(50);
+ });
+});
diff --git a/dev/timeline-layers/test/timeline-layer.spec.ts b/dev/timeline-layers/test/timeline-layer.spec.ts
new file mode 100644
index 000000000..057d603bb
--- /dev/null
+++ b/dev/timeline-layers/test/timeline-layer.spec.ts
@@ -0,0 +1,391 @@
+// deck.gl-community
+// SPDX-License-Identifier: MIT
+// Copyright (c) vis.gl contributors
+
+import {describe, it, expect, vi} from 'vitest';
+import {
+ assignClipsToSubtracks,
+ calculateSubtrackCount
+} from '../src/layers/timeline-layer/timeline-collision';
+import {
+ positionToTime,
+ timeToPosition,
+ timeAxisFormatters,
+ generateTimelineTicks
+} from '../src/layers/timeline-layer/timeline-utils';
+import {TimelineLayer} from '../src/layers/timeline-layer/timeline-layer';
+import type {TimelineClip, TimelineTrack} from '../src/layers/timeline-layer/timeline-types';
+
+// ===== COLLISION DETECTION =====
+
+describe('assignClipsToSubtracks', () => {
+ it('returns empty array for no clips', () => {
+ expect(assignClipsToSubtracks([])).toEqual([]);
+ });
+
+ it('assigns a single clip to subtrack 0', () => {
+ const clips: TimelineClip[] = [{id: 'a', startMs: 0, endMs: 1000}];
+ const result = assignClipsToSubtracks(clips);
+ expect(result).toHaveLength(1);
+ expect(result[0].subtrackIndex).toBe(0);
+ });
+
+ it('places non-overlapping clips on the same subtrack', () => {
+ const clips: TimelineClip[] = [
+ {id: 'a', startMs: 0, endMs: 500},
+ {id: 'b', startMs: 500, endMs: 1000}
+ ];
+ const result = assignClipsToSubtracks(clips);
+ const byId = Object.fromEntries(result.map((c) => [c.id, c.subtrackIndex]));
+ expect(byId.a).toBe(0);
+ expect(byId.b).toBe(0);
+ });
+
+ it('places overlapping clips on different subtracks', () => {
+ const clips: TimelineClip[] = [
+ {id: 'a', startMs: 0, endMs: 1000},
+ {id: 'b', startMs: 500, endMs: 1500}
+ ];
+ const result = assignClipsToSubtracks(clips);
+ const byId = Object.fromEntries(result.map((c) => [c.id, c.subtrackIndex]));
+ expect(byId.a).not.toBe(byId.b);
+ });
+
+ it('packs three clips into two subtracks when third fits in first lane', () => {
+ // a: 0-500, b: 0-500 (overlaps a), c: 600-1000 (fits after a)
+ const clips: TimelineClip[] = [
+ {id: 'a', startMs: 0, endMs: 500},
+ {id: 'b', startMs: 0, endMs: 500},
+ {id: 'c', startMs: 600, endMs: 1000}
+ ];
+ const result = assignClipsToSubtracks(clips);
+ const subtracks = result.map((c) => c.subtrackIndex);
+ const uniqueSubtracks = new Set(subtracks);
+ expect(uniqueSubtracks.size).toBe(2);
+ });
+
+ it('sorts clips by startMs before assigning subtracks', () => {
+ // Provide clips in reverse order; the algorithm must still work correctly
+ const clips: TimelineClip[] = [
+ {id: 'b', startMs: 1000, endMs: 2000},
+ {id: 'a', startMs: 0, endMs: 500}
+ ];
+ const result = assignClipsToSubtracks(clips);
+ const byId = Object.fromEntries(result.map((c) => [c.id, c.subtrackIndex]));
+ // Both fit on subtrack 0 since they don't overlap
+ expect(byId.a).toBe(0);
+ expect(byId.b).toBe(0);
+ });
+
+ it('preserves original clip properties', () => {
+ const clip: TimelineClip = {
+ id: 'x',
+ startMs: 100,
+ endMs: 200,
+ label: 'hello',
+ color: [1, 2, 3, 4]
+ };
+ const [result] = assignClipsToSubtracks([clip]);
+ expect(result.id).toBe('x');
+ expect(result.label).toBe('hello');
+ expect(result.color).toEqual([1, 2, 3, 4]);
+ expect(result.subtrackIndex).toBeTypeOf('number');
+ });
+});
+
+describe('calculateSubtrackCount', () => {
+ it('returns 1 for empty clips array', () => {
+ expect(calculateSubtrackCount([])).toBe(1);
+ });
+
+ it('returns 1 for non-overlapping clips', () => {
+ const clips: TimelineClip[] = [
+ {id: 'a', startMs: 0, endMs: 500},
+ {id: 'b', startMs: 500, endMs: 1000}
+ ];
+ expect(calculateSubtrackCount(clips)).toBe(1);
+ });
+
+ it('returns 2 for two fully overlapping clips', () => {
+ const clips: TimelineClip[] = [
+ {id: 'a', startMs: 0, endMs: 1000},
+ {id: 'b', startMs: 0, endMs: 1000}
+ ];
+ expect(calculateSubtrackCount(clips)).toBe(2);
+ });
+
+ it('returns 3 for three mutually overlapping clips', () => {
+ const clips: TimelineClip[] = [
+ {id: 'a', startMs: 0, endMs: 1000},
+ {id: 'b', startMs: 0, endMs: 1000},
+ {id: 'c', startMs: 0, endMs: 1000}
+ ];
+ expect(calculateSubtrackCount(clips)).toBe(3);
+ });
+});
+
+// ===== TIME / POSITION MATH =====
+
+describe('positionToTime', () => {
+ it('maps left edge to startMs', () => {
+ expect(positionToTime(0, 0, 100, 0, 1000)).toBe(0);
+ });
+
+ it('maps right edge to endMs', () => {
+ expect(positionToTime(100, 0, 100, 0, 1000)).toBe(1000);
+ });
+
+ it('maps midpoint correctly', () => {
+ expect(positionToTime(50, 0, 100, 0, 1000)).toBe(500);
+ });
+
+ it('clamps to startMs when x is before timeline', () => {
+ expect(positionToTime(-10, 0, 100, 0, 1000)).toBe(0);
+ });
+
+ it('clamps to endMs when x is beyond timeline', () => {
+ expect(positionToTime(200, 0, 100, 0, 1000)).toBe(1000);
+ });
+
+ it('respects non-zero timelineX offset', () => {
+ // Timeline starts at x=50, width=100 → x=100 is midpoint
+ expect(positionToTime(100, 50, 100, 0, 1000)).toBe(500);
+ });
+
+ it('works with non-zero startMs', () => {
+ expect(positionToTime(50, 0, 100, 5000, 6000)).toBe(5500);
+ });
+});
+
+describe('timeToPosition', () => {
+ it('maps startMs to left edge', () => {
+ expect(timeToPosition(0, 0, 100, 0, 1000)).toBe(0);
+ });
+
+ it('maps endMs to right edge', () => {
+ expect(timeToPosition(1000, 0, 100, 0, 1000)).toBe(100);
+ });
+
+ it('maps midpoint correctly', () => {
+ expect(timeToPosition(500, 0, 100, 0, 1000)).toBe(50);
+ });
+
+ it('clamps when time is before startMs', () => {
+ expect(timeToPosition(-100, 0, 100, 0, 1000)).toBe(0);
+ });
+
+ it('clamps when time is after endMs', () => {
+ expect(timeToPosition(9000, 0, 100, 0, 1000)).toBe(100);
+ });
+
+ it('round-trips with positionToTime', () => {
+ const timeMs = 750;
+ const x = timeToPosition(timeMs, 0, 800, 0, 60000);
+ expect(positionToTime(x, 0, 800, 0, 60000)).toBeCloseTo(timeMs);
+ });
+});
+
+// ===== TIME AXIS FORMATTERS =====
+
+describe('timeAxisFormatters', () => {
+ describe('seconds', () => {
+ it('formats 0ms as 0.0s', () => {
+ expect(timeAxisFormatters.seconds(0)).toBe('0.0s');
+ });
+
+ it('formats 5000ms as 5.0s', () => {
+ expect(timeAxisFormatters.seconds(5000)).toBe('5.0s');
+ });
+
+ it('formats 1500ms as 1.5s', () => {
+ expect(timeAxisFormatters.seconds(1500)).toBe('1.5s');
+ });
+ });
+
+ describe('minutesSeconds', () => {
+ it('formats 0ms as 0:00', () => {
+ expect(timeAxisFormatters.minutesSeconds(0)).toBe('0:00');
+ });
+
+ it('formats 90000ms as 1:30', () => {
+ expect(timeAxisFormatters.minutesSeconds(90000)).toBe('1:30');
+ });
+
+ it('formats 3600000ms as 60:00', () => {
+ expect(timeAxisFormatters.minutesSeconds(3600000)).toBe('60:00');
+ });
+
+ it('pads seconds with leading zero', () => {
+ expect(timeAxisFormatters.minutesSeconds(65000)).toBe('1:05');
+ });
+ });
+
+ describe('hoursMinutesSeconds', () => {
+ it('formats 0ms as 0:00:00', () => {
+ expect(timeAxisFormatters.hoursMinutesSeconds(0)).toBe('0:00:00');
+ });
+
+ it('formats 3661000ms as 1:01:01', () => {
+ expect(timeAxisFormatters.hoursMinutesSeconds(3661000)).toBe('1:01:01');
+ });
+
+ it('pads minutes and seconds with leading zeros', () => {
+ expect(timeAxisFormatters.hoursMinutesSeconds(3600000)).toBe('1:00:00');
+ });
+ });
+});
+
+// ===== TICK GENERATION =====
+
+describe('generateTimelineTicks', () => {
+ it('generates the requested number of ticks', () => {
+ const ticks = generateTimelineTicks({
+ startMs: 0,
+ endMs: 1000,
+ timelineX: 0,
+ timelineWidth: 100,
+ tickCount: 5,
+ formatter: timeAxisFormatters.seconds
+ });
+ expect(ticks).toHaveLength(5);
+ });
+
+ it('first tick is at timelineX with startMs', () => {
+ const ticks = generateTimelineTicks({
+ startMs: 0,
+ endMs: 1000,
+ timelineX: 50,
+ timelineWidth: 200,
+ tickCount: 3,
+ formatter: timeAxisFormatters.seconds
+ });
+ expect(ticks[0].position).toBe(50);
+ expect(ticks[0].timeMs).toBe(0);
+ });
+
+ it('last tick is at timelineX + width with endMs', () => {
+ const ticks = generateTimelineTicks({
+ startMs: 0,
+ endMs: 1000,
+ timelineX: 50,
+ timelineWidth: 200,
+ tickCount: 3,
+ formatter: timeAxisFormatters.seconds
+ });
+ expect(ticks[2].position).toBe(250);
+ expect(ticks[2].timeMs).toBe(1000);
+ });
+
+ it('applies the formatter to produce labels', () => {
+ const fmt = (ms: number) => `t=${ms}`;
+ const ticks = generateTimelineTicks({
+ startMs: 0,
+ endMs: 1000,
+ timelineX: 0,
+ timelineWidth: 100,
+ tickCount: 3,
+ formatter: fmt
+ });
+ expect(ticks.map((t) => t.label)).toEqual(['t=0', 't=500', 't=1000']);
+ });
+});
+
+// ===== TIMELINE LAYER =====
+
+describe('TimelineLayer', () => {
+ it('has correct static layerName', () => {
+ expect(TimelineLayer.layerName).toBe('TimelineLayer');
+ });
+
+ it('has default props', () => {
+ expect(TimelineLayer.defaultProps).toBeDefined();
+ expect(TimelineLayer.defaultProps.x).toBe(150);
+ expect(TimelineLayer.defaultProps.y).toBe(100);
+ expect(TimelineLayer.defaultProps.width).toBe(800);
+ expect(TimelineLayer.defaultProps.trackHeight).toBe(40);
+ expect(TimelineLayer.defaultProps.trackSpacing).toBe(10);
+ expect(TimelineLayer.defaultProps.showScrubber).toBe(true);
+ expect(TimelineLayer.defaultProps.showAxis).toBe(true);
+ expect(TimelineLayer.defaultProps.showClipLabels).toBe(true);
+ expect(TimelineLayer.defaultProps.showTrackLabels).toBe(true);
+ expect(TimelineLayer.defaultProps.showSubtrackSeparators).toBe(true);
+ });
+});
+
+describe('TimelineLayer.zoomToPoint', () => {
+ function makeLayer(
+ overrides: Partial> = {}
+ ): {
+ layer: TimelineLayer;
+ viewportSpy: ReturnType;
+ zoomSpy: ReturnType;
+ } {
+ const viewportSpy = vi.fn();
+ const zoomSpy = vi.fn();
+
+ const tracks: TimelineTrack[] = [];
+ const layer = new TimelineLayer({
+ id: 'test',
+ data: tracks,
+ timelineStart: 0,
+ timelineEnd: 60000,
+ x: 0,
+ width: 600,
+ onViewportChange: viewportSpy,
+ onZoomChange: zoomSpy
+ });
+
+ return {layer, viewportSpy, zoomSpy};
+ }
+
+ it('fires onZoomChange with new zoom level', () => {
+ const {layer, zoomSpy} = makeLayer();
+ layer.zoomToPoint(2.0, 300, 1.0); // zoom in 2× from centre
+ expect(zoomSpy).toHaveBeenCalledOnce();
+ expect(zoomSpy.mock.calls[0][0]).toBeCloseTo(2.0);
+ });
+
+ it('clamps zoom level to a maximum of 100', () => {
+ const {layer, zoomSpy} = makeLayer();
+ layer.zoomToPoint(1000, 300, 50.0);
+ expect(zoomSpy.mock.calls[0][0]).toBe(100);
+ });
+
+ it('clamps zoom level to a minimum of 1.0', () => {
+ const {layer, zoomSpy} = makeLayer();
+ layer.zoomToPoint(0.0001, 300, 1.0);
+ expect(zoomSpy.mock.calls[0][0]).toBe(1.0);
+ });
+
+ it('fires onViewportChange when zooming in', () => {
+ const {layer, viewportSpy} = makeLayer();
+ layer.zoomToPoint(2.0, 300, 1.0);
+ expect(viewportSpy).toHaveBeenCalledOnce();
+ const [startMs, endMs] = viewportSpy.mock.calls[0];
+ expect(endMs - startMs).toBeCloseTo(30000); // half the range
+ });
+
+ it('fires onViewportChange with full range when zooming back to 1×', () => {
+ const {layer, viewportSpy} = makeLayer();
+ // Force zoom back to exactly 1.0 (factor 0.5 × 2.0 = 1.0)
+ layer.zoomToPoint(0.5, 300, 2.0);
+ expect(viewportSpy).toHaveBeenCalledOnce();
+ const [startMs, endMs] = viewportSpy.mock.calls[0];
+ expect(startMs).toBe(0);
+ expect(endMs).toBe(60000);
+ });
+
+ it('viewport stays within timeline bounds when zooming near left edge', () => {
+ const {layer, viewportSpy} = makeLayer();
+ layer.zoomToPoint(4.0, 0, 1.0); // zoom in 4× at left edge
+ const [startMs] = viewportSpy.mock.calls[0];
+ expect(startMs).toBeGreaterThanOrEqual(0);
+ });
+
+ it('viewport stays within timeline bounds when zooming near right edge', () => {
+ const {layer, viewportSpy} = makeLayer();
+ layer.zoomToPoint(4.0, 600, 1.0); // zoom in 4× at right edge
+ const [, endMs] = viewportSpy.mock.calls[0];
+ expect(endMs).toBeLessThanOrEqual(60000);
+ });
+});
diff --git a/modules/timeline-layers/tsconfig.json b/dev/timeline-layers/tsconfig.json
similarity index 100%
rename from modules/timeline-layers/tsconfig.json
rename to dev/timeline-layers/tsconfig.json
diff --git a/dev/timeline-layers/vite.config.local.mjs b/dev/timeline-layers/vite.config.local.mjs
new file mode 100644
index 000000000..e8fda4224
--- /dev/null
+++ b/dev/timeline-layers/vite.config.local.mjs
@@ -0,0 +1,35 @@
+// Allows examples in dev/timeline-layers/examples/ to be built against
+// the source code in this repo instead of installed node_modules.
+
+import {defineConfig} from 'vite';
+import {getOcularConfig} from '@vis.gl/dev-tools';
+import {join} from 'path';
+
+const rootDir = join(__dirname, '../..');
+
+/** https://vitejs.dev/config/ */
+export default defineConfig(async () => {
+ const {aliases} = await getOcularConfig({root: rootDir});
+
+ return {
+ resolve: {
+ alias: {
+ ...aliases,
+ '@deck.gl-community/timeline-layers': join(rootDir, './dev/timeline-layers/src'),
+ '@deck.gl': join(rootDir, './node_modules/@deck.gl'),
+ '@luma.gl': join(rootDir, './node_modules/@luma.gl'),
+ '@math.gl': join(rootDir, './node_modules/@math.gl'),
+ '@loaders.gl': join(rootDir, './node_modules/@loaders.gl'),
+ 'react': join(rootDir, './node_modules/react'),
+ 'react-dom': join(rootDir, './node_modules/react-dom'),
+ }
+ },
+ server: {
+ open: true,
+ port: 8080
+ },
+ optimizeDeps: {
+ esbuildOptions: {target: 'es2020'}
+ }
+ };
+});
diff --git a/lerna.json b/lerna.json
index 19063139e..a2519130e 100644
--- a/lerna.json
+++ b/lerna.json
@@ -2,7 +2,8 @@
"version": "9.2.8",
"npmClient": "yarn",
"packages": [
- "modules/*"
+ "modules/*",
+ "dev/*"
],
"publishConfig": {
"access": "public"
diff --git a/modules/editable-layers/test/immutable-feature-collection.node.spec.ts b/modules/editable-layers/test/immutable-feature-collection.node.spec.ts
new file mode 100644
index 000000000..fa9c6f413
--- /dev/null
+++ b/modules/editable-layers/test/immutable-feature-collection.node.spec.ts
@@ -0,0 +1,272 @@
+// deck.gl-community
+// SPDX-License-Identifier: MIT
+// Copyright (c) vis.gl contributors
+
+import {describe, it, expect} from 'vitest';
+import {ImmutableFeatureCollection} from '../src/edit-modes/immutable-feature-collection';
+import type {SimpleFeatureCollection} from '../src/utils/geojson-types';
+
+// ---------------------------------------------------------------------------
+// Fixtures
+// ---------------------------------------------------------------------------
+
+function makeCollection(features: SimpleFeatureCollection['features']): ImmutableFeatureCollection {
+ return new ImmutableFeatureCollection({type: 'FeatureCollection', features});
+}
+
+const POINT_FEATURE = {
+ type: 'Feature' as const,
+ geometry: {type: 'Point' as const, coordinates: [1, 2]},
+ properties: {}
+};
+
+const LINE_FEATURE = {
+ type: 'Feature' as const,
+ geometry: {
+ type: 'LineString' as const,
+ coordinates: [
+ [0, 0],
+ [1, 1],
+ [2, 2]
+ ]
+ },
+ properties: {}
+};
+
+const POLYGON_FEATURE = {
+ type: 'Feature' as const,
+ geometry: {
+ type: 'Polygon' as const,
+ // outer ring (closed)
+ coordinates: [
+ [
+ [0, 0],
+ [1, 0],
+ [1, 1],
+ [0, 1],
+ [0, 0]
+ ]
+ ]
+ },
+ properties: {}
+};
+
+// ---------------------------------------------------------------------------
+// addFeature / addFeatures
+// ---------------------------------------------------------------------------
+
+describe('addFeature', () => {
+ it('appends a feature and returns a new collection', () => {
+ const fc = makeCollection([POINT_FEATURE]);
+ const updated = fc.addFeature(LINE_FEATURE);
+
+ expect(updated.featureCollection.features).toHaveLength(2);
+ // original is unchanged
+ expect(fc.featureCollection.features).toHaveLength(1);
+ });
+
+ it('addFeatures appends multiple features', () => {
+ const fc = makeCollection([]);
+ const updated = fc.addFeatures([POINT_FEATURE, LINE_FEATURE]);
+
+ expect(updated.featureCollection.features).toHaveLength(2);
+ });
+});
+
+// ---------------------------------------------------------------------------
+// deleteFeature / deleteFeatures
+// ---------------------------------------------------------------------------
+
+describe('deleteFeature', () => {
+ it('removes a feature by index', () => {
+ const fc = makeCollection([POINT_FEATURE, LINE_FEATURE, POLYGON_FEATURE]);
+ const updated = fc.deleteFeature(1);
+
+ expect(updated.featureCollection.features).toHaveLength(2);
+ expect(updated.featureCollection.features[0].geometry.type).toBe('Point');
+ expect(updated.featureCollection.features[1].geometry.type).toBe('Polygon');
+ });
+
+ it('deleteFeatures removes multiple features in correct order', () => {
+ const fc = makeCollection([POINT_FEATURE, LINE_FEATURE, POLYGON_FEATURE]);
+ const updated = fc.deleteFeatures([0, 2]);
+
+ expect(updated.featureCollection.features).toHaveLength(1);
+ expect(updated.featureCollection.features[0].geometry.type).toBe('LineString');
+ });
+
+ it('ignores out-of-range indexes', () => {
+ const fc = makeCollection([POINT_FEATURE]);
+ const updated = fc.deleteFeature(99);
+
+ expect(updated.featureCollection.features).toHaveLength(1);
+ });
+});
+
+// ---------------------------------------------------------------------------
+// replaceGeometry
+// ---------------------------------------------------------------------------
+
+describe('replaceGeometry', () => {
+ it('replaces the geometry of a feature', () => {
+ const fc = makeCollection([POINT_FEATURE, LINE_FEATURE]);
+ const newGeom = {type: 'Point' as const, coordinates: [99, 99]};
+ const updated = fc.replaceGeometry(0, newGeom);
+
+ expect((updated.featureCollection.features[0].geometry as any).coordinates).toEqual([99, 99]);
+ // feature 1 is unchanged
+ expect(updated.featureCollection.features[1].geometry.type).toBe('LineString');
+ // original is unchanged
+ expect((fc.featureCollection.features[0].geometry as any).coordinates).toEqual([1, 2]);
+ });
+});
+
+// ---------------------------------------------------------------------------
+// replacePosition — LineString
+// ---------------------------------------------------------------------------
+
+describe('replacePosition (LineString)', () => {
+ it('replaces a coordinate at a given index', () => {
+ const fc = makeCollection([LINE_FEATURE]);
+ const updated = fc.replacePosition(0, [1], [9, 9]);
+
+ const coords = (updated.featureCollection.features[0].geometry as any).coordinates;
+ expect(coords[1]).toEqual([9, 9]);
+ // other positions unchanged
+ expect(coords[0]).toEqual([0, 0]);
+ expect(coords[2]).toEqual([2, 2]);
+ });
+
+ it('preserves elevation when updating 2D position over a 3D point', () => {
+ const feature3D = {
+ type: 'Feature' as const,
+ geometry: {
+ type: 'LineString' as const,
+ coordinates: [
+ [0, 0, 10],
+ [1, 1, 20]
+ ]
+ },
+ properties: {}
+ };
+ const fc = makeCollection([feature3D]);
+ const updated = fc.replacePosition(0, [0], [5, 5]); // no elevation given
+
+ const coords = (updated.featureCollection.features[0].geometry as any).coordinates;
+ expect(coords[0]).toEqual([5, 5, 10]); // elevation copied from original
+ });
+
+ it('is immutable — original collection unchanged', () => {
+ const fc = makeCollection([LINE_FEATURE]);
+ fc.replacePosition(0, [0], [99, 99]);
+
+ const coords = (fc.featureCollection.features[0].geometry as any).coordinates;
+ expect(coords[0]).toEqual([0, 0]);
+ });
+});
+
+// ---------------------------------------------------------------------------
+// replacePosition — Polygon (wraps first/last)
+// ---------------------------------------------------------------------------
+
+describe('replacePosition (Polygon)', () => {
+ it('updates both first and last coordinate when replacing index 0', () => {
+ const fc = makeCollection([POLYGON_FEATURE]);
+ const updated = fc.replacePosition(0, [0, 0], [5, 5]);
+
+ const ring = (updated.featureCollection.features[0].geometry as any).coordinates[0];
+ expect(ring[0]).toEqual([5, 5]);
+ expect(ring[ring.length - 1]).toEqual([5, 5]);
+ });
+});
+
+// ---------------------------------------------------------------------------
+// addPosition
+// ---------------------------------------------------------------------------
+
+describe('addPosition (LineString)', () => {
+ it('inserts a position at the given index', () => {
+ const fc = makeCollection([LINE_FEATURE]);
+ const updated = fc.addPosition(0, [1], [0.5, 0.5]);
+
+ const coords = (updated.featureCollection.features[0].geometry as any).coordinates;
+ expect(coords).toHaveLength(4);
+ expect(coords[1]).toEqual([0.5, 0.5]);
+ expect(coords[2]).toEqual([1, 1]);
+ });
+
+ it('throws when adding a position to a Point', () => {
+ const fc = makeCollection([POINT_FEATURE]);
+ expect(() => fc.addPosition(0, [0], [1, 1])).toThrow();
+ });
+});
+
+// ---------------------------------------------------------------------------
+// removePosition
+// ---------------------------------------------------------------------------
+
+describe('removePosition (LineString)', () => {
+ it('removes a position from a LineString', () => {
+ const fc = makeCollection([LINE_FEATURE]);
+ const updated = fc.removePosition(0, [1]);
+
+ const coords = (updated.featureCollection.features[0].geometry as any).coordinates;
+ expect(coords).toHaveLength(2);
+ expect(coords[0]).toEqual([0, 0]);
+ expect(coords[1]).toEqual([2, 2]);
+ });
+
+ it('throws when removing from a Point', () => {
+ const fc = makeCollection([POINT_FEATURE]);
+ expect(() => fc.removePosition(0, [0])).toThrow();
+ });
+
+ it('throws when LineString would have fewer than 2 positions', () => {
+ const twoPointLine = {
+ type: 'Feature' as const,
+ geometry: {
+ type: 'LineString' as const,
+ coordinates: [
+ [0, 0],
+ [1, 1]
+ ]
+ },
+ properties: {}
+ };
+ const fc = makeCollection([twoPointLine]);
+ expect(() => fc.removePosition(0, [0])).toThrow();
+ });
+
+ it('throws when Polygon outer ring would have fewer than 4 positions', () => {
+ const triangle = {
+ type: 'Feature' as const,
+ geometry: {
+ type: 'Polygon' as const,
+ coordinates: [
+ [
+ [0, 0],
+ [1, 0],
+ [0.5, 1],
+ [0, 0]
+ ]
+ ]
+ },
+ properties: {}
+ };
+ const fc = makeCollection([triangle]);
+ expect(() => fc.removePosition(0, [0, 0])).toThrow();
+ });
+});
+
+// ---------------------------------------------------------------------------
+// getObject
+// ---------------------------------------------------------------------------
+
+describe('getObject', () => {
+ it('returns the underlying feature collection', () => {
+ const raw = {type: 'FeatureCollection' as const, features: [POINT_FEATURE]};
+ const fc = new ImmutableFeatureCollection(raw);
+
+ expect(fc.getObject()).toBe(raw);
+ });
+});
diff --git a/modules/editable-layers/test/imports.node.spec.ts b/modules/editable-layers/test/imports.node.spec.ts
new file mode 100644
index 000000000..c9fdff2cc
--- /dev/null
+++ b/modules/editable-layers/test/imports.node.spec.ts
@@ -0,0 +1,61 @@
+// deck.gl-community
+// SPDX-License-Identifier: MIT
+// Copyright (c) vis.gl contributors
+
+// Smoke test: verify the public API surface exports resolve without errors.
+// This runs in Node (no WebGL) to catch broken re-exports early.
+
+import {it, expect} from 'vitest';
+import * as EditableLayers from '../src/index';
+
+it('exports ImmutableFeatureCollection', () => {
+ expect(EditableLayers.ImmutableFeatureCollection).toBeDefined();
+});
+
+it('exports GeoJsonEditMode', () => {
+ expect(EditableLayers.GeoJsonEditMode).toBeDefined();
+});
+
+it('exports draw modes', () => {
+ const drawModes = [
+ 'DrawPointMode',
+ 'DrawLineStringMode',
+ 'DrawPolygonMode',
+ 'DrawRectangleMode',
+ 'DrawCircleByDiameterMode',
+ 'DrawCircleFromCenterMode'
+ ] as const;
+
+ for (const name of drawModes) {
+ expect(EditableLayers[name], `${name} should be exported`).toBeDefined();
+ }
+});
+
+it('exports alter modes', () => {
+ const alterModes = [
+ 'ModifyMode',
+ 'TranslateMode',
+ 'ScaleMode',
+ 'RotateMode',
+ 'DeleteMode',
+ 'DuplicateMode',
+ 'SplitPolygonMode',
+ 'TransformMode'
+ ] as const;
+
+ for (const name of alterModes) {
+ expect(EditableLayers[name], `${name} should be exported`).toBeDefined();
+ }
+});
+
+it('exports measurement modes', () => {
+ expect(EditableLayers.MeasureDistanceMode).toBeDefined();
+ expect(EditableLayers.MeasureAreaMode).toBeDefined();
+ expect(EditableLayers.MeasureAngleMode).toBeDefined();
+});
+
+it('exports composite modes', () => {
+ expect(EditableLayers.CompositeMode).toBeDefined();
+ expect(EditableLayers.SnappableMode).toBeDefined();
+ expect(EditableLayers.ViewMode).toBeDefined();
+});
diff --git a/modules/geo-layers/test/imports.node.spec.ts b/modules/geo-layers/test/imports.node.spec.ts
new file mode 100644
index 000000000..16c0e3e66
--- /dev/null
+++ b/modules/geo-layers/test/imports.node.spec.ts
@@ -0,0 +1,21 @@
+// deck.gl-community
+// SPDX-License-Identifier: MIT
+// Copyright (c) vis.gl contributors
+
+import {it, expect} from 'vitest';
+import * as GeoLayers from '../src/index';
+
+it('exports TileSourceLayer', () => {
+ expect(GeoLayers.TileSourceLayer).toBeDefined();
+});
+
+it('exports GlobalGridLayer', () => {
+ expect(GeoLayers.GlobalGridLayer).toBeDefined();
+});
+
+it('exports grid systems', () => {
+ expect(GeoLayers.H3Grid).toBeDefined();
+ expect(GeoLayers.S2Grid).toBeDefined();
+ expect(GeoLayers.GeohashGrid).toBeDefined();
+ expect(GeoLayers.QuadkeyGrid).toBeDefined();
+});
diff --git a/modules/layers/test/imports.node.spec.ts b/modules/layers/test/imports.node.spec.ts
new file mode 100644
index 000000000..0feba5afc
--- /dev/null
+++ b/modules/layers/test/imports.node.spec.ts
@@ -0,0 +1,14 @@
+// deck.gl-community
+// SPDX-License-Identifier: MIT
+// Copyright (c) vis.gl contributors
+
+import {it, expect} from 'vitest';
+import * as Layers from '../src/index';
+
+it('exports PathOutlineLayer', () => {
+ expect(Layers.PathOutlineLayer).toBeDefined();
+});
+
+it('exports PathMarkerLayer', () => {
+ expect(Layers.PathMarkerLayer).toBeDefined();
+});
diff --git a/modules/timeline-layers/src/index.ts b/modules/timeline-layers/src/index.ts
deleted file mode 100644
index d65c9b9fe..000000000
--- a/modules/timeline-layers/src/index.ts
+++ /dev/null
@@ -1,13 +0,0 @@
-// deck.gl-community
-// SPDX-License-Identifier: MIT
-// Copyright (c) vis.gl contributors
-
-export type {HorizonGraphLayerProps} from './layers/horizon-graph-layer/horizon-graph-layer';
-export {HorizonGraphLayer} from './layers/horizon-graph-layer/horizon-graph-layer';
-export type {MultiHorizonGraphLayerProps} from './layers/horizon-graph-layer/multi-horizon-graph-layer';
-export {MultiHorizonGraphLayer} from './layers/horizon-graph-layer/multi-horizon-graph-layer';
-export type {TimeAxisLayerProps} from './layers/time-axis-layer';
-export {TimeAxisLayer} from './layers/time-axis-layer';
-export type {VerticalGridLayerProps} from './layers/vertical-grid-layer';
-export {VerticalGridLayer} from './layers/vertical-grid-layer';
-export {formatTimeMs, formatTimeRangeMs} from './utils/format-utils';
diff --git a/modules/timeline-layers/test/index.spec.ts b/modules/timeline-layers/test/index.spec.ts
deleted file mode 100644
index 62b62609f..000000000
--- a/modules/timeline-layers/test/index.spec.ts
+++ /dev/null
@@ -1,21 +0,0 @@
-import {describe, it, expect} from 'vitest';
-import {
- HorizonGraphLayer,
- MultiHorizonGraphLayer,
- TimeAxisLayer,
- VerticalGridLayer,
- formatTimeMs
-} from '../src';
-
-describe('@deck.gl-community/timeline-layers', () => {
- it('exports HorizonGraph layers', () => {
- expect(HorizonGraphLayer).toBeDefined();
- expect(MultiHorizonGraphLayer).toBeDefined();
- });
-
- it('exports timeline utilities', () => {
- expect(TimeAxisLayer).toBeDefined();
- expect(VerticalGridLayer).toBeDefined();
- expect(formatTimeMs).toBeTypeOf('function');
- });
-});
diff --git a/modules/widgets/test/imports.node.spec.ts b/modules/widgets/test/imports.node.spec.ts
new file mode 100644
index 000000000..6d8c2410d
--- /dev/null
+++ b/modules/widgets/test/imports.node.spec.ts
@@ -0,0 +1,22 @@
+// deck.gl-community
+// SPDX-License-Identifier: MIT
+// Copyright (c) vis.gl contributors
+
+import {it, expect} from 'vitest';
+import * as Widgets from '../src/index';
+
+it('exports PanWidget', () => {
+ expect(Widgets.PanWidget).toBeDefined();
+});
+
+it('exports ZoomRangeWidget', () => {
+ expect(Widgets.ZoomRangeWidget).toBeDefined();
+});
+
+it('exports HtmlOverlayWidget', () => {
+ expect(Widgets.HtmlOverlayWidget).toBeDefined();
+});
+
+it('exports HtmlTooltipWidget', () => {
+ expect(Widgets.HtmlTooltipWidget).toBeDefined();
+});
diff --git a/package.json b/package.json
index 817d91417..a9dc30855 100644
--- a/package.json
+++ b/package.json
@@ -13,6 +13,8 @@
"type": "module",
"workspaces": [
"modules/*",
+ "dev/*",
+ "dev/timeline-layers/examples/*",
"examples/**"
],
"scripts": {
@@ -26,6 +28,7 @@
"publish-prod": "ocular-publish version-only-prod",
"test": "yarn test-node",
"test-node": "vitest --project node",
+ "test-ci": "vitest run --project node --coverage",
"test-browser": "vitest --project browser",
"test-headless": "vitest run --project headless",
"metrics": "ocular-metrics",
@@ -49,6 +52,7 @@
"@vitejs/plugin-react": "^5.1.4",
"@vitest/browser": "^4.0.18",
"@vitest/browser-playwright": "^4.0.18",
+ "@vitest/coverage-v8": "^4.0.18",
"@vitest/eslint-plugin": "^1.6.9",
"bin-pack": "^1.0.2",
"datauri": "^4.1.0",
diff --git a/tsconfig.json b/tsconfig.json
index dcc5733fe..2bf349b8d 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -34,7 +34,8 @@
"@deck.gl-community/react": ["./modules/react/src"],
"@deck.gl-community/template": ["./modules/template/src"],
"@deck.gl-community/graph-layers": ["./modules/graph-layers/src"],
- "@deck.gl-community/three": ["./modules/three/src"]
+ "@deck.gl-community/three": ["./modules/three/src"],
+ "@deck.gl-community/timeline-layers": ["./dev/timeline-layers/src"]
},
"typeRoots": ["./node_modules/@types"],
"plugins": [
@@ -64,6 +65,7 @@
},
"include": [
"modules",
+ "dev",
"test",
"examples",
"scripts"
diff --git a/vitest.config.ts b/vitest.config.ts
index 1b923c0ce..f4d2fa2b7 100644
--- a/vitest.config.ts
+++ b/vitest.config.ts
@@ -10,14 +10,21 @@ const CONFIG = defineConfig({
conditions: ['node'] // prefer node resolution
},
test: {
+ coverage: {
+ provider: 'v8',
+ reporter: ['text', 'lcov'],
+ include: ['modules/*/src/**/*.{ts,tsx,js,jsx}', 'dev/*/src/**/*.{ts,tsx,js,jsx}'],
+ exclude: ['modules/template/**', 'modules/*/src/**/*.d.ts', 'dev/*/src/**/*.d.ts']
+ },
projects: [
{
test: {
name: 'node',
environment: 'node',
- include: ['modules/**/*.{test,spec}.{js,ts}'],
+ include: ['modules/**/*.{test,spec}.{js,ts}', 'dev/**/*.{test,spec}.{js,ts}'],
exclude: [
'modules/**/*.browser.{test,spec}.{js,ts}',
+ 'dev/**/*.browser.{test,spec}.{js,ts}',
'modules/basemap-props/**'
],
browser: {
@@ -32,9 +39,10 @@ const CONFIG = defineConfig({
environment: 'node',
include: [
'modules/**/*.browser.{test,spec}.{js,ts,jsx,tsx}',
- 'modules/**/*.{test,spec}.{jsx,tsx}'
+ 'modules/**/*.{test,spec}.{jsx,tsx}',
+ 'dev/**/*.browser.{test,spec}.{js,ts,jsx,tsx}',
+ 'dev/**/*.{test,spec}.{jsx,tsx}'
],
- exclude: ['modules/basemap-props/**'],
browser: {
enabled: true,
provider: playwright(),
@@ -49,9 +57,10 @@ const CONFIG = defineConfig({
environment: 'node',
include: [
'modules/**/*.browser.{test,spec}.{js,ts,jsx,tsx}',
- 'modules/**/*.{test,spec}.{jsx,tsx}'
+ 'modules/**/*.{test,spec}.{jsx,tsx}',
+ 'dev/**/*.browser.{test,spec}.{js,ts,jsx,tsx}',
+ 'dev/**/*.{test,spec}.{jsx,tsx}'
],
- exclude: ['modules/basemap-props/**'],
browser: {
enabled: true,
headless: true,
diff --git a/website/docusaurus.config.js b/website/docusaurus.config.js
index ce51f43b0..83ae83fa9 100644
--- a/website/docusaurus.config.js
+++ b/website/docusaurus.config.js
@@ -88,7 +88,7 @@ const config = {
'@deck.gl-community/leaflet': resolve('../modules/leaflet/src'),
'@deck.gl-community/graph-layers': resolve('../modules/graph-layers/src'),
'@deck.gl-community/infovis-layers': resolve('../modules/infovis-layers/src'),
- '@deck.gl-community/timeline-layers': resolve('../modules/timeline-layers/src'),
+ '@deck.gl-community/timeline-layers': resolve('../dev/timeline-layers/src'),
'@deck.gl-community/react': resolve('../modules/react/src'),
'@deck.gl-community/layers': resolve('../modules/layers/src'),
'@deck.gl-community/arrow-layers': resolve('../modules/arrow-layers/src'),
diff --git a/website/src/examples-sidebar.js b/website/src/examples-sidebar.js
index 2cd284d8c..c073f9b6c 100644
--- a/website/src/examples-sidebar.js
+++ b/website/src/examples-sidebar.js
@@ -81,14 +81,6 @@ const sidebars = {
// ]
// },
- {
- type: 'category',
- label: '@deck.gl-community/three',
- items: [
- "three/wild-forest"
- ]
- },
-
// TODO - need BING map key
// {
// type: 'category',
diff --git a/website/src/examples/timeline-layers/horizon-graph-layer.tsx b/website/src/examples/timeline-layers/horizon-graph-layer.tsx
index 47700c297..2fbb44b2b 100644
--- a/website/src/examples/timeline-layers/horizon-graph-layer.tsx
+++ b/website/src/examples/timeline-layers/horizon-graph-layer.tsx
@@ -8,7 +8,7 @@ import {makeExample} from '../../components';
class HorizonDemo extends Component {
static title = 'Horizon Graph Layer Demo';
- static code = `${GITHUB_TREE}/examples/timeline-layers/horizon-graph-layer`;
+ static code = `${GITHUB_TREE}/dev/timeline-layers/examples/horizon-graph-layer`;
static renderInfo(meta) {
return <>>;
@@ -20,7 +20,7 @@ class HorizonDemo extends Component {
return (
{() => {
- const App = require('../../../../examples/timeline-layers/horizon-graph-layer/app').default;
+ const App = require('../../../../dev/timeline-layers/examples/horizon-graph-layer/app').default;
return ;
}}
diff --git a/yarn.lock b/yarn.lock
index 66046489e..be128eb71 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -195,13 +195,6 @@ __metadata:
languageName: node
linkType: hard
-"@babel/runtime@npm:^7.0.0, @babel/runtime@npm:^7.12.0":
- version: 7.28.6
- resolution: "@babel/runtime@npm:7.28.6"
- checksum: 10c0/358cf2429992ac1c466df1a21c1601d595c46930a13c1d4662fde908d44ee78ec3c183aaff513ecb01ef8c55c3624afe0309eeeb34715672dbfadb7feedb2c0d
- languageName: node
- linkType: hard
-
"@babel/runtime@npm:^7.1.2, @babel/runtime@npm:^7.12.5":
version: 7.28.4
resolution: "@babel/runtime@npm:7.28.4"
@@ -262,18 +255,10 @@ __metadata:
languageName: node
linkType: hard
-"@carto/api-client@npm:^0.5.19":
- version: 0.5.24
- resolution: "@carto/api-client@npm:0.5.24"
- dependencies:
- "@loaders.gl/schema": "npm:^4.3.3"
- "@types/geojson": "npm:^7946.0.16"
- d3-format: "npm:^3.1.0"
- d3-scale: "npm:^4.0.2"
- h3-js: "npm:^4.1.0"
- jsep: "npm:^1.4.0"
- quadbin: "npm:^0.4.1-alpha.0"
- checksum: 10c0/1a110955a9c0eefb4f322b1241d8a858d4cafd292ad988973c56765cad62c2f002a056f4692442e59129e483bc2814e2731bdd92492f213a7ebda2a241af76cf
+"@bcoe/v8-coverage@npm:^1.0.2":
+ version: 1.0.2
+ resolution: "@bcoe/v8-coverage@npm:1.0.2"
+ checksum: 10c0/1eb1dc93cc17fb7abdcef21a6e7b867d6aa99a7ec88ec8207402b23d9083ab22a8011213f04b2cf26d535f1d22dc26139b7929e6c2134c254bd1e14ba5e678c3
languageName: node
linkType: hard
@@ -286,7 +271,7 @@ __metadata:
languageName: node
linkType: hard
-"@deck.gl-community/arrow-layers@workspace:*, @deck.gl-community/arrow-layers@workspace:modules/arrow-layers":
+"@deck.gl-community/arrow-layers@workspace:modules/arrow-layers":
version: 0.0.0-use.local
resolution: "@deck.gl-community/arrow-layers@workspace:modules/arrow-layers"
dependencies:
@@ -525,9 +510,9 @@ __metadata:
languageName: unknown
linkType: soft
-"@deck.gl-community/timeline-layers@workspace:*, @deck.gl-community/timeline-layers@workspace:modules/timeline-layers":
+"@deck.gl-community/timeline-layers@workspace:*, @deck.gl-community/timeline-layers@workspace:dev/timeline-layers":
version: 0.0.0-use.local
- resolution: "@deck.gl-community/timeline-layers@workspace:modules/timeline-layers"
+ resolution: "@deck.gl-community/timeline-layers@workspace:dev/timeline-layers"
dependencies:
"@deck.gl/core": "npm:~9.2.8"
"@deck.gl/layers": "npm:~9.2.8"
@@ -551,7 +536,7 @@ __metadata:
languageName: unknown
linkType: soft
-"@deck.gl/aggregation-layers@npm:9.2.8, @deck.gl/aggregation-layers@npm:~9.2.8":
+"@deck.gl/aggregation-layers@npm:~9.2.8":
version: 9.2.8
resolution: "@deck.gl/aggregation-layers@npm:9.2.8"
dependencies:
@@ -569,62 +554,32 @@ __metadata:
languageName: node
linkType: hard
-"@deck.gl/arcgis@npm:9.2.8":
- version: 9.2.8
- resolution: "@deck.gl/arcgis@npm:9.2.8"
+"@deck.gl/core@npm:~9.2.1":
+ version: 9.2.10
+ resolution: "@deck.gl/core@npm:9.2.10"
dependencies:
+ "@loaders.gl/core": "npm:^4.3.4"
+ "@loaders.gl/images": "npm:^4.3.4"
"@luma.gl/constants": "npm:^9.2.6"
- esri-loader: "npm:^3.7.0"
- peerDependencies:
- "@arcgis/core": ^4.0.0
- "@deck.gl/core": ~9.2.0
- "@luma.gl/core": ~9.2.6
- "@luma.gl/engine": ~9.2.6
- "@luma.gl/webgl": ~9.2.6
- checksum: 10c0/2083b2dc5cb25982d0eab06b443be4a90f8315aca9b048c8578d9d2e1c43d9aa772db8ed2fcb726fb3c323a2e1af4f8abbe3c2189e13ede1f7346d88804f2d6b
- languageName: node
- linkType: hard
-
-"@deck.gl/carto@npm:9.2.8":
- version: 9.2.8
- resolution: "@deck.gl/carto@npm:9.2.8"
- dependencies:
- "@carto/api-client": "npm:^0.5.19"
- "@loaders.gl/compression": "npm:^4.3.4"
- "@loaders.gl/gis": "npm:^4.3.4"
- "@loaders.gl/loader-utils": "npm:^4.3.4"
- "@loaders.gl/mvt": "npm:^4.3.4"
- "@loaders.gl/schema": "npm:^4.3.4"
- "@loaders.gl/tiles": "npm:^4.3.4"
"@luma.gl/core": "npm:^9.2.6"
+ "@luma.gl/engine": "npm:^9.2.6"
"@luma.gl/shadertools": "npm:^9.2.6"
+ "@luma.gl/webgl": "npm:^9.2.6"
+ "@math.gl/core": "npm:^4.1.0"
+ "@math.gl/sun": "npm:^4.1.0"
+ "@math.gl/types": "npm:^4.1.0"
"@math.gl/web-mercator": "npm:^4.1.0"
- "@types/d3-array": "npm:^3.0.2"
- "@types/d3-color": "npm:^1.4.2"
- "@types/d3-scale": "npm:^3.0.0"
- cartocolor: "npm:^5.0.2"
- d3-array: "npm:^3.2.0"
- d3-color: "npm:^3.1.0"
- d3-format: "npm:^3.1.0"
- d3-scale: "npm:^4.0.0"
- earcut: "npm:^2.2.4"
- h3-js: "npm:^4.1.0"
- moment-timezone: "npm:^0.6.0"
- pbf: "npm:^3.2.1"
- quadbin: "npm:^0.4.0"
- peerDependencies:
- "@deck.gl/aggregation-layers": ~9.2.0
- "@deck.gl/core": ~9.2.0
- "@deck.gl/extensions": ~9.2.0
- "@deck.gl/geo-layers": ~9.2.0
- "@deck.gl/layers": ~9.2.0
- "@loaders.gl/core": ^4.3.4
- "@luma.gl/core": ~9.2.6
- checksum: 10c0/6d76eb6ba48ce2ba9791888af08c0439db81f27c916fec645fec327082bf625f9f63056b45b756a71d62a28a8f639777ba255a50d36b427bec68e4e62f0973cd
+ "@probe.gl/env": "npm:^4.1.1"
+ "@probe.gl/log": "npm:^4.1.1"
+ "@probe.gl/stats": "npm:^4.1.1"
+ "@types/offscreencanvas": "npm:^2019.6.4"
+ gl-matrix: "npm:^3.0.0"
+ mjolnir.js: "npm:^3.0.0"
+ checksum: 10c0/195a5a2ae4216bfef0c7d280fd8b28a3be14c423ee4d42e4a0c8c6d960b9845ce33eba72390b61d5f57fa9be676672fe7f92013ff5c02989f85792c309480c8e
languageName: node
linkType: hard
-"@deck.gl/core@npm:9.2.8, @deck.gl/core@npm:~9.2.8":
+"@deck.gl/core@npm:~9.2.8":
version: 9.2.8
resolution: "@deck.gl/core@npm:9.2.8"
dependencies:
@@ -649,7 +604,7 @@ __metadata:
languageName: node
linkType: hard
-"@deck.gl/extensions@npm:9.2.8, @deck.gl/extensions@npm:~9.2.8":
+"@deck.gl/extensions@npm:~9.2.8":
version: 9.2.8
resolution: "@deck.gl/extensions@npm:9.2.8"
dependencies:
@@ -664,7 +619,7 @@ __metadata:
languageName: node
linkType: hard
-"@deck.gl/geo-layers@npm:9.2.8, @deck.gl/geo-layers@npm:~9.2.8":
+"@deck.gl/geo-layers@npm:~9.2.8":
version: 9.2.8
resolution: "@deck.gl/geo-layers@npm:9.2.8"
dependencies:
@@ -697,35 +652,28 @@ __metadata:
languageName: node
linkType: hard
-"@deck.gl/google-maps@npm:9.2.8":
- version: 9.2.8
- resolution: "@deck.gl/google-maps@npm:9.2.8"
+"@deck.gl/layers@npm:~9.2.1":
+ version: 9.2.10
+ resolution: "@deck.gl/layers@npm:9.2.10"
dependencies:
- "@luma.gl/constants": "npm:^9.2.6"
- "@luma.gl/webgl": "npm:^9.2.6"
+ "@loaders.gl/images": "npm:^4.3.4"
+ "@loaders.gl/schema": "npm:^4.3.4"
+ "@luma.gl/shadertools": "npm:^9.2.6"
+ "@mapbox/tiny-sdf": "npm:^2.0.5"
"@math.gl/core": "npm:^4.1.0"
- "@types/google.maps": "npm:^3.48.6"
+ "@math.gl/polygon": "npm:^4.1.0"
+ "@math.gl/web-mercator": "npm:^4.1.0"
+ earcut: "npm:^2.2.4"
peerDependencies:
"@deck.gl/core": ~9.2.0
- "@luma.gl/constants": ~9.2.6
+ "@loaders.gl/core": ^4.3.4
"@luma.gl/core": ~9.2.6
- "@luma.gl/webgl": ~9.2.6
- checksum: 10c0/bdf7d26c29b7ae73c9f955ef2966fdcfa99fdfb94908e26e35be0e16850de4a7e84770e5eff1418acdac9eed1018a6478a42286ce36a26815fdeeabe5ca66376
- languageName: node
- linkType: hard
-
-"@deck.gl/json@npm:9.2.8":
- version: 9.2.8
- resolution: "@deck.gl/json@npm:9.2.8"
- dependencies:
- jsep: "npm:^0.3.0"
- peerDependencies:
- "@deck.gl/core": ~9.2.0
- checksum: 10c0/f46cd6aeb25bd09c43a5da17a39d539ab1e9e70225423fced353317160b0fbef59c628152c546212e11c50582593099d25898e5bbe98d00012a4ec077a4585ab
+ "@luma.gl/engine": ~9.2.6
+ checksum: 10c0/78e55822e06981725784b4d876a1aa2cc51a607c5e53bd5cf319abb6134305b83f2d07699932b6b0143cb4a14c8b3befedccb575cde62a24211e3c2052dc5a13
languageName: node
linkType: hard
-"@deck.gl/layers@npm:9.2.8, @deck.gl/layers@npm:~9.2.8":
+"@deck.gl/layers@npm:~9.2.8":
version: 9.2.8
resolution: "@deck.gl/layers@npm:9.2.8"
dependencies:
@@ -746,22 +694,7 @@ __metadata:
languageName: node
linkType: hard
-"@deck.gl/mapbox@npm:9.2.8":
- version: 9.2.8
- resolution: "@deck.gl/mapbox@npm:9.2.8"
- dependencies:
- "@luma.gl/constants": "npm:^9.2.6"
- "@math.gl/web-mercator": "npm:^4.1.0"
- peerDependencies:
- "@deck.gl/core": ~9.2.0
- "@luma.gl/constants": ~9.2.6
- "@luma.gl/core": ~9.2.6
- "@math.gl/web-mercator": ^4.1.0
- checksum: 10c0/2846a382b48bcf59c9c1d1246f7914597a9a2179ebb5246b71799b0c2ccdca5f84cadaae4a2fd9918671383fe1b22347f66ad7ecce1b1523632621ed94e77f47
- languageName: node
- linkType: hard
-
-"@deck.gl/mesh-layers@npm:9.2.8, @deck.gl/mesh-layers@npm:~9.2.8":
+"@deck.gl/mesh-layers@npm:~9.2.8":
version: 9.2.8
resolution: "@deck.gl/mesh-layers@npm:9.2.8"
dependencies:
@@ -779,7 +712,19 @@ __metadata:
languageName: node
linkType: hard
-"@deck.gl/react@npm:9.2.8, @deck.gl/react@npm:~9.2.8":
+"@deck.gl/react@npm:~9.2.1":
+ version: 9.2.10
+ resolution: "@deck.gl/react@npm:9.2.10"
+ peerDependencies:
+ "@deck.gl/core": ~9.2.0
+ "@deck.gl/widgets": ~9.2.0
+ react: ">=16.3.0"
+ react-dom: ">=16.3.0"
+ checksum: 10c0/87b79ad77e494fb16cb0c0ac0d00d9911478a95f9e686e681687fc3e395e469380f1f79429bbbdc67c9e8adac49d8c2311aba991222e30620ca5a540e72578ed
+ languageName: node
+ linkType: hard
+
+"@deck.gl/react@npm:~9.2.8":
version: 9.2.8
resolution: "@deck.gl/react@npm:9.2.8"
peerDependencies:
@@ -806,7 +751,7 @@ __metadata:
languageName: node
linkType: hard
-"@deck.gl/widgets@npm:9.2.8, @deck.gl/widgets@npm:~9.2.8":
+"@deck.gl/widgets@npm:~9.2.8":
version: 9.2.8
resolution: "@deck.gl/widgets@npm:9.2.8"
dependencies:
@@ -1781,7 +1726,7 @@ __metadata:
languageName: node
linkType: hard
-"@jridgewell/trace-mapping@npm:^0.3.12, @jridgewell/trace-mapping@npm:^0.3.24, @jridgewell/trace-mapping@npm:^0.3.28":
+"@jridgewell/trace-mapping@npm:^0.3.12, @jridgewell/trace-mapping@npm:^0.3.24, @jridgewell/trace-mapping@npm:^0.3.28, @jridgewell/trace-mapping@npm:^0.3.31":
version: 0.3.31
resolution: "@jridgewell/trace-mapping@npm:0.3.31"
dependencies:
@@ -1910,7 +1855,7 @@ __metadata:
languageName: node
linkType: hard
-"@loaders.gl/compression@npm:4.3.4, @loaders.gl/compression@npm:^4.3.4":
+"@loaders.gl/compression@npm:4.3.4":
version: 4.3.4
resolution: "@loaders.gl/compression@npm:4.3.4"
dependencies:
@@ -2086,7 +2031,7 @@ __metadata:
languageName: node
linkType: hard
-"@loaders.gl/schema@npm:4.3.4, @loaders.gl/schema@npm:^4.2.0, @loaders.gl/schema@npm:^4.3.3, @loaders.gl/schema@npm:^4.3.4":
+"@loaders.gl/schema@npm:4.3.4, @loaders.gl/schema@npm:^4.2.0, @loaders.gl/schema@npm:^4.3.4":
version: 4.3.4
resolution: "@loaders.gl/schema@npm:4.3.4"
dependencies:
@@ -2225,7 +2170,7 @@ __metadata:
languageName: node
linkType: hard
-"@luma.gl/core@npm:^9.2.6, @luma.gl/core@npm:~9.2.6":
+"@luma.gl/core@npm:^9.2.6, @luma.gl/core@npm:~9.2.0, @luma.gl/core@npm:~9.2.6":
version: 9.2.6
resolution: "@luma.gl/core@npm:9.2.6"
dependencies:
@@ -2336,13 +2281,6 @@ __metadata:
languageName: node
linkType: hard
-"@mapbox/geojson-types@npm:^1.0.2":
- version: 1.0.2
- resolution: "@mapbox/geojson-types@npm:1.0.2"
- checksum: 10c0/aa0a2cb95a358d8756ab5aa70356bcbd6f554a4571703a88a09e7db6580061d6ef4054db5fe3ecb2817c383b8b5433746a8f46712dc606b32063f73b154f99fc
- languageName: node
- linkType: hard
-
"@mapbox/jsonlint-lines-primitives@npm:^2.0.2, @mapbox/jsonlint-lines-primitives@npm:~2.0.2":
version: 2.0.2
resolution: "@mapbox/jsonlint-lines-primitives@npm:2.0.2"
@@ -2371,15 +2309,6 @@ __metadata:
languageName: node
linkType: hard
-"@mapbox/mapbox-gl-supported@npm:^1.5.0":
- version: 1.5.0
- resolution: "@mapbox/mapbox-gl-supported@npm:1.5.0"
- peerDependencies:
- mapbox-gl: ">=0.32.1 <2.0.0"
- checksum: 10c0/5b7712e8b546e598dc5152632504cad53081211b64ddc4447825840ddca703275bc36599167b9550ab906ca8a9554936bcdae562073fdef24b8d38d78ee262fb
- languageName: node
- linkType: hard
-
"@mapbox/martini@npm:^0.2.0":
version: 0.2.0
resolution: "@mapbox/martini@npm:0.2.0"
@@ -2394,13 +2323,6 @@ __metadata:
languageName: node
linkType: hard
-"@mapbox/tiny-sdf@npm:^1.1.1":
- version: 1.2.5
- resolution: "@mapbox/tiny-sdf@npm:1.2.5"
- checksum: 10c0/de0252388a628ddb491c986c715f0b63ca6a74f5dac16d3e51eb75a21935a31e34fba5db47c81cc59a462d782021fc68b2e3cc119b2d6cabe15d31e311674d6c
- languageName: node
- linkType: hard
-
"@mapbox/tiny-sdf@npm:^2.0.5, @mapbox/tiny-sdf@npm:^2.0.6":
version: 2.0.7
resolution: "@mapbox/tiny-sdf@npm:2.0.7"
@@ -2548,16 +2470,6 @@ __metadata:
languageName: node
linkType: hard
-"@math.gl/web-mercator@npm:^3.5.5":
- version: 3.6.3
- resolution: "@math.gl/web-mercator@npm:3.6.3"
- dependencies:
- "@babel/runtime": "npm:^7.12.0"
- gl-matrix: "npm:^3.4.0"
- checksum: 10c0/95a1267261e03fa0194ec5ac5f3f8c3a13e1182cf3865d4d3bbd669706d42bd067335d1812df2e844c63c1f0f21c50e08af2b90168f2db1d3fa3ba2087f59703
- languageName: node
- linkType: hard
-
"@monaco-editor/loader@npm:^1.5.0":
version: 1.6.1
resolution: "@monaco-editor/loader@npm:1.6.1"
@@ -3085,6 +2997,13 @@ __metadata:
languageName: node
linkType: hard
+"@probe.gl/env@npm:4.1.1, @probe.gl/env@npm:^4.1.1":
+ version: 4.1.1
+ resolution: "@probe.gl/env@npm:4.1.1"
+ checksum: 10c0/431f53d95936ba1ee08e8d4c155cc3c49d80d528acaff933ae8f32a4725f04d88ee634707e20baa885a505d6f1401f895bdfcfa42c259b3220dd26463d2cfb2d
+ languageName: node
+ linkType: hard
+
"@probe.gl/log@npm:4.1.0, @probe.gl/log@npm:^4.0.2, @probe.gl/log@npm:^4.0.4, @probe.gl/log@npm:^4.0.8, @probe.gl/log@npm:^4.1.0":
version: 4.1.0
resolution: "@probe.gl/log@npm:4.1.0"
@@ -3094,6 +3013,15 @@ __metadata:
languageName: node
linkType: hard
+"@probe.gl/log@npm:^4.1.1":
+ version: 4.1.1
+ resolution: "@probe.gl/log@npm:4.1.1"
+ dependencies:
+ "@probe.gl/env": "npm:4.1.1"
+ checksum: 10c0/713043aea095dbe6e97525a5d254f2563e9e0b6c64fdb888603f41342ed0f0a76a64e6996d2d461c7f1f9c6271a931b2e922d9e47f8c43f3d4116867f4cd5d0a
+ languageName: node
+ linkType: hard
+
"@probe.gl/stats@npm:^4.0.0, @probe.gl/stats@npm:^4.0.2, @probe.gl/stats@npm:^4.0.8, @probe.gl/stats@npm:^4.1.0":
version: 4.1.0
resolution: "@probe.gl/stats@npm:4.1.0"
@@ -3101,6 +3029,13 @@ __metadata:
languageName: node
linkType: hard
+"@probe.gl/stats@npm:^4.1.1":
+ version: 4.1.1
+ resolution: "@probe.gl/stats@npm:4.1.1"
+ checksum: 10c0/7fd49d891e5ecfc82330c0db9180cc6a3fae0f78062570fabdc1860f79f09b612c53af08b9d6e2bb701b79fb7156ec2c9c0ddf53e5dba12b0560c7bbc957afc7
+ languageName: node
+ linkType: hard
+
"@probe.gl/test-utils@npm:^4.0.4, @probe.gl/test-utils@npm:^4.0.6":
version: 4.1.0
resolution: "@probe.gl/test-utils@npm:4.1.0"
@@ -4223,36 +4158,6 @@ __metadata:
languageName: node
linkType: hard
-"@types/d3-array@npm:^3.0.2":
- version: 3.2.2
- resolution: "@types/d3-array@npm:3.2.2"
- checksum: 10c0/6137cb97302f8a4f18ca22c0560c585cfcb823f276b23d89f2c0c005d72697ec13bca671c08e68b4b0cabd622e3f0e91782ee221580d6774074050be96dd7028
- languageName: node
- linkType: hard
-
-"@types/d3-color@npm:^1.4.2":
- version: 1.4.5
- resolution: "@types/d3-color@npm:1.4.5"
- checksum: 10c0/d84233fdfb3ea81710a2aa7eb8f332a0199dfbefb10cd81a68e504dfb2a00b78d309ebe0b1b452c97d7b9de193a301b8ce3cec0842ec300277caef34ef2dd8ed
- languageName: node
- linkType: hard
-
-"@types/d3-scale@npm:^3.0.0":
- version: 3.3.5
- resolution: "@types/d3-scale@npm:3.3.5"
- dependencies:
- "@types/d3-time": "npm:^2"
- checksum: 10c0/2689ab13092e3fded22cdd1b888afd91aa60190be40c8eddc12b2d42de59b00917778340f90317c68c5ffc3a1bee68f5ca155434cd466bc7804f400f3f9e7529
- languageName: node
- linkType: hard
-
-"@types/d3-time@npm:^2":
- version: 2.1.4
- resolution: "@types/d3-time@npm:2.1.4"
- checksum: 10c0/b597bfa51a163d4231e953d6903b06fd6341d0f11a28222a79fafaddb46155d7f458a67c814de53df84926a47dd535897228a475679d228576b0cda87351e534
- languageName: node
- linkType: hard
-
"@types/deep-eql@npm:*":
version: 4.0.2
resolution: "@types/deep-eql@npm:4.0.2"
@@ -4290,20 +4195,6 @@ __metadata:
languageName: node
linkType: hard
-"@types/google.maps@npm:^3.48.6":
- version: 3.58.1
- resolution: "@types/google.maps@npm:3.58.1"
- checksum: 10c0/0247c61d2aeb216d04ab9392602ef41d58328c6336ed4e9a4e9e69834002e82c116a34def2bf4ceb31c4292f14899815058da4bb8d803147295c6b47c1162621
- languageName: node
- linkType: hard
-
-"@types/hammerjs@npm:^2.0.41":
- version: 2.0.46
- resolution: "@types/hammerjs@npm:2.0.46"
- checksum: 10c0/f3c1cb20dc2f0523f7b8c76065078544d50d8ae9b0edc1f62fed657210ed814266ff2dfa835d2c157a075991001eec3b64c88bf92e3e6e895c0db78d05711d06
- languageName: node
- linkType: hard
-
"@types/hoist-non-react-statics@npm:*":
version: 3.3.7
resolution: "@types/hoist-non-react-statics@npm:3.3.7"
@@ -4347,15 +4238,6 @@ __metadata:
languageName: node
linkType: hard
-"@types/mapbox-gl@npm:^2.0.3":
- version: 2.7.21
- resolution: "@types/mapbox-gl@npm:2.7.21"
- dependencies:
- "@types/geojson": "npm:*"
- checksum: 10c0/51bcf2a0481a53a8f8148af9b7f0f9e4d1be2ebdd9390194e4e4a0f8c8375446004c8624e67b4a255384cf0a0ae0c5b9eb99fa865acadca0ac0b0f5853f3f957
- languageName: node
- linkType: hard
-
"@types/mapbox__point-geometry@npm:*, @types/mapbox__point-geometry@npm:^0.1.4":
version: 0.1.4
resolution: "@types/mapbox__point-geometry@npm:0.1.4"
@@ -4493,16 +4375,6 @@ __metadata:
languageName: node
linkType: hard
-"@types/react@npm:^18.0.0":
- version: 18.3.28
- resolution: "@types/react@npm:18.3.28"
- dependencies:
- "@types/prop-types": "npm:*"
- csstype: "npm:^3.2.2"
- checksum: 10c0/683e19cd12b5c691215529af2e32b5ffbaccae3bf0ba93bfafa0e460e8dfee18423afed568be2b8eadf4b837c3749dd296a4f64e2d79f68fa66962c05f5af661
- languageName: node
- linkType: hard
-
"@types/react@npm:^18.3.3":
version: 18.3.26
resolution: "@types/react@npm:18.3.26"
@@ -4964,6 +4836,30 @@ __metadata:
languageName: node
linkType: hard
+"@vitest/coverage-v8@npm:^4.0.18":
+ version: 4.0.18
+ resolution: "@vitest/coverage-v8@npm:4.0.18"
+ dependencies:
+ "@bcoe/v8-coverage": "npm:^1.0.2"
+ "@vitest/utils": "npm:4.0.18"
+ ast-v8-to-istanbul: "npm:^0.3.10"
+ istanbul-lib-coverage: "npm:^3.2.2"
+ istanbul-lib-report: "npm:^3.0.1"
+ istanbul-reports: "npm:^3.2.0"
+ magicast: "npm:^0.5.1"
+ obug: "npm:^2.1.1"
+ std-env: "npm:^3.10.0"
+ tinyrainbow: "npm:^3.0.3"
+ peerDependencies:
+ "@vitest/browser": 4.0.18
+ vitest: 4.0.18
+ peerDependenciesMeta:
+ "@vitest/browser":
+ optional: true
+ checksum: 10c0/e23e0da86f0b2a020c51562bc40ebdc7fc7553c24f8071dfb39a6df0161badbd5eaf2eebbf8ceaef18933a18c1934ff52d1c0c4bde77bb87e0c1feb0c8cbee4d
+ languageName: node
+ linkType: hard
+
"@vitest/eslint-plugin@npm:^1.6.9":
version: 1.6.9
resolution: "@vitest/eslint-plugin@npm:1.6.9"
@@ -5277,7 +5173,7 @@ __metadata:
languageName: node
linkType: hard
-"apache-arrow@npm:>=14, apache-arrow@npm:>=15":
+"apache-arrow@npm:>=15":
version: 21.1.0
resolution: "apache-arrow@npm:21.1.0"
dependencies:
@@ -5561,6 +5457,17 @@ __metadata:
languageName: node
linkType: hard
+"ast-v8-to-istanbul@npm:^0.3.10":
+ version: 0.3.12
+ resolution: "ast-v8-to-istanbul@npm:0.3.12"
+ dependencies:
+ "@jridgewell/trace-mapping": "npm:^0.3.31"
+ estree-walker: "npm:^3.0.3"
+ js-tokens: "npm:^10.0.0"
+ checksum: 10c0/bad6ba222b1073c165c8d65dbf366193d4a90536dabe37f93a3df162269b1c9473975756e4c048f708c235efccc26f8e5321c547b7e9563b64b21b2e0f27cbc9
+ languageName: node
+ linkType: hard
+
"async-function@npm:^1.0.0":
version: 1.0.0
resolution: "async-function@npm:1.0.0"
@@ -6081,15 +5988,6 @@ __metadata:
languageName: node
linkType: hard
-"cartocolor@npm:^5.0.2":
- version: 5.0.2
- resolution: "cartocolor@npm:5.0.2"
- dependencies:
- colorbrewer: "npm:1.5.6"
- checksum: 10c0/305412d0354ed90b7db050f055a56fd68a95c2e9659d55c1f93272cd30d6c1148519e0b55378be1a1ad2c61147f63c3426c317f8ce5d7359f47266dc2d92f663
- languageName: node
- linkType: hard
-
"caseless@npm:~0.12.0":
version: 0.12.0
resolution: "caseless@npm:0.12.0"
@@ -6352,13 +6250,6 @@ __metadata:
languageName: node
linkType: hard
-"colorbrewer@npm:1.5.6":
- version: 1.5.6
- resolution: "colorbrewer@npm:1.5.6"
- checksum: 10c0/da8045058698e2df21df1b676184d9db6651b5c4c2d4775d24bfb60c761150c4e102b54db4a85f4b3019726b5a16053b040f026ee0214cc0ee6ba8612d8a54cd
- languageName: node
- linkType: hard
-
"columnify@npm:1.6.0":
version: 1.6.0
resolution: "columnify@npm:1.6.0"
@@ -6702,7 +6593,7 @@ __metadata:
languageName: node
linkType: hard
-"csscolorparser@npm:~1.0.2, csscolorparser@npm:~1.0.3":
+"csscolorparser@npm:~1.0.2":
version: 1.0.3
resolution: "csscolorparser@npm:1.0.3"
checksum: 10c0/57b30e1dd3e639fb74d63d3ee5a078ae6d0aaba26bc731fdec1a0f50fd77c2531a1fca2bbe07c18e0569255f92064cda0747e0115955fdb8c037332798ca634f
@@ -6725,13 +6616,6 @@ __metadata:
languageName: node
linkType: hard
-"csstype@npm:^3.2.2":
- version: 3.2.3
- resolution: "csstype@npm:3.2.3"
- checksum: 10c0/cd29c51e70fa822f1cecd8641a1445bed7063697469d35633b516e60fe8c1bde04b08f6c5b6022136bb669b64c63d4173af54864510fbb4ee23281801841a3ce
- languageName: node
- linkType: hard
-
"cwise-compiler@npm:^1.0.0":
version: 1.1.3
resolution: "cwise-compiler@npm:1.1.3"
@@ -6748,7 +6632,7 @@ __metadata:
languageName: node
linkType: hard
-"d3-array@npm:2 - 3, d3-array@npm:2.10.0 - 3, d3-array@npm:^3.2.0, d3-array@npm:^3.2.2, d3-array@npm:^3.2.4":
+"d3-array@npm:2 - 3, d3-array@npm:2.10.0 - 3, d3-array@npm:^3.2.2, d3-array@npm:^3.2.4":
version: 3.2.4
resolution: "d3-array@npm:3.2.4"
dependencies:
@@ -6757,7 +6641,7 @@ __metadata:
languageName: node
linkType: hard
-"d3-color@npm:1 - 3, d3-color@npm:^3.1.0":
+"d3-color@npm:1 - 3":
version: 3.1.0
resolution: "d3-color@npm:3.1.0"
checksum: 10c0/a4e20e1115fa696fce041fbe13fbc80dc4c19150fa72027a7c128ade980bc0eeeba4bcf28c9e21f0bce0e0dbfe7ca5869ef67746541dcfda053e4802ad19783c
@@ -6833,7 +6717,7 @@ __metadata:
languageName: node
linkType: hard
-"d3-scale@npm:^4.0.0, d3-scale@npm:^4.0.2":
+"d3-scale@npm:^4.0.2":
version: 4.0.2
resolution: "d3-scale@npm:4.0.2"
dependencies:
@@ -7003,6 +6887,7 @@ __metadata:
"@vitejs/plugin-react": "npm:^5.1.4"
"@vitest/browser": "npm:^4.0.18"
"@vitest/browser-playwright": "npm:^4.0.18"
+ "@vitest/coverage-v8": "npm:^4.0.18"
"@vitest/eslint-plugin": "npm:^1.6.9"
bin-pack: "npm:^1.0.2"
datauri: "npm:^4.1.0"
@@ -7019,166 +6904,6 @@ __metadata:
languageName: unknown
linkType: soft
-"deck.gl@npm:^9.1.0":
- version: 9.2.8
- resolution: "deck.gl@npm:9.2.8"
- dependencies:
- "@deck.gl/aggregation-layers": "npm:9.2.8"
- "@deck.gl/arcgis": "npm:9.2.8"
- "@deck.gl/carto": "npm:9.2.8"
- "@deck.gl/core": "npm:9.2.8"
- "@deck.gl/extensions": "npm:9.2.8"
- "@deck.gl/geo-layers": "npm:9.2.8"
- "@deck.gl/google-maps": "npm:9.2.8"
- "@deck.gl/json": "npm:9.2.8"
- "@deck.gl/layers": "npm:9.2.8"
- "@deck.gl/mapbox": "npm:9.2.8"
- "@deck.gl/mesh-layers": "npm:9.2.8"
- "@deck.gl/react": "npm:9.2.8"
- "@deck.gl/widgets": "npm:9.2.8"
- "@loaders.gl/core": "npm:^4.3.4"
- "@luma.gl/core": "npm:^9.2.6"
- "@luma.gl/engine": "npm:^9.2.6"
- peerDependencies:
- "@arcgis/core": ^4.0.0
- react: ">=16.3.0"
- react-dom: ">=16.3.0"
- peerDependenciesMeta:
- "@arcgis/core":
- optional: true
- react:
- optional: true
- react-dom:
- optional: true
- checksum: 10c0/1a442f0fd2f3cdc419b84ebea12acd525581ab6d61d9903342f4f3e05d55722369f9379d40325df12acdd968947fc5f75e931740f926ba6441b7ccf01a694a9d
- languageName: node
- linkType: hard
-
-"deckgl-example-geoarrow-linestring-layer@workspace:examples/arrow-layers-wip/linestring":
- version: 0.0.0-use.local
- resolution: "deckgl-example-geoarrow-linestring-layer@workspace:examples/arrow-layers-wip/linestring"
- dependencies:
- "@deck.gl-community/arrow-layers": "workspace:*"
- "@types/react": "npm:^18.0.0"
- "@types/react-dom": "npm:^18.0.0"
- apache-arrow: "npm:>=14"
- deck.gl: "npm:^9.1.0"
- react: "npm:^18.0.0"
- react-dom: "npm:^18.0.0"
- react-map-gl: "npm:^5.3.0"
- vite: "npm:^7.3.1"
- languageName: unknown
- linkType: soft
-
-"deckgl-example-geoarrow-multilinestring-layer@workspace:examples/arrow-layers-wip/multilinestring":
- version: 0.0.0-use.local
- resolution: "deckgl-example-geoarrow-multilinestring-layer@workspace:examples/arrow-layers-wip/multilinestring"
- dependencies:
- "@types/react": "npm:^18.0.0"
- "@types/react-dom": "npm:^18.0.0"
- apache-arrow: "npm:>=14"
- deck.gl: "npm:^9.1.0"
- react: "npm:^18.0.0"
- react-dom: "npm:^18.0.0"
- react-map-gl: "npm:^5.3.0"
- vite: "npm:^7.3.1"
- languageName: unknown
- linkType: soft
-
-"deckgl-example-geoarrow-multipoint-layer@workspace:examples/arrow-layers-wip/multipoint":
- version: 0.0.0-use.local
- resolution: "deckgl-example-geoarrow-multipoint-layer@workspace:examples/arrow-layers-wip/multipoint"
- dependencies:
- "@types/react": "npm:^18.0.0"
- "@types/react-dom": "npm:^18.0.0"
- apache-arrow: "npm:>=14"
- deck.gl: "npm:^9.1.0"
- react: "npm:^18.0.0"
- react-dom: "npm:^18.0.0"
- react-map-gl: "npm:^5.3.0"
- vite: "npm:^7.3.1"
- languageName: unknown
- linkType: soft
-
-"deckgl-example-geoarrow-multipolygon-layer@workspace:examples/arrow-layers-wip/multipolygon":
- version: 0.0.0-use.local
- resolution: "deckgl-example-geoarrow-multipolygon-layer@workspace:examples/arrow-layers-wip/multipolygon"
- dependencies:
- "@deck.gl-community/arrow-layers": "workspace:*"
- "@types/react": "npm:^18.0.0"
- "@types/react-dom": "npm:^18.0.0"
- apache-arrow: "npm:>=14"
- deck.gl: "npm:^9.1.0"
- react: "npm:^18.0.0"
- react-dom: "npm:^18.0.0"
- react-map-gl: "npm:^5.3.0"
- vite: "npm:^7.3.1"
- languageName: unknown
- linkType: soft
-
-"deckgl-example-geoarrow-point-layer@workspace:examples/arrow-layers-wip/point":
- version: 0.0.0-use.local
- resolution: "deckgl-example-geoarrow-point-layer@workspace:examples/arrow-layers-wip/point"
- dependencies:
- "@types/react": "npm:^18.0.0"
- "@types/react-dom": "npm:^18.0.0"
- apache-arrow: "npm:>=14"
- deck.gl: "npm:^9.1.0"
- react: "npm:^18.0.0"
- react-dom: "npm:^18.0.0"
- react-map-gl: "npm:^5.3.0"
- vite: "npm:^7.3.1"
- languageName: unknown
- linkType: soft
-
-"deckgl-example-geoarrow-polygon-layer@workspace:examples/arrow-layers-wip/polygon":
- version: 0.0.0-use.local
- resolution: "deckgl-example-geoarrow-polygon-layer@workspace:examples/arrow-layers-wip/polygon"
- dependencies:
- "@deck.gl-community/arrow-layers": "workspace:*"
- "@types/react": "npm:^18.0.0"
- "@types/react-dom": "npm:^18.0.0"
- apache-arrow: "npm:>=14"
- deck.gl: "npm:^9.1.0"
- react: "npm:^18.0.0"
- react-dom: "npm:^18.0.0"
- react-map-gl: "npm:^5.3.0"
- vite: "npm:^7.3.1"
- languageName: unknown
- linkType: soft
-
-"deckgl-example-geoarrow-text-layer@workspace:examples/arrow-layers-wip/text":
- version: 0.0.0-use.local
- resolution: "deckgl-example-geoarrow-text-layer@workspace:examples/arrow-layers-wip/text"
- dependencies:
- "@deck.gl-community/arrow-layers": "workspace:*"
- "@types/react": "npm:^18.0.0"
- "@types/react-dom": "npm:^18.0.0"
- apache-arrow: "npm:>=14"
- deck.gl: "npm:^9.1.0"
- react: "npm:^18.0.0"
- react-dom: "npm:^18.0.0"
- react-map-gl: "npm:^5.3.0"
- vite: "npm:^7.3.1"
- languageName: unknown
- linkType: soft
-
-"deckgl-example-geoarrow-trips-layer@workspace:examples/arrow-layers-wip/trips":
- version: 0.0.0-use.local
- resolution: "deckgl-example-geoarrow-trips-layer@workspace:examples/arrow-layers-wip/trips"
- dependencies:
- "@deck.gl-community/arrow-layers": "workspace:*"
- "@types/react": "npm:^18.0.0"
- "@types/react-dom": "npm:^18.0.0"
- apache-arrow: "npm:>=14"
- deck.gl: "npm:^9.1.0"
- react: "npm:^18.0.0"
- react-dom: "npm:^18.0.0"
- react-map-gl: "npm:^5.3.0"
- vite: "npm:^7.3.1"
- languageName: unknown
- linkType: soft
-
"decompress-response@npm:^6.0.0":
version: 6.0.0
resolution: "decompress-response@npm:6.0.0"
@@ -7506,7 +7231,7 @@ __metadata:
languageName: node
linkType: hard
-"earcut@npm:^2.2.2, earcut@npm:^2.2.4":
+"earcut@npm:^2.2.4":
version: 2.2.4
resolution: "earcut@npm:2.2.4"
checksum: 10c0/01ca51830edd2787819f904ae580087d37351f6048b4565e7add4b3da8a86b7bc19262ab2aa7fdc64129ab03af2d9cec8cccee4d230c82275f97ef285c79aafb
@@ -8537,13 +8262,6 @@ __metadata:
languageName: node
linkType: hard
-"esri-loader@npm:^3.7.0":
- version: 3.7.0
- resolution: "esri-loader@npm:3.7.0"
- checksum: 10c0/56c8febebb8698cb9884b9dd5d6c5800da0f4f3b66a10c0959892564621fbf976d73a3e65c5e940f86f7498897d50dff7354e17862041e9698a92a15efc60f8b
- languageName: node
- linkType: hard
-
"estraverse@npm:^5.1.0, estraverse@npm:^5.2.0, estraverse@npm:^5.3.0":
version: 5.3.0
resolution: "estraverse@npm:5.3.0"
@@ -8729,7 +8447,7 @@ __metadata:
languageName: node
linkType: hard
-"fdir@npm:^6.4.3, fdir@npm:^6.5.0":
+"fdir@npm:^6.4.3, fdir@npm:^6.4.6, fdir@npm:^6.5.0":
version: 6.5.0
resolution: "fdir@npm:6.5.0"
peerDependencies:
@@ -9103,13 +8821,6 @@ __metadata:
languageName: node
linkType: hard
-"geojson-vt@npm:^3.2.1":
- version: 3.2.1
- resolution: "geojson-vt@npm:3.2.1"
- checksum: 10c0/db2fc1a452067ee8436fa86e5a138f6ebd3d64893e0af097bc1cc960ec63d67c0ce77444711e9583036192d6bf9ce754bf9b56a76789684fc0fea4d52321fffc
- languageName: node
- linkType: hard
-
"geojson-vt@npm:^4.0.2":
version: 4.0.2
resolution: "geojson-vt@npm:4.0.2"
@@ -9313,7 +9024,7 @@ __metadata:
languageName: node
linkType: hard
-"gl-matrix@npm:^3.0.0, gl-matrix@npm:^3.2.1, gl-matrix@npm:^3.4.0, gl-matrix@npm:^3.4.3":
+"gl-matrix@npm:^3.0.0, gl-matrix@npm:^3.4.3":
version: 3.4.4
resolution: "gl-matrix@npm:3.4.4"
checksum: 10c0/9aa022ffac0d158212ad0cd29939864ad919ac31cd5dc5a5d35e9d66bb62679ddf152ff7b2173ded20131045e40572b87f31b26a920be2a7583a1516b13b5b4b
@@ -9484,13 +9195,6 @@ __metadata:
languageName: node
linkType: hard
-"grid-index@npm:^1.1.0":
- version: 1.1.0
- resolution: "grid-index@npm:1.1.0"
- checksum: 10c0/0ba2a622a52badc86642a002abee79b48c207092347e869528253e634573b9b55494db649fbd1779122d797aaa3b59e284023a96a7a5c666c7375f0e320f8f57
- languageName: node
- linkType: hard
-
"h3-js@npm:^4.1.0, h3-js@npm:^4.2.1":
version: 4.3.0
resolution: "h3-js@npm:4.3.0"
@@ -9498,13 +9202,6 @@ __metadata:
languageName: node
linkType: hard
-"hammerjs@npm:^2.0.8":
- version: 2.0.8
- resolution: "hammerjs@npm:2.0.8"
- checksum: 10c0/5c95e5774b5ea49492cb3fa8f1949aea67048a0b84af33acb555e7139abfcf3c83aca2b83e0c5008755bc230166df7b5e469d1e3eb6746c48f215f3672609fed
- languageName: node
- linkType: hard
-
"handlebars@npm:^4.7.7":
version: 4.7.8
resolution: "handlebars@npm:4.7.8"
@@ -9657,9 +9354,9 @@ __metadata:
languageName: node
linkType: hard
-"horizon-graph-layer-16cc55@workspace:examples/timeline-layers/horizon-graph-layer":
+"horizon-graph-layer-289f4b@workspace:dev/timeline-layers/examples/horizon-graph-layer":
version: 0.0.0-use.local
- resolution: "horizon-graph-layer-16cc55@workspace:examples/timeline-layers/horizon-graph-layer"
+ resolution: "horizon-graph-layer-289f4b@workspace:dev/timeline-layers/examples/horizon-graph-layer"
dependencies:
"@deck.gl-community/timeline-layers": "workspace:*"
"@deck.gl/core": "npm:~9.2.8"
@@ -10505,14 +10202,14 @@ __metadata:
languageName: node
linkType: hard
-"istanbul-lib-coverage@npm:^3.0.0, istanbul-lib-coverage@npm:^3.2.0":
+"istanbul-lib-coverage@npm:^3.0.0, istanbul-lib-coverage@npm:^3.2.0, istanbul-lib-coverage@npm:^3.2.2":
version: 3.2.2
resolution: "istanbul-lib-coverage@npm:3.2.2"
checksum: 10c0/6c7ff2106769e5f592ded1fb418f9f73b4411fd5a084387a5410538332b6567cd1763ff6b6cadca9b9eb2c443cce2f7ea7d7f1b8d315f9ce58539793b1e0922b
languageName: node
linkType: hard
-"istanbul-lib-report@npm:^3.0.0":
+"istanbul-lib-report@npm:^3.0.0, istanbul-lib-report@npm:^3.0.1":
version: 3.0.1
resolution: "istanbul-lib-report@npm:3.0.1"
dependencies:
@@ -10523,7 +10220,7 @@ __metadata:
languageName: node
linkType: hard
-"istanbul-reports@npm:^3.1.4":
+"istanbul-reports@npm:^3.1.4, istanbul-reports@npm:^3.2.0":
version: 3.2.0
resolution: "istanbul-reports@npm:3.2.0"
dependencies:
@@ -10608,6 +10305,13 @@ __metadata:
languageName: node
linkType: hard
+"js-tokens@npm:^10.0.0":
+ version: 10.0.0
+ resolution: "js-tokens@npm:10.0.0"
+ checksum: 10c0/a93498747812ba3e0c8626f95f75ab29319f2a13613a0de9e610700405760931624433a0de59eb7c27ff8836e526768fb20783861b86ef89be96676f2c996b64
+ languageName: node
+ linkType: hard
+
"js-tokens@npm:^3.0.0 || ^4.0.0, js-tokens@npm:^4.0.0":
version: 4.0.0
resolution: "js-tokens@npm:4.0.0"
@@ -10645,20 +10349,6 @@ __metadata:
languageName: node
linkType: hard
-"jsep@npm:^0.3.0":
- version: 0.3.5
- resolution: "jsep@npm:0.3.5"
- checksum: 10c0/fb5def7a4ba1cee41d144ebdd0d477785dc84b6bc1fed6cf5169f106de980dbe363bf99cb36a450435d7fd952d22b1d76e1609aeb5c7e7cbbbdb6d15fad03614
- languageName: node
- linkType: hard
-
-"jsep@npm:^1.4.0":
- version: 1.4.0
- resolution: "jsep@npm:1.4.0"
- checksum: 10c0/fe60adf47e050e22eadced42514a51a15a3cf0e2d147896584486acd8ee670fc16641101b9aeb81f4aaba382043d29744b7aac41171e8106515b14f27e0c7116
- languageName: node
- linkType: hard
-
"jsesc@npm:^3.0.2":
version: 3.1.0
resolution: "jsesc@npm:3.1.0"
@@ -10863,13 +10553,6 @@ __metadata:
languageName: node
linkType: hard
-"kdbush@npm:^3.0.0":
- version: 3.0.0
- resolution: "kdbush@npm:3.0.0"
- checksum: 10c0/3fc8795870bd04f60627e7345b26fd0644beb91bc4164912c9d9378b39c674ba01c31db68ecaf6266d51c9ad81bf5b770b7effa51eeee37553d38293a094a686
- languageName: node
- linkType: hard
-
"kdbush@npm:^4.0.2":
version: 4.0.2
resolution: "kdbush@npm:4.0.2"
@@ -11316,6 +10999,17 @@ __metadata:
languageName: node
linkType: hard
+"magicast@npm:^0.5.1":
+ version: 0.5.2
+ resolution: "magicast@npm:0.5.2"
+ dependencies:
+ "@babel/parser": "npm:^7.29.0"
+ "@babel/types": "npm:^7.29.0"
+ source-map-js: "npm:^1.2.1"
+ checksum: 10c0/924af677643c5a0a7d6cdb3247c0eb96fa7611b2ba6a5e720d35d81c503d3d9f5948eb5227f80f90f82ea3e7d38cffd10bb988f3fc09020db428e14f26e960d7
+ languageName: node
+ linkType: hard
+
"make-dir@npm:4.0.0, make-dir@npm:^4.0.0":
version: 4.0.0
resolution: "make-dir@npm:4.0.0"
@@ -11395,36 +11089,6 @@ __metadata:
languageName: node
linkType: hard
-"mapbox-gl@npm:^1.0.0":
- version: 1.13.3
- resolution: "mapbox-gl@npm:1.13.3"
- dependencies:
- "@mapbox/geojson-rewind": "npm:^0.5.2"
- "@mapbox/geojson-types": "npm:^1.0.2"
- "@mapbox/jsonlint-lines-primitives": "npm:^2.0.2"
- "@mapbox/mapbox-gl-supported": "npm:^1.5.0"
- "@mapbox/point-geometry": "npm:^0.1.0"
- "@mapbox/tiny-sdf": "npm:^1.1.1"
- "@mapbox/unitbezier": "npm:^0.0.0"
- "@mapbox/vector-tile": "npm:^1.3.1"
- "@mapbox/whoots-js": "npm:^3.1.0"
- csscolorparser: "npm:~1.0.3"
- earcut: "npm:^2.2.2"
- geojson-vt: "npm:^3.2.1"
- gl-matrix: "npm:^3.2.1"
- grid-index: "npm:^1.1.0"
- murmurhash-js: "npm:^1.0.0"
- pbf: "npm:^3.2.1"
- potpack: "npm:^1.0.1"
- quickselect: "npm:^2.0.0"
- rw: "npm:^1.3.3"
- supercluster: "npm:^7.1.0"
- tinyqueue: "npm:^2.0.3"
- vt-pbf: "npm:^3.1.1"
- checksum: 10c0/82bf86daa69e068d4138c4e5a1dbfc2a28c6d0e2cc142ef96d3ce671770e8173d3ffd62e6614b3606f932f99b40eb4dfc52d5f44c48dabd393ba27b61d9e50c4
- languageName: node
- linkType: hard
-
"maplibre-gl@npm:^4.1.3":
version: 4.7.1
resolution: "maplibre-gl@npm:4.7.1"
@@ -11832,16 +11496,6 @@ __metadata:
languageName: node
linkType: hard
-"mjolnir.js@npm:^2.5.0":
- version: 2.7.3
- resolution: "mjolnir.js@npm:2.7.3"
- dependencies:
- "@types/hammerjs": "npm:^2.0.41"
- hammerjs: "npm:^2.0.8"
- checksum: 10c0/82f86dedfd410a640e5d0ef6f04e1d72ab292e61d42434bc56f103670ada45b2aacf130527f434d7d4bd0b8d2d5bc82bd36850e7586f1b70ebb72da2fe2c72b2
- languageName: node
- linkType: hard
-
"mjolnir.js@npm:^3.0.0":
version: 3.0.0
resolution: "mjolnir.js@npm:3.0.0"
@@ -11886,22 +11540,6 @@ __metadata:
languageName: node
linkType: hard
-"moment-timezone@npm:^0.6.0":
- version: 0.6.0
- resolution: "moment-timezone@npm:0.6.0"
- dependencies:
- moment: "npm:^2.29.4"
- checksum: 10c0/16164cf321d8be0bf7d43855286b426c94c8200e0634f2e42cf469f591c6a230ac43f37d3826d76b05ac221f69a571400323fb8625e3d4e8669f4d9ab00fe779
- languageName: node
- linkType: hard
-
-"moment@npm:^2.29.4":
- version: 2.30.1
- resolution: "moment@npm:2.30.1"
- checksum: 10c0/865e4279418c6de666fca7786607705fd0189d8a7b7624e2e56be99290ac846f90878a6f602e34b4e0455c549b85385b1baf9966845962b313699e7cb847543a
- languageName: node
- linkType: hard
-
"monaco-editor@npm:^0.53.0":
version: 0.53.0
resolution: "monaco-editor@npm:0.53.0"
@@ -13239,13 +12877,6 @@ __metadata:
languageName: node
linkType: hard
-"potpack@npm:^1.0.1":
- version: 1.0.2
- resolution: "potpack@npm:1.0.2"
- checksum: 10c0/670c23898a4257130858b960c2e654d3327c0f6a7e7091ff5846f213e65af8f9476320b995b8ad561a47a4d1c359c7ef347de57d22e7b02597051abb52bc85c4
- languageName: node
- linkType: hard
-
"potpack@npm:^2.0.0":
version: 2.1.0
resolution: "potpack@npm:2.1.0"
@@ -13506,15 +13137,6 @@ __metadata:
languageName: node
linkType: hard
-"quadbin@npm:^0.4.0, quadbin@npm:^0.4.1-alpha.0":
- version: 0.4.2
- resolution: "quadbin@npm:0.4.2"
- dependencies:
- "@math.gl/web-mercator": "npm:^4.1.0"
- checksum: 10c0/40ad1efa7146762e3cf6d9abfe47d452e69cceb0a72b5caec27e8bdf66d1e4522d6a1bde7e643015d9651eaa9f12611208f073856662fb6266263dcbf72d6dd8
- languageName: node
- linkType: hard
-
"quadprog@npm:^1.6.1":
version: 1.6.1
resolution: "quadprog@npm:1.6.1"
@@ -13594,7 +13216,7 @@ __metadata:
languageName: node
linkType: hard
-"react-dom@npm:^18.0.0, react-dom@npm:^18.3.1":
+"react-dom@npm:^18.3.1":
version: 18.3.1
resolution: "react-dom@npm:18.3.1"
dependencies:
@@ -13653,24 +13275,6 @@ __metadata:
languageName: node
linkType: hard
-"react-map-gl@npm:^5.3.0":
- version: 5.3.21
- resolution: "react-map-gl@npm:5.3.21"
- dependencies:
- "@babel/runtime": "npm:^7.0.0"
- "@types/geojson": "npm:^7946.0.7"
- "@types/mapbox-gl": "npm:^2.0.3"
- mapbox-gl: "npm:^1.0.0"
- mjolnir.js: "npm:^2.5.0"
- prop-types: "npm:^15.7.2"
- resize-observer-polyfill: "npm:^1.5.1"
- viewport-mercator-project: "npm:^7.0.4"
- peerDependencies:
- react: ">=16.3.0"
- checksum: 10c0/fa7bf984b3c5af19dfdaf12018753bfe3cfd7a040bf94cb3ad669f5f1aa42a7ae8c32a8b4523554b16ba1c532197800a4656ecd7c0e2f8e5818b16b1c00ae0a3
- languageName: node
- linkType: hard
-
"react-map-gl@npm:^7.1.7":
version: 7.1.9
resolution: "react-map-gl@npm:7.1.9"
@@ -13742,7 +13346,7 @@ __metadata:
languageName: node
linkType: hard
-"react@npm:^18.0.0, react@npm:^18.3.1":
+"react@npm:^18.3.1":
version: 18.3.1
resolution: "react@npm:18.3.1"
dependencies:
@@ -13958,13 +13562,6 @@ __metadata:
languageName: node
linkType: hard
-"resize-observer-polyfill@npm:^1.5.1":
- version: 1.5.1
- resolution: "resize-observer-polyfill@npm:1.5.1"
- checksum: 10c0/5e882475067f0b97dc07e0f37c3e335ac5bc3520d463f777cec7e894bb273eddbfecb857ae668e6fb6881fd6f6bb7148246967172139302da50fa12ea3a15d95
- languageName: node
- linkType: hard
-
"resolve-cwd@npm:^3.0.0":
version: 3.0.0
resolution: "resolve-cwd@npm:3.0.0"
@@ -15214,15 +14811,6 @@ __metadata:
languageName: node
linkType: hard
-"supercluster@npm:^7.1.0":
- version: 7.1.5
- resolution: "supercluster@npm:7.1.5"
- dependencies:
- kdbush: "npm:^3.0.0"
- checksum: 10c0/bbebf45927d0019831731c94b78d1c9a1f3e2da0be9875d7ea75c6f261487e0f15d3f1822a9a49256e3c1672bdfb9138f9a5e44e2de99edb06cd1e758083e12d
- languageName: node
- linkType: hard
-
"supercluster@npm:^8.0.1":
version: 8.0.1
resolution: "supercluster@npm:8.0.1"
@@ -15516,6 +15104,21 @@ __metadata:
languageName: node
linkType: hard
+"timeline-layer-a0fda1@workspace:dev/timeline-layers/examples/timeline-layer":
+ version: 0.0.0-use.local
+ resolution: "timeline-layer-a0fda1@workspace:dev/timeline-layers/examples/timeline-layer"
+ dependencies:
+ "@deck.gl-community/timeline-layers": "workspace:*"
+ "@deck.gl/core": "npm:~9.2.1"
+ "@deck.gl/layers": "npm:~9.2.1"
+ "@deck.gl/react": "npm:~9.2.1"
+ "@luma.gl/core": "npm:~9.2.0"
+ react: "npm:^18.3.1"
+ react-dom: "npm:^18.3.1"
+ vite: "npm:7.1.1"
+ languageName: unknown
+ linkType: soft
+
"tiny-invariant@npm:^1.0.2":
version: 1.3.3
resolution: "tiny-invariant@npm:1.3.3"
@@ -15563,7 +15166,7 @@ __metadata:
languageName: node
linkType: hard
-"tinyglobby@npm:^0.2.12, tinyglobby@npm:^0.2.15":
+"tinyglobby@npm:^0.2.12, tinyglobby@npm:^0.2.14, tinyglobby@npm:^0.2.15":
version: 0.2.15
resolution: "tinyglobby@npm:0.2.15"
dependencies:
@@ -15573,7 +15176,7 @@ __metadata:
languageName: node
linkType: hard
-"tinyqueue@npm:^2.0.0, tinyqueue@npm:^2.0.3":
+"tinyqueue@npm:^2.0.0":
version: 2.0.3
resolution: "tinyqueue@npm:2.0.3"
checksum: 10c0/d7b590088f015a94a17132fa209c2f2a80c45158259af5474901fdf5932e95ea13ff6f034bcc725a6d5f66d3e5b888b048c310229beb25ad5bebb4f0a635abf2
@@ -16197,12 +15800,58 @@ __metadata:
languageName: node
linkType: hard
-"viewport-mercator-project@npm:^7.0.4":
- version: 7.0.4
- resolution: "viewport-mercator-project@npm:7.0.4"
+"vite@npm:7.1.1":
+ version: 7.1.1
+ resolution: "vite@npm:7.1.1"
dependencies:
- "@math.gl/web-mercator": "npm:^3.5.5"
- checksum: 10c0/9a8a12f4bd68355a8162a23d4b161133b45aee71b2453fd8d57f54c55466007428cf502d0c02f58620d4c92ba934c29608613bc3176dd8c1d348f8a9deb34e32
+ esbuild: "npm:^0.25.0"
+ fdir: "npm:^6.4.6"
+ fsevents: "npm:~2.3.3"
+ picomatch: "npm:^4.0.3"
+ postcss: "npm:^8.5.6"
+ rollup: "npm:^4.43.0"
+ tinyglobby: "npm:^0.2.14"
+ peerDependencies:
+ "@types/node": ^20.19.0 || >=22.12.0
+ jiti: ">=1.21.0"
+ less: ^4.0.0
+ lightningcss: ^1.21.0
+ sass: ^1.70.0
+ sass-embedded: ^1.70.0
+ stylus: ">=0.54.8"
+ sugarss: ^5.0.0
+ terser: ^5.16.0
+ tsx: ^4.8.1
+ yaml: ^2.4.2
+ dependenciesMeta:
+ fsevents:
+ optional: true
+ peerDependenciesMeta:
+ "@types/node":
+ optional: true
+ jiti:
+ optional: true
+ less:
+ optional: true
+ lightningcss:
+ optional: true
+ sass:
+ optional: true
+ sass-embedded:
+ optional: true
+ stylus:
+ optional: true
+ sugarss:
+ optional: true
+ terser:
+ optional: true
+ tsx:
+ optional: true
+ yaml:
+ optional: true
+ bin:
+ vite: bin/vite.js
+ checksum: 10c0/391a5c8b8f287b7b1653dedbe952fb4cb93bf7c99b19dab915cf63497892427198fef637e943a3391eacfecf7f2e8f55c40d0fa065fabdd885641430d0b74af7
languageName: node
linkType: hard
@@ -16433,7 +16082,7 @@ __metadata:
languageName: node
linkType: hard
-"vt-pbf@npm:^3.1.1, vt-pbf@npm:^3.1.3":
+"vt-pbf@npm:^3.1.3":
version: 3.1.3
resolution: "vt-pbf@npm:3.1.3"
dependencies: