Getting Started
Quick start
First, let's install Zune by choosing the installation method that is most convenient for you:
For security reasons, they require the http:// or https:// protocol.
To use ES modules locally, you need to serve the HTML file via a local HTTP server.
You can check the current script version using the global variable "ver"; if no version is specified, it will default to "0".
Any aspects that might be unclear are explained in more detail within the documentation for each section.
Methods labeled as
Therefore, to retrieve data or wait for the method to complete, you must use either
-
Download the archive, then unzip the files into your project folder.
Download ZIP Archive -
Download via Terminal.
Make sure the current folder is the one where you want to create the project, then run the command below in the terminal:npm install zunepnpm add zuneyarn add zunebun add zune
Next, create a new HTML or PHP file in the project folder, or use an existing one, and connect Zune to the project pages using one of the following methods:
-
Including script directly.
<script src="/zune/app.js" type="module"></script>
-
Dynamic import from the core in an existing script.
await (await import('./zune/core/main.js')).default();
Serving HTML Files for ES Modules
Opening the HTML file directly in the browser will cause an error because ES modules don't work with the file:// protocol.For security reasons, they require the http:// or https:// protocol.
To use ES modules locally, you need to serve the HTML file via a local HTTP server.
Managing File Versions
For easier development, you can disable file caching by adding the "cache" query parameter (e.g., "app.js?cache=false"), and manage file versions with the "v" parameter (e.g., "app.js?v=2").You can check the current script version using the global variable "ver"; if no version is specified, it will default to "0".
Folder and File Structure
components/
The folder is intended to contain various custom components for user interfaces.
core/
The folder contains the core of Zune and does not require any modifications to the scripts within it.
events/
The folder is intended to contain global common events, which will only be triggered if included in the configuration file using the "
$
" prefix.modules/
The folder is intended to contain various functional modules and third-party libraries.
app.js
This file serves as the central point for integrating and managing the core logic of your application; however, separate components, modules, and events are usually preferred.
config.js
Configuration file.
Basic Usage Examples
These are basic examples that are not fully implemented.Any aspects that might be unclear are explained in more detail within the documentation for each section.
Methods labeled as
async
in the documentation are asynchronous and return a promise.Therefore, to retrieve data or wait for the method to complete, you must use either
await
or .then()
.
Increment counter:
The following events must be enabled in the configuration file:
click
.export default () => {
it.myCounter++;
}
<button data-click="counter" data-it="myCounter">0</button>
Total count:
The following events must be enabled in the configuration file:
click
.
3
1
Total: 4 items
export default (e, data) => {
let count = data.counter.num();
data.counter.num(data.method === 'decrement' ? Math.max(count - 1, 0) : count + 1);
const total = $it.itemCount.reduce((acc, item) => acc + item.num(), 0);
it.totalCounter = total;
it.totalName = total === 1 ? 'item' : 'items';
/*
it.totalCounter = total;
Updates the content of the element with 'data-it="totalCounter"'.
*/
/*
$it.itemCount
Selects elements with 'data-it="itemCount"'.
*/
}
<div class="counter-group">
<div class="counter-item">
<button data-click="totalCount: { method: 'decrement', counter: $('>>') }">-</button>
<span data-it="itemCount">3</span>
<button data-click="totalCount: { method: 'increment', counter: $('<<') }">+</button>
</div>
<div class="counter-item">
<button data-click="totalCount: { method: 'decrement', counter: $('>>') }">-</button>
<span data-it="itemCount">1</span>
<button data-click="totalCount: { method: 'increment', counter: $('<<') }">+</button>
</div>
<div class="counter-total">
Total: <span data-it="totalCounter">4</span> <span data-it="totalName">items</span>
</div>
</div>
<!--
$('>>') - passes the next sibling element to the component.
$('<<') - passes the previous sibling element to the component.
-->
.counter-group {
display: grid;
gap: 15px;
}
.counter-item {
display: flex;
align-items: center;
justify-content: center;
gap: 12px;
}
.counter-item button {
font-size: 16px;
width: 50px;
border-radius: 4px;
background-color: #e3e3e3;
color: #333;
border: 1px solid #d3d3d3;
}
.counter-total {
text-align: center;
}
Dropdown:
The following events must be enabled in the configuration file:
click
.
Dropdown content
export default (e) => {
if (e.itType === 'click-out') {
e.it.cls.remove('active');
} else {
e.it.$('..').cls.toggle('active');
}
}
<div class="dropdown" data-click-out="dropdown">
<button class="dropdown-button" data-click="dropdown">
<span>Dropdown</span>
<svg fill="none" viewBox="0 0 24 24" stroke-width="1.5">
<path stroke-linecap="round" stroke-linejoin="round" d="m19.5 8.25-7.5 7.5-7.5-7.5" />
</svg>
</button>
<div class="dropdown-content">
Dropdown content
</div>
</div>
.dropdown {
position: relative;
}
.dropdown-button {
display: flex;
justify-content: center;
align-items: center;
height: 38px;
gap: 6px;
padding: 4px 12px;
border-radius: 6px;
font-size: 16px;
background-color: #e3e3e3;
color: #333;
border: 1px solid #d3d3d3;
}
.dropdown-button svg {
stroke: #333;
width: 12px;
height: 12px;
transform: rotate(0);
transition: transform 0.3s ease;
}
.dropdown.active .dropdown-button svg {
transform: rotate(-180deg);
}
.dropdown-content {
display: flex;
justify-content: center;
align-items: center;
position: absolute;
padding: 10px;
color: #333;
top: 38px;
margin-top: 5px;
min-width: 180px;
min-height: 100px;
border-radius: 6px;
background: #fff;
border: 1px solid #d3d3d3;
box-shadow: 2px 2px 6px 0 rgba(0, 0, 0, 0.1);
transition: all 0.3s ease;
z-index: 2;
opacity: 0;
visibility: hidden;
pointer-events: none;
transform: scaleY(0);
transform-origin: top;
}
.dropdown.active .dropdown-content {
opacity: 1;
transform: scaleY(1);
visibility: visible;
pointer-events: auto;
}
Appending Input Content:
The following events must be enabled in the configuration file:
submit
, input
.Initial text
Text to append:
export default (e) => {
if (e.type === 'submit') {
const value = e.form.text.trim();
if (value) $it.appendBlock.txt(' | ' + value, 'end');
it.appendText = '';
const input = e.it.$('input');
input.val('');
input.focus();
} else {
it.appendText = e.it.val();
}
}
<div class="append-block">
<div data-it="appendBlock">Initial text</div>
<form class="append-form" data-submit="appendText">
<input type="text" name="text" placeholder="Enter text" data-input="appendText">
<button type="submit">Append text</button>
</form>
<div class="append-value">Text to append: <span data-it="appendText"></span></div>
</div>
.append-block {
display: grid;
gap: 15px;
text-align: center;
}
.append-form {
display: flex;
align-items: center;
justify-content: center;
}
.append-form input {
width: 180px;
padding: 4px 12px;
border: 1px solid #d3d3d3;
border-radius: 6px 0 0 6px;
}
.append-form button {
display: flex;
justify-content: center;
align-items: center;
width: 120px;
padding: 4px 12px;
border-radius: 0 6px 6px 0;
font-size: 16px;
background-color: #e3e3e3;
color: #333;
border: 1px solid #d3d3d3;
border-left: none;
}
.append-value {
position: relative;
top: -10px;
font-size: 14px;
color: #777;
}
Search in the list:
The following events must be enabled in the configuration file:
input
.Results found: 4
- Apples
- Oranges
- Bananas
- Mangoes
export default (e, data) => {
const value = toLower(e.it.val());
data.items.map(item => {
item.attr.toggle('inert', !toLower(item.txt()).includes(value));
});
data.countBlock.num(len(data.items.filter(item => !item.attr.has('inert'))));
}
<div class="ex-search">
<input
type="text"
name="search"
placeholder="Search"
data-input="
search: {
items: $('>>{ul} li ?'),
countBlock: $('>>{div} span')
}
"
>
<div>Results found: <span>4</span></div>
<ul>
<li>Apples</li>
<li>Oranges</li>
<li>Bananas</li>
<li>Mangoes</li>
</ul>
</div>
.search {
color: #333;
display: grid;
gap: 5px;
}
.search > span {
font-size: 14px;
color: #777;
}
.search input {
width: 180px;
padding: 4px 12px;
border: 1px solid #d3d3d3;
border-radius: 6px;
}
.search ul li[inert] {
display: none;
}
Lazy loading:
The following events must be enabled in the configuration file:
visible
.









export default (e) => {
e.it.attr.set('src', e.it.attr.get('data-src'));
}
<div class="lazy">
<div class="lazy-stripe"></div>
<div class="lazy-images">
<img src="placeholder.webp" data-src="image.webp" alt="Lazy Image" data-visible="lazy" data-call-visible="1" data-config-visible="threshold: 0">
<img src="placeholder.webp" data-src="image.webp" alt="Lazy Image" data-visible="lazy" data-call-visible="1" data-config-visible="threshold: 1">
<img src="placeholder.webp" data-src="image.webp" alt="Lazy Image" data-visible="lazy" data-call-visible="1" data-config-visible="threshold: 1">
<img src="placeholder.webp" data-src="image.webp" alt="Lazy Image" data-visible="lazy" data-call-visible="1" data-config-visible="threshold: 1">
<img src="placeholder.webp" data-src="image.webp" alt="Lazy Image" data-visible="lazy" data-call-visible="1" data-config-visible="threshold: 1">
<img src="placeholder.webp" data-src="image.webp" alt="Lazy Image" data-visible="lazy" data-call-visible="1" data-config-visible="threshold: 1">
<img src="placeholder.webp" data-src="image.webp" alt="Lazy Image" data-visible="lazy" data-call-visible="1" data-config-visible="threshold: 1">
<img src="placeholder.webp" data-src="image.webp" alt="Lazy Image" data-visible="lazy" data-call-visible="1" data-config-visible="threshold: 1">
<img src="placeholder.webp" data-src="image.webp" alt="Lazy Image" data-visible="lazy" data-call-visible="1" data-config-visible="threshold: 1">
<img src="placeholder.webp" data-src="image.webp" alt="Lazy Image" data-visible="lazy" data-call-visible="1" data-config-visible="threshold: 1">
</div>
</div>
.lazy {
position: relative;
}
.lazy-stripe {
position: absolute;
top: calc(50% + 1px);
left: 15px;
width: calc(100% - 40px);
height: 2px;
background-color: #f0f0f0;
transform: translateY(-50%);
z-index: 2;
}
.lazy-images {
padding: 12px;
width: 400px;
height: 240px;
overflow-y: auto;
display: grid;
justify-content: center;
gap: 20px;
background: linear-gradient(to bottom, transparent 50%, #f0f0f0 50%);
}
.lazy-images img {
display: block;
width: 174px;
height: 116px;
border-radius: 6px;
}
To-Do list:
The following events must be enabled in the configuration file:
click
, submit
.No tasks yet.
export default async (e, data = null) => {
if (e.type === 'submit') {
const value = e.form.task.trim();
if (value) tpl.add(data, {title: value}, 'begin');
const input = e.it.$('input');
input.val('');
input.focus();
} else {
const posTpl = await tpl.pos(e.it);
tpl.remove(posTpl.name, posTpl.idx);
}
}
<div class="todo">
<form data-submit="todo: 'todo'">
<input type="text" name="task" placeholder="Enter a new task">
<button type="submit">Add Task</button>
</form>
<ul data-tpl="todo" data-set="[ { title: 'Buy groceries' }, { title: 'Let the cat out' } ]"></ul>
<div data-tpl-alt="todo" inert>No tasks yet.</div>
<template data-name="todo">
<li>
<label>
<input type="checkbox" name="todo">
<span>${title}</span>
</label>
<button data-click="todo"></button>
</li>
</template>
</div>
.todo {
display: grid;
gap: 10px;
}
.todo form {
display: flex;
align-items: center;
justify-content: center;
}
.todo form input {
width: 250px;
padding: 4px 12px;
border: 1px solid #d3d3d3;
border-radius: 6px 0 0 6px;
}
.todo form button {
display: flex;
justify-content: center;
align-items: center;
width: 100px;
padding: 4px 12px;
border-radius: 0 6px 6px 0;
font-size: 16px;
background-color: #e3e3e3;
color: #333;
border: 1px solid #d3d3d3;
border-left: none;
}
[data-tpl="todo"] {
width: 350px;
display: grid;
gap: 12px;
border-radius: 6px;
padding: 12px;
border: 1px solid #d3d3d3;
animation: todoEmpty .3s ease;
}
[data-tpl="todo"][inert] {
display: none;
}
@keyframes todoEmpty {
from {
opacity: 0;
transform: translateY(-10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
[data-tpl="todo"] li {
display: flex;
align-items: start;
justify-content: space-between;
gap: 10px;
margin: 0;
padding-left: 30px;
transform-origin: top;
transition: transform 0.3s ease, opacity 0.3s ease;
}
[data-tpl="todo"] li:not(:last-child) {
border-bottom: 1px solid #d3d3d3;
padding-bottom: 12px;
}
[data-tpl="todo"] li label {
position: relative;
user-select: none;
width: 100%;
display: block;
cursor: pointer;
}
[data-tpl="todo"] li label > input {
position: absolute;
top: 0;
left: 0;
opacity: 0;
cursor: pointer;
display: block;
height: 100%;
width: 100%;
margin: 0;
}
[data-tpl="todo"] li label > span::before {
content: "";
position: absolute;
left: -30px;
top: 50%;
transform: translateY(-50%) scale(1);
height: 20px;
width: 20px;
border-radius: 6px;
border: 1px solid #d3d3d3;
transition: 0.3s ease;
}
[data-tpl="todo"] li label > input:checked + span::before {
border-color: transparent;
transform: translateY(-50%) scale(0);
}
[data-tpl="todo"] li label > span::after {
content: "";
position: absolute;
left: -23px;
top: calc(50% - 2px);
transform: translateY(-50%) rotate(45deg);
height: 16px;
width: 8px;
border-radius: 1px;
border-bottom: 3px solid #333;
border-right: 3px solid #333;
opacity: 0;
transition: 0.3s ease;
}
[data-tpl="todo"] li label > input:checked + span::after {
opacity: 1;
}
[data-tpl="todo"] li label > input:checked + span {
color: #d3d3d3;
text-decoration: line-through;
}
[data-tpl="todo"] li button {
position: relative;
width: 28px;
height: 28px;
border-radius: 6px;
background-color: #f3f3f3;
}
[data-tpl="todo"] li button::before, [data-tpl="todo"] li button::after {
position: absolute;
left: 12px;
top: 6px;
content: " ";
height: 16px;
width: 1px;
background-color: #333;
}
[data-tpl="todo"] li button::before {
transform: rotate(45deg);
}
[data-tpl="todo"] li button::after {
transform: rotate(-45deg);
}
[data-tpl-alt="todo"] {
text-align: center;
font-size: 14px;
color: #777;
animation: todoEmpty .3s ease;
}
[data-tpl-alt="todo"][inert] {
display: none;
}
[data-tpl="exTodo"] li[inert] {
transform: scaleY(0);
opacity: 0;
}