1
0
mirror of https://github.com/jaandrle/deka-dom-el synced 2025-07-02 04:32:14 +02:00

11 Commits

Author SHA1 Message Date
a1d2c69049 🔤 md enhancements 2025-03-19 14:29:48 +01:00
7472cea880 🐛 logo alignemt (due to gh) 2025-03-19 14:07:39 +01:00
7664932041 🔤 Better build and improve texting 2025-03-19 13:57:16 +01:00
ad255e3e19 🐛 ensures only one disconncetd listener
…for cleanup
2025-03-19 11:39:45 +01:00
74ed180919 Signal ← SignalReadonly 2025-03-19 10:02:33 +01:00
a459c0d786 📺 version 2025-03-18 17:32:41 +01:00
a1831d2cc4 🔤 corrects irland page headers 2025-03-18 17:30:42 +01:00
a8fa048522 📺 requestIdleCallback doesn need to be global 2025-03-18 17:24:01 +01:00
4dba3a292a 🔤 2025-03-18 17:23:23 +01:00
8415f5cf6f adjust package size limits 2025-03-17 15:14:56 +01:00
7e3b54153d 🐛 fixes #41 2025-03-17 15:14:39 +01:00
20 changed files with 81 additions and 697 deletions

View File

@ -1,5 +1,5 @@
**Alpha** **Alpha**
| [Docs&Examples](https://jaandrle.github.io/deka-dom-el "Official documentation and guide site") | [Docs](https://jaandrle.github.io/deka-dom-el "Official documentation and guide site")
| [NPM](https://www.npmjs.com/package/deka-dom-el "Official NPM package page") | [NPM](https://www.npmjs.com/package/deka-dom-el "Official NPM package page")
| [GitHub](https://github.com/jaandrle/deka-dom-el "Official GitHub repository") | [GitHub](https://github.com/jaandrle/deka-dom-el "Official GitHub repository")
([*Gitea*](https://gitea.jaandrle.cz/jaandrle/deka-dom-el "GitHub repository mirror on my own Gitea instance")) ([*Gitea*](https://gitea.jaandrle.cz/jaandrle/deka-dom-el "GitHub repository mirror on my own Gitea instance"))
@ -9,7 +9,7 @@
```javascript ```javascript
// 🌟 Reactive component with clear separation of concerns // 🌟 Reactive component with clear separation of concerns
document.body.append( document.body.append(
el(EmojiCounter, { initial: "🚀" }), el(EmojiCounter, { initial: "🚀" })
); );
function EmojiCounter({ initial }) { function EmojiCounter({ initial }) {
@ -34,7 +34,7 @@ function EmojiCounter({ initial }) {
el(Option, "🎉"), el(Option, "🎉"),
el(Option, "🚀"), el(Option, "🚀"),
el(Option, "💖"), el(Option, "💖"),
), )
); );
} }
function Option({ textContent }){ function Option({ textContent }){
@ -56,10 +56,10 @@ Creating reactive elements, components, and Web Components using the native
## Features at a Glance ## Features at a Glance
-**No build step required** — use directly in browsers or Node.js -**No build step required** — use directly in browsers or Node.js
- **Minimalized footprint** — ~10-15kB minified bundle (original goal 10kB), **zero**/minimal dependencies and - ☑️ **Lightweight** — ~10-15kB minified (original goal 10kB) with **zero**/minimal dependencies
small in-memory size (auto-releasing resources as much as possible) -**Declarative & functional approach** for clean, maintainable code
-**Declarative & functional approach support** for clean, maintainable code
-**Signals and events** for reactive UI -**Signals and events** for reactive UI
-**Auto-releasing resources** for memory management but nice development experience
-**Memoization for performance** — optimize rendering with intelligent caching -**Memoization for performance** — optimize rendering with intelligent caching
- ☑️ **Optional build-in signals** with support for custom reactive implementations (#39) - ☑️ **Optional build-in signals** with support for custom reactive implementations (#39)
- ☑️ **Server-side rendering** support via [jsdom](https://github.com/jsdom/jsdom) - ☑️ **Server-side rendering** support via [jsdom](https://github.com/jsdom/jsdom)

View File

@ -9,4 +9,3 @@ npx editorconfig-checker -format gcc ${additional}
npx jshint index.js src ${additional} npx jshint index.js src ${additional}
[ "$one" = 'vim' ] && exit 0 [ "$one" = 'vim' ] && exit 0
npx size-limit npx size-limit
npx publint

View File

@ -430,10 +430,9 @@ function createElement(tag, attributes, ...addons) {
scoped = 1; scoped = 1;
const host = (...c) => !c.length ? el_host : (scoped === 1 ? addons.unshift(...c) : c.forEach((c2) => c2(el_host)), void 0); const host = (...c) => !c.length ? el_host : (scoped === 1 ? addons.unshift(...c) : c.forEach((c2) => c2(el_host)), void 0);
scope.push({ scope: tag, host }); scope.push({ scope: tag, host });
el = /** @type {Element} */ el = tag(attributes || void 0);
tag(attributes || void 0);
if (el.nodeName === "#comment") break;
const is_fragment = isInstance(el, enviroment.F); const is_fragment = isInstance(el, enviroment.F);
if (el.nodeName === "#comment") break;
const el_mark = createElement.mark({ const el_mark = createElement.mark({
type: "component", type: "component",
name: tag.name, name: tag.name,

File diff suppressed because one or more lines are too long

5
dist/esm.js vendored
View File

@ -414,10 +414,9 @@ function createElement(tag, attributes, ...addons) {
scoped = 1; scoped = 1;
const host = (...c) => !c.length ? el_host : (scoped === 1 ? addons.unshift(...c) : c.forEach((c2) => c2(el_host)), void 0); const host = (...c) => !c.length ? el_host : (scoped === 1 ? addons.unshift(...c) : c.forEach((c2) => c2(el_host)), void 0);
scope.push({ scope: tag, host }); scope.push({ scope: tag, host });
el = /** @type {Element} */ el = tag(attributes || void 0);
tag(attributes || void 0);
if (el.nodeName === "#comment") break;
const is_fragment = isInstance(el, enviroment.F); const is_fragment = isInstance(el, enviroment.F);
if (el.nodeName === "#comment") break;
const el_mark = createElement.mark({ const el_mark = createElement.mark({
type: "component", type: "component",
name: tag.name, name: tag.name,

2
dist/esm.min.js vendored

File diff suppressed because one or more lines are too long

View File

@ -475,10 +475,9 @@ var DDE = (() => {
scoped = 1; scoped = 1;
const host = (...c) => !c.length ? el_host : (scoped === 1 ? addons.unshift(...c) : c.forEach((c2) => c2(el_host)), void 0); const host = (...c) => !c.length ? el_host : (scoped === 1 ? addons.unshift(...c) : c.forEach((c2) => c2(el_host)), void 0);
scope.push({ scope: tag, host }); scope.push({ scope: tag, host });
el = /** @type {Element} */ el = tag(attributes || void 0);
tag(attributes || void 0);
if (el.nodeName === "#comment") break;
const is_fragment = isInstance(el, enviroment.F); const is_fragment = isInstance(el, enviroment.F);
if (el.nodeName === "#comment") break;
const el_mark = createElement.mark({ const el_mark = createElement.mark({
type: "component", type: "component",
name: tag.name, name: tag.name,

File diff suppressed because one or more lines are too long

5
dist/iife.js vendored
View File

@ -456,10 +456,9 @@ var DDE = (() => {
scoped = 1; scoped = 1;
const host = (...c) => !c.length ? el_host : (scoped === 1 ? addons.unshift(...c) : c.forEach((c2) => c2(el_host)), void 0); const host = (...c) => !c.length ? el_host : (scoped === 1 ? addons.unshift(...c) : c.forEach((c2) => c2(el_host)), void 0);
scope.push({ scope: tag, host }); scope.push({ scope: tag, host });
el = /** @type {Element} */ el = tag(attributes || void 0);
tag(attributes || void 0);
if (el.nodeName === "#comment") break;
const is_fragment = isInstance(el, enviroment.F); const is_fragment = isInstance(el, enviroment.F);
if (el.nodeName === "#comment") break;
const el_mark = createElement.mark({ const el_mark = createElement.mark({
type: "component", type: "component",
name: tag.name, name: tag.name,

2
dist/iife.min.js vendored

File diff suppressed because one or more lines are too long

View File

@ -82,10 +82,10 @@ export function ImageGallery(images= imagesSample) {
closeLightbox(); closeLightbox();
break; break;
case 'ArrowLeft': case 'ArrowLeft':
onPrevImage(e); document.querySelector('.lightbox-prev-btn').click();
break; break;
case 'ArrowRight': case 'ArrowRight':
onNextImage(e); document.querySelector('.lightbox-next-btn').click();
break; break;
} }
} }
@ -126,12 +126,12 @@ export function ImageGallery(images= imagesSample) {
el("div", { el("div", {
className: "gallery-item", className: "gallery-item",
dataTag: image.alt.toLowerCase() dataTag: image.alt.toLowerCase()
}, onImageClick(image.id)).append( }).append(
el("img", { el("img", {
src: image.src, src: image.src,
alt: image.alt, alt: image.alt,
loading: "lazy" loading: "lazy"
}), }, onImageClick(image.id)),
el("div", { className: "gallery-item-caption" }).append( el("div", { className: "gallery-item-caption" }).append(
el("h3", image.title), el("h3", image.title),
el("p", image.alt) el("p", image.alt)
@ -146,41 +146,41 @@ export function ImageGallery(images= imagesSample) {
S.el(isLightboxOpen, open => !open S.el(isLightboxOpen, open => !open
? el() ? el()
: el("div", { className: "lightbox-overlay" }, on("click", closeLightbox)).append( : el("div", { className: "lightbox-overlay" }, on("click", closeLightbox)).append(
el("div", { el("div", {
className: "lightbox-content", className: "lightbox-content",
onClick: e => e.stopPropagation() // Prevent closing when clicking inside onClick: e => e.stopPropagation() // Prevent closing when clicking inside
}).append( }).append(
el("button", { el("button", {
className: "lightbox-close-btn", className: "lightbox-close-btn",
ariaLabel: "Close lightbox", "aria-label": "Close lightbox"
}, on("click", closeLightbox)).append("×"), }, on("click", closeLightbox)).append("×"),
el("button", { el("button", {
className: "lightbox-prev-btn", className: "lightbox-prev-btn",
ariaLabel: "Previous image", "aria-label": "Previous image"
}, on("click", onPrevImage)).append(""), }, on("click", onPrevImage)).append(""),
el("button", { el("button", {
className: "lightbox-next-btn", className: "lightbox-next-btn",
ariaLabel: "Next image", "aria-label": "Next image"
}, on("click", onNextImage)).append(""), }, on("click", onNextImage)).append(""),
S.el(selectedImage, img => !img S.el(selectedImage, img => !img
? el() ? el()
: el("div", { className: "lightbox-image-container" }).append( : el("div", { className: "lightbox-image-container" }).append(
el("img", { el("img", {
src: img.src, src: img.src,
alt: img.alt, alt: img.alt,
className: "lightbox-image", className: "lightbox-image"
}), }),
el("div", { className: "lightbox-caption" }).append( el("div", { className: "lightbox-caption" }).append(
el("h2", img.title), el("h2", img.title),
el("p", img.alt), el("p", img.alt)
), )
)
) )
), )
), )
),
), ),
); );
} }

View File

@ -1,509 +0,0 @@
import { el, on, scope } from "deka-dom-el";
import { S } from "deka-dom-el/signals";
export function ProductCatalog() {
const { signal }= scope;
const itemsPerPage = 5;
const products = asyncSignal(S,
fetchProducts,
{ initial: [], keepLast: true, signal });
const searchTerm = S("");
const handleSearch = (e) => searchTerm.set(e.target.value);
const sortOrder = S("default");
const handleSort = (e) => sortOrder.set(e.target.value);
const page = S(1);
const handlePageChange = (newPage) => page.set(newPage);
const resetFilters = () => {
searchTerm.set("");
sortOrder.set("default");
page.set(1);
};
const filteredProducts = S(() => {
if (products.status.get() !== "resolved") return [];
const results = products.result.get().filter(product =>
product.title.toLowerCase().includes(searchTerm.get().toLowerCase()) ||
product.description.toLowerCase().includes(searchTerm.get().toLowerCase())
);
return [...results].sort((a, b) => {
const order = sortOrder.get();
if (order === "price-asc") return a.price - b.price;
if (order === "price-desc") return b.price - a.price;
if (order === "rating") return b.rating - a.rating;
return 0; // default: no sorting
});
});
const totalPages = S(() => Math.ceil(filteredProducts.get().length / itemsPerPage));
const paginatedProducts = S(() => {
const currentPage = page.get();
const filtered = filteredProducts.get();
const start = (currentPage - 1) * itemsPerPage;
return filtered.slice(start, start + itemsPerPage);
});
// Component structure
return el("div", { className: "product-catalog" }).append(
el("header", { className: "catalog-header" }).append(
el("h2", "Product Catalog"),
el("div", { className: "toolbar" }).append(
el("button", {
className: "refresh-btn",
textContent: "Refresh Products",
type: "button",
onclick: () => products.invoke(),
}),
el("button", {
className: "reset-btn",
textContent: "Reset Filters",
type: "button",
onclick: resetFilters,
})
)
),
// Search and filter controls
el("div", { className: "controls" }).append(
el("div", { className: "search-box" }).append(
el("input", {
type: "search",
placeholder: "Search products...",
value: searchTerm,
oninput: handleSearch,
})
),
el("div", { className: "sort-options" }).append(
el("label", "Sort by: "),
el("select", { onchange: handleSort }, on.defer(el => el.value = sortOrder.get())).append(
el("option", { value: "default", textContent: "Default" }),
el("option", { value: "price-asc", textContent: "Price: Low to High" }),
el("option", { value: "price-desc", textContent: "Price: High to Low" }),
el("option", { value: "rating", textContent: "Top Rated" })
)
)
),
// Status indicators
el("div", { className: "status-container" }).append(
S.el(products.status, status =>
status === "pending" ?
el("div", { className: "loader" }).append(
el("div", { className: "spinner" }),
el("p", "Loading products...")
)
: status === "rejected" ?
el("div", { className: "error-message" }).append(
el("p", products.error.get().message),
el("button", {
textContent: "Try Again",
onclick: () => products.invoke()
})
)
: el()
)
),
// Results count
S.el(S(()=> [filteredProducts.get(), searchTerm.get()]), ([filtered, term]) =>
products.status.get() === "resolved"
? el("div", {
className: "results-info",
textContent: term ?
`Found ${filtered.length} products matching "${term}"`
: `Showing all ${filtered.length} products`
})
: el()
),
// Products grid
el("div", { className: "products-grid" }).append(
S.el(paginatedProducts, paginatedItems =>
products.status.get() === "resolved" && paginatedItems.length > 0 ?
paginatedItems.map(product => el(ProductCard, { product }))
: products.status.get() === "resolved" && paginatedItems.length === 0 ?
el("p", { className: "no-results", textContent: "No products found matching your criteria." })
: el()
)
),
// Pagination
S.el(S(()=> [totalPages.get(), page.get()]), ([total, current]) =>
products.status.get() === "resolved" && total > 1 ?
el("div", { className: "pagination" }).append(
el("button", {
textContent: "Previous",
disabled: current === 1,
onclick: () => handlePageChange(current - 1)
}),
...Array.from({ length: total }, (_, i) => i + 1).map(num =>
el("button", {
className: num === current ? "current-page" : "",
textContent: num,
onclick: () => handlePageChange(num)
})
),
el("button", {
textContent: "Next",
disabled: current === total,
onclick: () => handlePageChange(current + 1)
})
)
: el()
)
);
}
// Product card component
function ProductCard({ product }) {
const showDetails = S(false);
return el("div", { className: "product-card" }).append(
el("div", { className: "product-image" }).append(
el("img", { src: product.thumbnail, alt: product.title })
),
el("div", { className: "product-info" }).append(
el("h3", { className: "product-title", textContent: product.title }),
el("div", { className: "product-price-rating" }).append(
el("span", { className: "product-price", textContent: `$${product.price.toFixed(2)}` }),
el("span", { className: "product-rating" }).append(
el("span", { className: "stars", textContent: "★".repeat(Math.round(product.rating)) }),
el("span", { className: "rating-value", textContent: `(${product.rating})` }),
)
),
el("p", { className: "product-category", textContent: `Category: ${product.category}` }),
S.el(showDetails, details =>
details ?
el("div", { className: "product-details" }).append(
el("p", { className: "product-description", textContent: product.description }),
el("div", { className: "product-meta" }).append(
el("p", `Brand: ${product.brand}`),
el("p", `Stock: ${product.stock} units`),
el("p", `Discount: ${product.discountPercentage}%`)
)
)
: el()
),
el("div", { className: "product-actions" }).append(
el("button", {
className: "details-btn",
textContent: S(() => showDetails.get() ? "Hide Details" : "Show Details"),
onclick: () => showDetails.set(!showDetails.get())
}),
el("button", {
className: "add-to-cart-btn",
textContent: "Add to Cart"
})
)
)
);
}
// Data fetching function
async function fetchProducts({ signal }) {
await simulateNetworkDelay();
// Simulate random errors for demonstration
if (Math.random() > 0.9) throw new Error("Failed to load products. Network error.");
const response = await fetch("https://dummyjson.com/products", { signal });
if (!response.ok) throw new Error(`API error: ${response.status}`);
const data = await response.json();
return data.products.slice(0, 20); // Limit to 20 products for the demo
}
// Utility for simulating network latency
function simulateNetworkDelay(min = 300, max = 1200) {
const delay = Math.floor(Math.random() * (max - min + 1)) + min;
return new Promise(resolve => setTimeout(resolve, delay));
}
/**
* Custom hook for async data fetching with signals
* @template T
* @param {typeof S} S - Signal constructor
* @param {(params: { signal: AbortSignal }) => Promise<T>} invoker - Async function to execute
* @param {{ initial?: T, keepLast?: boolean, signal?: AbortSignal }} options - Configuration options
* @returns {Object} Status signals and control methods
*/
export function asyncSignal(S, invoker, { initial, keepLast, signal } = {}) {
/** @type {(s: AbortSignal) => AbortSignal} */
const anySignal = !signal || !AbortSignal.any // TODO: make better
? s=> s
: s=> AbortSignal.any([s, signal]);
// Status tracking signals
const status = S("pending");
const result = S(initial);
const error = S(null);
let controller = null;
// Function to trigger data fetching
async function invoke() {
// Cancel any in-flight request
if (controller) controller.abort();
controller = new AbortController();
status.set("pending");
error.set(null);
if (!keepLast) result.set(initial);
try {
const data = await invoker({
signal: anySignal(controller.signal),
});
if (!controller.signal.aborted) {
status.set("resolved");
result.set(data);
}
} catch (e) {
if (e.name !== "AbortError") {
error.set(e);
status.set("rejected");
}
}
}
// Initial data fetch
invoke();
return { status, result, error, invoke };
}
// Initialize the component
document.body.append(
el(ProductCatalog),
el("style", `
.product-catalog {
font-family: system-ui, -apple-system, sans-serif;
max-width: 1200px;
margin: 0 auto;
padding: 20px;
}
.catalog-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
}
.toolbar button {
margin-left: 10px;
padding: 8px 12px;
border-radius: 4px;
border: none;
background: #4a6cf7;
color: white;
cursor: pointer;
}
.controls {
display: flex;
justify-content: space-between;
margin-bottom: 20px;
gap: 15px;
flex-wrap: wrap;
}
.search-box input {
padding: 8px 12px;
border: 1px solid #ddd;
border-radius: 4px;
width: 300px;
max-width: 100%;
}
.sort-options select {
padding: 8px 12px;
border: 1px solid #ddd;
border-radius: 4px;
}
.loader {
text-align: center;
padding: 40px 0;
}
.spinner {
display: inline-block;
width: 40px;
height: 40px;
border: 4px solid rgba(0, 0, 0, 0.1);
border-left-color: #4a6cf7;
border-radius: 50%;
animation: spin 1s linear infinite;
}
@keyframes spin {
to { transform: rotate(360deg); }
}
.error-message {
background: #ffebee;
color: #c62828;
padding: 15px;
border-radius: 4px;
margin: 20px 0;
text-align: center;
}
.results-info {
margin-bottom: 15px;
color: #666;
}
.products-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
gap: 20px;
margin-bottom: 30px;
}
.product-card {
border: 1px solid #eee;
border-radius: 8px;
overflow: hidden;
transition: transform 0.2s, box-shadow 0.2s;
}
.product-card:hover {
transform: translateY(-5px);
box-shadow: 0 10px 20px rgba(0, 0, 0, 0.1);
}
.product-image img {
width: 100%;
height: 180px;
object-fit: cover;
display: block;
}
.product-info {
padding: 15px;
}
.product-title {
margin: 0 0 10px;
font-size: 1.1rem;
height: 2.4rem;
overflow: hidden;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
}
.product-price-rating {
display: flex;
justify-content: space-between;
margin-bottom: 10px;
}
.product-price {
font-weight: bold;
color: #4a6cf7;
font-size: 1.2rem;
}
.stars {
color: gold;
margin-right: 5px;
}
.product-category {
color: #666;
font-size: 0.9rem;
margin-bottom: 15px;
}
.product-details {
margin: 15px 0;
font-size: 0.9rem;
}
.product-description {
line-height: 1.5;
margin-bottom: 10px;
color: #444;
}
.product-meta {
display: flex;
flex-wrap: wrap;
gap: 10px;
color: #666;
font-size: 0.85rem;
}
.product-actions {
display: flex;
gap: 10px;
margin-top: 15px;
}
.product-actions button {
flex: 1;
padding: 8px 0;
border: none;
border-radius: 4px;
cursor: pointer;
}
.details-btn {
background: #eee;
color: #333;
}
.add-to-cart-btn {
background: #4a6cf7;
color: white;
}
.pagination {
display: flex;
justify-content: center;
gap: 5px;
margin-top: 30px;
}
.pagination button {
padding: 8px 12px;
border: 1px solid #ddd;
background: white;
border-radius: 4px;
cursor: pointer;
}
.pagination button.current-page {
background: #4a6cf7;
color: white;
border-color: #4a6cf7;
}
.pagination button:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.no-results {
grid-column: 1 / -1;
text-align: center;
padding: 40px;
color: #666;
}
@media (max-width: 768px) {
.controls {
flex-direction: column;
}
.search-box input {
width: 100%;
}
.products-grid {
grid-template-columns: repeat(auto-fill, minmax(240px, 1fr));
}
}
`),
);

View File

@ -74,11 +74,13 @@ export function h3({ textContent, id }){
if(!id) id= "h-"+textContent.toLowerCase().replaceAll(/\s/g, "-").replaceAll(/[^a-z-]/g, ""); if(!id) id= "h-"+textContent.toLowerCase().replaceAll(/\s/g, "-").replaceAll(/[^a-z-]/g, "");
return el("h3", { id }).append( return el("h3", { id }).append(
el("a", { el("a", {
className: "heading-anchor", className: "heading-anchor",
href: "#"+id, href: "#"+id,
title: `Link to this section: ${textContent}`, textContent: "#",
title: `Link to this section: ${textContent}`,
"aria-label": `Link to section ${textContent}`
}), }),
"# ", " ",
textContent, textContent,
); );
} }

View File

@ -41,12 +41,7 @@ export function page({ pkg, info }){
el("h4", t`Key Benefits of dd<el>`), el("h4", t`Key Benefits of dd<el>`),
el("ul").append( el("ul").append(
el("li", t`No build step required — use directly in the browser`), el("li", t`No build step required — use directly in the browser`),
el("li", t`Minimalized footprint:`), el("li", t`Lightweight core (~1015kB minified) without unnecessary dependencies (0 at now 😇)`),
el("ul").append(
el("li", t`lightweight core (~1015kB minified)`),
el("li", t`…without unnecessary dependencies (0 at now 😇)`),
el("li", t`auto-releasing resources with focus on performance and development experience`),
),
el("li", t`Natural DOM API — work with real DOM nodes, not abstractions`), el("li", t`Natural DOM API — work with real DOM nodes, not abstractions`),
el("li", t`Built-in (but optional) reactivity with simplified but powerful signals system`), el("li", t`Built-in (but optional) reactivity with simplified but powerful signals system`),
el("li", t`Clean code organization with the 3PS pattern`) el("li", t`Clean code organization with the 3PS pattern`)

View File

@ -151,7 +151,7 @@ export function header({ info: { href, title, description }, pkg }){
), ),
el("span", { el("span", {
className: "version-badge", className: "version-badge",
ariaLabel: "Version", "aria-label": "Version",
textContent: pkg.version || "" textContent: pkg.version || ""
}) })
), ),
@ -165,13 +165,13 @@ export function header({ info: { href, title, description }, pkg }){
function nav({ href, pkg }){ function nav({ href, pkg }){
return el("nav", { return el("nav", {
role: "navigation", role: "navigation",
ariaLabel: "Main navigation", "aria-label": "Main navigation",
className: nav.name className: nav.name
}).append( }).append(
el("a", { el("a", {
href: pkg.homepage, href: pkg.homepage,
className: "github-link", className: "github-link",
ariaLabel: "View on GitHub", "aria-label": "View on GitHub",
target: "_blank", target: "_blank",
rel: "noopener noreferrer", rel: "noopener noreferrer",
}).append( }).append(
@ -185,11 +185,11 @@ function nav({ href, pkg }){
return el("a", { return el("a", {
href: isIndex ? "./" : p.href, href: isIndex ? "./" : p.href,
title: p.description || `Go to ${p.title}`, title: p.description || `Go to ${p.title}`,
ariaCurrent: isCurrent ? "page" : null, "aria-current": isCurrent ? "page" : null,
}).append( }).append(
el("span", { el("span", {
className: "nav-number", className: "nav-number",
ariaHidden: "true", "aria-hidden": "true",
textContent: `${i+1}. ` textContent: `${i+1}. `
}), }),
p.title p.title

View File

@ -168,22 +168,6 @@ export function page({ pkg, info }){
el("li", t`name - The name of the component function`), el("li", t`name - The name of the component function`),
el("li", t`host - Indicates whether the host is "this" (for DocumentFragments) or "parentElement"`), el("li", t`host - Indicates whether the host is "this" (for DocumentFragments) or "parentElement"`),
), ),
el("div", { className: "warning" }).append(
el("p").append(T`
There are edge case when the mark can be missing. For example, the (utility) components with reactive
keys such as ${el("code", ".textContent")}, ${el("code", ".innerText")} or ${el("code", ".innerHTML")}.
As they change the content of the host element.
`),
el(code, { content: `
function Counter() {
const count = S(0);
return el("button",
{ textContent: count, type: "button" },
on("click", () => count.set(count.get() + 1)),
);
}
`, language: "js" }),
),
el("h4", t`Identifying reactive elements in the DOM`), el("h4", t`Identifying reactive elements in the DOM`),
el("p").append(T` el("p").append(T`

View File

@ -33,6 +33,7 @@ export function page({ pkg, info }){
`), `),
el(example, { src: fileURL("./components/examples/case-studies/interactive-form.js"), variant: "big" }), el(example, { src: fileURL("./components/examples/case-studies/interactive-form.js"), variant: "big" }),
el(h3, t`Interactive Image Gallery`), el(h3, t`Interactive Image Gallery`),
el("p").append(T` el("p").append(T`
Responsive image gallery with lightbox, keyboard navigation, and filtering. Dynamic loading of content, Responsive image gallery with lightbox, keyboard navigation, and filtering. Dynamic loading of content,
@ -40,6 +41,7 @@ export function page({ pkg, info }){
`), `),
el(example, { src: fileURL("./components/examples/case-studies/image-gallery.js"), variant: "big" }), el(example, { src: fileURL("./components/examples/case-studies/image-gallery.js"), variant: "big" }),
el(h3, t`Task Manager`), el(h3, t`Task Manager`),
el("p").append(T` el("p").append(T`
Kanban-style task management app with drag-and-drop and localStorage persistence. Complex state management Kanban-style task management app with drag-and-drop and localStorage persistence. Complex state management
@ -48,27 +50,6 @@ export function page({ pkg, info }){
`), `),
el(example, { src: fileURL("./components/examples/case-studies/task-manager.js"), variant: "big" }), el(example, { src: fileURL("./components/examples/case-studies/task-manager.js"), variant: "big" }),
el(h3, t`Product Catalog with asyncSignal`),
el("p").append(T`
Interactive product catalog with search, sorting, and pagination. Features include dynamic product filtering,
responsive UI with detailed view toggles, error handling with retry capability, and proper resource cleanup.
Demonstrates advanced signal usage, including derived signals, abortable async data fetching, and optimized
rendering patterns.
`),
el("div", { className: "callout" }).append(
el("h4", t`asyncSignal Utility`),
el("p").append(T`
This example showcases the asyncSignal utility, which is a powerful abstraction for handling async data
fetching with proper state management. It provides:
`),
el("ul").append(
el("li", t`Automatic tracking of loading, success, and error states`),
el("li", t`AbortController integration for request cancellation`),
el("li", t`Error handling and recovery`),
el("li", t`Options for caching previous data during loading states`)
)
),
el(example, { src: fileURL("./components/examples/case-studies/products.js"), variant: "big" }),
el(h3, t`TodoMVC`), el(h3, t`TodoMVC`),
el("p").append(T` el("p").append(T`

65
package-lock.json generated
View File

@ -15,8 +15,7 @@
"esbuild": "~0.25", "esbuild": "~0.25",
"jsdom": "~26.0", "jsdom": "~26.0",
"jshint": "~2.13", "jshint": "~2.13",
"nodejsscript": "^1.0", "nodejsscript": "^1.0.2",
"publint": "^0.3",
"size-limit-node-esbuild": "~0.3" "size-limit-node-esbuild": "~0.3"
}, },
"engines": { "engines": {
@ -615,19 +614,6 @@
"node": ">= 8" "node": ">= 8"
} }
}, },
"node_modules/@publint/pack": {
"version": "0.1.2",
"resolved": "https://registry.npmjs.org/@publint/pack/-/pack-0.1.2.tgz",
"integrity": "sha512-S+9ANAvUmjutrshV4jZjaiG8XQyuJIZ8a4utWmN/vW1sgQ9IfBnPndwkmQYw53QmouOIytT874u65HEmu6H5jw==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=18"
},
"funding": {
"url": "https://bjornlu.com/sponsor"
}
},
"node_modules/@sindresorhus/merge-streams": { "node_modules/@sindresorhus/merge-streams": {
"version": "2.3.0", "version": "2.3.0",
"resolved": "https://registry.npmjs.org/@sindresorhus/merge-streams/-/merge-streams-2.3.0.tgz", "resolved": "https://registry.npmjs.org/@sindresorhus/merge-streams/-/merge-streams-2.3.0.tgz",
@ -2243,16 +2229,6 @@
"node": ">=4" "node": ">=4"
} }
}, },
"node_modules/package-manager-detector": {
"version": "0.2.11",
"resolved": "https://registry.npmjs.org/package-manager-detector/-/package-manager-detector-0.2.11.tgz",
"integrity": "sha512-BEnLolu+yuz22S56CU1SUKq3XC3PkwD5wv4ikR4MfGvnRVcmzXR9DwSlW2fEamyTPyXHomBJRzgapeuBvRNzJQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"quansync": "^0.2.7"
}
},
"node_modules/parse5": { "node_modules/parse5": {
"version": "7.2.1", "version": "7.2.1",
"resolved": "https://registry.npmjs.org/parse5/-/parse5-7.2.1.tgz", "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.2.1.tgz",
@ -2330,28 +2306,6 @@
"url": "https://github.com/sponsors/jonschlinkert" "url": "https://github.com/sponsors/jonschlinkert"
} }
}, },
"node_modules/publint": {
"version": "0.3.9",
"resolved": "https://registry.npmjs.org/publint/-/publint-0.3.9.tgz",
"integrity": "sha512-irTwfRfYW38vomkxxoiZQtFtUOQKpz5m0p9Z60z4xpXrl1KmvSrX1OMARvnnolB5usOXeNfvLj6d/W3rwXKfBQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@publint/pack": "^0.1.2",
"package-manager-detector": "^0.2.9",
"picocolors": "^1.1.1",
"sade": "^1.8.1"
},
"bin": {
"publint": "src/cli.js"
},
"engines": {
"node": ">=18"
},
"funding": {
"url": "https://bjornlu.com/sponsor"
}
},
"node_modules/punycode": { "node_modules/punycode": {
"version": "2.3.1", "version": "2.3.1",
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
@ -2362,23 +2316,6 @@
"node": ">=6" "node": ">=6"
} }
}, },
"node_modules/quansync": {
"version": "0.2.10",
"resolved": "https://registry.npmjs.org/quansync/-/quansync-0.2.10.tgz",
"integrity": "sha512-t41VRkMYbkHyCYmOvx/6URnN80H7k4X0lLdBMGsz+maAwrJQYB1djpV6vHrQIBE0WBSGqhtEHrK9U3DWWH8v7A==",
"dev": true,
"funding": [
{
"type": "individual",
"url": "https://github.com/sponsors/antfu"
},
{
"type": "individual",
"url": "https://github.com/sponsors/sxzz"
}
],
"license": "MIT"
},
"node_modules/queue-microtask": { "node_modules/queue-microtask": {
"version": "1.2.3", "version": "1.2.3",
"resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz",

View File

@ -1,10 +1,10 @@
{ {
"name": "deka-dom-el", "name": "deka-dom-el",
"version": "0.9.5-alpha", "version": "0.9.4-alpha",
"description": "A low-code library that simplifies the creation of native DOM elements/components using small wrappers and tweaks.", "description": "A low-code library that simplifies the creation of native DOM elements/components using small wrappers and tweaks.",
"author": "Jan Andrle <andrle.jan@centrum.cz>", "author": "Jan Andrle <andrle.jan@centrum.cz>",
"license": "MIT", "license": "MIT",
"homepage": "https://github.com/jaandrle/deka-dom-el", "homepage": "https://jaandrle.github.io/deka-dom-el/",
"repository": { "repository": {
"type": "git", "type": "git",
"url": "git+ssh://git@github.com/jaandrle/deka-dom-el.git" "url": "git+ssh://git@github.com/jaandrle/deka-dom-el.git"
@ -17,20 +17,20 @@
"type": "module", "type": "module",
"exports": { "exports": {
".": { ".": {
"types": "./index.d.ts", "import": "./index.js",
"import": "./index.js" "types": "./index.d.ts"
}, },
"./signals": { "./signals": {
"types": "./signals.d.ts", "import": "./signals.js",
"import": "./signals.js" "types": "./signals.d.ts"
}, },
"./jsdom": { "./jsdom": {
"types": "./jsdom.d.ts", "import": "./jsdom.js",
"import": "./jsdom.js" "types": "./jsdom.d.ts"
}, },
"./src/signals-lib": { "./src/signals-lib": {
"types": "./src/signals-lib/signals-lib.d.ts", "import": "./src/signals-lib/signals-lib.js",
"import": "./src/signals-lib/signals-lib.js" "types": "./src/signals-lib/signals-lib.d.ts"
} }
}, },
"files": [ "files": [
@ -103,8 +103,7 @@
"esbuild": "~0.25", "esbuild": "~0.25",
"jsdom": "~26.0", "jsdom": "~26.0",
"jshint": "~2.13", "jshint": "~2.13",
"nodejsscript": "^1.0", "nodejsscript": "^1.0.2",
"publint": "^0.3",
"size-limit-node-esbuild": "~0.3" "size-limit-node-esbuild": "~0.3"
} }
} }

View File

@ -51,9 +51,9 @@ export function createElement(tag, attributes, ...addons){
const host= (...c)=> !c.length ? el_host : const host= (...c)=> !c.length ? el_host :
(scoped===1 ? addons.unshift(...c) : c.forEach(c=> c(el_host)), undefined); (scoped===1 ? addons.unshift(...c) : c.forEach(c=> c(el_host)), undefined);
scope.push({ scope: tag, host }); scope.push({ scope: tag, host });
el= /** @type {Element} */(tag(attributes || undefined)); el= tag(attributes || undefined);
if(el.nodeName==="#comment") break;
const is_fragment= isInstance(el, env.F); const is_fragment= isInstance(el, env.F);
if(el.nodeName==="#comment") break;
const el_mark= createElement.mark({ const el_mark= createElement.mark({
type: "component", type: "component",
name: tag.name, name: tag.name,