lightview 2.0.7 → 2.0.8

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 CHANGED
@@ -7,7 +7,7 @@ Access the full documentaion at [lightview.dev](https://lightview.dev).
7
7
 
8
8
  This NPM package is both the library and the website supporting the library. The website is built using Lightview. The core library files are in the root directory. The Website entry point is index.html and the restr of the site is under ./docs. The site is served by a Cloudflare pages deployment.
9
9
 
10
- **Core**: ~8KB | **With Hypermedia Extensions and Component Library Support**: ~18KB total
10
+ **Core**: ~7.5KB | **Additional Hypermedia Extensions and Component Library Support**: ~15KB | **Router**: ~3KB
11
11
 
12
12
  Fast: This [gallery of components](https://lightview.dev/docs/components/) loads in about 1 second:
13
13
 
package/docs/about.html CHANGED
@@ -108,12 +108,6 @@
108
108
  it lean and modular so your apps stay fast.
109
109
  </p>
110
110
  </div>
111
- <div class="feature-card">
112
- <h3 class="feature-title">🔌 Zero Dependencies</h3>
113
- <p class="feature-description">
114
- No external dependencies. No supply chain worries. No version conflicts. Just works.
115
- </p>
116
- </div>
117
111
  <div class="feature-card">
118
112
  <h3 class="feature-title">🚀 No Build Step</h3>
119
113
  <p class="feature-description">
@@ -131,7 +125,7 @@
131
125
  <h3 class="feature-title">😊 Have Fun</h3>
132
126
  <p class="feature-description">
133
127
  Coding should spark joy. Simple API, clear and interactive docs, helpful errors. Built for humans
134
- who like to smile.
128
+ who like to smile and LLMs that need structure (so humans don't cry about slop).
135
129
  </p>
136
130
  </div>
137
131
  </div>
package/docs/index.html CHANGED
@@ -19,10 +19,6 @@
19
19
  <a href="./getting-started/" class="btn btn-primary btn-lg">Get Started</a>
20
20
  </div>
21
21
  <div class="hero-stats">
22
- <div class="hero-stat">
23
- <div class="hero-stat-value">Zero</div>
24
- <div class="hero-stat-label">Dependencies</div>
25
- </div>
26
22
  <div class="hero-stat">
27
23
  <div class="hero-stat-value">Zero</div>
28
24
  <div class="hero-stat-label">Build Steps</div>
@@ -1,364 +1,159 @@
1
1
  (() => {
2
+ /**
3
+ * LIGHTVIEW ROUTER
4
+ * A lightweight, pipeline-based History API router with middleware support.
5
+ */
2
6
  // ============= LIGHTVIEW ROUTER =============
3
7
  // Pipeline-based History API router with middleware support
4
8
 
5
9
  /**
6
- * Shim function for individual pages
7
- * Redirects direct page access to the shell with a load parameter
8
- * @param {string} shellPath - Relative path to the shell (e.g., '/index.html')
10
+ * Shell-based routing helper. If the 'content' element is missing,
11
+ * redirects to a shell path with the current path in the 'load' query parameter.
9
12
  */
10
13
  const base = (shellPath) => {
11
- if (typeof window === 'undefined') return;
12
-
13
- // Check if we're in the shell or loaded directly
14
- const inShell = document.getElementById('content') !== null;
15
- if (inShell) return;
16
-
17
- // Get current path relative to domain root
18
- const currentPath = window.location.pathname;
19
-
20
- // Build shell URL with load parameter
21
- const shellUrl = new URL(shellPath, window.location.href);
22
- shellUrl.searchParams.set('load', currentPath);
23
-
24
- // Redirect to shell
25
- window.location.href = shellUrl.toString();
14
+ if (typeof window === 'undefined' || document.getElementById('content')) return;
15
+ const url = new URL(shellPath, window.location.href);
16
+ url.searchParams.set('load', window.location.pathname);
17
+ window.location.href = url.toString();
26
18
  };
27
19
 
28
20
  /**
29
- * Create a new Router instance
21
+ * Creates a new router instance.
22
+ * @param {Object} options - Router configuration.
30
23
  */
31
24
  const router = (options = {}) => {
32
- const {
33
- base = '',
34
- contentEl = null,
35
- notFound = null,
36
- debug = false,
37
- onResponse = null,
38
- onStart = null
39
- } = options;
40
-
25
+ const { base = '', contentEl, notFound, debug, onResponse, onStart } = options;
41
26
  const chains = [];
42
27
 
43
28
  /**
44
- * Normalize a path by removing base and trailing slashes
29
+ * Normalizes paths by adding leading slash and removing trailing slash.
45
30
  */
46
- const normalizePath = (path) => {
47
- if (!path) return '/';
48
-
49
- // Handle full URLs
50
- if (path.startsWith('http') || path.startsWith('//')) {
51
- try {
52
- const url = new URL(path, window.location.origin);
53
- path = url.pathname;
54
- } catch (e) {
55
- // Invalid URL, treat as path
56
- }
57
- }
58
-
59
- if (base && path.startsWith(base)) {
60
- path = path.slice(base.length);
61
- }
62
- if (!path.startsWith('/')) {
63
- path = '/' + path;
64
- }
65
- if (path.length > 1 && path.endsWith('/')) {
66
- path = path.slice(0, -1);
67
- }
68
- return path;
31
+ const normalizePath = (p) => {
32
+ if (!p) return '/';
33
+ try { if (p.startsWith('http') || p.startsWith('//')) p = new URL(p, window.location.origin).pathname; } catch (e) { /* Invalid URL */ }
34
+ if (base && p.startsWith(base)) p = p.slice(base.length);
35
+ return p.replace(/\/+$/, '').replace(/^([^/])/, '/$1') || '/';
69
36
  };
70
37
 
71
- /**
72
- * Convert a matcher (string/regexp) into a function
73
- * Returns: (input) => params OR null (if no match)
74
- */
75
38
  const createMatcher = (pattern) => {
76
- if (pattern instanceof RegExp) {
77
- return (ctx) => {
78
- const path = typeof ctx === 'string' ? ctx : ctx.path;
79
- const match = path.match(pattern);
80
- return match ? { match, ...ctx } : null;
81
- };
82
- }
83
-
84
- if (typeof pattern === 'string') {
85
- return (ctx) => {
86
- const path = typeof ctx === 'string' ? ctx : ctx.path;
87
-
88
- // Specific check: if pattern is exactly '*', match everything
89
- if (pattern === '*') return { path, wildcard: path, ...ctx };
90
-
91
- // Exact match
92
- if (pattern === path) return { path, ...ctx };
93
-
94
- // Wildcard /api/*
95
- if (pattern.includes('*')) {
96
- const regexStr = '^' + pattern.replace(/[.*+?^${}()|[\]\\]/g, '\\$&').replace(/\\\*/g, '(.*)') + '$';
97
- const regex = new RegExp(regexStr);
98
- const match = path.match(regex);
99
- if (match) {
100
- return { path, wildcard: match[1], ...ctx };
101
- }
102
- }
103
-
104
- // Named params /user/:id
105
- if (pattern.includes(':')) {
106
- const keys = [];
107
- const regexStr = '^' + pattern.replace(/:([^/]+)/g, (_, key) => {
108
- keys.push(key);
109
- return '([^/]+)';
110
- }) + '$';
111
- const match = path.match(new RegExp(regexStr));
112
-
113
- if (match) {
114
- const params = {};
115
- keys.forEach((key, i) => {
116
- params[key] = match[i + 1];
117
- });
118
- return { path, params, ...ctx };
119
- }
120
- }
121
-
122
- return null;
123
- };
124
- }
125
-
126
- return pattern; // Already a function
127
- };
128
-
129
- /**
130
- * Convert a replacement string into a function
131
- * Returns: (ctx) => updated context with new path
132
- */
133
- const createReplacer = (pattern) => {
39
+ if (typeof pattern === 'function') return pattern;
134
40
  return (ctx) => {
135
- let newPath = pattern;
136
- if (ctx.wildcard && newPath.includes('*')) {
137
- newPath = newPath.replace('*', ctx.wildcard);
41
+ const { path } = ctx;
42
+ if (pattern instanceof RegExp) {
43
+ const m = path.match(pattern);
44
+ return m ? { ...ctx, match: m } : null;
138
45
  }
139
- if (ctx.params) {
140
- Object.entries(ctx.params).forEach(([key, val]) => {
141
- newPath = newPath.replace(':' + key, val);
142
- });
46
+ if (pattern === '*' || pattern === path) return { ...ctx, wildcard: path };
47
+
48
+ const keys = [];
49
+ const regexStr = '^' + pattern.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
50
+ .replace(/\\\*/g, '(.*)')
51
+ .replace(/:([^/]+)/g, (_, k) => (keys.push(k), '([^/]+)')) + '$';
52
+ const m = path.match(new RegExp(regexStr));
53
+ if (m) {
54
+ const params = {};
55
+ keys.forEach((k, i) => params[k] = m[i + 1]);
56
+ return { ...ctx, params, wildcard: m[1] };
143
57
  }
144
- // Return updated context instead of just string
145
- return { ...ctx, path: newPath };
58
+ return null;
146
59
  };
147
60
  };
148
61
 
149
- /**
150
- * Default fetch handler - fetches the current path and returns Response
151
- * Uses contentEl from context (allows middleware to override target)
152
- */
153
- const defaultFetchHandler = async (ctx) => {
154
- const path = typeof ctx === 'string' ? ctx : ctx.path;
62
+ const createReplacer = (pat) => (ctx) => ({
63
+ ...ctx,
64
+ path: pat.replace(/\*|:([^/]+)/g, (m, k) => (k ? ctx.params?.[k] : ctx.wildcard) || m)
65
+ });
66
+
67
+ const fetchHandler = async (ctx) => {
155
68
  try {
156
- const res = await fetch(path);
69
+ const res = await fetch(ctx.path);
157
70
  if (res.ok) return res;
158
- } catch (e) {
159
- if (debug) console.error('[Router] Fetch error:', e);
160
- }
71
+ } catch (e) { if (debug) console.error('[Router] Fetch error:', e); }
161
72
  return null;
162
73
  };
163
74
 
164
75
  /**
165
- * Register a route chain
166
- * usage: router.use(pattern, replacement, handler, ...)
167
- *
168
- * If contentEl is set and the chain ends with a string (path) or has no handlers,
169
- * the router automatically appends a fetch handler.
76
+ * Adds a route or middleware to the router's pipeline.
170
77
  */
171
78
  const use = (...args) => {
172
- if (args.length === 0) return;
173
- const chain = [];
174
- const firstArg = args[0];
175
-
176
- if (typeof firstArg !== 'function') {
177
- chain.push(createMatcher(firstArg));
178
- } else {
179
- chain.push(firstArg);
180
- }
181
-
182
- let hasCustomHandler = false;
183
- for (let i = 1; i < args.length; i++) {
184
- const arg = args[i];
185
- if (typeof arg === 'string') {
186
- chain.push(createReplacer(arg));
187
- } else if (typeof arg === 'function') {
188
- chain.push(arg);
189
- hasCustomHandler = true;
190
- }
191
- }
192
-
193
- // If contentEl is set and no custom handler provided, append default fetch
194
- if (contentEl && !hasCustomHandler) {
195
- chain.push(defaultFetchHandler);
196
- }
197
-
79
+ const chain = args.map((arg, i) => (i === 0 && typeof arg !== 'function') ? createMatcher(arg) : (typeof arg === 'string' ? createReplacer(arg) : arg));
80
+ if (contentEl && !chain.some(f => f.name === 'fetchHandler' || args.some(a => typeof a === 'function'))) chain.push(fetchHandler);
198
81
  chains.push(chain);
199
82
  return routerInstance;
200
83
  };
201
84
 
202
85
  /**
203
- * Execute routing for a given path
86
+ * Processes a path through the registered chains.
204
87
  */
205
- const route = async (rawPath) => {
206
- let currentPath = normalizePath(rawPath);
207
- // Include contentEl in context for middleware to access/override
208
- let context = { path: currentPath, contentEl };
209
-
210
- if (debug) console.log(`[Router] Routing: ${currentPath}`);
88
+ const route = async (raw) => {
89
+ let ctx = { path: normalizePath(raw), contentEl };
90
+ if (debug) console.log(`[Router] Routing: ${ctx.path}`);
211
91
 
212
92
  for (const chain of chains) {
213
- let chainResult = context;
214
- let chainFailed = false;
215
-
93
+ let res = ctx, failed = false;
216
94
  for (const fn of chain) {
217
95
  try {
218
- const result = await fn(chainResult);
219
-
220
- if (result instanceof Response) return result;
221
- if (!result) {
222
- chainFailed = true;
223
- break;
224
- }
225
-
226
- chainResult = result;
227
- } catch (err) {
228
- console.error('[Router] Error in route chain:', err);
229
- chainFailed = true;
230
- break;
231
- }
232
- }
233
-
234
- if (!chainFailed) {
235
- // Fallthrough with updated context
236
- if (typeof chainResult === 'string') {
237
- context = { path: chainResult, contentEl };
238
- if (debug) console.log(`[Router] Path updated to: ${chainResult}`);
239
- } else if (chainResult && chainResult.path) {
240
- context = chainResult;
241
- // Ensure contentEl is preserved if not in result
242
- if (!context.contentEl) context.contentEl = contentEl;
243
- }
96
+ res = await fn(res);
97
+ if (res instanceof Response) return res;
98
+ if (!res) { failed = true; break; }
99
+ } catch (e) { console.error('[Router] Chain error:', e); failed = true; break; }
244
100
  }
101
+ if (!failed) ctx = typeof res === 'string' ? { ...ctx, path: res } : { ...ctx, ...res };
245
102
  }
246
-
247
- if (notFound) return notFound(context);
248
- return null;
103
+ return notFound ? notFound(ctx) : null;
249
104
  };
250
105
 
251
106
  const handleRequest = async (path) => {
252
107
  if (onStart) onStart(path);
253
-
254
- const response = await route(path);
255
-
256
- if (!response) {
257
- console.warn(`[Router] No route handled path: ${path}`);
258
- return null;
259
- }
260
-
261
- // Auto-render to contentEl if provided and response is OK
262
- if (response.ok && contentEl) {
263
- const html = await response.text();
264
- contentEl.innerHTML = html;
265
-
266
- // Re-execute scripts in the loaded content
267
- const scripts = contentEl.querySelectorAll('script');
268
- scripts.forEach(script => {
269
- const newScript = document.createElement('script');
270
- if (script.type) newScript.type = script.type;
271
- if (script.src) {
272
- newScript.src = script.src;
273
- } else {
274
- newScript.textContent = script.textContent;
275
- }
276
- script.parentNode.replaceChild(newScript, script);
108
+ const res = await route(path);
109
+ if (!res) return console.warn(`[Router] No route: ${path}`);
110
+
111
+ if (res.ok && contentEl) {
112
+ contentEl.innerHTML = await res.text();
113
+ contentEl.querySelectorAll('script').forEach(s => {
114
+ const n = document.createElement('script');
115
+ [...s.attributes].forEach(a => n.setAttribute(a.name, a.value));
116
+ n.textContent = s.textContent;
117
+ s.replaceWith(n);
277
118
  });
278
119
  }
279
-
280
- // Call onResponse AFTER auto-render for post-render logic (analytics, scroll, etc.)
281
- if (onResponse) {
282
- await onResponse(response, path);
283
- }
284
-
285
- return response;
120
+ if (onResponse) await onResponse(res, path);
121
+ return res;
286
122
  };
287
123
 
124
+ /**
125
+ * Navigates to a new path and updates the browser history.
126
+ */
288
127
  const navigate = (path) => {
289
- path = normalizePath(path);
290
- let fullPath = base + path;
291
- return handleRequest(fullPath).then((response) => {
292
- let dest = response?.url;
293
- if (dest && (dest.startsWith('http') || dest.startsWith('//'))) {
294
- try {
295
- const u = new URL(dest, window.location.origin);
296
- dest = u.pathname + u.search + u.hash;
297
- } catch (e) { }
298
- }
299
- // Fallback to intent if response has no URL
300
- if (!dest) dest = fullPath;
128
+ const p = normalizePath(path);
129
+ return handleRequest(base + p).then(r => {
130
+ let dest = r?.url ? new URL(r.url, window.location.origin).pathname : base + p;
301
131
  window.history.pushState({ path: dest }, '', dest);
302
- }).catch((err) => {
303
- console.error('[Router] Error handling request:', err);
304
- })
132
+ }).catch(e => console.error('[Router] Nav error:', e));
305
133
  };
306
134
 
135
+ /**
136
+ * Starts the router by handling the initial path and setting up event listeners.
137
+ */
307
138
  const start = async () => {
308
- const urlParams = new URLSearchParams(window.location.search);
309
- const loadPath = urlParams.get('load');
310
-
311
- window.addEventListener('popstate', (e) => {
312
- const path = e.state?.path || normalizePath(window.location.pathname);
313
- handleRequest(path);
314
- });
315
-
316
- document.addEventListener('click', (e) => {
317
- const link = e.target.closest('a[href]');
318
- if (!link) return;
319
- const href = link.getAttribute('href');
320
- if (
321
- !href || href.startsWith('http') || href.startsWith('//') ||
322
- href.startsWith('#') || href.startsWith('mailto:') ||
323
- link.target === '_blank'
324
- ) return;
325
-
139
+ const load = new URLSearchParams(window.location.search).get('load');
140
+ window.onpopstate = (e) => handleRequest(e.state?.path || normalizePath(window.location.pathname));
141
+ document.onclick = (e) => {
142
+ const a = e.target.closest('a[href]');
143
+ if (!a || a.target === '_blank' || /^(http|#|mailto|tel)/.test(a.getAttribute('href'))) return;
326
144
  e.preventDefault();
327
- const url = new URL(href, document.baseURI);
328
- const path = normalizePath(url.pathname);
329
- navigate(path);
330
- });
331
-
332
- if (loadPath) {
333
- window.history.replaceState({ path: loadPath }, '', loadPath);
334
- handleRequest(loadPath);
335
- } else {
336
- const initialPath = normalizePath(window.location.pathname);
337
- window.history.replaceState({ path: initialPath }, '', base + initialPath);
338
- handleRequest(initialPath);
339
- }
340
-
341
- return routerInstance;
342
- };
343
-
344
- const routerInstance = {
345
- use,
346
- navigate,
347
- start
145
+ navigate(normalizePath(new URL(a.href, document.baseURI).pathname));
146
+ };
147
+ const init = load || normalizePath(window.location.pathname);
148
+ window.history.replaceState({ path: init }, '', base + init);
149
+ return handleRequest(init).then(() => routerInstance);
348
150
  };
349
151
 
152
+ const routerInstance = { use, navigate, start };
350
153
  return routerInstance;
351
154
  };
352
155
 
353
- const LightviewRouter = {
354
- base,
355
- router
356
- };
357
-
358
- if (typeof module !== 'undefined' && module.exports) {
359
- module.exports = LightviewRouter;
360
- }
361
- if (typeof window !== 'undefined') {
362
- window.LightviewRouter = LightviewRouter;
363
- }
156
+ const LightviewRouter = { base, router };
157
+ if (typeof module !== 'undefined' && module.exports) module.exports = LightviewRouter;
158
+ else if (typeof window !== 'undefined') window.LightviewRouter = LightviewRouter;
364
159
  })();