@zyrab/domo 1.3.0 → 1.5.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.
@@ -1,21 +1,21 @@
1
- MIT License
2
-
3
- Copyright (c) 2025 Zyrab
4
-
5
- Permission is hereby granted, free of charge, to any person obtaining a copy
6
- of this software and associated documentation files (the "Software"), to deal
7
- in the Software without restriction, including without limitation the rights
8
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
- copies of the Software, and to permit persons to whom the Software is
10
- furnished to do so, subject to the following conditions:
11
-
12
- The above copyright notice and this permission notice shall be included in
13
- all copies or substantial portions of the Software.
14
-
15
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
- THE SOFTWARE.
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Zyrab
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,88 @@
1
+ # @zyrab/domo
2
+
3
+ **Small, Fast, and Chainable.**
4
+
5
+ `domo` is a minimalist, industrial-strength DOM builder and UI micro-framework designed for developers who value performance, zero-boilerplate, and elegant APIs. It provides a fluent, chainable interface for building both dynamic browser applications and high-performance static sites.
6
+
7
+ ---
8
+
9
+ ## Why Domo?
10
+
11
+ In a world of heavy frameworks, `domo` returns to the fundamentals while providing modern features for the SSG era.
12
+
13
+ - **Fluent API**: Build complex structures with a natural, chainable syntax.
14
+ - **Dual-Entry Architecture**: Optimized builds for both Client and Server. The client bundle contains 0% string-building logic, while the server bundle acts as a high-speed metadata collector.
15
+ - **Zero-Branching**: No more `if (isServer)` in your components. Environment-aware modules handle the complexity for you.
16
+ - **SSG Native**: Deeply integrated with `@zyrab/domo-ssg`. Automatically collects events, refs, and state during the render pass to enable seamless hydration-free interactivity.
17
+ - **Microscopic Footprint**: Designed to be small enough to stay out of your way, yet powerful enough to build entire applications.
18
+
19
+ ---
20
+
21
+ ## Installation
22
+
23
+ ```bash
24
+ pnpm add @zyrab/domo
25
+ ```
26
+
27
+ ---
28
+
29
+ ## Usage
30
+
31
+ ### Basic Component
32
+ ```javascript
33
+ import Domo from '@zyrab/domo';
34
+
35
+ const card = Domo('div').cls('card')
36
+ .child([
37
+ Domo('h1').txt('Product Name').css({ color: 'blue' }),
38
+ Domo('p').txt('This is a description.'),
39
+ Domo('button').txt('Add to Cart').on('click', () => alert('Added!'))
40
+ ])
41
+ .build();
42
+
43
+ document.body.appendChild(card);
44
+ ```
45
+
46
+ ### Advanced State & Interactivity
47
+ Domo handles state synchronization between Server and Client via the `.state()` method.
48
+
49
+ ```javascript
50
+ import Domo from '@zyrab/domo';
51
+
52
+ export default function Counter(initial = 0) {
53
+ return Domo('div').cls('counter')
54
+ .state({ count: initial })
55
+ .child([
56
+ Domo('span').txt(`Count: ${initial}`).id('display'),
57
+ Domo('button').txt('+').on('click', (e) => {
58
+ // Access state via Domo runtime...
59
+ })
60
+ ]);
61
+ }
62
+ ```
63
+
64
+ ### Server-Side Rendering (SSG/SSR)
65
+ On the server, `domo` generates clean HTML while capturing metadata for the hydration bridge.
66
+
67
+ ```javascript
68
+ import Domo from '@zyrab/domo';
69
+
70
+ // Identical code works on the server!
71
+ const html = Domo('div').txt('Hello from Server').build();
72
+ console.log(html); // "<div>Hello from Server</div>"
73
+ ```
74
+
75
+ ---
76
+
77
+ ## Architecture
78
+
79
+ Domo uses a **Layered Inheritance System** instead of dynamic mixins, ensuring maximum compatibility with modern bundlers and IDEs.
80
+
81
+ - **Client Pass**: Uses native `document.createElement`, `classList`, and `addEventListener`.
82
+ - **Server Pass**: Uses a lightweight "Virtual Element" (`vel`) schema and collects metadata (events, refs, state) for the SSG to generate optimal client-side bridge logic.
83
+
84
+ ---
85
+
86
+ ## License
87
+
88
+ MIT © [Zyrab](https://github.com/zyrab) - see the [LICENSE.md](LICENSE.md) file for details.
package/package.json CHANGED
@@ -1,17 +1,19 @@
1
1
  {
2
2
  "name": "@zyrab/domo",
3
- "version": "1.3.0",
3
+ "version": "1.5.0",
4
4
  "description": "Minimalist DOM builder and chaining-friendly micro-framework with router support.",
5
- "main": "./src/domo.js",
5
+ "main": "./src/index.js",
6
6
  "type": "module",
7
7
  "exports": {
8
- "import": "./src/domo.js",
9
- "default": "./src/domo.js"
8
+ "import": "./src/index.js",
9
+ "default": "./src/index.js"
10
10
  },
11
11
  "author": "Zyrab",
12
12
  "license": "MIT",
13
13
  "files": [
14
- "src/"
14
+ "src/",
15
+ "README.md",
16
+ "LICENSE.md"
15
17
  ],
16
18
  "keywords": [
17
19
  "dom",
@@ -0,0 +1,38 @@
1
+ /**
2
+ * @class BaseClient
3
+ * @description Foundational class for Client-side Domo elements.
4
+ */
5
+ class BaseClient {
6
+ /**
7
+ * @property {HTMLElement} element - The actual DOM element.
8
+ */
9
+ element;
10
+
11
+ /**
12
+ * @property {boolean} _isDomo - Flag to identify Domo instances.
13
+ */
14
+ _isDomo = true;
15
+
16
+ /**
17
+ * Creates an instance of BaseClient.
18
+ * @param {string} [el="div"] - The HTML tag name.
19
+ */
20
+ constructor(el = "div") {
21
+ this.element = document.createElement(String(el || "div").toLowerCase());
22
+ }
23
+ island(component, enabled = true) {
24
+ this.element.appendChild(this._handleElementInstance(component()));
25
+ return this;
26
+ }
27
+ /**
28
+ * In browser context, passes the actual DOM element to the callback.
29
+ * @param {function(HTMLElement): void} callback
30
+ * @returns {this}
31
+ */
32
+ ref(callback) {
33
+ if (typeof callback === "function") callback(this.element);
34
+ return this;
35
+ }
36
+ }
37
+
38
+ export default BaseClient;
@@ -0,0 +1,17 @@
1
+ import ChildrenClient from "./children.client.js";
2
+
3
+ /**
4
+ * @class BuilderClient
5
+ * @extends ChildrenClient
6
+ */
7
+ class BuilderClient extends ChildrenClient {
8
+ /**
9
+ * Finalizes the element construction and returns the native DOM element.
10
+ * @returns {HTMLElement}
11
+ */
12
+ build() {
13
+ return this.element;
14
+ }
15
+ }
16
+
17
+ export default BuilderClient;
@@ -0,0 +1,72 @@
1
+ import EventsClient from "./events.client.js";
2
+
3
+ /**
4
+ * @class ChildrenClient
5
+ * @extends EventsClient
6
+ */
7
+ class ChildrenClient extends EventsClient {
8
+ _handleElementInstance(element) {
9
+ if (element && element._isDomo) return element.build();
10
+ if (element instanceof DocumentFragment || element instanceof Node) return element;
11
+ if (typeof element === "string" || typeof element === "number") {
12
+ return document.createTextNode(String(element));
13
+ }
14
+ return document.createTextNode(`⚠ Invalid child: ${String(element)}`);
15
+ }
16
+
17
+ child(children = []) {
18
+ const flattenedChildren = Array.isArray(children) ? children.flat() : [children];
19
+ flattenedChildren.forEach((child) => {
20
+ this.element.appendChild(this._handleElementInstance(child));
21
+ });
22
+ return this;
23
+ }
24
+
25
+ append(children = []) {
26
+ return this.child(children);
27
+ }
28
+
29
+ appendTo(target) {
30
+ const targetNode = target && target._isDomo ? target.element : target;
31
+ if (targetNode instanceof Node) {
32
+ targetNode.appendChild(this.element);
33
+ }
34
+ return this;
35
+ }
36
+
37
+ parent(target) {
38
+ return this.appendTo(target);
39
+ }
40
+
41
+ clear() {
42
+ this.element.replaceChildren();
43
+ return this;
44
+ }
45
+
46
+ replace(child, newChild) {
47
+ const resolvedNew = this._handleElementInstance(newChild);
48
+ if (child === this.element) {
49
+ this.element.replaceWith(resolvedNew);
50
+ this.element = resolvedNew;
51
+ } else if (this.element.contains(child)) {
52
+ child.replaceWith(resolvedNew);
53
+ }
54
+ return this;
55
+ }
56
+
57
+ show(visible = true, displayValue = "block") {
58
+ this.element.style.display = visible ? displayValue : "none";
59
+ return this;
60
+ }
61
+
62
+ if(condition) {
63
+ if (!condition) {
64
+ return new this.constructor("if")
65
+ .attr({ hidden: true })
66
+ .data({ if: this.element.tagName.toLowerCase() });
67
+ }
68
+ return this;
69
+ }
70
+ }
71
+
72
+ export default ChildrenClient;
@@ -0,0 +1,36 @@
1
+ import PropertiesClient from "./properties.client.js";
2
+
3
+ /**
4
+ * @class ClassesClient
5
+ * @extends PropertiesClient
6
+ */
7
+ class ClassesClient extends PropertiesClient {
8
+ /**
9
+ * Normalizes various inputs into a clean array of class names.
10
+ * @private
11
+ */
12
+ _parseClassList(input) {
13
+ return Array.isArray(input) ? input.filter(Boolean) : String(input).split(" ").filter(Boolean);
14
+ }
15
+
16
+ cls(classes) {
17
+ if (!classes) return this;
18
+ const clsList = this._parseClassList(classes);
19
+ this.element.classList.add(...clsList);
20
+ return this;
21
+ }
22
+
23
+ rmvCls(classes) {
24
+ if (classes) {
25
+ this.element.classList.remove(...this._parseClassList(classes));
26
+ }
27
+ return this;
28
+ }
29
+
30
+ tgglCls(className, force) {
31
+ this.element.classList.toggle(className, force);
32
+ return this;
33
+ }
34
+ }
35
+
36
+ export default ClassesClient;
@@ -0,0 +1,24 @@
1
+ import BuilderClient from "./builder.client.js";
2
+
3
+ /**
4
+ * @class DomoClient
5
+ * @extends BuilderClient
6
+ * @description Main Domo class for the Browser environment.
7
+ */
8
+ class DomoClient extends BuilderClient {
9
+ constructor(el = "div") {
10
+ super(el);
11
+ }
12
+ }
13
+
14
+ /**
15
+ * Factory function for Browser Domo elements.
16
+ * @param {string} [el="div"]
17
+ * @returns {DomoClient}
18
+ */
19
+ function Domo(el = "div") {
20
+ return new DomoClient(el);
21
+ }
22
+
23
+ export { DomoClient };
24
+ export default Domo;
@@ -0,0 +1,51 @@
1
+ import ClassesClient from "./classes.client.js";
2
+
3
+ /**
4
+ * @class EventsClient
5
+ * @extends ClassesClient
6
+ */
7
+ class EventsClient extends ClassesClient {
8
+ /**
9
+ * Adds event listeners to the element.
10
+ */
11
+ on(eventMapOrName, callback, options = {}) {
12
+ if (!eventMapOrName) return this;
13
+
14
+ if (typeof eventMapOrName === "object" && eventMapOrName !== null) {
15
+ for (const [event, value] of Object.entries(eventMapOrName)) {
16
+ if (typeof value === "function") {
17
+ this.element.addEventListener(event, value);
18
+ } else if (Array.isArray(value)) {
19
+ const [cb, opts] = value;
20
+ this.element.addEventListener(event, cb, opts);
21
+ }
22
+ }
23
+ } else if (typeof callback === "function") {
24
+ this.element.addEventListener(eventMapOrName, callback, options);
25
+ }
26
+ return this;
27
+ }
28
+
29
+ _handleClosest(e, map) {
30
+ for (const [selector, handler] of Object.entries(map)) {
31
+ const match = e.target.closest(selector);
32
+ if (match) handler(e, match);
33
+ }
34
+ }
35
+
36
+ onClosest(event, selectors = {}, options = {}) {
37
+ return this.on(event, (e) => this._handleClosest(e, selectors), options);
38
+ }
39
+
40
+ _handleMatches(e, map) {
41
+ for (const [selector, handler] of Object.entries(map)) {
42
+ if (e.target.matches(selector)) handler(e, e.target);
43
+ }
44
+ }
45
+
46
+ onMatch(event, selectors = {}, options = {}) {
47
+ return this.on(event, (e) => this._handleMatches(e, selectors), options);
48
+ }
49
+ }
50
+
51
+ export default EventsClient;
@@ -0,0 +1,78 @@
1
+ import BaseClient from "./base.client.js";
2
+
3
+ const _stateMap = new WeakMap();
4
+
5
+ /**
6
+ * @class PropertiesClient
7
+ * @extends BaseClient
8
+ */
9
+ class PropertiesClient extends BaseClient {
10
+ /**
11
+ * Sets a property on the element.
12
+ * @protected
13
+ * @param {string} key
14
+ * @param {*} val
15
+ * @returns {this}
16
+ */
17
+ _set(key, val) {
18
+ if (val === undefined) return this;
19
+ this.element[key] = val;
20
+ return this;
21
+ }
22
+
23
+ id(id) {
24
+ return this._set("id", id);
25
+ }
26
+
27
+ val(value) {
28
+ return this._set("value", value);
29
+ }
30
+
31
+ txt(text) {
32
+ return this._set("textContent", text);
33
+ }
34
+
35
+ attr(attributes = {}) {
36
+ for (const [key, value] of Object.entries(attributes)) {
37
+ if (typeof value === "boolean") {
38
+ value ? this.element.setAttribute(key, "") : this.element.removeAttribute(key);
39
+ } else if (value != null) {
40
+ this.element.setAttribute(key, value);
41
+ }
42
+ }
43
+ return this;
44
+ }
45
+
46
+ tgglAttr(attrName, force) {
47
+ if (typeof force === "boolean") {
48
+ force ? this.element.setAttribute(attrName, "") : this.element.removeAttribute(attrName);
49
+ } else {
50
+ this.element.hasAttribute(attrName) ? this.element.removeAttribute(attrName) : this.element.setAttribute(attrName, "");
51
+ }
52
+ return this;
53
+ }
54
+
55
+ data(data = {}) {
56
+ Object.entries(data).forEach(([key, val]) => {
57
+ this.element.dataset[key] = val;
58
+ });
59
+ return this;
60
+ }
61
+
62
+ css(styles = {}) {
63
+ Object.assign(this.element.style, styles);
64
+ return this;
65
+ }
66
+
67
+ /**
68
+ * Stores state object in a WeakMap for the client runtime.
69
+ * @param {object} obj
70
+ * @returns {this}
71
+ */
72
+ state(obj) {
73
+ _stateMap.set(this.element, obj);
74
+ return this;
75
+ }
76
+ }
77
+
78
+ export default PropertiesClient;
package/src/index.js ADDED
@@ -0,0 +1,14 @@
1
+ import DomoClientFactory, { DomoClient } from "./client/domo.client.js";
2
+ import DomoServerFactory, { DomoServer } from "./server/domo.server.js";
3
+
4
+ const isServer = typeof document === "undefined";
5
+
6
+ /**
7
+ * The main Domo factory function.
8
+ * Automatically resolves to DomoClient in the browser and DomoServer in Node.js/SSG environments.
9
+ * @type {typeof DomoClientFactory}
10
+ */
11
+ const Domo = isServer ? DomoServerFactory : DomoClientFactory;
12
+
13
+ export default Domo;
14
+ export { DomoClient, DomoServer };
@@ -0,0 +1,90 @@
1
+ // import { fileURLToPath } from "url";
2
+ /**
3
+ * @class BaseServer
4
+ * @description Foundational class for Server-side (Virtual) Domo elements.
5
+ * Acts as a Metadata Collector for the SSG.
6
+ */
7
+ class BaseServer {
8
+ /**
9
+ * @property {object} element - The virtual representation of the element.
10
+ */
11
+ element;
12
+
13
+ /**
14
+ * @property {boolean} _isDomo - Flag to identify Domo instances.
15
+ */
16
+ _isDomo = true;
17
+
18
+ /**
19
+ * Creates an instance of BaseServer.
20
+ * @param {string} [el="div"] - The HTML tag name.
21
+ */
22
+ constructor(el = "div") {
23
+ this.element = {
24
+ _tag: el,
25
+ _attr: {},
26
+ _cls: [],
27
+ _data: {},
28
+ _css: {},
29
+ _child: [],
30
+ _events: [],
31
+ _refs: [],
32
+ _island: false,
33
+ __island: null,
34
+ _state: {},
35
+ };
36
+ }
37
+
38
+ /**
39
+ * Manually mark this element/component as an island.
40
+ * This signals the SSG to bundle the Domo client runtime.
41
+ */
42
+ island(component, enabled = true) {
43
+ this.element._island = enabled;
44
+ this._getOrSetId();
45
+
46
+ if (enabled) {
47
+ this.element.__island = component;
48
+ }
49
+
50
+ return this;
51
+ }
52
+
53
+ /**
54
+ * On the server, captures the reference handler's name for ESM extraction.
55
+ * @param {Function} callback
56
+ * @returns {this}
57
+ */
58
+ ref(callback) {
59
+ if (typeof callback === "function") {
60
+ this.element._refs.push({
61
+ // 2. Use callback.name, or fallback to "anonymous"
62
+ // (Removed 'meta' reference which caused the crash)
63
+ name: callback.name || "anonymous",
64
+ handler: callback,
65
+ });
66
+
67
+ // Ensure element has a stable ID if it has a ref
68
+ this._getOrSetId();
69
+ }
70
+ return this;
71
+ }
72
+
73
+ /**
74
+ * Generates or retrieves a stable, hash-based ID for metadata association.
75
+ * @protected
76
+ * @returns {string} The data-domo-id.
77
+ */
78
+ _getOrSetId() {
79
+ const existing = this.element._attr["data-domo-id"];
80
+ if (existing) {
81
+ return existing;
82
+ }
83
+ const hash = Math.random().toString(36).substring(2, 7);
84
+ const id = `d-${hash}`;
85
+ this.element._attr["data-domo-id"] = id;
86
+ return id;
87
+ }
88
+ }
89
+
90
+ export default BaseServer;
@@ -0,0 +1,47 @@
1
+ import ChildrenServer from "./children.server.js";
2
+
3
+ /**
4
+ * @class BuilderServer
5
+ * @extends ChildrenServer
6
+ */
7
+ class BuilderServer extends ChildrenServer {
8
+ /**
9
+ * Serializes the virtual element and its metadata into an HTML string.
10
+ * @returns {string}
11
+ */
12
+ build() {
13
+ const tag = this.element._tag;
14
+ const cls = this.element._cls.join(" ");
15
+
16
+ const toKebab = (s) => s.replace(/([a-z])([A-Z])/g, "$1-$2").toLowerCase();
17
+
18
+ const css = Object.entries(this.element._css)
19
+ .map(([k, v]) => `${toKebab(k)}:${v}`)
20
+ .join(";");
21
+
22
+ const attrs = Object.entries(this.element._attr).map(([k, v]) =>
23
+ v === true ? k : `${k}="${String(v).replace(/"/g, "&quot;")}"`,
24
+ );
25
+
26
+ const data = Object.entries(this.element._data).map(([k, v]) => `data-${k}="${String(v).replace(/"/g, "&quot;")}"`);
27
+
28
+ if (cls) attrs.push(`class="${cls}"`);
29
+ if (css) attrs.push(`style="${css}"`);
30
+
31
+ const attrStr = attrs.concat(data).join(" ");
32
+
33
+ const escapeHTML = (str) => String(str).replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
34
+
35
+ const children = this.element._child
36
+ .map((c) => {
37
+ if (typeof c === "string" || typeof c === "number") return escapeHTML(c);
38
+ if (c && typeof c.build === "function") return c.build();
39
+ return "";
40
+ })
41
+ .join("");
42
+
43
+ return `<${tag}${attrStr ? " " + attrStr : ""}>${children}</${tag}>`;
44
+ }
45
+ }
46
+
47
+ export default BuilderServer;