zumly 0.18.1 → 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 +256 -34
- package/dist/zumly.css +243 -2
- package/dist/zumly.js +2 -2
- package/dist/zumly.min.css +1 -1
- package/dist/zumly.mjs +2 -2
- package/docs/DRIVER_API.md +340 -0
- package/package.json +27 -2
- package/src/drivers/driver-helpers.js +315 -0
- package/src/drivers/index.js +49 -0
package/README.md
CHANGED
|
@@ -1,26 +1,46 @@
|
|
|
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
|
|
|
15
21
|
## Status
|
|
16
22
|
|
|
17
|
-
Zumly is under active development. The
|
|
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)
|
|
18
28
|
|
|
19
29
|
## Overview
|
|
20
30
|
|
|
21
|
-
|
|
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).
|
|
22
34
|
|
|
23
|
-
|
|
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
|
|
24
44
|
|
|
25
45
|
## Installation
|
|
26
46
|
|
|
@@ -93,7 +113,7 @@ const app = new Zumly({
|
|
|
93
113
|
await app.init();
|
|
94
114
|
```
|
|
95
115
|
|
|
96
|
-
- Live example: [CodePen](https://codepen.io/
|
|
116
|
+
- Live example: [CodePen](https://codepen.io/zircle/pen/yyaXvRN)
|
|
97
117
|
|
|
98
118
|
### Options
|
|
99
119
|
|
|
@@ -105,33 +125,43 @@ await app.init();
|
|
|
105
125
|
| `initialView` | string | Yes | Name of the first view to show. |
|
|
106
126
|
| `views` | object | Yes | Map of view names to view sources (see View sources below). |
|
|
107
127
|
| `preload` | string[] | No | View names to resolve and cache when the app initializes. |
|
|
108
|
-
| `transitions` | object | No | Duration, ease, cover,
|
|
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`). |
|
|
109
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 }`. |
|
|
110
135
|
| `componentContext` | object | No | Context passed to component-style views. |
|
|
111
136
|
|
|
112
137
|
**Transitions (optional):**
|
|
113
138
|
|
|
114
139
|
```js
|
|
115
140
|
transitions: {
|
|
116
|
-
driver: 'css',
|
|
117
|
-
|
|
118
|
-
cover: 'width', // or 'height' — how the previous view scales to cover the trigger
|
|
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
|
|
119
143
|
duration: '1s',
|
|
120
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
|
|
121
149
|
}
|
|
122
150
|
```
|
|
123
151
|
|
|
124
|
-
|
|
152
|
+
`transitions.parallax` is accepted for compatibility but **not applied** (reserved; intensity is fixed to `0` in the engine).
|
|
153
|
+
|
|
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.
|
|
125
155
|
|
|
126
156
|
| Driver | Description |
|
|
127
157
|
|--------|-------------|
|
|
128
158
|
| `'css'` (default) | CSS keyframes and `animationend`; uses `zumly.css` variables. |
|
|
129
|
-
| `'waapi'` | Web Animations API (`element.animate()`). |
|
|
130
|
-
| `'anime'` | [Anime.js](https://animejs.com/) — load the library (e.g. from CDN) before use. |
|
|
131
|
-
| `'gsap'` | [GSAP](https://greensock.com/gsap/) — load the library (e.g. from CDN) before use. |
|
|
132
|
-
| `'motion'` | [Motion](https://motion.dev/) (motion.dev) — load the library (e.g. from CDN) before use. |
|
|
159
|
+
| `'waapi'` | Web Animations API (`element.animate()`). No extra dependency. |
|
|
133
160
|
| `'none'` | No animation; applies final state immediately. Useful for tests or instant UX. |
|
|
134
|
-
| `
|
|
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. |
|
|
135
165
|
|
|
136
166
|
Example with instant transitions (e.g. for tests):
|
|
137
167
|
|
|
@@ -144,15 +174,67 @@ const app = new Zumly({
|
|
|
144
174
|
});
|
|
145
175
|
```
|
|
146
176
|
|
|
177
|
+
**Lateral navigation:**
|
|
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
|
|
184
|
+
```
|
|
185
|
+
|
|
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`):**
|
|
194
|
+
|
|
195
|
+
```js
|
|
196
|
+
navPosition: 'bottom-center' // default
|
|
197
|
+
```
|
|
198
|
+
|
|
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
|
+
|
|
147
212
|
**Zoomable elements:**
|
|
148
213
|
|
|
149
214
|
- Give the view root the class `z-view`.
|
|
150
215
|
- Add class `zoom-me` and `data-to="viewName"` to the element that triggers zoom-in.
|
|
151
|
-
-
|
|
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`. |
|
|
152
229
|
|
|
153
230
|
```html
|
|
154
231
|
<div class="z-view">
|
|
155
|
-
<div class="zoom-me" data-to="detail"
|
|
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">
|
|
156
238
|
Zoom in
|
|
157
239
|
</div>
|
|
158
240
|
</div>
|
|
@@ -160,15 +242,147 @@ const app = new Zumly({
|
|
|
160
242
|
|
|
161
243
|
### View sources
|
|
162
244
|
|
|
163
|
-
|
|
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.
|
|
246
|
+
|
|
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 |
|
|
254
|
+
|
|
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.
|
|
256
|
+
|
|
257
|
+
### Framework integration
|
|
258
|
+
|
|
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'
|
|
266
|
+
|
|
267
|
+
const app = new Zumly({
|
|
268
|
+
mount: '.canvas',
|
|
269
|
+
initialView: 'home',
|
|
270
|
+
views: {
|
|
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
|
+
}
|
|
277
|
+
})
|
|
278
|
+
```
|
|
279
|
+
|
|
280
|
+
**Vue**
|
|
281
|
+
|
|
282
|
+
```js
|
|
283
|
+
import { createApp } from 'vue'
|
|
284
|
+
import Dashboard from './Dashboard.vue'
|
|
285
|
+
|
|
286
|
+
views: {
|
|
287
|
+
dashboard: ({ target, props }) => {
|
|
288
|
+
createApp(Dashboard, { id: props.id }).mount(target)
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
```
|
|
292
|
+
|
|
293
|
+
**Svelte**
|
|
294
|
+
|
|
295
|
+
```js
|
|
296
|
+
import Dashboard from './Dashboard.svelte'
|
|
297
|
+
|
|
298
|
+
views: {
|
|
299
|
+
dashboard: ({ target, props }) => {
|
|
300
|
+
new Dashboard({ target, props: { id: props.id } })
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
```
|
|
304
|
+
|
|
305
|
+
**Angular**
|
|
306
|
+
|
|
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:**
|
|
164
318
|
|
|
165
|
-
-
|
|
166
|
-
-
|
|
167
|
-
-
|
|
168
|
-
- **
|
|
169
|
-
-
|
|
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.
|
|
170
325
|
|
|
171
|
-
|
|
326
|
+
### Preload and prefetch
|
|
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)
|
|
339
|
+
```
|
|
340
|
+
|
|
341
|
+
A plugin is an object with `install(instance, options)` or a plain function `(instance, options) => void`.
|
|
342
|
+
|
|
343
|
+
#### Router plugin
|
|
344
|
+
|
|
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.
|
|
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()
|
|
358
|
+
```
|
|
359
|
+
|
|
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.
|
|
361
|
+
|
|
362
|
+
**Options:**
|
|
363
|
+
|
|
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. |
|
|
368
|
+
|
|
369
|
+
**Behavior:**
|
|
370
|
+
|
|
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()`) | — |
|
|
378
|
+
|
|
379
|
+
**Example URL:** `#/home/showcases/mercedes`
|
|
380
|
+
|
|
381
|
+
### Limitations and non-goals
|
|
382
|
+
|
|
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.
|
|
172
386
|
|
|
173
387
|
## Development
|
|
174
388
|
|
|
@@ -212,13 +426,21 @@ See [CHANGELOG.md](CHANGELOG.md) for version history.
|
|
|
212
426
|
|
|
213
427
|
## Roadmap
|
|
214
428
|
|
|
215
|
-
|
|
216
|
-
-
|
|
217
|
-
-
|
|
218
|
-
-
|
|
219
|
-
-
|
|
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))
|
|
438
|
+
|
|
439
|
+
**Planned:**
|
|
440
|
+
- Router deep-linking (open a multi-level hash cold)
|
|
441
|
+
- Accessibility (focus moves, broader ARIA)
|
|
220
442
|
|
|
221
|
-
Details and more topics: [docs/roadMap.md](docs/roadMap.md).
|
|
443
|
+
Details and more topics: [docs/roadMap.md](docs/roadMap.md). Driver contract and helpers: [docs/DRIVER_API.md](docs/DRIVER_API.md).
|
|
222
444
|
|
|
223
445
|
## Community
|
|
224
446
|
|
|
@@ -226,7 +448,7 @@ Details and more topics: [docs/roadMap.md](docs/roadMap.md).
|
|
|
226
448
|
|
|
227
449
|
## Origin
|
|
228
450
|
|
|
229
|
-
Zumly is a reimagined, framework-agnostic zoom engine inspired by [Zircle UI](https://github.com/zircleUI/zircleUI).
|
|
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.
|
|
230
452
|
|
|
231
453
|
## License
|
|
232
454
|
|
package/dist/zumly.css
CHANGED
|
@@ -27,26 +27,70 @@
|
|
|
27
27
|
user-select: none;
|
|
28
28
|
}
|
|
29
29
|
.z-view {
|
|
30
|
-
will-change: transform, opacity;
|
|
31
30
|
content-visibility: auto;
|
|
32
31
|
}
|
|
32
|
+
.zoom-current-view,
|
|
33
|
+
.zoom-current-view-reverse,
|
|
34
|
+
.zoom-previous-view,
|
|
35
|
+
.zoom-previous-view-reverse,
|
|
36
|
+
.zoom-last-view,
|
|
37
|
+
.zoom-last-view-reverse,
|
|
38
|
+
.zoom-lateral-back,
|
|
39
|
+
.zoom-lateral-out,
|
|
40
|
+
.zoom-lateral-in {
|
|
41
|
+
will-change:
|
|
42
|
+
transform,
|
|
43
|
+
opacity,
|
|
44
|
+
filter;
|
|
45
|
+
}
|
|
46
|
+
.z-view.has-effect {
|
|
47
|
+
transition: filter var(--zoom-duration, 1s) var(--zoom-ease, ease-in-out);
|
|
48
|
+
filter: var(--z-effect-filter, none);
|
|
49
|
+
}
|
|
50
|
+
.z-view.has-effect-reverse {
|
|
51
|
+
transition: filter var(--zoom-duration, 1s) var(--zoom-ease, ease-in-out);
|
|
52
|
+
filter: none;
|
|
53
|
+
}
|
|
33
54
|
.z-view.hide {
|
|
34
55
|
opacity: 0;
|
|
35
|
-
|
|
56
|
+
}
|
|
57
|
+
.zoom-me.z-trigger-hidden {
|
|
58
|
+
visibility: hidden;
|
|
59
|
+
}
|
|
60
|
+
.zoom-me.z-trigger-fade {
|
|
61
|
+
transition: opacity var(--zoom-duration, 1s) var(--zoom-ease, ease-in-out);
|
|
62
|
+
opacity: 0;
|
|
63
|
+
}
|
|
64
|
+
.zoom-me.z-trigger-fade-reverse {
|
|
65
|
+
transition: opacity var(--zoom-duration, 1s) var(--zoom-ease, ease-in-out);
|
|
66
|
+
opacity: 1;
|
|
67
|
+
}
|
|
68
|
+
.z-view.z-view-fade-in {
|
|
69
|
+
transition: opacity var(--zoom-duration, 1s) var(--zoom-ease, ease-in-out);
|
|
70
|
+
opacity: 1;
|
|
71
|
+
}
|
|
72
|
+
.z-view.z-view-fade-out {
|
|
73
|
+
transition: opacity var(--zoom-duration, 1s) var(--zoom-ease, ease-in-out);
|
|
74
|
+
opacity: 0;
|
|
36
75
|
}
|
|
37
76
|
.zoom-me {
|
|
38
77
|
cursor: zoom-in;
|
|
39
78
|
}
|
|
79
|
+
.zoom-me[role=button] {
|
|
80
|
+
-webkit-tap-highlight-color: transparent;
|
|
81
|
+
}
|
|
40
82
|
.zoom-current-view-reverse {
|
|
41
83
|
animation-name: zoom-current-view;
|
|
42
84
|
animation-duration: var(--zoom-duration);
|
|
43
85
|
animation-timing-function: var(--zoom-ease);
|
|
86
|
+
animation-fill-mode: both;
|
|
44
87
|
animation-direction: reverse;
|
|
45
88
|
}
|
|
46
89
|
.zoom-current-view {
|
|
47
90
|
animation-name: zoom-current-view;
|
|
48
91
|
animation-duration: var(--zoom-duration);
|
|
49
92
|
animation-timing-function: var(--zoom-ease);
|
|
93
|
+
animation-fill-mode: both;
|
|
50
94
|
}
|
|
51
95
|
@keyframes zoom-current-view {
|
|
52
96
|
0% {
|
|
@@ -60,11 +104,13 @@
|
|
|
60
104
|
animation-name: zoom-previous-view;
|
|
61
105
|
animation-duration: var(--zoom-duration);
|
|
62
106
|
animation-timing-function: var(--zoom-ease);
|
|
107
|
+
animation-fill-mode: both;
|
|
63
108
|
}
|
|
64
109
|
.zoom-previous-view-reverse {
|
|
65
110
|
animation-name: zoom-previous-view;
|
|
66
111
|
animation-duration: var(--zoom-duration);
|
|
67
112
|
animation-timing-function: var(--zoom-ease);
|
|
113
|
+
animation-fill-mode: both;
|
|
68
114
|
animation-direction: reverse;
|
|
69
115
|
}
|
|
70
116
|
@keyframes zoom-previous-view {
|
|
@@ -79,12 +125,14 @@
|
|
|
79
125
|
animation-name: zoom-last-view;
|
|
80
126
|
animation-duration: var(--zoom-duration);
|
|
81
127
|
animation-timing-function: var(--zoom-ease);
|
|
128
|
+
animation-fill-mode: both;
|
|
82
129
|
animation-direction: reverse;
|
|
83
130
|
}
|
|
84
131
|
.zoom-last-view {
|
|
85
132
|
animation-name: zoom-last-view;
|
|
86
133
|
animation-duration: var(--zoom-duration);
|
|
87
134
|
animation-timing-function: var(--zoom-ease);
|
|
135
|
+
animation-fill-mode: both;
|
|
88
136
|
}
|
|
89
137
|
@keyframes zoom-last-view {
|
|
90
138
|
0% {
|
|
@@ -94,3 +142,196 @@
|
|
|
94
142
|
transform: var(--last-view-transform-end);
|
|
95
143
|
}
|
|
96
144
|
}
|
|
145
|
+
.zoom-lateral-back {
|
|
146
|
+
animation: zoom-lateral-back var(--zoom-duration) var(--zoom-ease) forwards;
|
|
147
|
+
}
|
|
148
|
+
@keyframes zoom-lateral-back {
|
|
149
|
+
0% {
|
|
150
|
+
transform: var(--lateral-from);
|
|
151
|
+
}
|
|
152
|
+
100% {
|
|
153
|
+
transform: var(--lateral-to);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
.zoom-lateral-out {
|
|
157
|
+
animation: zoom-lateral-out var(--zoom-duration) var(--zoom-ease) forwards;
|
|
158
|
+
}
|
|
159
|
+
@keyframes zoom-lateral-out {
|
|
160
|
+
0% {
|
|
161
|
+
transform: var(--lateral-out-from);
|
|
162
|
+
opacity: 1;
|
|
163
|
+
}
|
|
164
|
+
100% {
|
|
165
|
+
transform: var(--lateral-out-to);
|
|
166
|
+
opacity: 0;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
.zoom-lateral-in {
|
|
170
|
+
animation: zoom-lateral-in var(--zoom-duration) var(--zoom-ease) forwards;
|
|
171
|
+
}
|
|
172
|
+
@keyframes zoom-lateral-in {
|
|
173
|
+
0% {
|
|
174
|
+
transform: var(--lateral-in-from);
|
|
175
|
+
opacity: 0;
|
|
176
|
+
}
|
|
177
|
+
100% {
|
|
178
|
+
transform: var(--lateral-in-to);
|
|
179
|
+
opacity: 1;
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
.z-nav {
|
|
183
|
+
position: absolute;
|
|
184
|
+
z-index: 10;
|
|
185
|
+
display: flex;
|
|
186
|
+
align-items: center;
|
|
187
|
+
gap: 0;
|
|
188
|
+
pointer-events: none;
|
|
189
|
+
background: rgba(0, 0, 0, 0.5);
|
|
190
|
+
backdrop-filter: blur(12px);
|
|
191
|
+
border: 1px solid rgba(255, 255, 255, 0.12);
|
|
192
|
+
border-radius: 100px;
|
|
193
|
+
padding: 6px 10px;
|
|
194
|
+
}
|
|
195
|
+
.z-nav--bottom-center {
|
|
196
|
+
bottom: 20px;
|
|
197
|
+
left: 50%;
|
|
198
|
+
transform: translateX(-50%);
|
|
199
|
+
}
|
|
200
|
+
.z-nav--bottom-left {
|
|
201
|
+
bottom: 20px;
|
|
202
|
+
left: 20px;
|
|
203
|
+
}
|
|
204
|
+
.z-nav--bottom-right {
|
|
205
|
+
bottom: 20px;
|
|
206
|
+
right: 20px;
|
|
207
|
+
}
|
|
208
|
+
.z-nav--top-center {
|
|
209
|
+
top: 20px;
|
|
210
|
+
left: 50%;
|
|
211
|
+
transform: translateX(-50%);
|
|
212
|
+
}
|
|
213
|
+
.z-nav--top-left {
|
|
214
|
+
top: 20px;
|
|
215
|
+
left: 20px;
|
|
216
|
+
}
|
|
217
|
+
.z-nav--top-right {
|
|
218
|
+
top: 20px;
|
|
219
|
+
right: 20px;
|
|
220
|
+
}
|
|
221
|
+
.z-nav--middle-left {
|
|
222
|
+
top: 50%;
|
|
223
|
+
left: 16px;
|
|
224
|
+
transform: translateY(-50%);
|
|
225
|
+
flex-direction: column;
|
|
226
|
+
}
|
|
227
|
+
.z-nav--middle-right {
|
|
228
|
+
top: 50%;
|
|
229
|
+
right: 16px;
|
|
230
|
+
transform: translateY(-50%);
|
|
231
|
+
flex-direction: column;
|
|
232
|
+
}
|
|
233
|
+
.z-nav > * {
|
|
234
|
+
pointer-events: auto;
|
|
235
|
+
}
|
|
236
|
+
.z-nav-depth,
|
|
237
|
+
.z-nav-lateral {
|
|
238
|
+
display: flex;
|
|
239
|
+
align-items: center;
|
|
240
|
+
gap: 8px;
|
|
241
|
+
}
|
|
242
|
+
.z-nav-sep {
|
|
243
|
+
width: 1px;
|
|
244
|
+
height: 18px;
|
|
245
|
+
background: rgba(255, 255, 255, 0.15);
|
|
246
|
+
margin: 0 8px;
|
|
247
|
+
flex-shrink: 0;
|
|
248
|
+
}
|
|
249
|
+
.z-nav--middle-left .z-nav-depth,
|
|
250
|
+
.z-nav--middle-left .z-nav-lateral,
|
|
251
|
+
.z-nav--middle-right .z-nav-depth,
|
|
252
|
+
.z-nav--middle-right .z-nav-lateral {
|
|
253
|
+
flex-direction: column;
|
|
254
|
+
}
|
|
255
|
+
.z-nav--middle-left .z-nav-depth-dots,
|
|
256
|
+
.z-nav--middle-left .z-nav-lateral-dots,
|
|
257
|
+
.z-nav--middle-right .z-nav-depth-dots,
|
|
258
|
+
.z-nav--middle-right .z-nav-lateral-dots {
|
|
259
|
+
flex-direction: column;
|
|
260
|
+
}
|
|
261
|
+
.z-nav--middle-left .z-nav-sep,
|
|
262
|
+
.z-nav--middle-right .z-nav-sep {
|
|
263
|
+
width: 18px;
|
|
264
|
+
height: 1px;
|
|
265
|
+
margin: 8px 0;
|
|
266
|
+
}
|
|
267
|
+
.z-nav--middle-left .z-nav-back,
|
|
268
|
+
.z-nav--middle-right .z-nav-back {
|
|
269
|
+
transform: rotate(180deg);
|
|
270
|
+
}
|
|
271
|
+
.z-nav--middle-left .z-nav-prev,
|
|
272
|
+
.z-nav--middle-right .z-nav-prev {
|
|
273
|
+
transform: rotate(90deg);
|
|
274
|
+
}
|
|
275
|
+
.z-nav--middle-left .z-nav-next,
|
|
276
|
+
.z-nav--middle-right .z-nav-next {
|
|
277
|
+
transform: rotate(90deg);
|
|
278
|
+
}
|
|
279
|
+
.z-nav-back {
|
|
280
|
+
transform: rotate(90deg);
|
|
281
|
+
}
|
|
282
|
+
.z-nav-back,
|
|
283
|
+
.z-nav-arrow {
|
|
284
|
+
width: 28px;
|
|
285
|
+
height: 28px;
|
|
286
|
+
border-radius: 50%;
|
|
287
|
+
border: 1px solid rgba(255, 255, 255, 0.25);
|
|
288
|
+
background: rgba(255, 255, 255, 0.08);
|
|
289
|
+
color: #fff;
|
|
290
|
+
font-size: 1rem;
|
|
291
|
+
line-height: 1;
|
|
292
|
+
cursor: pointer;
|
|
293
|
+
display: flex;
|
|
294
|
+
align-items: center;
|
|
295
|
+
justify-content: center;
|
|
296
|
+
transition: background 0.15s, border-color 0.15s;
|
|
297
|
+
padding: 0;
|
|
298
|
+
}
|
|
299
|
+
.z-nav-back:hover,
|
|
300
|
+
.z-nav-arrow:hover:not(:disabled) {
|
|
301
|
+
background: rgba(255, 255, 255, 0.18);
|
|
302
|
+
border-color: rgba(255, 255, 255, 0.4);
|
|
303
|
+
}
|
|
304
|
+
.z-nav-arrow:disabled {
|
|
305
|
+
opacity: 0.2;
|
|
306
|
+
cursor: default;
|
|
307
|
+
}
|
|
308
|
+
.z-nav-depth-dots,
|
|
309
|
+
.z-nav-lateral-dots {
|
|
310
|
+
display: flex;
|
|
311
|
+
align-items: center;
|
|
312
|
+
gap: 5px;
|
|
313
|
+
}
|
|
314
|
+
.z-nav-dot {
|
|
315
|
+
width: 7px;
|
|
316
|
+
height: 7px;
|
|
317
|
+
border-radius: 50%;
|
|
318
|
+
border: 1px solid rgba(255, 255, 255, 0.3);
|
|
319
|
+
background: rgba(255, 255, 255, 0.15);
|
|
320
|
+
padding: 0;
|
|
321
|
+
cursor: default;
|
|
322
|
+
transition:
|
|
323
|
+
background 0.2s,
|
|
324
|
+
transform 0.2s,
|
|
325
|
+
border-color 0.2s;
|
|
326
|
+
}
|
|
327
|
+
.z-nav-lat-dot {
|
|
328
|
+
cursor: pointer;
|
|
329
|
+
}
|
|
330
|
+
.z-nav-lat-dot:hover {
|
|
331
|
+
background: rgba(255, 255, 255, 0.4);
|
|
332
|
+
}
|
|
333
|
+
.z-nav-dot.is-active {
|
|
334
|
+
background: #fff;
|
|
335
|
+
border-color: #fff;
|
|
336
|
+
transform: scale(1.35);
|
|
337
|
+
}
|