zumly 0.9.11 → 0.92.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +325 -133
- package/dist/zumly.css +309 -22
- package/dist/zumly.js +7 -0
- package/dist/zumly.min.css +1 -5
- package/dist/zumly.mjs +6 -770
- package/docs/DRIVER_API.md +340 -0
- package/package.json +52 -41
- package/src/drivers/driver-helpers.js +315 -0
- package/src/drivers/index.js +49 -0
- package/dist/zumly.min.mjs +0 -6
- package/dist/zumly.umd.js +0 -809
package/README.md
CHANGED
|
@@ -1,26 +1,51 @@
|
|
|
1
1
|
<p align="center">
|
|
2
|
-
<a href="https://zumly
|
|
3
|
-
|
|
2
|
+
<a href="https://github.com/zumerlab/zumly">
|
|
3
|
+
<!-- Built from docs/zumly-logo.png in this repo; raw URL so the image works on npm too -->
|
|
4
|
+
<img src="https://raw.githubusercontent.com/zumerlab/zumly/main/docs/zumly-logo.png" alt="Zumly" width="200">
|
|
4
5
|
</a>
|
|
5
6
|
</p>
|
|
6
7
|
|
|
8
|
+
<h1 align="center">Z over XY</h1>
|
|
9
|
+
|
|
10
|
+
<p align="center"><strong>Focus-driven navigation.</strong></p>
|
|
11
|
+
<p align="center"><strong>Zoom into what matters.</strong></p>
|
|
12
|
+
|
|
7
13
|
<p align="center">
|
|
8
|
-
|
|
14
|
+
<a href="https://www.npmjs.com/package/zumly"><img src="https://img.shields.io/npm/v/zumly.svg" alt="npm version"></a>
|
|
9
15
|
</p>
|
|
10
16
|
|
|
11
17
|
<p align="center">
|
|
12
|
-
<a href="https://
|
|
18
|
+
<strong>Zumly</strong> is a JavaScript library for <strong>hierarchical zoom navigation</strong>: you move in <strong>Z</strong> (depth) through discrete views laid out in the <strong>XY</strong> plane, with spatial transitions instead of flat screen swaps. It is inspired by <a href="https://en.wikipedia.org/wiki/Zooming_user_interface">zoomable user interfaces (ZUI)</a> but targets <strong>structured, trigger-driven</strong> zoom—not infinite pan/zoom canvases.
|
|
13
19
|
</p>
|
|
14
20
|
|
|
21
|
+
## Status
|
|
22
|
+
|
|
23
|
+
Zumly is under active development. The core stack is stable: depth and lateral navigation, pluggable transition drivers (CSS, WAAPI, none, Anime.js, GSAP, Motion, custom), unified nav UI (depth + lateral, eight positions), view resolver and prefetch cache, optional **plugin** API (<code>.use()</code>), and the **hash router** plugin. View sources include HTML strings, URLs, async functions, objects with <code>render()</code>, DOM nodes, and web component tags.
|
|
24
|
+
|
|
25
|
+
Zoom-out geometry uses batched DOM reads plus pure math where possible to cut layout thrash before animations (see [Geometry optimization](docs/geometry-optimization.md)).
|
|
26
|
+
|
|
27
|
+
**Docs:** [Roadmap & topics](docs/roadMap.md) · [Transition drivers](docs/DRIVER_API.md) · [Geometry notes](docs/geometry-optimization.md)
|
|
28
|
+
|
|
15
29
|
## Overview
|
|
16
30
|
|
|
17
|
-
|
|
31
|
+
Unlike free-pan ZUIs, Zumly focuses on **discrete, hierarchical navigation**: users zoom into a focused element (<code>.zoom-me</code>) to open the next view, so **attention** (focus) and **depth** (Z) stay aligned with **layout** (XY).
|
|
32
|
+
|
|
33
|
+
The engine is **UI-agnostic**—you supply markup and CSS. Transforms and timing are handled for you; design systems and frameworks integrate by resolving each view to a DOM subtree (see **View sources** and **Framework integration** below).
|
|
18
34
|
|
|
19
|
-
|
|
35
|
+
## What Zumly is
|
|
36
|
+
|
|
37
|
+
Zumly is **not** a freeform zooming canvas or map-like navigation system. It is a **discrete, hierarchical zoom interface**: screens are **views** at different depths, connected by **triggers**, with continuous motion between them.
|
|
38
|
+
|
|
39
|
+
It fits especially well when:
|
|
40
|
+
|
|
41
|
+
* you want **focus-driven** flow (zoom into what matters)
|
|
42
|
+
* **spatial context** between parent and child should persist
|
|
43
|
+
* you are building menus, stories, dashboards, or exploratory UIs without a classic router-only metaphor
|
|
20
44
|
|
|
21
45
|
## Installation
|
|
22
46
|
|
|
23
47
|
### NPM
|
|
48
|
+
|
|
24
49
|
```sh
|
|
25
50
|
npm install zumly
|
|
26
51
|
|
|
@@ -29,235 +54,402 @@ npm install zumly
|
|
|
29
54
|
yarn add zumly
|
|
30
55
|
```
|
|
31
56
|
|
|
32
|
-
###
|
|
33
|
-
Include https://unpkg.com/zumly in your project in a `<script>` tag.
|
|
57
|
+
### CDN
|
|
34
58
|
|
|
59
|
+
Include Zumly in your project via a `<script>` tag from [unpkg.com/zumly](https://unpkg.com/zumly).
|
|
35
60
|
|
|
36
61
|
### Direct download
|
|
37
62
|
|
|
38
|
-
Download
|
|
39
|
-
|
|
63
|
+
Download the built files from [unpkg.com/zumly](https://unpkg.com/zumly/) (see the `dist` folder).
|
|
40
64
|
|
|
41
65
|
## Setup
|
|
42
66
|
|
|
67
|
+
### Browser bundle (global)
|
|
43
68
|
|
|
44
|
-
|
|
69
|
+
1. Add the CSS in your `<head>`:
|
|
45
70
|
|
|
46
|
-
1. Add CSS inside `<head>` tag:
|
|
47
71
|
```html
|
|
48
|
-
|
|
49
72
|
<link rel="stylesheet" href="zumly/dist/zumly.css">
|
|
73
|
+
<!-- or https://unpkg.com/zumly/dist/zumly.css -->
|
|
74
|
+
```
|
|
50
75
|
|
|
51
|
-
|
|
76
|
+
2. Load the JS bundle (it exposes `window.Zumly`):
|
|
52
77
|
|
|
78
|
+
```html
|
|
79
|
+
<script src="zumly/dist/zumly.js"></script>
|
|
80
|
+
<!-- or https://unpkg.com/zumly/dist/zumly.js -->
|
|
53
81
|
```
|
|
54
82
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
import Zumly from "zumly/dist/zumly.mjs"
|
|
83
|
+
## Hello World
|
|
84
|
+
|
|
85
|
+
1. Add a container with the class `zumly-canvas`:
|
|
59
86
|
|
|
60
|
-
|
|
61
|
-
|
|
87
|
+
```html
|
|
88
|
+
<div class="example zumly-canvas"></div>
|
|
62
89
|
```
|
|
63
90
|
|
|
64
|
-
|
|
91
|
+
2. Create your views and start Zumly:
|
|
65
92
|
|
|
66
|
-
|
|
67
|
-
|
|
93
|
+
```js
|
|
94
|
+
const hello = `
|
|
95
|
+
<div class="z-view">
|
|
96
|
+
H E L L O <br>
|
|
97
|
+
W <span class="zoom-me" data-to="world">O</span> R L D!
|
|
98
|
+
</div>
|
|
99
|
+
`;
|
|
68
100
|
|
|
69
|
-
|
|
101
|
+
const world = `
|
|
102
|
+
<div class="z-view">
|
|
103
|
+
<img src="https://raw.githubusercontent.com/zumly/website/gh-pages/images/world.png" alt="World">
|
|
104
|
+
</div>
|
|
105
|
+
`;
|
|
70
106
|
|
|
71
|
-
|
|
107
|
+
const app = new Zumly({
|
|
108
|
+
mount: '.example',
|
|
109
|
+
initialView: 'hello',
|
|
110
|
+
views: { hello, world },
|
|
111
|
+
});
|
|
72
112
|
|
|
113
|
+
await app.init();
|
|
73
114
|
```
|
|
74
115
|
|
|
75
|
-
|
|
76
|
-
|
|
116
|
+
- Live example: [CodePen](https://codepen.io/zircle/pen/yyaXvRN)
|
|
117
|
+
|
|
118
|
+
### Options
|
|
77
119
|
|
|
78
|
-
|
|
120
|
+
**Zumly constructor:**
|
|
79
121
|
|
|
80
|
-
|
|
122
|
+
| Option | Type | Required | Description |
|
|
123
|
+
|--------|------|----------|-------------|
|
|
124
|
+
| `mount` | string | Yes | CSS selector for the canvas element (must have class `zumly-canvas`). |
|
|
125
|
+
| `initialView` | string | Yes | Name of the first view to show. |
|
|
126
|
+
| `views` | object | Yes | Map of view names to view sources (see View sources below). |
|
|
127
|
+
| `preload` | string[] | No | View names to resolve and cache when the app initializes. |
|
|
128
|
+
| `transitions` | object | No | Duration, ease, cover, driver, effects, stagger, hideTrigger for zoom transitions. |
|
|
129
|
+
| `deferred` | boolean | No | Defer content rendering until after animation completes (default: `false`). |
|
|
130
|
+
| `debug` | boolean | No | Enable debug messages (default: `false`). |
|
|
131
|
+
| `lateralNav` | boolean \| object | No | Lateral navigation UI: `{ mode, arrows, dots, keepAlive }`. |
|
|
132
|
+
| `depthNav` | boolean \| object | No | Depth navigation UI: `{ button, indicator }`. |
|
|
133
|
+
| `navPosition` | string | No | Nav bar position preset (default: `'bottom-center'`). |
|
|
134
|
+
| `inputs` | boolean \| object | No | Input methods: `{ click, keyboard, wheel, touch }`. |
|
|
135
|
+
| `componentContext` | object | No | Context passed to component-style views. |
|
|
81
136
|
|
|
137
|
+
**Transitions (optional):**
|
|
138
|
+
|
|
139
|
+
```js
|
|
140
|
+
transitions: {
|
|
141
|
+
driver: 'css', // 'css' | 'waapi' | 'anime' | 'gsap' | 'motion' | 'none' or custom function(spec, onComplete)
|
|
142
|
+
cover: 'width', // or 'height' — how the previous view scales to cover the trigger
|
|
143
|
+
duration: '1s',
|
|
144
|
+
ease: 'ease-in-out',
|
|
145
|
+
effects: ['blur(3px) brightness(0.7)', 'blur(8px) saturate(0)'], // CSS filters for [previous, last] background views
|
|
146
|
+
stagger: 0, // delay (ms) between layers during transition
|
|
147
|
+
hideTrigger: false, // false | true (visibility:hidden) | 'fade' (opacity crossfade)
|
|
148
|
+
// threshold: { enabled: true, duration: 300, commitAt: 0.5 } // parsed but not wired in the engine yet
|
|
149
|
+
}
|
|
82
150
|
```
|
|
83
151
|
|
|
152
|
+
`transitions.parallax` is accepted for compatibility but **not applied** (reserved; intensity is fixed to `0` in the engine).
|
|
84
153
|
|
|
85
|
-
|
|
154
|
+
**Transition drivers:** Zoom animations are handled by a pluggable driver (`transitions.driver`). You can swap implementations without changing app logic. To author your own, see [docs/DRIVER_API.md](docs/DRIVER_API.md) and the `zumly/driver-helpers` export.
|
|
86
155
|
|
|
87
|
-
|
|
156
|
+
| Driver | Description |
|
|
157
|
+
|--------|-------------|
|
|
158
|
+
| `'css'` (default) | CSS keyframes and `animationend`; uses `zumly.css` variables. |
|
|
159
|
+
| `'waapi'` | Web Animations API (`element.animate()`). No extra dependency. |
|
|
160
|
+
| `'none'` | No animation; applies final state immediately. Useful for tests or instant UX. |
|
|
161
|
+
| `'anime'` | [Anime.js](https://animejs.com/) — requires global `anime` (load from CDN before use). |
|
|
162
|
+
| `'gsap'` | [GSAP](https://greensock.com/gsap/) — requires global `gsap` (load from CDN before use). |
|
|
163
|
+
| `'motion'` | [Motion](https://motion.dev/) — requires global `Motion` (load from CDN before use). |
|
|
164
|
+
| `function(spec, onComplete)` | Custom driver. Receives `{ type, currentView, previousView, lastView, currentStage, duration, ease }` and must call `onComplete()` when done. |
|
|
88
165
|
|
|
89
|
-
|
|
166
|
+
Example with instant transitions (e.g. for tests):
|
|
90
167
|
|
|
91
|
-
|
|
168
|
+
```js
|
|
169
|
+
const app = new Zumly({
|
|
170
|
+
mount: '.canvas',
|
|
171
|
+
initialView: 'home',
|
|
172
|
+
views: { home, detail },
|
|
173
|
+
transitions: { driver: 'none', duration: '0s' },
|
|
174
|
+
});
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
**Lateral navigation:**
|
|
92
178
|
|
|
179
|
+
```js
|
|
180
|
+
lateralNav: true // mode: 'auto' (default)
|
|
181
|
+
lateralNav: false // disabled
|
|
182
|
+
lateralNav: { mode: 'always' } // always show when siblings exist
|
|
183
|
+
lateralNav: { mode: 'auto', dots: false } // auto mode, no dots
|
|
93
184
|
```
|
|
94
185
|
|
|
95
|
-
|
|
186
|
+
| Mode | Description |
|
|
187
|
+
|------|-------------|
|
|
188
|
+
| `'auto'` (default) | Shows lateral nav only when the current view doesn't cover the full canvas — preserving spatial context. |
|
|
189
|
+
| `'always'` | Always shows lateral nav when siblings exist, regardless of coverage. |
|
|
190
|
+
|
|
191
|
+
In `'auto'` mode, when a view covers 100% of the canvas the user perceives a new independent space, so the lateral nav is suppressed to avoid a floating control with no visual context.
|
|
192
|
+
|
|
193
|
+
**Navigation position (`navPosition`):**
|
|
96
194
|
|
|
97
195
|
```js
|
|
98
|
-
//
|
|
99
|
-
|
|
100
|
-
<div class="z-view">
|
|
101
|
-
H E L L O <br>
|
|
102
|
-
W <span class="zoom-me" data-to="world">O</span> R L D!
|
|
103
|
-
</div>
|
|
104
|
-
`;
|
|
196
|
+
navPosition: 'bottom-center' // default
|
|
197
|
+
```
|
|
105
198
|
|
|
106
|
-
|
|
199
|
+
| Preset | Position | Layout |
|
|
200
|
+
|--------|----------|--------|
|
|
201
|
+
| `'bottom-center'` | Bottom center | Horizontal |
|
|
202
|
+
| `'bottom-left'` | Bottom left | Horizontal |
|
|
203
|
+
| `'bottom-right'` | Bottom right | Horizontal |
|
|
204
|
+
| `'top-center'` | Top center | Horizontal |
|
|
205
|
+
| `'top-left'` | Top left | Horizontal |
|
|
206
|
+
| `'top-right'` | Top right | Horizontal |
|
|
207
|
+
| `'middle-left'` | Left center | Vertical |
|
|
208
|
+
| `'middle-right'` | Right center | Vertical |
|
|
209
|
+
|
|
210
|
+
The `'middle-left'` and `'middle-right'` presets render the nav bar vertically with rotated arrows.
|
|
211
|
+
|
|
212
|
+
**Zoomable elements:**
|
|
213
|
+
|
|
214
|
+
- Give the view root the class `z-view`.
|
|
215
|
+
- Add class `zoom-me` and `data-to="viewName"` to the element that triggers zoom-in.
|
|
216
|
+
- Per-trigger overrides via `data-*` attributes:
|
|
217
|
+
|
|
218
|
+
| Attribute | Description |
|
|
219
|
+
|-----------|-------------|
|
|
220
|
+
| `data-to` | **Required.** Target view name. |
|
|
221
|
+
| `data-with-duration` | Override transition duration (e.g. `"2s"`). |
|
|
222
|
+
| `data-with-ease` | Override easing function. |
|
|
223
|
+
| `data-with-cover` | Override cover dimension (`"width"` or `"height"`). |
|
|
224
|
+
| `data-with-stagger` | Override stagger delay in ms (e.g. `"100"`). |
|
|
225
|
+
| `data-with-effects` | Override effects (pipe-separated: `"blur(5px)\|blur(10px)"`). |
|
|
226
|
+
| `data-hide-trigger` | Override hideTrigger (`"fade"` or presence = hide). |
|
|
227
|
+
| `data-deferred` | Override deferred rendering (presence = true). |
|
|
228
|
+
| `data-*` | Any other data attribute becomes a prop in `ViewContext.props`. |
|
|
229
|
+
|
|
230
|
+
```html
|
|
107
231
|
<div class="z-view">
|
|
108
|
-
<
|
|
232
|
+
<div class="zoom-me" data-to="detail"
|
|
233
|
+
data-with-duration="2s"
|
|
234
|
+
data-with-ease="ease-in"
|
|
235
|
+
data-with-cover="height"
|
|
236
|
+
data-with-stagger="100"
|
|
237
|
+
data-id="42">
|
|
238
|
+
Zoom in
|
|
239
|
+
</div>
|
|
109
240
|
</div>
|
|
110
|
-
|
|
241
|
+
```
|
|
111
242
|
|
|
112
|
-
|
|
113
|
-
const app = new Zumly({
|
|
114
|
-
mount: '.example',
|
|
115
|
-
initialView: 'hello',
|
|
116
|
-
views: {
|
|
117
|
-
hello,
|
|
118
|
-
world
|
|
119
|
-
}
|
|
120
|
-
})
|
|
243
|
+
### View sources
|
|
121
244
|
|
|
122
|
-
|
|
245
|
+
Each entry in `views` is a **view source**. The resolver detects the type and resolves to a DOM node. Hyphenated view names (e.g. `'my-dashboard'`) are resolved as keys in `views` first; only raw template strings with a hyphen are treated as web components.
|
|
123
246
|
|
|
124
|
-
|
|
247
|
+
| Type | Example | Cached? |
|
|
248
|
+
|------|---------|---------|
|
|
249
|
+
| **HTML string** | `'<div class="z-view">…</div>'` | Yes (indefinitely) |
|
|
250
|
+
| **URL** | `'/views/detail.html'`, `https://…` | Yes (5 min TTL) |
|
|
251
|
+
| **Async function** | `(ctx) => fetch(...).then(r => r.text())` or return `HTMLElement` | No |
|
|
252
|
+
| **Object with `render()`** | `{ render(ctx) { return '<div>…</div>' }, mounted?() }` | No |
|
|
253
|
+
| **Web component** | `'my-view'` (string with hyphen, not a key in `views`) | No |
|
|
125
254
|
|
|
126
|
-
-
|
|
255
|
+
**View pipeline:** Resolve → normalize `.z-view` → insert into canvas → call `mounted()` (if present). Static/URL views are cloned from cache on each `get()` so consumers cannot mutate the stored node.
|
|
127
256
|
|
|
128
|
-
###
|
|
257
|
+
### Framework integration
|
|
129
258
|
|
|
130
|
-
|
|
259
|
+
Zumly is framework-agnostic. Since views resolve to DOM elements, any framework that can mount into a container works out of the box. Use **function views** or **object views** to bridge your framework:
|
|
260
|
+
|
|
261
|
+
**React**
|
|
262
|
+
|
|
263
|
+
```jsx
|
|
264
|
+
import { createRoot } from 'react-dom/client'
|
|
265
|
+
import Dashboard from './Dashboard'
|
|
131
266
|
|
|
132
|
-
```js
|
|
133
267
|
const app = new Zumly({
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
// First rendered view name. String. Required
|
|
137
|
-
initialView: 'viewName',
|
|
138
|
-
// Store all views. Object. Required
|
|
268
|
+
mount: '.canvas',
|
|
269
|
+
initialView: 'home',
|
|
139
270
|
views: {
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
// Effects for background views. Array. ['blur', 'sepia', 'saturate']
|
|
147
|
-
effects: ['sepia'],
|
|
148
|
-
// How new injected view is adapted. String. Default 'width'
|
|
149
|
-
cover: 'height',
|
|
150
|
-
// Transition duration. String. Default '1s'
|
|
151
|
-
duration: '1300ms' ,
|
|
152
|
-
// Transition ease. String. Default 'ease-in-out'
|
|
153
|
-
ease: 'cubic-bezier(0.25,0.1,0.25,1)'
|
|
154
|
-
},
|
|
155
|
-
// Activate debug notifications. Boolean. Default false
|
|
156
|
-
debug: true
|
|
271
|
+
home: '<div class="z-view"><div class="zoom-me" data-to="dashboard" data-id="42">Open</div></div>',
|
|
272
|
+
dashboard: ({ target, props }) => {
|
|
273
|
+
const root = createRoot(target)
|
|
274
|
+
root.render(<Dashboard id={props.id} />)
|
|
275
|
+
}
|
|
276
|
+
}
|
|
157
277
|
})
|
|
158
|
-
// Initialize instance
|
|
159
|
-
app.init()
|
|
160
278
|
```
|
|
161
279
|
|
|
162
|
-
|
|
280
|
+
**Vue**
|
|
163
281
|
|
|
164
|
-
|
|
282
|
+
```js
|
|
283
|
+
import { createApp } from 'vue'
|
|
284
|
+
import Dashboard from './Dashboard.vue'
|
|
165
285
|
|
|
166
|
-
|
|
286
|
+
views: {
|
|
287
|
+
dashboard: ({ target, props }) => {
|
|
288
|
+
createApp(Dashboard, { id: props.id }).mount(target)
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
```
|
|
292
|
+
|
|
293
|
+
**Svelte**
|
|
167
294
|
|
|
168
|
-
|
|
295
|
+
```js
|
|
296
|
+
import Dashboard from './Dashboard.svelte'
|
|
169
297
|
|
|
298
|
+
views: {
|
|
299
|
+
dashboard: ({ target, props }) => {
|
|
300
|
+
new Dashboard({ target, props: { id: props.id } })
|
|
301
|
+
}
|
|
302
|
+
}
|
|
170
303
|
```
|
|
171
304
|
|
|
172
|
-
|
|
305
|
+
**Angular**
|
|
173
306
|
|
|
174
|
-
```
|
|
307
|
+
```ts
|
|
308
|
+
views: {
|
|
309
|
+
dashboard: ({ target, props }) => {
|
|
310
|
+
const compRef = viewContainerRef.createComponent(DashboardComponent)
|
|
311
|
+
compRef.instance.id = props.id
|
|
312
|
+
target.appendChild(compRef.location.nativeElement)
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
```
|
|
316
|
+
|
|
317
|
+
**Key points:**
|
|
318
|
+
|
|
319
|
+
- The `target` parameter is a fresh `<div>` created by Zumly — mount your component there.
|
|
320
|
+
- `props` contains data attributes from the trigger element (`data-id="42"` → `props.id`).
|
|
321
|
+
- `componentContext` (constructor option) is passed as `context` to all function/object views — use it for shared state (router, store, API client).
|
|
322
|
+
- Function views are **never cached** — they resolve fresh each time, so framework components get proper lifecycle management.
|
|
323
|
+
- Use `mounted()` (object views) for post-insertion setup — it runs after the node is in the DOM.
|
|
324
|
+
- Zumly handles wrapped elements (e.g. Svelte's extra parent div) in its cleanup logic.
|
|
175
325
|
|
|
176
|
-
|
|
326
|
+
### Preload and prefetch
|
|
177
327
|
|
|
328
|
+
- **Eager preload:** `preload: ['viewA', 'viewB']` — those views are resolved and cached during `init()`.
|
|
329
|
+
- **Hover prefetch:** `mouseover` on a `.zoom-me[data-to]` trigger prefetches its target in the background.
|
|
330
|
+
- **Focus prefetch:** `focusin` on a `.zoom-me[data-to]` also prefetches (for keyboard/accessibility).
|
|
331
|
+
- **Scan prefetch:** When a view becomes current, all `.zoom-me[data-to]` targets inside it are prefetched in the background. This works on touch devices where hover is unavailable.
|
|
332
|
+
|
|
333
|
+
### Plugins
|
|
334
|
+
|
|
335
|
+
Zumly has a lightweight plugin system. Register plugins with `.use()` before or after `init()`:
|
|
336
|
+
|
|
337
|
+
```js
|
|
338
|
+
app.use(plugin, options)
|
|
178
339
|
```
|
|
179
340
|
|
|
180
|
-
|
|
341
|
+
A plugin is an object with `install(instance, options)` or a plain function `(instance, options) => void`.
|
|
181
342
|
|
|
182
|
-
|
|
343
|
+
#### Router plugin
|
|
183
344
|
|
|
184
|
-
|
|
185
|
-
Zoom me!
|
|
186
|
-
</div>
|
|
345
|
+
Syncs the browser URL hash with Zumly's navigation state. Browser back triggers zoom-out or lateral navigation. Forward is intentionally blocked — in a ZUI, zoom-in requires a trigger element for proper origin and animation context.
|
|
187
346
|
|
|
347
|
+
```js
|
|
348
|
+
// Script tag
|
|
349
|
+
const app = new Zumly({ ... })
|
|
350
|
+
app.use(Zumly.Router)
|
|
351
|
+
await app.init()
|
|
352
|
+
|
|
353
|
+
// ES Module (named export from the package entry)
|
|
354
|
+
import { Zumly, ZumlyRouter } from 'zumly'
|
|
355
|
+
const app = new Zumly({ ... })
|
|
356
|
+
app.use(ZumlyRouter)
|
|
357
|
+
await app.init()
|
|
188
358
|
```
|
|
189
359
|
|
|
190
|
-
|
|
360
|
+
The UMD/IIFE bundle attaches the same plugin as `Zumly.Router`. There is no separate published subpath for the router; import it from `'zumly'` or use `Zumly.Router` on `window` when using a script tag.
|
|
191
361
|
|
|
192
|
-
|
|
362
|
+
**Options:**
|
|
193
363
|
|
|
194
|
-
|
|
364
|
+
| Option | Type | Default | Description |
|
|
365
|
+
|--------|------|---------|-------------|
|
|
366
|
+
| `separator` | string | `'/'` | Character used to join view path segments in the hash. |
|
|
367
|
+
| `prefix` | string | `'/'` | Prefix before the path in the hash. |
|
|
195
368
|
|
|
196
|
-
|
|
369
|
+
**Behavior:**
|
|
197
370
|
|
|
198
|
-
|
|
371
|
+
| Action | Hash update | History |
|
|
372
|
+
|--------|-------------|---------|
|
|
373
|
+
| Zoom in | `pushState` | Enables browser back |
|
|
374
|
+
| Lateral | `pushState` | Enables browser back |
|
|
375
|
+
| Zoom out | `replaceState` | No forward entry |
|
|
376
|
+
| Browser back | Triggers `zoomOut()` or lateral `goTo()` | — |
|
|
377
|
+
| Browser forward | Blocked (`history.back()`) | — |
|
|
199
378
|
|
|
200
|
-
|
|
379
|
+
**Example URL:** `#/home/showcases/mercedes`
|
|
201
380
|
|
|
202
|
-
|
|
203
|
-
npm run dev
|
|
381
|
+
### Limitations and non-goals
|
|
204
382
|
|
|
205
|
-
|
|
383
|
+
- **No deep-linking:** The router plugin syncs hash on navigation and supports browser back, but does not support forward or deep-linking (entering a multi-level URL directly). In a ZUI, zoom-in requires a trigger element for proper spatial context.
|
|
384
|
+
- **Resize handling:** Cheap correction when canvas resizes — translate and origin scaled by ratio; scale preserved. Correction is deferred if a transition is running.
|
|
385
|
+
- **Remote views:** URL-backed views use `innerHTML`; sanitize external content to avoid XSS.
|
|
206
386
|
|
|
207
|
-
|
|
208
|
-
|
|
387
|
+
## Development
|
|
388
|
+
|
|
389
|
+
### Requirements
|
|
209
390
|
|
|
210
|
-
|
|
391
|
+
- Node.js >= 18 (or 16+ with ES module support)
|
|
211
392
|
|
|
212
|
-
###
|
|
393
|
+
### Commands
|
|
213
394
|
|
|
214
395
|
```sh
|
|
396
|
+
# Build the library
|
|
397
|
+
npm run compile
|
|
398
|
+
|
|
399
|
+
# Build and serve the demo at http://localhost:9090
|
|
400
|
+
npm run dev
|
|
401
|
+
|
|
402
|
+
# Run tests (Vitest + Playwright). Install browsers first:
|
|
403
|
+
npm run test:install-browsers
|
|
215
404
|
npm run test
|
|
216
405
|
|
|
217
|
-
#
|
|
406
|
+
# Run tests with coverage
|
|
407
|
+
npm run test:coverage
|
|
218
408
|
|
|
219
|
-
|
|
409
|
+
# Build and pack for publish
|
|
410
|
+
npm run build
|
|
220
411
|
```
|
|
221
412
|
|
|
413
|
+
Tests use [Vitest](https://vitest.dev/) with the browser provider ([Playwright](https://playwright.dev/)), same setup as [SnapDOM](https://github.com/zumerlab/snapdom). Run `npm run test:install-browsers` once (or after upgrading Playwright) to install Chromium.
|
|
414
|
+
|
|
222
415
|
### Building
|
|
223
416
|
|
|
224
417
|
```sh
|
|
225
|
-
npm run
|
|
226
|
-
|
|
227
|
-
# or
|
|
228
|
-
|
|
229
|
-
yarn build
|
|
418
|
+
npm run compile
|
|
230
419
|
```
|
|
231
420
|
|
|
232
|
-
|
|
421
|
+
Output is in the `dist/` folder.
|
|
233
422
|
|
|
234
|
-
|
|
423
|
+
## Changelog
|
|
235
424
|
|
|
236
|
-
|
|
425
|
+
See [CHANGELOG.md](CHANGELOG.md) for version history.
|
|
237
426
|
|
|
238
|
-
|
|
427
|
+
## Roadmap
|
|
239
428
|
|
|
240
|
-
|
|
429
|
+
**Done:**
|
|
430
|
+
- Depth and lateral navigation (`zoomIn`, `zoomOut`, `goTo`, `back`, `zoomTo`)
|
|
431
|
+
- Lateral nav UI (`lateralNav`: `mode` auto/always, arrows, dots, `keepAlive`)
|
|
432
|
+
- Depth nav UI (`depthNav`) and `navPosition` presets
|
|
433
|
+
- Inputs toggles (`inputs`: wheel, keyboard, click, touch)
|
|
434
|
+
- Plugin system (`use()`), router plugin (hash sync, back, forward blocked)
|
|
435
|
+
- Resize correction (translate/origin scaling; deferred while transitioning)
|
|
436
|
+
- Pluggable drivers (CSS, WAAPI, none, Anime.js, GSAP, Motion, custom)
|
|
437
|
+
- Batched zoom-out reads + math helpers to reduce reflow (see [geometry-optimization.md](docs/geometry-optimization.md))
|
|
241
438
|
|
|
242
|
-
|
|
243
|
-
-
|
|
244
|
-
-
|
|
245
|
-
- Add programmatic navigation.
|
|
246
|
-
- Add preseted navigation.
|
|
247
|
-
- Add router. [#3](https://github.com/zumly/zumly/issues/3)
|
|
248
|
-
- Allow recalculate zoom position on resize events.
|
|
439
|
+
**Planned:**
|
|
440
|
+
- Router deep-linking (open a multi-level hash cold)
|
|
441
|
+
- Accessibility (focus moves, broader ARIA)
|
|
249
442
|
|
|
443
|
+
Details and more topics: [docs/roadMap.md](docs/roadMap.md). Driver contract and helpers: [docs/DRIVER_API.md](docs/DRIVER_API.md).
|
|
250
444
|
|
|
251
|
-
##
|
|
445
|
+
## Community
|
|
252
446
|
|
|
253
447
|
- [Telegram group](https://t.me/ZumlyCommunity)
|
|
254
448
|
|
|
255
|
-
##
|
|
449
|
+
## Origin
|
|
256
450
|
|
|
257
|
-
Zumly is a
|
|
451
|
+
Zumly is a reimagined, framework-agnostic zoom engine inspired by [Zircle UI](https://github.com/zircleUI/zircleUI). Part of the [Zumerlab](https://github.com/zumerlab) ecosystem — use it with [Orbit](https://github.com/zumerlab/orbit) for radial layouts and [SnapDOM](https://github.com/zumerlab/snapdom) for lightweight DOM diffing.
|
|
258
452
|
|
|
259
453
|
## License
|
|
260
454
|
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
455
|
+
MIT. See [LICENSE](LICENSE).
|