jops-core 1.0.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 ADDED
@@ -0,0 +1,297 @@
1
+ # JOPS — JavaScript OOP SPA Framework
2
+
3
+ A no-bundler, vanilla-JavaScript SPA framework for developers who think in OOP and native-mobile patterns.
4
+
5
+ ## What is JOPS?
6
+
7
+ JOPS is a lightweight SPA framework built on native ES6 modules. It discards the complexity of modern build toolchains while keeping a structured, class-based architecture inspired by Android's view and lifecycle model.
8
+
9
+ No Webpack. No Babel. No compiler. Just `<script type="module">` and a browser.
10
+
11
+ ## Where JOPS Fits
12
+
13
+ | | No Bundler | OOP Classes | Declarative View Nesting | No Element Registration |
14
+ | --------- | ------------- | ----------- | ------------------------ | ------------------------------ |
15
+ | **JOPS** | ✅ | ✅ | ✅ | ✅ |
16
+ | Lit | ✅ | ✅ | ⚠️ Web Components only | ❌ |
17
+ | Stimulus | ✅ | ✅ | ❌ | ✅ |
18
+ | Alpine.js | ✅ | ❌ | ❌ | ✅ |
19
+ | Angular | ❌ | ✅ | ✅ | ✅ |
20
+ | Svelte | ❌ (compiler) | ✅ | ✅ | ✅ |
21
+
22
+ **Target audience:** Teams building internal tools, dashboards, embedded WebViews, or native mobile hybrid apps who want a structured, class-based frontend architecture without the npm/webpack/babel ecosystem overhead.
23
+
24
+ ---
25
+
26
+ ## Installation
27
+
28
+ ### CDN
29
+
30
+ The fastest way to get started. Add these two tags to any HTML page:
31
+
32
+ ```html
33
+ <script type="importmap">
34
+ {
35
+ "imports": {
36
+ "jops-core": "https://cdn.ui-db.com/jops-core/1.0.0/jops-core.min.js"
37
+ }
38
+ }
39
+ </script>
40
+ <script src="https://cdn.ui-db.com/jops-core/1.0.0/jops-core.min.js" data-app="/src/App.js"></script>
41
+ ```
42
+
43
+ The import map lets your app modules use `import { View } from "jops-core"`. The script tag bootstraps the app entry point specified in `data-app`.
44
+
45
+ `App.js` is your entry point — a file you provide that instantiates your root `View` and mounts it to the page. Point `data-app` to wherever you place it.
46
+
47
+ Replace `1.0.0` with the version you want to pin. No build step, no npm, no bundler.
48
+
49
+ ### npm
50
+
51
+ For greenfield projects that want local tooling and scaffolding:
52
+
53
+ ```bash
54
+ npm init -y
55
+ npm install jops-core
56
+ npx jops-core init
57
+ ```
58
+
59
+ `npx jops-core init` vendors the library to `/lib/jops-core.min.js`, configures `index.html` with the import map, and injects build and postinstall scripts into `package.json`.
60
+
61
+ ### Build for production
62
+
63
+ ```bash
64
+ npm run build
65
+ ```
66
+
67
+ Generates a clean `dist/` folder ready to serve.
68
+
69
+ ### VS Code — HTML highlighting in layout templates
70
+
71
+ Install the [lit-html](https://marketplace.visualstudio.com/items?itemName=bierner.lit-html) extension, then add this to your `.vscode/settings.json`:
72
+
73
+ ```json
74
+ {
75
+ "lit-html.tags": ["layout"],
76
+ "editor.tokenColorCustomizations": {
77
+ "textMateRules": [
78
+ {
79
+ "scope": "invalid.illegal.unrecognized-tag.html",
80
+ "settings": { "foreground": "#569CD6" }
81
+ }
82
+ ]
83
+ }
84
+ }
85
+ ```
86
+
87
+ This gives you HTML syntax highlighting, Emmet, and autocomplete inside every `` layout`...` `` tagged template. Custom view tags (e.g. `<MyView>`) are coloured blue like standard HTML elements instead of red. `npx jops-core init` configures this automatically.
88
+
89
+ ---
90
+
91
+ ## Class Reference
92
+
93
+ ### `View` — Base Class
94
+
95
+ The foundational building block. Every screen, component, and sub-component extends `View`.
96
+
97
+ ```js
98
+ import { View } from "jops-core";
99
+
100
+ export default class MyView extends View { ... }
101
+ ```
102
+
103
+ #### Lifecycle (called by the framework in order)
104
+
105
+ | Method | When called |
106
+ | ------------------------- | ------------------------------------------------------------------------------ |
107
+ | `async loadLayout(path?)` | Override to load a layout `.js` module |
108
+ | `inflate()` | Converts `this.layout` string into `this.domNode` |
109
+ | `async onLayout()` | DOM is live — add subviews, read initial data |
110
+ | `async onResume()` | View becomes visible — called on first display and on every return navigation |
111
+ | `bindEvents()` | Wires `on*` attributes on `[type="jops-event-bind"]` elements to class methods |
112
+ | `onBackPressed()` | Called by Router when back navigation is detected on this view |
113
+
114
+ #### Instance Methods
115
+
116
+ | Method | Description |
117
+ | ------------------------------- | -------------------------------------------------------------------------------------------------------------- |
118
+ | `get(id)` | Returns an `Element` wrapper for the child with the given DOM `id` |
119
+ | `setStyle(style)` | Injects a `.css` file link or inline style tag into `<head>`, deduped |
120
+ | `addSubView(containerId, view)` | Appends `view` into the child element with `id="containerId"`, runs its full lifecycle |
121
+ | `insertSubView(view, index)` | Inserts `view` at position `index` within `this.domNode`, runs its full lifecycle |
122
+ | `removeSubView(view)` | Removes `view` from DOM and `this.subViews`, calls `view.destroy()` |
123
+ | `find(condition)` | Depth-first search across full subview subtree; returns flat array of all views matching the predicate |
124
+ | `show()` | Restores `this.domNode` display (removes inline `display` override) |
125
+ | `hide()` | Sets `this.domNode` to `display: none` |
126
+ | `destroy()` | Recursively destroys subtree — calls `destroy()` on all subviews, removes `domNode` from DOM, nulls references |
127
+
128
+ #### Declarative Event Binding
129
+
130
+ Mark any element with `type="jops-event-bind"` and add `on*` attributes whose values are method names on the View class:
131
+
132
+ ```html
133
+ <button type="jops-event-bind" onclick="onSave">Save</button>
134
+ ```
135
+
136
+ ```js
137
+ onSave(event) { ... }
138
+ ```
139
+
140
+ ---
141
+
142
+ ### `Element` — DOM Accessor (returned by `View.get()`)
143
+
144
+ Returned by `view.get(id)`. Wraps a single DOM element for reading and writing.
145
+
146
+ | Method | Description |
147
+ | ------------ | -------------------------------------------------------------------- |
148
+ | `set(value)` | Sets `innerHTML` of the element — accepts plain text or HTML strings |
149
+ | `value()` | Returns the current `innerHTML` of the element |
150
+
151
+ ```js
152
+ this.get("username").set("Alice");
153
+ const current = this.get("username").value();
154
+ ```
155
+
156
+ ---
157
+
158
+ ### `layout` — Tagged Template Function
159
+
160
+ XSS-safe tagged template literal for defining HTML layouts. Dynamic values are HTML-escaped automatically. Use `raw()` to opt out for trusted HTML.
161
+
162
+ ```js
163
+ import { layout, raw } from "jops-core";
164
+
165
+ const html = layout`<p>${userInput}</p>`; // escaped
166
+ const html = layout`<p>${raw("<b>trusted</b>")}</p>`; // raw HTML
167
+ ```
168
+
169
+ ---
170
+
171
+ ### `raw(html)` — Function
172
+
173
+ Wraps a trusted HTML string to bypass escaping inside a `layout` template.
174
+
175
+ ```js
176
+ import { raw } from "jops-core";
177
+
178
+ raw("<b>Hello</b>"); // passed through as-is inside layout``
179
+ ```
180
+
181
+ ---
182
+
183
+ ### `Router` — extends `View`
184
+
185
+ Hash-based SPA router. Singleton. Declared in the layout as `<Router type="jops" animation="slide">`.
186
+
187
+ #### Two Navigation Paradigms
188
+
189
+ | Pattern | Mechanism | Instance | Android analogy |
190
+ | ------------------------ | ------------------ | -------------------- | ------------------------------- |
191
+ | `href="#path"` | flat, hashchange | pre-existing, reused | Tab navigation |
192
+ | `navigate(path, Class?)` | hierarchical stack | always new instance | `startActivity()` |
193
+ | `replace(path, Class?)` | resets stack | existing or new | `startActivity(FLAG_CLEAR_TOP)` |
194
+
195
+ #### Static Methods
196
+
197
+ | Method | Description |
198
+ | -------------------- | ---------------------------------- |
199
+ | `Router.singleton()` | Returns the single Router instance |
200
+
201
+ #### Instance Methods
202
+
203
+ | Method | Description |
204
+ | --------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
205
+ | `navigate(path, ClassDef?)` | Creates a new view instance for `path`, pushes onto the nav stack, animates forward. Updates address bar to `#path?params`. `ClassDef` required for dynamic routes not in the static route map |
206
+ | `back()` | Navigates back — pops the nav stack with animation if non-empty, otherwise calls `history.back()` |
207
+ | `replace(path, ClassDef?)` | Clears the nav stack and navigates to `path`. Uses existing flat route instance if available, otherwise creates a new one from `ClassDef` |
208
+ | `getParams()` | Returns the current URL query params as a plain object. Works for both static (`href="#path?k=v"`) and dynamic (`navigate("/path?k=v")`) routes |
209
+
210
+ #### Animation
211
+
212
+ Set via the `animation` attribute on the `<Router>` element:
213
+
214
+ | Value | Behaviour |
215
+ | ------------------ | -------------------------------------------------------------------------- |
216
+ | `"none"` (default) | Instant switch, no animation |
217
+ | `"slide"` | Forward: incoming slides in from right. Back: outgoing slides out to right |
218
+ | `"fade"` | Crossfade between outgoing and incoming |
219
+
220
+ Animation only occurs on `navigate()` (forward) and `back()` when the nav stack is non-empty. Flat tab switching is always instant.
221
+
222
+ #### Layout Declaration
223
+
224
+ ```html
225
+ <Router type="jops" animation="slide">
226
+ <view type="jops" src="/src/Home.js" path="/home"></view>
227
+ <view type="jops" src="/src/Form.js" path="/form"></view>
228
+ </Router>
229
+ ```
230
+
231
+ #### Query Params
232
+
233
+ ```js
234
+ // static route with params
235
+ <a href="#form?tab=profile">Profile</a>;
236
+
237
+ // dynamic navigation with params
238
+ Router.singleton().navigate("/detail?id=42&mode=edit", DetailView);
239
+
240
+ // read params in any view's onResume()
241
+ const { id, mode } = Router.singleton().getParams();
242
+ ```
243
+
244
+ ---
245
+
246
+ ### `Store` — Singleton State Container
247
+
248
+ App-wide runtime state. No reactivity — a plain shared key-value store with dot-notation path access. Views read from Store in `onResume()` and write in event handlers.
249
+
250
+ ```js
251
+ import { Store } from "jops-core";
252
+
253
+ const store = Store.singleton();
254
+ ```
255
+
256
+ | Method | Description |
257
+ | ------------------- | ---------------------------------------------------------------------- |
258
+ | `Store.singleton()` | Returns the single Store instance, creating it on first call |
259
+ | `get(path)` | Reads a value by dot-notation path, e.g. `"user.name"` |
260
+ | `set(path, value)` | Writes a value by dot-notation path; auto-creates intermediate objects |
261
+ | `clear()` | Resets the entire state to `{}` |
262
+
263
+ ```js
264
+ Store.singleton().set("user.name", "Alice");
265
+ Store.singleton().get("user.name"); // "Alice"
266
+ Store.singleton().set("cart.items", [1, 2, 3]);
267
+ Store.singleton().clear();
268
+ ```
269
+
270
+ ---
271
+
272
+ ### `EventBus` — Static Pub/Sub
273
+
274
+ Stateless cross-view event broadcasting. All methods are static — no instance needed.
275
+
276
+ | Method | Description |
277
+ | --------------------------------------- | ---------------------------------------------------------------------- |
278
+ | `EventBus.subscribe(event, callback)` | Registers a listener for the named event |
279
+ | `EventBus.send(event, data?)` | Fires the named event to all subscribers with an optional data payload |
280
+ | `EventBus.unsubscribe(event, callback)` | Removes a specific listener by reference |
281
+
282
+ ```js
283
+ import { EventBus } from "jops-core";
284
+
285
+ EventBus.subscribe("user:login", (data) => console.log(data.name));
286
+ EventBus.send("user:login", { name: "Alice" });
287
+ EventBus.unsubscribe("user:login", handler);
288
+ ```
289
+
290
+ ---
291
+
292
+ ## CLI
293
+
294
+ | Command | Description |
295
+ | --------------------- | -------------------------------------------------------------------------- |
296
+ | `npx jops-core init` | Vendor library, configure `index.html`, inject scripts into `package.json` |
297
+ | `npx jops-core build` | Generate production-ready `dist/` folder |
package/bin/init.js ADDED
@@ -0,0 +1,223 @@
1
+ #!/usr/bin/env node
2
+ import fs from "fs";
3
+ import path from "path";
4
+ import readline from "readline";
5
+ import { fileURLToPath } from "url";
6
+
7
+ const [,, command] = process.argv;
8
+
9
+ if (command !== "init" && command !== "build") {
10
+ console.log("Usage:");
11
+ console.log(" npx jops-core init — vendor library and configure index.html");
12
+ console.log(" npx jops-core build — generate production-ready dist/ folder");
13
+ process.exit(0);
14
+ }
15
+
16
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
17
+ const consumerRoot = process.cwd();
18
+
19
+ function copyDir(src, dest) {
20
+ const SKIP = new Set(["node_modules", "dist", ".git", "package.json", "package-lock.json", ".DS_Store"]);
21
+ const SKIP_EXT = new Set([".md", ".tgz", ".log"]);
22
+ fs.mkdirSync(dest, { recursive: true });
23
+ for (const entry of fs.readdirSync(src, { withFileTypes: true })) {
24
+ if (SKIP.has(entry.name)) continue;
25
+ if (SKIP_EXT.has(path.extname(entry.name).toLowerCase())) continue;
26
+ const srcPath = path.join(src, entry.name);
27
+ const destPath = path.join(dest, entry.name);
28
+ if (entry.isDirectory()) {
29
+ copyDir(srcPath, destPath);
30
+ } else if (!fs.existsSync(destPath)) {
31
+ fs.copyFileSync(srcPath, destPath);
32
+ console.log(`[jops-core] Created ${path.relative(consumerRoot, destPath)}`);
33
+ }
34
+ }
35
+ }
36
+
37
+ // ─── init ────────────────────────────────────────────────────────────────────
38
+
39
+ if (command === "init") {
40
+ const indexPath = path.join(consumerRoot, "index.html");
41
+ const libDir = path.join(consumerRoot, "lib");
42
+ const vendorDest = path.join(libDir, "jops-core.min.js");
43
+ const vendorSrc = path.join(__dirname, "../lib/jops-core.min.js");
44
+
45
+ if (!fs.existsSync(libDir)) fs.mkdirSync(libDir);
46
+ fs.copyFileSync(vendorSrc, vendorDest);
47
+ console.log("[jops-core] Vendored jops-core.min.js → lib/jops-core.min.js");
48
+
49
+ const IMPORTMAP = ` <script type="importmap">
50
+ {
51
+ "imports": {
52
+ "jops-core": "/lib/jops-core.min.js"
53
+ }
54
+ }
55
+ </script>`;
56
+
57
+ const MODULE_SCRIPT = ` <script type="module" src="/lib/jops-core.min.js"></script>`;
58
+
59
+ const TEMPLATE = `<!doctype html>
60
+ <html lang="en">
61
+ <head>
62
+ <meta charset="UTF-8" />
63
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
64
+ <title>JOPS App</title>
65
+ ${IMPORTMAP}
66
+ </head>
67
+ <body>
68
+ ${MODULE_SCRIPT}
69
+ </body>
70
+ </html>
71
+ `;
72
+
73
+ if (!fs.existsSync(indexPath)) {
74
+ fs.writeFileSync(indexPath, TEMPLATE, "utf8");
75
+ console.log("[jops-core] Created index.html with importmap and module script.");
76
+ } else {
77
+ let html = fs.readFileSync(indexPath, "utf8");
78
+ let changed = false;
79
+
80
+ const hasImportmap = /type=["']importmap["']/.test(html) && html.includes('"jops-core"');
81
+ if (!hasImportmap) {
82
+ html = html.replace("</head>", `${IMPORTMAP}\n </head>`);
83
+ changed = true;
84
+ console.log("[jops-core] Added importmap to index.html.");
85
+ }
86
+
87
+ const hasModuleScript = /src=["'][^"']*jops-core\.min\.js["']/.test(html);
88
+ if (!hasModuleScript) {
89
+ html = html.replace("</body>", `${MODULE_SCRIPT}\n </body>`);
90
+ changed = true;
91
+ console.log("[jops-core] Added module script to index.html.");
92
+ }
93
+
94
+ if (changed) {
95
+ fs.writeFileSync(indexPath, html, "utf8");
96
+ } else {
97
+ console.log("[jops-core] index.html already configured.");
98
+ }
99
+ }
100
+
101
+ // VS Code extension recommendation
102
+ const vscodeSettingsDir = path.join(consumerRoot, ".vscode");
103
+ if (!fs.existsSync(vscodeSettingsDir)) fs.mkdirSync(vscodeSettingsDir);
104
+
105
+ const extensionsJsonPath = path.join(vscodeSettingsDir, "extensions.json");
106
+ const extensionsJson = fs.existsSync(extensionsJsonPath)
107
+ ? JSON.parse(fs.readFileSync(extensionsJsonPath, "utf8"))
108
+ : { recommendations: [] };
109
+ if (!extensionsJson.recommendations.includes("bierner.lit-html")) {
110
+ extensionsJson.recommendations.push("bierner.lit-html");
111
+ fs.writeFileSync(extensionsJsonPath, JSON.stringify(extensionsJson, null, 2) + "\n", "utf8");
112
+ }
113
+
114
+ const settingsJsonPath = path.join(vscodeSettingsDir, "settings.json");
115
+ const settingsJson = fs.existsSync(settingsJsonPath)
116
+ ? JSON.parse(fs.readFileSync(settingsJsonPath, "utf8"))
117
+ : {};
118
+ if (!settingsJson["lit-html.tags"]?.includes("layout")) {
119
+ settingsJson["lit-html.tags"] = [...(settingsJson["lit-html.tags"] ?? []), "layout"];
120
+ }
121
+ const jopsTagColor = "#569CD6";
122
+ const rules = settingsJson["editor.tokenColorCustomizations"]?.textMateRules ?? [];
123
+ const hasRule = rules.some(r => r.scope === "invalid.illegal.unrecognized-tag.html");
124
+ if (!hasRule) {
125
+ settingsJson["editor.tokenColorCustomizations"] = {
126
+ ...settingsJson["editor.tokenColorCustomizations"],
127
+ textMateRules: [
128
+ ...rules,
129
+ {
130
+ scope: "invalid.illegal.unrecognized-tag.html",
131
+ settings: { foreground: jopsTagColor }
132
+ }
133
+ ]
134
+ };
135
+ }
136
+ fs.writeFileSync(settingsJsonPath, JSON.stringify(settingsJson, null, 2) + "\n", "utf8");
137
+
138
+ console.log("[jops-core] VS Code: install the recommended 'lit-html' extension (bierner.lit-html) for HTML highlighting inside layout`` template literals.");
139
+
140
+ // Inject scripts into consumer's package.json
141
+ const pkgPath = path.join(consumerRoot, "package.json");
142
+ if (fs.existsSync(pkgPath)) {
143
+ const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf8"));
144
+ let changed = false;
145
+ if (!pkg.scripts?.build) {
146
+ pkg.scripts = { ...pkg.scripts, build: "npx jops-core build" };
147
+ changed = true;
148
+ console.log('[jops-core] Added "build" script to package.json.');
149
+ }
150
+ if (!pkg.scripts?.postinstall) {
151
+ pkg.scripts = { ...pkg.scripts, postinstall: "npx jops-core init" };
152
+ changed = true;
153
+ console.log('[jops-core] Added "postinstall" script to package.json.');
154
+ }
155
+ if (changed) fs.writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + "\n", "utf8");
156
+ }
157
+
158
+ // Template selection
159
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
160
+ const ask = (q) => new Promise(resolve => rl.question(q, resolve));
161
+
162
+ const choice = await ask(
163
+ "\nCreate JOPS project from existing templates:\n" +
164
+ " 1. Empty JOPS project (basic skeleton for JOPS project architecture)\n" +
165
+ " 2. Hello World JOPS project (an example project utilizing all features)\n" +
166
+ " 3. None (I'll create my own architecture)\n" +
167
+ "Enter choice (1-3): "
168
+ );
169
+ rl.close();
170
+
171
+ const templateSrc = path.join(__dirname, "../templates");
172
+ if (choice.trim() === "1") {
173
+ copyDir(path.join(templateSrc, "empty"), consumerRoot);
174
+ // Add data-app to the module script in index.html
175
+ let html = fs.readFileSync(indexPath, "utf8");
176
+ html = html.replace(
177
+ /(<script[^>]+src=["'][^"']*jops-core\.min\.js["'])([^>]*>)/,
178
+ (_, open, rest) => open.includes("data-app") ? open + rest : open + ' data-app="/src/App.js"' + rest
179
+ );
180
+ fs.writeFileSync(indexPath, html, "utf8");
181
+ console.log("[jops-core] Created empty JOPS project skeleton.");
182
+ } else if (choice.trim() === "2") {
183
+ copyDir(path.join(templateSrc, "hello-world"), consumerRoot);
184
+ let html = fs.readFileSync(indexPath, "utf8");
185
+ html = html.replace(
186
+ /(<script[^>]+src=["'][^"']*jops-core\.min\.js["'])([^>]*>)/,
187
+ (_, open, rest) => open.includes("data-app") ? open + rest : open + ' data-app="/src/App.js"' + rest
188
+ );
189
+ fs.writeFileSync(indexPath, html, "utf8");
190
+ console.log("[jops-core] Created Hello World JOPS project.");
191
+ } else {
192
+ console.log("[jops-core] No template applied. Setup complete.");
193
+ }
194
+ }
195
+
196
+ // ─── build ───────────────────────────────────────────────────────────────────
197
+
198
+ if (command === "build") {
199
+ const BUILD_EXCLUDE = new Set([
200
+ "node_modules", "dist", ".git",
201
+ "package.json", "package-lock.json", ".gitignore", ".npmrc",
202
+ ]);
203
+
204
+ function buildCopyDir(src, dest) {
205
+ fs.mkdirSync(dest, { recursive: true });
206
+ for (const entry of fs.readdirSync(src, { withFileTypes: true })) {
207
+ if (BUILD_EXCLUDE.has(entry.name)) continue;
208
+ if ([".md", ".tgz", ".log"].includes(path.extname(entry.name).toLowerCase())) continue;
209
+ const srcPath = path.join(src, entry.name);
210
+ const destPath = path.join(dest, entry.name);
211
+ if (entry.isDirectory()) {
212
+ buildCopyDir(srcPath, destPath);
213
+ } else {
214
+ fs.copyFileSync(srcPath, destPath);
215
+ }
216
+ }
217
+ }
218
+
219
+ const distDir = path.join(consumerRoot, "dist");
220
+ if (fs.existsSync(distDir)) fs.rmSync(distDir, { recursive: true, force: true });
221
+ buildCopyDir(consumerRoot, distDir);
222
+ console.log("[jops-core] Built → dist/");
223
+ }
@@ -0,0 +1,5 @@
1
+ var _=Object.defineProperty;var h=(a,t)=>_(a,"name",{value:t,configurable:!0});(()=>{console.log("JOPS initialized");let a=document.currentScript?.dataset.app??"/src/App.js",t=document.createElement("script");t.type="module",t.textContent=`
2
+ import('${a}')
3
+ .then(m => new m.default())
4
+ .catch(err => console.error("JOPS: App.js not found or failed to load", err));
5
+ `,document.body.appendChild(t)})();var f=class{static{h(this,"RawHTML")}constructor(t){this.html=t}},R=h(a=>new f(a),"raw");function E(a,...t){return a.reduce((e,s,n)=>{if(n>=t.length)return e+s;let o=t[n];return o==null?e+s:o instanceof f?e+s+o.html:e+s+String(o).replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;").replace(/"/g,"&quot;")},"")}h(E,"layout");var m=class{static{h(this,"Element")}constructor(t,e){this.element=t?.id===e?t:t.querySelector(`#${e}`)}set(t){this.element.innerHTML=t}value(){return this.element.innerHTML}},p=class a{static{h(this,"View")}#t=!1;#e=!1;constructor(t){this.loadLayout(t),this.subViews=[]}async loadLayout(t){if(t)if(t.endsWith(".js"))try{let e=await import(t);this.layout=e.default}catch(e){console.error("JOPS: failed to load layout:",t,e)}else this.layout=t}setStyle(t){if(t)if(this.style=t,t.endsWith(".css")){if(document.querySelector(`link[href="${t}"]`))return;let e=document.createElement("link");e.rel="stylesheet",e.href=t,document.head.appendChild(e)}else{if(document.querySelector(`style[data-jops="${t}"]`))return;let e=document.createElement("style");e.setAttribute("data-jops",t),e.textContent=t,document.head.appendChild(e)}}get(t){return new m(this.domNode,t)}bindEvents(){if(!this.domNode)return;let t=[...this.domNode.querySelectorAll("[type='jops-event-bind']")];this.domNode.getAttribute("type")==="jops-event-bind"&&t.unshift(this.domNode);for(let e of t)for(let s of e.attributes){if(!s.name.startsWith("on"))continue;let n=s.value.trim();typeof this[n]=="function"&&(e[s.name]=o=>this[n](o))}}inflate(){if(!this.layout||this.#t)return;this.#t=!0;let t=document.createElement("template");t.innerHTML=this.layout;let s=t.content.cloneNode(!0).firstElementChild;this.domNode=s}async addSubView(t,e){this.subViews.push(e),e.domNode||(await e.loadLayout(),e.inflate()),this.domNode.querySelector(`#${t}`).appendChild(e.domNode),await e?.onLayout?.(),await e?.onResume?.(),e?.bindEvents?.()}hide(){this.domNode.style.display="none"}show(){this.domNode.style.display=""}find(t){let e=[];for(let s of this.subViews)t(s)&&e.push(s),e.push(...s.find?.(t)??[]);return e}destroy(){this.subViews.forEach(t=>t.destroy?.()),this.subViews=[],this.domNode?.remove(),this.domNode=null,this.layout=null}removeSubView(t){t.destroy?.(),this.subViews=this.subViews.filter(e=>e!==t)}async insertSubView(t,e){t.domNode||(await t.loadLayout(),t.inflate());let s=this.domNode.children[e]??null;this.domNode.insertBefore(t.domNode,s),this.subViews.splice(e,0,t),await t?.onLayout?.(),await t?.onResume?.(),t?.bindEvents?.()}async render(){if(!this.layout||this.#e)return;this.#e=!0;let t=this.domNode??document.body;t===document.body&&(t.innerHTML=this.layout);let e=Array.from(t.querySelectorAll("[type='jops']")).filter(s=>s.tagName==="ROUTER"||!s.closest("router")||s.closest("router")===t);e.length!==0&&await Promise.all(e.map(async s=>{if(s.tagName==="ROUTER"){let d=new y(s);await d.render(),await d.onLayout();return}let n=s.getAttribute("layout"),o=s.getAttribute("src");if(!o&&!n)return;let r;if(o){let l=(await import(o)).default;r=new l}else r=new a;this.subViews.push(r),await r.loadLayout(n);let i=document.createElement("template");i.innerHTML=r.layout??"";let c=i.content.cloneNode(!0);if(s.children.length>0){let d=c.firstElementChild??c;Array.from(s.children).forEach(l=>d.appendChild(l))}let u=c.firstElementChild;r.routePath=s.getAttribute("path"),s.replaceWith(c),r.domNode=u,await r.render(),await r?.onLayout?.(),await r?.onResume?.(),r?.bindEvents?.()}))}},b=class a{static{h(this,"EventBus")}static#t={};static subscribe(t,e){a.#t[t]||(a.#t[t]=[]),a.#t[t].push(e)}static send(t,e){a.#t[t]?.forEach(s=>s(e))}static unsubscribe(t,e){a.#t[t]&&(a.#t[t]=a.#t[t].filter(s=>s!==e))}},T=class a{static{h(this,"Store")}static#t=null;static singleton(){return a.#t||(a.#t=new a),a.#t}constructor(){this._state={}}get(t){return t.split(".").reduce((e,s)=>e?.[s],this._state)}set(t,e){let s=t.split("."),n=s.pop(),o=s.reduce((r,i)=>(r[i]||(r[i]={}),r[i]),this._state);o[n]=e}clear(){this._state={}}},y=class a extends p{static{h(this,"Router")}static#t=null;static singleton(){return a.#t}constructor(t){super(),a.#t=this,this.rootNode=t.parentNode,this.routerDomNode=t,this.domNode=t,this.layout=t.innerHTML,this.animation=t.getAttribute("animation")??"none"}async onLayout(){let t=document.createDocumentFragment();for(;this.routerDomNode.firstChild;)t.appendChild(this.routerDomNode.firstChild);this.routerDomNode.replaceWith(t),this.domNode=this.rootNode,this.initRouter()}initRouter(){this.routes={};for(let e of this.subViews)if(e.routePath){let s=e.routePath.slice(1).split("?")[0];this.routes[s]=e}this._navStack=[],this._baseView=null;let t=Object.keys(this.routes)[0];history.replaceState(null,"","#"+t),window.addEventListener("hashchange",()=>{let e=window.location.hash.slice(1),s=e.split("?")[0];if(!(!s||!this.routes[s])){if(this._pendingBack){this._pendingBack=!1;return}this._switchTo(e)}}),window.addEventListener("popstate",()=>{if(this._navStack.length>0){let n=this._navStack.pop(),o=this._navStack.at(-1)??this._baseView;n?.onBackPressed?.(),this._animateTransition(n,o,"back").then(()=>n.destroy());return}this._currentView?.onBackPressed?.();let e=window.location.hash.slice(1),s=e.split("?")[0];s&&this.routes[s]&&(this._pendingBack=!0,this._switchTo(e))}),this._switchTo(t)}async _animateTransition(t,e,s){this.animation==="slide"?await this._slideTransition(t,e,s):this.animation==="fade"?await this._fadeTransition(t,e):(t.domNode.hidden=!0,e.domNode.hidden=!1,this._currentView=e,e?.onResume?.())}async _slideTransition(t,e,s){let n=this.domNode,o=s==="forward",r=n.getBoundingClientRect(),i=t.domNode.getBoundingClientRect(),c=i.left-r.left,u=i.top-r.top,d=i.width,l=n.offsetWidth-c,w=`position:absolute;left:${c}px;top:${u}px;width:${d}px`;n.style.cssText=`position:relative;overflow:hidden;height:${n.offsetHeight}px`,t.domNode.style.cssText=`${w};z-index:${o?1:2}`,t.domNode.hidden=!1,e.domNode.style.cssText=`${w};z-index:${o?2:1}`,e.domNode.hidden=!1;let x=o?e.domNode:t.domNode,S=o?`${l}px`:"0px",V=o?"0px":`${l}px`,N=x.animate([{transform:`translateX(${S})`},{transform:`translateX(${V})`}],{duration:300,easing:"ease",fill:"forwards"});await N.finished,N.cancel(),t.domNode.hidden=!0,t.domNode.style.cssText="",e.domNode.style.cssText="",n.style.cssText="",this._currentView=e,e?.onResume?.()}async _fadeTransition(t,e){let s=this.domNode,n=s.getBoundingClientRect(),o=t.domNode.getBoundingClientRect(),r=o.left-n.left,i=o.top-n.top,c=o.width,u=`position:absolute;left:${r}px;top:${i}px;width:${c}px`;s.style.cssText=`position:relative;overflow:hidden;height:${s.offsetHeight}px`,t.domNode.style.cssText=u,t.domNode.hidden=!1,e.domNode.style.cssText=u,e.domNode.hidden=!1;let d=t.domNode.animate([{opacity:1},{opacity:0}],{duration:450,easing:"ease",fill:"forwards"}),l=e.domNode.animate([{opacity:0},{opacity:1}],{duration:450,easing:"ease",fill:"forwards"});await Promise.all([d.finished,l.finished]),d.cancel(),l.cancel(),t.domNode.hidden=!0,t.domNode.style.cssText="",e.domNode.style.cssText="",s.style.cssText="",this._currentView=e,e?.onResume?.()}_switchTo(t){let e=t.split("?")[0];for(let[s,n]of Object.entries(this.routes)){let o=s===e;n.domNode.hidden=!o,o&&(this._currentView=n,n?.onResume?.())}}back(){history.back()}getParams(){let t=window.location.hash.split("?")[1];return t?Object.fromEntries(new URLSearchParams(t)):{}}async navigate(t,e){let[s,n]=t.replace(/^\//,"").split("?"),o=s,r=e??this.routes[o]?.constructor;if(!r){console.error(`JOPS Router: no class found for path "${t}"`);return}this._navStack.length===0&&(this._baseView=this._currentView);let i=new r;await i.loadLayout(),i.inflate(),this.domNode.appendChild(i.domNode),history.pushState({jopsNav:!0},"","#"+o+(n?"?"+n:"")),await i.onLayout?.(),await i.onResume?.(),i.bindEvents?.(),this._navStack.push(i),this._animateTransition(this._currentView,i,"forward")}async replace(t,e){this._navStack.forEach(i=>i.destroy?.()),this._navStack=[];let[s,n]=t.replace(/^\//,"").split("?"),o=s,r="#"+o+(n?"?"+n:"");if(this.routes[o])history.replaceState(null,"",r),this._switchTo(o);else if(e){let i=new e;await i.loadLayout(),i.inflate(),this.domNode.appendChild(i.domNode),await i.onLayout?.(),await i.onResume?.(),i.bindEvents?.(),this._currentView&&(this._currentView.domNode.hidden=!0),this._currentView=i,history.replaceState(null,"",r)}else console.error(`JOPS Router: no instance or class found for path "${t}"`)}};export{b as EventBus,y as Router,T as Store,p as View,E as layout,R as raw};
package/package.json ADDED
@@ -0,0 +1,34 @@
1
+ {
2
+ "name": "jops-core",
3
+ "version": "1.0.0",
4
+ "description": "A no-bundler, OOP-first SPA framework with Android-style view composition",
5
+ "type": "module",
6
+ "exports": "./lib/jops-core.min.js",
7
+ "browser": "./lib/jops-core.min.js",
8
+ "bin": {
9
+ "jops-core": "bin/init.js"
10
+ },
11
+ "files": [
12
+ "lib/jops-core.min.js",
13
+ "bin/init.js",
14
+ "templates/"
15
+ ],
16
+ "scripts": {
17
+ "build": "esbuild src/jops-core.js --bundle --format=esm --minify --keep-names --outfile=lib/jops-core.min.js && npm pack"
18
+ },
19
+ "keywords": [
20
+ "spa",
21
+ "framework",
22
+ "oop",
23
+ "no-bundler",
24
+ "router",
25
+ "view",
26
+ "AI friendly SDK"
27
+ ],
28
+ "author": "arik@ui-db.com",
29
+ "license": "UNLICENSED",
30
+ "devDependencies": {
31
+ "esbuild": "^0.25.12"
32
+ },
33
+ "packageManager": "npm@12.0.2+sha512.b885e890b9418fa1693544d05f53e64f9a73ec194837d4258b15fecdd692347b1dd2a517b1b0cbaf9d31cd8e92c3b70956bd2ecc72833a57b4b3098f5bfa7943"
34
+ }
@@ -0,0 +1,11 @@
1
+ import { View } from "jops-core";
2
+
3
+ export default class App {
4
+ constructor() {
5
+ const root = new View();
6
+ (async () => {
7
+ await root.loadLayout("/src/layout/mainlayout.js");
8
+ root.render();
9
+ })();
10
+ }
11
+ }
@@ -0,0 +1,2 @@
1
+ import { layout } from "jops-core";
2
+ export default layout`<div>I'm your main layout!</div>`;
@@ -0,0 +1,57 @@
1
+ <svg viewBox="0 0 400 320" xmlns="http://www.w3.org/2000/svg" font-family="monospace">
2
+ <rect width="400" height="320" fill="#0f1117"/>
3
+
4
+ <!-- Title -->
5
+ <text x="200" y="30" text-anchor="middle" fill="#7c8cf8" font-size="13" font-weight="bold" letter-spacing="2">VIEW HIERARCHY</text>
6
+
7
+ <!-- Root node -->
8
+ <rect x="150" y="50" width="100" height="36" rx="6" fill="#1e2235" stroke="#7c8cf8" stroke-width="1.5"/>
9
+ <text x="200" y="73" text-anchor="middle" fill="#7c8cf8" font-size="12">App</text>
10
+
11
+ <!-- Lines to level 2 -->
12
+ <line x1="200" y1="86" x2="100" y2="120" stroke="#3d4466" stroke-width="1.2"/>
13
+ <line x1="200" y1="86" x2="200" y2="120" stroke="#3d4466" stroke-width="1.2"/>
14
+ <line x1="200" y1="86" x2="300" y2="120" stroke="#3d4466" stroke-width="1.2"/>
15
+
16
+ <!-- Level 2 nodes -->
17
+ <rect x="50" y="120" width="100" height="36" rx="6" fill="#1e2235" stroke="#56cfb2" stroke-width="1.5"/>
18
+ <text x="100" y="143" text-anchor="middle" fill="#56cfb2" font-size="12">Router</text>
19
+
20
+ <rect x="150" y="120" width="100" height="36" rx="6" fill="#1e2235" stroke="#56cfb2" stroke-width="1.5"/>
21
+ <text x="200" y="143" text-anchor="middle" fill="#56cfb2" font-size="12">Header</text>
22
+
23
+ <rect x="250" y="120" width="100" height="36" rx="6" fill="#1e2235" stroke="#56cfb2" stroke-width="1.5"/>
24
+ <text x="300" y="143" text-anchor="middle" fill="#56cfb2" font-size="12">Footer</text>
25
+
26
+ <!-- Lines to level 3 (from Router) -->
27
+ <line x1="100" y1="156" x2="60" y2="192" stroke="#3d4466" stroke-width="1.2"/>
28
+ <line x1="100" y1="156" x2="140" y2="192" stroke="#3d4466" stroke-width="1.2"/>
29
+
30
+ <!-- Level 3 nodes -->
31
+ <rect x="20" y="192" width="80" height="36" rx="6" fill="#1e2235" stroke="#f0a040" stroke-width="1.5"/>
32
+ <text x="60" y="215" text-anchor="middle" fill="#f0a040" font-size="11">HomeView</text>
33
+
34
+ <rect x="110" y="192" width="80" height="36" rx="6" fill="#1e2235" stroke="#f0a040" stroke-width="1.5"/>
35
+ <text x="150" y="215" text-anchor="middle" fill="#f0a040" font-size="11">FormView</text>
36
+
37
+ <!-- Lines to level 4 -->
38
+ <line x1="150" y1="228" x2="130" y2="262" stroke="#3d4466" stroke-width="1.2"/>
39
+ <line x1="150" y1="228" x2="170" y2="262" stroke="#3d4466" stroke-width="1.2"/>
40
+
41
+ <!-- Level 4 nodes -->
42
+ <rect x="100" y="262" width="60" height="30" rx="5" fill="#1e2235" stroke="#f06292" stroke-width="1.5"/>
43
+ <text x="130" y="282" text-anchor="middle" fill="#f06292" font-size="10">Card</text>
44
+
45
+ <rect x="170" y="262" width="60" height="30" rx="5" fill="#1e2235" stroke="#f06292" stroke-width="1.5"/>
46
+ <text x="200" y="282" text-anchor="middle" fill="#f06292" font-size="10">Card</text>
47
+
48
+ <!-- Legend -->
49
+ <rect x="270" y="200" width="10" height="10" rx="2" fill="#7c8cf8"/>
50
+ <text x="285" y="210" fill="#5a6080" font-size="10">App root</text>
51
+ <rect x="270" y="218" width="10" height="10" rx="2" fill="#56cfb2"/>
52
+ <text x="285" y="228" fill="#5a6080" font-size="10">View</text>
53
+ <rect x="270" y="236" width="10" height="10" rx="2" fill="#f0a040"/>
54
+ <text x="285" y="246" fill="#5a6080" font-size="10">Route</text>
55
+ <rect x="270" y="254" width="10" height="10" rx="2" fill="#f06292"/>
56
+ <text x="285" y="264" fill="#5a6080" font-size="10">SubView</text>
57
+ </svg>